akanjs 2.3.0 → 2.3.1-rc.1

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 (180) hide show
  1. package/base/primitiveRegistry.ts +19 -12
  2. package/client/csrTypes.ts +16 -0
  3. package/constant/fieldInfo.ts +11 -9
  4. package/constant/getDefault.ts +1 -1
  5. package/fetch/requestStorage.ts +5 -0
  6. package/package.json +4 -4
  7. package/server/akanApp.ts +26 -3
  8. package/server/akanServer.ts +5 -1
  9. package/server/cachePolicy.ts +99 -5
  10. package/server/imageOptimizer.ts +14 -1
  11. package/server/metadata.tsx +117 -33
  12. package/server/resolver/database.resolver.ts +4 -4
  13. package/server/routeElementComposer.tsx +46 -14
  14. package/server/routeState.ts +379 -0
  15. package/server/routeTreeBuilder.ts +3 -2
  16. package/server/rscClient.tsx +316 -46
  17. package/server/rscClientFetch.ts +57 -0
  18. package/server/rscClientPatch.ts +157 -0
  19. package/server/rscHeadPatch.ts +80 -0
  20. package/server/rscNavigationState.ts +315 -0
  21. package/server/rscPartialCommit.ts +3 -0
  22. package/server/rscPatchSafety.ts +57 -0
  23. package/server/rscSegmentOutlet.tsx +69 -0
  24. package/server/rscSegmentOutletReference.ts +24 -0
  25. package/server/rscWorker.tsx +380 -53
  26. package/server/rscWorkerCache.ts +180 -0
  27. package/server/rscWorkerHost.ts +40 -12
  28. package/server/rscWorkerReplay.ts +11 -2
  29. package/server/ssrFromRscRenderer.tsx +15 -10
  30. package/server/ssrTypes.ts +18 -0
  31. package/server/types.tsx +4 -0
  32. package/server/webRouter.ts +198 -42
  33. package/service/predefinedAdaptor/database.adaptor.ts +72 -25
  34. package/signal/signalContext.ts +1 -1
  35. package/types/base/primitiveRegistry.d.ts +6 -6
  36. package/types/client/csrTypes.d.ts +16 -0
  37. package/types/constant/fieldInfo.d.ts +8 -7
  38. package/types/fetch/requestStorage.d.ts +2 -0
  39. package/types/server/cachePolicy.d.ts +36 -0
  40. package/types/server/metadata.d.ts +10 -1
  41. package/types/server/routeElementComposer.d.ts +9 -1
  42. package/types/server/routeState.d.ts +94 -0
  43. package/types/server/rscClient.d.ts +1 -0
  44. package/types/server/rscClientFetch.d.ts +24 -0
  45. package/types/server/rscClientPatch.d.ts +21 -0
  46. package/types/server/rscHeadPatch.d.ts +12 -0
  47. package/types/server/rscNavigationState.d.ts +78 -0
  48. package/types/server/rscPartialCommit.d.ts +1 -0
  49. package/types/server/rscPatchSafety.d.ts +8 -0
  50. package/types/server/rscSegmentOutlet.d.ts +17 -0
  51. package/types/server/rscSegmentOutletReference.d.ts +2 -0
  52. package/types/server/rscWorker.d.ts +5 -0
  53. package/types/server/rscWorkerCache.d.ts +63 -0
  54. package/types/server/rscWorkerHost.d.ts +8 -4
  55. package/types/server/rscWorkerReplay.d.ts +3 -0
  56. package/types/server/ssrFromRscRenderer.d.ts +1 -0
  57. package/types/server/ssrTypes.d.ts +17 -0
  58. package/types/server/types.d.ts +4 -0
  59. package/types/server/webRouter.d.ts +7 -3
  60. package/types/service/predefinedAdaptor/database.adaptor.d.ts +6 -0
  61. package/types/ui/Button.d.ts +1 -1
  62. package/types/ui/ClientSide.d.ts +1 -1
  63. package/types/ui/Constant/Doc.d.ts +6 -6
  64. package/types/ui/Constant/Mermaid.d.ts +1 -1
  65. package/types/ui/Constant/index.d.ts +1 -1
  66. package/types/ui/Constant/schemaDoc.d.ts +1 -1
  67. package/types/ui/Copy.d.ts +1 -1
  68. package/types/ui/CsrImage.d.ts +1 -1
  69. package/types/ui/Data/CardList.d.ts +1 -1
  70. package/types/ui/Data/Dashboard.d.ts +1 -1
  71. package/types/ui/Data/Insight.d.ts +1 -1
  72. package/types/ui/Data/Item.d.ts +6 -6
  73. package/types/ui/Data/ListContainer.d.ts +1 -1
  74. package/types/ui/Data/Pagination.d.ts +1 -1
  75. package/types/ui/Data/TableList.d.ts +1 -1
  76. package/types/ui/DatePicker.d.ts +3 -3
  77. package/types/ui/Dialog/Close.d.ts +1 -1
  78. package/types/ui/Dialog/Content.d.ts +1 -1
  79. package/types/ui/Dialog/Provider.d.ts +1 -1
  80. package/types/ui/Dialog/Trigger.d.ts +1 -1
  81. package/types/ui/Dialog/index.d.ts +3 -3
  82. package/types/ui/DragAction.d.ts +4 -4
  83. package/types/ui/DraggableList.d.ts +3 -3
  84. package/types/ui/Dropdown.d.ts +1 -1
  85. package/types/ui/Empty.d.ts +1 -1
  86. package/types/ui/Field.d.ts +22 -22
  87. package/types/ui/Image.d.ts +1 -1
  88. package/types/ui/InfiniteScroll.d.ts +1 -1
  89. package/types/ui/Input.d.ts +6 -6
  90. package/types/ui/KeyboardAvoiding.d.ts +1 -1
  91. package/types/ui/Layout/BottomAction.d.ts +1 -1
  92. package/types/ui/Layout/BottomInset.d.ts +1 -1
  93. package/types/ui/Layout/BottomTab.d.ts +1 -1
  94. package/types/ui/Layout/Header.d.ts +1 -1
  95. package/types/ui/Layout/LeftSider.d.ts +1 -1
  96. package/types/ui/Layout/Navbar.d.ts +1 -1
  97. package/types/ui/Layout/RightSider.d.ts +1 -1
  98. package/types/ui/Layout/Sider.d.ts +1 -1
  99. package/types/ui/Layout/Template.d.ts +1 -1
  100. package/types/ui/Layout/TopLeftAction.d.ts +1 -1
  101. package/types/ui/Layout/Unit.d.ts +1 -1
  102. package/types/ui/Layout/View.d.ts +1 -1
  103. package/types/ui/Layout/Zone.d.ts +1 -1
  104. package/types/ui/Layout/index.d.ts +12 -12
  105. package/types/ui/Link/Back.d.ts +1 -1
  106. package/types/ui/Link/Close.d.ts +1 -1
  107. package/types/ui/Link/CsrLink.d.ts +1 -1
  108. package/types/ui/Link/Lang.d.ts +1 -1
  109. package/types/ui/Link/SsrLink.d.ts +1 -1
  110. package/types/ui/Link/index.d.ts +1 -1
  111. package/types/ui/Load/Edit.d.ts +1 -1
  112. package/types/ui/Load/Edit_Client.d.ts +1 -1
  113. package/types/ui/Load/PageCSR.d.ts +1 -1
  114. package/types/ui/Load/Pagination.d.ts +1 -1
  115. package/types/ui/Load/Units.d.ts +1 -1
  116. package/types/ui/Load/View.d.ts +1 -1
  117. package/types/ui/Loading/Area.d.ts +1 -1
  118. package/types/ui/Loading/Button.d.ts +1 -1
  119. package/types/ui/Loading/Input.d.ts +1 -1
  120. package/types/ui/Loading/ProgressBar.d.ts +1 -1
  121. package/types/ui/Loading/Skeleton.d.ts +1 -1
  122. package/types/ui/Loading/Spin.d.ts +1 -1
  123. package/types/ui/Loading/index.d.ts +6 -6
  124. package/types/ui/Menu.d.ts +1 -1
  125. package/types/ui/Modal.d.ts +1 -1
  126. package/types/ui/Model/AdminPanel.d.ts +1 -1
  127. package/types/ui/Model/Edit.d.ts +1 -1
  128. package/types/ui/Model/EditModal.d.ts +1 -1
  129. package/types/ui/Model/EditWrapper.d.ts +1 -1
  130. package/types/ui/Model/LoadInit.d.ts +1 -1
  131. package/types/ui/Model/New.d.ts +1 -1
  132. package/types/ui/Model/NewWrapper.d.ts +1 -1
  133. package/types/ui/Model/NewWrapper_Client.d.ts +1 -1
  134. package/types/ui/Model/Remove.d.ts +1 -1
  135. package/types/ui/Model/RemoveWrapper.d.ts +1 -1
  136. package/types/ui/Model/SureToRemove.d.ts +1 -1
  137. package/types/ui/Model/View.d.ts +1 -1
  138. package/types/ui/Model/ViewEditModal.d.ts +1 -1
  139. package/types/ui/Model/ViewModal.d.ts +1 -1
  140. package/types/ui/Model/ViewWrapper.d.ts +1 -1
  141. package/types/ui/More.d.ts +1 -1
  142. package/types/ui/ObjectId.d.ts +1 -1
  143. package/types/ui/Popconfirm.d.ts +1 -1
  144. package/types/ui/Radio.d.ts +2 -2
  145. package/types/ui/RecentTime.d.ts +1 -1
  146. package/types/ui/Refresh.d.ts +1 -1
  147. package/types/ui/ScreenNavigator.d.ts +3 -3
  148. package/types/ui/Select.d.ts +1 -1
  149. package/types/ui/Signal/Arg.d.ts +13 -13
  150. package/types/ui/Signal/Doc.d.ts +6 -6
  151. package/types/ui/Signal/Listener.d.ts +2 -2
  152. package/types/ui/Signal/Message.d.ts +4 -4
  153. package/types/ui/Signal/Object.d.ts +4 -4
  154. package/types/ui/Signal/PubSub.d.ts +4 -4
  155. package/types/ui/Signal/Request.d.ts +2 -2
  156. package/types/ui/Signal/Response.d.ts +3 -3
  157. package/types/ui/Signal/RestApi.d.ts +5 -5
  158. package/types/ui/Signal/WebSocket.d.ts +2 -2
  159. package/types/ui/System/CSR.d.ts +5 -5
  160. package/types/ui/System/Client.d.ts +8 -8
  161. package/types/ui/System/Common.d.ts +2 -2
  162. package/types/ui/System/DevModeToggle.d.ts +1 -1
  163. package/types/ui/System/Gtag.d.ts +1 -1
  164. package/types/ui/System/Messages.d.ts +1 -1
  165. package/types/ui/System/Reconnect.d.ts +1 -1
  166. package/types/ui/System/Root.d.ts +1 -1
  167. package/types/ui/System/SSR.d.ts +4 -4
  168. package/types/ui/System/SelectLanguage.d.ts +1 -1
  169. package/types/ui/System/ThemeToggle.d.ts +1 -1
  170. package/types/ui/System/index.d.ts +7 -7
  171. package/types/ui/Tab/Menu.d.ts +1 -1
  172. package/types/ui/Tab/Menus.d.ts +1 -1
  173. package/types/ui/Tab/Panel.d.ts +1 -1
  174. package/types/ui/Tab/Provider.d.ts +1 -1
  175. package/types/ui/Tab/index.d.ts +4 -4
  176. package/types/ui/Table.d.ts +1 -1
  177. package/types/ui/ToggleSelect.d.ts +2 -2
  178. package/types/ui/Unauthorized.d.ts +1 -1
  179. package/ui/Constant/schemaDoc.ts +1 -1
  180. package/server/resolver/resolver.contract.fixture.ts +0 -222
