@finesoft/front 0.5.0 → 0.5.2

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 (128) hide show
  1. package/README.md +4 -4
  2. package/dist/Outlet.svelte +39 -0
  3. package/dist/Outlet.svelte.d.ts +7 -0
  4. package/dist/browser-DIU6Sxl3.mjs +1237 -0
  5. package/dist/browser-kFMjlLGT.d.mts +262 -0
  6. package/dist/browser.d.mts +7 -2
  7. package/dist/browser.mjs +9 -1
  8. package/dist/controller-types-CgmJ6-le.d.mts +16 -0
  9. package/dist/cookies-Bpf9VayB.d.mts +779 -0
  10. package/dist/fetch-policy-BHT8RtrL.mjs +82 -0
  11. package/dist/host-guard-DDWxLpFL.mjs +222 -0
  12. package/dist/http-B6CJqDyf.d.mts +46 -0
  13. package/dist/http-CaxrMD1A.d.mts +1 -0
  14. package/dist/http-D70PL72H.mjs +257 -0
  15. package/dist/http.d.mts +3 -0
  16. package/dist/http.mjs +2 -0
  17. package/dist/index-node.d.mts +15 -0
  18. package/dist/index-node.mjs +15 -0
  19. package/dist/index.d.mts +48 -698
  20. package/dist/index.mjs +44 -261
  21. package/dist/load-node.d.mts +5 -0
  22. package/dist/load-node.mjs +10 -0
  23. package/dist/load-portable.d.mts +5 -0
  24. package/dist/load-portable.mjs +8 -0
  25. package/dist/lru-map-BKoUAySU.mjs +50 -0
  26. package/dist/messages-CAt2QdGr.mjs +140 -0
  27. package/dist/native-contract-DuR25hYB.d.mts +14 -0
  28. package/dist/native-contract.d.mts +2 -0
  29. package/dist/native-contract.mjs +1 -0
  30. package/dist/node-D9hB4dsz.d.mts +35 -0
  31. package/dist/node.d.mts +2 -0
  32. package/dist/node.mjs +59 -0
  33. package/dist/path-CGFl2w7D.mjs +113 -0
  34. package/dist/path-CXT6xGPO.d.mts +261 -0
  35. package/dist/portable-CaxrMD1A.d.mts +1 -0
  36. package/dist/portable.d.mts +11 -0
  37. package/dist/portable.mjs +12 -0
  38. package/dist/proxy-1SphZ7x7.mjs +436 -0
  39. package/dist/proxy-2dSWO-Xw.d.mts +53 -0
  40. package/dist/public-types-BcJM-AYc.mjs +835 -0
  41. package/dist/react-DhwBRw01.d.mts +16 -0
  42. package/dist/react.d.mts +3 -0
  43. package/dist/react.mjs +29 -0
  44. package/dist/rolldown-runtime-B4iAMlE-.mjs +35 -0
  45. package/dist/secure-fetch-Xlht2jd7.d.mts +30 -0
  46. package/dist/server-controller-proxy-BkVuVWVD.d.mts +38 -0
  47. package/dist/session-DnB4ZC3x.d.mts +1279 -0
  48. package/dist/src-Ftl_0rhu.mjs +28 -0
  49. package/dist/src-qwx7Vw8g.mjs +3807 -0
  50. package/dist/ssr-BEUNDvbj.d.mts +210 -0
  51. package/dist/ssr-C8xnYXoY.mjs +357 -0
  52. package/dist/ssr.d.mts +3 -0
  53. package/dist/ssr.mjs +3 -0
  54. package/dist/svelte-Dr5to3SE.d.mts +16 -0
  55. package/dist/svelte.d.mts +3 -0
  56. package/dist/svelte.mjs +13 -0
  57. package/dist/typegen-C-WeJCtf.d.mts +12 -0
  58. package/dist/typegen-cli.d.mts +1 -0
  59. package/dist/typegen-cli.mjs +11 -0
  60. package/dist/typegen.d.mts +3 -0
  61. package/dist/typegen.mjs +2 -0
  62. package/dist/types-BuaZHRG7.mjs +402 -0
  63. package/dist/undici-CPfL25Hr.mjs +22262 -0
  64. package/dist/vite-Cj4SPA8D.d.mts +277 -0
  65. package/dist/vite.d.mts +4 -0
  66. package/dist/vite.mjs +2354 -0
  67. package/dist/vue-DGmzuKho.d.mts +32 -0
  68. package/dist/vue.d.mts +3 -0
  69. package/dist/vue.mjs +56 -0
  70. package/dist/web.d.mts +6 -0
  71. package/dist/web.mjs +8 -0
  72. package/dist/worker.d.mts +2 -0
  73. package/dist/worker.mjs +2 -0
  74. package/docs/01-getting-started.md +67 -199
  75. package/docs/02-routing-and-controllers.md +163 -241
  76. package/docs/03-middleware.md +10 -212
  77. package/docs/04-rendering-and-hydration.md +6 -333
  78. package/docs/05-i18n.md +7 -237
  79. package/docs/06-http-client.md +20 -263
  80. package/docs/07-di-container.md +23 -257
  81. package/docs/08-observability.md +6 -286
  82. package/docs/09-server-and-deployment.md +59 -219
  83. package/docs/10-features-platform-pwa.md +7 -231
  84. package/docs/11-navigation.md +143 -288
  85. package/docs/12-session-restoration.md +6 -214
  86. package/docs/README.md +7 -7
  87. package/docs/advanced/custom-action-handler.md +29 -229
  88. package/docs/advanced/custom-adapter.md +7 -259
  89. package/docs/advanced/custom-event-recorder.md +7 -312
  90. package/docs/advanced/inline-proxy-codegen.md +6 -185
  91. package/docs/advanced/multi-tenant-scopes.md +10 -323
  92. package/docs/engineering/ci-release-flow.md +35 -222
  93. package/docs/engineering/project-structure.md +34 -277
  94. package/docs/engineering/testing.md +8 -310
  95. package/docs/pitfalls/container-scope-leak.md +2 -214
  96. package/docs/pitfalls/i18n-bundle-size.md +6 -176
  97. package/docs/pitfalls/proxy-binary-payloads.md +8 -12
  98. package/docs/pitfalls/ssr-hydration-mismatch.md +4 -160
  99. package/docs/pitfalls/ssr-vs-csr-globals.md +9 -170
  100. package/docs/zh/01-getting-started.md +67 -199
  101. package/docs/zh/02-routing-and-controllers.md +166 -244
  102. package/docs/zh/03-middleware.md +10 -212
  103. package/docs/zh/04-rendering-and-hydration.md +6 -333
  104. package/docs/zh/05-i18n.md +7 -237
  105. package/docs/zh/06-http-client.md +20 -263
  106. package/docs/zh/07-di-container.md +23 -257
  107. package/docs/zh/08-observability.md +6 -283
  108. package/docs/zh/09-server-and-deployment.md +59 -219
  109. package/docs/zh/10-features-platform-pwa.md +7 -231
  110. package/docs/zh/11-navigation.md +130 -290
  111. package/docs/zh/12-session-restoration.md +6 -214
  112. package/docs/zh/README.md +4 -4
  113. package/docs/zh/advanced/custom-action-handler.md +29 -229
  114. package/docs/zh/advanced/custom-adapter.md +7 -259
  115. package/docs/zh/advanced/custom-event-recorder.md +7 -312
  116. package/docs/zh/advanced/inline-proxy-codegen.md +6 -185
  117. package/docs/zh/advanced/multi-tenant-scopes.md +10 -323
  118. package/docs/zh/engineering/ci-release-flow.md +35 -222
  119. package/docs/zh/engineering/project-structure.md +34 -277
  120. package/docs/zh/engineering/testing.md +8 -310
  121. package/docs/zh/pitfalls/container-scope-leak.md +2 -214
  122. package/docs/zh/pitfalls/i18n-bundle-size.md +6 -176
  123. package/docs/zh/pitfalls/proxy-binary-payloads.md +8 -12
  124. package/docs/zh/pitfalls/ssr-hydration-mismatch.md +4 -160
  125. package/docs/zh/pitfalls/ssr-vs-csr-globals.md +9 -170
  126. package/package.json +118 -20
  127. package/dist/browser-BYZq9Jp7.mjs +0 -2
  128. package/dist/browser-JTs2jqVY.d.mts +0 -2811
