From 52aa8b6a04939524833f2ecb4415df6843a81d9d Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Mon, 18 May 2026 19:18:29 -0400 Subject: [PATCH 1/5] file-extract route: pin canonical file-api URL as a static dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base FileDef class is implemented in card-api but consumers import it from file-api, which is also the URL the indexer references when invalidating file extracts. Previously file-api only entered the dep set via the runtime tracker — matrix-service's fileAPIModule importResource calls loader.import('file-api') during service init, which records the dep against whatever tracking session is active when the task body runs. The route lifecycle doesn't guarantee that import lands inside the active session window: beforeModel triggers lazy realm-service/matrix-service injection, the import task queues, and then model() opens the tracking session. On a fresh page right after a BrowserManager.restartBrowser() the import body usually slips inside the session; on a reused page the module is already cached, the task never reruns, and file-api silently drops from the dep set on subsequent renders. Promote file-api to a deterministic static dep on every file-extract, and warn (with both the extractor's static deps and the tracker's snapshot) when the runtime tracker missed it — so a future regression in the tracking-session lifecycle is debuggable from the prerender log without re-deriving the race. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../host/app/routes/render/file-extract.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/host/app/routes/render/file-extract.ts b/packages/host/app/routes/render/file-extract.ts index 357516b6fa7..1b27b421749 100644 --- a/packages/host/app/routes/render/file-extract.ts +++ b/packages/host/app/routes/render/file-extract.ts @@ -7,7 +7,9 @@ import { isTesting } from '@embroider/macros'; import { baseFileRef, + baseRealm, formattedError, + logger, snapshotRuntimeDependencies, withRuntimeDependencyTrackingContext, type RenderError, @@ -24,6 +26,9 @@ import type LoaderService from '../../services/loader-service'; import type NetworkService from '../../services/network'; import type RealmService from '../../services/realm'; import type { Model as RenderModel } from '../render'; + +const log = logger('render-route:file-extract'); + export type Model = { id: string; nonce: string } & FileDefExtractResult; export default class RenderFileExtractRoute extends Route { @@ -117,7 +122,28 @@ export default class RenderFileExtractRoute extends Route { }; } let { deps } = snapshotRuntimeDependencies({ excludeQueryOnly: true }); - let mergedDeps = [...new Set([...(result.deps ?? []), ...deps])]; + // `baseFileRef.module` points at `card-api` (where FileDef is implemented), + // but the indexer references `file-api` — the canonical public re-export — + // when invalidating file extracts. Pin it as a static dep so the contract + // doesn't ride on whether the runtime tracker happened to observe an + // import of file-api during the active session window. + let baseFileApiModule = `${baseRealm.url}file-api`; + let resultDeps = result.deps ?? []; + if ( + !deps.includes(baseFileApiModule) && + !resultDeps.includes(baseFileApiModule) + ) { + // If this fires repeatedly the static merge below is masking a real + // regression in the tracking-session lifecycle — worth investigating + // rather than treating as a permanent fallback. + log.warn( + `runtime tracker missed canonical file-api module for ${id} ` + + `(nonce=${nonce}); falling back to static dep. ` + + `extractor deps=${JSON.stringify(resultDeps)} ` + + `tracker deps=${JSON.stringify(deps)}`, + ); + } + let mergedDeps = [...new Set([baseFileApiModule, ...resultDeps, ...deps])]; return { id, nonce, From 4bbd296c59a62801b244a84f3449aa1c783e786b Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Mon, 18 May 2026 19:43:28 -0400 Subject: [PATCH 2/5] render route: await matrix-service.ready inside the tracking session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit pinned file-api as a static dep to mask the race between matrix-service's `fileAPIModule = importResource(() => 'file-api')` import and the render route's tracking-session window. That covered the symptom for the file-extract path but left the underlying problem in place — and the same race could drop other matrix-service-loaded modules from any render's dep set. Address the race directly: - Stamp `__boxelRenderContext = true` at the start of beforeModel, BEFORE `restoreSessionsFromStorage()`. RealmResource.tokenRefresher reads this flag synchronously on its first run; without the stamp it proceeds to its delayed loginTask, which lazily injects matrix-service outside any tracking session and lands its loader.import('file-api') in a no-session void. - After `beginRuntimeDependencyTrackingSession()` in model(), await `this.matrixService.ready`. This is both the first access to matrix-service in the render lifecycle (now guaranteed to be inside the session window) and the await on its existing public readiness promise — its loader.import calls inside loadSDK record their deps against THIS render's session. Revert the file-extract static-dep workaround; with matrix-service init deferred to inside the session, the runtime tracker observes file-api deterministically on every render. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/host/app/routes/render.ts | 26 +++++++++++++++-- .../host/app/routes/render/file-extract.ts | 28 +------------------ 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/packages/host/app/routes/render.ts b/packages/host/app/routes/render.ts index b0d62af3d62..737f6fb9385 100644 --- a/packages/host/app/routes/render.ts +++ b/packages/host/app/routes/render.ts @@ -59,6 +59,7 @@ import { } from '../utils/render-timer-stub'; import type LoaderService from '../services/loader-service'; +import type MatrixService from '../services/matrix-service'; import type NetworkService from '../services/network'; import type RealmService from '../services/realm'; import type RealmServerService from '../services/realm-server'; @@ -94,6 +95,7 @@ export default class RenderRoute extends Route { @service('render-store') declare store: RenderStoreService; @service declare router: RouterService; @service declare loaderService: LoaderService; + @service declare matrixService: MatrixService; @service declare realm: RealmService; @service declare realmServer: RealmServerService; @service declare private network: NetworkService; @@ -186,15 +188,20 @@ export default class RenderRoute extends Route { async beforeModel(transition: Transition) { await super.beforeModel?.(transition); resetRenderTimerStats(); + // Stamp render-context BEFORE touching realm / matrix services. Several + // prerender-aware short-circuits — most notably RealmResource's + // tokenRefresher — read this flag synchronously on their first run; if + // it's still unset, tokenRefresher proceeds to its delayed loginTask, + // which in turn lazily injects matrix-service. That early matrix-service + // construction lands its loader.import('file-api') outside model()'s + // tracking-session window and silently drops the dep from snapshots. + (globalThis as any).__boxelRenderContext = true; if (!isTesting()) { // tests have their own way of dealing with window level errors in card-prerender.gts this.#attachWindowErrorListeners(); this.realm.restoreSessionsFromStorage(); } - // activate() doesn't run early enough for this to be set before the model() - // hook is run - (globalThis as any).__boxelRenderContext = true; this.#registerGlobalsDestructor(); this.#authGuard.register(); if (!isTesting()) { @@ -323,6 +330,19 @@ export default class RenderRoute extends Route { : 'instance', }); + // Force the matrix-service SDK load to settle INSIDE the active + // tracking session. matrix-service is the only path in the host that + // imports `https://cardstack.com/base/file-api` at runtime (via its + // `fileAPIModule` importResource); without this await its + // `loader.import('file-api')` races against the session window and + // silently drops the dep on cached/reused prerender pages. Awaiting + // `ready` here both constructs matrix-service (if not yet) and waits + // for `loadSDK` (cardAPI + fileAPI imports) to complete with the + // session active, so the imports are recorded as deps of THIS render. + if (!isTesting()) { + await this.matrixService.ready; + } + // the window.boxelTransitionTo() function helper first normalizes the base // params by transitioning the router back to 'render' before it goes on to // 'render.html', 'render.meta', etc. That’s why you see the /render model diff --git a/packages/host/app/routes/render/file-extract.ts b/packages/host/app/routes/render/file-extract.ts index 1b27b421749..357516b6fa7 100644 --- a/packages/host/app/routes/render/file-extract.ts +++ b/packages/host/app/routes/render/file-extract.ts @@ -7,9 +7,7 @@ import { isTesting } from '@embroider/macros'; import { baseFileRef, - baseRealm, formattedError, - logger, snapshotRuntimeDependencies, withRuntimeDependencyTrackingContext, type RenderError, @@ -26,9 +24,6 @@ import type LoaderService from '../../services/loader-service'; import type NetworkService from '../../services/network'; import type RealmService from '../../services/realm'; import type { Model as RenderModel } from '../render'; - -const log = logger('render-route:file-extract'); - export type Model = { id: string; nonce: string } & FileDefExtractResult; export default class RenderFileExtractRoute extends Route { @@ -122,28 +117,7 @@ export default class RenderFileExtractRoute extends Route { }; } let { deps } = snapshotRuntimeDependencies({ excludeQueryOnly: true }); - // `baseFileRef.module` points at `card-api` (where FileDef is implemented), - // but the indexer references `file-api` — the canonical public re-export — - // when invalidating file extracts. Pin it as a static dep so the contract - // doesn't ride on whether the runtime tracker happened to observe an - // import of file-api during the active session window. - let baseFileApiModule = `${baseRealm.url}file-api`; - let resultDeps = result.deps ?? []; - if ( - !deps.includes(baseFileApiModule) && - !resultDeps.includes(baseFileApiModule) - ) { - // If this fires repeatedly the static merge below is masking a real - // regression in the tracking-session lifecycle — worth investigating - // rather than treating as a permanent fallback. - log.warn( - `runtime tracker missed canonical file-api module for ${id} ` + - `(nonce=${nonce}); falling back to static dep. ` + - `extractor deps=${JSON.stringify(resultDeps)} ` + - `tracker deps=${JSON.stringify(deps)}`, - ); - } - let mergedDeps = [...new Set([baseFileApiModule, ...resultDeps, ...deps])]; + let mergedDeps = [...new Set([...(result.deps ?? []), ...deps])]; return { id, nonce, From aaeafb6eb11211705a15c2518cd12159cb7432c1 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Mon, 18 May 2026 19:53:59 -0400 Subject: [PATCH 3/5] render route: track card-api / file-api directly, drop matrix-service coupling In RenderRoute.beforeModel, stamp __boxelRenderContext before touching the realm service so RealmResource.tokenRefresher short-circuits and does not lazily inject matrix-service in the prerender Chrome. In RenderRoute.model, immediately after opening the runtime-dep tracking session, await Promise.all of loader.import for card-api and file-api. The loader caches the modules so subsequent renders are a cheap re-track, and the deps are now recorded deterministically against THIS render's session without dragging in matrix-service (and its matrix SDK + client + event-binding setup that the prerender never uses). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/host/app/routes/render.ts | 41 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/host/app/routes/render.ts b/packages/host/app/routes/render.ts index 737f6fb9385..60a1f08c405 100644 --- a/packages/host/app/routes/render.ts +++ b/packages/host/app/routes/render.ts @@ -59,7 +59,6 @@ import { } from '../utils/render-timer-stub'; import type LoaderService from '../services/loader-service'; -import type MatrixService from '../services/matrix-service'; import type NetworkService from '../services/network'; import type RealmService from '../services/realm'; import type RealmServerService from '../services/realm-server'; @@ -95,7 +94,6 @@ export default class RenderRoute extends Route { @service('render-store') declare store: RenderStoreService; @service declare router: RouterService; @service declare loaderService: LoaderService; - @service declare matrixService: MatrixService; @service declare realm: RealmService; @service declare realmServer: RealmServerService; @service declare private network: NetworkService; @@ -188,13 +186,16 @@ export default class RenderRoute extends Route { async beforeModel(transition: Transition) { await super.beforeModel?.(transition); resetRenderTimerStats(); - // Stamp render-context BEFORE touching realm / matrix services. Several - // prerender-aware short-circuits — most notably RealmResource's - // tokenRefresher — read this flag synchronously on their first run; if - // it's still unset, tokenRefresher proceeds to its delayed loginTask, - // which in turn lazily injects matrix-service. That early matrix-service - // construction lands its loader.import('file-api') outside model()'s - // tracking-session window and silently drops the dep from snapshots. + // Stamp render-context BEFORE touching the realm service. Prerender- + // aware short-circuits read this flag synchronously on their first + // run — most importantly `RealmResource.tokenRefresher`. With the + // flag unset, tokenRefresher falls through to its delayed + // `loginTask`, which lazily injects matrix-service. Inside the + // prerender Chrome the matrix client + event bindings + token-refresh + // logic are dead weight that the render route never touches, and + // constructing matrix-service drags all of it in. Setting the flag + // first keeps matrix-service uninstantiated for the prerender's + // lifetime. (globalThis as any).__boxelRenderContext = true; if (!isTesting()) { // tests have their own way of dealing with window level errors in card-prerender.gts @@ -330,18 +331,16 @@ export default class RenderRoute extends Route { : 'instance', }); - // Force the matrix-service SDK load to settle INSIDE the active - // tracking session. matrix-service is the only path in the host that - // imports `https://cardstack.com/base/file-api` at runtime (via its - // `fileAPIModule` importResource); without this await its - // `loader.import('file-api')` races against the session window and - // silently drops the dep on cached/reused prerender pages. Awaiting - // `ready` here both constructs matrix-service (if not yet) and waits - // for `loadSDK` (cardAPI + fileAPI imports) to complete with the - // session active, so the imports are recorded as deps of THIS render. - if (!isTesting()) { - await this.matrixService.ready; - } + // Stamp `card-api` and `file-api` as runtime deps of this render. + // `file-api` is the public URL the indexer references when + // invalidating file extracts; doing the import here records the + // deps against the active tracking session without forcing + // matrix-service construction (which would also pull in the matrix + // SDK + client + event bindings the prerender never touches). + await Promise.all([ + this.loaderService.loader.import(`${baseRealm.url}card-api`), + this.loaderService.loader.import(`${baseRealm.url}file-api`), + ]); // the window.boxelTransitionTo() function helper first normalizes the base // params by transitioning the router back to 'render' before it goes on to From 31c352e145fbe0fbe247fc38b89369086cc264b4 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Mon, 18 May 2026 20:02:56 -0400 Subject: [PATCH 4/5] render route: keep dep imports inside the cached buildModel promise Move the card-api / file-api loader.import await out of model() and into #buildModel so the cached entry in #modelPromises covers the imports. Concurrent same-key model() calls (the render -> child-route transition flow that intentionally relies on #modelPromises for dedup) no longer race past the cache check while the imports are in flight. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/host/app/routes/render.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/host/app/routes/render.ts b/packages/host/app/routes/render.ts index 60a1f08c405..d92634dafb4 100644 --- a/packages/host/app/routes/render.ts +++ b/packages/host/app/routes/render.ts @@ -331,17 +331,6 @@ export default class RenderRoute extends Route { : 'instance', }); - // Stamp `card-api` and `file-api` as runtime deps of this render. - // `file-api` is the public URL the indexer references when - // invalidating file extracts; doing the import here records the - // deps against the active tracking session without forcing - // matrix-service construction (which would also pull in the matrix - // SDK + client + event bindings the prerender never touches). - await Promise.all([ - this.loaderService.loader.import(`${baseRealm.url}card-api`), - this.loaderService.loader.import(`${baseRealm.url}file-api`), - ]); - // the window.boxelTransitionTo() function helper first normalizes the base // params by transitioning the router back to 'render' before it goes on to // 'render.html', 'render.meta', etc. That’s why you see the /render model @@ -368,6 +357,16 @@ export default class RenderRoute extends Route { this.lastStoreResetKey = resetKey; } } + // Stamp `card-api` and `file-api` as runtime deps of this render. + // `file-api` is the public URL the indexer references when + // invalidating file extracts; doing the import here records the + // deps against the active tracking session without forcing + // matrix-service construction (which would also pull in the matrix + // SDK + client + event bindings the prerender never touches). + await Promise.all([ + this.loaderService.loader.import(`${baseRealm.url}card-api`), + this.loaderService.loader.import(`${baseRealm.url}file-api`), + ]); if (parsedOptions.fileExtract) { let state = new TrackedMap(); state.set('status', 'ready'); From 9a48c76d4cea0ed8281ebfe7b4467eccc90718f4 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Mon, 18 May 2026 20:08:22 -0400 Subject: [PATCH 5/5] render route: drop early __boxelRenderContext stamp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the original beforeModel ordering. With card-api / file-api tracked deterministically by the loader.import await in #buildModel, the early stamp is no longer needed to keep file-api in the dep set — tokenRefresher's lazy matrix-service injection (if it ever fires) no longer affects render correctness. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/host/app/routes/render.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/host/app/routes/render.ts b/packages/host/app/routes/render.ts index d92634dafb4..6973c3fc856 100644 --- a/packages/host/app/routes/render.ts +++ b/packages/host/app/routes/render.ts @@ -186,23 +186,15 @@ export default class RenderRoute extends Route { async beforeModel(transition: Transition) { await super.beforeModel?.(transition); resetRenderTimerStats(); - // Stamp render-context BEFORE touching the realm service. Prerender- - // aware short-circuits read this flag synchronously on their first - // run — most importantly `RealmResource.tokenRefresher`. With the - // flag unset, tokenRefresher falls through to its delayed - // `loginTask`, which lazily injects matrix-service. Inside the - // prerender Chrome the matrix client + event bindings + token-refresh - // logic are dead weight that the render route never touches, and - // constructing matrix-service drags all of it in. Setting the flag - // first keeps matrix-service uninstantiated for the prerender's - // lifetime. - (globalThis as any).__boxelRenderContext = true; if (!isTesting()) { // tests have their own way of dealing with window level errors in card-prerender.gts this.#attachWindowErrorListeners(); this.realm.restoreSessionsFromStorage(); } + // activate() doesn't run early enough for this to be set before the model() + // hook is run + (globalThis as any).__boxelRenderContext = true; this.#registerGlobalsDestructor(); this.#authGuard.register(); if (!isTesting()) {