@@ -4,6 +4,7 @@ import type {
4
4
  LayoutFallbackRoute,
5
5
  PathRoute,
6
6
  RedirectStatus,
7
+ ResolvedHead,
7
8
  } from "akanjs/client";
8
9
  import { type AkanI18nConfig, DEFAULT_AKAN_I18N, getBasePathFromPathname, Logger } from "akanjs/common";
9
10
  import {
@@ -19,23 +20,50 @@ import type { ReactNode } from "react";
19
20
  import { renderToReadableStream } from "react-server-dom-webpack/server.node";
20
21
  import type { ClientManifest } from "./artifact";
21
22
  import {
22
- createRouteCacheEntry,
23
- isPublicRouteCacheableRequest,
24
- isRouteCachePathAllowed,
25
23
  LruTtlCache,
26
24
  parsePositiveInt,
27
25
  type RouteCacheEntry,
26
+ type RouteCacheInvalidation,
28
27
  type RouteCacheRenderState,
29
- resolveAutoRouteCacheTtl,
28
+ resolvePublicRouteCacheEntryDecision,
30
29
  resolveRouteCacheStoreTtl,
31
30
  shouldStoreRouteCache,
32
31
  } from "./cachePolicy";
33
- import { shouldRenderLocaleAlternates } from "./metadata";
32
+ import {
33
+ createAkanLocaleAlternateHeadSnapshot,
34
+ mergeAkanHeadSnapshots,
35
+ renderAkanHeadSnapshot,
36
+ shouldRenderLocaleAlternates,
37
+ } from "./metadata";
34
38
  import { ProcessMetricsCollector } from "./processMetricsCollector";
35
39
  import { RouteElementComposer } from "./routeElementComposer";
40
+ import {
41
+ type AkanRscPatchDecision,
42
+ createAkanRouterState,
43
+ encodeAkanHeadSnapshot,
44
+ encodeAkanRouterState,
45
+ encodeAkanRscPatchSegmentPath,
46
+ readAkanRouterStateRequest,
47
+ resolveAkanRscPartialDecision,
48
+ resolveAkanRscPatchDecision,
49
+ } from "./routeState";
36
50
  import { type PagesContext, RouteTreeBuilder } from "./routeTreeBuilder";
37
51
  import { encodeAkanRedirectDigest } from "./rscHttp";
52
+ import { isAkanRscPartialCommitEnabled } from "./rscPartialCommit";
53
+ import { resolveAkanRscHeadSafePatchDecision } from "./rscPatchSafety";
54
+ import {
55
+ type CachedRscResult,
56
+ createCachedRscPatchMetadata,
57
+ createRscWorkerCachedPatchReplayDecision,
58
+ invalidateCachedRscResults,
59
+ isCachedRscPatchMetadataCompatible,
60
+ resolveRscWorkerPatchCacheEntry,
61
+ shouldCollectRscWorkerRenderChunks,
62
+ shouldStoreRscWorkerPatchResult,
63
+ shouldUseRscWorkerFullResultCache,
64
+ } from "./rscWorkerCache";
38
65
  import { replayCachedRscResult } from "./rscWorkerReplay";
66
+ import type { RscTraceMetadata } from "./ssrTypes";
39
67
  import { createSystemPageDocument, getSystemPageHomeHref } from "./systemPages";
40
68
 
41
69
  interface InitMsg {
@@ -74,6 +102,8 @@ interface UpdateCssAssetsMsg {
74
102
  interface InvalidateCacheMsg {
75
103
  type: "invalidate-cache";
76
104
  reason?: string;
105
+ tags?: string[];
106
+ paths?: string[];
77
107
  }
78
108
  type InMsg = InitMsg | RenderMsg | CancelMsg | ReloadMsg | UpdateCssAssetsMsg | InvalidateCacheMsg;
79
109
  type RenderControl =
@@ -89,6 +119,12 @@ interface FlightRenderResult {
89
119
  cancelled: boolean;
90
120
  }
91
121
 
122
+ function hashRscTraceCacheKey(cacheKey: string): string {
123
+ let hash = 5381;
124
+ for (let index = 0; index < cacheKey.length; index += 1) hash = (hash * 33) ^ cacheKey.charCodeAt(index);
125
+ return (hash >>> 0).toString(36);
126
+ }
127
+
92
128
  interface RscRendererStats {
93
129
  renderCount: number;
94
130
  inFlightRenderCount: number;
@@ -112,14 +148,6 @@ interface RouteRenderStats {
112
148
  totalDurationMs: number;
113
149
  }
114
150
 
115
- interface CachedRscResult {
116
- chunks: Uint8Array[];
117
- bytes: number;
118
- chunksCount: number;
119
- theme?: string;
120
- cacheState: RouteCacheRenderState;
121
- }
122
-
123
151
  export function isAkanRedirectError(error: unknown): error is AkanRedirectError {
124
152
  return (
125
153
  typeof error === "object" &&
@@ -142,7 +170,7 @@ export function isAkanNotFoundError(error: unknown): error is AkanNotFoundError
142
170
  );
143
171
  }
144
172
 
145
- class RscRenderer {
173
+ export class RscRenderer {
146
174
  readonly #logger = new Logger("scWorker");
147
175
  #clientManifest: ClientManifest = {};
148
176
  #pathRoutes: PathRoute[] = [];
@@ -169,6 +197,9 @@ class RscRenderer {
169
197
  #resultCache = new LruTtlCache<CachedRscResult>(
170
198
  parsePositiveInt(process.env.AKAN_RSC_RESULT_CACHE_MAX_ENTRIES) ?? 100,
171
199
  );
200
+ #patchResultCache = new LruTtlCache<CachedRscResult>(
201
+ parsePositiveInt(process.env.AKAN_RSC_RESULT_CACHE_MAX_ENTRIES) ?? 100,
202
+ );
172
203
  readonly #activeRenderReaders = new Map<string, ReadableStreamDefaultReader<Uint8Array>>();
173
204
  readonly #cancelledRenderRequests = new Set<string>();
174
205
  #resultCacheHits = 0;
@@ -215,7 +246,7 @@ class RscRenderer {
215
246
  return;
216
247
  case "invalidate-cache":
217
248
  this.#logger.verbose(`received invalidate-cache reason=${msg.reason ?? "(none)"}`);
218
- this.#resultCache.clear();
249
+ this.#invalidateResultCache(msg);
219
250
  return;
220
251
  }
221
252
  }
@@ -228,6 +259,11 @@ class RscRenderer {
228
259
  });
229
260
  }
230
261
 
262
+ #invalidateResultCache(invalidation: RouteCacheInvalidation): void {
263
+ invalidateCachedRscResults(this.#resultCache, invalidation);
264
+ invalidateCachedRscResults(this.#patchResultCache, invalidation);
265
+ }
266
+
231
267
  async #handleInit(msg: InitMsg): Promise<void> {
232
268
  const startedAt = Date.now();
233
269
  try {
@@ -240,6 +276,7 @@ class RscRenderer {
240
276
  this.#stats.pagesBundleBuildId = msg.pagesBundleBuildId;
241
277
  this.#routeStats.clear();
242
278
  this.#resultCache.clear();
279
+ this.#patchResultCache.clear();
243
280
  this.#logger.verbose(
244
281
  `init state pagesBundlePath=${msg.pagesBundlePath} buildId=${msg.pagesBundleBuildId} cssAssets=${Object.keys(this.#cssAssets).length} clientEntries=${Object.keys(msg.clientManifest).length}`,
245
282
  );
@@ -284,6 +321,7 @@ class RscRenderer {
284
321
  this.#fallbackRoutes = routes.fallbackRoutes;
285
322
  this.#routeStats.clear();
286
323
  this.#resultCache.clear();
324
+ this.#patchResultCache.clear();
287
325
  this.#logger.verbose(`reload complete buildId=${msg.buildId} in ${Date.now() - startedAt}ms`);
288
326
  this.#send({ type: "reloaded", buildId: msg.buildId });
289
327
  } catch (error) {
@@ -348,8 +386,119 @@ class RscRenderer {
348
386
  );
349
387
  else this.#logger.verbose(`render[${requestId}] no route matched pathname=${urlObj.pathname} — rendering 404`);
350
388
  const beforeLoadedKeys = RouteTreeBuilder.getCacheStats().loadedModuleKeys;
351
- const cacheEntry = match ? this.#getResultCacheEntry(request, urlObj) : null;
352
- const cached = cacheEntry ? this.#getCachedResult(cacheEntry.key) : null;
389
+ const cacheDecision = match ? this.#getResultCacheEntry(request, urlObj) : { entry: null };
390
+ const cacheEntry = cacheDecision.entry;
391
+ const targetRouterState = match
392
+ ? createAkanRouterState({
393
+ pathRoute: match.pathRoute,
394
+ href: urlObj.href,
395
+ buildId: this.#pagesBundleBuildId,
396
+ })
397
+ : null;
398
+ const searchParams = RouteTreeBuilder.parseSearchParams(urlObj.search);
399
+ const currentRouterState = readAkanRouterStateRequest(request.headers);
400
+ const partialDecision = targetRouterState
401
+ ? resolveAkanRscPartialDecision({
402
+ currentState: currentRouterState.state,
403
+ currentRoute: currentRouterState.currentRoute,
404
+ targetState: targetRouterState,
405
+ })
406
+ : { status: "full" as const, reason: "missing-route", commonPrefixLength: 0 };
407
+ const patchDecision: AkanRscPatchDecision =
408
+ targetRouterState && match
409
+ ? resolveAkanRscPatchDecision({
410
+ currentState: currentRouterState.state,
411
+ targetState: targetRouterState,
412
+ partialDecision,
413
+ })
414
+ : { status: "full" as const, reason: partialDecision.reason, commonPrefixLength: 0 };
415
+ const safePatchDecision = match
416
+ ? await this.#resolveHeadSafePatchDecision(
417
+ match.pathRoute,
418
+ patchDecision,
419
+ await this.#resolveRouteHeadSnapshot(urlObj, match, searchParams),
420
+ )
421
+ : patchDecision;
422
+ const patchCacheEntry = resolveRscWorkerPatchCacheEntry({
423
+ cacheEntry,
424
+ targetRouterState,
425
+ safePatchDecision,
426
+ partialCommitEnabled: isAkanRscPartialCommitEnabled(),
427
+ });
428
+ const createTraceBase = (
429
+ decision: AkanRscPatchDecision,
430
+ cacheKey = cacheEntry?.key,
431
+ routeState = targetRouterState,
432
+ ) => ({
433
+ navId: requestId,
434
+ pathname: urlObj.pathname,
435
+ routeId,
436
+ partial: decision.status,
437
+ partialReason: decision.reason ?? currentRouterState.reason,
438
+ partialCommonPrefixLength: decision.commonPrefixLength,
439
+ ...(decision.patch
440
+ ? {
441
+ patchStartIndex: decision.patch.patchStartIndex,
442
+ patchSegmentPath: encodeAkanRscPatchSegmentPath(decision.patch.segmentPath),
443
+ patchStartSegment: decision.patch.patchStartSegmentKey,
444
+ patchHeadSafe: decision.patch.headSafe,
445
+ patchHeadSnapshot: decision.patch.headSnapshot
446
+ ? (encodeAkanHeadSnapshot(decision.patch.headSnapshot) ?? undefined)
447
+ : undefined,
448
+ }
449
+ : {}),
450
+ ...(routeState ? { routeState: encodeAkanRouterState(routeState) } : {}),
451
+ ...(cacheKey ? { cacheKeyHash: hashRscTraceCacheKey(cacheKey) } : {}),
452
+ ...(cacheDecision.reason ? { cacheReason: cacheDecision.reason } : {}),
453
+ });
454
+ const traceBase = createTraceBase(safePatchDecision, patchCacheEntry?.key ?? cacheEntry?.key);
455
+ const cachedPatch = patchCacheEntry ? this.#getCachedPatchResult(patchCacheEntry.key) : null;
456
+ if (
457
+ cachedPatch?.patch &&
458
+ patchCacheEntry &&
459
+ isCachedRscPatchMetadataCompatible({
460
+ cached: cachedPatch.patch,
461
+ targetRouterState,
462
+ safePatchDecision,
463
+ })
464
+ ) {
465
+ const cachedPatchDecision = createRscWorkerCachedPatchReplayDecision({
466
+ cached: cachedPatch.patch,
467
+ safePatchDecision,
468
+ });
469
+ const cachedTraceBase = createTraceBase(
470
+ cachedPatchDecision,
471
+ patchCacheEntry.key,
472
+ cachedPatch.patch.targetRouterState,
473
+ );
474
+ this.#stats.lastRenderDurationMs = Date.now() - startedAt;
475
+ this.#stats.lastRenderLoadedModuleDelta = 0;
476
+ this.#stats.lastRenderLoadedModules = [];
477
+ this.#stats.lastFlightBytes = cachedPatch.bytes;
478
+ this.#stats.lastFlightChunks = cachedPatch.chunksCount;
479
+ this.#stats.totalFlightBytes += cachedPatch.bytes;
480
+ this.#stats.totalFlightChunks += cachedPatch.chunksCount;
481
+ this.#recordRouteStats(routeId, cachedPatch.bytes, this.#stats.lastRenderDurationMs);
482
+ await replayCachedRscResult({
483
+ requestId,
484
+ chunks: cachedPatch.chunks,
485
+ theme: cachedPatch.theme,
486
+ cacheState: cachedPatch.cacheState,
487
+ trace: {
488
+ ...cachedTraceBase,
489
+ cache: "hit",
490
+ partial: "patch",
491
+ partialReason: "cache-hit-patch-replay",
492
+ },
493
+ send: (message) => this.#send(message),
494
+ isCancelled: () => this.#cancelledRenderRequests.has(requestId),
495
+ });
496
+ return;
497
+ }
498
+ const cached =
499
+ shouldUseRscWorkerFullResultCache({ cacheEntry, patchCacheEntry }) && cacheEntry
500
+ ? this.#getCachedResult(cacheEntry.key)
501
+ : null;
353
502
  if (cached) {
354
503
  this.#stats.lastRenderDurationMs = Date.now() - startedAt;
355
504
  this.#stats.lastRenderLoadedModuleDelta = 0;
@@ -364,21 +513,59 @@ class RscRenderer {
364
513
  chunks: cached.chunks,
365
514
  theme: cached.theme,
366
515
  cacheState: cached.cacheState,
516
+ trace: {
517
+ ...traceBase,
518
+ cache: "hit",
519
+ partial: "full",
520
+ partialReason: "cache-hit-full-replay",
521
+ partialCommonPrefixLength: 0,
522
+ patchStartIndex: undefined,
523
+ patchSegmentPath: undefined,
524
+ patchStartSegment: undefined,
525
+ patchHeadSafe: undefined,
526
+ patchHeadSnapshot: undefined,
527
+ },
367
528
  send: (message) => this.#send(message),
368
529
  isCancelled: () => this.#cancelledRenderRequests.has(requestId),
369
530
  });
370
531
  return;
371
532
  }
372
533
  const theme = untrackedCookies().get("theme")?.value;
373
- const searchParams = RouteTreeBuilder.parseSearchParams(urlObj.search);
374
534
  let element: ReactNode;
375
- if (match) element = await this.#renderMatched(urlObj, match, theme, searchParams);
535
+ let effectivePatchDecision = safePatchDecision;
536
+ if (match && safePatchDecision.status === "patch" && safePatchDecision.patch) {
537
+ const suffixElement = await this.#renderMatchedSuffix(
538
+ urlObj,
539
+ match,
540
+ safePatchDecision.patch.patchStartIndex,
541
+ searchParams,
542
+ );
543
+ if (suffixElement === null) {
544
+ effectivePatchDecision = {
545
+ status: "full",
546
+ reason: "suffix-compose-fallback",
547
+ commonPrefixLength: safePatchDecision.commonPrefixLength,
548
+ };
549
+ element = await this.#renderMatched(urlObj, match, theme, searchParams);
550
+ } else element = suffixElement;
551
+ } else if (match) element = await this.#renderMatched(urlObj, match, theme, searchParams);
376
552
  else element = await this.#renderNotFound(urlObj);
553
+ const traceCacheKey =
554
+ effectivePatchDecision.status === "patch" ? (patchCacheEntry?.key ?? cacheEntry?.key) : cacheEntry?.key;
555
+ const trace: RscTraceMetadata = {
556
+ ...createTraceBase(effectivePatchDecision, traceCacheKey),
557
+ cache: cacheEntry ? "miss" : "bypass",
558
+ };
377
559
  this.#logger.verbose(`render[${requestId}] starting Flight stream`);
378
560
  const result = await this.#renderFlightElement(element, msg.clientManifest ?? this.#clientManifest, {
379
561
  requestId,
380
- collectChunks: cacheEntry !== null,
562
+ collectChunks: shouldCollectRscWorkerRenderChunks({
563
+ cacheEntry,
564
+ effectivePatchDecision,
565
+ patchCacheEntry,
566
+ }),
381
567
  status: match ? undefined : 404,
568
+ trace,
382
569
  onComplete: ({ chunks, bytes, chunksCount, control, lateControlSent }) => {
383
570
  const cacheState = shouldStoreRouteCache({
384
571
  policy: getRequestPolicy(),
@@ -387,13 +574,46 @@ class RscRenderer {
387
574
  lateRedirect: control?.type === "redirect" && lateControlSent,
388
575
  });
389
576
  const storeTtl = cacheEntry ? resolveRouteCacheStoreTtl(cacheEntry.ttl, cacheState) : null;
390
- if (cacheEntry && storeTtl !== null) {
577
+ if (
578
+ shouldStoreRscWorkerPatchResult({
579
+ cacheEntry,
580
+ patchCacheEntry,
581
+ effectivePatchDecision,
582
+ storeTtl,
583
+ }) &&
584
+ patchCacheEntry &&
585
+ targetRouterState &&
586
+ effectivePatchDecision.patch &&
587
+ storeTtl !== null
588
+ ) {
589
+ this.#setCachedPatchResult(
590
+ patchCacheEntry.key,
591
+ {
592
+ chunks,
593
+ bytes,
594
+ chunksCount,
595
+ pathname: urlObj.pathname,
596
+ routeId,
597
+ tags: cacheState.tags,
598
+ theme: getRequestTheme(),
599
+ cacheState,
600
+ patch: createCachedRscPatchMetadata({
601
+ targetRouterState,
602
+ patch: effectivePatchDecision.patch,
603
+ }),
604
+ },
605
+ storeTtl,
606
+ );
607
+ } else if (cacheEntry && storeTtl !== null && effectivePatchDecision.status !== "patch") {
391
608
  this.#setCachedResult(
392
609
  cacheEntry.key,
393
610
  {
394
611
  chunks,
395
612
  bytes,
396
613
  chunksCount,
614
+ pathname: urlObj.pathname,
615
+ routeId,
616
+ tags: cacheState.tags,
397
617
  theme: getRequestTheme(),
398
618
  cacheState,
399
619
  },
@@ -415,7 +635,7 @@ class RscRenderer {
415
635
  const systemResult = await this.#renderFlightElement(
416
636
  this.#renderSystemNotFound(urlObj),
417
637
  msg.clientManifest ?? this.#clientManifest,
418
- { requestId, status: 404 },
638
+ { requestId, status: 404, trace },
419
639
  );
420
640
  if (systemResult.cancelled) return;
421
641
  if (!systemResult.control) {
@@ -435,6 +655,7 @@ class RscRenderer {
435
655
  url: urlObj,
436
656
  error: control.type === "error" ? control.error : undefined,
437
657
  clientManifest: msg.clientManifest ?? this.#clientManifest,
658
+ trace,
438
659
  }))
439
660
  ) {
440
661
  return;
@@ -445,6 +666,7 @@ class RscRenderer {
445
666
  requestId,
446
667
  url: urlObj,
447
668
  clientManifest: msg.clientManifest ?? this.#clientManifest,
669
+ trace,
448
670
  }))
449
671
  ) {
450
672
  return;
@@ -583,7 +805,7 @@ class RscRenderer {
583
805
  rscLoadedRouteModuleKeys: routeStats.loadedModuleKeys,
584
806
  rscTopRoutesByRenderCount: this.#topRoutes((route) => route.count),
585
807
  rscTopRoutesByFlightBytes: this.#topRoutes((route) => route.flightBytes),
586
- rscResultCacheEntries: this.#resultCache.size,
808
+ rscResultCacheEntries: this.#resultCache.size + this.#patchResultCache.size,
587
809
  rscResultCacheHits: this.#resultCacheHits,
588
810
  rscResultCacheMisses: this.#resultCacheMisses,
589
811
  rscResultCacheBypass: this.#resultCacheBypass,
@@ -598,6 +820,7 @@ class RscRenderer {
598
820
  requestId?: string;
599
821
  collectChunks?: boolean;
600
822
  status?: number;
823
+ trace?: RscTraceMetadata;
601
824
  onComplete?: (result: {
602
825
  chunks: Uint8Array[];
603
826
  bytes: number;
@@ -642,7 +865,13 @@ class RscRenderer {
642
865
  const sendMeta = () => {
643
866
  if (!options.requestId || sentMeta) return;
644
867
  sentMeta = true;
645
- this.#send({ type: "meta", requestId: options.requestId, theme: getRequestTheme(), status: options.status });
868
+ this.#send({
869
+ type: "meta",
870
+ requestId: options.requestId,
871
+ theme: getRequestTheme(),
872
+ status: options.status,
873
+ trace: options.trace,
874
+ });
646
875
  };
647
876
  const sendLateRedirect = () => {
648
877
  if (!options.requestId || lateControlSent || controlRef.current?.type !== "redirect") return;
@@ -723,6 +952,7 @@ class RscRenderer {
723
952
  url,
724
953
  error,
725
954
  clientManifest,
955
+ trace,
726
956
  }: {
727
957
  requestId: string;
728
958
  kind: "not-found" | "error";
@@ -733,6 +963,7 @@ class RscRenderer {
733
963
  url: URL;
734
964
  error?: unknown;
735
965
  clientManifest: ClientManifest;
966
+ trace?: RscTraceMetadata;
736
967
  }): Promise<boolean> {
737
968
  try {
738
969
  const element = await this.#renderFallbackDocument({
@@ -749,6 +980,7 @@ class RscRenderer {
749
980
  const result = await this.#renderFlightElement(element, clientManifest, {
750
981
  requestId,
751
982
  status: kind === "not-found" ? 404 : 500,
983
+ trace,
752
984
  });
753
985
  if (result.cancelled) return true;
754
986
  if (result.control) return false;
@@ -771,15 +1003,18 @@ class RscRenderer {
771
1003
  requestId,
772
1004
  url,
773
1005
  clientManifest,
1006
+ trace,
774
1007
  }: {
775
1008
  requestId: string;
776
1009
  url: URL;
777
1010
  clientManifest: ClientManifest;
1011
+ trace?: RscTraceMetadata;
778
1012
  }): Promise<boolean> {
779
1013
  try {
780
1014
  const result = await this.#renderFlightElement(this.#renderSystemNotFound(url), clientManifest, {
781
1015
  requestId,
782
1016
  status: 404,
1017
+ trace,
783
1018
  });
784
1019
  if (result.cancelled) return true;
785
1020
  if (result.control) return false;
@@ -820,6 +1055,25 @@ class RscRenderer {
820
1055
  this.#send({ type: "not-found", requestId });
821
1056
  }
822
1057
 
1058
+ async #resolveHeadSafePatchDecision(
1059
+ pathRoute: PathRoute,
1060
+ patchDecision: AkanRscPatchDecision,
1061
+ headSnapshot: ResolvedHead["headSnapshot"],
1062
+ ): Promise<AkanRscPatchDecision> {
1063
+ if (patchDecision.status !== "patch" || !patchDecision.patch) {
1064
+ return patchDecision;
1065
+ }
1066
+ if (!isAkanRscPartialCommitEnabled()) {
1067
+ return { status: "full", reason: "guard-disabled", commonPrefixLength: patchDecision.commonPrefixLength };
1068
+ }
1069
+ return resolveAkanRscHeadSafePatchDecision({
1070
+ partialCommitEnabled: true,
1071
+ patchDecision,
1072
+ pageConfig: await pathRoute.renderPage.getPageConfig?.(),
1073
+ headSnapshot,
1074
+ });
1075
+ }
1076
+
823
1077
  #recordRouteStats(routeId: string, flightBytes: number, durationMs: number): void {
824
1078
  const current = this.#routeStats.get(routeId) ?? { routeId, count: 0, flightBytes: 0, totalDurationMs: 0 };
825
1079
  current.count += 1;
@@ -840,29 +1094,22 @@ class RscRenderer {
840
1094
  }));
841
1095
  }
842
1096
 
843
- #getResultCacheEntry(request: Request, url: URL): RouteCacheEntry | null {
844
- const ttl = resolveAutoRouteCacheTtl({
845
- enabled: process.env.AKAN_RSC_RESULT_CACHE,
846
- ttl: process.env.AKAN_RSC_RESULT_CACHE_TTL,
847
- });
848
- if (ttl === null) {
849
- this.#resultCacheBypass += 1;
850
- return null;
851
- }
852
- if (
853
- !isRouteCachePathAllowed(url.pathname, {
1097
+ #getResultCacheEntry(request: Request, url: URL): { entry: RouteCacheEntry | null; reason?: string } {
1098
+ const decision = resolvePublicRouteCacheEntryDecision({
1099
+ request,
1100
+ url,
1101
+ theme: untrackedCookies().get("theme")?.value,
1102
+ defaultEnabled: process.env.NODE_ENV === "production",
1103
+ defaultAllow: process.env.NODE_ENV === "production",
1104
+ env: {
1105
+ enabled: process.env.AKAN_RSC_RESULT_CACHE,
1106
+ ttl: process.env.AKAN_RSC_RESULT_CACHE_TTL,
854
1107
  allow: process.env.AKAN_RSC_RESULT_CACHE_PATHS,
855
1108
  deny: process.env.AKAN_RSC_RESULT_CACHE_EXCLUDE_PATHS,
856
- })
857
- ) {
858
- this.#resultCacheBypass += 1;
859
- return null;
860
- }
861
- if (!isPublicRouteCacheableRequest(request)) {
862
- this.#resultCacheBypass += 1;
863
- return null;
864
- }
865
- return createRouteCacheEntry({ request, url, theme: untrackedCookies().get("theme")?.value, ttl });
1109
+ },
1110
+ });
1111
+ if (!decision.entry) this.#resultCacheBypass += 1;
1112
+ return decision;
866
1113
  }
867
1114
 
868
1115
  #getCachedResult(cacheKey: string): CachedRscResult | null {
@@ -875,10 +1122,24 @@ class RscRenderer {
875
1122
  return cached;
876
1123
  }
877
1124
 
1125
+ #getCachedPatchResult(cacheKey: string): CachedRscResult | null {
1126
+ const cached = this.#patchResultCache.get(cacheKey);
1127
+ if (!cached) {
1128
+ this.#resultCacheMisses += 1;
1129
+ return null;
1130
+ }
1131
+ this.#resultCacheHits += 1;
1132
+ return cached;
1133
+ }
1134
+
878
1135
  #setCachedResult(cacheKey: string, result: CachedRscResult, ttl: number): void {
