akanjs 3.0.0-beta.24 → 3.0.0-beta.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/common/hmrPhase.ts +2 -0
  2. package/package.json +1 -1
  3. package/server/SSR_MEMORY_DIAGNOSIS.md +2 -2
  4. package/server/akanApp.ts +1 -0
  5. package/server/akanServer.ts +7 -1
  6. package/server/artifact/builderRpc.ts +13 -2
  7. package/server/artifact/devBuildRecovery.ts +12 -0
  8. package/server/artifact/index.ts +1 -0
  9. package/server/artifact/ipcTypes.ts +46 -3
  10. package/server/artifact/routeClientCache.ts +128 -29
  11. package/server/di/diLifecycle.ts +1 -1
  12. package/server/hmr/clientScript.ts +148 -86
  13. package/server/hmr/csrDevManifest.ts +17 -0
  14. package/server/hmr/csrDevRuntime.ts +238 -18
  15. package/server/hmr/csrDevShell.ts +90 -10
  16. package/server/hmr/devArtifactPruner.ts +77 -0
  17. package/server/hmr/devHmrController.ts +233 -137
  18. package/server/hmr/ssrDevShim.ts +112 -0
  19. package/server/hmr/ssrUpdateQueue.ts +111 -0
  20. package/server/hmr/wsHub.ts +19 -4
  21. package/server/routing/apiRouter.ts +12 -0
  22. package/server/rscWorker.tsx +11 -1
  23. package/server/rscWorkerHost.ts +223 -59
  24. package/server/systemPageDocument.tsx +6 -0
  25. package/server/types.tsx +4 -0
  26. package/server/webRouter.ts +57 -22
  27. package/types/common/hmrPhase.d.ts +2 -0
  28. package/types/server/artifact/builderRpc.d.ts +3 -2
  29. package/types/server/artifact/devBuildRecovery.d.ts +4 -0
  30. package/types/server/artifact/index.d.ts +1 -0
  31. package/types/server/artifact/ipcTypes.d.ts +47 -2
  32. package/types/server/artifact/routeClientCache.d.ts +9 -2
  33. package/types/server/hmr/csrDevManifest.d.ts +15 -0
  34. package/types/server/hmr/csrDevRuntime.d.ts +34 -0
  35. package/types/server/hmr/csrDevShell.d.ts +9 -1
  36. package/types/server/hmr/devArtifactPruner.d.ts +9 -0
  37. package/types/server/hmr/devHmrController.d.ts +8 -5
  38. package/types/server/hmr/ssrDevShim.d.ts +7 -0
  39. package/types/server/hmr/ssrUpdateQueue.d.ts +23 -0
  40. package/types/server/hmr/wsHub.d.ts +16 -7
  41. package/types/server/routing/apiRouter.d.ts +8 -0
  42. package/types/server/rscWorkerHost.d.ts +14 -2
  43. package/types/server/systemPageDocument.d.ts +2 -0
  44. package/types/server/types.d.ts +4 -0
  45. package/types/server/webRouter.d.ts +8 -2
@@ -5,6 +5,7 @@ declare global {
5
5
  var __AKAN_HMR_PHASE__: AkanHmrPhase | undefined;
6
6
  }
7
7
 
