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.
- package/common/hmrPhase.ts +2 -0
- package/package.json +1 -1
- package/server/SSR_MEMORY_DIAGNOSIS.md +2 -2
- package/server/akanApp.ts +1 -0
- package/server/akanServer.ts +7 -1
- package/server/artifact/builderRpc.ts +13 -2
- package/server/artifact/devBuildRecovery.ts +12 -0
- package/server/artifact/index.ts +1 -0
- package/server/artifact/ipcTypes.ts +46 -3
- package/server/artifact/routeClientCache.ts +128 -29
- package/server/di/diLifecycle.ts +1 -1
- package/server/hmr/clientScript.ts +148 -86
- package/server/hmr/csrDevManifest.ts +17 -0
- package/server/hmr/csrDevRuntime.ts +238 -18
- package/server/hmr/csrDevShell.ts +90 -10
- package/server/hmr/devArtifactPruner.ts +77 -0
- package/server/hmr/devHmrController.ts +233 -137
- package/server/hmr/ssrDevShim.ts +112 -0
- package/server/hmr/ssrUpdateQueue.ts +111 -0
- package/server/hmr/wsHub.ts +19 -4
- package/server/routing/apiRouter.ts +12 -0
- package/server/rscWorker.tsx +11 -1
- package/server/rscWorkerHost.ts +223 -59
- package/server/systemPageDocument.tsx +6 -0
- package/server/types.tsx +4 -0
- package/server/webRouter.ts +57 -22
- package/types/common/hmrPhase.d.ts +2 -0
- package/types/server/artifact/builderRpc.d.ts +3 -2
- package/types/server/artifact/devBuildRecovery.d.ts +4 -0
- package/types/server/artifact/index.d.ts +1 -0
- package/types/server/artifact/ipcTypes.d.ts +47 -2
- package/types/server/artifact/routeClientCache.d.ts +9 -2
- package/types/server/hmr/csrDevManifest.d.ts +15 -0
- package/types/server/hmr/csrDevRuntime.d.ts +34 -0
- package/types/server/hmr/csrDevShell.d.ts +9 -1
- package/types/server/hmr/devArtifactPruner.d.ts +9 -0
- package/types/server/hmr/devHmrController.d.ts +8 -5
- package/types/server/hmr/ssrDevShim.d.ts +7 -0
- package/types/server/hmr/ssrUpdateQueue.d.ts +23 -0
- package/types/server/hmr/wsHub.d.ts +16 -7
- package/types/server/routing/apiRouter.d.ts +8 -0
- package/types/server/rscWorkerHost.d.ts +14 -2
- package/types/server/systemPageDocument.d.ts +2 -0
- package/types/server/types.d.ts +4 -0
- package/types/server/webRouter.d.ts +8 -2
package/common/hmrPhase.ts
CHANGED
|
@@ -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
|
@@ -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=
|
|
134
|
-
AKAN_BUILDER_MAX_RSS=
|
|
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
package/server/akanServer.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
}
|
package/server/artifact/index.ts
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
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(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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({
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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.#
|
|
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.#
|
|
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("#");
|
package/server/di/diLifecycle.ts
CHANGED
|
@@ -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.
|
|
121
|
+
this.logger.debug(`Database mode: ${DatabaseModes.describe(databaseMode)}`);
|
|
122
122
|
|
|
123
123
|
this.#predefinedAdaptor = { ...getPredefinedAdaptor(databaseMode) };
|
|
124
124
|
this.#libs = libs;
|