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.
- package/base/primitiveRegistry.ts +19 -12
- package/client/csrTypes.ts +16 -0
- package/constant/fieldInfo.ts +11 -9
- package/constant/getDefault.ts +1 -1
- package/fetch/requestStorage.ts +5 -0
- package/package.json +4 -4
- package/server/akanApp.ts +26 -3
- package/server/akanServer.ts +5 -1
- package/server/cachePolicy.ts +99 -5
- package/server/imageOptimizer.ts +14 -1
- package/server/metadata.tsx +117 -33
- package/server/resolver/database.resolver.ts +4 -4
- package/server/routeElementComposer.tsx +46 -14
- package/server/routeState.ts +379 -0
- package/server/routeTreeBuilder.ts +3 -2
- package/server/rscClient.tsx +316 -46
- package/server/rscClientFetch.ts +57 -0
- package/server/rscClientPatch.ts +157 -0
- package/server/rscHeadPatch.ts +80 -0
- package/server/rscNavigationState.ts +315 -0
- package/server/rscPartialCommit.ts +3 -0
- package/server/rscPatchSafety.ts +57 -0
- package/server/rscSegmentOutlet.tsx +69 -0
- package/server/rscSegmentOutletReference.ts +24 -0
- package/server/rscWorker.tsx +380 -53
- package/server/rscWorkerCache.ts +180 -0
- package/server/rscWorkerHost.ts +40 -12
- package/server/rscWorkerReplay.ts +11 -2
- package/server/ssrFromRscRenderer.tsx +15 -10
- package/server/ssrTypes.ts +18 -0
- package/server/types.tsx +4 -0
- package/server/webRouter.ts +198 -42
- package/service/predefinedAdaptor/database.adaptor.ts +72 -25
- package/signal/signalContext.ts +1 -1
- package/types/base/primitiveRegistry.d.ts +6 -6
- package/types/client/csrTypes.d.ts +16 -0
- package/types/constant/fieldInfo.d.ts +8 -7
- package/types/fetch/requestStorage.d.ts +2 -0
- package/types/server/cachePolicy.d.ts +36 -0
- package/types/server/metadata.d.ts +10 -1
- package/types/server/routeElementComposer.d.ts +9 -1
- package/types/server/routeState.d.ts +94 -0
- package/types/server/rscClient.d.ts +1 -0
- package/types/server/rscClientFetch.d.ts +24 -0
- package/types/server/rscClientPatch.d.ts +21 -0
- package/types/server/rscHeadPatch.d.ts +12 -0
- package/types/server/rscNavigationState.d.ts +78 -0
- package/types/server/rscPartialCommit.d.ts +1 -0
- package/types/server/rscPatchSafety.d.ts +8 -0
- package/types/server/rscSegmentOutlet.d.ts +17 -0
- package/types/server/rscSegmentOutletReference.d.ts +2 -0
- package/types/server/rscWorker.d.ts +5 -0
- package/types/server/rscWorkerCache.d.ts +63 -0
- package/types/server/rscWorkerHost.d.ts +8 -4
- package/types/server/rscWorkerReplay.d.ts +3 -0
- package/types/server/ssrFromRscRenderer.d.ts +1 -0
- package/types/server/ssrTypes.d.ts +17 -0
- package/types/server/types.d.ts +4 -0
- package/types/server/webRouter.d.ts +7 -3
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +6 -0
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/ClientSide.d.ts +1 -1
- package/types/ui/Constant/Doc.d.ts +6 -6
- package/types/ui/Constant/Mermaid.d.ts +1 -1
- package/types/ui/Constant/index.d.ts +1 -1
- package/types/ui/Constant/schemaDoc.d.ts +1 -1
- package/types/ui/Copy.d.ts +1 -1
- package/types/ui/CsrImage.d.ts +1 -1
- package/types/ui/Data/CardList.d.ts +1 -1
- package/types/ui/Data/Dashboard.d.ts +1 -1
- package/types/ui/Data/Insight.d.ts +1 -1
- package/types/ui/Data/Item.d.ts +6 -6
- package/types/ui/Data/ListContainer.d.ts +1 -1
- package/types/ui/Data/Pagination.d.ts +1 -1
- package/types/ui/Data/TableList.d.ts +1 -1
- package/types/ui/DatePicker.d.ts +3 -3
- package/types/ui/Dialog/Close.d.ts +1 -1
- package/types/ui/Dialog/Content.d.ts +1 -1
- package/types/ui/Dialog/Provider.d.ts +1 -1
- package/types/ui/Dialog/Trigger.d.ts +1 -1
- package/types/ui/Dialog/index.d.ts +3 -3
- package/types/ui/DragAction.d.ts +4 -4
- package/types/ui/DraggableList.d.ts +3 -3
- package/types/ui/Dropdown.d.ts +1 -1
- package/types/ui/Empty.d.ts +1 -1
- package/types/ui/Field.d.ts +22 -22
- package/types/ui/Image.d.ts +1 -1
- package/types/ui/InfiniteScroll.d.ts +1 -1
- package/types/ui/Input.d.ts +6 -6
- package/types/ui/KeyboardAvoiding.d.ts +1 -1
- package/types/ui/Layout/BottomAction.d.ts +1 -1
- package/types/ui/Layout/BottomInset.d.ts +1 -1
- package/types/ui/Layout/BottomTab.d.ts +1 -1
- package/types/ui/Layout/Header.d.ts +1 -1
- package/types/ui/Layout/LeftSider.d.ts +1 -1
- package/types/ui/Layout/Navbar.d.ts +1 -1
- package/types/ui/Layout/RightSider.d.ts +1 -1
- package/types/ui/Layout/Sider.d.ts +1 -1
- package/types/ui/Layout/Template.d.ts +1 -1
- package/types/ui/Layout/TopLeftAction.d.ts +1 -1
- package/types/ui/Layout/Unit.d.ts +1 -1
- package/types/ui/Layout/View.d.ts +1 -1
- package/types/ui/Layout/Zone.d.ts +1 -1
- package/types/ui/Layout/index.d.ts +12 -12
- package/types/ui/Link/Back.d.ts +1 -1
- package/types/ui/Link/Close.d.ts +1 -1
- package/types/ui/Link/CsrLink.d.ts +1 -1
- package/types/ui/Link/Lang.d.ts +1 -1
- package/types/ui/Link/SsrLink.d.ts +1 -1
- package/types/ui/Link/index.d.ts +1 -1
- package/types/ui/Load/Edit.d.ts +1 -1
- package/types/ui/Load/Edit_Client.d.ts +1 -1
- package/types/ui/Load/PageCSR.d.ts +1 -1
- package/types/ui/Load/Pagination.d.ts +1 -1
- package/types/ui/Load/Units.d.ts +1 -1
- package/types/ui/Load/View.d.ts +1 -1
- package/types/ui/Loading/Area.d.ts +1 -1
- package/types/ui/Loading/Button.d.ts +1 -1
- package/types/ui/Loading/Input.d.ts +1 -1
- package/types/ui/Loading/ProgressBar.d.ts +1 -1
- package/types/ui/Loading/Skeleton.d.ts +1 -1
- package/types/ui/Loading/Spin.d.ts +1 -1
- package/types/ui/Loading/index.d.ts +6 -6
- package/types/ui/Menu.d.ts +1 -1
- package/types/ui/Modal.d.ts +1 -1
- package/types/ui/Model/AdminPanel.d.ts +1 -1
- package/types/ui/Model/Edit.d.ts +1 -1
- package/types/ui/Model/EditModal.d.ts +1 -1
- package/types/ui/Model/EditWrapper.d.ts +1 -1
- package/types/ui/Model/LoadInit.d.ts +1 -1
- package/types/ui/Model/New.d.ts +1 -1
- package/types/ui/Model/NewWrapper.d.ts +1 -1
- package/types/ui/Model/NewWrapper_Client.d.ts +1 -1
- package/types/ui/Model/Remove.d.ts +1 -1
- package/types/ui/Model/RemoveWrapper.d.ts +1 -1
- package/types/ui/Model/SureToRemove.d.ts +1 -1
- package/types/ui/Model/View.d.ts +1 -1
- package/types/ui/Model/ViewEditModal.d.ts +1 -1
- package/types/ui/Model/ViewModal.d.ts +1 -1
- package/types/ui/Model/ViewWrapper.d.ts +1 -1
- package/types/ui/More.d.ts +1 -1
- package/types/ui/ObjectId.d.ts +1 -1
- package/types/ui/Popconfirm.d.ts +1 -1
- package/types/ui/Radio.d.ts +2 -2
- package/types/ui/RecentTime.d.ts +1 -1
- package/types/ui/Refresh.d.ts +1 -1
- package/types/ui/ScreenNavigator.d.ts +3 -3
- package/types/ui/Select.d.ts +1 -1
- package/types/ui/Signal/Arg.d.ts +13 -13
- package/types/ui/Signal/Doc.d.ts +6 -6
- package/types/ui/Signal/Listener.d.ts +2 -2
- package/types/ui/Signal/Message.d.ts +4 -4
- package/types/ui/Signal/Object.d.ts +4 -4
- package/types/ui/Signal/PubSub.d.ts +4 -4
- package/types/ui/Signal/Request.d.ts +2 -2
- package/types/ui/Signal/Response.d.ts +3 -3
- package/types/ui/Signal/RestApi.d.ts +5 -5
- package/types/ui/Signal/WebSocket.d.ts +2 -2
- package/types/ui/System/CSR.d.ts +5 -5
- package/types/ui/System/Client.d.ts +8 -8
- package/types/ui/System/Common.d.ts +2 -2
- package/types/ui/System/DevModeToggle.d.ts +1 -1
- package/types/ui/System/Gtag.d.ts +1 -1
- package/types/ui/System/Messages.d.ts +1 -1
- package/types/ui/System/Reconnect.d.ts +1 -1
- package/types/ui/System/Root.d.ts +1 -1
- package/types/ui/System/SSR.d.ts +4 -4
- package/types/ui/System/SelectLanguage.d.ts +1 -1
- package/types/ui/System/ThemeToggle.d.ts +1 -1
- package/types/ui/System/index.d.ts +7 -7
- package/types/ui/Tab/Menu.d.ts +1 -1
- package/types/ui/Tab/Menus.d.ts +1 -1
- package/types/ui/Tab/Panel.d.ts +1 -1
- package/types/ui/Tab/Provider.d.ts +1 -1
- package/types/ui/Tab/index.d.ts +4 -4
- package/types/ui/Table.d.ts +1 -1
- package/types/ui/ToggleSelect.d.ts +2 -2
- package/types/ui/Unauthorized.d.ts +1 -1
- package/ui/Constant/schemaDoc.ts +1 -1
- package/server/resolver/resolver.contract.fixture.ts +0 -222
package/server/webRouter.ts
CHANGED
|
@@ -12,22 +12,25 @@ import { type AkanRequestStore, createRequestStore, parseCookieHeader } from "ak
|
|
|
12
12
|
import type { AkanMetricsReport } from "akanjs/service";
|
|
13
13
|
import {
|
|
14
14
|
type BuilderRpc,
|
|
15
|
+
type ClientManifest,
|
|
16
|
+
type MergedManifest,
|
|
15
17
|
RouteClientCache,
|
|
16
18
|
type RouteSeedIndex,
|
|
17
19
|
RouteSeedIndexStore,
|
|
18
20
|
RoutesManifestStore,
|
|
19
21
|
} from "./artifact";
|
|
20
22
|
import {
|
|
21
|
-
createRouteCacheEntry,
|
|
22
23
|
getClientFacingOrigin,
|
|
23
|
-
|
|
24
|
+
hasRouteCacheInvalidationScope,
|
|
24
25
|
isRouteCachePathAllowed,
|
|
25
26
|
LruTtlCache,
|
|
26
|
-
normalizeRouteCacheTtl,
|
|
27
27
|
parsePositiveInt,
|
|
28
28
|
type RouteCacheEntry,
|
|
29
|
+
type RouteCacheInvalidation,
|
|
29
30
|
type RouteCacheRenderState,
|
|
31
|
+
resolvePublicRouteCacheEntryDecision,
|
|
30
32
|
resolveRouteCacheStoreTtl,
|
|
33
|
+
shouldInvalidateRouteCacheEntry,
|
|
31
34
|
shouldStoreRouteCache,
|
|
32
35
|
} from "./cachePolicy";
|
|
33
36
|
import { DevHmrController } from "./hmr";
|
|
@@ -35,13 +38,24 @@ import { HMR_CLIENT_SCRIPT } from "./hmr/clientScript";
|
|
|
35
38
|
import type { HmrWsData, HmrWsHub } from "./hmr/wsHub";
|
|
36
39
|
import { ImageOptimizer } from "./imageOptimizer";
|
|
37
40
|
import { createDefaultRobotsTxt } from "./robots";
|
|
41
|
+
import {
|
|
42
|
+
AKAN_RSC_PATCH_HEAD_SAFE_HEADER,
|
|
43
|
+
AKAN_RSC_PATCH_HEAD_SNAPSHOT_HEADER,
|
|
44
|
+
AKAN_RSC_PATCH_SEGMENT_PATH_HEADER,
|
|
45
|
+
AKAN_RSC_PATCH_START_INDEX_HEADER,
|
|
46
|
+
AKAN_RSC_PATCH_START_SEGMENT_HEADER,
|
|
47
|
+
AKAN_RSC_RESPONSE_STATE_HEADER,
|
|
48
|
+
} from "./routeState";
|
|
38
49
|
import { type RscRedirectMethod, type RscRedirectStatus, type RscRenderResult, RscWorker } from "./rscWorkerHost";
|
|
39
50
|
import { createDefaultSitemapXml, getSitemapBasePath } from "./sitemap";
|
|
40
51
|
import { SsrFromRscRenderer } from "./ssrFromRscRenderer";
|
|
52
|
+
import type { RscTraceMetadata, SsrManifest } from "./ssrTypes";
|
|
41
53
|
import { createSystemPageResponse, getSystemPageHomeHref } from "./systemPages";
|
|
42
54
|
import type { BaseBuildArtifact, HttpRoutes, RenderState } from "./types";
|
|
43
55
|
|
|
44
56
|
const RESERVED_BASE_PATHS = new Set(["admin"]);
|
|
57
|
+
const CLIENT_CLOSED_REQUEST_STATUS = 499;
|
|
58
|
+
export const DEFAULT_HTML_RESULT_CACHE_MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
45
59
|
|
|
46
60
|
export function createRscRedirectResponse(
|
|
47
61
|
location: string,
|
|
@@ -74,10 +88,36 @@ export function createRscNotFoundFallbackResponse(): Response {
|
|
|
74
88
|
return createRscStreamResponse("0:null\n", 404);
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
function appendRscTraceHeaders(headers: Headers, trace?: RscTraceMetadata): void {
|
|
92
|
+
if (!trace) return;
|
|
93
|
+
if (trace.navId) headers.set("X-Akan-Rsc-Nav-Id", trace.navId);
|
|
94
|
+
headers.set("X-Akan-Rsc-Pathname", trace.pathname);
|
|
95
|
+
headers.set("X-Akan-Rsc-Route", trace.routeId);
|
|
96
|
+
headers.set("X-Akan-Rsc-Cache", trace.cache);
|
|
97
|
+
if (trace.cacheReason) headers.set("X-Akan-Rsc-Cache-Reason", trace.cacheReason);
|
|
98
|
+
if (trace.cacheKeyHash) headers.set("X-Akan-Rsc-Cache-Key", trace.cacheKeyHash);
|
|
99
|
+
if (trace.partial) headers.set("X-Akan-Rsc-Partial", trace.partial);
|
|
100
|
+
if (trace.partialReason) headers.set("X-Akan-Rsc-Partial-Reason", trace.partialReason);
|
|
101
|
+
if (trace.partialCommonPrefixLength !== undefined) {
|
|
102
|
+
headers.set("X-Akan-Rsc-Partial-Common-Prefix", String(trace.partialCommonPrefixLength));
|
|
103
|
+
}
|
|
104
|
+
if (trace.patchStartIndex !== undefined)
|
|
105
|
+
headers.set(AKAN_RSC_PATCH_START_INDEX_HEADER, String(trace.patchStartIndex));
|
|
106
|
+
if (trace.patchSegmentPath) headers.set(AKAN_RSC_PATCH_SEGMENT_PATH_HEADER, trace.patchSegmentPath);
|
|
107
|
+
if (trace.patchStartSegment) headers.set(AKAN_RSC_PATCH_START_SEGMENT_HEADER, trace.patchStartSegment);
|
|
108
|
+
if (trace.patchHeadSafe) headers.set(AKAN_RSC_PATCH_HEAD_SAFE_HEADER, "1");
|
|
109
|
+
if (trace.patchHeadSnapshot) headers.set(AKAN_RSC_PATCH_HEAD_SNAPSHOT_HEADER, trace.patchHeadSnapshot);
|
|
110
|
+
if (trace.routeState) headers.set(AKAN_RSC_RESPONSE_STATE_HEADER, trace.routeState);
|
|
111
|
+
}
|
|
112
|
+
|
|
77
113
|
export function cacheHtmlWhileStreaming(
|
|
78
114
|
stream: ReadableStream<Uint8Array>,
|
|
79
115
|
onComplete: (html: string) => void,
|
|
80
|
-
options: {
|
|
116
|
+
options: {
|
|
117
|
+
shouldCache?: () => boolean | Promise<boolean>;
|
|
118
|
+
maxBodyBytes?: number | null;
|
|
119
|
+
onSkip?: (reason: "body-too-large" | "store-skip") => void;
|
|
120
|
+
} = {},
|
|
81
121
|
): ReadableStream<Uint8Array> {
|
|
82
122
|
const chunks: Uint8Array[] = [];
|
|
83
123
|
let byteLength = 0;
|
|
@@ -99,7 +139,10 @@ export function cacheHtmlWhileStreaming(
|
|
|
99
139
|
controller.enqueue(chunk);
|
|
100
140
|
},
|
|
101
141
|
async flush() {
|
|
102
|
-
if (exceededMaxBodyBytes)
|
|
142
|
+
if (exceededMaxBodyBytes) {
|
|
143
|
+
options.onSkip?.("body-too-large");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
103
146
|
const body = new Uint8Array(byteLength);
|
|
104
147
|
let offset = 0;
|
|
105
148
|
for (const chunk of chunks) {
|
|
@@ -107,7 +150,10 @@ export function cacheHtmlWhileStreaming(
|
|
|
107
150
|
offset += chunk.byteLength;
|
|
108
151
|
}
|
|
109
152
|
try {
|
|
110
|
-
if (options.shouldCache && !(await options.shouldCache()))
|
|
153
|
+
if (options.shouldCache && !(await options.shouldCache())) {
|
|
154
|
+
options.onSkip?.("store-skip");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
111
157
|
onComplete(decoder.decode(body));
|
|
112
158
|
} catch {
|
|
113
159
|
}
|
|
@@ -144,10 +190,12 @@ export function isHtmlRouteCachePathAllowed(
|
|
|
144
190
|
AKAN_HTML_RESULT_CACHE_PATHS?: string;
|
|
145
191
|
AKAN_HTML_RESULT_CACHE_EXCLUDE_PATHS?: string;
|
|
146
192
|
} = process.env as Record<string, string | undefined>,
|
|
193
|
+
options: { defaultAllow?: boolean } = {},
|
|
147
194
|
): boolean {
|
|
148
195
|
return isRouteCachePathAllowed(pathname, {
|
|
149
196
|
allow: env.AKAN_HTML_RESULT_CACHE_PATHS,
|
|
150
197
|
deny: env.AKAN_HTML_RESULT_CACHE_EXCLUDE_PATHS,
|
|
198
|
+
defaultAllow: options.defaultAllow,
|
|
151
199
|
});
|
|
152
200
|
}
|
|
153
201
|
|
|
@@ -155,7 +203,9 @@ export async function createRscNavigationStreamResponse(
|
|
|
155
203
|
result: Extract<RscRenderResult, { type: "stream" }>,
|
|
156
204
|
): Promise<Response> {
|
|
157
205
|
|
|
158
|
-
|
|
206
|
+
const response = createRscStreamResponse(result.stream, result.status ?? 200);
|
|
207
|
+
appendRscTraceHeaders(response.headers, result.trace);
|
|
208
|
+
return response;
|
|
159
209
|
}
|
|
160
210
|
|
|
161
211
|
export function normalizeRscTargetUrlForHostBasePath(
|
|
@@ -209,6 +259,9 @@ interface WebRouterOptions {
|
|
|
209
259
|
|
|
210
260
|
interface CachedHtmlResult {
|
|
211
261
|
html: string;
|
|
262
|
+
pathname: string;
|
|
263
|
+
routeId?: string;
|
|
264
|
+
tags?: string[];
|
|
212
265
|
}
|
|
213
266
|
|
|
214
267
|
export class WebRouter {
|
|
@@ -217,7 +270,7 @@ export class WebRouter {
|
|
|
217
270
|
#artifact: BaseBuildArtifact;
|
|
218
271
|
#rsc: RscWorker;
|
|
219
272
|
#hub: HmrWsHub | null = null;
|
|
220
|
-
#prodMode = process.env.NODE_ENV === "production"
|
|
273
|
+
#prodMode = process.env.NODE_ENV === "production";
|
|
221
274
|
#builderRpc: BuilderRpc | null;
|
|
222
275
|
#routeCache: RouteClientCache;
|
|
223
276
|
#devHmr: DevHmrController | null = null;
|
|
@@ -267,7 +320,7 @@ export class WebRouter {
|
|
|
267
320
|
if (prebuilt) {
|
|
268
321
|
this.#routeCache.seed(prebuilt);
|
|
269
322
|
await this.#rsc.reload({
|
|
270
|
-
clientManifest: this.#routeCache.merged.clientManifest,
|
|
323
|
+
clientManifest: this.#mergeRuntimeManifest(this.#routeCache.merged).clientManifest,
|
|
271
324
|
cssAssets: this.renderState.cssAssets,
|
|
272
325
|
buildId: this.renderState.buildId,
|
|
273
326
|
});
|
|
@@ -480,7 +533,8 @@ export class WebRouter {
|
|
|
480
533
|
try {
|
|
481
534
|
this.#requestStats.fullSsr += 1;
|
|
482
535
|
const manifest = await this.#ensureRoute(url);
|
|
483
|
-
const
|
|
536
|
+
const htmlCacheDecision = this.#getHtmlCacheEntry(req, url);
|
|
537
|
+
const htmlCacheEntry = htmlCacheDecision.entry;
|
|
484
538
|
const cachedHtml = htmlCacheEntry ? this.#getCachedHtml(htmlCacheEntry.key) : null;
|
|
485
539
|
if (cachedHtml) {
|
|
486
540
|
return new Response(cachedHtml, {
|
|
@@ -496,16 +550,24 @@ export class WebRouter {
|
|
|
496
550
|
});
|
|
497
551
|
if (rscResult.type === "redirect")
|
|
498
552
|
return Response.redirect(new URL(rscResult.location, url.origin), rscResult.status);
|
|
499
|
-
if (rscResult.type === "not-found") return this.#
|
|
553
|
+
if (rscResult.type === "not-found") return this.#renderSystemNotFoundFallbackResponse(req, url);
|
|
500
554
|
const themeCookieExists = WebRouter.#hasCookie(req, "theme");
|
|
501
555
|
const hostRequestStore = createRequestStore(req);
|
|
556
|
+
const extraBootstrapInline = [
|
|
557
|
+
rscResult.trace?.routeState
|
|
558
|
+
? `self.__AKAN_RSC_INITIAL_STATE__=${JSON.stringify(rscResult.trace.routeState)};`
|
|
559
|
+
: "",
|
|
560
|
+
!this.#prodMode ? HMR_CLIENT_SCRIPT : "",
|
|
561
|
+
]
|
|
562
|
+
.filter(Boolean)
|
|
563
|
+
.join("\n");
|
|
502
564
|
const htmlStream = await new SsrFromRscRenderer().render({
|
|
503
565
|
request: req,
|
|
504
566
|
requestStore: hostRequestStore,
|
|
505
567
|
rscStream: rscResult.stream,
|
|
506
568
|
ssrManifest: manifest.ssrManifest,
|
|
507
569
|
bootstrapModules: [this.#artifact.rscClientUrl],
|
|
508
|
-
extraBootstrapInline:
|
|
570
|
+
extraBootstrapInline: extraBootstrapInline || undefined,
|
|
509
571
|
importmap: this.#artifact.vendorMap,
|
|
510
572
|
theme: themeCookieExists ? undefined : (rscResult.theme ?? "system"),
|
|
511
573
|
lateControl: rscResult.lateControl,
|
|
@@ -518,6 +580,10 @@ export class WebRouter {
|
|
|
518
580
|
if (req.method === "HEAD") {
|
|
519
581
|
const headers = new Headers(responseHeaders);
|
|
520
582
|
if (htmlCacheEntry && responseStatus === 200) headers.set("X-Akan-Cache", "MISS");
|
|
583
|
+
else if (htmlCacheDecision.reason) {
|
|
584
|
+
headers.set("X-Akan-Cache", "BYPASS");
|
|
585
|
+
headers.set("X-Akan-Cache-Reason", htmlCacheDecision.reason);
|
|
586
|
+
}
|
|
521
587
|
cancelStreamForHeadResponse(htmlStream, new Error("HEAD response does not consume body"));
|
|
522
588
|
return new Response(null, { status: responseStatus, headers });
|
|
523
589
|
}
|
|
@@ -525,6 +591,7 @@ export class WebRouter {
|
|
|
525
591
|
const headers = new Headers(responseHeaders);
|
|
526
592
|
headers.set("X-Akan-Cache", "MISS");
|
|
527
593
|
let htmlStoreTtl = htmlCacheEntry.ttl;
|
|
594
|
+
let htmlCacheMetadata: Omit<CachedHtmlResult, "html"> = { pathname: url.pathname };
|
|
528
595
|
const shouldCacheHtml = Promise.all([rscResult.lateControl, rscResult.cacheState]).then(
|
|
529
596
|
([control, cacheState]) => {
|
|
530
597
|
const storeTtl = resolveHtmlRouteCacheStoreTtl({
|
|
@@ -535,6 +602,11 @@ export class WebRouter {
|
|
|
535
602
|
});
|
|
536
603
|
if (storeTtl === null) return false;
|
|
537
604
|
htmlStoreTtl = storeTtl;
|
|
605
|
+
htmlCacheMetadata = {
|
|
606
|
+
pathname: url.pathname,
|
|
607
|
+
routeId: cacheState.routeId,
|
|
608
|
+
tags: cacheState.tags,
|
|
609
|
+
};
|
|
538
610
|
return true;
|
|
539
611
|
},
|
|
540
612
|
);
|
|
@@ -542,11 +614,16 @@ export class WebRouter {
|
|
|
542
614
|
cacheHtmlWhileStreaming(
|
|
543
615
|
htmlStream,
|
|
544
616
|
(html) => {
|
|
545
|
-
this.#setCachedHtml(htmlCacheEntry.key, html, htmlStoreTtl);
|
|
617
|
+
this.#setCachedHtml(htmlCacheEntry.key, html, htmlStoreTtl, htmlCacheMetadata);
|
|
546
618
|
},
|
|
547
619
|
{
|
|
548
620
|
shouldCache: () => shouldCacheHtml,
|
|
549
|
-
maxBodyBytes:
|
|
621
|
+
maxBodyBytes:
|
|
622
|
+
parsePositiveInt(process.env.AKAN_HTML_RESULT_CACHE_MAX_BODY_BYTES) ??
|
|
623
|
+
DEFAULT_HTML_RESULT_CACHE_MAX_BODY_BYTES,
|
|
624
|
+
onSkip: (reason) => {
|
|
625
|
+
this.#logger.verbose(`html cache store skipped pathname=${url.pathname} reason=${reason}`);
|
|
626
|
+
},
|
|
550
627
|
},
|
|
551
628
|
),
|
|
552
629
|
{
|
|
@@ -555,9 +632,14 @@ export class WebRouter {
|
|
|
555
632
|
},
|
|
556
633
|
);
|
|
557
634
|
}
|
|
635
|
+
const headers = new Headers(responseHeaders);
|
|
636
|
+
if (htmlCacheDecision.reason) {
|
|
637
|
+
headers.set("X-Akan-Cache", "BYPASS");
|
|
638
|
+
headers.set("X-Akan-Cache-Reason", htmlCacheDecision.reason);
|
|
639
|
+
}
|
|
558
640
|
return new Response(htmlStream, {
|
|
559
641
|
status: responseStatus,
|
|
560
|
-
headers
|
|
642
|
+
headers,
|
|
561
643
|
});
|
|
562
644
|
} catch (err) {
|
|
563
645
|
return this.#renderErrorResponse(req, url.pathname, err);
|
|
@@ -593,10 +675,24 @@ export class WebRouter {
|
|
|
593
675
|
};
|
|
594
676
|
}
|
|
595
677
|
|
|
596
|
-
/** @internal Clears local route result caches owned by the host and RSC worker. */
|
|
597
|
-
invalidateRouteCaches(
|
|
598
|
-
|
|
599
|
-
|
|
678
|
+
/** @internal Clears or scopes invalidation for local route result caches owned by the host and RSC worker. */
|
|
679
|
+
invalidateRouteCaches(invalidation?: string | RouteCacheInvalidation): void {
|
|
680
|
+
const payload = typeof invalidation === "string" ? { reason: invalidation } : invalidation;
|
|
681
|
+
if (!hasRouteCacheInvalidationScope(payload)) {
|
|
682
|
+
this.#htmlCache.clear();
|
|
683
|
+
} else if (payload) {
|
|
684
|
+
this.#htmlCache.invalidate((_key, value) =>
|
|
685
|
+
shouldInvalidateRouteCacheEntry(
|
|
686
|
+
{
|
|
687
|
+
pathname: value.pathname,
|
|
688
|
+
routeId: value.routeId,
|
|
689
|
+
tags: value.tags,
|
|
690
|
+
},
|
|
691
|
+
payload,
|
|
692
|
+
),
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
this.#rsc.invalidateRouteResultCache(invalidation);
|
|
600
696
|
}
|
|
601
697
|
|
|
602
698
|
/**
|
|
@@ -631,25 +727,22 @@ export class WebRouter {
|
|
|
631
727
|
static #hasCookie(req: Request, name: string): boolean {
|
|
632
728
|
return parseCookieHeader(req.headers.get("cookie") ?? "").has(name);
|
|
633
729
|
}
|
|
634
|
-
#getHtmlCacheEntry(req: Request, url: URL): RouteCacheEntry | null {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
this.#
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (
|
|
649
|
-
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
return createRouteCacheEntry({ request: req, url, theme: WebRouter.#cookieValue(req, "theme"), ttl });
|
|
730
|
+
#getHtmlCacheEntry(req: Request, url: URL): { entry: RouteCacheEntry | null; reason?: string } {
|
|
731
|
+
const decision = resolvePublicRouteCacheEntryDecision({
|
|
732
|
+
request: req,
|
|
733
|
+
url,
|
|
734
|
+
theme: WebRouter.#cookieValue(req, "theme"),
|
|
735
|
+
defaultEnabled: this.#prodMode,
|
|
736
|
+
defaultAllow: this.#prodMode,
|
|
737
|
+
env: {
|
|
738
|
+
enabled: process.env.AKAN_HTML_RESULT_CACHE,
|
|
739
|
+
ttl: process.env.AKAN_HTML_RESULT_CACHE_TTL,
|
|
740
|
+
allow: process.env.AKAN_HTML_RESULT_CACHE_PATHS,
|
|
741
|
+
deny: process.env.AKAN_HTML_RESULT_CACHE_EXCLUDE_PATHS,
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
if (!decision.entry) this.#htmlCacheBypass += 1;
|
|
745
|
+
return decision;
|
|
653
746
|
}
|
|
654
747
|
|
|
655
748
|
#getCachedHtml(cacheKey: string): string | null {
|
|
@@ -662,8 +755,8 @@ export class WebRouter {
|
|
|
662
755
|
return cached.html;
|
|
663
756
|
}
|
|
664
757
|
|
|
665
|
-
#setCachedHtml(cacheKey: string, html: string, ttl: number): void {
|
|
666
|
-
this.#htmlCache.set(cacheKey, { html }, ttl);
|
|
758
|
+
#setCachedHtml(cacheKey: string, html: string, ttl: number, metadata: Omit<CachedHtmlResult, "html">): void {
|
|
759
|
+
this.#htmlCache.set(cacheKey, { html, ...metadata }, ttl);
|
|
667
760
|
}
|
|
668
761
|
|
|
669
762
|
static #cookieValue(req: Request, name: string): string | undefined {
|
|
@@ -683,9 +776,17 @@ export class WebRouter {
|
|
|
683
776
|
this.#logger.verbose(
|
|
684
777
|
`[route-cache] ensure pathname=${url.pathname} routeId=${matched?.entry.routeId ?? "(none)"} in ${Date.now() - started}ms`,
|
|
685
778
|
);
|
|
686
|
-
return this.#routeCache.snapshot();
|
|
779
|
+
return this.#mergeRuntimeManifest(this.#routeCache.snapshot());
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
#mergeRuntimeManifest(manifest: MergedManifest): MergedManifest {
|
|
783
|
+
return {
|
|
784
|
+
...manifest,
|
|
785
|
+
clientManifest: WebRouter.#mergeClientManifest(this.#artifact.rscRuntimeClientManifest, manifest.clientManifest),
|
|
786
|
+
ssrManifest: WebRouter.#mergeSsrManifest(this.#artifact.rscRuntimeSsrManifest, manifest.ssrManifest),
|
|
787
|
+
};
|
|
687
788
|
}
|
|
688
|
-
#
|
|
789
|
+
#renderSystemNotFoundFallbackResponse(req: Request, url: URL): Promise<Response> {
|
|
689
790
|
return createSystemPageResponse({
|
|
690
791
|
kind: "not-found",
|
|
691
792
|
method: req.method,
|
|
@@ -697,6 +798,7 @@ export class WebRouter {
|
|
|
697
798
|
}
|
|
698
799
|
|
|
699
800
|
#renderErrorResponse(req: Request, scope: string, err: unknown): Promise<Response> {
|
|
801
|
+
if (WebRouter.#isExpectedRequestAbort(err)) return Promise.resolve(WebRouter.#clientClosedResponse());
|
|
700
802
|
const message = err instanceof Error ? err.message : String(err);
|
|
701
803
|
this.#logger.error(`[SSR] render failed scope=${scope}: ${message}`);
|
|
702
804
|
this.#hub?.broadcast({ type: "error", message });
|
|
@@ -713,6 +815,7 @@ export class WebRouter {
|
|
|
713
815
|
}
|
|
714
816
|
|
|
715
817
|
#renderRscErrorResponse(scope: string, err: unknown): Response {
|
|
818
|
+
if (WebRouter.#isExpectedRequestAbort(err)) return WebRouter.#clientClosedResponse();
|
|
716
819
|
const message = err instanceof Error ? err.message : String(err);
|
|
717
820
|
this.#logger.error(`[SSR] render failed scope=${scope}: ${message}`);
|
|
718
821
|
this.#hub?.broadcast({ type: "error", message });
|
|
@@ -722,6 +825,23 @@ export class WebRouter {
|
|
|
722
825
|
});
|
|
723
826
|
}
|
|
724
827
|
|
|
828
|
+
static #clientClosedResponse(): Response {
|
|
829
|
+
return new Response(null, {
|
|
830
|
+
status: CLIENT_CLOSED_REQUEST_STATUS,
|
|
831
|
+
headers: { "Cache-Control": "no-store" },
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
static #isExpectedRequestAbort(error: unknown): boolean {
|
|
836
|
+
if (!(error instanceof Error)) return false;
|
|
837
|
+
return (
|
|
838
|
+
error.name === "AbortError" ||
|
|
839
|
+
error.message === "The connection was closed." ||
|
|
840
|
+
error.message === "Connection closed." ||
|
|
841
|
+
error.message.includes("The connection was closed")
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
725
845
|
#getSystemPageHomeHref(req: Request, pathname: string): string {
|
|
726
846
|
return getSystemPageHomeHref({
|
|
727
847
|
pathname,
|
|
@@ -811,10 +931,46 @@ export class WebRouter {
|
|
|
811
931
|
...artifact,
|
|
812
932
|
cssAssets: artifact.cssAssets ?? {},
|
|
813
933
|
pagesBundlePath,
|
|
934
|
+
rscRuntimeSsrManifest: artifact.rscRuntimeSsrManifest
|
|
935
|
+
? WebRouter.#normalizeSsrManifest(artifact.rscRuntimeSsrManifest, normalizedArtifactDir)
|
|
936
|
+
: undefined,
|
|
814
937
|
i18n: artifact.i18n ?? DEFAULT_AKAN_I18N,
|
|
815
938
|
};
|
|
816
939
|
}
|
|
817
940
|
|
|
941
|
+
static #mergeClientManifest(...manifests: Array<ClientManifest | undefined>): ClientManifest {
|
|
942
|
+
return Object.assign({}, ...manifests.filter(Boolean));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
static #mergeSsrManifest(...manifests: Array<SsrManifest | undefined>): SsrManifest {
|
|
946
|
+
const definedManifests = manifests.filter((manifest): manifest is SsrManifest => Boolean(manifest));
|
|
947
|
+
return {
|
|
948
|
+
moduleLoading: null,
|
|
949
|
+
moduleMap: Object.assign({}, ...definedManifests.map((manifest) => manifest.moduleMap)),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
static #normalizeSsrManifest(ssrManifest: SsrManifest, artifactDir: string): SsrManifest {
|
|
954
|
+
return {
|
|
955
|
+
...ssrManifest,
|
|
956
|
+
moduleMap: Object.fromEntries(
|
|
957
|
+
Object.entries(ssrManifest.moduleMap).map(([entryUrl, byName]) => [
|
|
958
|
+
entryUrl,
|
|
959
|
+
Object.fromEntries(
|
|
960
|
+
Object.entries(byName).map(([name, entry]) => [
|
|
961
|
+
name,
|
|
962
|
+
{
|
|
963
|
+
...entry,
|
|
964
|
+
id: WebRouter.#resolveArtifactPath(entry.id, artifactDir),
|
|
965
|
+
chunks: entry.chunks.map((chunk) => WebRouter.#resolveArtifactPath(chunk, artifactDir)),
|
|
966
|
+
},
|
|
967
|
+
]),
|
|
968
|
+
),
|
|
969
|
+
]),
|
|
970
|
+
),
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
818
974
|
static async #loadCssBytesByUrl(
|
|
819
975
|
artifact: BaseBuildArtifact,
|
|
820
976
|
artifactDir: string,
|
|
@@ -593,20 +593,8 @@ export class SqliteDocumentStore {
|
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
async update(id: string, patch: DocumentRecord) {
|
|
596
|
-
const current = await this.
|
|
597
|
-
|
|
598
|
-
await this.runHooks("save", "update", doc, "pre");
|
|
599
|
-
await this.runHooks("update", "update", doc, "pre");
|
|
600
|
-
const row = this.toRow(doc);
|
|
601
|
-
await this.owner
|
|
602
|
-
.getConnection()
|
|
603
|
-
.prepare(
|
|
604
|
-
`UPDATE ${quoteIdent(this.table)} SET "createdAt" = ?, "updatedAt" = ?, "removedAt" = ?, "_doc" = ? WHERE "id" = ?`,
|
|
605
|
-
)
|
|
606
|
-
.run(row.createdAt, row.updatedAt, row.removedAt, row._doc, id);
|
|
607
|
-
await this.runHooks("update", "update", doc, "post");
|
|
608
|
-
await this.runHooks("save", "update", doc, "post");
|
|
609
|
-
return doc;
|
|
596
|
+
const current = await this.pickByIdForWrite(id);
|
|
597
|
+
return await this.writeUpdatedDocument(id, { ...current, ...patch, id, updatedAt: dayjs() }, current);
|
|
610
598
|
}
|
|
611
599
|
|
|
612
600
|
async remove(id: string) {
|
|
@@ -614,19 +602,20 @@ export class SqliteDocumentStore {
|
|
|
614
602
|
}
|
|
615
603
|
|
|
616
604
|
async updateOneByQuery(query: DocumentQuery, update: DocumentUpdate, options: DocumentUpdateOptions = {}) {
|
|
617
|
-
const doc = await this.
|
|
605
|
+
const doc = await this.findOneForWrite(query);
|
|
618
606
|
if (!doc) {
|
|
619
607
|
if (!options.upsert) return { acknowledged: true, matchedCount: 0, modifiedCount: 0, upsertedId: null };
|
|
620
608
|
const inserted = await this.create(this.applyDocumentUpdate(this.extractInsertBase(query), update, true));
|
|
621
609
|
return { acknowledged: true, matchedCount: 0, modifiedCount: 1, upsertedId: inserted.id };
|
|
622
610
|
}
|
|
623
|
-
await this.
|
|
611
|
+
await this.writeUpdatedDocument(doc.id as string, this.applyDocumentUpdate(doc, update), doc);
|
|
624
612
|
return { acknowledged: true, matchedCount: 1, modifiedCount: 1, upsertedId: null };
|
|
625
613
|
}
|
|
626
614
|
|
|
627
615
|
async updateManyByQuery(query: DocumentQuery, update: DocumentUpdate) {
|
|
628
|
-
const docs = await this.
|
|
629
|
-
for (const doc of docs)
|
|
616
|
+
const docs = await this.findForWrite(query);
|
|
617
|
+
for (const doc of docs)
|
|
618
|
+
await this.writeUpdatedDocument(doc.id as string, this.applyDocumentUpdate(doc, update), doc);
|
|
630
619
|
return { acknowledged: true, matchedCount: docs.length, modifiedCount: docs.length };
|
|
631
620
|
}
|
|
632
621
|
|
|
@@ -656,12 +645,12 @@ export class SqliteDocumentStore {
|
|
|
656
645
|
const limit = limitValue ? ` LIMIT ${limitValue}` : "";
|
|
657
646
|
const offset = skipValue ? ` OFFSET ${skipValue}` : "";
|
|
658
647
|
const order = options.sample ? "ORDER BY random()" : `ORDER BY ${this.compiler.orderBy(options.sort ?? undefined)}`;
|
|
659
|
-
const projection = this.
|
|
648
|
+
const projection = this.resolveProjection(options.select);
|
|
660
649
|
if (projection) {
|
|
661
650
|
const rows = await this.prepareReadStmt(
|
|
662
651
|
`SELECT ${this.projectionSql(projection)} FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`,
|
|
663
652
|
).all<ProjectedSqliteDocumentRow>(...params);
|
|
664
|
-
return rows.map((row) => this.fromProjectedRow(row, projection));
|
|
653
|
+
return rows.map((row) => this.hydrate(this.fromProjectedRow(row, projection)));
|
|
665
654
|
}
|
|
666
655
|
const rows = await this.prepareReadStmt(
|
|
667
656
|
`SELECT * FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`,
|
|
@@ -922,10 +911,22 @@ export class SqliteDocumentStore {
|
|
|
922
911
|
const fields = Object.entries(select)
|
|
923
912
|
.filter(([, included]) => included)
|
|
924
913
|
.map(([field]) => field);
|
|
925
|
-
if (!fields.length) return null;
|
|
926
914
|
return [...new Set(fields.filter((field) => field !== "_doc"))];
|
|
927
915
|
}
|
|
928
916
|
|
|
917
|
+
private resolveProjection(select: ProjectionOption): string[] | null {
|
|
918
|
+
const projection = this.normalizeProjection(select);
|
|
919
|
+
if (projection !== null) return projection;
|
|
920
|
+
return this.defaultProjection();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
private defaultProjection(): string[] | null {
|
|
924
|
+
const fields = this.database.doc[FIELD_META] as unknown as FieldMap;
|
|
925
|
+
const entries = Object.entries(fields).filter(([key]) => !BASE_COLUMNS.has(key));
|
|
926
|
+
if (!entries.some(([, field]) => field.getProps().select === false)) return null;
|
|
927
|
+
return entries.flatMap(([key, field]) => (field.getProps().select === false ? [] : [key]));
|
|
928
|
+
}
|
|
929
|
+
|
|
929
930
|
private projectionSql(fields: string[]) {
|
|
930
931
|
const jsonFields = fields.filter((field) => !BASE_COLUMNS.has(field));
|
|
931
932
|
const baseColumns = [...BASE_COLUMNS].map((field) => quoteIdent(field));
|
|
@@ -942,19 +943,65 @@ export class SqliteDocumentStore {
|
|
|
942
943
|
private fromProjectedRow(row: ProjectedSqliteDocumentRow, fields: string[]) {
|
|
943
944
|
const doc: DocumentRecord = {
|
|
944
945
|
id: row.id,
|
|
945
|
-
createdAt: Number(row.createdAt),
|
|
946
|
-
updatedAt: Number(row.updatedAt),
|
|
947
|
-
removedAt: row.removedAt ? Number(row.removedAt) : undefined,
|
|
946
|
+
createdAt: dayjs(Number(row.createdAt)),
|
|
947
|
+
updatedAt: dayjs(Number(row.updatedAt)),
|
|
948
|
+
removedAt: row.removedAt ? dayjs(Number(row.removedAt)) : undefined,
|
|
948
949
|
};
|
|
949
950
|
const jsonFields = fields.filter((field) => !BASE_COLUMNS.has(field));
|
|
950
951
|
for (const [idx, field] of jsonFields.entries()) {
|
|
951
952
|
const value = this.parseProjectedValue(row[this.projectionAlias(idx)]);
|
|
952
953
|
const props = (this.database.doc[FIELD_META] as unknown as FieldMap)[field]?.getProps?.();
|
|
953
|
-
|
|
954
|
+
if (value === null && props?.default !== undefined && props.default !== null && !props.nullable) {
|
|
955
|
+
doc[field] =
|
|
956
|
+
typeof props.default === "function" ? (props.default as (data: unknown) => unknown)(doc) : props.default;
|
|
957
|
+
} else {
|
|
958
|
+
doc[field] = props ? this.decodeFieldValue(value, props) : value;
|
|
959
|
+
}
|
|
954
960
|
}
|
|
955
961
|
return doc;
|
|
956
962
|
}
|
|
957
963
|
|
|
964
|
+
private async findForWrite(query?: DocumentQuery, options: FindManyOptions = {}) {
|
|
965
|
+
const { where, params } = this.safeQuery(query);
|
|
966
|
+
const limitValue = Number(options.limit ?? 0);
|
|
967
|
+
const skipValue = Number(options.skip ?? 0);
|
|
968
|
+
const limit = limitValue ? ` LIMIT ${limitValue}` : "";
|
|
969
|
+
const offset = skipValue ? ` OFFSET ${skipValue}` : "";
|
|
970
|
+
const order = options.sample ? "ORDER BY random()" : `ORDER BY ${this.compiler.orderBy(options.sort ?? undefined)}`;
|
|
971
|
+
const rows = await this.prepareReadStmt(
|
|
972
|
+
`SELECT * FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`,
|
|
973
|
+
).all<SqliteDocumentRow>(...params);
|
|
974
|
+
return rows.map((row) => this.hydrate(this.fromRow(row)));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
private async findOneForWrite(query?: DocumentQuery, options: FindOneOptions = {}) {
|
|
978
|
+
return (
|
|
979
|
+
(await this.findForWrite(query, { ...options, limit: 1, sample: options.sample ? 1 : undefined })).at(0) ?? null
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private async pickByIdForWrite(id: string) {
|
|
984
|
+
const doc = await this.findOneForWrite({ id } as DocumentQuery);
|
|
985
|
+
if (!doc) throw new Error(`No Document (${this.table}): ${id}`);
|
|
986
|
+
return doc;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
private async writeUpdatedDocument(id: string, data: DocumentRecord, originalData: DocumentRecord) {
|
|
990
|
+
const doc = this.hydrate(this.prepareDocument({ ...data, id, updatedAt: dayjs() }), originalData);
|
|
991
|
+
await this.runHooks("save", "update", doc, "pre");
|
|
992
|
+
await this.runHooks("update", "update", doc, "pre");
|
|
993
|
+
const row = this.toRow(doc);
|
|
994
|
+
await this.owner
|
|
995
|
+
.getConnection()
|
|
996
|
+
.prepare(
|
|
997
|
+
`UPDATE ${quoteIdent(this.table)} SET "createdAt" = ?, "updatedAt" = ?, "removedAt" = ?, "_doc" = ? WHERE "id" = ?`,
|
|
998
|
+
)
|
|
999
|
+
.run(row.createdAt, row.updatedAt, row.removedAt, row._doc, id);
|
|
1000
|
+
await this.runHooks("update", "update", doc, "post");
|
|
1001
|
+
await this.runHooks("save", "update", doc, "post");
|
|
1002
|
+
return doc;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
958
1005
|
private parseProjectedValue(value: unknown) {
|
|
959
1006
|
if (typeof value !== "string") return value;
|
|
960
1007
|
const trimmed = value.trim();
|
package/signal/signalContext.ts
CHANGED
|
@@ -249,7 +249,7 @@ export class SignalContext<
|
|
|
249
249
|
const resolvedValue = {} as RuntimeRecord;
|
|
250
250
|
await Promise.all(
|
|
251
251
|
Object.entries((returnRef as ConstantCls)[FIELD_META]).map(async ([key, field]) => {
|
|
252
|
-
if (field.fieldType === "hidden") return;
|
|
252
|
+
if (field.fieldType === "hidden" || field.fieldType === "secret") return;
|
|
253
253
|
else if (field.fieldType === "resolve") {
|
|
254
254
|
const refName = ConstantRegistry.getRefName(returnRef as ConstantCls);
|
|
255
255
|
const internal = live.internal.get(`${refName}Internal`);
|
|
@@ -110,12 +110,12 @@ declare global {
|
|
|
110
110
|
[PRIMITIVE_CLIENT_VALUE]: boolean;
|
|
111
111
|
[PRIMITIVE_DEFAULT_VALUE]: boolean;
|
|
112
112
|
[PRIMITIVE_EXAMPLE_VALUE]: boolean;
|
|
113
|
-
validate(value: boolean): boolean;
|
|
114
|
-
parseValue(input: boolean): boolean;
|
|
115
|
-
serializeValue(value: boolean): boolean;
|
|
116
|
-
_parse(input: boolean): boolean;
|
|
117
|
-
_serialize(value: boolean): boolean;
|
|
118
|
-
_checkValue(value: boolean): void;
|
|
113
|
+
validate(value: boolean | number): boolean;
|
|
114
|
+
parseValue(input: boolean | number): boolean | number;
|
|
115
|
+
serializeValue(value: boolean | number): boolean | number;
|
|
116
|
+
_parse(input: boolean | number): boolean;
|
|
117
|
+
_serialize(value: boolean | number): boolean;
|
|
118
|
+
_checkValue(value: boolean | number): void;
|
|
119
119
|
}
|
|
120
120
|
interface DateConstructor {
|
|
121
121
|
refName: "Date";
|
|
@@ -16,6 +16,12 @@ export interface PageConfig {
|
|
|
16
16
|
bottomInset?: boolean | number;
|
|
17
17
|
gesture?: boolean;
|
|
18
18
|
cache?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Opt in to guarded RSC page suffix commits when the page does not require
|
|
21
|
+
* head/metadata updates and the retained route chain head is invariant for
|
|
22
|
+
* sibling navigations under the same layout.
|
|
23
|
+
*/
|
|
24
|
+
rscPatchHeadSafe?: boolean;
|
|
19
25
|
topSafeAreaColor?: string;
|
|
20
26
|
bottomSafeAreaColor?: string;
|
|
21
27
|
}
|
|
@@ -61,9 +67,19 @@ export interface LayoutErrorProps extends LayoutNotFoundProps {
|
|
|
61
67
|
}
|
|
62
68
|
export type Head = ReactNode;
|
|
63
69
|
export type GenerateHead = (props: PageProps) => PromiseOrObject<Head | null | undefined>;
|
|
70
|
+
export interface AkanHeadSnapshotNode {
|
|
71
|
+
tag: "title" | "meta" | "link";
|
|
72
|
+
attrs?: Record<string, string>;
|
|
73
|
+
text?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface AkanHeadSnapshotV1 {
|
|
76
|
+
version: 1;
|
|
77
|
+
nodes: AkanHeadSnapshotNode[];
|
|
78
|
+
}
|
|
64
79
|
export interface ResolvedHead {
|
|
65
80
|
node: Head | null | undefined;
|
|
66
81
|
hasExplicitLanguageAlternates: boolean;
|
|
82
|
+
headSnapshot?: AkanHeadSnapshotV1;
|
|
67
83
|
}
|
|
68
84
|
export type ResolveHeadResult = Head | ResolvedHead | null | undefined;
|
|
69
85
|
export type ResolveHead = (props: PageProps) => PromiseOrObject<ResolveHeadResult>;
|