@@ -0,0 +1,3807 @@
1
+ import { i as HttpError, n as classifyUrl, r as HostGuardError } from "./host-guard-DDWxLpFL.mjs";
2
+ import { n as ExecutionError, r as executionErrorFromHttp, t as compilePath } from "./path-CGFl2w7D.mjs";
3
+ import { t as LruMap } from "./lru-map-BKoUAySU.mjs";
4
+ import { t as DEP_KEYS } from "./src-Ftl_0rhu.mjs";
5
+ //#region ../core/src/dependencies/container.ts
6
+ var Container = class Container {
7
+ parent;
8
+ children = /* @__PURE__ */ new Set();
9
+ providers = /* @__PURE__ */ new Map();
10
+ values = /* @__PURE__ */ new Map();
11
+ pending = /* @__PURE__ */ new Set();
12
+ cleanups = [];
13
+ closed = false;
14
+ disposal;
15
+ bindings = Object.freeze({});
16
+ registerProvider(provider) {
17
+ this.assertOpen();
18
+ this.providers.set(provider.token, provider);
19
+ if (provider.lifetime === "runtime" && Object.hasOwn(provider, "value") && provider.owned && provider.dispose) this.cleanups.push({
20
+ provider,
21
+ dispose: () => provider.dispose(provider.value)
22
+ });
23
+ return this;
24
+ }
25
+ onDispose(cleanup) {
26
+ this.assertOpen();
27
+ this.cleanups.push({ dispose: cleanup });
28
+ }
29
+ hasProvider(token) {
30
+ return this.providers.has(token) || this.parent?.hasProvider(token) === true;
31
+ }
32
+ assertOpen() {
33
+ if (this.closed) throw new Error("Container is closed");
34
+ }
35
+ async get(token) {
36
+ this.assertOpen();
37
+ return this.getProvider(token, []);
38
+ }
39
+ findProvider(token) {
40
+ const provider = this.providers.get(token);
41
+ if (provider) return {
42
+ provider,
43
+ owner: this
44
+ };
45
+ if (this.parent) return this.parent.findProvider(token);
46
+ throw new Error(`Unknown provider: ${token.id}`);
47
+ }
48
+ getProvider(token, path) {
49
+ if (path.includes(token)) return Promise.reject(/* @__PURE__ */ new Error(`Cyclic provider dependency: ${token.id}`));
50
+ const { provider, owner } = this.findProvider(token);
51
+ const target = provider.lifetime === "runtime" ? owner : this;
52
+ const existing = provider.lifetime !== "transient" && target.values.get(token);
53
+ if (existing) return existing;
54
+ const nextPath = [...path, token];
55
+ const promise = Promise.resolve().then(async () => {
56
+ let creating = true;
57
+ const dependencies = /* @__PURE__ */ new Map();
58
+ const context = {
59
+ bindings: provider.lifetime === "runtime" ? Object.freeze({}) : target.bindings,
60
+ get: async (dependency) => {
61
+ if (!creating) throw new Error(`Provider resolver is closed: ${token.id}`);
62
+ if (!provider.dependencies?.includes(dependency)) throw new Error(`Undeclared dependency: ${token.id} -> ${dependency.id}`);
63
+ const dep = target.findProvider(dependency).provider;
64
+ if (nextPath.some((t) => target.findProvider(t).provider.lifetime === "runtime") && dep.lifetime === "scope") throw new Error(`runtime provider cannot resolve scope: ${dependency.id}`);
65
+ let value = dependencies.get(dependency);
66
+ if (!value) {
67
+ value = target.getProvider(dependency, nextPath);
68
+ dependencies.set(dependency, value);
69
+ }
70
+ return value;
71
+ }
72
+ };
73
+ try {
74
+ for (const dependency of provider.dependencies ?? []) await context.get(dependency);
75
+ const value = provider.create ? await provider.create(context) : provider.value;
76
+ if (provider.dispose && (provider.owned ?? !!provider.create) && (provider.create || provider.lifetime !== "runtime")) target.cleanups.push({
77
+ provider,
78
+ dispose: () => provider.dispose(value)
79
+ });
80
+ return value;
81
+ } finally {
82
+ creating = false;
83
+ }
84
+ });
85
+ target.pending.add(promise);
86
+ if (provider.lifetime !== "transient") target.values.set(token, promise);
87
+ promise.then(() => target.pending.delete(promise), () => {
88
+ target.pending.delete(promise);
89
+ if (target.values.get(token) === promise) target.values.delete(token);
90
+ });
91
+ return promise;
92
+ }
93
+ /** Sort owned cleanup by dependencies, retaining creation order for unrelated resources. */
94
+ orderedCleanups() {
95
+ const ordered = [];
96
+ const visited = /* @__PURE__ */ new Set();
97
+ const visit = (cleanup) => {
98
+ if (visited.has(cleanup)) return;
99
+ visited.add(cleanup);
100
+ const dependencies = (provider, path) => {
101
+ for (const token of provider.dependencies ?? []) {
102
+ if (path.has(token)) continue;
103
+ path.add(token);
104
+ dependencies(this.findProvider(token).provider, path);
105
+ for (const dependency of this.cleanups) if (dependency.provider?.token === token) visit(dependency);
106
+ }
107
+ };
108
+ if (cleanup.provider) dependencies(cleanup.provider, /* @__PURE__ */ new Set());
109
+ ordered.push(cleanup);
110
+ };
111
+ for (const cleanup of this.cleanups) visit(cleanup);
112
+ return ordered.reverse();
113
+ }
114
+ /**
115
+ * 创建子容器(请求级 scope)
116
+ *
117
+ * 子容器可覆写父容器的依赖(如每请求的 locale、user),
118
+ * 未覆写的 key 自动回退到父容器解析。子容器会被父容器跟踪,
119
+ * 父容器 dispose 时一并销毁所有未独立 dispose 的子容器。
120
+ */
121
+ createScope(bindings = {}) {
122
+ this.assertOpen();
123
+ const child = new Container();
124
+ child.bindings = Object.freeze({ ...bindings });
125
+ child.parent = this;
126
+ this.children.add(child);
127
+ return child;
128
+ }
129
+ /**
130
+ * 销毁容器,清除所有缓存。
131
+ *
132
+ * - 递归 dispose 所有 createScope() 创建的未 dispose 子容器
133
+ * - 自身被 dispose 后从父容器移除引用,允许 GC
134
+ * - 重复 dispose 安全(幂等)
135
+ */
136
+ dispose() {
137
+ if (this.disposal) return this.disposal;
138
+ this.closed = true;
139
+ const children = [...this.children].map((child) => child.dispose());
140
+ this.children.clear();
141
+ this.disposal = (async () => {
142
+ const errors = [];
143
+ for (const result of await Promise.allSettled(children)) if (result.status === "rejected") errors.push(result.reason);
144
+ while (this.pending.size) await Promise.allSettled(this.pending);
145
+ for (const cleanup of this.orderedCleanups()) try {
146
+ await cleanup.dispose();
147
+ } catch (error) {
148
+ errors.push(error);
149
+ }
150
+ this.cleanups = [];
151
+ this.providers.clear();
152
+ this.values.clear();
153
+ this.parent?.children.delete(this);
154
+ this.parent = void 0;
155
+ if (errors.length) throw new AggregateError(errors, "Container cleanup failed");
156
+ })();
157
+ return this.disposal;
158
+ }
159
+ };
160
+ //#endregion
161
+ //#region ../core/src/schema/multi.ts
162
+ /** 运行时判断一个 schema 是否为多值 codec(resolve 据此决定取全部值还是单值)。 */
163
+ function isMultiValueSchema(schema) {
164
+ return schema.multi === true;
165
+ }
166
+ /**
167
+ * 多值 query 原语:用内部单值 codec 逐项校验/转换,产出数组。
168
+ * 缺失(undefined)→ 空数组;任一项校验失败 → 整体失败(resolve 据此 fall-through)。
169
+ */
170
+ function list(item, opts = {}) {
171
+ return {
172
+ multi: true,
173
+ "~standard": {
174
+ version: 1,
175
+ vendor: "finesoft",
176
+ validate: async (value) => {
177
+ const raw = value === void 0 ? [] : Array.isArray(value) ? value : [value];
178
+ const out = [];
179
+ for (const v of raw) {
180
+ const res = await item["~standard"].validate(v);
181
+ if (res.issues) return { issues: res.issues };
182
+ out.push(res.value);
183
+ }
184
+ if (opts.min !== void 0 && out.length < opts.min) return { issues: [{ message: `must have at least ${opts.min} item(s)` }] };
185
+ if (opts.max !== void 0 && out.length > opts.max) return { issues: [{ message: `must have at most ${opts.max} item(s)` }] };
186
+ return { value: out };
187
+ }
188
+ }
189
+ };
190
+ }
191
+ //#endregion
192
+ //#region ../core/src/logger/composite.ts
193
+ var CompositeLoggerFactory = class {
194
+ factories;
195
+ constructor(factories) {
196
+ this.factories = factories;
197
+ }
198
+ loggerFor(name) {
199
+ return new CompositeLogger(this.factories.map((f) => f.loggerFor(name)));
200
+ }
201
+ };
202
+ var CompositeLogger = class {
203
+ loggers;
204
+ constructor(loggers) {
205
+ this.loggers = loggers;
206
+ }
207
+ debug(...args) {
208
+ this.callAll("debug", args);
209
+ }
210
+ info(...args) {
211
+ this.callAll("info", args);
212
+ }
213
+ warn(...args) {
214
+ this.callAll("warn", args);
215
+ }
216
+ error(...args) {
217
+ this.callAll("error", args);
218
+ }
219
+ callAll(method, args) {
220
+ for (const logger of this.loggers) logger[method](...args);
221
+ }
222
+ };
223
+ //#endregion
224
+ //#region ../core/src/logger/console.ts
225
+ var ConsoleLogger = class {
226
+ category;
227
+ shouldLog;
228
+ constructor(category, shouldLog = () => true) {
229
+ this.category = category;
230
+ this.shouldLog = shouldLog;
231
+ }
232
+ debug(...args) {
233
+ if (this.shouldLog(this.category, "debug")) console.debug(`[${this.category}]`, ...args);
234
+ }
235
+ info(...args) {
236
+ if (this.shouldLog(this.category, "info")) console.info(`[${this.category}]`, ...args);
237
+ }
238
+ warn(...args) {
239
+ if (this.shouldLog(this.category, "warn")) console.warn(`[${this.category}]`, ...args);
240
+ }
241
+ error(...args) {
242
+ console.error(`[${this.category}]`, ...args);
243
+ }
244
+ };
245
+ var ConsoleLoggerFactory = class {
246
+ shouldLog;
247
+ constructor(shouldLog = () => true) {
248
+ this.shouldLog = shouldLog;
249
+ }
250
+ loggerFor(category) {
251
+ return new ConsoleLogger(category, this.shouldLog);
252
+ }
253
+ };
254
+ //#endregion
255
+ //#region ../core/src/logger/reporting.ts
256
+ const LEVEL_PRIORITY = {
257
+ debug: 0,
258
+ info: 1,
259
+ warn: 2,
260
+ error: 3
261
+ };
262
+ var ReportingLogger = class {
263
+ category;
264
+ minPriority;
265
+ report;
266
+ constructor(category, options) {
267
+ this.category = category;
268
+ this.minPriority = LEVEL_PRIORITY[options.minLevel ?? "warn"];
269
+ this.report = options.report;
270
+ }
271
+ debug(...args) {
272
+ this.maybeReport("debug", args);
273
+ }
274
+ info(...args) {
275
+ this.maybeReport("info", args);
276
+ }
277
+ warn(...args) {
278
+ this.maybeReport("warn", args);
279
+ }
280
+ error(...args) {
281
+ this.maybeReport("error", args);
282
+ }
283
+ maybeReport(level, args) {
284
+ if (LEVEL_PRIORITY[level] < this.minPriority) return;
285
+ try {
286
+ this.report(level, this.category, args);
287
+ } catch (e) {
288
+ console.error("[ReportingLogger] report callback threw:", e);
289
+ }
290
+ }
291
+ };
292
+ var ReportingLoggerFactory = class {
293
+ options;
294
+ constructor(options) {
295
+ this.options = options;
296
+ }
297
+ loggerFor(category) {
298
+ return new ReportingLogger(category, this.options);
299
+ }
300
+ };
301
+ //#endregion
302
+ //#region ../core/src/utils/stable-stringify.ts
303
+ /**
304
+ * stableStringify — 确定性 JSON 序列化(keys 按字母排序)
305
+ *
306
+ * 用作缓存 key:相同内容的对象始终产生相同字符串。
307
+ */
308
+ /** 最大递归深度 */
309
+ const MAX_DEPTH = 50;
310
+ function stableStringify(obj) {
311
+ return stringifyWithContext(obj, /* @__PURE__ */ new Set(), 0);
312
+ }
313
+ function stringifyWithContext(obj, seen, depth) {
314
+ if (obj === null || obj === void 0) return String(obj);
315
+ if (typeof obj !== "object") return JSON.stringify(obj);
316
+ if (depth > MAX_DEPTH) return "\"[Max Depth]\"";
317
+ if (seen.has(obj)) return "\"[Circular]\"";
318
+ seen.add(obj);
319
+ try {
320
+ let result;
321
+ if (Array.isArray(obj)) result = "[" + obj.map((v) => stringifyWithContext(v, seen, depth + 1)).join(",") + "]";
322
+ else {
323
+ const keys = Object.keys(obj).sort();
324
+ const parts = [];
325
+ for (const k of keys) {
326
+ const v = obj[k];
327
+ if (v !== void 0) parts.push(JSON.stringify(k) + ":" + stringifyWithContext(v, seen, depth + 1));
328
+ }
329
+ result = "{" + parts.join(",") + "}";
330
+ }
331
+ return result;
332
+ } finally {
333
+ seen.delete(obj);
334
+ }
335
+ }
336
+ //#endregion
337
+ //#region ../core/src/http/target-guard.ts
338
+ /** Shared target policy for HttpClient and secureFetch. */
339
+ async function enforceHostGuard(url, options = {}) {
340
+ let parsed;
341
+ try {
342
+ parsed = new URL(url);
343
+ } catch {
344
+ parsed = new URL(url, "http://relative.invalid/");
345
+ if (parsed.origin === "http://relative.invalid") return;
346
+ }
347
+ const verdict = classifyUrl(parsed.href);
348
+ if (!verdict.ok) throw new HostGuardError(url, verdict.reason);
349
+ if (options.validateDns === false || parsed.hostname.includes(":") || /^[0-9.]+$/.test(parsed.hostname)) return;
350
+ if (!options.lookup) throw new HostGuardError(url, "DNS lookup capability is required");
351
+ let addresses;
352
+ try {
353
+ addresses = await options.lookup(parsed.hostname);
354
+ } catch {
355
+ throw new HostGuardError(url, "DNS lookup failed");
356
+ }
357
+ if (addresses.length === 0) throw new HostGuardError(url, "DNS lookup returned no addresses");
358
+ for (const address of addresses) {
359
+ const result = classifyUrl(`http://${address.includes(":") ? `[${address}]` : address}/`);
360
+ if (!result.ok) throw new HostGuardError(url, `host resolves to ${address} (${result.reason})`);
361
+ }
362
+ }
363
+ //#endregion
364
+ //#region ../core/src/http/redirect-fetch.ts
365
+ /** One redirect owner for protected clients and fixed-origin proxies. */
366
+ async function fetchWithRedirects(fetch, input, init, validate) {
367
+ const request = typeof input === "object" && "url" in input ? input : void 0;
368
+ const mode = init?.redirect ?? request?.redirect ?? "follow";
369
+ let url = typeof input === "string" ? input : "url" in input ? input.url : input.href;
370
+ let options = init;
371
+ let method = (init?.method ?? request?.method ?? "GET").toUpperCase();
372
+ let body = init?.body ?? request?.body;
373
+ const signal = init?.signal ?? request?.signal;
374
+ for (let hop = 0;; hop++) {
375
+ signal?.throwIfAborted();
376
+ await validate(url);
377
+ signal?.throwIfAborted();
378
+ const response = await fetch(input, {
379
+ ...options,
380
+ redirect: mode === "follow" ? "manual" : mode
381
+ });
382
+ if (signal?.aborted) {
383
+ response.body?.cancel().catch(() => {});
384
+ signal.throwIfAborted();
385
+ }
386
+ if (mode !== "follow") return response;
387
+ if (response.type === "opaqueredirect") throw new TypeError("Protected fetch cannot inspect an opaque redirect");
388
+ const location = response.headers.get("location");
389
+ if (![
390
+ 301,
391
+ 302,
392
+ 303,
393
+ 307,
394
+ 308
395
+ ].includes(response.status) || location === null) {
396
+ if (hop) Object.defineProperty(response, "redirected", { value: true });
397
+ return response;
398
+ }
399
+ response.body?.cancel().catch(() => {});
400
+ if (hop === 20) throw new TypeError("Too many redirects");
401
+ const base = new URL(response.url || url, "http://relative.invalid/");
402
+ const next = new URL(location, base);
403
+ if (!/^https?:$/.test(next.protocol) || next.username || next.password) throw new TypeError("Invalid redirect target");
404
+ const nextUrl = !URL.canParse(url) && next.origin === base.origin ? next.pathname + next.search + next.hash : next.href;
405
+ const headers = new Headers(options?.headers ?? request?.headers);
406
+ if (next.origin !== base.origin) for (const name of [
407
+ "authorization",
408
+ "proxy-authorization",
409
+ "cookie",
410
+ "cookie2",
411
+ "host"
412
+ ]) headers.delete(name);
413
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
414
+ method = "GET";
415
+ body = void 0;
416
+ for (const name of [
417
+ "content-encoding",
418
+ "content-language",
419
+ "content-location",
420
+ "content-type",
421
+ "content-length"
422
+ ]) headers.delete(name);
423
+ } else if (body instanceof ReadableStream) throw new TypeError("Cannot replay a streamed request body after a redirect");
424
+ options = {
425
+ ...request ? {
426
+ credentials: request.credentials,
427
+ cache: request.cache,
428
+ integrity: request.integrity,
429
+ keepalive: request.keepalive,
430
+ mode: request.mode,
431
+ referrer: request.referrer,
432
+ referrerPolicy: request.referrerPolicy
433
+ } : {},
434
+ ...options,
435
+ method,
436
+ headers,
437
+ body,
438
+ signal
439
+ };
440
+ input = url = nextUrl;
441
+ }
442
+ }
443
+ //#endregion
444
+ //#region ../core/src/http/secure-fetch.ts
445
+ /**
446
+ * secureFetch — `fetch` with SSRF defense baked in.
447
+ *
448
+ * Wraps an existing fetch implementation and refuses targets that resolve to
449
+ * private / loopback / reserved ranges. Use when your controller fetches a
450
+ * URL that the user can influence (image proxies, link previews, callbacks).
451
+ *
452
+ * Defaults to checking IP literals (sync) AND resolving hostnames via DNS
453
+ * (requires an injected lookup; browsers explicitly select validateDns: false). Pass `allowInternalHosts: true`
454
+ * to opt out — for example, when you have a legitimate server-to-server call
455
+ * to 10.x or 127.0.0.1 and you trust the URL source.
456
+ *
457
+ * @example
458
+ * ```ts
459
+ * import { BaseController, DEP_KEYS, type ControllerInput } from "@finesoft/front";
460
+ *
461
+ * class ShareController extends BaseController<ControllerInput<{ next?: string }>, Page> {
462
+ * async execute({ params, context }: ControllerInput<{ next?: string }>) {
463
+ * // The host configures SAFE_FETCH with a DNS lookup or an explicit supported policy.
464
+ * const fetch = await context.get(DEP_KEYS.SAFE_FETCH);
465
+ * const response = await fetch(params.next ?? "https://example.com");
466
+ * ...
467
+ * }
468
+ * }
469
+ * ```
470
+ */
471
+ /**
472
+ * Return a `fetch`-shaped function that refuses requests to private hosts
473
+ * before each request. DNS preflight alone cannot pin an arbitrary transport;
474
+ * select a host connection policy when DNS rebinding protection is required.
475
+ */
476
+ function secureFetch(baseFetch, options = {}) {
477
+ if (options.allowInternalHosts ?? false) return baseFetch;
478
+ const fetch = options.wrapFetch?.(baseFetch) ?? baseFetch;
479
+ return (input, init) => fetchWithRedirects(fetch, input, init, (url) => enforceHostGuard(url, options));
480
+ }
481
+ //#endregion
482
+ //#region ../core/src/utils/platform.ts
483
+ /**
484
+ * 从 User-Agent 字符串解析平台信息
485
+ *
486
+ * @param ua - 主机显式提供的 User-Agent 字符串,默认空字符串
487
+ * @param maxTouchPoints - 主机显式提供的触摸点数量,默认 0
488
+ */
489
+ function detectPlatform(ua = "", maxTouchPoints = 0) {
490
+ const agent = ua;
491
+ const lower = agent.toLowerCase();
492
+ return {
493
+ os: detectOS(lower),
494
+ browser: detectBrowser(lower),
495
+ engine: detectEngine(lower),
496
+ isMobile: /mobile|android|iphone|ipad|ipod/i.test(agent),
497
+ isTouch: maxTouchPoints > 0
498
+ };
499
+ }
500
+ function detectOS(ua) {
501
+ if (/iphone|ipad|ipod/.test(ua)) return "ios";
502
+ if (/android/.test(ua)) return "android";
503
+ if (/macintosh|mac os x/.test(ua)) return "macos";
504
+ if (/windows/.test(ua)) return "windows";
505
+ if (/linux/.test(ua)) return "linux";
506
+ return "unknown";
507
+ }
508
+ function detectBrowser(ua) {
509
+ if (/edg\//.test(ua)) return "edge";
510
+ if (/opr\/|opera/.test(ua)) return "opera";
511
+ if (/samsungbrowser/.test(ua)) return "samsung";
512
+ if (/chrome|crios/.test(ua) && !/edg\//.test(ua)) return "chrome";
513
+ if (/firefox|fxios/.test(ua)) return "firefox";
514
+ if (/safari/.test(ua) && !/chrome/.test(ua)) return "safari";
515
+ return "unknown";
516
+ }
517
+ function detectEngine(ua) {
518
+ if (/applewebkit/.test(ua) && !/chrome/.test(ua)) return "webkit";
519
+ if (/applewebkit/.test(ua) && /chrome/.test(ua)) return "blink";
520
+ if (/gecko\//.test(ua)) return "gecko";
521
+ return "unknown";
522
+ }
523
+ //#endregion
524
+ //#region ../core/src/utils/uuid.ts
525
+ /**
526
+ * UUID v4 生成器
527
+ */
528
+ function generateUuid() {
529
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
530
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
531
+ const r = Math.random() * 16 | 0;
532
+ return (c === "x" ? r : r & 3 | 8).toString(16);
533
+ });
534
+ }
535
+ //#endregion
536
+ //#region ../core/src/i18n/interpolate.ts
537
+ /**
538
+ * ICU 消息格式插值
539
+ *
540
+ * 支持 `{name}` 占位符替换和基础复数规则。
541
+ */
542
+ /** 将 `{key}` 占位符替换为 values 中的对应值 */
543
+ function interpolate(template, values) {
544
+ if (!values) return template;
545
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
546
+ const val = values[key];
547
+ return val !== void 0 ? String(val) : `{${key}}`;
548
+ });
549
+ }
550
+ /**
551
+ * 英语复数规则(默认)
552
+ * 0 → other, 1 → one, 2+ → other
553
+ */
554
+ function englishPlural(count) {
555
+ return count === 1 ? "one" : "other";
556
+ }
557
+ /**
558
+ * 解析带复数后缀的翻译 key
559
+ *
560
+ * 约定: `key.one`, `key.other`, `key.zero`, etc.
561
+ */
562
+ function resolvePluralKey(key, category) {
563
+ return `${key}.${category}`;
564
+ }
565
+ //#endregion
566
+ //#region ../core/src/i18n/locale.ts
567
+ /** RTL 语言列表 */
568
+ const RTL_LANGUAGES = /* @__PURE__ */ new Set([
569
+ "ar",
570
+ "arc",
571
+ "dv",
572
+ "fa",
573
+ "ha",
574
+ "he",
575
+ "khw",
576
+ "ks",
577
+ "ku",
578
+ "ps",
579
+ "ur",
580
+ "yi"
581
+ ]);
582
+ /** 检测语言是否为 RTL */
583
+ function isRtl(language) {
584
+ const primary = language.split("-")[0].toLowerCase();
585
+ return RTL_LANGUAGES.has(primary);
586
+ }
587
+ /** 获取文本方向 */
588
+ function getTextDirection(language) {
589
+ return isRtl(language) ? "rtl" : "ltr";
590
+ }
591
+ /**
592
+ * 从语言代码生成 HTML lang/dir 属性
593
+ *
594
+ * @example
595
+ * ```ts
596
+ * getLocaleAttributes("ar-SA") // { lang: "ar-SA", dir: "rtl" }
597
+ * getLocaleAttributes("en-US") // { lang: "en-US", dir: "ltr" }
598
+ * ```
599
+ */
600
+ function getLocaleAttributes(language) {
601
+ return {
602
+ lang: language,
603
+ dir: getTextDirection(language)
604
+ };
605
+ }
606
+ /**
607
+ * 构建 LocaleInfo
608
+ *
609
+ * @param language - 语言代码(如 "zh-Hans")
610
+ * @param region - 地区代码(如 "CN"),可选
611
+ */
612
+ function makeLocaleInfo(language, region) {
613
+ return {
614
+ language,
615
+ region,
616
+ bcp47: region ? `${language}-${region}` : language,
617
+ dir: getTextDirection(language)
618
+ };
619
+ }
620
+ /**
621
+ * 从 URL 前缀中提取 locale
622
+ *
623
+ * @param url - 请求 URL(如 "/zh/about")
624
+ * @param supportedLocales - 支持的 locale 列表(如 ["zh", "en", "ja"])
625
+ * @returns 匹配时返回 `{ locale, strippedUrl }`,不匹配返回 null
626
+ *
627
+ * @example
628
+ * ```ts
629
+ * resolveLocaleFromUrl("/zh/about", ["zh", "en"])
630
+ * // → { locale: "zh", strippedUrl: "/about" }
631
+ *
632
+ * resolveLocaleFromUrl("/about", ["zh", "en"])
633
+ * // → null
634
+ * ```
635
+ */
636
+ function resolveLocaleFromUrl(url, supportedLocales) {
637
+ const match = url.split("?")[0].match(/^\/([^/]+)(\/.*)?$/);
638
+ if (!match) return null;
639
+ const candidate = match[1];
640
+ const found = supportedLocales.find((l) => l.toLowerCase() === candidate.toLowerCase());
641
+ if (!found) return null;
642
+ return {
643
+ locale: found,
644
+ strippedUrl: match[2] || "/"
645
+ };
646
+ }
647
+ //#endregion
648
+ //#region ../core/src/i18n/messages.ts
649
+ /**
650
+ * Resolve `TranslationMessages` into the flat map consumed by
651
+ * `SimpleTranslator`.
652
+ */
653
+ function resolveMessages(messages, locale) {
654
+ const entries = Object.entries(messages);
655
+ if (entries.length === 0) return void 0;
656
+ if (typeof entries[0][1] === "string") return messages;
657
+ const localeMessages = messages[locale];
658
+ if (!localeMessages) return void 0;
659
+ const flat = {};
660
+ for (const [key, value] of Object.entries(localeMessages)) {
661
+ if (typeof value === "string") {
662
+ flat[key] = value;
663
+ continue;
664
+ }
665
+ for (const [suffix, text] of Object.entries(value)) flat[`${key}.${suffix}`] = text;
666
+ }
667
+ return flat;
668
+ }
669
+ //#endregion
670
+ //#region ../core/src/i18n/translator.ts
671
+ /**
672
+ * SimpleTranslator — 默认翻译器实现
673
+ *
674
+ * 从扁平的 key→string 映射提供翻译,支持 ICU 插值和复数规则。
675
+ */
676
+ var SimpleTranslator = class {
677
+ locale;
678
+ messages;
679
+ pluralRule;
680
+ fallback;
681
+ constructor(options) {
682
+ this.locale = options.locale;
683
+ this.messages = options.messages;
684
+ this.pluralRule = options.pluralRule ?? englishPlural;
685
+ this.fallback = options.fallback ?? ((key) => key);
686
+ }
687
+ t(key, values) {
688
+ const template = this.messages[key];
689
+ if (template === void 0) return this.fallback(key);
690
+ return interpolate(template, values);
691
+ }
692
+ plural(key, count, values) {
693
+ const pluralKey = resolvePluralKey(key, this.pluralRule(count));
694
+ const mergedValues = {
695
+ count,
696
+ ...values
697
+ };
698
+ return this.t(pluralKey, mergedValues);
699
+ }
700
+ };
701
+ //#endregion
702
+ //#region ../core/src/application/operation.ts
703
+ function defineOperation(operation) {
704
+ return Object.freeze({
705
+ ...operation,
706
+ policies: Object.freeze([...operation.policies ?? []]),
707
+ capabilities: Object.freeze([...operation.capabilities ?? []]),
708
+ cache: operation.cache && Object.freeze({
709
+ ...operation.cache,
710
+ tags: Object.freeze([...operation.cache.tags ?? []])
711
+ })
712
+ });
713
+ }
714
+ function implementOperation(operation, handler) {
715
+ return Object.freeze({
716
+ operation,
717
+ handler
718
+ });
719
+ }
720
+ /** A fresh controller owns each call; reusable definitions never retain request state. */
721
+ function implementController(operation, create) {
722
+ return implementOperation(operation, (input, context) => create().perform(input, context));
723
+ }
724
+ //#endregion
725
+ //#region ../core/src/application/definition.ts
726
+ function freezeDefinition(value) {
727
+ const copy = { ...value };
728
+ for (const key of [
729
+ "operations",
730
+ "implementations",
731
+ "providers",
732
+ "policies",
733
+ "dependsOn",
734
+ "modules"
735
+ ]) {
736
+ const values = copy[key];
737
+ if (Array.isArray(values)) Object.assign(copy, { [key]: Object.freeze([...values]) });
738
+ }
739
+ return Object.freeze(copy);
740
+ }
741
+ function defineModule(module) {
742
+ return freezeDefinition(module);
743
+ }
744
+ function defineApp(app) {
745
+ return freezeDefinition(app);
746
+ }
747
+ function configuration(message) {
748
+ throw new ExecutionError("configuration", message);
749
+ }
750
+ function analyze(app) {
751
+ if (!app.id) configuration("Application ID must not be empty");
752
+ const modules = /* @__PURE__ */ new Map();
753
+ for (const module of app.modules ?? []) {
754
+ if (!module.id || modules.has(module.id)) configuration(`Duplicate or empty module: ${module.id}`);
755
+ modules.set(module.id, module);
756
+ }
757
+ const ordered = [], visiting = /* @__PURE__ */ new Set(), visited = /* @__PURE__ */ new Set();
758
+ function visit(module) {
759
+ if (visiting.has(module)) configuration(`Cyclic module dependency: ${module.id}`);
760
+ if (visited.has(module)) return;
761
+ visiting.add(module);
762
+ for (const dependency of module.dependsOn ?? []) {
763
+ if (modules.get(dependency.id) !== dependency) configuration(`Unknown module: ${dependency.id}`);
764
+ visit(dependency);
765
+ }
766
+ visiting.delete(module);
767
+ visited.add(module);
768
+ ordered.push(module);
769
+ }
770
+ for (const module of modules.values()) visit(module);
771
+ const operations = /* @__PURE__ */ new Map(), implementations = /* @__PURE__ */ new Map(), providers = /* @__PURE__ */ new Map(), tokenIds = /* @__PURE__ */ new Map(), policies = /* @__PURE__ */ new Map();
772
+ for (const owner of [...ordered, app]) {
773
+ for (const op of owner.operations ?? []) {
774
+ if (!op.id || operations.has(op.id)) configuration(`Duplicate or empty operation: ${op.id}`);
775
+ if (op.kind === "command" && op.cache) configuration(`Command cannot cache: ${op.id}`);
776
+ if (op.cache && (!Number.isFinite(op.cache.ttlMs) || op.cache.ttlMs <= 0)) configuration(`Invalid cache ttl: ${op.id}`);
777
+ operations.set(op.id, op);
778
+ policies.set(op, [
779
+ ...app.policies ?? [],
780
+ ...owner === app ? [] : owner.policies ?? [],
781
+ ...op.policies ?? []
782
+ ]);
783
+ if (op.handler) implementations.set(op, {
784
+ operation: op,
785
+ handler: op.handler
786
+ });
787
+ }
788
+ for (const p of owner.providers ?? []) {
789
+ if (tokenIds.has(p.token.id)) configuration(`Duplicate provider: ${p.token.id}`);
790
+ tokenIds.set(p.token.id, p.token);
791
+ providers.set(p.token, p);
792
+ }
793
+ }
794
+ for (const owner of [...ordered, app]) for (const impl of owner.implementations ?? []) {
795
+ if (operations.get(impl.operation.id) !== impl.operation) configuration(`Unknown operation: ${impl.operation.id}`);
796
+ if (implementations.has(impl.operation)) configuration(`Duplicate implementation: ${impl.operation.id}`);
797
+ implementations.set(impl.operation, impl);
798
+ }
799
+ return {
800
+ operations,
801
+ implementations,
802
+ providers,
803
+ policies
804
+ };
805
+ }
806
+ const structures = /* @__PURE__ */ new WeakMap();
807
+ function reusable(app) {
808
+ const arraysFrozen = (value, keys) => keys.every((key) => {
809
+ const array = value[key];
810
+ return array === void 0 || Object.isFrozen(array);
811
+ });
812
+ return [app, ...app.modules ?? []].every((owner) => Object.isFrozen(owner) && arraysFrozen(owner, [
813
+ "modules",
814
+ "dependsOn",
815
+ "operations",
816
+ "implementations",
817
+ "providers",
818
+ "policies"
819
+ ]) && (owner.operations ?? []).every((op) => Object.isFrozen(op) && arraysFrozen(op, ["policies", "capabilities"]) && (!op.cache || Object.isFrozen(op.cache) && arraysFrozen(op.cache, ["tags"]))) && (owner.implementations ?? []).every(Object.isFrozen) && (owner.providers ?? []).every((provider) => Object.isFrozen(provider) && arraysFrozen(provider, ["dependencies", "capabilities"])));
820
+ }
821
+ /** Reuse immutable application structure; validate every host override and capability separately. */
822
+ function normalize(options) {
823
+ const { app } = options;
824
+ let base = structures.get(app);
825
+ if (!base) {
826
+ base = analyze(app);
827
+ if (reusable(app)) structures.set(app, base);
828
+ }
829
+ const { operations, policies } = base;
830
+ const implementations = new Map(base.implementations);
831
+ const providers = new Map(base.providers);
832
+ const overrides = /* @__PURE__ */ new Set();
833
+ for (const impl of options.implementations ?? []) {
834
+ if (operations.get(impl.operation.id) !== impl.operation) configuration(`Unknown operation: ${impl.operation.id}`);
835
+ if (overrides.has(impl.operation)) configuration(`Duplicate override: ${impl.operation.id}`);
836
+ overrides.add(impl.operation);
837
+ implementations.set(impl.operation, impl);
838
+ }
839
+ const providerOverrides = /* @__PURE__ */ new Set();
840
+ for (const p of options.providers ?? []) {
841
+ if (!providers.has(p.token)) configuration(`Unknown provider override: ${p.token.id}`);
842
+ if (providerOverrides.has(p.token)) configuration(`Duplicate provider override: ${p.token.id}`);
843
+ providerOverrides.add(p.token);
844
+ providers.set(p.token, p);
845
+ }
846
+ const checkCapabilities = (names = []) => {
847
+ for (const name of names) if (options.capabilities?.[name] == null) throw new ExecutionError("capability", `Missing capability: ${name}`);
848
+ };
849
+ for (const op of operations.values()) {
850
+ if (!implementations.has(op)) configuration(`Unbound operation: ${op.id}`);
851
+ checkCapabilities(op.capabilities?.filter((name) => name !== "fetch" || !options.invocationCapabilities?.includes("fetch")));
852
+ }
853
+ for (const p of providers.values()) {
854
+ if (typeof p.create === "function" === Object.hasOwn(p, "value")) configuration(`Provider requires exactly create or value: ${p.token.id}`);
855
+ checkCapabilities(p.capabilities);
856
+ const walk = (current, path) => {
857
+ if (path.has(current.token)) configuration(`Cyclic provider dependency: ${current.token.id}`);
858
+ if (p.lifetime === "runtime" && current.lifetime === "scope") configuration(`runtime provider ${p.token.id} cannot depend on scope provider ${current.token.id}`);
859
+ const next = new Set(path).add(current.token);
860
+ for (const token of current.dependencies ?? []) {
861
+ const dep = providers.get(token);
862
+ if (!dep) configuration(`Unknown provider: ${token.id}`);
863
+ walk(dep, next);
864
+ }
865
+ };
866
+ walk(p, /* @__PURE__ */ new Set());
867
+ }
868
+ return {
869
+ operations,
870
+ implementations,
871
+ providers,
872
+ policies
873
+ };
874
+ }
875
+ //#endregion
876
+ //#region ../core/src/application/runtime.ts
877
+ function afterSettled(promise, callback) {
878
+ promise.then(callback, callback);
879
+ }
880
+ const MAX_CACHE_KEY_DEPTH = 50;
881
+ /**
882
+ * Cache identity only accepts JSON data. A permissive serializer silently
883
+ * aliases Dates, class instances, cycles, and over-deep values, which makes a
884
+ * query cache return the wrong business result. `undefined` is intentional
885
+ * input data and therefore gets a distinct explicit representation.
886
+ */
887
+ function encodeCacheIdentity(value) {
888
+ const seen = /* @__PURE__ */ new Set();
889
+ const encode = (current, depth) => {
890
+ if (depth > MAX_CACHE_KEY_DEPTH) throw new ExecutionError("configuration", "Cache input exceeds maximum depth");
891
+ if (current === void 0) return ["undefined"];
892
+ if (current === null) return ["null"];
893
+ if (typeof current === "string" || typeof current === "boolean") return [typeof current, current];
894
+ if (typeof current === "number") {
895
+ if (!Number.isFinite(current)) throw new ExecutionError("configuration", "Cache input contains a non-finite number");
896
+ return ["number", Object.is(current, -0) ? "-0" : current];
897
+ }
898
+ if (typeof current !== "object") throw new ExecutionError("configuration", "Cache input must contain JSON values only");
899
+ if (seen.has(current)) throw new ExecutionError("configuration", "Cache input is cyclic");
900
+ const prototype = Object.getPrototypeOf(current);
901
+ if (prototype !== Object.prototype && prototype !== null && !Array.isArray(current)) throw new ExecutionError("configuration", "Cache input must not contain class instances");
902
+ seen.add(current);
903
+ try {
904
+ if (Object.getOwnPropertySymbols(current).length) throw new ExecutionError("configuration", "Cache input must not contain symbol properties");
905
+ if (Array.isArray(current)) {
906
+ if (Object.keys(current).length !== current.length || Object.keys(current).some((key, index) => key !== String(index))) throw new ExecutionError("configuration", "Cache input arrays must be dense without extra properties");
907
+ return ["array", Array.from({ length: current.length }, (_, index) => {
908
+ const descriptor = Object.getOwnPropertyDescriptor(current, String(index));
909
+ if (!Object.hasOwn(descriptor, "value")) throw new ExecutionError("configuration", "Cache input must not contain accessor properties");
910
+ return encode(descriptor.value, depth + 1);
911
+ })];
912
+ }
913
+ const entries = [];
914
+ for (const key of Object.getOwnPropertyNames(current).sort()) {
915
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
916
+ if (!descriptor.enumerable || !Object.hasOwn(descriptor, "value")) throw new ExecutionError("configuration", "Cache input must contain enumerable data properties only");
917
+ entries.push([key, encode(descriptor.value, depth + 1)]);
918
+ }
919
+ return ["object", entries];
920
+ } finally {
921
+ seen.delete(current);
922
+ }
923
+ };
924
+ return JSON.stringify(encode(value, 0));
925
+ }
926
+ function createRuntime(options) {
927
+ const plan = normalize(options);
928
+ const capacity = options.cacheCapacity ?? 100;
929
+ if (!Number.isSafeInteger(capacity) || capacity < 1) configuration("Runtime cache capacity must be a positive finite integer");
930
+ const container = new Container();
931
+ const applicationId = options.app.id;
932
+ const runtimeId = generateUuid();
933
+ const active = /* @__PURE__ */ new Set();
934
+ const cache = new LruMap(capacity);
935
+ const scopeInvalidators = /* @__PURE__ */ new Set();
936
+ const invalidateListeners = /* @__PURE__ */ new Set();
937
+ let closed = false;
938
+ let disposal;
939
+ let runtimeGeneration = 0;
940
+ for (const provider of plan.providers.values()) container.registerProvider(provider);
941
+ const record = (type, fields = {}) => {
942
+ try {
943
+ const result = options.recorder?.record(type, {
944
+ ...fields,
945
+ applicationId,
946
+ runtimeId
947
+ });
948
+ if (result && typeof result.then === "function") Promise.resolve(result).catch(() => {});
949
+ } catch {}
950
+ };
951
+ const invalidate = (inputTags) => {
952
+ const tags = Object.freeze([...inputTags]);
953
+ if (!tags.length) return;
954
+ runtimeGeneration++;
955
+ for (const [key, entry] of cache) if (entry.tags.some((tag) => tags.includes(tag))) cache.delete(key);
956
+ for (const invalidateScope of scopeInvalidators) invalidateScope(tags);
957
+ for (const listener of invalidateListeners) try {
958
+ const result = listener(tags);
959
+ if (result && typeof result.then === "function") Promise.resolve(result).catch(() => {});
960
+ } catch {}
961
+ };
962
+ const runtime = {
963
+ applicationId,
964
+ runtimeId,
965
+ invalidate,
966
+ onInvalidate(listener) {
967
+ if (closed) configuration("Runtime is closed");
968
+ invalidateListeners.add(listener);
969
+ return () => invalidateListeners.delete(listener);
970
+ },
971
+ record,
972
+ async execute(operation, input, invocation) {
973
+ const execution = runtime.createExecution(invocation);
974
+ let output;
975
+ try {
976
+ output = await execution.execute(operation, input);
977
+ } catch (error) {
978
+ await execution.dispose().catch(() => {});
979
+ throw error;
980
+ }
981
+ try {
982
+ await execution.dispose();
983
+ } catch (cause) {
984
+ throw new ExecutionError("failure", void 0, { cause });
985
+ }
986
+ return output;
987
+ },
988
+ createExecution(invocation = {}) {
989
+ if (closed) configuration("Runtime is closed");
990
+ invocation = Object.freeze({
991
+ ...invocation,
992
+ bindings: Object.freeze({ ...invocation.bindings })
993
+ });
994
+ const abort = new AbortController();
995
+ const scope = container.createScope(invocation.bindings);
996
+ const executionId = generateUuid();
997
+ const traceId = invocation.traceId ?? executionId;
998
+ const scopeCache = new LruMap(capacity);
999
+ const inFlight = /* @__PURE__ */ new Map();
1000
+ let scopeGeneration = 0;
1001
+ const invalidateScope = (tags) => {
1002
+ scopeGeneration++;
1003
+ for (const [key, entry] of scopeCache) if (entry.tags.some((tag) => tags.includes(tag))) scopeCache.delete(key);
1004
+ };
1005
+ scopeInvalidators.add(invalidateScope);
1006
+ const forwardAbort = () => abort.abort(invocation.signal?.reason);
1007
+ if (invocation.signal?.aborted) forwardAbort();
1008
+ else invocation.signal?.addEventListener("abort", forwardAbort, { once: true });
1009
+ let disposed = false;
1010
+ let disposing;
1011
+ const running = /* @__PURE__ */ new Set();
1012
+ const assertActive = () => {
1013
+ if (disposed) configuration("Execution is closed");
1014
+ if (abort.signal.aborted) throw new ExecutionError("cancelled");
1015
+ };
1016
+ const executionRecord = (type, fields = {}) => record(type, {
1017
+ ...fields,
1018
+ executionId,
1019
+ traceId
1020
+ });
1021
+ const context = Object.freeze({
1022
+ applicationId,
1023
+ runtimeId,
1024
+ executionId,
1025
+ traceId,
1026
+ signal: abort.signal,
1027
+ container: scope,
1028
+ bindings: Object.freeze({ ...invocation.bindings }),
1029
+ identity: invocation.identity,
1030
+ locale: invocation.locale,
1031
+ get: async (token) => {
1032
+ assertActive();
1033
+ return scope.get(token);
1034
+ },
1035
+ execute: (op, input) => execution.execute(op, input),
1036
+ invalidate,
1037
+ record: executionRecord,
1038
+ onDispose: (cleanup) => {
1039
+ assertActive();
1040
+ scope.onDispose(cleanup);
1041
+ },
1042
+ fetch: async (input, init) => {
1043
+ assertActive();
1044
+ const fetch = invocation.fetch ?? options.capabilities?.fetch;
1045
+ if (!fetch) throw new ExecutionError("capability", "Missing capability: fetch");
1046
+ const supplied = init?.signal ?? (typeof Request !== "undefined" && input instanceof Request ? input.signal : void 0);
1047
+ const signal = supplied ? AbortSignal.any([abort.signal, supplied]) : abort.signal;
1048
+ signal.throwIfAborted();
1049
+ const response = await fetch(input, {
1050
+ ...init,
1051
+ signal
1052
+ });
1053
+ signal.throwIfAborted();
1054
+ return response;
1055
+ }
1056
+ });
1057
+ async function execute(operation, input) {
1058
+ const start = Date.now();
1059
+ executionRecord("operation", {
1060
+ operationId: operation.id,
1061
+ phase: "start"
1062
+ });
1063
+ const complete = (cacheHit = false) => executionRecord("operation", {
1064
+ operationId: operation.id,
1065
+ phase: "complete",
1066
+ ...cacheHit ? { cacheHit: true } : {},
1067
+ durationMs: Date.now() - start
1068
+ });
1069
+ try {
1070
+ assertActive();
1071
+ if (plan.operations.get(operation.id) !== operation) configuration(`Unknown operation reference: ${operation.id}`);
1072
+ for (const name of operation.capabilities ?? []) if ((name === "fetch" ? invocation.fetch ?? options.capabilities?.fetch : options.capabilities?.[name]) == null) throw new ExecutionError("capability", `Missing capability: ${name}`);
1073
+ let validated = input;
1074
+ if (operation.input) {
1075
+ const result = await operation.input["~standard"].validate(input);
1076
+ if (result.issues) throw new ExecutionError("validation");
1077
+ validated = result.value;
1078
+ }
1079
+ for (const policy of plan.policies.get(operation) ?? []) {
1080
+ assertActive();
1081
+ await policy(validated, context);
1082
+ }
1083
+ assertActive();
1084
+ const cachePolicy = operation.cache;
1085
+ if (!cachePolicy) {
1086
+ const output = await invoke(operation, validated);
1087
+ complete();
1088
+ return output;
1089
+ }
1090
+ const cacheValue = cachePolicy.key ? cachePolicy.key(validated) : validated;
1091
+ const key = encodeCacheIdentity([
1092
+ operation.id,
1093
+ invocation.identity ?? null,
1094
+ invocation.locale ?? null,
1095
+ cacheValue
1096
+ ]);
1097
+ const selectedCache = cachePolicy.scope === "execution" ? scopeCache : cache;
1098
+ const cached = selectedCache.get(key);
1099
+ if (cached && cached.expires > Date.now()) {
1100
+ complete(true);
1101
+ return cached.value;
1102
+ }
1103
+ if (cached) selectedCache.delete(key);
1104
+ const currentRuntimeGeneration = runtimeGeneration;
1105
+ const currentScopeGeneration = scopeGeneration;
1106
+ const existing = inFlight.get(key);
1107
+ const output = await (existing && existing.runtimeGeneration === currentRuntimeGeneration && existing.scopeGeneration === currentScopeGeneration ? existing.promise : startCached(operation, validated, key, cachePolicy, selectedCache, currentRuntimeGeneration, currentScopeGeneration));
1108
+ complete();
1109
+ return output;
1110
+ } catch (cause) {
1111
+ const error = abort.signal.aborted || cause instanceof Error && cause.name === "AbortError" ? new ExecutionError("cancelled", void 0, { cause }) : cause instanceof ExecutionError ? cause : cause instanceof HttpError ? executionErrorFromHttp(cause) : new ExecutionError("failure", void 0, { cause });
1112
+ executionRecord("operation", {
1113
+ operationId: operation.id,
1114
+ phase: "error",
1115
+ code: error.code,
1116
+ durationMs: Date.now() - start
1117
+ });
1118
+ throw error;
1119
+ }
1120
+ }
1121
+ async function invoke(operation, input) {
1122
+ const implementation = plan.implementations.get(operation);
1123
+ if (!implementation) configuration(`Unbound operation: ${operation.id}`);
1124
+ let output = await implementation.handler(input, context);
1125
+ assertActive();
1126
+ if (operation.output) {
1127
+ const result = await operation.output["~standard"].validate(output);
1128
+ if (result.issues) throw new ExecutionError("failure");
1129
+ output = result.value;
1130
+ }
1131
+ assertActive();
1132
+ return output;
1133
+ }
1134
+ function startCached(operation, input, key, policy, selectedCache, expectedRuntimeGeneration, expectedScopeGeneration) {
1135
+ const promise = invoke(operation, input).then((output) => {
1136
+ if (runtimeGeneration === expectedRuntimeGeneration && scopeGeneration === expectedScopeGeneration) selectedCache.set(key, {
1137
+ value: output,
1138
+ expires: Date.now() + policy.ttlMs,
1139
+ tags: policy.tags ?? []
1140
+ });
1141
+ return output;
1142
+ });
1143
+ const entry = {
1144
+ promise,
1145
+ runtimeGeneration: expectedRuntimeGeneration,
1146
+ scopeGeneration: expectedScopeGeneration
1147
+ };
1148
+ inFlight.set(key, entry);
1149
+ afterSettled(promise, () => {
1150
+ if (inFlight.get(key) === entry) inFlight.delete(key);
1151
+ });
1152
+ return promise;
1153
+ }
1154
+ const execution = {
1155
+ context,
1156
+ cancel(reason) {
1157
+ abort.abort(reason);
1158
+ },
1159
+ execute(operation, input) {
1160
+ const promise = execute(operation, input);
1161
+ running.add(promise);
1162
+ afterSettled(promise, () => running.delete(promise));
1163
+ return promise;
1164
+ },
1165
+ dispose() {
1166
+ if (disposing) return disposing;
1167
+ disposed = true;
1168
+ abort.abort();
1169
+ invocation.signal?.removeEventListener("abort", forwardAbort);
1170
+ disposing = (async () => {
1171
+ while (running.size) await Promise.allSettled(running);
1172
+ try {
1173
+ await scope.dispose();
1174
+ } finally {
1175
+ active.delete(execution);
1176
+ scopeInvalidators.delete(invalidateScope);
1177
+ scopeCache.clear();
1178
+ inFlight.clear();
1179
+ }
1180
+ })();
1181
+ return disposing;
1182
+ }
1183
+ };
1184
+ active.add(execution);
1185
+ return execution;
1186
+ },
1187
+ dispose() {
1188
+ if (disposal) return disposal;
1189
+ closed = true;
1190
+ disposal = (async () => {
1191
+ const results = await Promise.allSettled([...active].map((scope) => scope.dispose()));
1192
+ try {
1193
+ await container.dispose();
1194
+ } finally {
1195
+ cache.clear();
1196
+ scopeInvalidators.clear();
1197
+ invalidateListeners.clear();
1198
+ }
1199
+ const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
1200
+ if (errors.length) throw new AggregateError(errors, "Execution cleanup failed");
1201
+ })();
1202
+ return disposal;
1203
+ }
1204
+ };
1205
+ return runtime;
1206
+ }
1207
+ //#endregion
1208
+ //#region ../core/src/dependencies/providers.ts
1209
+ function provide(provider) {
1210
+ return Object.freeze({
1211
+ ...provider,
1212
+ dependencies: Object.freeze([...provider.dependencies ?? []]),
1213
+ capabilities: Object.freeze([...provider.capabilities ?? []])
1214
+ });
1215
+ }
1216
+ //#endregion
1217
+ //#region ../web/src/actions/types.ts
1218
+ /** Action Kind 常量 */
1219
+ const ACTION_KINDS = {
1220
+ FLOW: "flow",
1221
+ EXTERNAL_URL: "externalUrl",
1222
+ COMPOUND: "compound",
1223
+ PUSH: "push",
1224
+ POP: "pop",
1225
+ POP_TO_ROOT: "popToRoot",
1226
+ POP_TO: "popTo",
1227
+ REPLACE_TOP: "replaceTop",
1228
+ SELECT_TAB: "selectTab",
1229
+ SELECT_COLUMN: "selectColumn",
1230
+ SET_VISIBILITY: "setVisibility",
1231
+ HYDRATE: "hydrate",
1232
+ REUSE_ENTRY: "reuseEntry",
1233
+ REFRESH: "refresh"
1234
+ };
1235
+ function isFlowAction(action) {
1236
+ return action.kind === ACTION_KINDS.FLOW;
1237
+ }
1238
+ function isExternalUrlAction(action) {
1239
+ return action.kind === ACTION_KINDS.EXTERNAL_URL;
1240
+ }
1241
+ function isCompoundAction(action) {
1242
+ return action.kind === ACTION_KINDS.COMPOUND;
1243
+ }
1244
+ function makeFlowAction(url, presentationContext) {
1245
+ return {
1246
+ kind: ACTION_KINDS.FLOW,
1247
+ url,
1248
+ presentationContext
1249
+ };
1250
+ }
1251
+ function makeExternalUrlAction(url) {
1252
+ return {
1253
+ kind: ACTION_KINDS.EXTERNAL_URL,
1254
+ url
1255
+ };
1256
+ }
1257
+ //#endregion
1258
+ //#region ../web/src/actions/dispatcher.ts
1259
+ /**
1260
+ * ActionDispatcher — Action 分发器
1261
+ *
1262
+ * 注册不同 kind 的 handler,按类型分发。
1263
+ * CompoundAction 自动展开递归执行。
1264
+ */
1265
+ /** CompoundAction 最大递归展开深度 */
1266
+ const MAX_COMPOUND_DEPTH = 32;
1267
+ var ActionDispatcher = class {
1268
+ currentResult;
1269
+ handlers = /* @__PURE__ */ new Map();
1270
+ closed = false;
1271
+ epoch = 0;
1272
+ invocations = /* @__PURE__ */ new WeakMap();
1273
+ pendingFlow;
1274
+ constructor(currentResult) {
1275
+ this.currentResult = currentResult;
1276
+ }
1277
+ get generation() {
1278
+ return this.epoch;
1279
+ }
1280
+ /** Cancel pending groups; a handler may retain its own invocation during URL admission. */
1281
+ cancel = (invocation) => {
1282
+ this.epoch++;
1283
+ const current = invocation && this.invocations.get(invocation);
1284
+ if (current) current.generation = this.epoch;
1285
+ };
1286
+ /**
1287
+ * 注册指定 kind 的 handler。
1288
+ *
1289
+ * 重复 kind 时保留第一个注册者并发出警告——这是有意设计:
1290
+ * framework 内部 handler 先注册,应用层意外覆盖会被记录而非静默生效。
1291
+ * 如需显式替换,先调用 removeAction(kind)。
1292
+ */
1293
+ onAction = (kind, handler) => {
1294
+ if (this.handlers.has(kind)) {
1295
+ console.warn(`[ActionDispatcher] kind="${kind}" already registered, skipping`);
1296
+ return;
1297
+ }
1298
+ this.handlers.set(kind, handler);
1299
+ };
1300
+ /** 移除指定 kind 的 handler(用于显式覆盖场景) */
1301
+ removeAction = (kind) => {
1302
+ return this.handlers.delete(kind);
1303
+ };
1304
+ close() {
1305
+ this.closed = true;
1306
+ this.epoch++;
1307
+ this.handlers.clear();
1308
+ this.pendingFlow = void 0;
1309
+ }
1310
+ /** 执行一个 Action(CompoundAction 递归展开,有深度限制) */
1311
+ perform = (action, invocation) => this.dispatch(action, invocation, 0);
1312
+ async dispatch(action, invocation, depth) {
1313
+ const existing = invocation && this.invocations.get(invocation);
1314
+ const context = existing ? invocation : { ...invocation };
1315
+ const group = existing ?? { generation: this.epoch };
1316
+ if (!existing) this.invocations.set(context, group);
1317
+ let flow = false;
1318
+ try {
1319
+ if (group.generation !== this.epoch) throw new ExecutionError("cancelled");
1320
+ if (this.closed) throw new ExecutionError("configuration", "Action dispatcher is closed");
1321
+ if (isCompoundAction(action)) {
1322
+ if (depth >= MAX_COMPOUND_DEPTH) throw new Error(`[ActionDispatcher] CompoundAction recursion depth exceeded (max ${MAX_COMPOUND_DEPTH})`);
1323
+ for (const subAction of action.actions) {
1324
+ if (context.signal?.aborted) throw new ExecutionError("cancelled");
1325
+ const result = await this.dispatch(subAction, context, depth + 1);
1326
+ if (this.currentResult && result !== this.currentResult()) return result;
1327
+ }
1328
+ return this.currentResult?.();
1329
+ }
1330
+ const handler = this.handlers.get(action.kind);
1331
+ if (!handler) {
1332
+ console.warn(`[ActionDispatcher] No handler for kind="${action.kind}"`);
1333
+ return this.currentResult?.();
1334
+ }
1335
+ flow = action.kind === "flow" && action.presentationContext !== "modal";
1336
+ if ((flow || action.kind !== "flow" && action.kind !== "externalUrl") && this.pendingFlow && this.pendingFlow !== group) {
1337
+ this.pendingFlow.generation = -1;
1338
+ this.pendingFlow = void 0;
1339
+ }
1340
+ if (flow) this.pendingFlow = group;
1341
+ const result = await handler(action, context);
1342
+ return result === void 0 ? this.currentResult?.() : result;
1343
+ } finally {
1344
+ if (flow && this.pendingFlow === group) this.pendingFlow = void 0;
1345
+ if (!existing) this.invocations.delete(context);
1346
+ }
1347
+ }
1348
+ };
1349
+ //#endregion
1350
+ //#region ../web/src/router/types.ts
1351
+ /** Absent and empty query values share one cache identity. */
1352
+ function routeIntent(id, params = {}, query) {
1353
+ return {
1354
+ id,
1355
+ params,
1356
+ ...query && Object.keys(query).length ? { query } : {}
1357
+ };
1358
+ }
1359
+ //#endregion
1360
+ //#region ../web/src/router/router.ts
1361
+ /** URL router — route metadata layered over Core's portable pathname matcher. */
1362
+ function createNullPrototypeRecord(source) {
1363
+ return Object.assign(Object.create(null), source);
1364
+ }
1365
+ var Router = class {
1366
+ debug;
1367
+ routes = [];
1368
+ sealed = false;
1369
+ constructor(debug) {
1370
+ this.debug = debug;
1371
+ }
1372
+ seal() {
1373
+ this.sealed = true;
1374
+ return this;
1375
+ }
1376
+ add(pattern, intentId, options = {}) {
1377
+ if (this.sealed) throw new Error("Router is sealed");
1378
+ const compiled = compilePath(pattern);
1379
+ this.routes.push({
1380
+ pattern,
1381
+ intentId,
1382
+ path: compiled.descriptor,
1383
+ compiled,
1384
+ ...options
1385
+ });
1386
+ return this;
1387
+ }
1388
+ async resolve(urlOrPath) {
1389
+ const { path, queryParams, searchParams } = this.parseUrl(urlOrPath);
1390
+ for (const route of this.routes) {
1391
+ const pathParams = route.compiled.match(path);
1392
+ if (!pathParams) continue;
1393
+ const params = createNullPrototypeRecord(pathParams);
1394
+ const query = createNullPrototypeRecord(queryParams);
1395
+ let valid = true;
1396
+ for (const parameter of route.path.parameters) {
1397
+ const codec = route.paramCodecs?.[parameter.name];
1398
+ if (!codec) continue;
1399
+ const result = await codec["~standard"].validate(pathParams[parameter.name]);
1400
+ if (result.issues) {
1401
+ this.debug?.(`[Router] route "${route.pattern}" skipped: path param "${parameter.name}" failed validation: ${result.issues[0]?.message ?? "invalid"}`);
1402
+ valid = false;
1403
+ break;
1404
+ }
1405
+ params[parameter.name] = result.value;
1406
+ }
1407
+ if (!valid) continue;
1408
+ if (route.queryCodecs) for (const name of Object.keys(route.queryCodecs)) {
1409
+ const codec = route.queryCodecs[name];
1410
+ const raw = isMultiValueSchema(codec) ? searchParams.getAll(name) : queryParams[name];
1411
+ const result = await codec["~standard"].validate(raw);
1412
+ if (result.issues) {
1413
+ this.debug?.(`[Router] route "${route.pattern}" skipped: query param "${name}" failed validation: ${result.issues[0]?.message ?? "invalid"}`);
1414
+ valid = false;
1415
+ break;
1416
+ }
1417
+ query[name] = result.value;
1418
+ }
1419
+ if (!valid) continue;
1420
+ return {
1421
+ intent: routeIntent(route.intentId, params, query),
1422
+ action: makeFlowAction(urlOrPath),
1423
+ renderMode: route.renderMode,
1424
+ cache: route.cache,
1425
+ beforeGuards: route.beforeGuards,
1426
+ afterGuards: route.afterGuards
1427
+ };
1428
+ }
1429
+ return null;
1430
+ }
1431
+ hasIntent(intentId) {
1432
+ return this.routes.some((route) => route.intentId === intentId);
1433
+ }
1434
+ getDefinitions() {
1435
+ return this.routes.map(({ pattern, intentId, path }) => Object.freeze({
1436
+ pattern,
1437
+ intentId,
1438
+ path
1439
+ }));
1440
+ }
1441
+ reverse(intentId, params, queryParams = {}) {
1442
+ const matches = this.routes.filter((route) => route.intentId === intentId).map((route) => ({
1443
+ route,
1444
+ path: route.compiled.reverse(params)
1445
+ })).filter((match) => match.path !== void 0);
1446
+ if (matches.length > 1) throw new Error(`Ambiguous route for ${intentId}; provide the matched URL`);
1447
+ const match = matches[0];
1448
+ if (!match) return void 0;
1449
+ const query = new URLSearchParams();
1450
+ for (const key of Object.keys(queryParams).sort()) {
1451
+ const values = Array.isArray(queryParams[key]) ? queryParams[key] : [queryParams[key]];
1452
+ for (const value of values) if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) query.append(key, String(value));
1453
+ }
1454
+ const serialized = query.toString();
1455
+ return serialized ? `${match.path}?${serialized}` : match.path;
1456
+ }
1457
+ parseUrl(url) {
1458
+ try {
1459
+ const parsed = new URL(url, "http://localhost");
1460
+ return {
1461
+ path: parsed.pathname,
1462
+ queryParams: createNullPrototypeRecord(Object.fromEntries(parsed.searchParams)),
1463
+ searchParams: parsed.searchParams
1464
+ };
1465
+ } catch {
1466
+ return {
1467
+ path: url.split("?")[0].split("#")[0],
1468
+ queryParams: createNullPrototypeRecord(),
1469
+ searchParams: new URLSearchParams()
1470
+ };
1471
+ }
1472
+ }
1473
+ };
1474
+ //#endregion
1475
+ //#region ../web/src/prefetched-intents/prefetched-intents.ts
1476
+ var PrefetchedIntents = class PrefetchedIntents {
1477
+ entryIds;
1478
+ intents;
1479
+ constructor(intents, entryIds = /* @__PURE__ */ new Map()) {
1480
+ this.entryIds = entryIds;
1481
+ this.intents = intents;
1482
+ }
1483
+ /** 从 PrefetchedIntent 数组创建缓存实例 */
1484
+ static fromArray(items) {
1485
+ const map = /* @__PURE__ */ new Map();
1486
+ const entries = /* @__PURE__ */ new Map();
1487
+ for (const item of items) if (item.entryId && item.intent && item.data !== void 0) {
1488
+ const key = stableStringify([item.entryId, item.intent]);
1489
+ map.set(key, item.data);
1490
+ if (item.entryId) {
1491
+ const intentKey = stableStringify(item.intent);
1492
+ entries.set(intentKey, [...entries.get(intentKey) ?? [], item.entryId]);
1493
+ }
1494
+ }
1495
+ return new PrefetchedIntents(map, entries);
1496
+ }
1497
+ /** 创建空缓存实例 */
1498
+ static empty() {
1499
+ return new PrefetchedIntents(/* @__PURE__ */ new Map());
1500
+ }
1501
+ /**
1502
+ * 获取缓存的 Intent 结果(一次性使用)。
1503
+ * 命中后从缓存中删除。
1504
+ */
1505
+ get(intent, entryId) {
1506
+ if (!entryId) return void 0;
1507
+ const key = stableStringify([entryId, intent]);
1508
+ const data = this.intents.get(key);
1509
+ if (data !== void 0) {
1510
+ this.intents.delete(key);
1511
+ return data;
1512
+ }
1513
+ }
1514
+ /** @internal Navigation-owned staged reads; direct get remains one-shot. */
1515
+ stage() {
1516
+ const original = new Map(this.intents);
1517
+ const cache = new PrefetchedIntents(new Map(original), this.entryIds);
1518
+ return {
1519
+ cache,
1520
+ commit: () => {
1521
+ for (const [key, value] of original) if (!cache.intents.has(key) && this.intents.get(key) === value) this.intents.delete(key);
1522
+ }
1523
+ };
1524
+ }
1525
+ /** Read identity metadata without consuming data or bypassing operation policies. */
1526
+ entryIdFor(intent) {
1527
+ const ids = (this.entryIds.get(stableStringify(intent)) ?? []).filter((entryId) => this.has(intent, entryId));
1528
+ return ids.length === 1 ? ids[0] : void 0;
1529
+ }
1530
+ /** 检查缓存中是否有某个 Intent 的数据 */
1531
+ has(intent, entryId) {
1532
+ return !!entryId && this.intents.has(stableStringify([entryId, intent]));
1533
+ }
1534
+ /** 缓存中的条目数 */
1535
+ get size() {
1536
+ return this.intents.size;
1537
+ }
1538
+ };
1539
+ //#endregion
1540
+ //#region ../web/src/bootstrap/define-routes.ts
1541
+ /**
1542
+ * 构造一条强类型路由定义。
1543
+ * `params` 的 key 受 `path` 字面量约束——写入 path 中不存在的参数名会编译期报错。
1544
+ *
1545
+ * @example
1546
+ * route("/product/:id", { intentId: "product", params: { id: int() } })
1547
+ */
1548
+ function route(path, def) {
1549
+ return Object.freeze({
1550
+ path,
1551
+ ...def
1552
+ });
1553
+ }
1554
+ //#endregion
1555
+ //#region ../web/src/navigation/types.ts
1556
+ /** 导航节点 Kind 常量 */
1557
+ const NAVIGATION_NODE_KINDS = {
1558
+ LEAF: "leaf",
1559
+ STACK: "stack",
1560
+ TABS: "tabs",
1561
+ SPLIT: "split"
1562
+ };
1563
+ /**
1564
+ * Split 列可见性,对标 SwiftUI `NavigationSplitViewVisibility`。
1565
+ *
1566
+ * 这是**可绑定 / 可序列化 / 可恢复的导航状态**(不是渲染样式):它决定哪些列算「可见」,
1567
+ * 进而影响 `collectVisibleDestinations` 与 SSR 预取——例如深链到 `detailOnly` 时服务端只预取 detail 列。
1568
+ *
1569
+ * - `automatic`(缺省):框架不裁剪,所有有内容的列都可见(SSR 端无视口信息时的安全默认;客户端再按视口自适应)。
1570
+ * - `all`:显式所有列可见(语义同 automatic 的全列)。
1571
+ * - `doubleColumn`:仅首列 + 末列可见(三列时隐藏中间 content 列)。
1572
+ * - `detailOnly`:仅末列(detail)可见。
1573
+ *
1574
+ * 注意:compact 视口塌缩成单栈(SwiftUI 的 `preferredCompactColumn`)是视口反应式的纯渲染决策,
1575
+ * 框架不建模,交给应用按 `getPlatform()` / 视口自行处理。
1576
+ */
1577
+ const SPLIT_VISIBILITIES = {
1578
+ AUTOMATIC: "automatic",
1579
+ ALL: "all",
1580
+ DOUBLE_COLUMN: "doubleColumn",
1581
+ DETAIL_ONLY: "detailOnly"
1582
+ };
1583
+ /** 错误类型:序列化 / 路径 / 操作非法时抛出 */
1584
+ var NavigationError = class extends Error {
1585
+ constructor(message) {
1586
+ super(message);
1587
+ this.name = "NavigationError";
1588
+ }
1589
+ };
1590
+ //#endregion
1591
+ //#region ../web/src/navigation/nodes.ts
1592
+ /**
1593
+ * Navigation — 节点构造器 + 类型守卫
1594
+ *
1595
+ * 构造器产出冻结的不可变节点;守卫用 `is*Node` 前缀做可辨识联合的窄化。
1596
+ */
1597
+ /** 构造叶子节点(一个具体导航目标)。 */
1598
+ function leaf(intent, params = {}, options = {}) {
1599
+ return {
1600
+ kind: NAVIGATION_NODE_KINDS.LEAF,
1601
+ intent,
1602
+ params,
1603
+ ...options.query && Object.keys(options.query).length ? { query: options.query } : {},
1604
+ entryId: options.entryId ?? generateUuid(),
1605
+ ...options.url ? { url: options.url } : {}
1606
+ };
1607
+ }
1608
+ /**
1609
+ * 构造栈节点。
1610
+ * 接受单个根节点(栈仅含根)或一个 entries 数组(entries[0]=根,末尾=栈顶)。
1611
+ *
1612
+ * @example
1613
+ * stack(leaf("home")) // 单根栈
1614
+ * stack([leaf("home"), leaf("detail")]) // 根 + 栈顶
1615
+ */
1616
+ function stack(rootOrEntries) {
1617
+ const entries = Array.isArray(rootOrEntries) ? [...rootOrEntries] : [rootOrEntries];
1618
+ return {
1619
+ kind: NAVIGATION_NODE_KINDS.STACK,
1620
+ entries
1621
+ };
1622
+ }
1623
+ /**
1624
+ * 构造 Tabs 节点。
1625
+ * 缺省 `order` 时按 `branches` 的插入顺序(`Object.keys`)推导稳定顺序。
1626
+ */
1627
+ function tabs(init) {
1628
+ const order = init.order ? [...init.order] : Object.keys(init.branches);
1629
+ return {
1630
+ kind: NAVIGATION_NODE_KINDS.TABS,
1631
+ active: init.active,
1632
+ order,
1633
+ branches: { ...init.branches }
1634
+ };
1635
+ }
1636
+ /**
1637
+ * 构造 Split 节点(多列并存)。
1638
+ * `visibility` 缺省(不写字段)等价 `automatic` = 全列可见;
1639
+ * 显式传入时纳入节点状态,影响 `collectVisibleDestinations` 与 SSR 预取。
1640
+ *
1641
+ * @example
1642
+ * split([{ id: "sidebar", content: leaf("folders") }, { id: "detail" }])
1643
+ * split([...], "detailOnly") // 深链:仅 detail 列可见
1644
+ */
1645
+ function split(columns, visibility) {
1646
+ const normalized = columns.map((c) => ({
1647
+ id: c.id,
1648
+ content: c.content
1649
+ }));
1650
+ return visibility === void 0 ? {
1651
+ kind: NAVIGATION_NODE_KINDS.SPLIT,
1652
+ columns: normalized
1653
+ } : {
1654
+ kind: NAVIGATION_NODE_KINDS.SPLIT,
1655
+ columns: normalized,
1656
+ visibility
1657
+ };
1658
+ }
1659
+ function isLeafNode(node) {
1660
+ return node.kind === NAVIGATION_NODE_KINDS.LEAF;
1661
+ }
1662
+ function isStackNode(node) {
1663
+ return node.kind === NAVIGATION_NODE_KINDS.STACK;
1664
+ }
1665
+ function isTabsNode(node) {
1666
+ return node.kind === NAVIGATION_NODE_KINDS.TABS;
1667
+ }
1668
+ function isSplitNode(node) {
1669
+ return node.kind === NAVIGATION_NODE_KINDS.SPLIT;
1670
+ }
1671
+ //#endregion
1672
+ //#region ../web/src/navigation/keys.ts
1673
+ /** Resource identity never identifies a page instance. */
1674
+ function resourceKey(intent, params, partition = {}, query) {
1675
+ return stableStringify([
1676
+ intent,
1677
+ partition.identity ?? null,
1678
+ partition.locale ?? null,
1679
+ params,
1680
+ ...query && Object.keys(query).length ? [query] : []
1681
+ ]);
1682
+ }
1683
+ //#endregion
1684
+ //#region ../web/src/navigation/operations.ts
1685
+ /**
1686
+ * Navigation — 纯函数操作(不可变 + 结构共享)
1687
+ *
1688
+ * 所有操作都不修改输入节点,返回带结构共享的新节点:仅被改动路径上的节点重建,
1689
+ * 其余子树原样复用。操作通过「目标路径」定位要变更的子树;省略 target 时默认作用于
1690
+ * 「激活路径」(root → tabs.active → stack 顶 → split 最后一个非空列 → …)。
1691
+ *
1692
+ * 非法目标(如对非 tabs 节点 selectTab、激活路径上没有 stack 却 push)会抛 NavigationError。
1693
+ */
1694
+ /**
1695
+ * 解析「激活路径」:从根沿可见分支一路向下,直到叶子或无法继续。
1696
+ * - leaf:路径在此结束
1697
+ * - stack:进入栈顶 entry
1698
+ * - tabs:进入 active 分支
1699
+ * - split:进入最后一个有内容的列(无任何内容则结束)
1700
+ */
1701
+ function resolveActivePath(tree) {
1702
+ return walkActive(tree);
1703
+ }
1704
+ /** 找到 split 中最后一个 content 非 undefined 的列。 */
1705
+ function lastNonEmptyColumn(node) {
1706
+ for (let i = node.columns.length - 1; i >= 0; i--) if (node.columns[i].content !== void 0) return node.columns[i];
1707
+ }
1708
+ /**
1709
+ * 按路径定位节点;任一步无效(索引越界 / 键不存在 / 列为空 / kind 不匹配)返回 undefined。
1710
+ */
1711
+ function findNode(tree, path) {
1712
+ let node = tree;
1713
+ for (const step of path) {
1714
+ if (node === void 0) return void 0;
1715
+ node = stepInto(node, step);
1716
+ }
1717
+ return node;
1718
+ }
1719
+ /** 沿一步路径下钻;不匹配返回 undefined。 */
1720
+ function stepInto(node, step) {
1721
+ switch (step.kind) {
1722
+ case "stack-entry":
1723
+ if (node.kind !== NAVIGATION_NODE_KINDS.STACK) return void 0;
1724
+ return node.entries[step.index];
1725
+ case "tab":
1726
+ if (node.kind !== NAVIGATION_NODE_KINDS.TABS) return void 0;
1727
+ return node.branches[step.key];
1728
+ case "column":
1729
+ if (node.kind !== NAVIGATION_NODE_KINDS.SPLIT) return void 0;
1730
+ return node.columns.find((c) => c.id === step.id)?.content;
1731
+ }
1732
+ }
1733
+ /**
1734
+ * 找到 target 处(默认激活路径)「at/under」最近的 StackNode 路径。
1735
+ * 从 target 节点沿激活分支向下,返回第一个遇到的 StackNode 的完整路径;
1736
+ * 找不到则返回 undefined。
1737
+ */
1738
+ function findNearestStack(tree, path) {
1739
+ const start = findNode(tree, path);
1740
+ if (start === void 0) return void 0;
1741
+ let stackPath;
1742
+ walkActive(start, (node, activePath) => {
1743
+ if (node.kind === NAVIGATION_NODE_KINDS.STACK) {
1744
+ stackPath = [...path, ...activePath];
1745
+ return true;
1746
+ }
1747
+ return false;
1748
+ });
1749
+ return stackPath;
1750
+ }
1751
+ /** Walk the active branch once, optionally observing every visited node. */
1752
+ function walkActive(tree, visit) {
1753
+ const path = [];
1754
+ let node = tree;
1755
+ for (;;) {
1756
+ if (visit?.(node, path)) return path;
1757
+ switch (node.kind) {
1758
+ case NAVIGATION_NODE_KINDS.LEAF: return path;
1759
+ case NAVIGATION_NODE_KINDS.STACK: {
1760
+ const index = node.entries.length - 1;
1761
+ const entry = node.entries[index];
1762
+ if (entry === void 0) return path;
1763
+ path.push({
1764
+ kind: "stack-entry",
1765
+ index
1766
+ });
1767
+ node = entry;
1768
+ break;
1769
+ }
1770
+ case NAVIGATION_NODE_KINDS.TABS: {
1771
+ const branch = node.branches[node.active];
1772
+ if (branch === void 0) return path;
1773
+ path.push({
1774
+ kind: "tab",
1775
+ key: node.active
1776
+ });
1777
+ node = branch;
1778
+ break;
1779
+ }
1780
+ case NAVIGATION_NODE_KINDS.SPLIT: {
1781
+ const column = lastNonEmptyColumn(node);
1782
+ if (column?.content === void 0) return path;
1783
+ path.push({
1784
+ kind: "column",
1785
+ id: column.id
1786
+ });
1787
+ node = column.content;
1788
+ break;
1789
+ }
1790
+ }
1791
+ }
1792
+ }
1793
+ /**
1794
+ * 收集所有可见的叶子目标(顺序即渲染/解析顺序)。
1795
+ * - leaf → [leaf]
1796
+ * - stack → 栈顶 entry 的可见目标
1797
+ * - tabs → active 分支的可见目标
1798
+ * - split → 每个有内容的列的可见目标,按列序拼接
1799
+ */
1800
+ function collectVisibleDestinations(tree) {
1801
+ const out = [];
1802
+ collectInto(tree, out);
1803
+ return out;
1804
+ }
1805
+ function collectInto(node, out) {
1806
+ switch (node.kind) {
1807
+ case NAVIGATION_NODE_KINDS.LEAF:
1808
+ out.push(node);
1809
+ return;
1810
+ case NAVIGATION_NODE_KINDS.STACK:
1811
+ if (node.entries.length === 0) return;
1812
+ collectInto(node.entries[node.entries.length - 1], out);
1813
+ return;
1814
+ case NAVIGATION_NODE_KINDS.TABS: {
1815
+ const branch = node.branches[node.active];
1816
+ if (branch !== void 0) collectInto(branch, out);
1817
+ return;
1818
+ }
1819
+ case NAVIGATION_NODE_KINDS.SPLIT:
1820
+ for (const col of visibleSplitColumns(node)) if (col.content !== void 0) collectInto(col.content, out);
1821
+ return;
1822
+ }
1823
+ }
1824
+ /**
1825
+ * 收集树中**全部存在**的叶子(含不可见:栈非顶 entry、未激活 tab 分支、所有非空 split 列)。
1826
+ * 区别于 `collectVisibleDestinations`(只沿可见分支)—— 保活 / 缓存 prune / 作用域保留需要
1827
+ * 全部 present 条目。顺序:栈按序、tabs 按 `Object.values(branches)` 序、split 按列序。
1828
+ */
1829
+ function collectAllLeaves(tree) {
1830
+ const out = [];
1831
+ collectAllInto(tree, out);
1832
+ return out;
1833
+ }
1834
+ function collectAllInto(node, out) {
1835
+ switch (node.kind) {
1836
+ case NAVIGATION_NODE_KINDS.LEAF:
1837
+ out.push(node);
1838
+ return;
1839
+ case NAVIGATION_NODE_KINDS.STACK:
1840
+ for (const entry of node.entries) collectAllInto(entry, out);
1841
+ return;
1842
+ case NAVIGATION_NODE_KINDS.TABS:
1843
+ for (const branch of Object.values(node.branches)) collectAllInto(branch, out);
1844
+ return;
1845
+ case NAVIGATION_NODE_KINDS.SPLIT:
1846
+ for (const col of node.columns) if (col.content !== void 0) collectAllInto(col.content, out);
1847
+ return;
1848
+ }
1849
+ }
1850
+ /**
1851
+ * 按 `visibility` 求出一个 split 节点当前**可见**的列(不裁剪空内容列——空 content 由调用方处理)。
1852
+ *
1853
+ * - `automatic`(缺省)/ `all`:全部列。
1854
+ * - `doubleColumn`:首列 + 末列(三列时隐藏中间 content 列;列数 ≤ 2 时等价全部)。
1855
+ * - `detailOnly`:仅末列(detail)。
1856
+ *
1857
+ * 应用渲染时也可用它决定该画哪几列,无需自行重实现可见性映射。
1858
+ */
1859
+ function visibleSplitColumns(node) {
1860
+ const { columns, visibility } = node;
1861
+ if (columns.length === 0) return columns;
1862
+ switch (visibility) {
1863
+ case SPLIT_VISIBILITIES.DETAIL_ONLY: return [columns[columns.length - 1]];
1864
+ case SPLIT_VISIBILITIES.DOUBLE_COLUMN: {
1865
+ const last = columns.length - 1;
1866
+ return last === 0 ? [columns[0]] : [columns[0], columns[last]];
1867
+ }
1868
+ default: return columns;
1869
+ }
1870
+ }
1871
+ /**
1872
+ * 在 `path` 指向的节点上应用 `mapper`,返回重建后的根(仅路径上的节点重建)。
1873
+ * 任一步无效抛 NavigationError。
1874
+ */
1875
+ function transformAt(tree, path, mapper) {
1876
+ if (path.length === 0) return mapper(tree);
1877
+ return rebuild(tree, path, 0, mapper);
1878
+ }
1879
+ function rebuild(node, path, depth, mapper) {
1880
+ if (depth === path.length) return mapper(node);
1881
+ const step = path[depth];
1882
+ switch (step.kind) {
1883
+ case "stack-entry": {
1884
+ if (node.kind !== NAVIGATION_NODE_KINDS.STACK) throw new NavigationError(`路径无效:在 ${node.kind} 节点上期望 stack(stack-entry 步)`);
1885
+ if (step.index < 0 || step.index >= node.entries.length) throw new NavigationError(`路径无效:stack entry 索引越界 ${step.index}`);
1886
+ const entries = node.entries.slice();
1887
+ entries[step.index] = rebuild(entries[step.index], path, depth + 1, mapper);
1888
+ return {
1889
+ ...node,
1890
+ entries
1891
+ };
1892
+ }
1893
+ case "tab": {
1894
+ if (node.kind !== NAVIGATION_NODE_KINDS.TABS) throw new NavigationError(`路径无效:在 ${node.kind} 节点上期望 tabs(tab 步)`);
1895
+ const branch = node.branches[step.key];
1896
+ if (branch === void 0) throw new NavigationError(`路径无效:tabs 分支 "${step.key}" 不存在`);
1897
+ return {
1898
+ ...node,
1899
+ branches: {
1900
+ ...node.branches,
1901
+ [step.key]: rebuild(branch, path, depth + 1, mapper)
1902
+ }
1903
+ };
1904
+ }
1905
+ case "column": {
1906
+ if (node.kind !== NAVIGATION_NODE_KINDS.SPLIT) throw new NavigationError(`路径无效:在 ${node.kind} 节点上期望 split(column 步)`);
1907
+ const idx = node.columns.findIndex((c) => c.id === step.id);
1908
+ if (idx === -1) throw new NavigationError(`路径无效:split 列 "${step.id}" 不存在`);
1909
+ const content = node.columns[idx].content;
1910
+ if (content === void 0) throw new NavigationError(`路径无效:split 列 "${step.id}" 内容为空`);
1911
+ const columns = node.columns.slice();
1912
+ columns[idx] = {
1913
+ ...columns[idx],
1914
+ content: rebuild(content, path, depth + 1, mapper)
1915
+ };
1916
+ return {
1917
+ ...node,
1918
+ columns
1919
+ };
1920
+ }
1921
+ }
1922
+ }
1923
+ /**
1924
+ * 沿激活路径(root → 可见叶子)下行,返回「最深」(top 当前可见)的 stack 路径。
1925
+ * 这是栈操作(push/pop/…)在未显式给 target 时的默认作用栈。
1926
+ * 激活路径上没有任何 stack 则返回 undefined。
1927
+ */
1928
+ function findActiveStack(tree) {
1929
+ let deepest;
1930
+ walkActive(tree, (node, path) => {
1931
+ if (node.kind === NAVIGATION_NODE_KINDS.STACK) deepest = [...path];
1932
+ return false;
1933
+ });
1934
+ return deepest;
1935
+ }
1936
+ /**
1937
+ * 在栈操作的目标栈上执行更新:
1938
+ * - 显式 target → 取 target「at/under」最近的 stack(findNearestStack 向下钻);
1939
+ * - 缺省 → 取激活路径上最深的 stack(findActiveStack)。
1940
+ */
1941
+ function updateStack(tree, target, operation, update) {
1942
+ const stackPath = target ? findNearestStack(tree, target) : findActiveStack(tree);
1943
+ if (stackPath === void 0) throw new NavigationError(`${operation} 失败:激活路径上没有可用的 stack`);
1944
+ return transformAt(tree, stackPath, (node) => update(node));
1945
+ }
1946
+ /** 在目标栈(默认激活栈,或 target「at/under」最近的 stack)顶部 push 一个节点。 */
1947
+ function push(tree, node, target) {
1948
+ return updateStack(tree, target, "push", (stack) => ({
1949
+ ...stack,
1950
+ entries: [...stack.entries, node]
1951
+ }));
1952
+ }
1953
+ /** 从 target 处最近的 stack 弹出 count 个 entry(默认 1);绝不弹到根 entry 之下。 */
1954
+ function pop(tree, count = 1, target) {
1955
+ if (count <= 0) return tree;
1956
+ return updateStack(tree, target, "pop", (stack) => {
1957
+ const keep = Math.max(1, stack.entries.length - count);
1958
+ return keep === stack.entries.length ? stack : {
1959
+ ...stack,
1960
+ entries: stack.entries.slice(0, keep)
1961
+ };
1962
+ });
1963
+ }
1964
+ /** 把 target 处最近的 stack 弹回到根 entry。 */
1965
+ function popToRoot(tree, target) {
1966
+ return updateStack(tree, target, "popToRoot", (stack) => {
1967
+ return stack.entries.length <= 1 ? stack : {
1968
+ ...stack,
1969
+ entries: stack.entries.slice(0, 1)
1970
+ };
1971
+ });
1972
+ }
1973
+ /** 把 target 处最近的 stack 弹回到指定 index(保留 [0..index])。 */
1974
+ function popTo(tree, index, target) {
1975
+ return updateStack(tree, target, "popTo", (stack) => {
1976
+ if (index < 0 || index >= stack.entries.length) throw new NavigationError(`popTo 失败:index ${index} 越界(栈深 ${stack.entries.length})`);
1977
+ return index === stack.entries.length - 1 ? stack : {
1978
+ ...stack,
1979
+ entries: stack.entries.slice(0, index + 1)
1980
+ };
1981
+ });
1982
+ }
1983
+ /** 替换 target 处最近 stack 的栈顶 entry(栈为空时抛错)。 */
1984
+ function replaceTop(tree, node, target) {
1985
+ return updateStack(tree, target, "replaceTop", (stack) => {
1986
+ if (stack.entries.length === 0) throw new NavigationError("replaceTop 失败:stack 为空");
1987
+ const entries = stack.entries.slice();
1988
+ entries[entries.length - 1] = node;
1989
+ return {
1990
+ ...stack,
1991
+ entries
1992
+ };
1993
+ });
1994
+ }
1995
+ /**
1996
+ * 切换 tabs 节点的激活分支。
1997
+ * target 默认为「最近的激活 tabs 节点」;target 必须指向 TabsNode,且 key 必须是已知分支。
1998
+ */
1999
+ function selectTab(tree, key, target) {
2000
+ const tabsPath = target ?? findActiveKind(tree, NAVIGATION_NODE_KINDS.TABS);
2001
+ if (tabsPath === void 0) throw new NavigationError("selectTab 失败:激活路径上没有 tabs 节点");
2002
+ return transformAt(tree, tabsPath, (n) => {
2003
+ if (n.kind !== NAVIGATION_NODE_KINDS.TABS) throw new NavigationError(`selectTab 失败:目标是 ${n.kind} 节点,不是 tabs`);
2004
+ const t = n;
2005
+ if (!Object.hasOwn(t.branches, key)) throw new NavigationError(`selectTab 失败:未知分支 "${key}"`);
2006
+ if (t.active === key) return t;
2007
+ return {
2008
+ ...t,
2009
+ active: key
2010
+ };
2011
+ });
2012
+ }
2013
+ /**
2014
+ * 设置 split 某列内容,并清空它之后的所有列(content 置 undefined)。
2015
+ * target 默认为「最近的激活 split 节点」;columnId 必须是已知列。
2016
+ */
2017
+ function selectColumn(tree, columnId, content, target) {
2018
+ const splitPath = target ?? findActiveKind(tree, NAVIGATION_NODE_KINDS.SPLIT);
2019
+ if (splitPath === void 0) throw new NavigationError("selectColumn 失败:激活路径上没有 split 节点");
2020
+ return transformAt(tree, splitPath, (n) => {
2021
+ if (n.kind !== NAVIGATION_NODE_KINDS.SPLIT) throw new NavigationError(`selectColumn 失败:目标是 ${n.kind} 节点,不是 split`);
2022
+ const sp = n;
2023
+ const idx = sp.columns.findIndex((c) => c.id === columnId);
2024
+ if (idx === -1) throw new NavigationError(`selectColumn 失败:未知列 "${columnId}"`);
2025
+ const columns = sp.columns.map((c, i) => {
2026
+ if (i === idx) return {
2027
+ ...c,
2028
+ content
2029
+ };
2030
+ if (i > idx) return {
2031
+ ...c,
2032
+ content: void 0
2033
+ };
2034
+ return c;
2035
+ });
2036
+ return {
2037
+ ...sp,
2038
+ columns
2039
+ };
2040
+ });
2041
+ }
2042
+ /**
2043
+ * 设置 split 节点的列可见性(对标 SwiftUI `NavigationSplitViewVisibility`)。
2044
+ * target 默认为「最近的激活 split 节点」。改变 visibility 会影响 `collectVisibleDestinations`,
2045
+ * 进而触发 controller 对「新变可见」的列做 dispatch / SSR 预取(如 detailOnly → all 时补预取 sidebar/content)。
2046
+ */
2047
+ function setVisibility(tree, visibility, target) {
2048
+ const splitPath = target ?? findActiveKind(tree, NAVIGATION_NODE_KINDS.SPLIT);
2049
+ if (splitPath === void 0) throw new NavigationError("setVisibility 失败:激活路径上没有 split 节点");
2050
+ return transformAt(tree, splitPath, (n) => {
2051
+ if (n.kind !== NAVIGATION_NODE_KINDS.SPLIT) throw new NavigationError(`setVisibility 失败:目标是 ${n.kind} 节点,不是 split`);
2052
+ return {
2053
+ ...n,
2054
+ visibility
2055
+ };
2056
+ });
2057
+ }
2058
+ /**
2059
+ * 沿激活路径(root → 可见叶子)下行,返回 kind 匹配的「最外层」(最接近根)节点路径。
2060
+ * 这是 `selectTab`/`selectColumn` 的默认目标:无 target 时操作最顶层的 tabs/split
2061
+ * (主标签栏 / 主分栏);需要更深的容器由调用方显式传 target。
2062
+ * 整条激活路径上都没有匹配则返回 undefined。
2063
+ */
2064
+ function findActiveKind(tree, kind) {
2065
+ let match;
2066
+ walkActive(tree, (node, path) => {
2067
+ if (node.kind === kind) {
2068
+ match = [...path];
2069
+ return true;
2070
+ }
2071
+ return false;
2072
+ });
2073
+ return match;
2074
+ }
2075
+ /** Reveal a particular existing entry, preserving its identity and retained state. */
2076
+ function reuseEntry(tree, entryId) {
2077
+ const reveal = (node) => {
2078
+ switch (node.kind) {
2079
+ case "leaf": return node.entryId === entryId ? node : void 0;
2080
+ case "stack":
2081
+ for (let index = 0; index < node.entries.length; index++) {
2082
+ const found = reveal(node.entries[index]);
2083
+ if (found) return {
2084
+ ...node,
2085
+ entries: [...node.entries.slice(0, index), found]
2086
+ };
2087
+ }
2088
+ return;
2089
+ case "tabs":
2090
+ for (const key of node.order) {
2091
+ const found = reveal(node.branches[key]);
2092
+ if (found) return {
2093
+ ...node,
2094
+ active: key,
2095
+ branches: {
2096
+ ...node.branches,
2097
+ [key]: found
2098
+ }
2099
+ };
2100
+ }
2101
+ return;
2102
+ case "split":
2103
+ for (let index = 0; index < node.columns.length; index++) {
2104
+ const column = node.columns[index];
2105
+ const found = column.content && reveal(column.content);
2106
+ if (found) return {
2107
+ ...node,
2108
+ visibility: "all",
2109
+ columns: node.columns.map((item, i) => i === index ? {
2110
+ ...item,
2111
+ content: found
2112
+ } : item)
2113
+ };
2114
+ }
2115
+ return;
2116
+ }
2117
+ };
2118
+ const result = reveal(tree);
2119
+ if (!result) throw new NavigationError(`Unknown entry ID: ${entryId}`);
2120
+ return result;
2121
+ }
2122
+ /** Apply resolved route metadata to leaves without changing their instance identities. */
2123
+ function mapNavigationLeaves(tree, transform) {
2124
+ switch (tree.kind) {
2125
+ case "leaf": return transform(tree);
2126
+ case "stack": return {
2127
+ ...tree,
2128
+ entries: tree.entries.map((node) => mapNavigationLeaves(node, transform))
2129
+ };
2130
+ case "tabs": return {
2131
+ ...tree,
2132
+ branches: Object.fromEntries(Object.entries(tree.branches).map(([key, node]) => [key, mapNavigationLeaves(node, transform)]))
2133
+ };
2134
+ case "split": return {
2135
+ ...tree,
2136
+ columns: tree.columns.map((column) => ({
2137
+ ...column,
2138
+ content: column.content && mapNavigationLeaves(column.content, transform)
2139
+ }))
2140
+ };
2141
+ }
2142
+ }
2143
+ //#endregion
2144
+ //#region ../web/src/navigation/serialization.ts
2145
+ /**
2146
+ * Navigation — 序列化 / 反序列化
2147
+ *
2148
+ * `serializeNavigation` 把导航树转成 JSON 安全的纯对象(SerializedNavigation),
2149
+ * `deserializeNavigation` 反向还原并校验结构,畸形数据抛 NavigationError。
2150
+ * 往返必须无损(含 split 空列 `content: undefined`、params 任意 JSON 值)。
2151
+ *
2152
+ * 复用 `stableStringify` 提供 `serializeNavigationStable` —— 给需要稳定字符串/键的
2153
+ * 场景(如 codec 的紧凑稳定编码、缓存键)一个确定性输出。
2154
+ */
2155
+ /** 合法的 split 可见性取值集合(反序列化校验用) */
2156
+ const SPLIT_VISIBILITY_VALUES = new Set(Object.values(SPLIT_VISIBILITIES));
2157
+ /** 把导航树序列化为 JSON 安全的纯对象。 */
2158
+ function serializeNavigation(tree) {
2159
+ switch (tree.kind) {
2160
+ case NAVIGATION_NODE_KINDS.LEAF: return {
2161
+ kind: NAVIGATION_NODE_KINDS.LEAF,
2162
+ intent: tree.intent,
2163
+ params: tree.params,
2164
+ ...tree.query ? { query: tree.query } : {},
2165
+ entryId: tree.entryId,
2166
+ ...tree.url ? { url: tree.url } : {}
2167
+ };
2168
+ case NAVIGATION_NODE_KINDS.STACK: return {
2169
+ kind: NAVIGATION_NODE_KINDS.STACK,
2170
+ entries: tree.entries.map(serializeNavigation)
2171
+ };
2172
+ case NAVIGATION_NODE_KINDS.TABS: {
2173
+ const branches = Object.create(null);
2174
+ for (const key of Object.keys(tree.branches)) branches[key] = serializeNavigation(tree.branches[key]);
2175
+ return {
2176
+ kind: NAVIGATION_NODE_KINDS.TABS,
2177
+ active: tree.active,
2178
+ order: [...tree.order],
2179
+ branches
2180
+ };
2181
+ }
2182
+ case NAVIGATION_NODE_KINDS.SPLIT: {
2183
+ const columns = tree.columns.map((c) => ({
2184
+ id: c.id,
2185
+ content: c.content === void 0 ? null : serializeNavigation(c.content)
2186
+ }));
2187
+ return tree.visibility === void 0 ? {
2188
+ kind: NAVIGATION_NODE_KINDS.SPLIT,
2189
+ columns
2190
+ } : {
2191
+ kind: NAVIGATION_NODE_KINDS.SPLIT,
2192
+ columns,
2193
+ visibility: tree.visibility
2194
+ };
2195
+ }
2196
+ }
2197
+ }
2198
+ /** 导航树的确定性字符串形式(keys 排序);用于稳定缓存键 / 紧凑编码。 */
2199
+ function serializeNavigationStable(tree) {
2200
+ return stableStringify(serializeNavigation(tree));
2201
+ }
2202
+ const KNOWN_KINDS = /* @__PURE__ */ new Set([
2203
+ NAVIGATION_NODE_KINDS.LEAF,
2204
+ NAVIGATION_NODE_KINDS.STACK,
2205
+ NAVIGATION_NODE_KINDS.TABS,
2206
+ NAVIGATION_NODE_KINDS.SPLIT
2207
+ ]);
2208
+ /** 从 JSON 安全数据还原导航树;结构畸形抛 NavigationError。 */
2209
+ function deserializeNavigation(data) {
2210
+ const tree = parseNode(data, "$");
2211
+ const ids = /* @__PURE__ */ new Set();
2212
+ for (const leaf of collectAllLeaves(tree)) {
2213
+ if (ids.has(leaf.entryId)) throw new NavigationError(`Duplicate entry ID: ${leaf.entryId}`);
2214
+ ids.add(leaf.entryId);
2215
+ }
2216
+ return tree;
2217
+ }
2218
+ function isPlainObject$1(v) {
2219
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2220
+ }
2221
+ function parseNode(data, path) {
2222
+ if (!isPlainObject$1(data)) throw new NavigationError(`反序列化失败:${path} 不是对象`);
2223
+ const kind = data.kind;
2224
+ if (typeof kind !== "string" || !KNOWN_KINDS.has(kind)) throw new NavigationError(`反序列化失败:${path}.kind 非法(${String(kind)})`);
2225
+ switch (kind) {
2226
+ case NAVIGATION_NODE_KINDS.LEAF: return parseLeaf(data, path);
2227
+ case NAVIGATION_NODE_KINDS.STACK: return parseStack(data, path);
2228
+ case NAVIGATION_NODE_KINDS.TABS: return parseTabs(data, path);
2229
+ case NAVIGATION_NODE_KINDS.SPLIT: return parseSplit(data, path);
2230
+ }
2231
+ }
2232
+ function parseLeaf(data, path) {
2233
+ if (typeof data.entryId !== "string" || !data.entryId.trim()) throw new NavigationError(`Invalid entry ID at ${path}`);
2234
+ if (data.url !== void 0 && typeof data.url !== "string") throw new NavigationError(`Invalid URL at ${path}`);
2235
+ if (typeof data.intent !== "string") throw new NavigationError(`反序列化失败:${path}.intent 必须是字符串`);
2236
+ if (!isPlainObject$1(data.params)) throw new NavigationError(`反序列化失败:${path}.params 必须是对象`);
2237
+ if (data.query !== void 0 && !isPlainObject$1(data.query)) throw new NavigationError(`Invalid query at ${path}`);
2238
+ return {
2239
+ kind: NAVIGATION_NODE_KINDS.LEAF,
2240
+ entryId: data.entryId,
2241
+ ...typeof data.url === "string" ? { url: data.url } : {},
2242
+ intent: data.intent,
2243
+ params: { ...data.params },
2244
+ ...data.query ? { query: { ...data.query } } : {}
2245
+ };
2246
+ }
2247
+ function parseStack(data, path) {
2248
+ if (!Array.isArray(data.entries)) throw new NavigationError(`反序列化失败:${path}.entries 必须是数组`);
2249
+ return {
2250
+ kind: NAVIGATION_NODE_KINDS.STACK,
2251
+ entries: data.entries.map((e, i) => parseNode(e, `${path}.entries[${i}]`))
2252
+ };
2253
+ }
2254
+ function parseTabs(data, path) {
2255
+ if (typeof data.active !== "string") throw new NavigationError(`反序列化失败:${path}.active 必须是字符串`);
2256
+ if (!Array.isArray(data.order) || !data.order.every((k) => typeof k === "string")) throw new NavigationError(`反序列化失败:${path}.order 必须是字符串数组`);
2257
+ if (!isPlainObject$1(data.branches)) throw new NavigationError(`反序列化失败:${path}.branches 必须是对象`);
2258
+ const branches = Object.create(null);
2259
+ for (const key of Object.keys(data.branches)) branches[key] = parseNode(data.branches[key], `${path}.branches.${key}`);
2260
+ for (const key of data.order) if (!Object.hasOwn(branches, key)) throw new NavigationError(`反序列化失败:${path}.order 包含不在 branches 中的键 "${key}"`);
2261
+ if (!Object.hasOwn(branches, data.active)) throw new NavigationError(`反序列化失败:${path}.active "${data.active}" 不在 branches 中`);
2262
+ return {
2263
+ kind: NAVIGATION_NODE_KINDS.TABS,
2264
+ active: data.active,
2265
+ order: [...data.order],
2266
+ branches
2267
+ };
2268
+ }
2269
+ function parseSplit(data, path) {
2270
+ if (!Array.isArray(data.columns)) throw new NavigationError(`反序列化失败:${path}.columns 必须是数组`);
2271
+ const columns = data.columns.map((col, i) => {
2272
+ const colPath = `${path}.columns[${i}]`;
2273
+ if (!isPlainObject$1(col)) throw new NavigationError(`反序列化失败:${colPath} 不是对象`);
2274
+ if (typeof col.id !== "string") throw new NavigationError(`反序列化失败:${colPath}.id 必须是字符串`);
2275
+ const content = col.content === null || col.content === void 0 ? void 0 : parseNode(col.content, `${colPath}.content`);
2276
+ return {
2277
+ id: col.id,
2278
+ content
2279
+ };
2280
+ });
2281
+ if (data.visibility === void 0) return {
2282
+ kind: NAVIGATION_NODE_KINDS.SPLIT,
2283
+ columns
2284
+ };
2285
+ if (typeof data.visibility !== "string" || !SPLIT_VISIBILITY_VALUES.has(data.visibility)) throw new NavigationError(`反序列化失败:${path}.visibility 非法值 ${JSON.stringify(data.visibility)}`);
2286
+ return {
2287
+ kind: NAVIGATION_NODE_KINDS.SPLIT,
2288
+ columns,
2289
+ visibility: data.visibility
2290
+ };
2291
+ }
2292
+ //#endregion
2293
+ //#region ../web/src/navigation/codec.ts
2294
+ const DEFAULT_NAV_PARAM = "__nav";
2295
+ function encodeNavigationTreeParam(tree) {
2296
+ const bytes = new TextEncoder().encode(stableStringify(serializeNavigation(tree)));
2297
+ let binary = "";
2298
+ for (const byte of bytes) binary += String.fromCharCode(byte);
2299
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2300
+ }
2301
+ function decodeNavigationTreeParam(encoded) {
2302
+ try {
2303
+ const binary = atob(encoded.replace(/-/g, "+").replace(/_/g, "/"));
2304
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
2305
+ return deserializeNavigation(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
2306
+ } catch {
2307
+ throw new NavigationError("Invalid navigation parameter");
2308
+ }
2309
+ }
2310
+ function activeUrl(tree, router) {
2311
+ const target = findNode(tree, resolveActivePath(tree));
2312
+ return target?.kind === "leaf" ? target.url ?? router.reverse(target.intent, target.params, target.query) ?? "/" : "/";
2313
+ }
2314
+ function createActiveLeafCodec() {
2315
+ return {
2316
+ encode: activeUrl,
2317
+ decode(url) {
2318
+ const value = new URL(url, "http://localhost").searchParams.get(DEFAULT_NAV_PARAM);
2319
+ return value ? decodeNavigationTreeParam(value) : void 0;
2320
+ }
2321
+ };
2322
+ }
2323
+ function createFullStateCodec(options = {}) {
2324
+ const param = options.param ?? "__nav";
2325
+ return {
2326
+ encode(tree, router) {
2327
+ const url = new URL(activeUrl(tree, router), "http://localhost");
2328
+ url.searchParams.set(param, encodeNavigationTreeParam(tree));
2329
+ return url.pathname + url.search;
2330
+ },
2331
+ decode(url) {
2332
+ const value = new URL(url, "http://localhost").searchParams.get(param);
2333
+ return value ? decodeNavigationTreeParam(value) : void 0;
2334
+ }
2335
+ };
2336
+ }
2337
+ //#endregion
2338
+ //#region ../web/src/application/controller-context.ts
2339
+ const SERVER_CONTROLLER = Symbol.for("finesoft.serverController");
2340
+ const SERVER_REQUEST = "@finesoft/web/server-request";
2341
+ function controllerContext(execution, navigation) {
2342
+ const state = execution.bindings[SERVER_REQUEST];
2343
+ return Object.freeze({
2344
+ ...execution,
2345
+ url: navigation.url,
2346
+ path: navigation.path,
2347
+ intent: navigation.intent,
2348
+ isServer: navigation.isServer,
2349
+ getCookie: (name) => state ? state.cookies.get(name) : navigation.getCookie(name),
2350
+ getHeader: (name) => navigation.getHeader(name)
2351
+ });
2352
+ }
2353
+ //#endregion
2354
+ //#region ../web/src/application/definition.ts
2355
+ const WEB_EXECUTION = "@finesoft/web/execution";
2356
+ function consumePage(context, id, input) {
2357
+ const state = context.bindings[WEB_EXECUTION];
2358
+ return state?.retained.get(input) ?? state?.prefetched.get(routeIntent(id, input.params, input.query), state.entryIds.get(input));
2359
+ }
2360
+ const plans = /* @__PURE__ */ new WeakMap();
2361
+ function getWebPlan(definition) {
2362
+ const plan = plans.get(definition);
2363
+ if (!plan) throw new ExecutionError("configuration", "Use defineWebApp to assemble the Web definition");
2364
+ return plan;
2365
+ }
2366
+ function freezeSnapshot(value) {
2367
+ if (value && typeof value === "object") {
2368
+ for (const child of Object.values(value)) freezeSnapshot(child);
2369
+ Object.freeze(value);
2370
+ }
2371
+ return value;
2372
+ }
2373
+ function defineWebApp(input) {
2374
+ const routes = Object.freeze(input.pages.flatMap((page) => (page.routes ?? []).map((route) => ({
2375
+ ...typeof route === "string" ? { path: route } : route,
2376
+ intentId: page.id
2377
+ }))).map((route) => {
2378
+ if ("controller" in route) throw new ExecutionError("configuration", "Web routes reference controller factories by intentId");
2379
+ return Object.freeze({
2380
+ ...route,
2381
+ beforeLoad: route.beforeLoad && Object.freeze([...route.beforeLoad]),
2382
+ afterLoad: route.afterLoad && Object.freeze([...route.afterLoad])
2383
+ });
2384
+ }));
2385
+ const pages = Object.freeze(input.pages.map((controller) => Object.freeze({ ...controller })));
2386
+ const definition = Object.freeze({
2387
+ ...input,
2388
+ pages,
2389
+ navigation: typeof input.navigation === "function" ? input.navigation : input.navigation && freezeSnapshot(structuredClone(input.navigation)),
2390
+ beforeNavigate: input.beforeNavigate && Object.freeze([...input.beforeNavigate]),
2391
+ beforeCommit: input.beforeCommit && Object.freeze([...input.beforeCommit]),
2392
+ beforeLoad: input.beforeLoad && Object.freeze([...input.beforeLoad]),
2393
+ afterLoad: input.afterLoad && Object.freeze([...input.afterLoad]),
2394
+ configuration: input.configuration && Object.freeze({ ...input.configuration })
2395
+ });
2396
+ const operations = /* @__PURE__ */ new Map();
2397
+ for (const controller of pages) {
2398
+ if (operations.has(controller.id) || !!controller.create === !!controller.handler) throw new ExecutionError("configuration", `Invalid page controller: ${controller.id}`);
2399
+ operations.set(controller.id, defineOperation({
2400
+ id: controller.id,
2401
+ kind: "query",
2402
+ policies: controller.policies?.map((policy) => (input, context) => policy(input.params, context)),
2403
+ handler: (input, context) => {
2404
+ const state = context.bindings[WEB_EXECUTION];
2405
+ const request = context.bindings[SERVER_REQUEST];
2406
+ const current = state?.contexts.get(input) ?? controllerContext(context, {
2407
+ url: "",
2408
+ path: "",
2409
+ intent: routeIntent(controller.id, input.params, input.query),
2410
+ isServer: !!request || typeof window === "undefined",
2411
+ getCookie: () => void 0,
2412
+ getHeader: (name) => request?.request.headers.get(name) ?? void 0
2413
+ });
2414
+ const cached = consumePage(context, controller.id, input);
2415
+ if (cached !== void 0) return cached;
2416
+ const instance = controller.create?.();
2417
+ if (request?.remote && (!instance || !(SERVER_CONTROLLER in instance))) throw new ExecutionError("not_found");
2418
+ if (controller.handler) return controller.handler(input.params, current, input.query);
2419
+ return instance.perform(input.params, current, input.query);
2420
+ }
2421
+ }));
2422
+ }
2423
+ const router = new Router();
2424
+ const paths = /* @__PURE__ */ new Set();
2425
+ for (const route of routes) {
2426
+ if (paths.has(route.path)) throw new ExecutionError("configuration", `Duplicate route: ${route.path}`);
2427
+ if (!operations.has(route.intentId)) throw new ExecutionError("configuration", `Unknown page: ${route.intentId}`);
2428
+ paths.add(route.path);
2429
+ router.add(route.path, route.intentId, {
2430
+ renderMode: route.renderMode,
2431
+ cache: route.cache,
2432
+ beforeGuards: route.beforeLoad,
2433
+ afterGuards: route.afterLoad,
2434
+ paramCodecs: route.params,
2435
+ queryCodecs: route.query
2436
+ });
2437
+ }
2438
+ const app = defineApp({
2439
+ ...input.app,
2440
+ id: input.id,
2441
+ operations: [...input.app?.operations ?? [], ...operations.values()]
2442
+ });
2443
+ plans.set(definition, {
2444
+ app,
2445
+ router: router.seal(),
2446
+ operations,
2447
+ routes
2448
+ });
2449
+ return definition;
2450
+ }
2451
+ //#endregion
2452
+ //#region ../web/src/middleware/pipeline.ts
2453
+ /**
2454
+ * Guard Pipeline — 顺序执行守卫链
2455
+ *
2456
+ * 按注册顺序执行守卫,遇到第一个非 `next` 结果立即短路返回。
2457
+ */
2458
+ async function runGuards(guards, ctx) {
2459
+ for (const guard of guards) {
2460
+ if (ctx.signal?.aborted) throw new ExecutionError("cancelled");
2461
+ const result = await guard(ctx);
2462
+ if (ctx.signal?.aborted) throw new ExecutionError("cancelled");
2463
+ if (result.kind !== "next") return result;
2464
+ }
2465
+ return { kind: "next" };
2466
+ }
2467
+ /** 执行 beforeLoad 守卫链 */
2468
+ const runBeforeLoadGuards = runGuards;
2469
+ /** 执行 afterLoad 守卫链 */
2470
+ const runAfterLoadGuards = runGuards;
2471
+ //#endregion
2472
+ //#region ../web/src/application/execution.ts
2473
+ /** Attach a cancellation boundary to the existing scope without taking its disposal ownership. */
2474
+ function bindExecutionCancellation(execution, signal) {
2475
+ if (!signal) return () => {};
2476
+ const cancel = () => execution.cancel(signal.reason);
2477
+ if (signal.aborted) cancel();
2478
+ else signal.addEventListener("abort", cancel, { once: true });
2479
+ return () => signal.removeEventListener("abort", cancel);
2480
+ }
2481
+ //#endregion
2482
+ //#region ../web/src/protocol.ts
2483
+ const FRAMEWORK_PROTOCOL_VERSION = 2;
2484
+ function getFrameworkBuildId() {
2485
+ return typeof __FINESOFT_BUILD_ID__ === "string" ? __FINESOFT_BUILD_ID__ : "unbundled";
2486
+ }
2487
+ function record(value) {
2488
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2489
+ }
2490
+ function decodeWireEnvelope(value, buildId = getFrameworkBuildId()) {
2491
+ const invalid = {
2492
+ status: "fresh-load",
2493
+ code: "invalid-payload"
2494
+ };
2495
+ if (!record(value)) return invalid;
2496
+ if (value.protocolVersion !== 2) return {
2497
+ status: "fresh-load",
2498
+ code: "protocol-mismatch"
2499
+ };
2500
+ if (value.buildId !== buildId) return {
2501
+ status: "fresh-load",
2502
+ code: "build-mismatch"
2503
+ };
2504
+ if (!record(value.payload) || !Array.isArray(value.payload.pages) || !value.payload.tree) return invalid;
2505
+ let leaves;
2506
+ try {
2507
+ leaves = collectAllLeaves(deserializeNavigation(value.payload.tree));
2508
+ } catch {
2509
+ return invalid;
2510
+ }
2511
+ const entries = new Map(leaves.map((leaf) => [leaf.entryId, leaf]));
2512
+ const seen = /* @__PURE__ */ new Set();
2513
+ for (const page of value.payload.pages) {
2514
+ if (!record(page) || typeof page.entryId !== "string" || seen.has(page.entryId) || !record(page.intent) || !record(page.data)) return invalid;
2515
+ const leaf = entries.get(page.entryId);
2516
+ if (!leaf || leaf.intent !== page.intent.id || page.intent.params !== void 0 && !record(page.intent.params) || page.intent.query !== void 0 && !record(page.intent.query)) return invalid;
2517
+ if (typeof page.data.pageType !== "string" || !page.data.pageType) return invalid;
2518
+ seen.add(page.entryId);
2519
+ }
2520
+ return {
2521
+ status: "ready",
2522
+ data: value.payload
2523
+ };
2524
+ }
2525
+ //#endregion
2526
+ //#region ../web/src/application/server-controller-proxy.ts
2527
+ const remotePages = /* @__PURE__ */ new WeakMap();
2528
+ const SERVER_CONTROLLER_PATH = "/__finesoft/controller";
2529
+ var RemoteNavigationResult = class extends ExecutionError {
2530
+ result;
2531
+ constructor(result) {
2532
+ super("denied", "Server controller navigation result");
2533
+ this.result = result;
2534
+ }
2535
+ };
2536
+ /** The compiler replaces a complete server controller module with these inert references. */
2537
+ var ServerControllerProxy = class {
2538
+ async perform(params, execution, query = {}) {
2539
+ const context = execution;
2540
+ if (!context.intent || context.isServer) throw new ExecutionError("configuration", "Server controller proxy requires browser navigation");
2541
+ const response = await context.fetch(SERVER_CONTROLLER_PATH, {
2542
+ method: "POST",
2543
+ credentials: "same-origin",
2544
+ headers: {
2545
+ "content-type": "application/json",
2546
+ "x-finesoft-controller": "1"
2547
+ },
2548
+ body: JSON.stringify({
2549
+ intent: context.intent.id,
2550
+ params,
2551
+ query,
2552
+ url: context.url
2553
+ })
2554
+ });
2555
+ if (!response.ok) throw new ExecutionError(response.status === 403 ? "denied" : "failure");
2556
+ const result = await response.json();
2557
+ if (result.redirect) throw new RemoteNavigationResult({
2558
+ kind: "redirect",
2559
+ ...result.redirect
2560
+ });
2561
+ if (result.rejection) throw new RemoteNavigationResult({
2562
+ kind: "deny",
2563
+ ...result.rejection
2564
+ });
2565
+ const decoded = decodeWireEnvelope(result);
2566
+ if (decoded.status !== "ready" || decoded.data.pages.length !== 1) throw new ExecutionError("failure", "Invalid server controller response");
2567
+ const loaded = decoded.data.pages[0];
2568
+ const target = collectAllLeaves(deserializeNavigation(decoded.data.tree)).find((leaf) => leaf.entryId === loaded.entryId);
2569
+ const page = loaded.data;
2570
+ remotePages.set(page, {
2571
+ target,
2572
+ rewriteUrl: response.headers.get("content-location") ?? void 0
2573
+ });
2574
+ return page;
2575
+ }
2576
+ };
2577
+ //#endregion
2578
+ //#region ../web/src/application/load-page.ts
2579
+ /** One page pipeline, shared by URL, SSR and every visible tree destination. */
2580
+ async function loadPage(options) {
2581
+ const { web } = options;
2582
+ const owned = !options.execution;
2583
+ const execution = options.execution ?? web.createExecution({ signal: options.signal });
2584
+ const unbind = bindExecutionCancellation(execution, options.signal);
2585
+ const check = () => {
2586
+ if (execution.context.signal.aborted || options.signal?.aborted) throw new ExecutionError("cancelled");
2587
+ };
2588
+ try {
2589
+ let target = options.target;
2590
+ let entryId = typeof target === "string" ? options.entryId : target.entryId;
2591
+ let retained = options.retained;
2592
+ for (let depth = 0; depth < 5; depth++) {
2593
+ check();
2594
+ const direct = typeof target !== "string" ? target : void 0;
2595
+ const hasRoutes = direct && web.router.hasIntent(direct.intent);
2596
+ const url = typeof target === "string" ? target : target.url ?? (hasRoutes ? createActiveLeafCodec().encode(target, web.router) : "");
2597
+ let match = direct && !direct.url && !hasRoutes ? {
2598
+ intent: routeIntent(direct.intent, direct.params, direct.query),
2599
+ action: {
2600
+ kind: "flow",
2601
+ url: ""
2602
+ }
2603
+ } : await web.router.resolve(url);
2604
+ check();
2605
+ if (!match) return {
2606
+ kind: "deny",
2607
+ status: 404,
2608
+ message: "Page not found"
2609
+ };
2610
+ if (direct && match.intent.id !== direct.intent) throw new ExecutionError("configuration", "Leaf URL does not match its intent");
2611
+ let destination = typeof target === "string" ? leaf(match.intent.id, match.intent.params ?? {}, {
2612
+ url,
2613
+ query: match.intent.query,
2614
+ entryId: entryId ?? web.prefetchedIntents.entryIdFor(match.intent)
2615
+ }) : !hasRoutes && !target.url ? target : {
2616
+ ...target,
2617
+ intent: match.intent.id,
2618
+ params: match.intent.params ?? {},
2619
+ query: match.intent.query,
2620
+ url
2621
+ };
2622
+ entryId = destination.entryId;
2623
+ let navContext = {
2624
+ ...options.createContext?.({
2625
+ url,
2626
+ intent: match.intent,
2627
+ execution
2628
+ }) ?? {
2629
+ url,
2630
+ path: new URL(url, "http://localhost").pathname,
2631
+ intent: match.intent,
2632
+ params: match.intent.params ?? {},
2633
+ query: match.intent.query ?? {},
2634
+ container: execution.context.container,
2635
+ isServer: true,
2636
+ getCookie: () => void 0,
2637
+ getHeader: () => void 0
2638
+ },
2639
+ params: match.intent.params ?? {},
2640
+ query: match.intent.query ?? {},
2641
+ container: execution.context.container,
2642
+ signal: options.signal ? AbortSignal.any([execution.context.signal, options.signal]) : execution.context.signal
2643
+ };
2644
+ const before = await runBeforeLoadGuards([
2645
+ ...web.definition.beforeLoad ?? [],
2646
+ ...match.beforeGuards ?? [],
2647
+ ...options.beforeLoad ?? []
2648
+ ], navContext);
2649
+ check();
2650
+ if (before.kind === "rewrite") {
2651
+ target = before.url;
2652
+ retained = void 0;
2653
+ continue;
2654
+ }
2655
+ if (before.kind !== "next") return before;
2656
+ let page;
2657
+ try {
2658
+ const operation = getWebPlan(web.definition).operations.get(match.intent.id);
2659
+ if (!operation) throw new ExecutionError("not_found");
2660
+ const input = {
2661
+ params: { ...match.intent.params },
2662
+ query: { ...match.intent.query }
2663
+ };
2664
+ const state = execution.context.bindings[WEB_EXECUTION];
2665
+ state.entryIds.set(input, destination.entryId);
2666
+ state.contexts.set(input, controllerContext(execution.context, navContext));
2667
+ if (retained) state.retained.set(input, retained);
2668
+ page = await execution.execute(operation, input);
2669
+ } catch (error) {
2670
+ if (error instanceof RemoteNavigationResult) return error.result;
2671
+ if (error instanceof ExecutionError && error.code === "cancelled") throw error;
2672
+ return {
2673
+ kind: "deny",
2674
+ status: error instanceof ExecutionError ? error.status : 500,
2675
+ message: error instanceof ExecutionError ? error.message : "Internal error"
2676
+ };
2677
+ }
2678
+ check();
2679
+ const remote = remotePages.get(page);
2680
+ if (remote) {
2681
+ remotePages.delete(page);
2682
+ destination = {
2683
+ ...remote.target,
2684
+ entryId: destination.entryId
2685
+ };
2686
+ if (destination.url) match = await web.router.resolve(destination.url);
2687
+ navContext = {
2688
+ ...navContext,
2689
+ intent: routeIntent(destination.intent, destination.params, destination.query),
2690
+ params: destination.params,
2691
+ query: destination.query ?? {},
2692
+ url: destination.url ?? "",
2693
+ path: new URL(destination.url || "/", "http://localhost").pathname
2694
+ };
2695
+ check();
2696
+ }
2697
+ const after = await runAfterLoadGuards([
2698
+ ...web.definition.afterLoad ?? [],
2699
+ ...match?.afterGuards ?? [],
2700
+ ...options.afterLoad ?? []
2701
+ ], {
2702
+ ...navContext,
2703
+ page
2704
+ });
2705
+ check();
2706
+ if (after.kind === "deny" || after.kind === "redirect") return after;
2707
+ return {
2708
+ kind: "page",
2709
+ page,
2710
+ target: destination,
2711
+ match: match ?? void 0,
2712
+ ...after.kind === "rewrite" ? { rewriteUrl: after.url } : remote?.rewriteUrl ? { rewriteUrl: remote.rewriteUrl } : {}
2713
+ };
2714
+ }
2715
+ throw new ExecutionError("configuration", "Page rewrite recursion depth exceeded");
2716
+ } finally {
2717
+ unbind();
2718
+ if (owned) await execution.dispose();
2719
+ }
2720
+ }
2721
+ //#endregion
2722
+ //#region ../web/src/utils/notify-observers.ts
2723
+ /** Notify without awaiting observers or allowing their failures to break a committed state. */
2724
+ function notifyObservers(listeners, onError, ...args) {
2725
+ for (const listener of listeners) try {
2726
+ const result = listener(...args);
2727
+ if (result && typeof result.then === "function") Promise.resolve(result).catch(onError);
2728
+ } catch {
2729
+ onError();
2730
+ }
2731
+ }
2732
+ //#endregion
2733
+ //#region ../web/src/session/types.ts
2734
+ var StorageUnavailableError = class extends Error {
2735
+ constructor() {
2736
+ super("storage-unavailable");
2737
+ this.name = "StorageUnavailableError";
2738
+ }
2739
+ };
2740
+ /** 会话快照在 Storage 中的默认键。 */
2741
+ const SESSION_DEFAULT_KEY = "__finesoft_session__";
2742
+ /** 会话快照的默认版本号;解码时不匹配即整份丢弃。 */
2743
+ const SESSION_DEFAULT_VERSION = 2;
2744
+ /** 会话错误:序列化 / 编排过程中需要显式标识的错误类型。 */
2745
+ var SessionError = class extends Error {
2746
+ constructor(message) {
2747
+ super(message);
2748
+ this.name = "SessionError";
2749
+ }
2750
+ };
2751
+ //#endregion
2752
+ //#region ../web/src/application/session.ts
2753
+ /**
2754
+ * One WebSession owns navigation transactions and native view snapshots.
2755
+ * EntryId indexes both visible and retained pages in stable mount order.
2756
+ * Browser history, DOM acknowledgement and storage remain host responsibilities.
2757
+ */
2758
+ const TREE_ACTION_KINDS = Object.values(ACTION_KINDS).filter((kind) => kind !== "flow" && kind !== "externalUrl" && kind !== "compound");
2759
+ function createWebSession(options) {
2760
+ const web = options.web;
2761
+ let locale, translator;
2762
+ const beforeNavigate = [...web.definition.beforeNavigate ?? [], ...options.beforeNavigate ?? []];
2763
+ const beforeCommit = [...web.definition.beforeCommit ?? [], ...options.beforeCommit ?? []];
2764
+ const getErrorPage = options.getErrorPage ?? web.definition.getErrorPage;
2765
+ let snapshot = Object.freeze({
2766
+ tree: options.initial,
2767
+ destinations: [],
2768
+ entries: [],
2769
+ revision: 0,
2770
+ navigation: summarize(options.initial)
2771
+ });
2772
+ const actions = new ActionDispatcher(() => snapshot);
2773
+ const cancelActions = actions.cancel;
2774
+ let retryInitial = false;
2775
+ const navigationTree = () => retryInitial ? options.initial : snapshot.tree;
2776
+ const listeners = /* @__PURE__ */ new Set();
2777
+ const entries = /* @__PURE__ */ new Map();
2778
+ let presentIds = new Set(collectAllLeaves(options.initial).map((entry) => entry.entryId));
2779
+ const stale = /* @__PURE__ */ new Set();
2780
+ const commitSteps = /* @__PURE__ */ new Set();
2781
+ let invalidationVersion = 0;
2782
+ const unsubscribeInvalidation = web.runtime.onInvalidate(() => {
2783
+ invalidationVersion++;
2784
+ for (const id of entries.keys()) stale.add(id);
2785
+ });
2786
+ let inflight = Promise.resolve();
2787
+ const settling = /* @__PURE__ */ new Set();
2788
+ let current;
2789
+ let closed = false;
2790
+ function enqueue(produce) {
2791
+ if (closed) return Promise.reject(new ExecutionError("configuration", "Navigation is closed"));
2792
+ const submittedGeneration = actions.generation;
2793
+ const runCurrent = () => {
2794
+ if (closed || submittedGeneration !== actions.generation) throw new ExecutionError("cancelled");
2795
+ return produce();
2796
+ };
2797
+ const run = inflight.then(runCurrent, runCurrent);
2798
+ settling.add(run);
2799
+ run.then(() => settling.delete(run), () => settling.delete(run));
2800
+ inflight = run.catch(() => {});
2801
+ return run;
2802
+ }
2803
+ async function resolveTree(nextTree, signal, historyMode, refreshEntryId) {
2804
+ const ownGeneration = actions.generation;
2805
+ const check = () => {
2806
+ if (signal?.aborted) {
2807
+ options.execution?.cancel(signal.reason);
2808
+ throw new ExecutionError("cancelled");
2809
+ }
2810
+ if (closed || ownGeneration !== actions.generation) throw new ExecutionError("cancelled");
2811
+ };
2812
+ const transaction = {
2813
+ from: retryInitial ? {
2814
+ tree: options.initial,
2815
+ destinations: []
2816
+ } : snapshot,
2817
+ transitionId: generateUuid()
2818
+ };
2819
+ for (let redirects = 0;; redirects++) {
2820
+ check();
2821
+ const result = await resolveCandidate(nextTree, transaction, redirects === 0, signal, historyMode, refreshEntryId);
2822
+ check();
2823
+ if (!result.redirect) return result.snapshot;
2824
+ if (redirects === 5) throw new ExecutionError("configuration", "Too many navigation redirects (maximum 5)");
2825
+ const redirectedTree = await options.onRedirect?.(result.redirect, result.snapshot);
2826
+ check();
2827
+ if (!redirectedTree) return result.snapshot;
2828
+ nextTree = redirectedTree;
2829
+ }
2830
+ }
2831
+ async function resolveCandidate(nextTree, transaction, admission, signal, historyMode, refreshEntryId) {
2832
+ const ownGeneration = actions.generation;
2833
+ const ownInvalidation = invalidationVersion;
2834
+ const ids = /* @__PURE__ */ new Set();
2835
+ for (const dest of collectAllLeaves(nextTree)) {
2836
+ if (ids.has(dest.entryId)) throw new Error(`Duplicate entry ID: ${dest.entryId}`);
2837
+ ids.add(dest.entryId);
2838
+ }
2839
+ current = new AbortController();
2840
+ const combined = signal ? AbortSignal.any([signal, current.signal]) : current.signal;
2841
+ const execution = options.execution ?? web.createExecution({ signal: combined });
2842
+ const unbind = bindExecutionCancellation(execution, combined);
2843
+ const check = () => {
2844
+ if (combined.aborted || execution.context.signal.aborted || ownGeneration !== actions.generation || ownInvalidation !== invalidationVersion) throw new ExecutionError("cancelled");
2845
+ };
2846
+ const executionState = execution.context.bindings[WEB_EXECUTION];
2847
+ const prefetched = executionState.prefetched;
2848
+ const stage = prefetched.stage();
2849
+ executionState.prefetched = stage.cache;
2850
+ const context = {
2851
+ ...transaction,
2852
+ tree: nextTree,
2853
+ execution: execution.context,
2854
+ signal: execution.context.signal,
2855
+ isServer: options.isServer ?? true
2856
+ };
2857
+ const reject = (result, candidate) => ({
2858
+ snapshot: {
2859
+ ...candidate,
2860
+ ...result.kind === "deny" ? { rejection: result } : {},
2861
+ ...result.kind === "redirect" ? { redirect: {
2862
+ url: result.url,
2863
+ status: result.status
2864
+ } } : {}
2865
+ },
2866
+ ...result.kind === "redirect" ? { redirect: {
2867
+ url: result.url,
2868
+ status: result.status
2869
+ } } : {}
2870
+ });
2871
+ const destinations = [];
2872
+ const resolvedLeaves = /* @__PURE__ */ new Map();
2873
+ let redirect;
2874
+ try {
2875
+ if (admission) for (const policy of beforeNavigate) {
2876
+ check();
2877
+ const result = await policy(context);
2878
+ check();
2879
+ if (!result || ![
2880
+ "next",
2881
+ "deny",
2882
+ "redirect"
2883
+ ].includes(result.kind)) throw new ExecutionError("configuration", "Invalid beforeNavigate result");
2884
+ if (result.kind !== "next") return reject(result, {
2885
+ tree: nextTree,
2886
+ destinations: [],
2887
+ transitionId: transaction.transitionId,
2888
+ historyMode
2889
+ });
2890
+ }
2891
+ for (const dest of collectVisibleDestinations(nextTree)) {
2892
+ check();
2893
+ const key = resourceKey(dest.intent, dest.params, execution.context, dest.query);
2894
+ const retained = retryInitial || dest.entryId === refreshEntryId || stale.has(dest.entryId) ? void 0 : entries.get(dest.entryId);
2895
+ const result = await loadPage({
2896
+ web,
2897
+ target: dest,
2898
+ execution,
2899
+ signal: combined,
2900
+ retained: retained?.resourceKey === key ? retained.page : void 0,
2901
+ beforeLoad: options.beforeLoad,
2902
+ afterLoad: options.afterLoad,
2903
+ createContext: ({ url, intent, execution: active }) => {
2904
+ return options.createContext?.({
2905
+ execution: active.context,
2906
+ intent: intent.id,
2907
+ params: intent.params ?? {},
2908
+ query: intent.query ?? {},
2909
+ signal: combined,
2910
+ url
2911
+ }) ?? {
2912
+ url,
2913
+ path: new URL(url, "http://localhost").pathname,
2914
+ params: intent.params ?? {},
2915
+ query: intent.query ?? {},
2916
+ intent,
2917
+ isServer: options.isServer ?? true,
2918
+ container: active.context.container,
2919
+ getCookie: () => void 0,
2920
+ getHeader: () => void 0
2921
+ };
2922
+ }
2923
+ });
2924
+ check();
2925
+ if (result.kind === "page") resolvedLeaves.set(dest.entryId, result.rewriteUrl ? {
2926
+ ...result.target,
2927
+ entryId: dest.entryId,
2928
+ url: result.rewriteUrl
2929
+ } : result.target);
2930
+ if (result.kind === "redirect") redirect ??= result;
2931
+ destinations.push({
2932
+ ...result.kind === "page" && result.match?.cache ? { cache: result.match.cache } : {},
2933
+ ...result.kind === "page" ? {
2934
+ renderMode: result.match?.renderMode,
2935
+ rewriteUrl: result.rewriteUrl
2936
+ } : {},
2937
+ entryId: dest.entryId,
2938
+ resourceKey: result.kind === "page" ? resourceKey(result.target.intent, result.target.params, execution.context, result.target.query) : key,
2939
+ intent: result.kind === "page" ? result.target.intent : dest.intent,
2940
+ params: result.kind === "page" ? result.target.params : dest.params,
2941
+ query: result.kind === "page" ? result.target.query : dest.query,
2942
+ page: result.kind === "page" ? result.page : getErrorPage(result.status, result.kind === "deny" ? result.message : "Redirecting"),
2943
+ ...result.kind !== "page" ? { status: result.status } : {}
2944
+ });
2945
+ }
2946
+ check();
2947
+ const candidate = {
2948
+ tree: mapNavigationLeaves(nextTree, (node) => resolvedLeaves.get(node.entryId) ?? node),
2949
+ destinations,
2950
+ transitionId: transaction.transitionId,
2951
+ historyMode
2952
+ };
2953
+ if (redirect) return {
2954
+ snapshot: candidate,
2955
+ redirect: {
2956
+ url: redirect.url,
2957
+ status: redirect.status
2958
+ }
2959
+ };
2960
+ if (destinations.some((dest) => dest.status !== void 0)) return { snapshot: candidate };
2961
+ for (const policy of beforeCommit) {
2962
+ check();
2963
+ const result = await policy({
2964
+ ...context,
2965
+ tree: candidate.tree,
2966
+ candidate
2967
+ });
2968
+ check();
2969
+ if (!result || !["next", "deny"].includes(result.kind)) throw new ExecutionError("configuration", "Invalid beforeCommit result: only next or deny is supported");
2970
+ if (result.kind === "deny") return reject(result, candidate);
2971
+ }
2972
+ check();
2973
+ stage.commit();
2974
+ const previous = snapshot;
2975
+ publish(candidate, ids);
2976
+ for (const dest of destinations) stale.delete(dest.entryId);
2977
+ const commitErrors = [];
2978
+ for (const step of commitSteps) try {
2979
+ step(snapshot, previous);
2980
+ } catch (error) {
2981
+ commitErrors.push(error);
2982
+ }
2983
+ notifyObservers(listeners, () => web.runtime.record("session.observer-error", { transitionId: snapshot.transitionId }), snapshot);
2984
+ try {
2985
+ await options.viewReady?.(snapshot, combined);
2986
+ check();
2987
+ } catch (error) {
2988
+ commitErrors.push(error);
2989
+ }
2990
+ if (commitErrors.length) throw new NavigationCommitError(snapshot, commitErrors);
2991
+ return { snapshot };
2992
+ } finally {
2993
+ executionState.prefetched = prefetched;
2994
+ unbind();
2995
+ if (!options.execution) await execution.dispose();
2996
+ }
2997
+ }
2998
+ function publish(candidate, ids) {
2999
+ if (retryInitial) {
3000
+ entries.clear();
3001
+ stale.clear();
3002
+ }
3003
+ retryInitial = false;
3004
+ const visible = new Set(candidate.destinations.map((entry) => entry.entryId));
3005
+ for (const [id, entry] of entries) if (!ids.has(id)) {
3006
+ entries.delete(id);
3007
+ stale.delete(id);
3008
+ } else if (entry.visible && !visible.has(id)) entries.set(id, Object.freeze({
3009
+ ...entry,
3010
+ visible: false
3011
+ }));
3012
+ for (const entry of candidate.destinations) {
3013
+ const old = entries.get(entry.entryId);
3014
+ if (!old || !old.visible || old.page !== entry.page || old.resourceKey !== entry.resourceKey || old.status !== entry.status || old.cache !== entry.cache || old.renderMode !== entry.renderMode || old.rewriteUrl !== entry.rewriteUrl) entries.set(entry.entryId, Object.freeze({
3015
+ ...entry,
3016
+ visible: true
3017
+ }));
3018
+ }
3019
+ presentIds = ids;
3020
+ snapshot = Object.freeze({
3021
+ ...candidate,
3022
+ revision: snapshot.revision + 1,
3023
+ destinations: Object.freeze(candidate.destinations.map((entry) => entries.get(entry.entryId))),
3024
+ entries: Object.freeze([...entries.values()]),
3025
+ navigation: summarize(candidate.tree)
3026
+ });
3027
+ }
3028
+ function computeNextTree(op) {
3029
+ const tree = navigationTree();
3030
+ switch (op.kind) {
3031
+ case ACTION_KINDS.REUSE_ENTRY: return reuseEntry(tree, op.entryId);
3032
+ case ACTION_KINDS.PUSH: return push(tree, leaf(op.intent, op.params, {
3033
+ url: op.url,
3034
+ query: op.query
3035
+ }), op.target);
3036
+ case ACTION_KINDS.POP: return pop(tree, op.count, op.target);
3037
+ case ACTION_KINDS.POP_TO_ROOT: return popToRoot(tree, op.target);
3038
+ case ACTION_KINDS.POP_TO: return popTo(tree, op.index, op.target);
3039
+ case ACTION_KINDS.REPLACE_TOP: return replaceTop(tree, leaf(op.intent, op.params, {
3040
+ url: op.url,
3041
+ query: op.query
3042
+ }), op.target);
3043
+ case ACTION_KINDS.SELECT_TAB: return selectTab(tree, op.key, op.target);
3044
+ case ACTION_KINDS.SELECT_COLUMN: return selectColumn(tree, op.columnId, op.intent === void 0 ? void 0 : leaf(op.intent, op.params, { query: op.query }), op.target);
3045
+ case ACTION_KINDS.SET_VISIBILITY: return setVisibility(tree, op.visibility, op.target);
3046
+ case ACTION_KINDS.HYDRATE: return op.tree;
3047
+ case ACTION_KINDS.REFRESH: return tree;
3048
+ }
3049
+ }
3050
+ function performTree(op, invocation) {
3051
+ return enqueue(() => {
3052
+ return resolveTree(computeNextTree(op), invocation?.signal, op.kind === "refresh" ? void 0 : op.kind === "replaceTop" ? "replace" : "push", op.kind === "refresh" ? findActiveLeaf(navigationTree())?.entryId : void 0);
3053
+ });
3054
+ }
3055
+ for (const kind of TREE_ACTION_KINDS) actions.onAction(kind, performTree);
3056
+ const session = Object.assign(actions, {
3057
+ runtime: web.runtime,
3058
+ commit(revision) {
3059
+ if (revision === snapshot.revision) options.commit?.(revision);
3060
+ },
3061
+ captureNavigation: () => serializeNavigation(navigationTree()),
3062
+ captureUrl: options.captureUrl,
3063
+ presentKeys: () => presentIds.values(),
3064
+ async restoreNavigation(navigation) {
3065
+ if (navigation === void 0) return;
3066
+ if (await session.perform({
3067
+ kind: "hydrate",
3068
+ tree: deserializeNavigation(navigation)
3069
+ }) !== snapshot) throw new SessionError("navigation-uncommitted");
3070
+ },
3071
+ async start(invocation) {
3072
+ if (snapshot.revision) throw new ExecutionError("configuration", "Web session already started");
3073
+ const ownGeneration = actions.generation;
3074
+ [locale, translator] = await Promise.all([web.getLocale(options.execution), web.getTranslator(options.execution)]);
3075
+ if (closed || ownGeneration !== actions.generation || invocation?.signal?.aborted) throw new ExecutionError("cancelled");
3076
+ const candidate = await enqueue(() => resolveTree(navigationTree(), invocation?.signal));
3077
+ if (closed || ownGeneration !== actions.generation) throw new ExecutionError("cancelled");
3078
+ if (candidate !== snapshot) {
3079
+ let result = candidate;
3080
+ if (candidate.rejection) {
3081
+ const { status, message } = candidate.rejection;
3082
+ const error = leaf("@finesoft/error");
3083
+ result = {
3084
+ tree: stack(error),
3085
+ destinations: [{
3086
+ ...error,
3087
+ resourceKey: resourceKey(error.intent, error.params),
3088
+ page: getErrorPage(status, message),
3089
+ status
3090
+ }]
3091
+ };
3092
+ }
3093
+ publish(result, new Set(collectAllLeaves(options.initial).map((entry) => entry.entryId)));
3094
+ retryInitial = true;
3095
+ }
3096
+ return snapshot;
3097
+ },
3098
+ onCommit(listener) {
3099
+ commitSteps.add(listener);
3100
+ return () => {
3101
+ commitSteps.delete(listener);
3102
+ };
3103
+ },
3104
+ getTree() {
3105
+ return navigationTree();
3106
+ },
3107
+ getSnapshot() {
3108
+ return snapshot;
3109
+ },
3110
+ async dispose() {
3111
+ closed = true;
3112
+ actions.close();
3113
+ current?.abort();
3114
+ await Promise.allSettled(settling);
3115
+ unsubscribeInvalidation();
3116
+ listeners.clear();
3117
+ commitSteps.clear();
3118
+ entries.clear();
3119
+ stale.clear();
3120
+ },
3121
+ cancel(invocation) {
3122
+ cancelActions(invocation);
3123
+ current?.abort();
3124
+ inflight = Promise.resolve();
3125
+ },
3126
+ invalidate(entryKey) {
3127
+ if (entryKey === void 0) for (const id of entries.keys()) stale.add(id);
3128
+ else stale.add(entryKey);
3129
+ },
3130
+ subscribe(listener) {
3131
+ listeners.add(listener);
3132
+ return () => {
3133
+ listeners.delete(listener);
3134
+ };
3135
+ }
3136
+ });
3137
+ Object.defineProperties(session, {
3138
+ session: {
3139
+ get: () => options.session?.(),
3140
+ enumerable: true
3141
+ },
3142
+ locale: {
3143
+ get: () => locale,
3144
+ enumerable: true
3145
+ },
3146
+ translator: {
3147
+ get: () => translator,
3148
+ enumerable: true
3149
+ }
3150
+ });
3151
+ return session;
3152
+ }
3153
+ /** 找到树中「激活路径」末端的 leaf;无(如空 stack/空 split)则返回 undefined。 */
3154
+ function findActiveLeaf(tree) {
3155
+ const node = findNode(tree, resolveActivePath(tree));
3156
+ if (node === void 0) return void 0;
3157
+ return node.kind === "leaf" ? node : void 0;
3158
+ }
3159
+ /** State is already committed; observers and hosts must not report a pre-commit rejection. */
3160
+ var NavigationCommitError = class extends Error {
3161
+ snapshot;
3162
+ committed = true;
3163
+ constructor(snapshot, causes) {
3164
+ super("Navigation committed but host presentation failed", { cause: new AggregateError(causes) });
3165
+ this.snapshot = snapshot;
3166
+ this.name = "NavigationCommitError";
3167
+ }
3168
+ };
3169
+ function summarize(tree) {
3170
+ let node = tree;
3171
+ let canGoBack = false;
3172
+ let tabs;
3173
+ while (node) {
3174
+ if (node.kind === "leaf") return {
3175
+ canGoBack,
3176
+ tabs,
3177
+ activeEntryId: node.entryId
3178
+ };
3179
+ if (node.kind === "stack") {
3180
+ canGoBack ||= node.entries.length > 1;
3181
+ node = node.entries.at(-1);
3182
+ } else if (node.kind === "tabs") {
3183
+ tabs = {
3184
+ active: node.active,
3185
+ order: node.order
3186
+ };
3187
+ node = node.branches[node.active];
3188
+ } else node = visibleSplitColumns(node).at(-1)?.content;
3189
+ }
3190
+ return {
3191
+ canGoBack,
3192
+ tabs
3193
+ };
3194
+ }
3195
+ //#endregion
3196
+ //#region ../web/src/session/snapshot.ts
3197
+ /**
3198
+ * Session — 快照编码 / 解码
3199
+ *
3200
+ * `encodeSnapshot` 用 `stableStringify` 产出确定性字符串(相同内容 → 相同输出,利于去重 / 对比)。
3201
+ * `decodeSnapshot` 做 `JSON.parse` + 结构校验 + 版本检查:任何异常(缺字段 / 类型错 / 版本不符 /
3202
+ * 畸形 JSON / `undefined`)一律返回 `undefined`,**绝不把异常抛进调用方** —— 旧态恢复失败不应崩应用。
3203
+ */
3204
+ /** @internal Synchronously detach JSON state without freezing or invoking object codecs. */
3205
+ function cloneSnapshotValue(value) {
3206
+ try {
3207
+ return cloneValue(value, /* @__PURE__ */ new Set());
3208
+ } catch {
3209
+ throw new SessionError("invalid-snapshot-value");
3210
+ }
3211
+ }
3212
+ function cloneValue(value, ancestors) {
3213
+ if (value === void 0 || value === null || typeof value === "string" || typeof value === "boolean") return value;
3214
+ if (typeof value === "number" && Number.isFinite(value)) return value;
3215
+ if (typeof value !== "object" || value === null || ancestors.has(value)) throw new SessionError("invalid-snapshot-value");
3216
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new SessionError("invalid-snapshot-value");
3217
+ ancestors.add(value);
3218
+ try {
3219
+ if (Array.isArray(value)) return Array.from(value, (item) => cloneValue(item, ancestors) ?? null);
3220
+ const result = Object.create(null);
3221
+ for (const key of Object.keys(value)) {
3222
+ const item = cloneValue(value[key], ancestors);
3223
+ if (item !== void 0) result[key] = item;
3224
+ }
3225
+ return result;
3226
+ } finally {
3227
+ ancestors.delete(value);
3228
+ }
3229
+ }
3230
+ /** 把快照编码为确定性字符串(keys 排序),用作 `storage.set` 的值。 */
3231
+ function encodeSnapshot(snapshot) {
3232
+ return stableStringify(snapshot);
3233
+ }
3234
+ /**
3235
+ * 解码并校验快照。
3236
+ *
3237
+ * 校验:`version === expectedVersion`、`slices`/`scoped` 为对象、`capturedAt` 为数值;
3238
+ * `navigation` 可缺省。任一不符或解析失败 → `undefined`(永不抛)。
3239
+ */
3240
+ function decodeSnapshot(raw, expectedVersion) {
3241
+ if (raw === void 0) return void 0;
3242
+ let parsed;
3243
+ try {
3244
+ parsed = JSON.parse(raw);
3245
+ } catch {
3246
+ return;
3247
+ }
3248
+ if (!isPlainObject(parsed)) return void 0;
3249
+ if (parsed.version !== expectedVersion) return void 0;
3250
+ if (typeof parsed.capturedAt !== "number" || !Number.isFinite(parsed.capturedAt)) return void 0;
3251
+ if (!isPlainObject(parsed.slices)) return void 0;
3252
+ if (!isPlainObject(parsed.scoped)) return void 0;
3253
+ if (Object.keys(parsed.scoped).some((key) => !key.trim())) return void 0;
3254
+ if (parsed.url !== void 0 && typeof parsed.url !== "string") return void 0;
3255
+ if (parsed.navigation !== void 0) {
3256
+ const nav = parsed.navigation;
3257
+ if (!isPlainObject(nav)) return void 0;
3258
+ try {
3259
+ deserializeNavigation(nav);
3260
+ } catch {
3261
+ return;
3262
+ }
3263
+ }
3264
+ return parsed;
3265
+ }
3266
+ /** 普通对象判别(排除 null 与数组)。 */
3267
+ function isPlainObject(value) {
3268
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3269
+ }
3270
+ //#endregion
3271
+ //#region ../web/src/session/scoped-state.ts
3272
+ /**
3273
+ * Session — 导航作用域状态(SwiftUI push/pop 生命周期的核心)
3274
+ *
3275
+ * 状态按实际 EntryId 存入 Map;序列化/恢复保留该 ID,不由业务参数推导。每次导航提交后,框架按**树中实际存在的全部条目**
3276
+ * (注意「存在」而非「可见」)prune —— 身份不在树里的条目状态被丢弃,得到 SwiftUI `@State` 语义:
3277
+ *
3278
+ * A → push B:present `{A, B}` → A 状态保留(仍在栈、只是不可见),B 拿到自己的作用域。
3279
+ * pop B:present `{A}` → B 的作用域被 prune 丢弃,A 原样保留;返回 A 按保留态渲染。
3280
+ * TabView 切 tab:其它分支仍在树中 → 其状态保留(与 SwiftUI tab 保活一致)。
3281
+ *
3282
+ * `collectLeafKeys` 收集树中**全部 leaf**(含不可见的栈底、未激活分支、各 split 列),
3283
+ * 区别于 `collectVisibleDestinations`(仅沿可见路径)—— 作用域保留需要全部 present 条目。
3284
+ */
3285
+ /**
3286
+ * 收集导航树中**全部 leaf** 的身份键(含不可见 / 未激活分支 / 各 split 列)。
3287
+ * 委派给 `collectAllLeaves`(同一「全部存在」遍历),映射成 entryKey。
3288
+ * 「全部存在」而非「可见」,用于 scoped 状态保留。
3289
+ */
3290
+ function collectLeafKeys(tree) {
3291
+ return collectAllLeaves(tree).map((l) => l.entryId);
3292
+ }
3293
+ /**
3294
+ * 创建导航作用域状态容器。
3295
+ *
3296
+ * 内部持一个 `Map`;`prune` 由 `presentKeys` 构造 `Set`,删除不在其中的全部键。
3297
+ * `initial` 在构造时浅拷贝进 `Map`,构造后改动源对象不会泄漏进容器。
3298
+ */
3299
+ function createNavigationScopedState(initial) {
3300
+ const store = new Map(initial ? Object.entries(initial) : void 0);
3301
+ return {
3302
+ get(entryKey) {
3303
+ return store.get(entryKey);
3304
+ },
3305
+ set(entryKey, data) {
3306
+ if (!entryKey.trim()) throw new SessionError("invalid-entry-id");
3307
+ store.set(entryKey, data);
3308
+ },
3309
+ delete(entryKey) {
3310
+ store.delete(entryKey);
3311
+ },
3312
+ prune(presentKeys) {
3313
+ const present = new Set(presentKeys);
3314
+ for (const key of store.keys()) if (!present.has(key)) store.delete(key);
3315
+ },
3316
+ keys() {
3317
+ return [...store.keys()];
3318
+ }
3319
+ };
3320
+ }
3321
+ //#endregion
3322
+ //#region ../web/src/session/session-store.ts
3323
+ /** One ordered persistence owner; implicit saves capture when their queue slot starts. */
3324
+ function createSessionStore(options) {
3325
+ const { storage, key = SESSION_DEFAULT_KEY, version = 2, maxAgeMs, navigation, now = () => Date.now(), onError } = options;
3326
+ const providers = /* @__PURE__ */ new Map();
3327
+ let scope = createNavigationScopedState();
3328
+ let queue = Promise.resolve();
3329
+ let closed = false;
3330
+ let queuedImplicit;
3331
+ function report(ctx) {
3332
+ try {
3333
+ onError?.(new SessionError(ctx.code ?? "session-failed"), ctx);
3334
+ } catch {}
3335
+ }
3336
+ function failure(cause, phase) {
3337
+ const unavailable = cause instanceof StorageUnavailableError;
3338
+ report({
3339
+ phase,
3340
+ code: unavailable ? "storage-unavailable" : "storage-failed"
3341
+ });
3342
+ return unavailable ? { status: "unavailable" } : {
3343
+ status: "failed",
3344
+ cause
3345
+ };
3346
+ }
3347
+ function enqueue(work, implicit = false) {
3348
+ if (closed) return Promise.resolve({ status: "closed" });
3349
+ if (!implicit) queuedImplicit = void 0;
3350
+ let slot;
3351
+ const result = queue.then(async () => {
3352
+ if (slot) {
3353
+ slot.started = true;
3354
+ if (queuedImplicit === slot) queuedImplicit = void 0;
3355
+ }
3356
+ return work();
3357
+ });
3358
+ queue = result.then(() => {}, () => {});
3359
+ if (implicit) {
3360
+ slot = {
3361
+ result,
3362
+ started: false
3363
+ };
3364
+ queuedImplicit = slot;
3365
+ }
3366
+ return result;
3367
+ }
3368
+ function capture() {
3369
+ const slices = Object.create(null);
3370
+ for (const provider of providers.values()) try {
3371
+ slices[provider.key] = {
3372
+ version: provider.version,
3373
+ data: cloneSnapshotValue(provider.capture())
3374
+ };
3375
+ } catch {
3376
+ report({
3377
+ phase: "capture",
3378
+ key: provider.key,
3379
+ code: "slice-failed"
3380
+ });
3381
+ }
3382
+ const scoped = cloneSnapshotValue(Object.fromEntries(scope.keys().map((id) => [id, scope.get(id)])));
3383
+ return {
3384
+ version,
3385
+ navigation: cloneSnapshotValue(navigation?.captureNavigation()),
3386
+ url: navigation?.captureUrl?.(),
3387
+ slices,
3388
+ scoped,
3389
+ capturedAt: now()
3390
+ };
3391
+ }
3392
+ async function read() {
3393
+ try {
3394
+ const raw = await storage.get(key);
3395
+ if (raw === void 0) return { status: "missing" };
3396
+ const snapshot = decodeSnapshot(raw, version);
3397
+ if (!snapshot) return { status: "invalid" };
3398
+ if (maxAgeMs !== void 0 && now() - snapshot.capturedAt > maxAgeMs) return { status: "expired" };
3399
+ return {
3400
+ status: "loaded",
3401
+ snapshot
3402
+ };
3403
+ } catch (cause) {
3404
+ return failure(cause, "load");
3405
+ }
3406
+ }
3407
+ function persist(snapshot) {
3408
+ if (closed) return Promise.resolve({ status: "closed" });
3409
+ if (snapshot === void 0) {
3410
+ if (queuedImplicit && !queuedImplicit.started) return queuedImplicit.result;
3411
+ return enqueue(async () => {
3412
+ try {
3413
+ await storage.set(key, encodeSnapshot(capture()));
3414
+ return { status: "saved" };
3415
+ } catch (cause) {
3416
+ return failure(cause, "persist");
3417
+ }
3418
+ }, true);
3419
+ }
3420
+ let admitted;
3421
+ let rejected;
3422
+ try {
3423
+ admitted = encodeSnapshot(cloneSnapshotValue(snapshot));
3424
+ } catch (cause) {
3425
+ rejected = failure(cause, "persist");
3426
+ }
3427
+ return enqueue(async () => {
3428
+ if (rejected) return rejected;
3429
+ try {
3430
+ await storage.set(key, admitted);
3431
+ return { status: "saved" };
3432
+ } catch (cause) {
3433
+ return failure(cause, "persist");
3434
+ }
3435
+ });
3436
+ }
3437
+ return {
3438
+ register(provider) {
3439
+ if (closed) throw new SessionError("session-closed");
3440
+ if (providers.has(provider.key)) throw new SessionError("duplicate-provider-key");
3441
+ if (!provider.key || !Number.isInteger(provider.version) || provider.version < 1 || typeof provider.decode !== "function") throw new SessionError("invalid-provider-schema");
3442
+ providers.set(provider.key, provider);
3443
+ return () => {
3444
+ if (providers.get(provider.key) === provider) providers.delete(provider.key);
3445
+ };
3446
+ },
3447
+ get scope() {
3448
+ return scope;
3449
+ },
3450
+ capture,
3451
+ persist,
3452
+ save: () => persist(),
3453
+ load: () => enqueue(read),
3454
+ restore(snapshot) {
3455
+ return enqueue(async () => {
3456
+ if (snapshot === void 0) {
3457
+ const loaded = await read();
3458
+ if (loaded.status !== "loaded") return loaded;
3459
+ snapshot = loaded.snapshot;
3460
+ }
3461
+ let valid;
3462
+ try {
3463
+ valid = decodeSnapshot(encodeSnapshot(snapshot), version);
3464
+ } catch {}
3465
+ if (!valid) {
3466
+ report({
3467
+ phase: "restore",
3468
+ code: "navigation-invalid"
3469
+ });
3470
+ return { status: "invalid" };
3471
+ }
3472
+ try {
3473
+ await navigation?.restoreNavigation(valid.navigation);
3474
+ } catch (cause) {
3475
+ report({
3476
+ phase: "restore",
3477
+ code: "navigation-invalid"
3478
+ });
3479
+ return {
3480
+ status: "failed",
3481
+ cause
3482
+ };
3483
+ }
3484
+ scope = createNavigationScopedState({ ...valid.scoped });
3485
+ const discarded = [];
3486
+ for (const provider of providers.values()) {
3487
+ if (!Object.hasOwn(valid.slices, provider.key)) continue;
3488
+ const slice = valid.slices[provider.key];
3489
+ try {
3490
+ if (!slice || !Number.isInteger(slice.version) || slice.version !== provider.version && !provider.migrate) {
3491
+ discarded.push(provider.key);
3492
+ report({
3493
+ phase: "restore",
3494
+ key: provider.key,
3495
+ code: "slice-incompatible"
3496
+ });
3497
+ continue;
3498
+ }
3499
+ const data = slice.version === provider.version ? slice.data : provider.migrate(slice.data, slice.version);
3500
+ await provider.restore(provider.decode(data));
3501
+ } catch {
3502
+ discarded.push(provider.key);
3503
+ report({
3504
+ phase: "restore",
3505
+ key: provider.key,
3506
+ code: "slice-failed"
3507
+ });
3508
+ }
3509
+ }
3510
+ return discarded.length ? {
3511
+ status: "partial",
3512
+ discarded
3513
+ } : { status: "restored" };
3514
+ });
3515
+ },
3516
+ clear: () => enqueue(async () => {
3517
+ try {
3518
+ await storage.delete(key);
3519
+ return { status: "cleared" };
3520
+ } catch (cause) {
3521
+ return failure(cause, "clear");
3522
+ }
3523
+ }),
3524
+ dispose() {
3525
+ closed = true;
3526
+ return queue;
3527
+ }
3528
+ };
3529
+ }
3530
+ //#endregion
3531
+ //#region ../web/src/application/runtime.ts
3532
+ const SERVICES = "@finesoft/web/services";
3533
+ var MissingWebServiceError = class extends ExecutionError {};
3534
+ function createWebRuntime(input) {
3535
+ const config = {
3536
+ ...input.definition.configuration,
3537
+ ...input
3538
+ };
3539
+ const plan = getWebPlan(config.definition);
3540
+ const prefetchedIntents = config.prefetchedIntents ?? PrefetchedIntents.empty();
3541
+ let defaultTranslator;
3542
+ const getTranslatorForLocale = (locale) => {
3543
+ if (!locale) return void 0;
3544
+ if (locale === config.locale) {
3545
+ if (!defaultTranslator) {
3546
+ const messages = config.messages && resolveMessages(config.messages, locale);
3547
+ if (messages) defaultTranslator = new SimpleTranslator({
3548
+ locale,
3549
+ messages
3550
+ });
3551
+ }
3552
+ return defaultTranslator;
3553
+ }
3554
+ const messages = config.messages && resolveMessages(config.messages, locale);
3555
+ return messages ? new SimpleTranslator({
3556
+ locale,
3557
+ messages
3558
+ }) : void 0;
3559
+ };
3560
+ let loggerFactory;
3561
+ const getLoggerFactory = () => loggerFactory ??= config.reportCallback ? new CompositeLoggerFactory([new ConsoleLoggerFactory(config.logFilter), new ReportingLoggerFactory({ report: config.reportCallback })]) : new ConsoleLoggerFactory(config.logFilter);
3562
+ let externalStorage;
3563
+ const createMemoryStorage = () => {
3564
+ const values = /* @__PURE__ */ new Map();
3565
+ return {
3566
+ get: (key) => values.get(key),
3567
+ set: (key, value) => {
3568
+ values.set(key, value);
3569
+ },
3570
+ delete: (key) => {
3571
+ values.delete(key);
3572
+ }
3573
+ };
3574
+ };
3575
+ const getExternalStorage = () => externalStorage ??= createMemoryStorage();
3576
+ function createDefaultProviders(scopeFallback = false) {
3577
+ const defaultLifetime = scopeFallback ? "scope" : "runtime";
3578
+ const storageLifetime = scopeFallback || config.storageScope === "execution" ? "scope" : "runtime";
3579
+ return [
3580
+ provide({
3581
+ token: DEP_KEYS.LOGGER_FACTORY,
3582
+ lifetime: defaultLifetime,
3583
+ create: getLoggerFactory
3584
+ }),
3585
+ provide({
3586
+ token: DEP_KEYS.LOGGER,
3587
+ lifetime: defaultLifetime,
3588
+ dependencies: [DEP_KEYS.LOGGER_FACTORY],
3589
+ create: async (c) => (await c.get(DEP_KEYS.LOGGER_FACTORY)).loggerFor("web")
3590
+ }),
3591
+ provide({
3592
+ token: DEP_KEYS.FETCH,
3593
+ lifetime: "scope",
3594
+ create: (c) => c.bindings[SERVICES].fetch
3595
+ }),
3596
+ provide({
3597
+ token: DEP_KEYS.SAFE_FETCH,
3598
+ lifetime: "scope",
3599
+ dependencies: [DEP_KEYS.FETCH],
3600
+ create: async (c) => secureFetch(await c.get(DEP_KEYS.FETCH), c.bindings[SERVICES].safeFetch)
3601
+ }),
3602
+ provide({
3603
+ token: DEP_KEYS.LOCALE,
3604
+ lifetime: "scope",
3605
+ create: (c) => {
3606
+ const value = c.bindings[SERVICES].locale;
3607
+ if (!value) throw new MissingWebServiceError("configuration", "Locale is not configured");
3608
+ return value;
3609
+ }
3610
+ }),
3611
+ provide({
3612
+ token: DEP_KEYS.TRANSLATOR,
3613
+ lifetime: "scope",
3614
+ create: (c) => {
3615
+ const value = c.bindings[SERVICES].translator;
3616
+ if (!value) throw new MissingWebServiceError("configuration", "Messages are not configured");
3617
+ return value;
3618
+ }
3619
+ }),
3620
+ provide({
3621
+ token: DEP_KEYS.PLATFORM,
3622
+ lifetime: defaultLifetime,
3623
+ create: () => {
3624
+ return config.platform ?? detectPlatform();
3625
+ }
3626
+ }),
3627
+ provide({
3628
+ token: DEP_KEYS.EVENT_RECORDER,
3629
+ lifetime: defaultLifetime,
3630
+ create: () => scopeFallback && config.eventRecorder ? config.eventRecorder : { record: (type, fields) => runtime.record(type, fields) }
3631
+ }),
3632
+ provide({
3633
+ token: DEP_KEYS.STORAGE,
3634
+ lifetime: storageLifetime,
3635
+ create: () => {
3636
+ if (scopeFallback && config.storageScope !== "execution") return getExternalStorage();
3637
+ return createMemoryStorage();
3638
+ }
3639
+ }),
3640
+ provide({
3641
+ token: DEP_KEYS.FEATURE_FLAGS,
3642
+ lifetime: defaultLifetime,
3643
+ create: () => {
3644
+ const providers = [...config.featureFlagsProviders ?? []].reverse();
3645
+ return {
3646
+ isEnabled: (key) => providers.some((p) => p.isEnabled(key)) || config.featureFlags?.[key] === true,
3647
+ getString: (key) => {
3648
+ for (const provider of providers) {
3649
+ const value = provider.getString?.(key);
3650
+ if (value !== void 0) return value;
3651
+ }
3652
+ const value = config.featureFlags?.[key];
3653
+ return typeof value === "string" ? value : void 0;
3654
+ },
3655
+ getNumber: (key) => {
3656
+ for (const provider of providers) {
3657
+ const value = provider.getNumber?.(key);
3658
+ if (value !== void 0) return value;
3659
+ }
3660
+ const value = config.featureFlags?.[key];
3661
+ return typeof value === "number" ? value : void 0;
3662
+ }
3663
+ };
3664
+ }
3665
+ })
3666
+ ];
3667
+ }
3668
+ function createOwnedRuntime() {
3669
+ const defaults = createDefaultProviders();
3670
+ const supplied = new Set([...plan.app.providers ?? [], ...(plan.app.modules ?? []).flatMap((m) => m.providers ?? [])].map((p) => p.token));
3671
+ return createRuntime({
3672
+ app: defineApp({
3673
+ ...plan.app,
3674
+ providers: [...defaults.filter((p) => !supplied.has(p.token)), ...plan.app.providers ?? []]
3675
+ }),
3676
+ capabilities: { fetch: config.fetch ?? globalThis.fetch?.bind(globalThis) },
3677
+ invocationCapabilities: ["fetch"],
3678
+ recorder: config.eventRecorder
3679
+ });
3680
+ }
3681
+ const runtime = config.runtime ?? createOwnedRuntime();
3682
+ const externalDefaultProviders = config.runtime ? createDefaultProviders(true) : [];
3683
+ const active = /* @__PURE__ */ new Set();
3684
+ let closed = false, disposing;
3685
+ let viewExecution;
3686
+ const serviceContext = (execution) => (execution ?? (viewExecution ??= web.createExecution())).context;
3687
+ async function optionalService(token, execution) {
3688
+ try {
3689
+ return await serviceContext(execution).get(token);
3690
+ } catch (error) {
3691
+ if (!(error instanceof MissingWebServiceError)) throw error;
3692
+ return;
3693
+ }
3694
+ }
3695
+ const web = {
3696
+ definition: config.definition,
3697
+ router: plan.router,
3698
+ runtime,
3699
+ prefetchedIntents,
3700
+ getLocale: (execution) => optionalService(DEP_KEYS.LOCALE, execution),
3701
+ getTranslator: (execution) => optionalService(DEP_KEYS.TRANSLATOR, execution),
3702
+ getLogger: (execution) => serviceContext(execution).get(DEP_KEYS.LOGGER),
3703
+ createExecution(invocation = {}) {
3704
+ if (closed) throw new ExecutionError("configuration", "Web runtime is closed");
3705
+ const base = config.invocation;
3706
+ const signals = [base?.signal, invocation.signal].filter((s) => !!s);
3707
+ const services = {};
3708
+ const execution = runtime.createExecution({
3709
+ ...base,
3710
+ ...invocation,
3711
+ signal: signals.length ? AbortSignal.any(signals) : void 0,
3712
+ locale: invocation.locale ?? base?.locale ?? config.locale,
3713
+ fetch: invocation.fetch ?? base?.fetch ?? config.fetch,
3714
+ bindings: {
3715
+ ...base?.bindings,
3716
+ ...invocation.bindings,
3717
+ [SERVICES]: services,
3718
+ [WEB_EXECUTION]: {
3719
+ prefetched: prefetchedIntents,
3720
+ retained: /* @__PURE__ */ new WeakMap(),
3721
+ entryIds: /* @__PURE__ */ new WeakMap(),
3722
+ contexts: /* @__PURE__ */ new WeakMap()
3723
+ }
3724
+ }
3725
+ });
3726
+ for (const provider of externalDefaultProviders) if (!execution.context.container.hasProvider(provider.token)) execution.context.container.registerProvider(provider);
3727
+ services.fetch = execution.context.fetch;
3728
+ services.locale = execution.context.locale ? (execution.context.locale === config.locale ? config.localeAttributes : void 0) ?? getLocaleAttributes(execution.context.locale) : void 0;
3729
+ Object.defineProperty(services, "translator", { get: () => getTranslatorForLocale(execution.context.locale) });
3730
+ services.safeFetch = config.safeFetch;
3731
+ const dispose = execution.dispose.bind(execution);
3732
+ execution.dispose = async () => {
3733
+ try {
3734
+ await dispose();
3735
+ } finally {
3736
+ active.delete(execution);
3737
+ }
3738
+ };
3739
+ active.add(execution);
3740
+ return execution;
3741
+ },
3742
+ dispose() {
3743
+ closed = true;
3744
+ return disposing ??= (async () => {
3745
+ const results = await Promise.allSettled([...active].map((e) => e.dispose()));
3746
+ if (!config.runtime) results.push(...await Promise.allSettled([runtime.dispose()]));
3747
+ const errors = results.filter((r) => r.status === "rejected").map((r) => r.reason);
3748
+ if (errors.length) throw new AggregateError(errors, "Web cleanup failed");
3749
+ })();
3750
+ }
3751
+ };
3752
+ return web;
3753
+ }
3754
+ //#endregion
3755
+ //#region ../web/src/application/initial-navigation.ts
3756
+ /** Resolve and validate once, then let an application arrange typed targets. */
3757
+ async function resolveInitialNavigation(web, url, options = {}) {
3758
+ const overlay = (options.codec ?? web.definition.navigationCodec ?? createActiveLeafCodec()).decode(url, web.router);
3759
+ const match = await web.router.resolve(url);
3760
+ const target = match ? leaf(match.intent.id, match.intent.params, {
3761
+ url,
3762
+ query: match.intent.query
3763
+ }) : void 0;
3764
+ const initial = options.initial ?? web.definition.navigation;
3765
+ const arranged = typeof initial === "function" ? initial({
3766
+ url,
3767
+ match: match ?? void 0,
3768
+ target
3769
+ }) : initial;
3770
+ const tree = overlay ?? arranged ?? (target && stack(target));
3771
+ if (!tree) return void 0;
3772
+ return {
3773
+ tree: deserializeNavigation(serializeNavigation(tree)),
3774
+ renderMode: match?.renderMode
3775
+ };
3776
+ }
3777
+ //#endregion
3778
+ //#region ../web/src/application/page.ts
3779
+ function definePage(input) {
3780
+ return Object.freeze({
3781
+ ...input,
3782
+ route: ((path, options) => route(path, {
3783
+ ...options,
3784
+ intentId: input.id
3785
+ })),
3786
+ leaf: (params = {}, options = {}) => leaf(input.id, params, options),
3787
+ bindView: (pageType, view) => Object.freeze({ [pageType]: view })
3788
+ });
3789
+ }
3790
+ //#endregion
3791
+ //#region ../web/src/middleware/cookies.ts
3792
+ function parseCookieString(str) {
3793
+ const map = /* @__PURE__ */ new Map();
3794
+ if (!str) return map;
3795
+ for (const pair of str.split(";")) {
3796
+ const idx = pair.indexOf("=");
3797
+ if (idx === -1) continue;
3798
+ const key = pair.slice(0, idx).trim();
3799
+ const val = pair.slice(idx + 1).trim();
3800
+ if (key) map.set(key, val);
3801
+ }
3802
+ return map;
3803
+ }
3804
+ //#endregion
3805
+ export { isLeafNode as $, createActiveLeafCodec as A, getTextDirection as At, findNode as B, fetchWithRedirects as Bt, runBeforeLoadGuards as C, defineModule as Ct, SERVER_REQUEST as D, SimpleTranslator as Dt, SERVER_CONTROLLER as E, implementOperation as Et, serializeNavigation as F, interpolate as Ft, push as G, ConsoleLogger as Gt, pop as H, stableStringify as Ht, serializeNavigationStable as I, resolvePluralKey as It, selectColumn as J, CompositeLoggerFactory as Jt, replaceTop as K, ConsoleLoggerFactory as Kt, collectAllLeaves as L, generateUuid as Lt, decodeNavigationTreeParam as M, makeLocaleInfo as Mt, encodeNavigationTreeParam as N, resolveLocaleFromUrl as Nt, controllerContext as O, resolveMessages as Ot, deserializeNavigation as P, englishPlural as Pt, resourceKey as Q, collectVisibleDestinations as R, detectPlatform as Rt, runAfterLoadGuards as S, defineApp as St, getWebPlan as T, implementController as Tt, popTo as U, ReportingLogger as Ut, mapNavigationLeaves as V, enforceHostGuard as Vt, popToRoot as W, ReportingLoggerFactory as Wt, setVisibility as X, list as Xt, selectTab as Y, isMultiValueSchema as Yt, visibleSplitColumns as Z, Container as Zt, SERVER_CONTROLLER_PATH as _, isFlowAction as _t, createSessionStore as a, stack as at, decodeWireEnvelope as b, provide as bt, decodeSnapshot as c, NavigationError as ct, createWebSession as d, PrefetchedIntents as dt, isSplitNode as et, SESSION_DEFAULT_KEY as f, Router as ft, loadPage as g, isExternalUrlAction as gt, StorageUnavailableError as h, isCompoundAction as ht, createWebRuntime as i, split as it, createFullStateCodec as j, isRtl as jt, DEFAULT_NAV_PARAM as k, getLocaleAttributes as kt, encodeSnapshot as l, SPLIT_VISIBILITIES as lt, SessionError as m, ACTION_KINDS as mt, definePage as n, isTabsNode as nt, collectLeafKeys as o, tabs as ot, SESSION_DEFAULT_VERSION as p, ActionDispatcher as pt, resolveActivePath as q, CompositeLogger as qt, resolveInitialNavigation as r, leaf as rt, createNavigationScopedState as s, NAVIGATION_NODE_KINDS as st, parseCookieString as t, isStackNode as tt, NavigationCommitError as u, route as ut, ServerControllerProxy as v, makeExternalUrlAction as vt, defineWebApp as w, defineOperation as wt, getFrameworkBuildId as x, createRuntime as xt, FRAMEWORK_PROTOCOL_VERSION as y, makeFlowAction as yt, findNearestStack as z, secureFetch as zt };
3806
+
3807
+ //# sourceMappingURL=src-qwx7Vw8g.mjs.map