879
1136
  this.#resultCache.set(cacheKey, result, ttl);
880
1137
  }
881
1138
 
1139
+ #setCachedPatchResult(cacheKey: string, result: CachedRscResult, ttl: number): void {
1140
+ this.#patchResultCache.set(cacheKey, result, ttl);
1141
+ }
1142
+
882
1143
  #runWithRequest<T>(request: Request, fn: () => Promise<T>): Promise<T> {
883
1144
  if (requestStorage) return Promise.resolve(requestStorage.run(request, fn));
884
1145
  return fn();
@@ -921,7 +1182,7 @@ class RscRenderer {
921
1182
  searchParams,
922
1183
  })
923
1184
  : { node: undefined, hasExplicitLanguageAlternates: false };
924
- const renderLocaleAlternates = shouldRenderLocaleAlternates({
1185
+ const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
925
1186
  hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
926
1187
  });
927
1188
  const theme = untrackedCookies().get("theme")?.value;
@@ -934,8 +1195,13 @@ class RscRenderer {
934
1195
  <meta key="charset" charSet="utf-8" />
935
1196
  <meta key="viewport" name="viewport" content="width=device-width, initial-scale=1" />
936
1197
  <meta key="robots" name="robots" content="noindex" />
937
- {routeHead.node ?? this.#renderDefaultHead()}
938
- {renderLocaleAlternates ? this.#renderLocaleAlternates(url) : null}
1198
+ {routeHeadSnapshot
1199
+ ? renderAkanHeadSnapshot(routeHeadSnapshot)
1200
+ : (routeHead.node ?? this.#renderDefaultHead())}
1201
+ {!routeHeadSnapshot &&
1202
+ shouldRenderLocaleAlternates({ hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates })
1203
+ ? this.#renderLocaleAlternates(url)
1204
+ : null}
939
1205
  {this.#renderStylesheet(pathname)}
940
1206
  </head>
941
1207
  <body key="body">{body}</body>
@@ -957,7 +1223,7 @@ class RscRenderer {
957
1223
  params: match.params,
958
1224
  searchParams,
959
1225
  });
960
- const renderLocaleAlternates = shouldRenderLocaleAlternates({
1226
+ const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
961
1227
  isSpecialRoute: match.pathRoute.isSpecialRoute,
962
1228
  hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
963
1229
  });
@@ -974,8 +1240,16 @@ class RscRenderer {
974
1240
  <head key="head">
975
1241
  <meta key="charset" charSet="utf-8" />
976
1242
  <meta key="viewport" name="viewport" content="width=device-width, initial-scale=1" />
977
- {routeHead.node ?? this.#renderDefaultHead()}
978
- {renderLocaleAlternates ? this.#renderLocaleAlternates(url) : null}
1243
+ {routeHeadSnapshot
1244
+ ? renderAkanHeadSnapshot(routeHeadSnapshot)
1245
+ : (routeHead.node ?? this.#renderDefaultHead())}
1246
+ {!routeHeadSnapshot &&
1247
+ shouldRenderLocaleAlternates({
1248
+ isSpecialRoute: match.pathRoute.isSpecialRoute,
1249
+ hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
1250
+ })
1251
+ ? this.#renderLocaleAlternates(url)
1252
+ : null}
979
1253
  {this.#renderStylesheet(url.pathname)}
980
1254
  </head>
981
1255
  <body key="body">{body}</body>
@@ -983,6 +1257,23 @@ class RscRenderer {
983
1257
  );
984
1258
  }
985
1259
 
1260
+ async #renderMatchedSuffix(
1261
+ url: URL,
1262
+ match: { pathRoute: PathRoute; params: Record<string, string> },
1263
+ patchStartIndex: number,
1264
+ searchParams = RouteTreeBuilder.parseSearchParams(url.search),
1265
+ ): Promise<ReactNode | null> {
1266
+ this.#logger.verbose(
1267
+ `composing route suffix pathname=${url.pathname} start=${patchStartIndex} params=${JSON.stringify(match.params)}`,
1268
+ );
1269
+ return RouteElementComposer.composeSuffix({
1270
+ pathRoute: match.pathRoute,
1271
+ params: match.params,
1272
+ searchParams,
1273
+ patchStartIndex,
1274
+ });
1275
+ }
1276
+
986
1277
  async #renderNotFound(url: URL): Promise<ReactNode> {
987
1278
  const matchedFallback = RouteTreeBuilder.matchFallback(url.pathname, this.#fallbackRoutes);
988
1279
  if (matchedFallback) {
@@ -1024,7 +1315,39 @@ class RscRenderer {
1024
1315
  return <title key="title">{process.env.AKAN_PUBLIC_APP_NAME ?? "Akan App"}</title>;
1025
1316
  }
1026
1317
 
1027
- #renderLocaleAlternates(url: URL): ReactNode {
1318
+ async #resolveRouteHeadSnapshot(
1319
+ url: URL,
1320
+ match: { pathRoute: PathRoute; params: Record<string, string> },
1321
+ searchParams: Record<string, string | string[]>,
1322
+ ): Promise<ResolvedHead["headSnapshot"]> {
1323
+ const routeHead = await RouteElementComposer.resolveHeadWithMetadata({
1324
+ pathRoute: match.pathRoute,
1325
+ params: match.params,
1326
+ searchParams,
1327
+ });
1328
+ return this.#createRouteHeadSnapshot(url, routeHead, {
1329
+ isSpecialRoute: match.pathRoute.isSpecialRoute,
1330
+ hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
1331
+ });
1332
+ }
1333
+
1334
+ #createRouteHeadSnapshot(
1335
+ url: URL,
1336
+ routeHead: ResolvedHead,
1337
+ options: { isSpecialRoute?: boolean; hasExplicitLanguageAlternates?: boolean },
1338
+ ): ResolvedHead["headSnapshot"] {
1339
+ if (!routeHead.headSnapshot) return undefined;
1340
+ return mergeAkanHeadSnapshots(
1341
+ routeHead.headSnapshot,
1342
+ shouldRenderLocaleAlternates(options) ? this.#createLocaleAlternateHeadSnapshot(url) : undefined,
1343
+ );
1344
+ }
1345
+
1346
+ #createLocaleAlternateHeadSnapshot(url: URL): ResolvedHead["headSnapshot"] {
1347
+ return createAkanLocaleAlternateHeadSnapshot(this.#getLocaleAlternateLanguages(url));
1348
+ }
1349
+
1350
+ #getLocaleAlternateLanguages(url: URL): Record<string, string> {
1028
1351
  const languages: Record<string, string> = {};
1029
1352
  const publicUrl = RscRenderer.#getPublicRequestUrl(url);
1030
1353
  for (const lang of this.#i18n.locales) {
@@ -1037,7 +1360,11 @@ class RscRenderer {
1037
1360
  xDefaultUrl.search = "";
1038
1361
  xDefaultUrl.hash = "";
1039
1362
  languages["x-default"] = xDefaultUrl.href;
1040
- return Object.entries(languages).map(([lang, href]) => (
1363
+ return languages;
1364
+ }
1365
+
1366
+ #renderLocaleAlternates(url: URL): ReactNode {
1367
+ return Object.entries(this.#getLocaleAlternateLanguages(url)).map(([lang, href]) => (
1041
1368
  <link key={`alternate:${lang}`} rel="alternate" hrefLang={lang} href={href} />
1042
1369
  ));
1043
1370
  }