Skip to content

Infinite scroll (#2334) [bump version to v0.22.165]#2334

Merged
drew-harris merged 30 commits intomainfrom
drewh/infinite-scroll
Mar 19, 2026
Merged

Infinite scroll (#2334) [bump version to v0.22.165]#2334
drew-harris merged 30 commits intomainfrom
drewh/infinite-scroll

Conversation

@drew-harris
Copy link
Copy Markdown
Contributor

@drew-harris drew-harris commented Mar 2, 2026

Adds a subscribeInfiniteQuery function to core and a useInfiniteQuery hook to react package.

Sandbox available at http://localhost:4000/play/infinite-scroll

Example Usage:

  const {canLoadMore, loadMore, data, isLoading} = db.useInfiniteQuery('items', {
    $: {
      limit: 4,
      order: {
        value: 'asc',
      },
    },
  });

Limit represents how many items to load at once.

New type exports from Core:

Cursor,
Order,
InstaQLQueryEntityResult,

Notes: core/weakHash was changed to ignore fields with the value undefined as part of the hash.

Note: Deferred in core migrated to typescript file, + optional generic for what the promise resolves to (defaults to any). This isn't linked to anything in current infinite query implementation.

Note: Added a new option to the nextjs sandbox ephemeral page component to wait for the db to initialize before rendering the page. This simulates what it would be like to do a client side navigation to this page.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds bidirectional cursor-based infinite-query support across React and core: new useInfiniteQuerySubscription hook, useInfiniteQuery API, cursor/UUID arithmetic, end-cursor handling in instaql, stronger Deferred typings, weakHash change, TS target bumps, and an infinite-scroll demo page.

Changes

Cohort / File(s) Summary
Infinite Query Hook & Utilities
client/packages/react-common/src/useInfiniteQuerySubscription.ts, client/packages/react-common/src/uuidMath.ts
New bidirectional infinite-query hook with chunked forward/reverse paging, subscription lifecycle, loadMore/canLoadMore APIs, and UUID/cursor increment/decrement helpers. Review pagination logic, chunk state transitions, cursor arithmetic, and subscription cleanup.
React API Integration
client/packages/react-common/src/InstantReactAbstractDatabase.tsx, client/packages/react/src/index.ts
Adds useInfiniteQuery method to InstantReactAbstractDatabase and re-exports new query types from the React package. Verify typings and public API surface.
Core Query Flow
client/packages/core/src/instaql.ts, client/packages/core/src/Reactor.js
Introduces compareToCursor-based boundary checks and endCursor pruning in data loading; changed Deferred import path in Reactor. Inspect end-cursor gating with streaming subscriptions.
Public Types & Core Exports
client/packages/core/src/queryTypes.ts, client/packages/core/src/index.ts
Exports new public types: ValidQueryObject, ValidInfiniteQueryObject, Cursor, Order, InstaQLQueryEntityResult (and updates core index exports). Validate API stability and re-exports.
Deferred Typing
client/packages/core/src/utils/Deferred.ts
Makes Deferred generic Deferred<T = any> with explicit promise, _resolve, and _reject typings. Check callers for type compatibility.
Hashing Behavior
client/packages/core/src/utils/weakHash.ts
Skips keys with undefined values when computing object hashes, changing determinism for objects with undefined-valued properties.
Tooling / TS Config
client/packages/react-common/tsconfig.json, client/packages/react-common/tsconfig.dev.json
TypeScript compilation target bumped to ES2023. Verify build/tooling compatibility.
Demo & Sandbox
client/sandbox/react-nextjs/pages/play/infinite-scroll.tsx, client/sandbox/react-nextjs/components/EphemeralAppPage.tsx
Adds infinite-scroll demo page wired to useInfiniteQuery and sets init(..., verbose:false). Review example wiring and mutation queueing.
Minor Cleanup
client/sandbox/react-nextjs/pages/play/paginated-messages.tsx, server/src/instant/admin/routes.clj
Removes a debug log and normalizes comment formatting. Minimal review needed.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant React as "React Component"
    participant Hook as "useInfiniteQuerySubscription"
    participant Core as "InstantDB Core"
    participant DB as "Database"

    User->>React: mount / call loadMore
    React->>Hook: useInfiniteQuery(entity, query)
    Hook->>Core: subscribeQuery(initial query with limit/order)
    Core->>DB: execute query (limit, order, after/before)
    DB-->>Core: data + pageInfo (endCursor, hasNextPage)
    Core-->>Hook: onData(chunk, pageInfo)
    Hook->>Hook: update forward & reverse chunks
    Hook-->>React: return InfiniteQueryResult (data, chunks)
    User->>React: click loadMore
    React->>Hook: loadMore()
    Hook->>Hook: freeze forward, compute next cursor
    Hook->>Core: subscribeQuery(next page after endCursor)
    DB-->>Core: next data + pageInfo
    Core-->>Hook: onData(new chunk)
    Hook-->>React: update data with appended chunk
Loading
sequenceDiagram
    participant Item as "Item"
    participant Compare as "compareToCursor"
    participant Trip as "compareOrderTriples"

    Item->>Compare: compare item vs cursor
    Compare->>Trip: compareOrderTriples(order, itemAttr, cursorAttr)
    Trip-->>Compare: -1 | 0 | 1
    Compare->>Item: numeric result used for boundary checks (isBefore/isAfter)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • solidjs sdk #2258: Overlaps Reactor import/path and core public API edits touching the same Reactor symbol and export surface.

Suggested reviewers

  • stopachka
  • nezaj
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Infinite scroll' is directly related to the primary change, which adds a useInfiniteQuery hook enabling infinite scrolling functionality.
Description check ✅ Passed The PR description accurately describes the main feature (useInfiniteQuery hook), provides example usage, documents API changes (new type exports), and explains auxiliary modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch drewh/infinite-scroll
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 2, 2026

View Vercel preview at instant-www-js-drewh-infinite-scroll-jsv.vercel.app.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
client/packages/core/src/utils/Deferred.ts (1)

1-4: Deferred<T> still leaks any through method signatures.

Line 13 and Line 17 are untyped, so the generic T is not enforced at call sites.

♻️ Proposed typing cleanup
 export class Deferred<T = any> {
   promise: Promise<T>;
   _resolve: (value: T) => void;
-  _reject: (...reason: any) => void;
+  _reject: (reason?: any) => void;
@@
-  resolve(value) {
+  resolve(value: T) {
     this._resolve(value);
   }
 
-  reject(reason) {
+  reject(reason?: any) {
     this._reject(reason);
   }
 }

Also applies to: 13-18

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/core/src/utils/Deferred.ts` around lines 1 - 4, The
Deferred<T> class currently leaks any through its resolver signatures; update
the field types so they enforce the generic T: change _resolve to (value: T |
PromiseLike<T>) => void and change _reject to (reason?: any) => void (or use
unknown for stricter typing) while keeping promise: Promise<T>; ensure the
constructor initializes promise and assigns these typed functions to _resolve
and _reject so callers get proper T typing when using Deferred<T>.
client/packages/react-common/src/uuidMath.ts (1)

1-1: Use a type-only import for Cursor.

Cursor is used only in type annotations (lines 3 and 7), never as a runtime value. Using import type follows best practices and prevents potential issues if stricter TypeScript settings are enabled.

-import { Cursor } from '@instantdb/core';
+import type { Cursor } from '@instantdb/core';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/uuidMath.ts` at line 1, Replace the runtime
import of Cursor with a type-only import since Cursor is only used in type
annotations in uuidMath.ts; change the import statement to use "import type {
Cursor } from '@instantdb/core'" and ensure no runtime references to Cursor
remain in the functions or signatures (e.g., any function parameter or return
type annotations that mention Cursor) so TypeScript treats it purely as a type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/packages/core/src/instaql.ts`:
- Around line 646-653: The isBefore/isAfter functions rely on exact equality to
1 or -1 from compareToCursor but must use sign checks because comparators can
return any positive/negative integer; update isBefore to compute const cmp =
compareToCursor(startCursor, orderAttr, idVec) and return direction === 'desc' ?
cmp > 0 : cmp < 0, and update isAfter to compute const cmp =
compareToCursor(endCursor, orderAttr, idVec) and return direction === 'desc' ?
cmp < 0 : cmp > 0 (i.e., replace === 1 / === -1 checks with >0 / <0 sign
comparisons in isBefore/isAfter).

In `@client/packages/core/src/queryTypes.ts`:
- Around line 498-516: ValidInfiniteQueryObject lacks the same keyof T guard
used by ValidQueryObject, so callers can pass extra arbitrary keys; update
ValidInfiniteQueryObject to enforce "keyof T extends keyof
Schema['entities'][EntityName]['links'] | '$'" (or use a conditional type that
yields never when the constraint fails) so only link keys and '$' are allowed,
while preserving the existing $ shape (where, pageSize, fields, order) and the
per-link nested ValidQueryObject recursion.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts`:
- Around line 104-110: The subscription callbacks (e.g., the one passing
frozenData into setReverseChunk) are dereferencing frozenData.data and
frozenData.pageInfo with non-null assertions and will crash on error emissions;
update each callback (the one using frozenData at the shown location and the
similar callbacks around lines 131-136, 158-163, 193-198, 218-225) to first
check frozenData && frozenData.data and frozenData.pageInfo before accessing
properties, and gracefully handle missing fields (for example set status to
'error' or return early) so setReverseChunk and other setters never receive
undefined.data or undefined.pageInfo. Ensure you reference the same identifiers
(frozenData, data, pageInfo, setReverseChunk) when implementing the guards so
the fixes apply to all affected callbacks.
- Around line 290-292: The returned loading flags are hardcoded false; update
the return in useInfiniteQuerySubscription to compute isLoading and
isLoadingMore from the hook's actual state (e.g., use the internal flags like
loading/initialLoading or isFetching/isFetchingNextPage from the query result
used by useInfiniteQuerySubscription) and keep loadMore as-is; specifically, set
isLoading to the initial fetch indicator (e.g., loading || isFetching when no
pages loaded) and set isLoadingMore to the next-page fetch indicator (e.g.,
isFetchingNextPage or a local isLoadingMore state used around the loadMore
function) so consumers can distinguish bootstrap vs. pagination loading.
- Around line 204-231: The effect currently using core.subscribeQuery should
list its true dependencies (core, entity, query, pushNewForward, pushNewReverse)
instead of an empty array so a new subscription is created whenever any of those
change; on cleanup it must unsubscribe the previous subscription and reset
hasKickstarted.current so state doesn't become stale (refer to the useEffect
block, core.subscribeQuery, hasKickstarted, pushNewForward, pushNewReverse).
Also stop returning hardcoded isLoading/isLoadingMore: compute and return proper
loading flags (update the hook's returned InfiniteQueryResult) based on
subscription state/response lifecycle so the public API matches the type
(replace the hardcoded false values for isLoading and isLoadingMore).

In `@client/packages/react-common/src/uuidMath.ts`:
- Around line 11-48: The decrementUuid and incrementUuid functions currently
perform hex arithmetic on possibly-invalid input, producing NaNs and corrupted
output; add a validation step that strips hyphens, verifies the resulting string
is exactly 32 characters and matches /^[0-9a-fA-F]{32}$/, and throw a clear
error (or return/propagate) if validation fails before doing parseInt and
arithmetic; implement this as a small helper (e.g., validateUuidHex or
normalizeUuidHex) and use it at the start of both decrementUuid and
incrementUuid to ensure only valid 32-char hex UUIDs are processed.

---

Nitpick comments:
In `@client/packages/core/src/utils/Deferred.ts`:
- Around line 1-4: The Deferred<T> class currently leaks any through its
resolver signatures; update the field types so they enforce the generic T:
change _resolve to (value: T | PromiseLike<T>) => void and change _reject to
(reason?: any) => void (or use unknown for stricter typing) while keeping
promise: Promise<T>; ensure the constructor initializes promise and assigns
these typed functions to _resolve and _reject so callers get proper T typing
when using Deferred<T>.

In `@client/packages/react-common/src/uuidMath.ts`:
- Line 1: Replace the runtime import of Cursor with a type-only import since
Cursor is only used in type annotations in uuidMath.ts; change the import
statement to use "import type { Cursor } from '@instantdb/core'" and ensure no
runtime references to Cursor remain in the functions or signatures (e.g., any
function parameter or return type annotations that mention Cursor) so TypeScript
treats it purely as a type.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4604f4 and 86a45b9.

📒 Files selected for processing (16)
  • client/packages/core/src/Reactor.js
  • client/packages/core/src/index.ts
  • client/packages/core/src/instaql.ts
  • client/packages/core/src/queryTypes.ts
  • client/packages/core/src/utils/Deferred.ts
  • client/packages/core/src/utils/weakHash.ts
  • client/packages/react-common/src/InstantReactAbstractDatabase.tsx
  • client/packages/react-common/src/useInfiniteQuerySubscription.ts
  • client/packages/react-common/src/uuidMath.ts
  • client/packages/react-common/tsconfig.dev.json
  • client/packages/react-common/tsconfig.json
  • client/packages/react/src/index.ts
  • client/sandbox/react-nextjs/components/EphemeralAppPage.tsx
  • client/sandbox/react-nextjs/pages/play/infinite-scroll.tsx
  • client/sandbox/react-nextjs/pages/play/paginated-messages.tsx
  • server/src/instant/admin/routes.clj
💤 Files with no reviewable changes (1)
  • client/sandbox/react-nextjs/pages/play/paginated-messages.tsx

Comment thread client/packages/core/src/instaql.ts Outdated
Comment thread client/packages/core/src/queryTypes.ts Outdated
Comment thread client/packages/react-common/src/useInfiniteQuerySubscription.ts Outdated
Comment thread client/packages/react-common/src/useInfiniteQuerySubscription.ts Outdated
Comment thread client/packages/react-common/src/useInfiniteQuerySubscription.ts Outdated
Comment thread client/packages/react-common/src/uuidMath.ts Outdated
@drew-harris drew-harris marked this pull request as draft March 3, 2026 00:36
@drew-harris drew-harris marked this pull request as ready for review March 3, 2026 17:10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
client/packages/react-common/src/useInfiniteQuerySubscription.ts (2)

224-251: ⚠️ Potential issue | 🟠 Major

Bootstrap effect has empty dependency array but captures changing values.

The effect uses core, entity, query, and internally defined functions (pushNewForward, pushNewReverse), but the dependency array is empty. If these props change, the hook will continue using stale subscriptions.

Consider adding proper dependencies and resetting state when they change:

   useEffect(() => {
+    // Reset state on prop changes
+    for (const unsub of subs.current.values()) {
+      unsub?.();
+    }
+    subs.current.clear();
+    setForwardChunks(new Map());
+    setBackwardChunks(new Map());
+    hasKickstarted.current = false;
+
     const sub = core.subscribeQuery(
       // ...
     );
 
     return sub;
-  }, []);
+  }, [core, entity, JSON.stringify(query)]);

