Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/mobile/src/features/threads/useThreadListV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ export function useThreadListV2(input: {
threadListInboxReturns.getSnapshot,
);
const inbox = useMemo(
() => threadListInboxReturns.observe(workingShelfEnabled ? threads : null),
// oxlint-disable-next-line react/memo-dependencies -- the version signals an expired grace
() => threadListInboxReturns.observe(workingShelfEnabled ? threads : null, inboxVersion),
[threads, workingShelfEnabled, inboxVersion],
);
// Up/down menu availability for every card, computed once per section per
Expand Down
26 changes: 26 additions & 0 deletions apps/mobile/src/reactCompilerCoverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,32 @@ describe("React Compiler coverage", () => {
).toEqual([true]);
});

// The compiler infers memo deps from the callback, so an unread version dep is dropped and
// an expired working grace never re-observes the Working shelf.
it("re-observes the inbox tracker when its version changes", () => {
const result = compileLikeMetro("src/features/threads/useThreadListV2.ts", [], { ast: true });
const cacheKeys: string[][] = [];
if (typeof result === "object" && result !== null && "ast" in result) {
walkAst(result.ast, (node, ancestors) => {
if (node.type !== "CallExpression" || !isAstNode(node.callee)) return;
const property = node.callee.property;
if (!isAstNode(property) || property.name !== "observe") return;
const guard = ancestors.findLast(
(ancestor, index) =>
ancestor.type === "IfStatement" && ancestors[index + 1] === ancestor.consequent,
);
const names: string[] = [];
walkAst(guard?.test, (child) => {
if (child.type === "Identifier" && typeof child.name === "string" && child.name !== "$")
names.push(child.name);
});
cacheKeys.push(names);
});
}
expect(cacheKeys).toHaveLength(1);
expect(cacheKeys[0]).toContain("inboxVersion");
});

it("lists only files that exist", () => {
expect(Object.keys(KNOWN_BAILOUTS).filter((file) => !sourceFiles.includes(file))).toEqual([]);
});
Expand Down
8 changes: 6 additions & 2 deletions packages/client-runtime/src/state/threadInbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ export const WORKING_EXIT_GRACE_MS = 2_000;
/**
* Keep one at module scope so placement survives routes that unmount the list. Call
* `observe` when the shells or the snapshot version change, or with null while the beta
* is off. Its result stays fixed, so a list built from it can be cached.
* is off. Its result stays fixed, so a list built from it can be cached. A memoized caller
* passes the version it read: the React Compiler keys memos only on values the callback reads.
*/
export function createInboxReturnTracker() {
const keyOf = (thread: ThreadKeyInput) => `${thread.environmentId}:${thread.id}`;
Expand Down Expand Up @@ -166,7 +167,10 @@ export function createInboxReturnTracker() {
};
},
getSnapshot: () => version,
observe(threads: readonly TrackedInboxThread[] | null): {
observe(
threads: readonly TrackedInboxThread[] | null,
_version?: number,
): {
readonly isWorking: (thread: WorkingThreadInput & ThreadKeyInput) => boolean;
readonly returnedAt: (thread: ThreadKeyInput) => number | undefined;
} {
Expand Down
Loading