8
+ /** @deprecated Always `null`: the dev client that set the phase applies modules through its registry now. */
8
9
  export function getAkanHmrPhase(): AkanHmrPhase {
9
10
  if (typeof globalThis === "undefined") return null;
10
11
  const phase = globalThis.__AKAN_HMR_PHASE__;
@@ -12,6 +13,7 @@ export function getAkanHmrPhase(): AkanHmrPhase {
12
13
  return null;
13
14
  }
14
15
 
16
+ /** @deprecated Always `false`, for the reason `getAkanHmrPhase` is always `null`. */
15
17
  export function isAkanHmrApplying(): boolean {
16
18
  return getAkanHmrPhase() !== null;
17
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-beta.24",
3
+ "version": "3.0.0-beta.26",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -130,8 +130,8 @@ that no GC reclaims, so the process only returns that memory by exiting. `AkanAp
130
130
  recycles it — gracefully, after its queues drain and once it has stayed quiet — past a ceiling:
131
131
 
132
132
  ```sh
133
- AKAN_BUILDER_MAX_RSS_MB=1200 # 0 leaves the builder unbounded; default is 1200 in dev
134
- AKAN_BUILDER_MAX_RSS=1200mb # same ceiling with a unit suffix
133
+ AKAN_BUILDER_MAX_RSS_MB=2048 # 0 leaves the builder unbounded; default is 2048 in dev
134
+ AKAN_BUILDER_MAX_RSS=2gb # same ceiling with a unit suffix
135
135
  ```
136
136
 
137
137
  Both resolve through `MemoryLimit.resolveMaxRssBytes`, so `AKAN_MEMORY_LIMIT` or a cgroup limit also
package/server/akanApp.ts CHANGED
@@ -1054,6 +1054,7 @@ export class AkanApp {
1054
1054
  case "css-updated":
1055
1055
  case "pages-updated":
1056
1056
  case "csr-updated":
1057
+ case "ssr-updated":
1057
1058
  case "build-status":
1058
1059
  this.#fanoutToFederation(message);
1059
1060
  return;
@@ -380,7 +380,13 @@ export class AkanServer {
380
380
  registry: this.#di.registry,
381
381
  live: this.#di.live,
382
382
  hmrHub,
383
- hmrState: webRouter ? { state: webRouter.renderState } : null,
383
+ hmrState: webRouter
384
+ ? {
385
+ state: webRouter.renderState,
386
+ refresh: () => webRouter.refreshHmrState(),
387
+ errors: () => webRouter.hmrBuildErrors(),
388
+ }
389
+ : null,
384
390
  logger: this.logger,
385
391
  onDrain: () => this.#binaryPubsub.flush(),
386
392
  }),
@@ -12,6 +12,7 @@ import type {
12
12
  CssPayload,
13
13
  DevBuildStatus,
14
14
  PagesBundlePayload,
15
+ SsrUpdatedPayload,
15
16
  } from "./ipcTypes";
16
17
 
17
18
  export interface BuilderRpcEventHandlers {
@@ -19,8 +20,9 @@ export interface BuilderRpcEventHandlers {
19
20
  onInvalidate?: (event: { kinds: ("code" | "css" | "config")[]; files: string[]; generation?: number }) => void;
20
21
  onCssUpdated?: (css: CssPayload) => void;
21
22
  /** A fresh `pages-*.js` bundle: re-import it in the running worker (`RscWorker.reload`) rather than respawning. */
22
- onPagesUpdated?: (bundle: PagesBundlePayload) => void;
23
+ onPagesUpdated?: (bundle: PagesBundlePayload) => void | Promise<void>;
23
24
  onCsrUpdated?: (update: CsrUpdatedPayload) => void;
25
+ onSsrUpdated?: (update: SsrUpdatedPayload) => void;
24
26
  onBuildStatus?: (status: DevBuildStatus) => void;
25
27
  }
26
28
 
@@ -59,11 +61,19 @@ export class BuilderRpc {
59
61
  handlers.onCssUpdated?.(ev.data);
60
62
  return;
61
63
  case "pages-updated":
62
- handlers.onPagesUpdated?.(ev.data);
64
+
65
+ void Promise.resolve(handlers.onPagesUpdated?.(ev.data)).catch((error: unknown) =>
66
+ this.#logger.error(
67
+ `[builder] pages-updated failed: ${error instanceof Error ? error.message : String(error)}`,
68
+ ),
69
+ );
63
70
  return;
64
71
  case "csr-updated":
65
72
  handlers.onCsrUpdated?.(ev.data);
66
73
  return;
74
+ case "ssr-updated":
75
+ handlers.onSsrUpdated?.(ev.data);
76
+ return;
67
77
  case "build-status":
68
78
  handlers.onBuildStatus?.(ev.data);
69
79
  return;
@@ -90,6 +100,7 @@ export class BuilderRpc {
90
100
  discoveredEntries: payload.discoveredEntries,
91
101
  clientDeps: payload.clientDeps,
92
102
  clientDepsByEntry: payload.clientDepsByEntry,
103
+ ...(payload.seenGeneration !== undefined ? { seenGeneration: payload.seenGeneration } : {}),
93
104
  };
94
105
  }
95
106
 
@@ -0,0 +1,12 @@
1
+ import type { DevBuildStatus } from "./ipcTypes";
2
+
3
+ export class DevBuildRecovery {
4
+
5
+ static recovers(previous: DevBuildStatus | undefined, status: DevBuildStatus): boolean {
6
+ if (!previous || previous.ok || !status.ok) return false;
7
+ if (status.phase === "route" && status.scope !== undefined && previous.scope !== undefined)
8
+ return status.scope === previous.scope && status.generation >= previous.generation;
9
+ if (status.generation !== previous.generation) return status.generation > previous.generation;
10
+ return status.phase === "backend";
11
+ }
12
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./builderRpc";
2
+ export * from "./devBuildRecovery";
2
3
  export * from "./ipcTypes";
3
4
  export * from "./routeClientCache";
4
5
  export * from "./routeSeedIndexStore";
@@ -9,6 +9,8 @@ export interface BuildRouteResultPayload {
9
9
  clientDepsByEntry?: Record<string, string[]>;
10
10
  routeId?: string;
11
11
  generation?: number;
12
+ /** The newest save batch whose files its client-entry discovery had taken in when it started. */
13
+ seenGeneration?: number;
12
14
  }
13
15
 
14
16
  /** Re-announces a recycled builder's artifact (not an edit); the host drops it when the hashed output did not move. */
@@ -42,7 +44,7 @@ export interface DevChangePlan {
42
44
  reasonByFile: Record<string, string[]>;
43
45
  }
44
46
 
45
- export type BuildPhase = "scan" | "barrel" | "csr" | "pages" | "css" | "route" | "backend";
47
+ export type BuildPhase = "scan" | "barrel" | "csr" | "ssr" | "pages" | "css" | "route" | "backend";
46
48
 
47
49
  export interface DevBuildStatus {
48
50
  generation: number;
@@ -50,6 +52,8 @@ export interface DevBuildStatus {
50
52
  ok: boolean;
51
53
  files: string[];
52
54
  message?: string;
55
+ /** The route a route build's status speaks for. */
56
+ scope?: string;
53
57
  }
54
58
 
55
59
  export type BuilderReq = {
@@ -76,19 +80,35 @@ export type BuilderCsrRes =
76
80
  /** Drain and exit (the only way bundler memory returns to the OS); draining, not killing, keeps a rebuild whole. */
77
81
  export type BuilderControl = { type: "builder-shutdown"; reason: string };
78
82
 
83
+ /** Epoch-ms marks a save picks up on its way to the page, so a slow update says which hop it waited in. */
84
+ export interface HmrTrace {
85
+ eventAt?: number;
86
+ flushAt?: number;
87
+ batchAt?: number;
88
+ spawnAt?: number;
89
+ workerStartAt?: number;
90
+ workerAt?: number;
91
+ patchAt?: number;
92
+ sentAt?: number;
93
+ broadcastAt?: number;
94
+ }
95
+
79
96
  export interface PagesBundlePayload {
80
97
  bundlePath: string;
81
98
  buildId: number;
82
99
  generation?: number;
83
100
  changedFiles?: string[];
84
101
  reason?: BuilderStateReason;
102
+ trace?: HmrTrace;
103
+ /** False when no changed file is one the server renders (a `"use client"` edit that kept its export names). */
104
+ serverTouched?: boolean;
85
105
  }
86
106
 
87
107
  /** `Bun.build` keeps native arenas `Bun.gc(true)` never frees (macOS returns none when idle): hence recycling. */
88
108
  export interface BuilderMetrics {
89
109
  /** A peak sampled when the queues drain; stale within seconds on Linux, so the host re-reads RSS from the OS. */
90
110
  rssBytes: number;
91
- /** The builder's newest generation; 0 until it has processed a watch batch since spawning. */
111
+ /** The builder's newest generation; a replacement builder continues from the last one its host saw. */
92
112
  generation: number;
93
113
  /** Work items completed since this builder spawned, so a host can require real work before recycling. */
94
114
  workCount: number;
@@ -101,6 +121,24 @@ export interface CsrUpdatedPayload {
101
121
  reason?: string;
102
122
  patchUrl?: string;
103
123
  changedIds?: string[];
124
+ trace?: HmrTrace;
125
+ }
126
+
127
+ /** A new generation of the SSR dev registry, where SSR pages load their client code from: a patch, or a reload. */
128
+ export interface SsrUpdatedPayload {
129
+ generation: number;
130
+ reload: boolean;
131
+ reason?: string;
132
+ patchUrl?: string;
133
+ changedIds?: string[];
134
+ trace?: HmrTrace;
135
+ /** The save also changed what the server renders: the tabs get this patch with that batch's RSC refresh. */
136
+ hold?: boolean;
137
+ /** The watch batch this patch came from, which the `pages-updated` releasing it names as its generation. */
138
+ batchGeneration?: number;
139
+ epoch?: number;
140
+ /** The registry's first build: no tab holds a module of it, so there is nothing to send them. */
141
+ first?: boolean;
104
142
  }
105
143
 
106
144
  export type BuilderEvent =
@@ -117,8 +155,11 @@ export type BuilderEvent =
117
155
  | { type: "css-updated"; data: CssPayload }
118
156
  | { type: "pages-updated"; data: PagesBundlePayload }
119
157
  | { type: "csr-updated"; data: CsrUpdatedPayload }
158
+ | { type: "ssr-updated"; data: SsrUpdatedPayload }
120
159
  | { type: "build-status"; data: DevBuildStatus }
121
- | { type: "builder-metrics"; data: BuilderMetrics };
160
+ | { type: "builder-metrics"; data: BuilderMetrics }
161
+ /** The boot builds settled (the SSR registry's, and CSR's when the env arms it): their workers are a boot's peak. */
162
+ | { type: "boot-armed" };
122
163
 
123
164
  export type BuilderMessage = BuilderReq | BuilderRes | BuilderCsrReq | BuilderCsrRes | BuilderControl | BuilderEvent;
124
165
 
@@ -138,4 +179,6 @@ export interface BuildRouteClientResult {
138
179
  discoveredEntries?: string[];
139
180
  clientDeps: string[];
140
181
  clientDepsByEntry?: Record<string, string[]>;
182
+ /** The newest save batch whose files its client-entry discovery had taken in when it started. */
183
+ seenGeneration?: number;
141
184
  }
@@ -30,9 +30,24 @@ type PendingBuild = { generation: number; promise: Promise<BuildRouteClientResul
30
30
  export interface InvalidateClientEntriesOptions {
31
31
  routePredicate: (routeId: string) => boolean;
32
32
  staleEntries: Iterable<string>;
33
+ /** The saved files behind it: a build still running that bundled one of them is built again. */
34
+ files?: Iterable<string>;
35
+ /** The save batch behind it: a route build whose discovery had taken that batch in is neither dropped nor rebuilt. */
36
+ batch?: number;
37
+ }
38
+ interface Invalidation {
39
+ /** The generation it moved the cache to: only a build started before it is checked against it. */
40
+ generation: number;
41
+ cleared: boolean;
42
+ routePredicate: (routeId: string) => boolean;
43
+ /** Its stale entries and saved files, normalized. */
44
+ touched: Set<string>;
45
+ batch?: number;
33
46
  }
34
47
 
35
48
  export class RouteClientCache {
49
+ static readonly #minEnsureAttempts = 3;
50
+ static readonly #ensureBudgetMs = 15_000;
36
51
  readonly #logger = new Logger("RouteClientCache");
37
52
  readonly #built = new Map<string, BuildRouteClientResult>();
38
53
  readonly #building = new Map<string, PendingBuild>();
@@ -40,6 +55,7 @@ export class RouteClientCache {
40
55
  readonly #buildRoute: RouteBuildFn;
41
56
  readonly #onMerge?: OnMergeFn;
42
57
  #revision = 0;
58
+ #invalidations: Invalidation[] = [];
43
59
 
44
60
  constructor({ buildRoute, onMerge }: RouteClientCacheOptions) {
45
61
  this.#buildRoute = buildRoute;
@@ -76,21 +92,33 @@ export class RouteClientCache {
76
92
  }
77
93
 
78
94
  async ensure(routeId: string, seeds: string[]): Promise<MergedManifest> {
79
- if (this.#built.has(routeId)) return this.merged;
80
- const existing = this.#building.get(routeId);
81
- if (existing && existing.generation === this.merged.generation) {
82
- await existing.promise;
83
- return this.merged;
84
- }
85
- const generation = this.merged.generation;
86
- const promise = this.#runBuild(routeId, seeds, generation);
87
- this.#building.set(routeId, { generation, promise });
88
- try {
89
- await promise;
90
- } finally {
91
- const current = this.#building.get(routeId);
92
- if (current?.promise === promise) this.#building.delete(routeId);
95
+ const started = Date.now();
96
+ const deadline = started + RouteClientCache.#ensureBudgetMs;
97
+ const within = (attempt: number) => attempt < RouteClientCache.#minEnsureAttempts || Date.now() < deadline;
98
+ let attempt = 0;
99
+ for (; within(attempt) && !this.#built.has(routeId); attempt += 1) {
100
+ const existing = this.#building.get(routeId);
101
+ if (existing) {
102
+
103
+ if (existing.generation === this.merged.generation) await existing.promise;
104
+ else await existing.promise.catch(() => undefined);
105
+ continue;
106
+ }
107
+ const generation = this.merged.generation;
108
+ const promise = this.#runBuild(routeId, seeds, generation);
109
+ this.#building.set(routeId, { generation, promise });
110
+ try {
111
+ await promise;
112
+ } finally {
113
+ const current = this.#building.get(routeId);
114
+ if (current?.promise === promise) this.#building.delete(routeId);
115
+ this.#prune();
116
+ }
93
117
  }
118
+ if (attempt > 1)
119
+ this.#logger.verbose(
120
+ `[route-cache] ensure routeId=${routeId} took ${attempt} attempts in ${Date.now() - started}ms${this.#built.has(routeId) ? "" : "; rendering without it"}`,
121
+ );
94
122
  return this.merged;
95
123
  }
96
124
 
@@ -113,7 +141,7 @@ export class RouteClientCache {
113
141
  const knownEntries = new Set(this.merged.knownEntries);
114
142
  this.#logger.verbose(`[route-cache] build start routeId=${routeId} generation=${generation} seeds=${seeds.length}`);
115
143
  const delta = await this.#buildRoute(routeId, { seeds, knownEntries, generation });
116
- if (this.merged.generation !== generation) {
144
+ if (this.merged.generation !== generation && this.#overtaken(routeId, generation, delta)) {
117
145
  this.#logger.verbose(
118
146
  `[route-cache] stale build ignored routeId=${routeId} generation=${generation} current=${this.merged.generation}`,
119
147
  );
@@ -142,39 +170,106 @@ export class RouteClientCache {
142
170
  return dropped;
143
171
  }
144
172
 
145
- invalidate(predicate: (routeId: string) => boolean): string[] {
146
- const dropped = this.#dropBuilt(predicate);
147
- if (dropped.length > 0) {
148
- this.#rebuildKnownEntriesPreservingManifest(this.merged.generation + 1);
149
- this.#building.clear();
150
- this.#logger.verbose(`[route-cache] invalidated ${dropped.length} routes: ${dropped.join(", ")}`);
151
- }
173
+ invalidate(
174
+ predicate: (routeId: string) => boolean,
175
+ { files = [], batch }: { files?: Iterable<string>; batch?: number } = {},
176
+ ): string[] {
177
+ const spared = this.#sparedBy(batch);
178
+ const routePredicate = (routeId: string) => predicate(routeId) && !spared.has(routeId);
179
+ const dropped = this.#dropBuilt(routePredicate);
180
+ const touched = RouteClientCache.#normalizeAll(files);
181
+ if (dropped.length === 0 && !this.#mayOvertake(routePredicate, touched)) return dropped;
182
+ this.#rebuildKnownEntriesPreservingManifest(this.merged.generation + 1);
183
+ this.#record({ routePredicate, touched, batch });
184
+ this.#logger.verbose(`[route-cache] invalidated ${dropped.length} routes: ${dropped.join(", ")}`);
152
185
  return dropped;
153
186
  }
154
187
 
155
- invalidateClientEntries({ routePredicate, staleEntries }: InvalidateClientEntriesOptions): string[] {
156
- const normalizedStaleEntries = new Set([...staleEntries].map((entry) => RouteClientCache.#normalizePath(entry)));
157
- const dropped = this.#dropBuilt(routePredicate);
158
- if (dropped.length === 0 && normalizedStaleEntries.size === 0) return dropped;
188
+ invalidateClientEntries({
189
+ routePredicate,
190
+ staleEntries,
191
+ files = [],
192
+ batch,
193
+ }: InvalidateClientEntriesOptions): string[] {
194
+ const spared = this.#sparedBy(batch);
195
+
196
+ const fresh = [...spared.values()].flatMap((delta) => [...RouteClientCache.#reachedOf(delta)]);
197
+ const normalizedStaleEntries = new Set(
198
+ [...RouteClientCache.#normalizeAll(staleEntries)].filter(
199
+ (entry) => !fresh.some((file) => file === entry || file.endsWith(`/${entry}`)),
200
+ ),
201
+ );
202
+ const touched = new Set([...normalizedStaleEntries, ...RouteClientCache.#normalizeAll(files)]);
203
+ const predicate = (routeId: string) => routePredicate(routeId) && !spared.has(routeId);
204
+ const dropped = this.#dropBuilt(predicate);
205
+ if (dropped.length === 0 && normalizedStaleEntries.size === 0 && !this.#mayOvertake(predicate, touched))
206
+ return dropped;
159
207
 
160
208
  this.#rebuildKnownEntriesPreservingManifest(this.merged.generation + 1, normalizedStaleEntries);
161
- this.#building.clear();
209
+ this.#record({ routePredicate: predicate, touched, batch });
162
210
  this.#logger.verbose(
163
- `[route-cache] client invalidated routes=${dropped.join(",") || "(none)"} entries=${normalizedStaleEntries.size}`,
211
+ `[route-cache] client invalidated routes=${dropped.join(",") || "(none)"} entries=${normalizedStaleEntries.size}${spared.size > 0 ? ` kept=${[...spared.keys()].join(",")}` : ""}`,
164
212
  );
165
213
  return dropped;
166
214
  }
167
215
 
216
+ #sparedBy(batch: number | undefined): Map<string, BuildRouteClientResult> {
217
+ if (batch === undefined) return new Map();
218
+ return new Map([...this.#built].filter(([, delta]) => (delta.seenGeneration ?? -1) >= batch));
219
+ }
220
+
221
+ static #reachedOf(delta: BuildRouteClientResult): Set<string> {
222
+ return RouteClientCache.#normalizeAll([
223
+ ...(delta.discoveredEntries ?? []),
224
+ ...delta.newEntries,
225
+ ...delta.clientDeps,
226
+ ...Object.values(delta.clientDepsByEntry ?? {}).flat(),
227
+ ]);
228
+ }
229
+
168
230
  clear(): string[] {
169
231
  const dropped = this.#dropBuilt(() => true);
170
232
  const nextGeneration = this.merged.generation + 1;
171
233
  this.merged = this.#getEmptyMerged(nextGeneration);
172
234
  this.#revision += 1;
173
- this.#building.clear();
235
+ this.#record({ cleared: true, routePredicate: () => true, touched: new Set() });
174
236
  this.#logger.verbose(`[route-cache] cleared generation=${nextGeneration} dropped=${dropped.length}`);
175
237
  return dropped;
176
238
  }
177
239
 
240
+ #mayOvertake(predicate: (routeId: string) => boolean, touched: Set<string>): boolean {
241
+ return this.#building.size > 0 && (touched.size > 0 || [...this.#building.keys()].some(predicate));
242
+ }
243
+
244
+ #record({
245
+ cleared = false,
246
+ routePredicate,
247
+ touched,
248
+ batch,
249
+ }: Omit<Invalidation, "generation" | "cleared"> & { cleared?: boolean }) {
250
+ this.#invalidations.push({ generation: this.merged.generation, cleared, routePredicate, touched, batch });
251
+ this.#prune();
252
+ }
253
+
254
+ #prune(): void {
255
+ let oldest = Number.POSITIVE_INFINITY;
256
+ for (const pending of this.#building.values()) oldest = Math.min(oldest, pending.generation);
257
+ this.#invalidations = this.#invalidations.filter((invalidation) => invalidation.generation > oldest);
258
+ }
259
+
260
+ #overtaken(routeId: string, since: number, delta: BuildRouteClientResult): boolean {
261
+ const reached = RouteClientCache.#reachedOf(delta);
262
+ return this.#invalidations.some(
263
+ (invalidation) =>
264
+ invalidation.generation > since &&
265
+ !(invalidation.batch !== undefined && (delta.seenGeneration ?? -1) >= invalidation.batch) &&
266
+ (invalidation.cleared ||
267
+ invalidation.routePredicate(routeId) ||
268
+ (!delta.discoveredEntries && invalidation.touched.size > 0) ||
269
+ [...invalidation.touched].some((file) => reached.has(file))),
270
+ );
271
+ }
272
+
178
273
  #rebuildKnownEntriesPreservingManifest(generation: number, staleEntries: Set<string> = new Set()): void {
179
274
  const staleUrls = new Set<string>();
180
275
  for (const [key, row] of Object.entries(this.merged.clientManifest)) {
@@ -210,6 +305,10 @@ export class RouteClientCache {
210
305
  return filePath.split("\\").join("/");
211
306
  }
212
307
 
308
+ static #normalizeAll(filePaths: Iterable<string>): Set<string> {
309
+ return new Set([...filePaths].map((filePath) => RouteClientCache.#normalizePath(filePath)));
310
+ }
311
+
213
312
  static #manifestKeyMatchesEntries(key: string, entries: Set<string>): boolean {
214
313
  if (entries.size === 0) return false;
215
314
  const hashIdx = key.lastIndexOf("#");
@@ -118,7 +118,7 @@ export class DiLifecycle {
118
118
  constructor({ env, modules = [], disableModules = [], disableLibs = [] }: DiLifecycleProps, ...libs: AkanLib[]) {
119
119
  this.#env = env;
120
120
  const databaseMode = DiLifecycle.#databaseMode();
121
- this.logger.info(`Database mode: ${DatabaseModes.describe(databaseMode)}`);
121
+ this.logger.debug(`Database mode: ${DatabaseModes.describe(databaseMode)}`);
122
122
 
123
123
  this.#predefinedAdaptor = { ...getPredefinedAdaptor(databaseMode) };
124
124
  this.#libs = libs;