Note: The inner functions (pushNewForward, etc.) would need to be either memoized with useCallback or the effect would need restructuring to avoid stale closures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts` around
lines 224 - 251, The effect that calls core.subscribeQuery (inside the useEffect
block) captures changing values (core, entity, query, pushNewForward,
pushNewReverse, hasKickstarted) but uses an empty dependency array, causing
stale closures; update the dependency array to include core, entity, query (or
the specific query.$ props used), and the handler functions, and either memoize
pushNewForward and pushNewReverse with useCallback or move their logic inside
the effect so closures stay fresh; also ensure you properly reset hasKickstarted
and any related state when those dependencies change and return a cleanup that
unsubscribes the previous subscription from core.subscribeQuery to avoid leaks.

310-311: ⚠️ Potential issue | 🟠 Major

Loading flags are hardcoded to false, breaking the API contract.

Consumers cannot determine loading state with these hardcoded values. Compute them from actual state:

+  const isLoading = forwardChunks.size === 0 && reverseChunks.size === 0;
+  const isLoadingMore = Array.from(forwardChunks.values()).some(
+    (chunk) => chunk.status === 'bootstrapping',
+  );
+
   return {
     // ...
-    isLoading: false,
-    isLoadingMore: false,
+    isLoading,
+    isLoadingMore,
     loadMore: loadMore,
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts` around
lines 310 - 311, The returned flags isLoading and isLoadingMore are hardcoded to
false in useInfiniteQuerySubscription; change them to reflect the real hook
state by computing isLoading from the query status/isFetching (e.g., status ===
'loading' || isFetching) and isLoadingMore from the fetch-next-page state (e.g.,
isFetchingNextPage || fetchStatus === 'fetchMore' or similar variables used in
this hook). Update the return object in useInfiniteQuerySubscription so
consumers receive dynamic booleans derived from the existing state variables
(status, isFetching, isFetchingNextPage/fetchStatus) instead of constant false.
🧹 Nitpick comments (7)
client/packages/react-common/src/useInfiniteQuerySubscription.ts (7)

