Skip to content
47 changes: 47 additions & 0 deletions packages/host/app/routes/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export default class ModuleRoute extends Route<Model> {
options?: string;
}) {
let parsedOptions = parseRenderRouteOptions(options);
this.#synchronizeLoaderEpoch(parsedOptions.loaderEpoch);
return await buildModuleModel(
{
id,
Expand All @@ -187,6 +188,52 @@ export default class ModuleRoute extends Route<Model> {
);
}

// Loader-epoch synchronization, this route's half of what routes/render.ts
// does for visits: the realm's loader epoch is re-minted whenever its
// executable modules change (an index pass that invalidates one, or the
// write that changed the bytes), and a mismatch in either direction means
// this tab's loader belongs to a different module timeline. Without it a
// module render imports out of whatever this tab already evaluated, so a
// module rewritten since then reports its previous shape — with the current
// file metadata attached, since that is re-read per render — and the
// definition cache stores that under the rewritten module's URL.
//
// On the route rather than in `buildModuleModel`, which the route shares
// with card-prerender.gts. That component mounts only under `isTesting()`,
// so its renders run inside the test harness's own tab, sharing the loader
// and store with the application under test — state the harness and the
// app's own file resources and realm subscriptions already keep current.
// Replacing it from here would discard live state on a schedule neither
// has a part in. A tab that reached this route exists to serve renders and
// holds nothing else worth keeping.
//
// Held under its own key rather than the one routes/render.ts uses. Visits
// thread the epoch their indexing batch minted, which is not committed
// until the batch ends, while this route's callers read the committed
// column; a single key would read that lag as two timelines alternating and
// reset the loader on every render for the length of the batch. Separate
// keys cost a tab one extra reset per epoch — each series synchronizes
// independently — and a reset only ever leaves the loader fresher than the
// other series assumes.
#synchronizeLoaderEpoch(loaderEpoch: string | undefined) {
if (loaderEpoch === undefined) {
return;
}
let held = (globalThis as any).__boxelModuleLoaderEpoch as
| string
| undefined;
if (held === loaderEpoch) {
return;
}
this.typesCache = new WeakMap<typeof BaseDef, Promise<TypesWithErrors>>();
this.loaderService.resetLoader({
clearFetchCache: true,
reason: 'module-route loader epoch changed',
});
this.store.resetCache();
(globalThis as any).__boxelModuleLoaderEpoch = loaderEpoch;
}

#moduleModelContext(): ModuleModelContext {
return {
router: this.router,
Expand Down
Loading
Loading