itx SSR/loader prefetch: getServerItx, getLoaderItx, best-effort prefetchItxQuery#1457
Merged
Conversation
…etchItxQuery Converted routes fetched only over the browser itx socket, so every visit painted a spinner first. This restores loader prefetch for itx-backed queries (itx DECISIONS D18): - src/itx/access.ts: accessForPrincipal + resolveAccessibleContextId + requireWorkerExports extracted from fetch.ts so the WebSocket door and the new SSR door share one auth boundary. - src/itx/server.ts: getServerItx — in-process project-narrowed handle built from the request context (no socket, no capnweb), mirroring the /api/itx connect chain. - src/itx/loader.ts: getLoaderItx (isomorphic, orpc/client.ts shape; server branch dynamically imports server.ts, browser branch reuses the per-tab socket singleton now living in react/browser-client.ts) and prefetchItxQuery, which seeds the QueryClient and swallows all errors — prefetch must never crash a route into the error boundary. - lib/itx-queries.ts: one ItxQueryDefinition per piece of data, consumed by both the hooks and loader prefetches so key/fn/staleTime cannot drift. - streams index route: loader seeds the root stream state (the query that gates first paint); its tree-browser source now keys on itxKey so tree nodes, breadcrumbs, and the prefetch share cache entries per path. - tests: loader.test.ts (seeding + swallow contract) and a new itx-server-handle worker harness proving getServerItx against a real Stream DO via the capability loopback, including denied principals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Worker-harness entry/config files are reachable only through their pnpm script, so knip needs them declared like the sibling families. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An unauthenticated SSR render threw "not found. (Unauthenticated request.)" — wording that distinguishes auth state from denial and so breaks the kernel's no-probing posture (bugbot on #1457). Both denial paths now throw byte-identical ItxError NOT_FOUND, which also gives the SSR door the same coded errors as the socket door. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
capnweb's receiver rebuilds a plain Error — custom names and class identity never survive the wire, which is exactly why D18 makes detection duck-typed. The e2e contradicted its own doc comment by asserting error.name === "ItxError"; it now asserts getItxErrorCode + details, the actual contract. (This assertion also failed inside PR #1456's Preview e2e job, which nevertheless reported success — the preview runner swallowing a suite failure is a separate problem, recorded in the PR thread.) Also gives the two-event /api/itx/run record test 90s: the cold first run of isolate + stream DO on a fresh preview blew the 45s default and passed on the in-job retry at 12s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3928b28. Configure here.
The preview e2e caught a real regression shipped in #1456: capnweb's receiver rebuilds `new Error(message)` and never assigns the custom name (verified in the 0.8.0 deserializer and against a live preview), so the `name === "ItxError"` gate rejected every wire-crossed kernel error — getItxErrorCode returned undefined, access errors were retried again, and the access-denied UI never fired. The unit suite missed it because its simulated crossing unfaithfully copied the name. Detection is now code-only (the five-code set; foreign code strings like ENOENT are outside it), the simulation matches the real receiver (name comes back "Error" and a test pins that), and the doc comments record why the name must not participate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…se; keep code-only getItxErrorCode
Main reverted the descendantPaths catalog (CORE_STATE_VERSION 3) and deleted ItxStreams.list() — the explorer walks childPaths level by level instead. The loader-prefetch work keeps its shape: the streams index still seeds the root stream state through the shared projectStreamStateQuery definition (now parsing StreamNavigationState per main), and the server-handle harness proves access via the root's childPaths rather than the deleted list(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein
added a commit
that referenced
this pull request
Jun 10, 2026
Streams index route keeps main's projectStreamStateQuery/prefetch structure; the query factory's type moves from the deleted StreamNavigationState to StreamState (same shape now). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein
added a commit
that referenced
this pull request
Jun 10, 2026
Follow-up to #1457, addressing bugbot's "failed prefetch poisons query cache" finding: a swallowed prefetch failure left an errored, data-less entry on the shared query key, so the consuming component (streams tree, breadcrumb navigators) flashed its error state on mount before `retryOnMount` refetched. The catch now removes the entry when it is errored **and** empty — components mount pending and fetch fresh. Entries that already hold data are kept (stale data + background error beats no data), covered by a new revalidation test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Narrow loader/cache hygiene change with matching unit tests; no auth or API surface changes. > > **Overview** > Fixes a **prefetch cache poisoning** issue: when `prefetchItxQuery` swallowed a failure on a **first** fetch, React Query still left an **errored, data-less** entry on the shared key, so streams tree / breadcrumb UI briefly showed error on mount before `retryOnMount` refetched. > > After the catch, the loader now **`removeQueries`** when state is `error` and `data` is undefined, so consumers mount **pending** and fetch fresh. **Stale entries with existing data are untouched** when revalidation fails—stale data is preferred over wiping the cache. > > Tests now assert no cache residue on failed prefetch and add coverage for failed revalidation with seeded data. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3219b15. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- CLOUDFLARE_PREVIEW --> ## Environment Config Lease <!-- CLOUDFLARE_PREVIEW_STATE --> <!-- { "apps": { "os": { "appDisplayName": "OS", "appSlug": "os", "status": "deployed", "updatedAt": "2026-06-10T18:06:42.920Z", "headSha": "3219b1530a952cd1e70eaefb2d670358443f17cd", "message": null, "publicUrl": "https://os.iterate-preview-4.com", "runUrl": "https://github.com/iterate/iterate/actions/runs/27295793774", "shortSha": "3219b15" } }, "environmentConfigLease": { "dopplerConfig": "preview_4", "leasedUntil": 1781118146120, "leaseId": "8496eb91-1658-4e35-bde7-707083605ac3", "slug": "preview-4", "type": "environment-config-lease" } } --> <!-- /CLOUDFLARE_PREVIEW_STATE --> Lease: `preview-4` Doppler config: `preview_4` Type: `environment-config-lease` Leased until: 2026-06-10T19:02:26.120Z ### OS Status: deployed Commit: `3219b15` Preview: https://os.iterate-preview-4.com [Workflow run](https://github.com/iterate/iterate/actions/runs/27295793774) Updated: 2026-06-10T18:06:42.920Z <!-- /CLOUDFLARE_PREVIEW --> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein
added a commit
that referenced
this pull request
Jun 10, 2026
#1479) ## Streams review — Stages 4–7 (browser robustness, perf, dead code, docs) The final batch of the `packages/streams` adversarial review (`tasks/streams-review-fixes.md`), bundled into one PR per request. Stages 0/1/3 already shipped (#1455, #1458, #1459); **Stage 2 (lazy init) was abandoned** — #1460's subscriber-presence model appends a `subscriber-connected` fact on every `subscribe()`, which fundamentally conflicts with "don't initialize storage until the first `append()`". `created`/`woken` stay eager. All findings were re-verified against current `main` after #1460/#1457 moved the codebase; items those PRs already fixed or made obsolete are called out below. ### Stage 4 — browser-runtime correctness - **C1-browser** — the server delivery pump is fire-and-forget for inbound subscribers and never reports a failed `ingest`, so a browser mirror that fails to apply a batch silently desyncs forever. The browser now self-heals: `ingestWithSelfHeal` resubscribes from the persisted checkpoint with bounded exponential backoff on ingest failure. - **B1** — connection-epoch guard so a stale connection's late status callback can't clobber the live connection. - **B2** — `appendBatch`/`runtimeState` await connection readiness (`whenStreamReady`) instead of throwing "disposed" during a transient reconnect; only throw when actually disposed. - **B3** — the `ROLLBACK` in `stream-db.worker.ts` `batch()` is now in its own try/catch so it can't mask the original error (which `withBusyRetry`/`isBusyError` need to see). - **B4** — reconcile guards on stream incarnation (server `createdAt` + a `mirror_meta` table): rebuild the mirror on incarnation change rather than trusting the offset comparison after a `reset()`. - **B5** — `AbortSignal` on the Web Lock request (released on disposal) + the request rejection is surfaced instead of `void`-swallowed. - **B6** — query lifecycle: arm GC at query creation (not only on unsubscribe), skip listenerless queries in `#onChange`, and equality-check before notifying to avoid spurious `useSyncExternalStore` churn. ### Stage 5 — performance - **P1** — `browser-event-feed` O(n²) write amplification: coalesce to one op per `local_index` (was a cumulative op per event, each re-serializing the whole accumulated array). - **P2** — bound group rows with `MAX_GROUP_EVENTS = 200` so one dominant event type can't grow a single blob unboundedly. - **P3** — drop the per-event full-state `stateSchema.parse` in core `reduce`; state is validated only at the KV/recovery trust boundary now. - **P4** — already fixed by #1460 (subscribe override forwards `eventTypes`; filtering is server-side). No change. ### Stage 6 — dead code / elegance (~247 lines deleted) - **E1** dead exports in `shared/stream-processors.ts`; **E2** `waitForOpen`; **E3** unreachable circuit-breaker `paused` guard; **E5** dead `waitForEvent`/`messageInbox.error` machinery in `subscription.ts`; **E6** redundant circuit-breaker `consumes` entries. - **E4 obsolete** (#1460 removed `processor-registered`; the shared `circuit-breaker-types` is a live, separately-imported hierarchy, not dead code). **E7 declined** — post-#1460 the subscribe override genuinely needs to retain the client callback / wire `onRpcBroken`, so folding it into the generic `makeRpcTargetClass` isn't worth it. ### Stage 7 — docs - **D1** `design.md`: status banner, fixed 1-based offsets + core-event examples, the callable subscriber spec, SQLite/512KB-chunking storage, live-tail `replayAfterOffset` default, OPFS mirror; superseded banners on the never-shipped `implementProcessor`/`connectStream` sections. - **D2** `README.md`: real browser export (`withStreamConnectionFromBrowser`), sync `Disposable`, `?path=` route map; new "Append & subscription semantics" section (D5). - **D3** ADR 0001 superseded with the shipped per-`(namespace, path, slug)` singleton model. - **D4** remaining comment drift (`beforeAppend`→`validateAppend`, `afterAppendBatch`→`processEventBatch`). ### Testing `pnpm typecheck`, `pnpm lint`, node (79) + workers (15) suites, and example-app typecheck all green. Example-app Playwright e2e run locally against a clean `vite dev` (real Stream DO via Miniflare): **26 passed**. `origin/main` merged in (no `packages/streams` overlap; the new project-config stream-processor consumers don't touch any deleted exports). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches browser reconnect, mirror discard, and ingest self-heal paths that affect live viewers and local SQLite mirrors; server stream DO behavior is mostly unchanged aside from core reduce hot-path optimization. > > **Overview** > Closes out **Stages 4–7** of the `packages/streams` adversarial review: browser mirror reliability, feed/core hot-path perf, dead-code removal, and doc alignment. Stage 2 (lazy stream init) stays **abandoned** and is only noted in `tasks/streams-review-fixes.md`. > > **Browser runtime** (`stream-browser-store.ts` and friends) is reworked so the OPFS mirror can recover instead of silently drifting. Inbound delivery is fire-and-forget, so failed `ingest` now triggers **`ingestWithSelfHeal`** (resubscribe from the last checkpoint with capped exponential backoff). Reconnects go through one **`scheduleReconnect`** path with a **connection epoch** so stale WebSocket callbacks cannot tear down the replacement connection. **`appendBatch` / `runtimeState`** use **`callWhenReady`** / **`whenStreamReady`** during transient reconnects instead of throwing “disposed”. Mirror trust uses server **`createdAt`** as incarnation identity via new **`mirror_meta`** helpers in **`stream-browser-db.ts`**. Web Lock **`release()`** aborts pending lock requests; the SQLite worker preserves the original batch error if `ROLLBACK` fails; reactive queries GC earlier, skip unobserved refreshes, and avoid notify when snapshots are unchanged. > > **Performance:** `browser-event-feed` **`planFeedOps`** coalesces one SQL op per group row and caps groups at **`MAX_GROUP_EVENTS` (200)**. Core **`reduce`** no longer exit-parses the full `stateSchema` on every appended event. > > **Cleanup:** ~247 lines removed (unused `stream-processors` exports, `waitForOpen`, dead subscription `waitForEvent` machinery, trimmed circuit-breaker `consumes` and an unreachable guard). **Docs:** `README` (real browser API, `?path=` routes, append/subscription semantics), `design.md` and ADR 0001 marked superseded where they diverge from shipped code. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f52f305. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- CLOUDFLARE_PREVIEW --> ## Environment Config Lease <!-- CLOUDFLARE_PREVIEW_STATE --> <!-- { "apps": { "os": { "appDisplayName": "OS", "appSlug": "os", "status": "deployed", "updatedAt": "2026-06-10T22:02:58.860Z", "headSha": "f52f305a9f1ab43c9749f80e30bcd96705cbce59", "message": null, "publicUrl": "https://os.iterate-preview-3.com", "runUrl": "https://github.com/iterate/iterate/actions/runs/27309066809", "shortSha": "f52f305" } }, "environmentConfigLease": { "dopplerConfig": "preview_3", "leasedUntil": 1781132382486, "leaseId": "52648479-bd91-469d-91e5-a3338534feae", "slug": "preview-3", "type": "environment-config-lease" } } --> <!-- /CLOUDFLARE_PREVIEW_STATE --> Lease: `preview-3` Doppler config: `preview_3` Type: `environment-config-lease` Leased until: 2026-06-10T22:59:42.486Z ### OS Status: deployed Commit: `f52f305` Preview: https://os.iterate-preview-3.com [Workflow run](https://github.com/iterate/iterate/actions/runs/27309066809) Updated: 2026-06-10T22:02:58.860Z <!-- /CLOUDFLARE_PREVIEW --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Problem
Converted dashboard routes (streams index) fetch only via
useItxQueryover the browser's itx WebSocket — route loaders lost theirensureQueryDataprefetch, so every visit painted a first-visit spinner.What this adds (itx DECISIONS D18)
src/itx/access.ts—accessForPrincipal/resolveAccessibleContextId/requireWorkerExportsextracted fromfetch.ts(no behavior change;resolveAccessibleContextIdnow takesdbinstead of the wholeRequestContext). The/api/itxdoor and the new SSR door share one auth boundary, so they cannot drift.src/itx/server.ts—getServerItx(projectSlugOrId, requestContext?): an in-process project-narrowedItxhandle built from the request context (principalfrom the auth request middleware,envfromcloudflare:workers,workerExportsfrom the context), mirroring exactly whatfetch.tsdoes at WebSocket connect time minus the transport. Throws with the kernel's no-existence-probing "not found" wording when the principal may not hold the project.src/itx/loader.ts—getLoaderItx: isomorphic accessor in theorpc/client.tsshape. Server branch dynamically importsserver.ts(socloudflare:workers/db never enter the browser graph even before the Start compiler strips the branch); browser branch reuses the per-tab socket singleton, moved tosrc/itx/react/browser-client.tsso loaders share the hooks' socket and project-handle cache without importing React. Explicit return types, non-route module — no routeTree Register cycles.prefetchItxQuery(same module): best-effortensureQueryDatathat catches and discards every failure. Prefetch is an optimization, never a gate — this is the guard against the prod incident where a FORBIDDEN thrown during route loading crashed the streams page into the generic error boundary. The component'suseItxQueryre-surfaces the same error inline.lib/itx-queries.tsrestructured: each piece of data has ONEItxQueryDefinition({project, queryKey, queryFn, staleTime}) consumed by both the React hook and loader prefetches.Note: adaptation to main's drift
The original plan said to prefetch
useProjectStreamsList. Since then the streams index was rewritten aroundStreamTreeBrowser(#1443/#1449), whose first paint gates on the root stream state, not the list — and its inline source used ad-hoc keys (["project", id, "streams", ...]) that didn't share cache with the breadcrumbs'itxKey-based entries. So the loader prefetchesprojectStreamStateQuery(root), and the route's tree source now keys onitxKey.project(id, "streams")— tree nodes, breadcrumb navigators, and the prefetch all land onprojectStreamStateKey(...)per path.useProjectStreamsListkeeps its single shared definition.DECISIONS numbering
The new entry is D18 (next free number at time of writing). If another in-flight PR also claims D18, renumber on merge.
SSR seeding verification
router.tsxwiressetupRouterSsrQueryIntegration({ router, queryClient, wrapQueryClient: true }), which dehydrates every query seeded during SSR loaders and hydrates it client-side — the same machinery the establishedensureProjectBySlugbeforeLoad pattern already relies on. Nothing there needed changing; the loader awaitsprefetchItxQuery, so the entry is in the cache before dehydration.Tests
src/itx/loader.test.ts— prefetch seeds the cache through the resolved handle; swallows handle-resolution failures (no principal / forbidden, leaving component-visible error state in cache); swallows kernel queryFn failures;getLoaderItxserver-branch wiring.src/durable-objects/itx-server-handle.*(pnpm test:itx-server-handle, same family/convention asitx-stream-subscribe.*): hand-builtRequestContext(principal + real D1 projects row +ctx.exports) →getServerItx→itx.streams.list()/append()against a real Stream Durable Object through the StreamsCapability loopback. Covers member by slug + id, admin, stranger (denied), anonymous (denied), unknown slug (denied) — the insufficient-principal SSR path is exercised here and in the unit suite.pnpm vitest run src/itx/react— 22 tests green (plus loader tests).pnpm typecheck && pnpm lint && pnpm format && pnpm testall green.Docs: D18 in
src/itx/DECISIONS.md, SSR section ofdocs/itx-orpc-replacement-plan.mdmarked done,tasks/os-orpc-teardown.mdcheckbox ticked.🤖 Generated with Claude Code
Note
Medium Risk
Touches authentication boundaries for SSR and /api/itx (shared module) and changes loader error handling; well-covered by new harness and loader tests, but auth drift or silent prefetch failures could affect first paint or error UX.
Overview
Adds SSR/loader prefetch for itx so TanStack routes can seed React Query without opening a WebSocket, restoring the prefetch behavior lost when streams moved off oRPC.
Shared auth boundary —
access.tscentralizesaccessForPrincipal,resolveAccessibleContextId, andrequireWorkerExports;/api/itxinfetch.tsand the new SSR path both use it so connect-time and loader-time access cannot drift.In-process SSR handle —
getServerItxbuilds a project-narrowedItxfrom request context via the same resolve chain as WebSocket connect (no capnweb).getLoaderItxis the isomorphic entry (server → dynamicgetServerItx; client → shared tab singleton inbrowser-client.ts).prefetchItxQueryrunsensureQueryDatabut swallows all errors so forbidden/missing projects do not blow up route loaders (prod streams error-boundary fix).Query definitions —
ItxQueryDefinitioninlib/itx-queries.tsis shared by hooks and loaders; the streams index loader prefetches root stream state and aligns treeitxKeyusage with breadcrumbs.Tests — Unit tests for loader prefetch contract; new
test:itx-server-handleworker harness provinggetServerItx→ real Stream DO + auth denial cases. Minor e2e timeout/assertion tweaks.Reviewed by Cursor Bugbot for commit 4a96cca. Bugbot is set up for automated code reviews on this repo. Configure here.
Environment Config Lease
No active environment config lease.
OS
Status: released
Commit:
4a96ccaPreview: https://os.iterate-preview-4.com
Summary: Preview app released.
Workflow run
Updated: 2026-06-10T18:00:52.363Z
Semaphore
Status: released
Commit:
4a96ccaPreview: https://semaphore.iterate-preview-4.com
Summary: Preview app released.
Workflow run
Updated: 2026-06-10T18:00:39.706Z