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
@@ -19,6 +19,7 @@ import {
19
19
  RouteClientCache,
20
20
  type RouteSeedIndex,
21
21
  RouteSeedIndexStore,
22
+ type RoutesManifest,
22
23
  RoutesManifestStore,
23
24
  } from "./artifact";
24
25
  import {
@@ -36,9 +37,10 @@ import {
36
37
  } from "./cachePolicy";
37
38
  import { encodedFileResponse } from "./contentEncoding";
38
39
  import { HMR_CLIENT_SCRIPT } from "./hmr/clientScript";
39
- import { CSR_DEV_ROUTE_PREFIX, resolveDevCsrMode } from "./hmr/csrDevManifest";
40
+ import { CSR_DEV_ROUTE_PREFIX, resolveDevCsrMode, SSR_DEV_DIRNAME, SSR_DEV_ROUTE_PREFIX } from "./hmr/csrDevManifest";
40
41
  import { CsrDevShell } from "./hmr/csrDevShell";
41
42
  import { DevHmrController } from "./hmr/devHmrController";
43
+ import { SsrDevShim } from "./hmr/ssrDevShim";
42
44
  import type { HmrWsData, HmrWsHub } from "./hmr/wsHub";
43
45
  import { ImageOptimizer } from "./imageOptimizer";
44
46
  import { normalizeHost, resolveArtifactDir, warnIgnoredSubRouteBasePaths } from "./proxy/hostBasePathWebProxy";
@@ -250,6 +252,8 @@ interface WebRouterOptions {
250
252
  rsc: RscWorker;
251
253
  seedIndex: RouteSeedIndex;
252
254
  upgradeHmrWs: (req: Request, data: HmrWsData) => boolean;
255
+ /** The production build's route manifest, which the worker was already handed at boot. */
256
+ prebuilt?: RoutesManifest | null;
253
257
  }
254
258
 
255
259
  interface CachedHtmlResult {
@@ -272,6 +276,8 @@ export class WebRouter {
272
276
  #routeCache: RouteClientCache;
273
277
  #devHmr: DevHmrController | null = null;
274
278
  #csrDevShell: CsrDevShell | null = null;
279
+ /** Dev only: SSR pages load their client code from the dev module registry it serves. */
280
+ #ssrDevShell: CsrDevShell | null = null;
275
281
  #csrArmed = false;
276
282
  #csrOnDemandBuild: Promise<unknown> | null = null;
277
283
  readonly #requestStats = {
@@ -297,7 +303,8 @@ export class WebRouter {
297
303
  /** What this router actually mounts, already intersected with what the artifact carries. */
298
304
  readonly web: AkanWebConfig;
299
305
  #seedIndex: RouteSeedIndex;
300
- constructor({ artifact, web, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs }: WebRouterOptions) {
306
+ readonly #prebuilt: RoutesManifest | null;
307
+ constructor({ artifact, web, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs, prebuilt = null }: WebRouterOptions) {
301
308
  this.#logger.verbose(`[SSR] loaded ${Object.keys(cssBytesByUrl).length} CSS assets`);
302
309
  this.web = web;
303
310
  if (process.env.NODE_ENV === "production" && !this.#prodMode)
@@ -317,6 +324,7 @@ export class WebRouter {
317
324
  cssBytesByUrl,
318
325
  };
319
326
  this.#seedIndex = seedIndex;
327
+ this.#prebuilt = prebuilt;
320
328
  if (this.#prodMode) {
321
329
  this.#builderRpc = null;
322
330
  this.#routeCache = new RouteClientCache({
@@ -333,25 +341,21 @@ export class WebRouter {
333
341
  rsc: this.#rsc,
334
342
  seedIndex: this.#seedIndex,
335
343
  upgradeHmrWs,
344
+ pagesBundlePath: this.#artifact.pagesBundlePath,
336
345
  });
337
346
  this.#builderRpc = this.#devHmr.builderRpc;
338
347
  this.#routeCache = this.#devHmr.routeCache;
339
348
  this.#hub = this.#devHmr.hub;
340
349
  if (resolveDevCsrMode() === "registry") this.#csrDevShell = new CsrDevShell(this.#artifactDir);
350
+ this.#ssrDevShell = new CsrDevShell(this.#artifactDir, {
351
+ dirName: SSR_DEV_DIRNAME,
352
+ routePrefix: SSR_DEV_ROUTE_PREFIX,
353
+ });
341
354
  }
342
355
  }
343
356
 
344
357
  async initializeRoute() {
345
- const prebuilt = this.#prodMode ? await RoutesManifestStore.read(this.#artifactDir) : null;
346
- if (prebuilt) {
347
- this.#routeCache.seed(prebuilt);
348
- await this.#rsc.reload({
349
- clientManifest: this.#mergeRuntimeManifest().clientManifest,
350
- cssAssets: this.renderState.cssAssets,
351
-
352
- buildId: this.#artifact.pagesBundleBuildId,
353
- });
354
- }
358
+ if (this.#prebuilt) this.#routeCache.seed(this.#prebuilt);
355
359
 
356
360
  const clientServePrefix = `/_akan/client`;
357
361
  const clientOutputDir = `${this.#artifactDir}/client`;
@@ -390,7 +394,7 @@ export class WebRouter {
390
394
  <script type="module" src="/csr.js"></script>
391
395
  </body>
392
396
  </html>`;
393
- return new Response(this.#withCsrHmr(htmlText), { headers: WebRouter.#htmlResponseHeaders(200) });
397
+ return new Response(this.#withCsrHmr(htmlText), { headers: this.#htmlResponseHeaders(200) });
394
398
  },
395
399
  ...(this.#csrDevShell
396
400
  ? {
@@ -402,6 +406,14 @@ export class WebRouter {
402
406
  : {}),
403
407
  }
404
408
  : {}),
409
+ ...(this.#ssrDevShell
410
+ ? {
411
+ [`${SSR_DEV_ROUTE_PREFIX}*`]: async (req: Request) => {
412
+ this.#requestStats.staticAsset += 1;
413
+ return (await this.#ssrDevShell?.serve(req)) ?? new Response("Not Found", { status: 404 });
414
+ },
415
+ }
416
+ : {}),
405
417
  [`${clientServePrefix}/*`]: async (req) => {
406
418
  this.#requestStats.staticAsset += 1;
407
419
  const url = new URL(req.url);
@@ -453,8 +465,6 @@ export class WebRouter {
453
465
  ? {
454
466
  "/_akan/hmr": (req: Request) =>
455
467
  this.#devHmr?.handleWs(req) ?? new Response("HMR unavailable", { status: 404 }),
456
- "/_akan/hmr/client-refresh": (req: Request) =>
457
- this.#devHmr?.handleClientRefresh(req) ?? new Response("HMR unavailable", { status: 404 }),
458
468
  }
459
469
  : {}),
460
470
  "/__rsc": async (req) => {
@@ -535,7 +545,7 @@ export class WebRouter {
535
545
  const csrHtml = await this.#resolveCsrHtml(csrOutputDir, url.pathname);
536
546
  if (!csrHtml) return this.#csrUnavailableResponse(url.pathname);
537
547
  const html = await Bun.file(csrHtml).text();
538
- return new Response(this.#withCsrHmr(html), { headers: WebRouter.#htmlResponseHeaders(200) });
548
+ return new Response(this.#withCsrHmr(html), { headers: this.#htmlResponseHeaders(200) });
539
549
  }
540
550
 
541
551
  const csrAssetPath = path.extname(url.pathname) ? resolveStaticPath(csrOutputDir, url.pathname) : null;
@@ -594,7 +604,7 @@ export class WebRouter {
594
604
  const htmlCacheEntry = htmlCacheDecision.entry;
595
605
  const cachedHtml = htmlCacheEntry ? this.#getCachedHtml(htmlCacheEntry.key) : null;
596
606
  if (cachedHtml) {
597
- const cachedHeaders = WebRouter.#htmlResponseHeaders(200);
607
+ const cachedHeaders = this.#htmlResponseHeaders(200);
598
608
  cachedHeaders.set("X-Akan-Cache", "HIT");
599
609
  return new Response(cachedHtml, { headers: cachedHeaders });
600
610
  }
@@ -612,6 +622,9 @@ export class WebRouter {
612
622
  rscResult.trace?.routeState
613
623
  ? `self.__AKAN_RSC_INITIAL_STATE__=${JSON.stringify(rscResult.trace.routeState)};`
614
624
  : "",
625
+ this.#ssrDevShell
626
+ ? SsrDevShim.script(await this.#ssrDevShell.readManifest(), Object.keys(this.#artifact.vendorMap))
627
+ : "",
615
628
  !this.#prodMode ? HMR_CLIENT_SCRIPT : "",
616
629
  ]
617
630
  .filter(Boolean)
@@ -632,7 +645,7 @@ export class WebRouter {
632
645
  },
633
646
  });
634
647
  const responseStatus = rscResult.status ?? 200;
635
- const responseHeaders = WebRouter.#htmlResponseHeaders(responseStatus);
648
+ const responseHeaders = this.#htmlResponseHeaders(responseStatus);
636
649
  if (req.method === "HEAD") {
637
650
  const headers = new Headers(responseHeaders);
638
651
  if (htmlCacheEntry && responseStatus === 200) headers.set("X-Akan-Cache", "MISS");
@@ -704,6 +717,12 @@ export class WebRouter {
704
717
  };
705
718
  return { renderEnvRoutes, hmrHub: this.#hub, builderRpc: this.#builderRpc };
706
719
  }
720
+ refreshHmrState(): void {
721
+ this.#devHmr?.refreshRegistryState();
722
+ }
723
+ hmrBuildErrors(): { phase: string }[] {
724
+ return this.#devHmr?.buildErrorMessages() ?? [];
725
+ }
707
726
  dispose() {
708
727
  this.#devHmr?.dispose();
709
728
  this.#devHmr = null;
@@ -793,6 +812,11 @@ export class WebRouter {
793
812
  }
794
813
 
795
814
  #getHtmlCacheEntry(req: Request, url: URL): { entry: RouteCacheEntry | null; reason?: string } {
815
+
816
+ if (this.#ssrDevShell) {
817
+ this.#htmlCacheBypass += 1;
818
+ return { entry: null, reason: "dev-registry" };
819
+ }
796
820
  const decision = resolvePublicRouteCacheEntryDecision({
797
821
  request: req,
798
822
  url,
@@ -893,6 +917,7 @@ export class WebRouter {
893
917
  stylesheetHref: this.#getStylesheetHref(req, new URL(req.url).pathname),
894
918
  showDetails: !this.#prodMode,
895
919
  error: err,
920
+ ...(this.#prodMode ? {} : { script: `self.__AKAN_HMR_SYSTEM_PAGE__=true;${HMR_CLIENT_SCRIPT}` }),
896
921
  });
897
922
  }
898
923
 
@@ -942,9 +967,9 @@ export class WebRouter {
942
967
  return this.renderState.cssAssets[basePath ?? ""]?.cssUrl ?? null;
943
968
  }
944
969
 
945
- static #htmlResponseHeaders(status: number): Headers {
970
+ #htmlResponseHeaders(status: number): Headers {
946
971
  const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
947
- if (status >= 400) headers.set("Cache-Control", "no-store");
972
+ if (status >= 400 || !this.#prodMode) headers.set("Cache-Control", "no-store");
948
973
  return WebRouter.#applySecurityHeaders(headers, { html: true });
949
974
  }
950
975
  #withCsrHmr(html: string): string {
@@ -965,7 +990,7 @@ export class WebRouter {
965
990
  cssHref: this.#getStylesheetHref(req, pathname),
966
991
  });
967
992
  if (!html) return this.#csrUnavailableResponse(pathname);
968
- return new Response(this.#withCsrHmr(html), { headers: WebRouter.#htmlResponseHeaders(200) });
993
+ return new Response(this.#withCsrHmr(html), { headers: this.#htmlResponseHeaders(200) });
969
994
  }
970
995
  async #resolveCsrHtml(csrOutputDir: string, pathname: string): Promise<string | null> {
971
996
  const resolved = WebRouter.#resolveCsrHtmlPath(csrOutputDir, pathname, this.#artifact);
@@ -1015,7 +1040,16 @@ export class WebRouter {
1015
1040
  const cssBytesByUrl = await WebRouter.#loadCssBytesByUrl(artifact, artifactDir);
1016
1041
  const prodMode = process.env.NODE_ENV === "production" && process.env.AKAN_COMMAND_TYPE !== "start";
1017
1042
 
1018
- const rsc = new RscWorker(artifact, { failBeforeReady: prodMode });
1043
+ const prebuilt = prodMode ? await RoutesManifestStore.read(artifactDir) : null;
1044
+ const rsc = new RscWorker(
1045
+ prebuilt
1046
+ ? {
1047
+ ...artifact,
1048
+ rscRuntimeClientManifest: { ...artifact.rscRuntimeClientManifest, ...prebuilt.clientManifest },
1049
+ }
1050
+ : artifact,
1051
+ { failBeforeReady: prodMode },
1052
+ );
1019
1053
  if (prodMode)
1020
1054
  void rsc.ready.catch((error: unknown) => {
1021
1055
  new Logger("WebRouter").error(`RSC worker failed to load the pages bundle: ${String(error)}`);
@@ -1030,6 +1064,7 @@ export class WebRouter {
1030
1064
  rsc,
1031
1065
  seedIndex,
1032
1066
  upgradeHmrWs,
1067
+ prebuilt,
1033
1068
  });
1034
1069
  }
1035
1070
 
@@ -2,5 +2,7 @@ export type AkanHmrPhase = "refresh-import" | "react-refresh" | null;
2
2
  declare global {
3
3
  var __AKAN_HMR_PHASE__: AkanHmrPhase | undefined;
4
4
  }
5
+ /** @deprecated Always `null`: the dev client that set the phase applies modules through its registry now. */
5
6
  export declare function getAkanHmrPhase(): AkanHmrPhase;
7
+ /** @deprecated Always `false`, for the reason `getAkanHmrPhase` is always `null`. */
6
8
  export declare function isAkanHmrApplying(): boolean;
@@ -1,4 +1,4 @@
1
- import type { BuildRouteClientResult, CsrUpdatedPayload, CssPayload, DevBuildStatus, PagesBundlePayload } from "./ipcTypes.d.ts";
1
+ import type { BuildRouteClientResult, CsrUpdatedPayload, CssPayload, DevBuildStatus, PagesBundlePayload, SsrUpdatedPayload } from "./ipcTypes.d.ts";
2
2
  export interface BuilderRpcEventHandlers {
3
3
  /** A watcher batch changed files: clear the route cache and broadcast a dev reload. */
4
4
  onInvalidate?: (event: {
@@ -8,8 +8,9 @@ export interface BuilderRpcEventHandlers {
8
8
  }) => void;
9
9
  onCssUpdated?: (css: CssPayload) => void;
10
10
  /** A fresh `pages-*.js` bundle: re-import it in the running worker (`RscWorker.reload`) rather than respawning. */
11
- onPagesUpdated?: (bundle: PagesBundlePayload) => void;
11
+ onPagesUpdated?: (bundle: PagesBundlePayload) => void | Promise<void>;
12
12
  onCsrUpdated?: (update: CsrUpdatedPayload) => void;
13
+ onSsrUpdated?: (update: SsrUpdatedPayload) => void;
13
14
  onBuildStatus?: (status: DevBuildStatus) => void;
14
15
  }
15
16
  /** One per backend process: subscribes to the IPC channel on construction and unsubscribes on `dispose()`. */
@@ -0,0 +1,4 @@
1
+ import type { DevBuildStatus } from "./ipcTypes.d.ts";
2
+ export declare class DevBuildRecovery {
3
+ static recovers(previous: DevBuildStatus | undefined, status: DevBuildStatus): boolean;
4
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./builderRpc.d.ts";
2
+ export * from "./devBuildRecovery.d.ts";
2
3
  export * from "./ipcTypes.d.ts";
3
4
  export * from "./routeClientCache.d.ts";
4
5
  export * from "./routeSeedIndexStore.d.ts";
@@ -18,6 +18,8 @@ export interface BuildRouteResultPayload {
18
18
  clientDepsByEntry?: Record<string, string[]>;
19
19
  routeId?: string;
20
20
  generation?: number;
21
+ /** The newest save batch whose files its client-entry discovery had taken in when it started. */
22
+ seenGeneration?: number;
21
23
  }
22
24
  /** Re-announces a recycled builder's artifact (not an edit); the host drops it when the hashed output did not move. */
23
25
  export type BuilderStateReason = "builder-recycle";
@@ -41,13 +43,15 @@ export interface DevChangePlan {
41
43
  actions: DevChangeAction[];
42
44
  reasonByFile: Record<string, string[]>;
43
45
  }
44
- export type BuildPhase = "scan" | "barrel" | "csr" | "pages" | "css" | "route" | "backend";
46
+ export type BuildPhase = "scan" | "barrel" | "csr" | "ssr" | "pages" | "css" | "route" | "backend";
45
47
  export interface DevBuildStatus {
46
48
  generation: number;
47
49
  phase: BuildPhase;
48
50
  ok: boolean;
49
51
  files: string[];
50
52
  message?: string;
53
+ /** The route a route build's status speaks for. */
54
+ scope?: string;
51
55
  }
52
56
  export type BuilderReq = {
53
57
  type: "build-route";
@@ -90,18 +94,33 @@ export type BuilderControl = {
90
94
  type: "builder-shutdown";
91
95
  reason: string;
92
96
  };
97
+ /** Epoch-ms marks a save picks up on its way to the page, so a slow update says which hop it waited in. */
98
+ export interface HmrTrace {
99
+ eventAt?: number;
100
+ flushAt?: number;
101
+ batchAt?: number;
102
+ spawnAt?: number;
103
+ workerStartAt?: number;
104
+ workerAt?: number;
105
+ patchAt?: number;
106
+ sentAt?: number;
107
+ broadcastAt?: number;
108
+ }
93
109
  export interface PagesBundlePayload {
94
110
  bundlePath: string;
95
111
  buildId: number;
96
112
  generation?: number;
97
113
  changedFiles?: string[];
98
114
  reason?: BuilderStateReason;
115
+ trace?: HmrTrace;
116
+ /** False when no changed file is one the server renders (a `"use client"` edit that kept its export names). */
117
+ serverTouched?: boolean;
99
118
  }
100
119
  /** `Bun.build` keeps native arenas `Bun.gc(true)` never frees (macOS returns none when idle): hence recycling. */
101
120
  export interface BuilderMetrics {
102
121
  /** A peak sampled when the queues drain; stale within seconds on Linux, so the host re-reads RSS from the OS. */
103
122
  rssBytes: number;
104
- /** The builder's newest generation; 0 until it has processed a watch batch since spawning. */
123
+ /** The builder's newest generation; a replacement builder continues from the last one its host saw. */
105
124
  generation: number;
106
125
  /** Work items completed since this builder spawned, so a host can require real work before recycling. */
107
126
  workCount: number;
@@ -113,6 +132,23 @@ export interface CsrUpdatedPayload {
113
132
  reason?: string;
114
133
  patchUrl?: string;
115
134
  changedIds?: string[];
135
+ trace?: HmrTrace;
136
+ }
137
+ /** A new generation of the SSR dev registry, where SSR pages load their client code from: a patch, or a reload. */
138
+ export interface SsrUpdatedPayload {
139
+ generation: number;
140
+ reload: boolean;
141
+ reason?: string;
142
+ patchUrl?: string;
143
+ changedIds?: string[];
144
+ trace?: HmrTrace;
145
+ /** The save also changed what the server renders: the tabs get this patch with that batch's RSC refresh. */
146
+ hold?: boolean;
147
+ /** The watch batch this patch came from, which the `pages-updated` releasing it names as its generation. */
148
+ batchGeneration?: number;
149
+ epoch?: number;
150
+ /** The registry's first build: no tab holds a module of it, so there is nothing to send them. */
151
+ first?: boolean;
116
152
  }
117
153
  export type BuilderEvent = {
118
154
  type: "builder-ready";
@@ -135,12 +171,19 @@ export type BuilderEvent = {
135
171
  } | {
136
172
  type: "csr-updated";
137
173
  data: CsrUpdatedPayload;
174
+ } | {
175
+ type: "ssr-updated";
176
+ data: SsrUpdatedPayload;
138
177
  } | {
139
178
  type: "build-status";
140
179
  data: DevBuildStatus;
141
180
  } | {
142
181
  type: "builder-metrics";
143
182
  data: BuilderMetrics;
183
+ }
184
+ /** The boot builds settled (the SSR registry's, and CSR's when the env arms it): their workers are a boot's peak. */
185
+ | {
186
+ type: "boot-armed";
144
187
  };
145
188
  export type BuilderMessage = BuilderReq | BuilderRes | BuilderCsrReq | BuilderCsrRes | BuilderControl | BuilderEvent;
146
189
  export interface ClientManifestEntry {
@@ -157,4 +200,6 @@ export interface BuildRouteClientResult {
157
200
  discoveredEntries?: string[];
158
201
  clientDeps: string[];
159
202
  clientDepsByEntry?: Record<string, string[]>;
203
+ /** The newest save batch whose files its client-entry discovery had taken in when it started. */
204
+ seenGeneration?: number;
160
205
  }
@@ -26,6 +26,10 @@ interface RouteClientCacheOptions {
26
26
  export interface InvalidateClientEntriesOptions {
27
27
  routePredicate: (routeId: string) => boolean;
28
28
  staleEntries: Iterable<string>;
29
+ /** The saved files behind it: a build still running that bundled one of them is built again. */
30
+ files?: Iterable<string>;
31
+ /** The save batch behind it: a route build whose discovery had taken that batch in is neither dropped nor rebuilt. */
32
+ batch?: number;
29
33
  }
30
34
  export declare class RouteClientCache {
31
35
  #private;
@@ -36,8 +40,11 @@ export declare class RouteClientCache {
36
40
  seed(manifest: RoutesManifest): void;
37
41
  ensure(routeId: string, seeds: string[]): Promise<MergedManifest>;
38
42
  snapshot(): MergedManifest;
39
- invalidate(predicate: (routeId: string) => boolean): string[];
40
- invalidateClientEntries({ routePredicate, staleEntries }: InvalidateClientEntriesOptions): string[];
43
+ invalidate(predicate: (routeId: string) => boolean, { files, batch }?: {
44
+ files?: Iterable<string>;
45
+ batch?: number;
46
+ }): string[];
47
+ invalidateClientEntries({ routePredicate, staleEntries, files, batch, }: InvalidateClientEntriesOptions): string[];
41
48
  clear(): string[];
42
49
  }
43
50
  export {};
@@ -2,13 +2,28 @@ export declare const CSR_DEV_DIRNAME = "csr-dev";
2
2
  export declare const CSR_DEV_ROUTE_PREFIX = "/_akan/csr-dev/";
3
3
  export declare const CSR_DEV_APP_FILE = "app.js";
4
4
  export declare const CSR_DEV_MANIFEST_FILE = "manifest.json";
5
+ /** Present only while the resident builder patches: a builder that dies holding it takes the patcher off. */
6
+ export declare const CSR_DEV_PATCHING_MARKER = ".patching";
7
+ /** Patches kept on disk behind the newest, and as many as a reconnecting tab catches up on before it reloads instead. */
8
+ export declare const CSR_DEV_KEPT_PATCHES = 40;
9
+ export declare const SSR_DEV_DIRNAME = "ssr-dev";
10
+ export declare const SSR_DEV_ROUTE_PREFIX = "/_akan/ssr-dev/";
11
+ /** The one chunk every SSR registry row names: loading it boots the registry in the tab. */
12
+ export declare const SSR_DEV_CHUNK = "ssr-dev";
13
+ /** Marks a row id as a registry module, so the dev shim tells it from the runtime rows that stay chunk URLs. */
14
+ export declare const SSR_DEV_ID_PREFIX = "ssr-dev:";
5
15
  export type DevCsrMode = "registry" | "artifact";
6
16
  export interface CsrDevManifest {
7
17
  version: 1;
8
18
  generation: number;
19
+ /** The generation `app.js` holds; a patch is announced before `app.js` catches up, so it may trail `generation`. */
20
+ appGeneration?: number;
9
21
  vendorFile: string;
10
22
  entries: Record<string, string>;
23
+ /** When the registry was last built whole: a tab from an earlier registry must reload, whatever its generation. */
24
+ epoch?: number;
11
25
  }
26
+ export declare const appGenerationOf: (manifest: CsrDevManifest) => number;
12
27
  /** Where each module's generated code starts in a combined file (`app.js`, a patch), for its source map. */
13
28
  export interface CsrDevLayout {
14
29
  lineCount: number;
@@ -26,17 +26,43 @@ export interface CsrDevRuntimeApi {
26
26
  generation: number;
27
27
  refresh: string;
28
28
  }): void;
29
+ /** No entry to run: an SSR page requires the modules its RSC payload names, after the bootstrap has run. */
30
+ startLibrary(options: LibraryStart): void;
31
+ /** Registers a module the page already loaded outside the registry (an import map vendor). */
32
+ provide(id: string, namespace: unknown): void;
33
+ require(id: string): unknown;
34
+ has(id: string): boolean;
35
+ /** Settles once a patch defines `id`: the RSC payload can name a module whose patch is still on its way. */
36
+ whenDefined(id: string): Promise<void>;
37
+ /** Settles once every update handed to `hot` so far is applied, its accept callbacks included. */
38
+ whenSettled(): Promise<void>;
29
39
  update(generation: number, factories: Record<string, CsrModuleFactory>): void;
30
40
  accept(ownerId: string, deps: string[], callback: CsrAcceptCallback): void;
31
41
  hot(message: CsrUpdateMessage): void;
42
+ /** Loads every patch after the newest generation handed to `hot` up to `generation`, from `<prefix>patch-<n>.js`. */
43
+ catchUp(generation: number, prefix: string): void;
32
44
  toESM(mod: unknown, isNodeMode?: number): unknown;
33
45
  reExport(target: object, mod: unknown, secondTarget?: object): object | undefined;
46
+ /** `target` is the newest generation handed to `hot`, applied or still loading. */
34
47
  inspect(): {
35
48
  generation: number;
49
+ target: number;
50
+ started: boolean;
51
+ failed: boolean;
36
52
  executed: string[];
37
53
  modules: number;
54
+ /** What the library app.js was built with; undefined for a CSR page. */
55
+ vendorFile?: string;
56
+ epoch?: number;
38
57
  };
39
58
  }
59
+ export interface LibraryStart {
60
+ generation: number;
61
+ refresh: string;
62
+ bootstrap: string;
63
+ vendorFile?: string;
64
+ epoch?: number;
65
+ }
40
66
  interface CsrScriptElement {
41
67
  src: string;
42
68
  dataset: Record<string, string | undefined>;
@@ -49,6 +75,8 @@ export interface CsrDevRuntimeHost {
49
75
  __AKAN_CSR_LAST_UPDATE__?: {
50
76
  generation: number;
51
77
  executed: string[];
78
+ appliedAt?: number;
79
+ refreshedAt?: number;
52
80
  };
53
81
  document: {
54
82
  currentScript: unknown;
@@ -63,6 +91,12 @@ export interface CsrDevRuntimeHost {
63
91
  console: {
64
92
  warn(...args: unknown[]): void;
65
93
  };
94
+ fetch?(url: string, init: {
95
+ method: string;
96
+ cache: "no-store";
97
+ }): Promise<{
98
+ ok: boolean;
99
+ }>;
66
100
  setTimeout(callback: () => void, ms: number): unknown;
67
101
  clearTimeout(handle: unknown): void;
68
102
  }
@@ -5,9 +5,17 @@ export interface CsrDevShellRenderOptions {
5
5
  title: string;
6
6
  cssHref: string | null;
7
7
  }
8
+ export interface CsrDevShellOptions {
9
+ /** The registry this serves: the CSR one by default, `ssr-dev` for the client code of SSR pages. */
10
+ dirName?: string;
11
+ routePrefix?: string;
12
+ appWaitMs?: number;
13
+ appPollMs?: number;
14
+ bootWaitMs?: number;
15
+ }
8
16
  export declare class CsrDevShell {
9
17
  #private;
10
- constructor(artifactDir: string);
18
+ constructor(artifactDir: string, { dirName, routePrefix, appWaitMs, appPollMs, bootWaitMs, }?: CsrDevShellOptions);
11
19
  readManifest(): Promise<CsrDevManifest | null>;
12
20
  render({ basePath, lang, title, cssHref }: CsrDevShellRenderOptions): Promise<string | null>;
13
21
  serve(req: Request): Promise<Response>;
@@ -0,0 +1,9 @@
1
+ import type { CssAsset } from "../types.d.ts";
2
+ export declare class DevArtifactPruner {
3
+ #private;
4
+ constructor(artifactDir: string, { graceMs }?: {
5
+ graceMs?: number;
6
+ });
7
+ prunePages(runningPath: string): Promise<void>;
8
+ pruneStyles(cssAssets: Record<string, CssAsset>): Promise<void>;
9
+ }
@@ -14,17 +14,20 @@ export interface DevHmrControllerOptions {
14
14
  rsc: RscWorker;
15
15
  seedIndex: RouteSeedIndex;
16
16
  upgradeHmrWs: (req: Request, data: HmrWsData) => boolean;
17
+ /** The pages bundle the RSC worker booted with: the base build's, however many pages builds came after it. */
18
+ pagesBundlePath?: string;
17
19
  }
18
20
  export declare class DevHmrController {
19
21
  #private;
20
22
  readonly routeCache: RouteClientCache;
21
- constructor({ artifactDir, renderState, rsc, seedIndex, upgradeHmrWs }: DevHmrControllerOptions);
23
+ constructor({ artifactDir, renderState, rsc, seedIndex, upgradeHmrWs, pagesBundlePath }: DevHmrControllerOptions);
24
+ refreshRegistryState(): void;
25
+ /** The phases failing now, for a socket that connects after their status went out (a page opened after a failed boot). */
26
+ buildErrorMessages(): Extract<HmrMessage, {
27
+ type: "build-status";
28
+ }>[];
22
29
  get hub(): HmrWsHub;
23
30
  get builderRpc(): BuilderRpc;
24
31
  dispose(): void;
25
32
  handleWs(req: Request): Response | undefined;
26
- handleClientRefresh(req: Request): Promise<Response>;
27
- ensureRoute(url: URL): Promise<import("..").MergedManifest>;
28
- routeIdsForPath(pathname: string): string[] | undefined;
29
- static clientChunkUrls(clientManifest: ClientManifest): string[];
30
33
  }
@@ -0,0 +1,7 @@
1
+ import { type CsrDevManifest } from "./csrDevManifest.d.ts";
2
+ export declare class SsrDevShim {
3
+ #private;
4
+ static readonly runtimeUrl: string;
5
+ /** `vendors` are the import map's specifiers: the registry requires them as `vendor:<specifier>`, never compiles them. */
6
+ static script(manifest: CsrDevManifest | null, vendors: string[]): string;
7
+ }
@@ -0,0 +1,23 @@
1
+ import type { SsrUpdatedPayload } from "../artifact.d.ts";
2
+ import type { HmrMessage } from "./wsHub.d.ts";
3
+ export type SsrUpdateMessage = Extract<HmrMessage, {
4
+ type: "ssr-update";
5
+ }>;
6
+ export declare class SsrUpdateQueue {
7
+ #private;
8
+ constructor(send: (message: SsrUpdateMessage) => void, { maxHoldMs }?: {
9
+ maxHoldMs?: number;
10
+ });
11
+ get size(): number;
12
+ push(update: SsrUpdatedPayload): void;
13
+ /**
14
+ * The pages batch of `generation` finished or failed: sends what it covers and everything queued behind that, and
15
+ * says whether a reload went out (the tabs then need no RSC refresh).
16
+ */
17
+ release(generation: number | undefined): {
18
+ released: number;
19
+ reload: boolean;
20
+ };
21
+ /** Drops what the pages batch of `generation` covers, or everything without one: the tabs are about to reload. */
22
+ clear(generation?: number): void;
23
+ }
@@ -1,8 +1,9 @@
1
- import type { BuildPhase } from "../artifact.d.ts";
1
+ import type { BuildPhase, HmrTrace } from "../artifact.d.ts";
2
2
  export type ChangeKind = "code" | "css" | "config" | "ignore";
3
3
  export interface ChangeBatch {
4
4
  files: string[];
5
5
  kinds: Set<Exclude<ChangeKind, "ignore">>;
6
+ trace?: HmrTrace;
6
7
  }
7
8
  export type HmrClientKind = "ssr" | "csr";
8
9
  export interface HmrWsData {
@@ -18,6 +19,10 @@ export type HmrMessage = {
18
19
  cssRelPath: string;
19
20
  }>;
20
21
  csrGeneration?: number;
22
+ ssrGeneration?: number;
23
+ ssrEpoch?: number;
24
+ /** The phases failing now, each sent right after: a tab drops an error it holds for any other. */
25
+ failingPhases?: string[];
21
26
  } | {
22
27
  type: "reload";
23
28
  buildId: number;
@@ -28,18 +33,22 @@ export type HmrMessage = {
28
33
  changedIds?: string[];
29
34
  reload?: boolean;
30
35
  reason?: string;
36
+ trace?: HmrTrace;
31
37
  } | {
32
- type: "rsc-refresh";
33
- buildId: number;
34
- generation?: number;
35
- changedFiles?: string[];
36
- routeIds?: string[];
38
+ type: "ssr-update";
39
+ generation: number;
40
+ url?: string;
41
+ changedIds?: string[];
42
+ reload?: boolean;
43
+ reason?: string;
44
+ trace?: HmrTrace;
37
45
  } | {
38
- type: "client-refresh";
46
+ type: "rsc-refresh";
39
47
  buildId: number;
40
48
  generation?: number;
41
49
  changedFiles?: string[];
42
50
  routeIds?: string[];
51
+ trace?: HmrTrace;
43
52
  } | {
44
53
  type: "css-update";
45
54
  cssAssets?: Record<string, {
@@ -12,7 +12,15 @@ export interface HmrStateSource {
12
12
  cssRelPath: string;
13
13
  }>;
14
14
  csrGeneration?: number;
15
+ ssrGeneration?: number;
16
+ ssrEpoch?: number;
15
17
  };
18
+ /** Called before each hello: brings what the state says about the dev registries up to what is on disk. */
19
+ refresh?: () => void;
20
+ /** Sent right after hello: the build statuses failing now, which the socket connected too late to hear. */
21
+ errors?: () => {
22
+ phase: string;
23
+ }[];
16
24
  }
17
25
  export type NonNullHttpRoutes = NonNullable<HttpRoutes>;
18
26
  export interface ApiRouteInputs {