69-79: Parameter naming: use camelCase for variables.

The parameter Chunk uses PascalCase, which is conventionally reserved for types/classes. Use chunk instead.

-  const setChunk = (startCursor: Cursor, Chunk: Chunk) => {
+  const setChunk = (startCursor: Cursor, chunk: Chunk) => {
     setForwardChunks((prev) =>
-      new Map(prev).set(JSON.stringify(startCursor), Chunk),
+      new Map(prev).set(JSON.stringify(startCursor), chunk),
     );
   };
 
-  const setReverseChunk = (startCursor: Cursor, Chunk: Chunk) => {
+  const setReverseChunk = (startCursor: Cursor, chunk: Chunk) => {
     setBackwardChunks((prev) =>
-      new Map(prev).set(JSON.stringify(startCursor), Chunk),
+      new Map(prev).set(JSON.stringify(startCursor), chunk),
     );
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts` around
lines 69 - 79, Change the parameter name from PascalCase to camelCase in both
setter functions: rename the parameter Chunk to chunk in setChunk and
setReverseChunk and update their bodies to use the new variable (e.g.,
set(JSON.stringify(startCursor), chunk)). This keeps naming consistent with
conventions and avoids confusion with type/class names; update any local
references inside those functions accordingly.

62-64: Inconsistent naming between state and setter.

The state variable is reverseChunks but the setter is setBackwardChunks. This inconsistency could cause confusion during maintenance.

-  const [reverseChunks, setBackwardChunks] = useState<Map<string, Chunk>>(
+  const [reverseChunks, setReverseChunks] = useState<Map<string, Chunk>>(

Then update all usages of setBackwardChunks to setReverseChunks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts` around
lines 62 - 64, The state setter name is inconsistent: the state variable is
reverseChunks but the setter is setBackwardChunks; rename the setter to
setReverseChunks and update every usage accordingly (replace setBackwardChunks
-> setReverseChunks) so the pair is reverseChunks / setReverseChunks (check the
useState call that creates reverseChunks and all places where setBackwardChunks
is referenced, e.g., any callbacks or effects) to maintain consistent naming.

11-11: Remove .ts extension from import path.

Including the .ts extension in the import may cause issues with certain bundlers and module resolution strategies. TypeScript's module resolution typically expects paths without extensions.

-import { decrementCursor, incrementCursor } from './uuidMath.ts';
+import { decrementCursor, incrementCursor } from './uuidMath';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts` at line 11,
Update the import in useInfiniteQuerySubscription.ts to remove the .ts
extension: change the import that currently references './uuidMath.ts' to
'./uuidMath' so decrementCursor and incrementCursor are imported without a file
extension, ensuring compatibility with TypeScript/module bundlers and standard
resolution rules.

90-103: Heavy use of as any bypasses type safety.

Multiple subscription calls cast the query object to any. This hides potential type mismatches that could cause runtime errors. Consider creating properly typed query builder helpers or refining the generic constraints to avoid these casts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts` around
lines 90 - 103, The subscription call in useInfiniteQuerySubscription uses an
unsafe cast ("as any") when calling core.subscribeQuery with the constructed
query (see newSub, core.subscribeQuery, entity, query); remove the cast and
instead create a properly typed query shape or helper that enforces the expected
GraphQL/query types (e.g., a buildPagedQuery(entity, query, { after, before,
where, fields, order }) function) or tighten the generic constraints on
useInfiniteQuerySubscription/core.subscribeQuery so the object matches the
required type; update all subscription call sites to use that typed helper or
adjusted generics rather than casting to any, ensuring type-safe properties like
$.where, $.fields and $.order are preserved.

287-296: Unnecessary async keyword.

The function is declared async but doesn't await anything. This is misleading and the returned Promise<void> resolves immediately regardless of whether the subscription callbacks have completed.

If the intent is to wait for the next page to load, consider returning a Promise that resolves when the subscription emits data. Otherwise, remove async:

-  const loadMore = async () => {
+  const loadMore = () => {
     if (!canLoadMore) return;
     // ...
   };

And update the type:

-  loadMore: () => Promise<void>;
+  loadMore: () => void;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts` around
lines 287 - 296, The loadMore function is declared async but doesn't await
anything, which is misleading; either remove the async keyword and change its
signature to return void (update any callers/types expecting Promise<void>), or
if you intended to wait for the next subscription emission, implement and return
a Promise that resolves when the subscription emits new data (e.g., listen for
the subscription event after calling freezeForward and pushNewForward and
resolve when the next page arrives). Locate loadMore in
useInfiniteQuerySubscription.ts and adjust its signature and callers
accordingly; keep references to forwardChunks, canLoadMore, freezeForward, and
pushNewForward intact while performing the change.

316-330: reverseOrder only handles single-key ordering.

If order contains multiple keys (compound ordering), only the first key is reversed and the rest are dropped. This could produce unexpected results.

If compound ordering isn't supported, consider adding validation or documentation. Otherwise, reverse all keys:

 const reverseOrder = (order?: Order<any, any>): Order<any, any> => {
   if (!order) {
     return {
       serverCreatedAt: 'asc',
     };
   }
-  const key = Object.keys(order).at(0);
-  if (!key)
-    return {
-      serverCreatedAt: 'asc',
-    };
-  return {
-    [key]: order[key] === 'asc' ? 'desc' : 'asc',
-  };
+  const keys = Object.keys(order);
+  if (keys.length === 0) {
+    return { serverCreatedAt: 'asc' };
+  }
+  return Object.fromEntries(
+    keys.map((key) => [key, order[key] === 'asc' ? 'desc' : 'asc']),
+  );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts` around
lines 316 - 330, The reverseOrder function currently only flips the first key of
an Order<any, any> and drops any additional keys; update reverseOrder to handle
compound ordering by iterating over all keys in the provided order and producing
a new object where each key's direction is flipped ('asc' <-> 'desc'), while
preserving key order; if order is undefined or empty, return the default {
serverCreatedAt: 'asc' } as before. If compound ordering is intentionally
unsupported, instead add explicit validation in reverseOrder that throws or logs
a clear error when Object.keys(order).length > 1 and document this behavior.

89-89: Address TODO comment for unsubscription.

There's a TODO comment indicating the unsubscription logic may need attention. Please clarify if additional cleanup is needed here or remove the comment if the current implementation is sufficient.

Would you like me to help analyze the subscription lifecycle to ensure proper cleanup?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts` at line 89,
The TODO "unsub" indicates missing cleanup for the subscription created in
useInfiniteQuerySubscription; implement proper unsubscription by returning a
cleanup function from the effect that created the subscription (or calling
subscription.unsubscribe()/observer.unsubscribe() or abortController.abort() for
the specific subscription variable used in useInfiniteQuerySubscription) to stop
updates on unmount or when dependencies change, or remove the TODO if no cleanup
is required because the subscription is already handled elsewhere; reference the
subscription variable and the useEffect inside useInfiniteQuerySubscription to
locate where to add the return () => { /* unsubscribe */ } cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts`:
- Around line 242-244: The empty catch around calling decrementCursor hides
errors; replace it with explicit error handling—wrap the
pushNewForward(decrementCursor(pageInfo.startCursor) as any) call in try/catch
and in the catch log the error and context (e.g., console.error or the module's
logger) including pageInfo.startCursor and a clear message, or call an existing
error handler/prop if available, rather than swallowing the exception silently;
keep pushNewForward and decrementCursor usage unchanged but ensure failures are
surfaced for debugging.
- Around line 277-285: The effect uses reverseChunks, freezeBackward, and
pushNewReverse but only lists canLoadPrevious in its dependency array, risking
stale closures; also the non-null assertion on chunksInOrder.at(-1)! can throw
if the map becomes empty. Fix by adding reverseChunks, freezeBackward, and
pushNewReverse to the effect dependencies (and memoize freezeBackward and
pushNewReverse with useCallback where they are defined to prevent render loops),
and replace the unconditional destructuring of .at(-1)! with a safe guard that
checks chunksInOrder.length > 0 before accessing the last entry and ensuring
lastChunk.endCursor exists before calling freezeBackward and pushNewReverse.

---

Duplicate comments:
In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts`:
- Around line 224-251: The effect that calls core.subscribeQuery (inside the
useEffect block) captures changing values (core, entity, query, pushNewForward,
pushNewReverse, hasKickstarted) but uses an empty dependency array, causing
stale closures; update the dependency array to include core, entity, query (or
the specific query.$ props used), and the handler functions, and either memoize
pushNewForward and pushNewReverse with useCallback or move their logic inside
the effect so closures stay fresh; also ensure you properly reset hasKickstarted
and any related state when those dependencies change and return a cleanup that
unsubscribes the previous subscription from core.subscribeQuery to avoid leaks.
- Around line 310-311: The returned flags isLoading and isLoadingMore are
hardcoded to false in useInfiniteQuerySubscription; change them to reflect the
real hook state by computing isLoading from the query status/isFetching (e.g.,
status === 'loading' || isFetching) and isLoadingMore from the fetch-next-page
state (e.g., isFetchingNextPage || fetchStatus === 'fetchMore' or similar
variables used in this hook). Update the return object in
useInfiniteQuerySubscription so consumers receive dynamic booleans derived from
the existing state variables (status, isFetching,
isFetchingNextPage/fetchStatus) instead of constant false.

---

Nitpick comments:
In `@client/packages/react-common/src/useInfiniteQuerySubscription.ts`:
- Around line 69-79: Change the parameter name from PascalCase to camelCase in
both setter functions: rename the parameter Chunk to chunk in setChunk and
setReverseChunk and update their bodies to use the new variable (e.g.,
set(JSON.stringify(startCursor), chunk)). This keeps naming consistent with
conventions and avoids confusion with type/class names; update any local
references inside those functions accordingly.
- Around line 62-64: The state setter name is inconsistent: the state variable
is reverseChunks but the setter is setBackwardChunks; rename the setter to
setReverseChunks and update every usage accordingly (replace setBackwardChunks
-> setReverseChunks) so the pair is reverseChunks / setReverseChunks (check the
useState call that creates reverseChunks and all places where setBackwardChunks
is referenced, e.g., any callbacks or effects) to maintain consistent naming.
- Line 11: Update the import in useInfiniteQuerySubscription.ts to remove the
.ts extension: change the import that currently references './uuidMath.ts' to
'./uuidMath' so decrementCursor and incrementCursor are imported without a file
extension, ensuring compatibility with TypeScript/module bundlers and standard
resolution rules.
- Around line 90-103: The subscription call in useInfiniteQuerySubscription uses
an unsafe cast ("as any") when calling core.subscribeQuery with the constructed
query (see newSub, core.subscribeQuery, entity, query); remove the cast and
instead create a properly typed query shape or helper that enforces the expected
GraphQL/query types (e.g., a buildPagedQuery(entity, query, { after, before,
where, fields, order }) function) or tighten the generic constraints on
useInfiniteQuerySubscription/core.subscribeQuery so the object matches the
required type; update all subscription call sites to use that typed helper or
adjusted generics rather than casting to any, ensuring type-safe properties like
$.where, $.fields and $.order are preserved.
- Around line 287-296: The loadMore function is declared async but doesn't await
anything, which is misleading; either remove the async keyword and change its
signature to return void (update any callers/types expecting Promise<void>), or
if you intended to wait for the next subscription emission, implement and return
a Promise that resolves when the subscription emits new data (e.g., listen for
the subscription event after calling freezeForward and pushNewForward and
resolve when the next page arrives). Locate loadMore in
useInfiniteQuerySubscription.ts and adjust its signature and callers
accordingly; keep references to forwardChunks, canLoadMore, freezeForward, and
pushNewForward intact while performing the change.
- Around line 316-330: The reverseOrder function currently only flips the first
key of an Order<any, any> and drops any additional keys; update reverseOrder to
handle compound ordering by iterating over all keys in the provided order and
producing a new object where each key's direction is flipped ('asc' <-> 'desc'),
while preserving key order; if order is undefined or empty, return the default {
serverCreatedAt: 'asc' } as before. If compound ordering is intentionally
unsupported, instead add explicit validation in reverseOrder that throws or logs
a clear error when Object.keys(order).length > 1 and document this behavior.
- Line 89: The TODO "unsub" indicates missing cleanup for the subscription
created in useInfiniteQuerySubscription; implement proper unsubscription by
returning a cleanup function from the effect that created the subscription (or
calling subscription.unsubscribe()/observer.unsubscribe() or
abortController.abort() for the specific subscription variable used in
useInfiniteQuerySubscription) to stop updates on unmount or when dependencies
change, or remove the TODO if no cleanup is required because the subscription is
already handled elsewhere; reference the subscription variable and the useEffect
inside useInfiniteQuerySubscription to locate where to add the return () => { /*
unsubscribe */ } cleanup.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86a45b9 and 65939ba.

📒 Files selected for processing (2)
  • client/packages/core/src/instaql.ts
  • client/packages/react-common/src/useInfiniteQuerySubscription.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/packages/core/src/instaql.ts

Comment thread client/packages/react-common/src/useInfiniteQuerySubscription.ts Outdated
Comment thread client/packages/react-common/src/useInfiniteQuerySubscription.ts Outdated
Comment thread client/packages/react-common/src/InstantReactAbstractDatabase.tsx Outdated
Comment thread client/packages/react-common/src/useInfiniteQuerySubscription.ts Outdated
Comment thread client/packages/react-common/src/useInfiniteQuerySubscription.ts Outdated
Comment thread client/packages/react-common/src/useInfiniteQuerySubscription.ts Outdated
Comment thread client/packages/react-common/src/useInfiniteQuerySubscription.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts
Comment thread client/packages/core/src/infiniteQuery.ts
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Comment thread client/packages/core/src/infiniteQuery.ts Outdated
Copy link
Copy Markdown
Contributor

@stopachka stopachka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's goo!!

@nezaj nezaj mentioned this pull request Apr 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants