ignotum 0.0.13 → 0.0.15

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 (131) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/bin.mjs +6111 -4448
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
  5. package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
  6. package/dist/cli/ffi-7IElU4Co.mjs +148 -0
  7. package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
  8. package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
  9. package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
  10. package/dist/cli/managed-release.d.mts +1 -0
  11. package/dist/cli/managed-release.mjs +14 -0
  12. package/dist/cli/managed-release.mjs.map +1 -0
  13. package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
  14. package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
  15. package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
  16. package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
  17. package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
  18. package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
  19. package/dist/runtime/client.d.ts +50 -15
  20. package/dist/runtime/client.js +7 -1138
  21. package/dist/runtime/client.js.map +1 -1
  22. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +269 -0
  23. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
  24. package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
  25. package/dist/runtime/id--1m0NuUL.js.map +1 -0
  26. package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
  27. package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
  28. package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
  29. package/dist/runtime/image-Djtjy4Z8.js +19 -0
  30. package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
  31. package/dist/runtime/index-D4seNHlX.d.ts +184 -0
  32. package/dist/runtime/internal/api.d.ts +1 -1
  33. package/dist/runtime/internal/api.js +30 -1
  34. package/dist/runtime/internal/api.js.map +1 -0
  35. package/dist/runtime/internal/bootstrap.d.ts +2 -0
  36. package/dist/runtime/internal/bootstrap.js +2 -0
  37. package/dist/runtime/internal/client.d.ts +2 -0
  38. package/dist/runtime/internal/client.js +75 -0
  39. package/dist/runtime/internal/client.js.map +1 -0
  40. package/dist/runtime/internal/host.d.ts +25 -8
  41. package/dist/runtime/internal/host.js +27 -6
  42. package/dist/runtime/internal/host.js.map +1 -1
  43. package/dist/runtime/internal/routes.d.ts +2 -0
  44. package/dist/runtime/internal/routes.js +2 -0
  45. package/dist/runtime/internal/server.d.ts +1 -1
  46. package/dist/runtime/internal/server.js +1 -1
  47. package/dist/runtime/internal/types.d.ts +2 -1
  48. package/dist/runtime/internal/types.js +1 -1
  49. package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
  50. package/dist/runtime/pagination-D3qd6s9N.js +33 -0
  51. package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
  52. package/dist/runtime/result-BkziOG1L.d.ts +1 -0
  53. package/dist/runtime/router-BSBI1oN1.js +2377 -0
  54. package/dist/runtime/router-BSBI1oN1.js.map +1 -0
  55. package/dist/runtime/routes-Ce8TVP-T.js +690 -0
  56. package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
  57. package/dist/runtime/{schema-1Zs03-iS.js → schema-D1wOqiNH.js} +40 -12
  58. package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
  59. package/dist/runtime/server.d.ts +5 -5
  60. package/dist/runtime/server.js +2 -2
  61. package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
  62. package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
  63. package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
  64. package/dist/runtime/types-DeCCyqLU-BIm7rU-0.d.ts +341 -0
  65. package/package.json +29 -4
  66. package/src/cli/agent-files.ts +14 -8
  67. package/src/cli/build/managed-client.ts +411 -0
  68. package/src/cli/build/managed-server.ts +294 -0
  69. package/src/cli/build/managed.ts +67 -0
  70. package/src/cli/build/output.ts +47 -0
  71. package/src/cli/build/server.ts +51 -220
  72. package/src/cli/client-config.ts +1 -140
  73. package/src/cli/client-styles.ts +1 -0
  74. package/src/cli/codegen.ts +9 -4
  75. package/src/cli/command.ts +28 -5
  76. package/src/cli/control-client.ts +84 -101
  77. package/src/cli/deploy.ts +133 -177
  78. package/src/cli/dev.ts +506 -107
  79. package/src/cli/image-assets.ts +499 -0
  80. package/src/cli/managed-client-boundaries.ts +103 -0
  81. package/src/cli/managed-dev-platform.ts +118 -0
  82. package/src/cli/managed-exports.ts +219 -0
  83. package/src/cli/managed-release-bin.ts +13 -0
  84. package/src/cli/managed-release.ts +260 -0
  85. package/src/cli/module-boundaries.ts +11 -1
  86. package/src/cli/new-app.ts +111 -87
  87. package/src/cli/route-codegen.ts +311 -0
  88. package/src/cli/route-static.ts +710 -0
  89. package/src/client/bootstrap.ts +110 -0
  90. package/src/client/errors.ts +13 -7
  91. package/src/client/files.ts +64 -0
  92. package/src/client/id.ts +9 -5
  93. package/src/client/image.ts +28 -0
  94. package/src/client/index.ts +25 -2
  95. package/src/client/managed-client.ts +729 -0
  96. package/src/client/managed-upgrade.ts +283 -0
  97. package/src/client/recovery-journal.ts +195 -0
  98. package/src/client/route-boundaries.ts +184 -0
  99. package/src/client/router-history.ts +152 -0
  100. package/src/client/router-store.ts +644 -0
  101. package/src/client/router.ts +324 -0
  102. package/src/client/routes.ts +28 -0
  103. package/src/client/sync.ts +66 -592
  104. package/src/dev-runtime/database.ts +77 -2
  105. package/src/dev-runtime/functions.ts +3 -3
  106. package/src/dev-runtime/id.ts +19 -4
  107. package/src/dev-runtime/managed-functions.ts +255 -0
  108. package/src/dev-runtime/managed-sync.ts +263 -0
  109. package/src/dev-runtime/managed-websocket.ts +71 -0
  110. package/src/dev-runtime/migrations.ts +20 -0
  111. package/src/dev-runtime/sync.ts +236 -236
  112. package/src/image-assets.d.ts +36 -0
  113. package/src/internal/client.ts +186 -0
  114. package/src/internal/host.ts +1 -1
  115. package/src/internal/http-paths.ts +0 -3
  116. package/src/internal/routes.ts +45 -0
  117. package/dist/runtime/api-CAgKDij7.js.map +0 -1
  118. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
  119. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
  120. package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
  121. package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
  122. package/dist/runtime/index-D54flWtH.d.ts +0 -402
  123. package/dist/runtime/pagination-DnKg3dkI-r5ZUxBBx.d.ts +0 -112
  124. package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
  125. package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
  126. package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
  127. package/src/cli/build/client.ts +0 -119
  128. package/src/cli/build/public.ts +0 -186
  129. package/src/cli/client-entry.ts +0 -152
  130. package/src/cli/client-plugin.ts +0 -136
  131. package/src/client/app.ts +0 -15
@@ -0,0 +1,2377 @@
1
+ import { A as RuntimeFileMetadata, B as makeFileValue, D as encodeTransportObject, E as decodeTransportValue, F as fileIdOf, G as InvocationId, H as AppId, I as fileLimits, J as TableId, K as SessionEpoch, L as fileMimeTypes, N as fileFormats, P as fileGrantUrlOf, R as grantFileValue, U as GeneratedId, W as IdAlphabet, Y as VersionId, _ as Result$1, a as SessionResponse, d as sessionPath, f as sessionRequestsPath, i as SessionRequest, j as RuntimeFileOccurrence, k as FileId, l as developmentSelectionKey, n as SessionOutcome, o as SessionState, p as initial, q as SubscriptionId, r as SessionRedirect, t as DevelopmentSelection, x as pending, y as failureFromWire, z as isFileValue } from "./id--1m0NuUL.js";
2
+ import { a as ActivationGeneration, i as WireSuccess, l as DataHistoryId, n as WireFailure, o as AppStateRevision, r as WireResult, t as FunctionAddress, u as InvocationKey } from "./sync-DzUwA8W9.js";
3
+ import { a as RouteDefinition, d as RecoveryIdentity, f as RecoverySessionId, l as appDefinitionLimits, n as getRouteDefinition, p as sameExecutionSelection, r as LayoutDefinition, u as ExecutionSelection } from "./routes-Ce8TVP-T.js";
4
+ import { Array as Array$1, Cause, Context, DateTime, Deferred, Effect, ErrorReporter, Layer, ManagedRuntime, Option, Predicate, Schema, Semaphore, String as String$1 } from "effect";
5
+ import { customAlphabet } from "nanoid";
6
+ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
7
+ import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
8
+ import { createContext, createElement } from "preact";
9
+ //#region ../contracts/dist/json.js
10
+ const encodeJson = (value) => {
11
+ if (Array$1.isArray(value)) return `[${value.map(encodeJson).join(",")}]`;
12
+ if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return JSON.stringify(value);
13
+ return `{${Array$1.sortWith(Object.entries(value), ([key]) => key, String$1.Order).map(([key, field]) => `${JSON.stringify(key)}:${encodeJson(field)}`).join(",")}}`;
14
+ };
15
+ const encodeCanonicalJson = (value) => encodeJson(Schema.decodeSync(Schema.Json)(value));
16
+ //#endregion
17
+ //#region src/client/errors.ts
18
+ /** @effect-diagnostics globalConsole:skip-file */
19
+ var ConnectionUnavailable = class extends Schema.TaggedError()("ConnectionUnavailable", {
20
+ cause: Schema.optional(Schema.Defect()),
21
+ message: Schema.String
22
+ }) {};
23
+ var PlatformUpgrading = class extends Schema.TaggedError()("PlatformUpgrading", { message: Schema.String }) {};
24
+ var InvalidMutationArguments = class extends Schema.TaggedError()("InvalidMutationArguments", {
25
+ cause: Schema.Defect(),
26
+ function: FunctionAddress,
27
+ message: Schema.String
28
+ }) {};
29
+ var InvalidServerMessage = class extends Schema.TaggedError()("InvalidServerMessage", {
30
+ cause: Schema.Defect(),
31
+ message: Schema.String
32
+ }) {};
33
+ var ServerError = class extends Schema.TaggedError()("ServerError", {
34
+ code: Schema.Literals([
35
+ "PlatformUpgrading",
36
+ "SessionExpired",
37
+ "InvalidMessage",
38
+ "Unavailable",
39
+ "InvalidArguments",
40
+ "InvocationIdConflict",
41
+ "ResourceLimitExceeded"
42
+ ]),
43
+ message: Schema.String
44
+ }) {};
45
+ const ClientInfrastructureError = Schema.Union([
46
+ PlatformUpgrading,
47
+ ConnectionUnavailable,
48
+ InvalidMutationArguments,
49
+ InvalidServerMessage,
50
+ ServerError
51
+ ]);
52
+ const browserErrorReporter = ErrorReporter.make(({ attributes, error }) => {
53
+ globalThis.console.error("Ignotum infrastructure error", error, attributes);
54
+ });
55
+ const clientErrorReporterLayer = ErrorReporter.layer([browserErrorReporter]);
56
+ Effect.fn("ClientErrorReporter.report")(function* (error) {
57
+ yield* ErrorReporter.report(Cause.fail(error));
58
+ });
59
+ //#endregion
60
+ //#region ../shared/dist/id.js
61
+ const decodeGenerated = (definition, payload) => Schema.decodeSync(definition)(definition.idPrefix === void 0 ? payload : `${definition.idPrefix}_${payload}`);
62
+ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/id/IdGenerator") {
63
+ static layer = Layer.sync(IdGenerator, () => {
64
+ const generatePayload = customAlphabet(IdAlphabet, 24);
65
+ return IdGenerator.of({ generate: (definition) => Effect.sync(() => decodeGenerated(definition, generatePayload())) });
66
+ });
67
+ static deterministic = (startAt = 1) => Layer.sync(IdGenerator, () => {
68
+ const counters = /* @__PURE__ */ new Map();
69
+ return IdGenerator.of({ generate: (definition) => Effect.sync(() => {
70
+ const counter = (counters.get(definition.idPrefix) ?? startAt - 1) + 1;
71
+ counters.set(definition.idPrefix, counter);
72
+ return decodeGenerated(definition, counter.toString(36).padStart(24, "0"));
73
+ }) });
74
+ });
75
+ };
76
+ //#endregion
77
+ //#region src/client/files.ts
78
+ const Files = { url: (file) => {
79
+ const url = fileGrantUrlOf(file);
80
+ if (url === void 0) throw new Error("This FileValue has no active query grant.");
81
+ return url;
82
+ } };
83
+ const formatByMime = {};
84
+ for (const format of fileFormats) formatByMime[fileMimeTypes[format]] = format;
85
+ const MutationObject = Schema.ObjectKeyword.check(Schema.makeFilter((value) => Object.prototype.toString.call(value) === "[object Object]", { message: "Mutation objects must contain ordinary fields; built-in collections are unsupported." }));
86
+ const collectNativeFiles = (value, files) => {
87
+ if (value instanceof File) {
88
+ files.add(value);
89
+ return;
90
+ }
91
+ if (isFileValue(value) || Predicate.isDate(value)) return;
92
+ if (globalThis.Array.isArray(value)) {
93
+ for (const child of value) collectNativeFiles(child, files);
94
+ return;
95
+ }
96
+ if (!Predicate.isObject(value)) return;
97
+ for (const child of Object.values(Schema.decodeSync(MutationObject)(value))) collectNativeFiles(child, files);
98
+ };
99
+ const replaceNativeFiles = (value, replacements) => {
100
+ if (value instanceof File) return replacements.get(value);
101
+ if (isFileValue(value) || Predicate.isDate(value)) return value;
102
+ if (globalThis.Array.isArray(value)) return value.map((child) => replaceNativeFiles(child, replacements));
103
+ if (!Predicate.isObject(value)) return value;
104
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, replaceNativeFiles(child, replacements)]));
105
+ };
106
+ const prepareMutationArguments = Effect.fn("SyncClient.prepareMutationArguments")(function* (input) {
107
+ const ids = yield* IdGenerator;
108
+ const unique = yield* Effect.try(() => {
109
+ const files = /* @__PURE__ */ new Set();
110
+ collectNativeFiles(input, files);
111
+ if (files.size > fileLimits.filesPerMutation) throw new Error(`A mutation may upload at most ${fileLimits.filesPerMutation} files.`);
112
+ if ([...files].reduce((total, file) => total + file.size, 0) > fileLimits.mutationBytes) throw new Error(`Mutation uploads may total at most ${fileLimits.mutationBytes} bytes.`);
113
+ return files;
114
+ });
115
+ const replacements = /* @__PURE__ */ new Map();
116
+ const nativeFiles = /* @__PURE__ */ new Map();
117
+ for (const file of unique) {
118
+ const format = yield* Effect.try(() => {
119
+ const format = formatByMime[file.type.toLowerCase()];
120
+ if (format === void 0) throw new Error(`File '${file.name}' has unsupported media type '${file.type || "unknown"}'.`);
121
+ if (file.size === 0 || file.size > fileLimits.fileBytes) throw new Error(`File '${file.name}' must be between 1 and ${fileLimits.fileBytes} bytes.`);
122
+ if (new TextEncoder().encode(file.name).byteLength > fileLimits.filenameBytes) throw new Error(`File '${file.name}' has a filename that is too long.`);
123
+ return format;
124
+ });
125
+ const id = yield* ids.generate(FileId);
126
+ replacements.set(file, makeFileValue(id, {
127
+ format,
128
+ name: file.name,
129
+ size: file.size
130
+ }));
131
+ nativeFiles.set(id, file);
132
+ }
133
+ return {
134
+ value: unique.size === 0 ? input : yield* Effect.try(() => replaceNativeFiles(input, replacements)),
135
+ nativeFiles,
136
+ fileIds: Array.from(nativeFiles.keys())
137
+ };
138
+ });
139
+ const applyFileGrants = (value, grants) => {
140
+ if (grants.length === 0) return value;
141
+ const byId = /* @__PURE__ */ new Map();
142
+ for (const grant of grants) {
143
+ if (byId.has(grant.id)) throw new Error("A query file grant ID is duplicated.");
144
+ byId.set(grant.id, grant.url);
145
+ }
146
+ const matched = /* @__PURE__ */ new Set();
147
+ const visit = (current) => {
148
+ if (isFileValue(current)) {
149
+ const id = fileIdOf(current);
150
+ const url = byId.get(id);
151
+ if (url === void 0) return current;
152
+ matched.add(id);
153
+ return grantFileValue(current, url);
154
+ }
155
+ if (globalThis.Array.isArray(current)) return current.map(visit);
156
+ if (!Predicate.isObject(current) || Predicate.isDate(current)) return current;
157
+ return Object.fromEntries(Object.entries(current).map(([key, child]) => [key, visit(child)]));
158
+ };
159
+ const granted = visit(value);
160
+ if (matched.size !== byId.size) throw new Error("A query file grant has no result value.");
161
+ return granted;
162
+ };
163
+ const uploadPreparedFiles = Effect.fn("uploadPreparedFiles")(function* (nativeFiles, uploads, functionAddress) {
164
+ const expected = new Set(nativeFiles.keys());
165
+ const received = /* @__PURE__ */ new Set();
166
+ const transfers = [];
167
+ for (const upload of uploads) {
168
+ if (!expected.has(upload.id) || received.has(upload.id)) return yield* InvalidServerMessage.make({
169
+ cause: /* @__PURE__ */ new Error(`Unexpected prepared file '${upload.id}'.`),
170
+ message: "The server returned invalid application file preparation data."
171
+ });
172
+ received.add(upload.id);
173
+ const file = nativeFiles.get(upload.id);
174
+ if (file === void 0) return yield* InvalidServerMessage.make({
175
+ cause: /* @__PURE__ */ new Error(`Missing native file '${upload.id}'.`),
176
+ message: "The server prepared an unknown application file."
177
+ });
178
+ if (upload.url === void 0) continue;
179
+ transfers.push({
180
+ file,
181
+ url: upload.url
182
+ });
183
+ }
184
+ if (received.size !== expected.size) return yield* InvalidServerMessage.make({
185
+ cause: /* @__PURE__ */ new Error("The server omitted a prepared file."),
186
+ message: "The server returned incomplete application file preparation data."
187
+ });
188
+ yield* Effect.forEach(transfers, Effect.fn("SyncClient.uploadFile")(function* ({ file, url }) {
189
+ const response = yield* Effect.tryPromise({
190
+ try: (signal) => fetch(url, {
191
+ method: "PUT",
192
+ body: file,
193
+ headers: { "content-type": file.type },
194
+ signal
195
+ }),
196
+ catch: (cause) => ConnectionUnavailable.make({
197
+ cause,
198
+ message: `File '${file.name}' could not be uploaded.`
199
+ })
200
+ });
201
+ if (!response.ok) {
202
+ const detail = yield* Effect.promise(() => response.text());
203
+ return yield* InvalidMutationArguments.make({
204
+ cause: new Error(detail),
205
+ function: functionAddress,
206
+ message: `File '${file.name}' was rejected by application storage.`
207
+ });
208
+ }
209
+ }), {
210
+ concurrency: 3,
211
+ discard: true
212
+ });
213
+ });
214
+ //#endregion
215
+ //#region src/client/id.ts
216
+ var IdRequestError = class extends Schema.TaggedError()("IdRequestError", { message: Schema.String }) {};
217
+ const outcomeKey = "ignotum.id.outcome";
218
+ const selectionJson = Schema.fromJsonString(DevelopmentSelection);
219
+ var BrowserSession = class BrowserSession extends Context.Service()("ignotum/client/id/BrowserSession") {
220
+ static layer = Layer.effect(BrowserSession, Effect.gen(function* () {
221
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
222
+ const ids = yield* IdGenerator;
223
+ const bootstrapLock = yield* Semaphore.make(1);
224
+ const listeners = /* @__PURE__ */ new Set();
225
+ let state = { status: "pending" };
226
+ let current;
227
+ let generation = 0;
228
+ let deadline = Infinity;
229
+ let development = false;
230
+ let selection;
231
+ let expiry;
232
+ const notify = () => {
233
+ for (const listener of listeners) listener();
234
+ };
235
+ const suspend = (error) => {
236
+ generation += 1;
237
+ deadline = Infinity;
238
+ if (expiry !== void 0) clearTimeout(expiry);
239
+ current = void 0;
240
+ state = error === void 0 ? {
241
+ status: "pending",
242
+ outcome: state.outcome
243
+ } : {
244
+ status: "error",
245
+ error,
246
+ outcome: state.outcome
247
+ };
248
+ notify();
249
+ };
250
+ const accept = (session, serverTime) => {
251
+ generation += 1;
252
+ if (expiry !== void 0) clearTimeout(expiry);
253
+ current = session;
254
+ state = session.user === null ? {
255
+ status: "signedOut",
256
+ outcome: state.outcome
257
+ } : {
258
+ status: "signedIn",
259
+ user: session.user,
260
+ outcome: state.outcome
261
+ };
262
+ const delay = Math.max(0, session.validUntil - serverTime);
263
+ deadline = performance.now() + delay;
264
+ if (delay < 2147483647) expiry = setTimeout(() => suspend(), delay);
265
+ notify();
266
+ };
267
+ const checkExpiry = () => {
268
+ if (current !== void 0 && performance.now() >= deadline) suspend();
269
+ };
270
+ globalThis.addEventListener?.("focus", checkExpiry);
271
+ globalThis.document?.addEventListener("visibilitychange", checkExpiry);
272
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
273
+ if (expiry !== void 0) clearTimeout(expiry);
274
+ listeners.clear();
275
+ globalThis.removeEventListener?.("focus", checkExpiry);
276
+ globalThis.document?.removeEventListener("visibilitychange", checkExpiry);
277
+ }));
278
+ const readOutcome = () => {
279
+ try {
280
+ const raw = globalThis.sessionStorage.getItem(outcomeKey);
281
+ if (raw !== null) {
282
+ const decoded = Schema.decodeUnknownOption(SessionOutcome)(raw);
283
+ if (Option.isSome(decoded)) state = {
284
+ ...state,
285
+ outcome: decoded.value
286
+ };
287
+ globalThis.sessionStorage.removeItem(outcomeKey);
288
+ }
289
+ } catch {}
290
+ };
291
+ const parameters = () => {
292
+ if (!development || selection === void 0) return "";
293
+ const params = new URLSearchParams({ epoch: selection.epoch });
294
+ if (selection.username !== null) params.set("username", selection.username);
295
+ return `?${params}`;
296
+ };
297
+ const readSession = Effect.fn("BrowserSession.read")(function* () {
298
+ return yield* client.get(`${sessionPath}${parameters()}`, { headers: { "x-ignotum-request": "1" } }).pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(SessionResponse)));
299
+ });
300
+ let bootstrapGeneration = 0;
301
+ const bootstrap = bootstrapLock.withPermits(1)(Effect.gen(function* () {
302
+ const admittedGeneration = generation;
303
+ bootstrapGeneration = admittedGeneration;
304
+ const startedAt = performance.now();
305
+ let session = yield* readSession();
306
+ development = session.development === true;
307
+ if (development && selection === void 0) {
308
+ const saved = globalThis.sessionStorage.getItem(developmentSelectionKey);
309
+ const decoded = saved === null ? Option.none() : Schema.decodeOption(selectionJson)(saved);
310
+ selection = Option.getOrElse(decoded, () => ({
311
+ formatVersion: 1,
312
+ username: null,
313
+ epoch: ""
314
+ }));
315
+ if (selection.epoch === "") selection = {
316
+ ...selection,
317
+ epoch: yield* ids.generate(SessionEpoch)
318
+ };
319
+ globalThis.sessionStorage.setItem(developmentSelectionKey, Schema.encodeSync(selectionJson)(selection));
320
+ session = yield* readSession();
321
+ }
322
+ if (generation !== admittedGeneration) {
323
+ if (current !== void 0) return current;
324
+ return yield* IdRequestError.make({ message: "Your ID session changed while loading." });
325
+ }
326
+ readOutcome();
327
+ if (session.outcome !== void 0) state = {
328
+ ...state,
329
+ outcome: session.outcome
330
+ };
331
+ accept(session, session.serverTime + performance.now() - startedAt);
332
+ return session;
333
+ })).pipe(Effect.mapError(() => IdRequestError.make({ message: "Could not load your ID session." })), Effect.tapError((error) => Effect.sync(() => {
334
+ if (generation === bootstrapGeneration) suspend(error.message);
335
+ })));
336
+ const start = Effect.fn("BrowserSession.start")(function* (intent, profile, returnTo) {
337
+ const session = current ?? (yield* bootstrap);
338
+ if (intent === "requestProfile" && session.user === null) return yield* IdRequestError.make({ message: "Sign in before requesting profile information." });
339
+ const request = yield* Schema.decodeUnknownEffect(SessionRequest)({
340
+ intent,
341
+ profile,
342
+ sessionEpoch: session.sessionEpoch,
343
+ returnTo: returnTo ?? `${globalThis.location.pathname}${globalThis.location.search}${globalThis.location.hash}`
344
+ }).pipe(Effect.mapError(() => IdRequestError.make({ message: "The ID request is invalid." })));
345
+ const redirect = yield* HttpClientRequest.post(sessionRequestsPath).pipe(HttpClientRequest.setHeader("x-ignotum-request", "1"), HttpClientRequest.bodyJsonUnsafe(request), client.execute, Effect.flatMap(HttpClientResponse.schemaBodyJson(SessionRedirect)), Effect.mapError(() => IdRequestError.make({ message: "Could not start the ID request." })));
346
+ if (current?.sessionEpoch !== session.sessionEpoch) return yield* IdRequestError.make({ message: "Your ID session changed. Try again." });
347
+ globalThis.location.assign(redirect.redirectUrl);
348
+ });
349
+ const signOut = Effect.gen(function* () {
350
+ const session = current ?? (yield* bootstrap);
351
+ yield* HttpClientRequest.delete(sessionPath).pipe(HttpClientRequest.setHeader("x-ignotum-request", "1"), HttpClientRequest.bodyJsonUnsafe({ sessionEpoch: session.sessionEpoch }), client.execute, Effect.mapError(() => IdRequestError.make({ message: "Could not sign out." })));
352
+ suspend();
353
+ if (development) {
354
+ selection = {
355
+ formatVersion: 1,
356
+ username: null,
357
+ epoch: yield* ids.generate(SessionEpoch)
358
+ };
359
+ globalThis.sessionStorage.setItem(developmentSelectionKey, Schema.encodeSync(selectionJson)(selection));
360
+ }
361
+ globalThis.location.reload();
362
+ });
363
+ return BrowserSession.of({
364
+ snapshot: () => {
365
+ checkExpiry();
366
+ return state;
367
+ },
368
+ current: () => {
369
+ checkExpiry();
370
+ return current;
371
+ },
372
+ subscribe: (listener) => {
373
+ listeners.add(listener);
374
+ return () => {
375
+ listeners.delete(listener);
376
+ };
377
+ },
378
+ accept,
379
+ suspend,
380
+ bootstrap,
381
+ start,
382
+ signOut,
383
+ socketParameters: parameters
384
+ });
385
+ })).pipe(Layer.provide(FetchHttpClient.layer));
386
+ };
387
+ //#endregion
388
+ //#region ../contracts/dist/runtime/index.js
389
+ const IndexIdentity = Schema.String.pipe(Schema.brand("ignotum/runtime/IndexIdentity"));
390
+ const EncodedIndexKey = Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"));
391
+ Schema.Union([
392
+ Schema.Boolean,
393
+ Schema.Finite,
394
+ Schema.String
395
+ ]);
396
+ Schema.String.pipe(Schema.brand("ignotum/hosted/QueryKey"));
397
+ const TableDependency = Schema.Struct({
398
+ type: Schema.Literal("Table"),
399
+ tableId: TableId
400
+ });
401
+ const DocumentDependency = Schema.Struct({
402
+ type: Schema.Literal("Document"),
403
+ tableId: TableId,
404
+ id: GeneratedId
405
+ });
406
+ const IndexRangeDependency = Schema.Struct({
407
+ type: Schema.Literal("IndexRange"),
408
+ tableId: TableId,
409
+ index: IndexIdentity,
410
+ lower: Schema.optional(EncodedIndexKey),
411
+ upper: Schema.optional(EncodedIndexKey)
412
+ });
413
+ const IndexPointInvalidation = Schema.Struct({
414
+ type: Schema.Literal("IndexPoint"),
415
+ tableId: TableId,
416
+ index: IndexIdentity,
417
+ key: EncodedIndexKey
418
+ });
419
+ const IdDependency = Schema.Struct({ type: Schema.Literal("Id") });
420
+ const ReadDependency = Schema.Union([
421
+ IdDependency,
422
+ TableDependency,
423
+ DocumentDependency,
424
+ IndexRangeDependency
425
+ ]);
426
+ const WriteInvalidation = Schema.Union([
427
+ TableDependency,
428
+ DocumentDependency,
429
+ IndexPointInvalidation
430
+ ]);
431
+ Schema.Union([ReadDependency, IndexPointInvalidation]);
432
+ const DependencySet = Schema.Array(ReadDependency);
433
+ const InvalidationSet = Schema.Array(WriteInvalidation);
434
+ const RuntimeQueryResult = Schema.Struct({
435
+ type: Schema.Literal("Query"),
436
+ result: WireResult,
437
+ dependencies: DependencySet,
438
+ observedRevision: AppStateRevision,
439
+ files: Schema.optional(Schema.Array(RuntimeFileOccurrence))
440
+ });
441
+ const FileReferenceAdded = Schema.Struct({
442
+ type: Schema.Literal("AddReference"),
443
+ file: RuntimeFileMetadata,
444
+ tableId: TableId,
445
+ documentId: GeneratedId
446
+ });
447
+ const FileReferenceRemoved = Schema.Struct({
448
+ type: Schema.Literal("RemoveReference"),
449
+ fileId: FileId,
450
+ tableId: TableId,
451
+ documentId: GeneratedId
452
+ });
453
+ const FileReferenceEffect = Schema.Union([FileReferenceAdded, FileReferenceRemoved]);
454
+ const FileEffectBatch = Schema.Struct({
455
+ id: InvocationId,
456
+ dataHistoryId: Schema.optional(DataHistoryId),
457
+ committedRevision: AppStateRevision,
458
+ effects: Schema.Array(FileReferenceEffect)
459
+ });
460
+ Schema.Struct({ appId: AppId });
461
+ Schema.Struct({ batches: Schema.Array(FileEffectBatch) });
462
+ Schema.Struct({
463
+ appId: AppId,
464
+ invocationId: InvocationId,
465
+ acknowledgedAt: Schema.Int,
466
+ dataHistoryId: DataHistoryId
467
+ });
468
+ Schema.Struct({ completed: Schema.Literal(true) });
469
+ const RuntimeMutationSuccess = Schema.Struct({
470
+ type: Schema.Literal("Mutation"),
471
+ result: WireSuccess,
472
+ invalidations: InvalidationSet,
473
+ committedRevision: AppStateRevision,
474
+ fileEffects: Schema.optional(FileEffectBatch)
475
+ });
476
+ const RuntimeMutationFailure = Schema.Struct({
477
+ type: Schema.Literal("Mutation"),
478
+ result: WireFailure
479
+ });
480
+ const RuntimeMutationResult = Schema.Union([RuntimeMutationSuccess, RuntimeMutationFailure]);
481
+ Schema.Union([RuntimeQueryResult, RuntimeMutationResult]);
482
+ const RuntimeRecoveryOwner = Schema.Struct({
483
+ clientSessionId: RecoverySessionId,
484
+ sessionEpoch: Schema.NullOr(Schema.String)
485
+ });
486
+ Schema.Struct({
487
+ appId: AppId,
488
+ identity: RecoveryIdentity,
489
+ owner: RuntimeRecoveryOwner
490
+ });
491
+ const RuntimeMutationOutcome = Schema.Union([
492
+ Schema.Struct({
493
+ type: Schema.Literal("Settled"),
494
+ result: RuntimeMutationResult
495
+ }),
496
+ Schema.Struct({ type: Schema.Literal("Interrupted") }),
497
+ Schema.Struct({ type: Schema.Literal("Pending") }),
498
+ Schema.Struct({ type: Schema.Literal("Uncertain") })
499
+ ]);
500
+ const InvocationBase = {
501
+ appId: AppId,
502
+ versionId: VersionId,
503
+ generation: ActivationGeneration,
504
+ selection: Schema.optional(ExecutionSelection),
505
+ invocationKey: InvocationKey,
506
+ function: FunctionAddress,
507
+ args: Schema.Json,
508
+ identity: Schema.optional(SessionState)
509
+ };
510
+ const QueryInvocation = Schema.Struct({
511
+ ...InvocationBase,
512
+ type: Schema.Literal("Query")
513
+ });
514
+ const MutationInvocation = Schema.Struct({
515
+ ...InvocationBase,
516
+ type: Schema.Literal("Mutation"),
517
+ invocationId: InvocationId,
518
+ recoveryOwner: Schema.optional(RuntimeRecoveryOwner),
519
+ files: Schema.optional(Schema.Array(RuntimeFileOccurrence))
520
+ });
521
+ Schema.Union([QueryInvocation, MutationInvocation]);
522
+ Schema.Struct({
523
+ appId: AppId,
524
+ expectedRevision: AppStateRevision
525
+ });
526
+ Schema.Struct({ currentRevision: AppStateRevision });
527
+ var RuntimeVersionUnavailable = class extends Schema.TaggedError()("RuntimeVersionUnavailable", {
528
+ versionId: VersionId,
529
+ message: Schema.String
530
+ }) {};
531
+ const RuntimeInvocationRejectionCode = Schema.Literals([
532
+ "SessionExpired",
533
+ "FunctionUnavailable",
534
+ "InvalidArguments",
535
+ "InvocationIdConflict",
536
+ "ResourceLimitExceeded",
537
+ "UnknownFunction",
538
+ "WrongFunctionKind"
539
+ ]);
540
+ var RuntimeInvocationRejected = class extends Schema.TaggedError()("RuntimeInvocationRejected", {
541
+ code: RuntimeInvocationRejectionCode,
542
+ invocationKey: InvocationKey,
543
+ message: Schema.String
544
+ }) {};
545
+ var RuntimeInvocationUnavailable = class extends Schema.TaggedError()("RuntimeInvocationUnavailable", {
546
+ invocationKey: InvocationKey,
547
+ message: Schema.String
548
+ }) {};
549
+ Schema.TaggedError()("RuntimeRevisionRejected", {
550
+ appId: AppId,
551
+ message: Schema.String
552
+ });
553
+ Schema.Union([
554
+ RuntimeVersionUnavailable,
555
+ RuntimeInvocationRejected,
556
+ RuntimeInvocationUnavailable
557
+ ]);
558
+ //#endregion
559
+ //#region ../contracts/dist/managed-sync.js
560
+ const managedSyncPath = "/_ignotum/v2/sync";
561
+ const managedSelectionPath = "/_ignotum/v2/selection";
562
+ const ManagedSelectionResponse = Schema.Struct({
563
+ protocolVersion: Schema.Literal(1),
564
+ selection: ExecutionSelection
565
+ });
566
+ const ManagedClientMessage = Schema.Union([
567
+ Schema.Struct({
568
+ type: Schema.Literal("Connect"),
569
+ protocolVersion: Schema.Natural,
570
+ selection: ExecutionSelection,
571
+ clientSessionId: RecoverySessionId
572
+ }),
573
+ Schema.Struct({
574
+ type: Schema.Literal("Prepare"),
575
+ invocationId: InvocationId,
576
+ function: FunctionAddress,
577
+ args: Schema.Json,
578
+ files: Schema.Array(FileId).check(Schema.isMaxLength(fileLimits.filesPerMutation))
579
+ }),
580
+ Schema.Struct({
581
+ type: Schema.Literal("Invoke"),
582
+ invocationId: InvocationId,
583
+ function: FunctionAddress,
584
+ args: Schema.Json
585
+ }),
586
+ Schema.Struct({
587
+ type: Schema.Literal("RecoverMutation"),
588
+ id: Schema.String.check(Schema.isMaxLength(128)),
589
+ identity: RecoveryIdentity
590
+ }),
591
+ Schema.Struct({
592
+ type: Schema.Literal("Subscribe"),
593
+ id: SubscriptionId,
594
+ function: FunctionAddress,
595
+ args: Schema.Json
596
+ }),
597
+ Schema.Struct({
598
+ type: Schema.Literal("Unsubscribe"),
599
+ id: SubscriptionId
600
+ })
601
+ ]);
602
+ /** Decode this envelope before the normal server protocol, including on unsupported versions. */
603
+ const ManagedUpgradeRequired = Schema.Struct({
604
+ type: Schema.Literal("UpgradeRequired"),
605
+ selection: ExecutionSelection
606
+ });
607
+ const ManagedServerMessage = Schema.Union([
608
+ ManagedUpgradeRequired,
609
+ Schema.Struct({
610
+ type: Schema.Literal("Handshake"),
611
+ protocolVersion: Schema.Literal(1),
612
+ selection: ExecutionSelection,
613
+ session: SessionState,
614
+ serverTime: Schema.Finite
615
+ }),
616
+ Schema.Struct({
617
+ type: Schema.Literal("Preparation"),
618
+ invocationId: InvocationId,
619
+ uploads: Schema.Array(Schema.Struct({
620
+ id: FileId,
621
+ url: Schema.optional(Schema.String)
622
+ }))
623
+ }),
624
+ Schema.Struct({
625
+ type: Schema.Literal("MutationOutcome"),
626
+ id: Schema.String,
627
+ identity: RecoveryIdentity,
628
+ outcome: RuntimeMutationOutcome
629
+ }),
630
+ Schema.Struct({
631
+ type: Schema.Literal("QuerySnapshot"),
632
+ selection: ExecutionSelection,
633
+ id: SubscriptionId,
634
+ result: RuntimeQueryResult,
635
+ files: Schema.optional(Schema.Array(Schema.Struct({
636
+ id: FileId,
637
+ url: Schema.String
638
+ })))
639
+ }),
640
+ Schema.Struct({
641
+ type: Schema.Literal("Error"),
642
+ id: Schema.optional(Schema.String),
643
+ code: Schema.Literals([
644
+ "PlatformUpgrading",
645
+ "SessionExpired",
646
+ "InvalidMessage",
647
+ "Unavailable",
648
+ "InvalidArguments",
649
+ "InvocationIdConflict",
650
+ "ResourceLimitExceeded"
651
+ ]),
652
+ message: Schema.String
653
+ })
654
+ ]);
655
+ //#endregion
656
+ //#region src/client/managed-upgrade.ts
657
+ const TabStateV1 = Schema.Struct({
658
+ formatVersion: Schema.Literal(1),
659
+ appId: AppId,
660
+ clientSessionId: RecoverySessionId,
661
+ attempts: Schema.Natural.check(Schema.isLessThanOrEqualTo(3)),
662
+ attemptedAt: Schema.Natural,
663
+ target: Schema.NullOr(ExecutionSelection),
664
+ uncertainStorage: Schema.Boolean
665
+ }).annotate({ parseOptions: { onExcessProperty: "error" } });
666
+ const TabState = initial(TabStateV1);
667
+ const TabStateJson = Schema.fromJsonString(TabState);
668
+ /** One attempt owns admission, the settling deadline, and the reload across target changes. */
669
+ var ManagedUpgrade = class {
670
+ selection;
671
+ browser;
672
+ callbacks;
673
+ clientSessionId;
674
+ key;
675
+ tab;
676
+ snapshot = {
677
+ phase: "connecting",
678
+ recovery: []
679
+ };
680
+ listeners = /* @__PURE__ */ new Set();
681
+ target;
682
+ attempt;
683
+ disposed = false;
684
+ storageAvailable = true;
685
+ readyTimer;
686
+ constructor(selection, browser, callbacks) {
687
+ this.selection = selection;
688
+ this.browser = browser;
689
+ this.callbacks = callbacks;
690
+ this.key = `ignotum.managed.tab.${selection.appId}`;
691
+ this.tab = {
692
+ formatVersion: 1,
693
+ appId: selection.appId,
694
+ clientSessionId: RecoverySessionId.make(`rec_${browser.crypto.randomUUID().replaceAll("-", "").slice(0, 24)}`),
695
+ attempts: 0,
696
+ attemptedAt: 0,
697
+ target: null,
698
+ uncertainStorage: false
699
+ };
700
+ try {
701
+ const raw = browser.sessionStorage.getItem(this.key);
702
+ if (raw !== null) {
703
+ const decoded = Schema.decodeOption(TabStateJson)(raw);
704
+ if (Option.isSome(decoded) && decoded.value.appId === selection.appId && (decoded.value.target === null || decoded.value.target.appId === selection.appId)) this.tab = decoded.value;
705
+ else this.reportRecovery("Saved update information could not be read. Previous mutation outcomes may be unknown.");
706
+ }
707
+ } catch {
708
+ this.storageAvailable = false;
709
+ }
710
+ const history = Schema.decodeUnknownOption(Schema.Struct({ __ignotumUpdate: TabState }))(browser.history.state);
711
+ if (Option.isSome(history) && history.value.__ignotumUpdate.appId === selection.appId && history.value.__ignotumUpdate.attemptedAt >= this.tab.attemptedAt && (history.value.__ignotumUpdate.target === null || history.value.__ignotumUpdate.target.appId === selection.appId)) this.tab = history.value.__ignotumUpdate;
712
+ if (this.tab.uncertainStorage) this.reportRecovery("Some previous mutation outcomes could not be saved in browser storage and remain unknown.");
713
+ this.clientSessionId = this.tab.clientSessionId;
714
+ this.save();
715
+ }
716
+ getSnapshot = () => this.snapshot;
717
+ subscribe = (listener) => {
718
+ this.listeners.add(listener);
719
+ return () => this.listeners.delete(listener);
720
+ };
721
+ update(next) {
722
+ if (this.disposed) return;
723
+ this.snapshot = next;
724
+ for (const listener of this.listeners) listener();
725
+ }
726
+ reportRecovery(message) {
727
+ if (!this.snapshot.recovery.includes(message)) this.update({
728
+ ...this.snapshot,
729
+ recovery: [...this.snapshot.recovery, message].slice(-100)
730
+ });
731
+ }
732
+ dismissRecovery = () => {
733
+ this.tab = {
734
+ ...this.tab,
735
+ uncertainStorage: false
736
+ };
737
+ this.save();
738
+ this.update({
739
+ ...this.snapshot,
740
+ recovery: []
741
+ });
742
+ };
743
+ save() {
744
+ let saved = false;
745
+ try {
746
+ this.browser.sessionStorage.setItem(this.key, Schema.encodeSync(TabStateJson)(this.tab));
747
+ this.storageAvailable = true;
748
+ saved = true;
749
+ } catch {
750
+ this.storageAvailable = false;
751
+ }
752
+ try {
753
+ const existing = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown))(this.browser.history.state);
754
+ this.browser.history.replaceState({
755
+ ...Option.getOrElse(existing, () => ({})),
756
+ __ignotumUpdate: Schema.encodeSync(TabState)(this.tab)
757
+ }, "");
758
+ saved = true;
759
+ } catch {}
760
+ return saved;
761
+ }
762
+ get obsolete() {
763
+ return this.target !== void 0 || this.attempt !== void 0;
764
+ }
765
+ get ready() {
766
+ return this.snapshot.phase === "ready" && !this.obsolete;
767
+ }
768
+ connecting() {
769
+ if (this.readyTimer !== void 0) clearTimeout(this.readyTimer);
770
+ if (!this.obsolete) this.update({
771
+ ...this.snapshot,
772
+ phase: "connecting",
773
+ message: void 0
774
+ });
775
+ }
776
+ accepted() {
777
+ if (this.obsolete) return;
778
+ this.update({
779
+ ...this.snapshot,
780
+ phase: "ready",
781
+ message: void 0
782
+ });
783
+ if (this.readyTimer !== void 0) clearTimeout(this.readyTimer);
784
+ this.readyTimer = setTimeout(() => {
785
+ this.tab = {
786
+ ...this.tab,
787
+ attempts: 0,
788
+ attemptedAt: 0,
789
+ target: null
790
+ };
791
+ this.save();
792
+ }, 6e4);
793
+ }
794
+ unavailable(message = "The app could not check for updates. Reconnect and try again.") {
795
+ if (this.readyTimer !== void 0) clearTimeout(this.readyTimer);
796
+ if (!this.obsolete) this.update({
797
+ ...this.snapshot,
798
+ phase: "error",
799
+ message
800
+ });
801
+ }
802
+ require(target) {
803
+ if (this.disposed) return Promise.resolve();
804
+ if (target !== void 0) this.target = target;
805
+ this.callbacks.stopAdmission();
806
+ if (this.attempt !== void 0 && this.snapshot.phase !== "error") return this.attempt;
807
+ if (this.readyTimer !== void 0) clearTimeout(this.readyTimer);
808
+ this.update({
809
+ ...this.snapshot,
810
+ phase: "settling",
811
+ message: "Updating the app. Waiting briefly for pending changes…"
812
+ });
813
+ this.attempt = this.run();
814
+ return this.attempt;
815
+ }
816
+ async run() {
817
+ const deadline = Date.now() + 2e3;
818
+ while (!this.disposed && this.callbacks.unresolved() > 0 && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 25));
819
+ if (this.disposed) return;
820
+ try {
821
+ const issues = this.callbacks.persist();
822
+ for (const issue of issues) this.reportRecovery(issue);
823
+ if (issues.length > 0) {
824
+ this.tab = {
825
+ ...this.tab,
826
+ uncertainStorage: true
827
+ };
828
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
829
+ }
830
+ const fresh = await this.callbacks.freshSelection();
831
+ if (fresh !== void 0 && fresh.appId !== this.selection.appId) throw new Error("The app address now refers to a different app.");
832
+ this.target = fresh ?? this.target;
833
+ const response = await this.browser.fetch(this.browser.location.href, {
834
+ cache: "no-store",
835
+ credentials: "same-origin",
836
+ headers: { accept: "text/html" },
837
+ signal: AbortSignal.timeout(1e4)
838
+ });
839
+ if (!response.ok) throw new Error("The updated app document is temporarily unavailable.");
840
+ await response.body?.cancel();
841
+ if (this.disposed) return;
842
+ const attempts = fresh !== void 0 && this.tab.target !== null && !sameExecutionSelection(this.tab.target, fresh) ? 0 : this.tab.attempts;
843
+ if (attempts >= 2) throw new Error("The app update could not load a consistent release. Try again when the connection or release is available.");
844
+ this.tab = {
845
+ ...this.tab,
846
+ attempts: attempts + 1,
847
+ attemptedAt: Date.now(),
848
+ target: fresh ?? this.target ?? this.selection
849
+ };
850
+ if (!this.save()) throw new Error("Browser storage is unavailable. Pending changes may have unknown outcomes. Use Reload app to continue the update.");
851
+ this.update({
852
+ ...this.snapshot,
853
+ phase: "reloading",
854
+ message: "Loading the updated app…"
855
+ });
856
+ this.callbacks.reload();
857
+ } catch (error) {
858
+ this.update({
859
+ ...this.snapshot,
860
+ phase: "error",
861
+ message: error instanceof Error ? error.message : "The app update could not be loaded. Try again."
862
+ });
863
+ }
864
+ }
865
+ /** An explicit retry resets the automatic budget, while obsolete code remains fenced. */
866
+ retry = () => {
867
+ if (this.disposed) return;
868
+ if (!this.storageAvailable) {
869
+ this.callbacks.persist();
870
+ this.callbacks.reload();
871
+ return;
872
+ }
873
+ this.tab = {
874
+ ...this.tab,
875
+ attempts: 0,
876
+ attemptedAt: 0
877
+ };
878
+ this.save();
879
+ this.attempt = void 0;
880
+ this.require(this.target);
881
+ };
882
+ async check() {
883
+ if (this.obsolete || this.disposed) return false;
884
+ try {
885
+ const fresh = await this.callbacks.freshSelection();
886
+ if (fresh === void 0 || !sameExecutionSelection(this.selection, fresh)) {
887
+ this.require(fresh);
888
+ return false;
889
+ }
890
+ return !this.obsolete && !this.disposed;
891
+ } catch {
892
+ this.unavailable();
893
+ return false;
894
+ }
895
+ }
896
+ dispose() {
897
+ this.disposed = true;
898
+ if (this.readyTimer !== void 0) clearTimeout(this.readyTimer);
899
+ this.listeners.clear();
900
+ }
901
+ };
902
+ //#endregion
903
+ //#region src/client/recovery-journal.ts
904
+ const RecoveryJournalLimits = {
905
+ maxEntries: 100,
906
+ retentionMs: 864e5,
907
+ maxBytes: 65536
908
+ };
909
+ const RecoveryEntry = Schema.Struct({
910
+ identity: RecoveryIdentity,
911
+ recordedAt: Schema.Natural,
912
+ expiresAt: Schema.Natural,
913
+ state: Schema.Literals([
914
+ "pending",
915
+ "settled",
916
+ "interrupted",
917
+ "uncertain"
918
+ ])
919
+ });
920
+ const RecoveryJournalV1 = Schema.Struct({
921
+ formatVersion: Schema.Literal(1),
922
+ appId: AppId,
923
+ sessionEpoch: Schema.String,
924
+ clientSessionId: RecoverySessionId,
925
+ entries: Schema.Array(RecoveryEntry).check(Schema.isMaxLength(RecoveryJournalLimits.maxEntries))
926
+ }).check(Schema.makeFilter((record) => {
927
+ const keys = record.entries.map((entry) => identityKey(entry.identity));
928
+ return new Set(keys).size === keys.length && record.entries.every((entry) => entry.identity.originalSelection.appId === record.appId && entry.expiresAt > entry.recordedAt && entry.expiresAt - entry.recordedAt <= RecoveryJournalLimits.retentionMs);
929
+ })).annotate({ parseOptions: { onExcessProperty: "error" } });
930
+ const RecoveryJournalRecord = initial(RecoveryJournalV1);
931
+ const identityKey = (identity) => `${identity.originalSelection.dataHistoryId}/${identity.invocationId}`;
932
+ const bytes = (value) => new TextEncoder().encode(value).byteLength;
933
+ /** sessionStorage isolates tabs. A cloned tab may look up copied identities, never replay them. */
934
+ var RecoveryJournal = class {
935
+ options;
936
+ storageKey;
937
+ clientSessionId;
938
+ constructor(options) {
939
+ this.options = options;
940
+ this.storageKey = `ignotum:recovery:${options.appId}`;
941
+ this.clientSessionId = options.clientSessionId;
942
+ }
943
+ storage() {
944
+ const storage = this.options.storage;
945
+ return storage === void 0 ? globalThis.sessionStorage : Predicate.isFunction(storage) ? storage() : storage;
946
+ }
947
+ now() {
948
+ return this.options.now?.() ?? DateTime.toEpochMillis(DateTime.nowUnsafe());
949
+ }
950
+ snapshot(entries, issues) {
951
+ return {
952
+ clientSessionId: this.clientSessionId,
953
+ entries,
954
+ issues
955
+ };
956
+ }
957
+ read() {
958
+ let raw;
959
+ try {
960
+ raw = this.storage().getItem(this.storageKey);
961
+ } catch {
962
+ return this.snapshot([], ["denied"]);
963
+ }
964
+ if (raw === null) return this.snapshot([], []);
965
+ if (bytes(raw) > RecoveryJournalLimits.maxBytes) return this.discard("full");
966
+ const parsed = Schema.decodeUnknownOption(Schema.fromJsonString(RecoveryJournalRecord))(raw);
967
+ if (parsed._tag === "None" || parsed.value.appId !== this.options.appId) return this.discard("corrupt");
968
+ if (parsed.value.sessionEpoch !== this.options.sessionEpoch) return this.discard("session-changed");
969
+ this.clientSessionId = parsed.value.clientSessionId;
970
+ const now = this.now();
971
+ const entries = parsed.value.entries.filter((entry) => entry.expiresAt > now && entry.recordedAt <= now);
972
+ const issues = entries.length === parsed.value.entries.length ? [] : ["expired"];
973
+ return issues.length === 0 ? this.snapshot(entries, issues) : this.persist(entries, issues);
974
+ }
975
+ discard(issue) {
976
+ try {
977
+ this.storage().removeItem(this.storageKey);
978
+ return this.snapshot([], [issue]);
979
+ } catch {
980
+ return this.snapshot([], [issue, "denied"]);
981
+ }
982
+ }
983
+ persist(entries, issues) {
984
+ const record = {
985
+ formatVersion: 1,
986
+ appId: this.options.appId,
987
+ sessionEpoch: this.options.sessionEpoch,
988
+ clientSessionId: this.clientSessionId,
989
+ entries
990
+ };
991
+ const encoded = Schema.encodeOption(Schema.fromJsonString(RecoveryJournalRecord))(record);
992
+ if (encoded._tag === "None") return this.snapshot(entries, [...issues, "corrupt"]);
993
+ if (bytes(encoded.value) > RecoveryJournalLimits.maxBytes) return this.snapshot(entries, [...issues, "full"]);
994
+ try {
995
+ this.storage().setItem(this.storageKey, encoded.value);
996
+ return this.snapshot(entries, issues);
997
+ } catch (error) {
998
+ return this.snapshot(entries, [...issues, error instanceof DOMException && error.name === "QuotaExceededError" ? "full" : "denied"]);
999
+ }
1000
+ }
1001
+ /** Call before abandoning the page; any issue means durable recovery is not proven. */
1002
+ write(identities) {
1003
+ const current = this.read();
1004
+ if (current.issues.includes("denied")) return current;
1005
+ const entries = new Map(current.entries.map((entry) => [identityKey(entry.identity), entry]));
1006
+ const now = this.now();
1007
+ for (const identity of identities) {
1008
+ if (identity.originalSelection.appId !== this.options.appId) return this.snapshot(current.entries, [...current.issues, "corrupt"]);
1009
+ const key = identityKey(identity);
1010
+ if (!entries.has(key)) entries.set(key, {
1011
+ identity: {
1012
+ invocationId: identity.invocationId,
1013
+ originalSelection: identity.originalSelection
1014
+ },
1015
+ recordedAt: now,
1016
+ expiresAt: now + RecoveryJournalLimits.retentionMs,
1017
+ state: "pending"
1018
+ });
1019
+ }
1020
+ if (entries.size > RecoveryJournalLimits.maxEntries) return this.snapshot(current.entries, [...current.issues, "full"]);
1021
+ return this.persist([...entries.values()], current.issues);
1022
+ }
1023
+ reconcile(identity, state) {
1024
+ const current = this.read();
1025
+ if (current.issues.includes("denied")) return current;
1026
+ return this.persist(current.entries.map((entry) => identityKey(entry.identity) === identityKey(identity) ? {
1027
+ ...entry,
1028
+ state
1029
+ } : entry), current.issues);
1030
+ }
1031
+ remove(identity) {
1032
+ const current = this.read();
1033
+ if (current.issues.includes("denied")) return current;
1034
+ return this.persist(current.entries.filter((entry) => identityKey(entry.identity) !== identityKey(identity)), current.issues);
1035
+ }
1036
+ clear() {
1037
+ try {
1038
+ this.storage().removeItem(this.storageKey);
1039
+ return this.snapshot([], []);
1040
+ } catch {
1041
+ return this.snapshot([], ["denied"]);
1042
+ }
1043
+ }
1044
+ };
1045
+ //#endregion
1046
+ //#region src/client/managed-client.ts
1047
+ const issueMessage = (issue) => {
1048
+ switch (issue) {
1049
+ case "denied": return "Browser storage is unavailable. Some previous mutation outcomes may be unknown.";
1050
+ case "full": return "Browser recovery storage is full. Some mutation outcomes could not be saved.";
1051
+ case "corrupt": return "Saved mutation information could not be read. Its outcomes remain unknown.";
1052
+ case "expired": return "Some saved mutation information expired. Its outcomes remain unknown.";
1053
+ case "session-changed": return "The browser session changed. Previous mutation outcomes cannot be retrieved in this session.";
1054
+ }
1055
+ };
1056
+ /** The managed connection is mounted by bootstrap, independently of query observers. */
1057
+ var ManagedBrowserClient = class {
1058
+ selection;
1059
+ browser;
1060
+ helpers;
1061
+ upgrade;
1062
+ queries;
1063
+ socket;
1064
+ acceptedSocket;
1065
+ session;
1066
+ ids;
1067
+ sessionEpoch;
1068
+ sessionViewRevision;
1069
+ unsubscribeSession;
1070
+ sessionDeadline = 0;
1071
+ clientSessionId;
1072
+ journal;
1073
+ pending = /* @__PURE__ */ new Map();
1074
+ recovery = /* @__PURE__ */ new Map();
1075
+ recoveryDeadlines = /* @__PURE__ */ new Map();
1076
+ recoveryTimeouts = /* @__PURE__ */ new Map();
1077
+ recoveryTimers = /* @__PURE__ */ new Set();
1078
+ reconnectTimer;
1079
+ handshakeTimer;
1080
+ sessionTimer;
1081
+ heartbeat;
1082
+ reconnectDelay = 250;
1083
+ disposed = false;
1084
+ readyWaiters = /* @__PURE__ */ new Set();
1085
+ suspended = true;
1086
+ lastActivity = Date.now();
1087
+ validation;
1088
+ connectionGeneration = 0;
1089
+ constructor(selection, browser, helpers, callbacks) {
1090
+ this.selection = selection;
1091
+ this.browser = browser;
1092
+ this.helpers = helpers;
1093
+ this.upgrade = new ManagedUpgrade(selection, browser, {
1094
+ stopAdmission: () => {
1095
+ this.suspended = true;
1096
+ for (const pending of this.pending.values()) if (pending.preparation !== void 0) Deferred.doneUnsafe(pending.prepared, Effect.fail(PlatformUpgrading.make({ message: "The app is updating. This upload was interrupted before its mutation ran." })));
1097
+ callbacks.stopAdmission();
1098
+ },
1099
+ unresolved: () => this.pending.size,
1100
+ persist: () => this.persistPending(),
1101
+ reload: callbacks.reload,
1102
+ freshSelection: this.freshSelection
1103
+ });
1104
+ this.clientSessionId = this.upgrade.clientSessionId;
1105
+ this.queries = helpers.makeQueryCache(Effect.suspend(() => this.ids === void 0 ? Effect.die("The client identity service is not ready.") : this.ids.generate(SubscriptionId)), (message) => Effect.sync(() => {
1106
+ if (this.canSend()) this.send(message);
1107
+ }));
1108
+ }
1109
+ attachSession(session, ids) {
1110
+ if (this.disposed) return;
1111
+ this.unsubscribeSession?.();
1112
+ this.session = session;
1113
+ this.ids = ids;
1114
+ this.unsubscribeSession = session.subscribe(() => {
1115
+ const current = session.current();
1116
+ if (current === void 0 || this.sessionEpoch !== void 0 && (current.sessionEpoch !== this.sessionEpoch || current.viewRevision !== this.sessionViewRevision)) {
1117
+ this.queries.invalidatePrivate();
1118
+ this.suspended = true;
1119
+ this.acceptedSocket = void 0;
1120
+ this.socket?.close();
1121
+ }
1122
+ });
1123
+ }
1124
+ whenReady() {
1125
+ if (this.upgrade.ready) return Promise.resolve(true);
1126
+ if (this.upgrade.obsolete || this.disposed) return Promise.resolve(false);
1127
+ return new Promise((resolve) => {
1128
+ const finish = (ready) => {
1129
+ unsubscribe();
1130
+ this.readyWaiters.delete(finish);
1131
+ resolve(ready);
1132
+ };
1133
+ const unsubscribe = this.upgrade.subscribe(() => {
1134
+ if (this.upgrade.ready || this.upgrade.obsolete || this.disposed) finish(this.upgrade.ready);
1135
+ });
1136
+ this.readyWaiters.add(finish);
1137
+ });
1138
+ }
1139
+ retry = () => {
1140
+ if (this.upgrade.obsolete) this.upgrade.retry();
1141
+ else this.resume();
1142
+ };
1143
+ recoverLoad = async () => {
1144
+ await this.upgrade.check();
1145
+ };
1146
+ freshSelection = async () => {
1147
+ const response = await this.browser.fetch(managedSelectionPath, {
1148
+ cache: "no-store",
1149
+ credentials: "same-origin",
1150
+ signal: AbortSignal.timeout(1e4)
1151
+ });
1152
+ if (!response.ok) throw new Error("The current app selection is unavailable.");
1153
+ const current = Schema.decodeUnknownOption(ManagedSelectionResponse)(await response.json());
1154
+ return Option.getOrUndefined(current)?.selection;
1155
+ };
1156
+ start() {
1157
+ if (this.disposed) return;
1158
+ this.browser.addEventListener("pageshow", this.resume);
1159
+ this.browser.addEventListener("pagehide", this.hide);
1160
+ this.browser.addEventListener("online", this.resume);
1161
+ this.browser.addEventListener("offline", this.offline);
1162
+ this.browser.addEventListener("focus", this.resume);
1163
+ this.browser.document.addEventListener("visibilitychange", this.visibility);
1164
+ this.heartbeat = setInterval(() => {
1165
+ const now = Date.now();
1166
+ if (now - this.lastActivity > 15e3) this.resume();
1167
+ this.lastActivity = now;
1168
+ }, 5e3);
1169
+ this.connect();
1170
+ }
1171
+ canSend() {
1172
+ return !this.disposed && !this.suspended && !this.upgrade.obsolete && this.acceptedSocket !== void 0 && this.acceptedSocket === this.socket && this.socket.readyState === 1 && Date.now() < this.sessionDeadline;
1173
+ }
1174
+ send(message) {
1175
+ if (this.socket?.readyState !== 1) return;
1176
+ this.socket.send(Schema.encodeSync(Schema.fromJsonString(ManagedClientMessage))(message));
1177
+ }
1178
+ connect() {
1179
+ if (this.disposed || this.upgrade.obsolete) return;
1180
+ this.suspended = true;
1181
+ this.upgrade.connecting();
1182
+ this.acceptedSocket = void 0;
1183
+ this.recovery.clear();
1184
+ for (const timer of this.recoveryTimers) clearTimeout(timer);
1185
+ this.recoveryTimers.clear();
1186
+ this.recoveryTimeouts.clear();
1187
+ if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
1188
+ if (this.handshakeTimer !== void 0) clearTimeout(this.handshakeTimer);
1189
+ this.socket?.close();
1190
+ this.socket = void 0;
1191
+ const generation = ++this.connectionGeneration;
1192
+ if (this.session === void 0) return;
1193
+ Effect.runPromise(this.session.bootstrap).then(() => {
1194
+ if (this.disposed || this.upgrade.obsolete || generation !== this.connectionGeneration) return;
1195
+ this.openSocket();
1196
+ }, () => {
1197
+ if (this.disposed || this.upgrade.obsolete || generation !== this.connectionGeneration) return;
1198
+ this.upgrade.unavailable("The browser session could not be verified. Reconnecting…");
1199
+ this.reconnectTimer = setTimeout(() => this.connect(), 1e3);
1200
+ });
1201
+ }
1202
+ openSocket() {
1203
+ const url = new URL(managedSyncPath, this.browser.location.href);
1204
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1205
+ const socket = new WebSocket(url);
1206
+ this.socket = socket;
1207
+ socket.onopen = () => {
1208
+ if (this.socket !== socket || this.disposed) return;
1209
+ this.send({
1210
+ type: "Connect",
1211
+ protocolVersion: 1,
1212
+ selection: this.selection,
1213
+ clientSessionId: this.clientSessionId
1214
+ });
1215
+ };
1216
+ this.handshakeTimer = setTimeout(() => {
1217
+ if (this.socket === socket && this.acceptedSocket !== socket) socket.close();
1218
+ }, 1e4);
1219
+ socket.onmessage = (event) => {
1220
+ if (this.socket !== socket || this.disposed) return;
1221
+ const stable = Schema.decodeUnknownOption(Schema.fromJsonString(ManagedUpgradeRequired))(event.data);
1222
+ if (Option.isSome(stable)) {
1223
+ this.upgrade.require(stable.value.selection);
1224
+ return;
1225
+ }
1226
+ const decoded = Schema.decodeUnknownOption(Schema.fromJsonString(ManagedServerMessage))(event.data);
1227
+ if (Option.isNone(decoded)) {
1228
+ this.upgrade.require();
1229
+ return;
1230
+ }
1231
+ this.message(socket, decoded.value);
1232
+ };
1233
+ socket.onerror = () => socket.close();
1234
+ socket.onclose = (event) => {
1235
+ if (this.socket !== socket || this.disposed) return;
1236
+ if (event.code === 1012 || event.code === 1008) this.queries.invalidatePrivate();
1237
+ this.acceptedSocket = void 0;
1238
+ this.suspended = true;
1239
+ if (this.handshakeTimer !== void 0) clearTimeout(this.handshakeTimer);
1240
+ if (this.upgrade.obsolete) return;
1241
+ this.upgrade.unavailable("The connection was lost. Checking for updates before resuming…");
1242
+ this.reconnectTimer = setTimeout(() => this.connect(), this.reconnectDelay);
1243
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 5e3);
1244
+ };
1245
+ }
1246
+ message(socket, message) {
1247
+ if (message.type === "UpgradeRequired") {
1248
+ this.upgrade.require(message.selection);
1249
+ return;
1250
+ }
1251
+ if (message.type === "Handshake") {
1252
+ if (!sameExecutionSelection(this.selection, message.selection)) {
1253
+ this.upgrade.require(message.selection);
1254
+ return;
1255
+ }
1256
+ if (this.upgrade.obsolete) return;
1257
+ if (this.handshakeTimer !== void 0) clearTimeout(this.handshakeTimer);
1258
+ const previousEpoch = this.sessionEpoch;
1259
+ if (previousEpoch !== void 0 && (previousEpoch !== message.session.sessionEpoch || this.sessionViewRevision !== message.session.viewRevision)) this.queries.invalidatePrivate();
1260
+ this.sessionEpoch = message.session.sessionEpoch;
1261
+ this.sessionViewRevision = message.session.viewRevision;
1262
+ this.sessionDeadline = Date.now() + Math.max(0, message.session.validUntil - message.serverTime);
1263
+ if (this.sessionTimer !== void 0) clearTimeout(this.sessionTimer);
1264
+ this.sessionTimer = setTimeout(() => {
1265
+ this.session?.suspend();
1266
+ this.queries.invalidatePrivate();
1267
+ this.resume();
1268
+ }, Math.min(2147483647, Math.max(0, this.sessionDeadline - Date.now())));
1269
+ this.session?.accept(message.session, message.serverTime);
1270
+ this.journal = new RecoveryJournal({
1271
+ appId: this.selection.appId,
1272
+ sessionEpoch: message.session.sessionEpoch,
1273
+ clientSessionId: this.clientSessionId,
1274
+ storage: () => this.browser.sessionStorage
1275
+ });
1276
+ const saved = this.journal.read();
1277
+ for (const issue of saved.issues) this.upgrade.reportRecovery(issueMessage(issue));
1278
+ if (saved.clientSessionId !== this.clientSessionId) {
1279
+ this.clientSessionId = saved.clientSessionId;
1280
+ this.connect();
1281
+ return;
1282
+ }
1283
+ if (previousEpoch !== void 0 && previousEpoch !== this.sessionEpoch) {
1284
+ for (const [id, pending] of this.pending) {
1285
+ Deferred.doneUnsafe(pending.prepared, Effect.fail(ConnectionUnavailable.make({ message: "The session changed before file preparation completed." })));
1286
+ this.upgrade.reportRecovery(`Mutation ${id} has an unknown outcome because the session changed.`);
1287
+ Deferred.doneUnsafe(pending.done, Effect.fail(ConnectionUnavailable.make({ message: "The session changed. This mutation may already have completed." })));
1288
+ }
1289
+ this.pending.clear();
1290
+ }
1291
+ this.acceptedSocket = socket;
1292
+ this.suspended = false;
1293
+ this.reconnectDelay = 250;
1294
+ this.upgrade.accepted();
1295
+ for (const entry of this.queries.entries()) this.send({
1296
+ type: "Subscribe",
1297
+ id: entry.subscriptionId,
1298
+ function: entry.function,
1299
+ args: entry.args
1300
+ });
1301
+ for (const entry of saved.entries) if (entry.state === "pending") {
1302
+ if (!this.pending.has(entry.identity.invocationId)) this.recover(entry.identity);
1303
+ } else {
1304
+ this.upgrade.reportRecovery(`Mutation ${entry.identity.invocationId}: ${entry.state}.`);
1305
+ this.journal.remove(entry.identity);
1306
+ }
1307
+ for (const pending of this.pending.values()) if (pending.preparation !== void 0) this.send(pending.preparation);
1308
+ else this.recover(pending.identity);
1309
+ this.journal.write([]);
1310
+ return;
1311
+ }
1312
+ if (this.acceptedSocket !== socket) {
1313
+ socket.close();
1314
+ return;
1315
+ }
1316
+ switch (message.type) {
1317
+ case "Preparation": {
1318
+ if (!this.canSend()) return;
1319
+ const pending = this.pending.get(message.invocationId);
1320
+ if (pending?.preparation !== void 0) Deferred.doneUnsafe(pending.prepared, Effect.succeed(message.uploads));
1321
+ return;
1322
+ }
1323
+ case "QuerySnapshot": {
1324
+ if (!this.canSend()) return;
1325
+ if (!sameExecutionSelection(this.selection, message.selection)) {
1326
+ this.upgrade.require(message.selection);
1327
+ return;
1328
+ }
1329
+ const result = Effect.runSync(this.helpers.resultFromWire(message.result.result, message.files).pipe(Effect.catchTags({ InvalidServerMessage: (error) => Effect.succeed(error) })));
1330
+ this.queries.setResult(message.id, result, message.result.observedRevision);
1331
+ return;
1332
+ }
1333
+ case "MutationOutcome":
1334
+ if (!sameExecutionSelection(message.identity.originalSelection, this.selection) && !this.recovery.has(message.id)) return;
1335
+ this.outcome(message.id, message.identity, message.outcome);
1336
+ return;
1337
+ case "Error":
1338
+ if (message.code === "PlatformUpgrading") {
1339
+ this.upgrade.require();
1340
+ return;
1341
+ }
1342
+ if (message.code === "SessionExpired") {
1343
+ this.suspended = true;
1344
+ this.session?.suspend();
1345
+ this.queries.invalidatePrivate();
1346
+ socket.close();
1347
+ return;
1348
+ }
1349
+ if (message.code === "InvalidArguments" || message.code === "InvocationIdConflict" || message.code === "ResourceLimitExceeded") {
1350
+ for (const [invocationId, pending] of this.pending) {
1351
+ if (invocationId !== message.id) continue;
1352
+ const failure = Effect.fail(ServerError.make({
1353
+ code: message.code,
1354
+ message: message.message
1355
+ }));
1356
+ Deferred.doneUnsafe(pending.prepared, failure);
1357
+ Deferred.doneUnsafe(pending.done, failure);
1358
+ this.pending.delete(invocationId);
1359
+ this.journal?.remove(pending.identity);
1360
+ return;
1361
+ }
1362
+ for (const query of this.queries.entries()) {
1363
+ if (query.subscriptionId !== message.id) continue;
1364
+ this.queries.setResult(query.subscriptionId, ServerError.make({
1365
+ code: message.code,
1366
+ message: message.message
1367
+ }));
1368
+ return;
1369
+ }
1370
+ return;
1371
+ }
1372
+ this.upgrade.unavailable(message.message);
1373
+ socket.close();
1374
+ return;
1375
+ }
1376
+ }
1377
+ recover(identity) {
1378
+ if (!this.canSend()) return;
1379
+ const id = `recovery:${identity.originalSelection.dataHistoryId}:${identity.invocationId}`;
1380
+ if (this.recovery.has(id)) return;
1381
+ this.recovery.set(id, identity);
1382
+ const deadline = this.recoveryDeadlines.get(id) ?? Date.now() + 3e4;
1383
+ this.recoveryDeadlines.set(id, deadline);
1384
+ if (Date.now() >= deadline) {
1385
+ this.outcome(id, identity, { type: "Uncertain" });
1386
+ return;
1387
+ }
1388
+ this.send({
1389
+ type: "RecoverMutation",
1390
+ id,
1391
+ identity
1392
+ });
1393
+ const timer = setTimeout(() => {
1394
+ this.recoveryTimers.delete(timer);
1395
+ this.recoveryTimeouts.delete(id);
1396
+ if (!this.recovery.has(id)) return;
1397
+ this.recovery.delete(id);
1398
+ this.recover(identity);
1399
+ }, 5e3);
1400
+ this.recoveryTimers.add(timer);
1401
+ this.recoveryTimeouts.set(id, timer);
1402
+ }
1403
+ outcome(id, identity, outcome) {
1404
+ const recovery = this.recovery.get(id);
1405
+ const pending = this.pending.get(identity.invocationId);
1406
+ if (recovery === void 0 && pending === void 0) return;
1407
+ const expected = recovery ?? pending.identity;
1408
+ if (expected.invocationId !== identity.invocationId || !sameExecutionSelection(expected.originalSelection, identity.originalSelection)) return;
1409
+ this.recovery.delete(id);
1410
+ const timeout = this.recoveryTimeouts.get(id);
1411
+ if (timeout !== void 0) {
1412
+ clearTimeout(timeout);
1413
+ this.recoveryTimers.delete(timeout);
1414
+ this.recoveryTimeouts.delete(id);
1415
+ }
1416
+ if (outcome.type === "Pending") {
1417
+ const timer = setTimeout(() => {
1418
+ this.recoveryTimers.delete(timer);
1419
+ this.recover(identity);
1420
+ }, 1e3);
1421
+ this.recoveryTimers.add(timer);
1422
+ return;
1423
+ }
1424
+ const recoveryId = `recovery:${identity.originalSelection.dataHistoryId}:${identity.invocationId}`;
1425
+ this.recovery.delete(recoveryId);
1426
+ this.recoveryDeadlines.delete(recoveryId);
1427
+ if (pending !== void 0) {
1428
+ this.pending.delete(identity.invocationId);
1429
+ if (outcome.type === "Settled") Deferred.doneUnsafe(pending.done, this.helpers.resultFromWire(outcome.result.result));
1430
+ else Deferred.doneUnsafe(pending.done, Effect.fail(ConnectionUnavailable.make({ message: outcome.type === "Interrupted" ? "The mutation was interrupted before it committed." : "The mutation outcome is unknown. Check the app before trying it again." })));
1431
+ }
1432
+ const state = outcome.type === "Settled" ? "settled" : outcome.type === "Interrupted" ? "interrupted" : "uncertain";
1433
+ if (recovery !== void 0 || outcome.type !== "Settled") {
1434
+ const detail = outcome.type === "Settled" ? outcome.result.result.type === "Success" ? "completed" : "completed with an error" : state;
1435
+ this.upgrade.reportRecovery(`Mutation ${identity.invocationId}: ${detail}.`);
1436
+ }
1437
+ for (const issue of this.journal?.reconcile(identity, state).issues ?? []) this.upgrade.reportRecovery(issueMessage(issue));
1438
+ for (const issue of this.journal?.remove(identity).issues ?? []) this.upgrade.reportRecovery(issueMessage(issue));
1439
+ }
1440
+ persistPending() {
1441
+ if (this.pending.size === 0) return [];
1442
+ if (this.journal === void 0) return ["Pending mutation outcomes could not be saved and remain unknown."];
1443
+ return this.journal.write([...this.pending.values()].map((entry) => entry.identity)).issues.map(issueMessage);
1444
+ }
1445
+ observe = Effect.fn("ManagedBrowserClient.observe")((address, args) => this.queries.observe(address, args));
1446
+ mutate = Effect.fn("ManagedBrowserClient.mutate")(function* (address, args) {
1447
+ if (this.upgrade.obsolete) return yield* PlatformUpgrading.make({ message: "The platform is updating. Try again after the app reloads." });
1448
+ const valid = yield* Effect.promise(() => this.validate());
1449
+ if (this.upgrade.obsolete) return yield* PlatformUpgrading.make({ message: "The platform is updating. Try again after the app reloads." });
1450
+ if (!valid || !this.canSend() || this.sessionEpoch === void 0) return yield* ConnectionUnavailable.make({ message: "The app must reconnect and verify its selection before accepting a mutation." });
1451
+ if (this.pending.size >= RecoveryJournalLimits.maxEntries) return yield* ConnectionUnavailable.make({ message: "Too many mutations are waiting for outcomes. Wait for them to settle before trying again." });
1452
+ const ids = this.ids;
1453
+ if (ids === void 0) return yield* ConnectionUnavailable.make({ message: "The client is not ready." });
1454
+ const prepared = yield* prepareMutationArguments(args).pipe(Effect.provideService(IdGenerator, ids), Effect.mapError((cause) => InvalidMutationArguments.make({
1455
+ cause,
1456
+ function: address,
1457
+ message: "The mutation arguments cannot be sent."
1458
+ })));
1459
+ const encoded = yield* Effect.try({
1460
+ try: () => encodeTransportObject(prepared.value),
1461
+ catch: (cause) => InvalidMutationArguments.make({
1462
+ cause,
1463
+ function: address,
1464
+ message: "The mutation arguments cannot be sent."
1465
+ })
1466
+ });
1467
+ if (!this.canSend()) return yield* PlatformUpgrading.make({ message: "The app is reconnecting or updating. Try again when it is ready." });
1468
+ const invocationId = yield* ids.generate(InvocationId);
1469
+ const done = yield* Deferred.make();
1470
+ const identity = {
1471
+ invocationId,
1472
+ originalSelection: this.selection
1473
+ };
1474
+ const uploadsReady = yield* Deferred.make();
1475
+ const preparation = prepared.fileIds.length === 0 ? void 0 : {
1476
+ type: "Prepare",
1477
+ invocationId,
1478
+ function: address,
1479
+ args: encoded,
1480
+ files: prepared.fileIds
1481
+ };
1482
+ this.pending.set(invocationId, {
1483
+ identity,
1484
+ sessionEpoch: this.sessionEpoch,
1485
+ done,
1486
+ prepared: uploadsReady,
1487
+ preparation
1488
+ });
1489
+ for (const issue of this.persistPending()) this.upgrade.reportRecovery(issue);
1490
+ if (preparation !== void 0) {
1491
+ this.send(preparation);
1492
+ yield* Effect.gen({ self: this }, function* () {
1493
+ const uploads = yield* Deferred.await(uploadsReady);
1494
+ yield* uploadPreparedFiles(prepared.nativeFiles, uploads, address);
1495
+ if (!this.canSend()) return yield* PlatformUpgrading.make({ message: "The app is reconnecting or updating. Try again when it is ready." });
1496
+ }).pipe(Effect.tapError(() => Effect.sync(() => {
1497
+ this.pending.delete(invocationId);
1498
+ this.journal?.remove(identity);
1499
+ })));
1500
+ const pending = this.pending.get(invocationId);
1501
+ if (pending === void 0) return yield* Deferred.await(done);
1502
+ this.pending.set(invocationId, {
1503
+ ...pending,
1504
+ preparation: void 0
1505
+ });
1506
+ }
1507
+ this.send({
1508
+ type: "Invoke",
1509
+ invocationId,
1510
+ function: address,
1511
+ args: encoded
1512
+ });
1513
+ const timer = setTimeout(() => {
1514
+ this.recoveryTimers.delete(timer);
1515
+ if (this.pending.has(invocationId)) this.recover(identity);
1516
+ }, 5e3);
1517
+ this.recoveryTimers.add(timer);
1518
+ return yield* Deferred.await(done);
1519
+ }).bind(this);
1520
+ validate = () => {
1521
+ if (this.upgrade.obsolete || this.disposed) return Promise.resolve(false);
1522
+ if (this.validation !== void 0) return this.validation;
1523
+ this.validation = this.upgrade.check().then((current) => {
1524
+ const ready = current && this.canSend();
1525
+ if (ready && this.upgrade.getSnapshot().phase !== "ready") this.upgrade.accepted();
1526
+ return ready;
1527
+ }).finally(() => {
1528
+ this.validation = void 0;
1529
+ });
1530
+ return this.validation;
1531
+ };
1532
+ resume = () => {
1533
+ if (this.disposed || this.upgrade.obsolete) return;
1534
+ this.suspended = true;
1535
+ this.acceptedSocket = void 0;
1536
+ this.recovery.clear();
1537
+ this.queries.invalidate(void 0);
1538
+ this.connect();
1539
+ };
1540
+ offline = () => {
1541
+ this.suspended = true;
1542
+ this.acceptedSocket = void 0;
1543
+ this.upgrade.unavailable("The app is offline. Reconnect to check for updates and mutation outcomes.");
1544
+ this.socket?.close();
1545
+ };
1546
+ hide = () => {
1547
+ for (const issue of this.persistPending()) this.upgrade.reportRecovery(issue);
1548
+ this.suspended = true;
1549
+ this.acceptedSocket = void 0;
1550
+ this.socket?.close();
1551
+ };
1552
+ visibility = () => {
1553
+ if (this.browser.document.visibilityState === "visible") this.resume();
1554
+ else this.hide();
1555
+ };
1556
+ dispose() {
1557
+ if (this.disposed) return;
1558
+ this.disposed = true;
1559
+ for (const finish of this.readyWaiters) finish(false);
1560
+ this.unsubscribeSession?.();
1561
+ this.persistPending();
1562
+ this.socket?.close();
1563
+ if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
1564
+ if (this.handshakeTimer !== void 0) clearTimeout(this.handshakeTimer);
1565
+ if (this.sessionTimer !== void 0) clearTimeout(this.sessionTimer);
1566
+ if (this.heartbeat !== void 0) clearInterval(this.heartbeat);
1567
+ for (const timer of this.recoveryTimers) clearTimeout(timer);
1568
+ this.browser.removeEventListener("pageshow", this.resume);
1569
+ this.browser.removeEventListener("pagehide", this.hide);
1570
+ this.browser.removeEventListener("online", this.resume);
1571
+ this.browser.removeEventListener("offline", this.offline);
1572
+ this.browser.removeEventListener("focus", this.resume);
1573
+ this.browser.document.removeEventListener("visibilitychange", this.visibility);
1574
+ this.upgrade.dispose();
1575
+ }
1576
+ };
1577
+ //#endregion
1578
+ //#region src/client/sync.ts
1579
+ const queryKey = (functionAddress, args) => encodeCanonicalJson([functionAddress, args]);
1580
+ const subscribeMessage = (entry) => ({
1581
+ type: "Subscribe",
1582
+ id: entry.subscriptionId,
1583
+ function: entry.function,
1584
+ args: entry.args
1585
+ });
1586
+ const makeQueryCache = (allocateId, sendLifecycleMessage) => {
1587
+ const queries = /* @__PURE__ */ new Map();
1588
+ const queriesById = /* @__PURE__ */ new Map();
1589
+ const observe = Effect.fn("SyncClient.QueryCache.observe")(function* (functionAddress, args) {
1590
+ const key = queryKey(functionAddress, args);
1591
+ let entry = queries.get(key);
1592
+ if (entry === void 0) {
1593
+ const created = {
1594
+ args,
1595
+ function: functionAddress,
1596
+ generation: 0,
1597
+ listeners: /* @__PURE__ */ new Set(),
1598
+ revision: void 0,
1599
+ references: 0,
1600
+ result: pending(),
1601
+ subscriptionId: yield* allocateId
1602
+ };
1603
+ queries.set(key, created);
1604
+ queriesById.set(created.subscriptionId, created);
1605
+ yield* sendLifecycleMessage(subscribeMessage(created));
1606
+ entry = created;
1607
+ }
1608
+ const observed = entry;
1609
+ observed.references += 1;
1610
+ observed.generation += 1;
1611
+ let released = false;
1612
+ return {
1613
+ getSnapshot: () => observed.result,
1614
+ subscribe: (listener) => {
1615
+ observed.listeners.add(listener);
1616
+ return () => observed.listeners.delete(listener);
1617
+ },
1618
+ release: Effect.gen(function* () {
1619
+ if (released) return;
1620
+ released = true;
1621
+ observed.references -= 1;
1622
+ if (observed.references !== 0) return;
1623
+ const generation = ++observed.generation;
1624
+ yield* Effect.sleep("25 millis");
1625
+ if (observed.references !== 0 || observed.generation !== generation || queries.get(key) !== observed) return;
1626
+ queries.delete(key);
1627
+ queriesById.delete(observed.subscriptionId);
1628
+ yield* sendLifecycleMessage({
1629
+ type: "Unsubscribe",
1630
+ id: observed.subscriptionId
1631
+ });
1632
+ })
1633
+ };
1634
+ });
1635
+ const clear = (entry) => {
1636
+ entry.result = pending();
1637
+ entry.revision = void 0;
1638
+ for (const listener of entry.listeners) listener();
1639
+ };
1640
+ return {
1641
+ invalidate: (ids) => {
1642
+ for (const entry of queries.values()) if (ids === void 0 || ids.includes(entry.subscriptionId)) clear(entry);
1643
+ },
1644
+ invalidatePrivate: () => {
1645
+ for (const entry of queries.values()) if (entry.dependsOnId !== false) clear(entry);
1646
+ },
1647
+ entries: () => queries.values(),
1648
+ getById: (id) => queriesById.get(id),
1649
+ observe,
1650
+ setResult: (id, result, revision) => {
1651
+ const entry = queriesById.get(id);
1652
+ if (entry === void 0) return;
1653
+ if (revision !== void 0) {
1654
+ if (entry.revision !== void 0 && revision < entry.revision) return;
1655
+ entry.revision = revision;
1656
+ }
1657
+ entry.result = result;
1658
+ for (const listener of entry.listeners) listener();
1659
+ }
1660
+ };
1661
+ };
1662
+ const syncClientInternals = {
1663
+ makeQueryCache,
1664
+ queryIdentity: queryKey,
1665
+ resultFromWire: Effect.fn("SyncClient.resultFromWire")((wire, files = []) => Effect.try({
1666
+ try: () => wire.type === "Success" ? Result$1.succeed(wire.value === void 0 ? void 0 : applyFileGrants(decodeTransportValue(wire.value, wire.dates ?? []), files)) : failureFromWire(wire.error, wire.dates),
1667
+ catch: (cause) => InvalidServerMessage.make({
1668
+ cause,
1669
+ message: "The server returned an invalid function result."
1670
+ })
1671
+ }))
1672
+ };
1673
+ var SyncClient = class extends Context.Service()("ignotum/client/sync/SyncClient") {};
1674
+ const makeUnmountedSyncRuntime = () => ManagedRuntime.make(Layer.succeed(SyncClient)({
1675
+ observe: () => Effect.fail(ConnectionUnavailable.make({ message: "The application has not mounted." })),
1676
+ mutate: () => Effect.fail(ConnectionUnavailable.make({ message: "The application has not mounted." }))
1677
+ }).pipe(Layer.provideMerge(BrowserSession.layer), Layer.provideMerge(clientErrorReporterLayer), Layer.provideMerge(IdGenerator.layer)));
1678
+ let syncRuntime = makeUnmountedSyncRuntime();
1679
+ const mountManagedSync = (selection, browser, callbacks) => {
1680
+ const client = new ManagedBrowserClient(selection, browser, syncClientInternals, callbacks);
1681
+ syncRuntime.dispose();
1682
+ syncRuntime = ManagedRuntime.make(Layer.effect(SyncClient, Effect.gen(function* () {
1683
+ client.attachSession(yield* BrowserSession, yield* IdGenerator);
1684
+ return SyncClient.of({
1685
+ observe: client.observe,
1686
+ mutate: client.mutate
1687
+ });
1688
+ })).pipe(Layer.provideMerge(BrowserSession.layer), Layer.provideMerge(clientErrorReporterLayer), Layer.provideMerge(IdGenerator.layer)));
1689
+ const runtime = syncRuntime;
1690
+ runtime.runPromise(SyncClient).then(() => client.start(), () => client.dispose());
1691
+ return {
1692
+ client,
1693
+ dispose: () => {
1694
+ client.dispose();
1695
+ runtime.dispose();
1696
+ if (syncRuntime === runtime) syncRuntime = makeUnmountedSyncRuntime();
1697
+ }
1698
+ };
1699
+ };
1700
+ const hot = import.meta.hot;
1701
+ if (hot !== void 0) hot.dispose(() => void syncRuntime.dispose());
1702
+ //#endregion
1703
+ //#region ../shared/dist/routing/pattern.js
1704
+ var RoutingDefinitionError = class extends Schema.TaggedError()("RoutingDefinitionError", {
1705
+ message: Schema.String,
1706
+ pattern: Schema.optional(Schema.String)
1707
+ }) {};
1708
+ const reservedIdentifiers = /* @__PURE__ */ new Set([
1709
+ "await",
1710
+ "break",
1711
+ "case",
1712
+ "catch",
1713
+ "class",
1714
+ "const",
1715
+ "continue",
1716
+ "debugger",
1717
+ "default",
1718
+ "delete",
1719
+ "do",
1720
+ "else",
1721
+ "enum",
1722
+ "export",
1723
+ "extends",
1724
+ "false",
1725
+ "finally",
1726
+ "for",
1727
+ "function",
1728
+ "if",
1729
+ "implements",
1730
+ "import",
1731
+ "in",
1732
+ "instanceof",
1733
+ "interface",
1734
+ "let",
1735
+ "new",
1736
+ "null",
1737
+ "package",
1738
+ "private",
1739
+ "protected",
1740
+ "public",
1741
+ "return",
1742
+ "static",
1743
+ "super",
1744
+ "switch",
1745
+ "this",
1746
+ "throw",
1747
+ "true",
1748
+ "try",
1749
+ "typeof",
1750
+ "var",
1751
+ "void",
1752
+ "while",
1753
+ "with",
1754
+ "yield"
1755
+ ]);
1756
+ const ParameterName = Schema.String.check(Schema.isPattern(/^[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*$/u), Schema.makeFilter((name) => !reservedIdentifiers.has(name) || "Parameter names must be TypeScript identifiers"));
1757
+ const CanonicalPattern = RouteDefinition.fields.pattern.check(Schema.makeFilter((pattern) => pattern === "/" || !pattern.endsWith("/") && !pattern.includes("//") || "Patterns must not contain empty segments or trailing slashes"), Schema.makeFilter((pattern) => !/[[\]()\u0000-\u001F\u007F\uD800-\uDFFF]/u.test(pattern) || "Patterns must use decoded literal segments and supported parameter syntax"));
1758
+ const invalid$1 = (message, pattern) => new RoutingDefinitionError({
1759
+ message,
1760
+ pattern
1761
+ });
1762
+ const weight = (segment) => segment.kind === "Literal" ? 3 : segment.kind === "Parameter" ? 2 : 1;
1763
+ const segmentText = (segment) => segment.kind === "Literal" ? segment.value : `${segment.kind === "Parameter" ? ":" : "*"}${segment.name}`;
1764
+ const makePattern = (segments) => ({
1765
+ pattern: `/${segments.map(segmentText).join("/")}`,
1766
+ segments,
1767
+ precedence: segments.map(weight)
1768
+ });
1769
+ const structuralKey = (pattern) => JSON.stringify(pattern.segments.map((segment) => segment.kind === "Literal" ? [segment.kind, segment.value] : [segment.kind]));
1770
+ const compare = (left, right) => {
1771
+ for (let index = 0; index < Math.max(left.precedence.length, right.precedence.length); index++) {
1772
+ const difference = (right.precedence[index] ?? 0) - (left.precedence[index] ?? 0);
1773
+ if (difference !== 0) return difference;
1774
+ }
1775
+ return 0;
1776
+ };
1777
+ const contains = (directory, pattern) => directory.segments.length <= pattern.segments.length && directory.segments.every((segment, index) => segmentText(segment) === segmentText(pattern.segments[index]));
1778
+ const parsePattern = Effect.fn("Routing.parsePattern")(function* (input) {
1779
+ const pattern = yield* Schema.decodeUnknownEffect(CanonicalPattern)(input).pipe(Effect.mapError(() => invalid$1("Route patterns must be canonical absolute paths outside /_ignotum.", input)));
1780
+ if (pattern === "/") return makePattern([]);
1781
+ const parts = pattern.slice(1).split("/");
1782
+ if (parts.length > 128) return yield* invalid$1("Route patterns exceed the segment limit.", pattern);
1783
+ const segments = [];
1784
+ const names = /* @__PURE__ */ new Set();
1785
+ for (const [index, part] of parts.entries()) if (part.startsWith(":") || part.startsWith("*")) {
1786
+ const name = yield* Schema.decodeUnknownEffect(ParameterName)(part.slice(1)).pipe(Effect.mapError(() => invalid$1("Route parameter names must be valid TypeScript identifiers.", pattern)));
1787
+ if (names.has(name)) return yield* invalid$1(`Repeated route parameter '${name}'.`, pattern);
1788
+ names.add(name);
1789
+ if (part.startsWith("*")) {
1790
+ if (index !== parts.length - 1) return yield* invalid$1("A catch-all must be the final segment.", pattern);
1791
+ segments.push({
1792
+ kind: "CatchAll",
1793
+ name
1794
+ });
1795
+ } else segments.push({
1796
+ kind: "Parameter",
1797
+ name
1798
+ });
1799
+ } else {
1800
+ if (part === "." || part === ".." || /[:*]/.test(part)) return yield* invalid$1("Route literals must not contain dot segments or parameter syntax.", pattern);
1801
+ segments.push({
1802
+ kind: "Literal",
1803
+ value: part
1804
+ });
1805
+ }
1806
+ return makePattern(segments);
1807
+ });
1808
+ const compileRoute = Effect.fn("Routing.compileRoute")(function* (input) {
1809
+ const route = yield* Schema.decodeUnknownEffect(RouteDefinition)(input).pipe(Effect.mapError(() => invalid$1("The route definition is invalid.")));
1810
+ const parsed = yield* parsePattern(route.pattern);
1811
+ const directory = yield* parsePattern(route.directoryScope ?? route.pattern);
1812
+ if (!contains(directory, parsed) || directory.segments.length < parsed.segments.length - 1) return yield* invalid$1("A route directory must be its full pattern for an index page or its immediate parent for a page file.", route.pattern);
1813
+ if (route.precedence.length !== parsed.precedence.length || route.precedence.some((value, index) => value !== parsed.precedence[index])) return yield* invalid$1("Stored route precedence does not match its pattern.", route.pattern);
1814
+ const parameters = parsed.segments.filter((segment) => segment.kind !== "Literal");
1815
+ if (route.params.length !== parameters.length || new Set(route.params.map((field) => field.name)).size !== route.params.length) return yield* invalid$1("Route parameters must describe each dynamic name exactly once.", route.pattern);
1816
+ for (const parameter of parameters) {
1817
+ const field = route.params.find((field) => field.name === parameter.name);
1818
+ if (field === void 0 || (parameter.kind === "CatchAll" ? field.value.type !== "array" : field.value.type === "array")) return yield* invalid$1("Single parameters require scalar values and catch-alls require string arrays.", route.pattern);
1819
+ }
1820
+ if (new Set(route.searchParams.map((field) => field.name)).size !== route.searchParams.length) return yield* invalid$1("Search parameter names must be unique.", route.pattern);
1821
+ return {
1822
+ pattern: parsed,
1823
+ directory
1824
+ };
1825
+ });
1826
+ const validateRoutePattern = Effect.fn("Routing.validateRoutePattern")(function* (input) {
1827
+ return (yield* compileRoute(input)).pattern;
1828
+ });
1829
+ const compilePatterns = Effect.fn("Routing.compilePatterns")(function* (routeInput, layoutInput) {
1830
+ const routes = yield* Schema.decodeUnknownEffect(Schema.Array(RouteDefinition).check(Schema.isMaxLength(appDefinitionLimits.routes)))(routeInput).pipe(Effect.mapError(() => invalid$1("Route definitions are invalid or exceed the route limit.")));
1831
+ const layouts = yield* Schema.decodeUnknownEffect(Schema.Array(LayoutDefinition).check(Schema.isMaxLength(appDefinitionLimits.layouts)))(layoutInput).pipe(Effect.mapError(() => invalid$1("Layout definitions are invalid or exceed the layout limit.")));
1832
+ if (new Set(routes.map((route) => route.id)).size !== routes.length) return yield* invalid$1("Route identities must be unique.");
1833
+ if (new Set(layouts.map((layout) => layout.id)).size !== layouts.length) return yield* invalid$1("Layout identities must be unique.");
1834
+ const parsedRoutes = yield* Effect.forEach(routes, (route) => compileRoute(route));
1835
+ const parsedLayouts = yield* Effect.forEach(layouts, (layout) => parsePattern(layout.scope));
1836
+ if (new Set(parsedRoutes.map((route) => structuralKey(route.pattern))).size !== routes.length) return yield* invalid$1("Duplicate or structurally ambiguous route patterns.");
1837
+ if (new Set(parsedLayouts.map(structuralKey)).size !== layouts.length) return yield* invalid$1("Duplicate or structurally ambiguous layout scopes.");
1838
+ const layoutPatterns = layouts.map((layout, index) => ({
1839
+ layout,
1840
+ pattern: parsedLayouts[index]
1841
+ }));
1842
+ const applicable = (pattern) => layoutPatterns.filter((item) => contains(item.pattern, pattern)).sort((a, b) => a.pattern.segments.length - b.pattern.segments.length).map((item) => item.layout);
1843
+ for (const { layout, pattern } of layoutPatterns) {
1844
+ const ancestors = applicable(pattern).filter((candidate) => candidate.id !== layout.id);
1845
+ if (layout.parent !== ancestors.at(-1)?.id) return yield* invalid$1(`Layout '${layout.id}' must name its nearest directory layout as parent.`, layout.scope);
1846
+ }
1847
+ const directories = /* @__PURE__ */ new Map();
1848
+ const addDirectory = Effect.fn("Routing.addDirectory")(function* (pattern) {
1849
+ const key = structuralKey(pattern);
1850
+ const existing = directories.get(key);
1851
+ if (existing !== void 0 && existing.pattern !== pattern.pattern) return yield* invalid$1("Dynamic directory parameter names are structurally ambiguous.", pattern.pattern);
1852
+ directories.set(key, pattern);
1853
+ });
1854
+ yield* addDirectory(makePattern([]));
1855
+ for (const pattern of parsedLayouts) for (let length = 1; length <= pattern.segments.length; length++) yield* addDirectory(makePattern(pattern.segments.slice(0, length)));
1856
+ const compiledRoutes = [];
1857
+ for (const [index, route] of routes.entries()) {
1858
+ const { pattern, directory } = parsedRoutes[index];
1859
+ const branch = applicable(directory);
1860
+ if (route.layouts.length !== branch.length || route.layouts.some((id, index) => id !== branch[index]?.id)) return yield* invalid$1(`Route '${route.id}' must include its complete directory layout branch.`, route.pattern);
1861
+ for (let length = 1; length <= directory.segments.length; length++) yield* addDirectory(makePattern(directory.segments.slice(0, length)));
1862
+ compiledRoutes.push({
1863
+ route,
1864
+ pattern,
1865
+ layouts: branch
1866
+ });
1867
+ }
1868
+ return {
1869
+ routes: compiledRoutes.sort((a, b) => compare(a.pattern, b.pattern)),
1870
+ directories: [...directories.values()].map((pattern) => ({
1871
+ pattern,
1872
+ layouts: applicable(pattern)
1873
+ })).sort((a, b) => compare(a.pattern, b.pattern))
1874
+ };
1875
+ });
1876
+ const captures = (pattern, segments, directory) => {
1877
+ const values = [];
1878
+ for (const [index, part] of pattern.segments.entries()) {
1879
+ const value = segments[index];
1880
+ if (value === void 0 || value.length === 0) return void 0;
1881
+ if (part.kind === "Literal") {
1882
+ if (part.value !== value) return void 0;
1883
+ } else if (part.kind === "Parameter") values.push([part.name, value]);
1884
+ else {
1885
+ values.push([part.name, segments.slice(index)]);
1886
+ return Object.fromEntries(values);
1887
+ }
1888
+ }
1889
+ if (!directory && segments.length !== pattern.segments.length) return void 0;
1890
+ return Object.fromEntries(values);
1891
+ };
1892
+ const matchDirectory = (compiled, decodedSegments) => {
1893
+ if (decodedSegments[0] === "_ignotum") return {
1894
+ route: void 0,
1895
+ layouts: [],
1896
+ params: {}
1897
+ };
1898
+ for (const directory of compiled.directories) {
1899
+ const params = captures(directory.pattern, decodedSegments, true);
1900
+ if (params !== void 0) return {
1901
+ route: void 0,
1902
+ layouts: directory.layouts,
1903
+ params
1904
+ };
1905
+ }
1906
+ return {
1907
+ route: void 0,
1908
+ layouts: [],
1909
+ params: {}
1910
+ };
1911
+ };
1912
+ const matchPattern = (compiled, decodedSegments) => {
1913
+ if (decodedSegments[0] === "_ignotum") return {
1914
+ route: void 0,
1915
+ layouts: [],
1916
+ params: {}
1917
+ };
1918
+ for (const entry of compiled.routes) {
1919
+ const params = captures(entry.pattern, decodedSegments, false);
1920
+ if (params !== void 0) return {
1921
+ route: entry.route,
1922
+ layouts: entry.layouts,
1923
+ params
1924
+ };
1925
+ }
1926
+ return matchDirectory(compiled, decodedSegments);
1927
+ };
1928
+ //#endregion
1929
+ //#region ../shared/dist/routing/url.js
1930
+ const UrlIssue = Schema.Struct({
1931
+ source: Schema.Literals(["params", "searchParams"]),
1932
+ field: Schema.optional(Schema.String),
1933
+ message: Schema.String
1934
+ });
1935
+ const ParsedLocation = Schema.Struct({
1936
+ segments: Schema.Array(Schema.String),
1937
+ pathname: Schema.String,
1938
+ search: Schema.String,
1939
+ hash: Schema.String,
1940
+ canonicalUrl: Schema.String
1941
+ });
1942
+ const PartialLocation = Schema.Struct({
1943
+ ...ParsedLocation.fields,
1944
+ pathComplete: Schema.Boolean
1945
+ });
1946
+ var InvalidUrl = class extends Schema.TaggedError()("InvalidUrl", {
1947
+ issues: Schema.Array(UrlIssue),
1948
+ partial: Schema.optional(PartialLocation)
1949
+ }) {};
1950
+ const invalid = (source, message, field, partial) => InvalidUrl.make({
1951
+ issues: [{
1952
+ source,
1953
+ field,
1954
+ message
1955
+ }],
1956
+ partial
1957
+ });
1958
+ const invalidField = (source, field) => invalid(source, `The URL field '${field}' does not match its declaration.`, field);
1959
+ const SafeSegment = Schema.String.check(Schema.isMinLength(1), Schema.isPattern(/^[^/\\\u0000-\u001f\u007f]+$/u), Schema.makeFilter((value) => value !== "." && value !== ".." || "Dot segments are not allowed"));
1960
+ const DecimalText = Schema.String.check(Schema.isPattern(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/));
1961
+ const InputRecord = Schema.Record(Schema.String, Schema.Unknown);
1962
+ const BuildInput = Schema.Struct({
1963
+ params: Schema.optional(Schema.Unknown),
1964
+ searchParams: Schema.optional(Schema.Unknown),
1965
+ hash: Schema.optional(Schema.String.check(Schema.makeFilter((value) => !value.startsWith("#") || "Hash input must omit the leading #")))
1966
+ }).annotate({ parseOptions: { onExcessProperty: "error" } });
1967
+ const decodeComponent = Effect.fn("Routing.decodeComponent")(function* (value, source, field, partial) {
1968
+ return yield* Effect.try({
1969
+ try: () => {
1970
+ return decodeURIComponent(value);
1971
+ },
1972
+ catch: () => invalid(source, "The URL contains malformed escapes or Unicode.", field, partial)
1973
+ });
1974
+ });
1975
+ const encodeComponent = Effect.fn("Routing.encodeComponent")(function* (value, source, field) {
1976
+ return yield* Effect.try({
1977
+ try: () => encodeURIComponent(value),
1978
+ catch: () => invalid(source, "The URL contains malformed Unicode.", field)
1979
+ });
1980
+ });
1981
+ const searchEntries = Effect.fn("Routing.searchEntries")(function* (search, partial) {
1982
+ yield* Schema.decodeUnknownEffect(Schema.String)(search).pipe(Effect.mapError(() => invalid("searchParams", "Search parameters must be URL query text.", void 0, partial)));
1983
+ const query = search.startsWith("?") ? search.slice(1) : search;
1984
+ const entries = [];
1985
+ for (const item of query.split("&")) {
1986
+ if (item === "") continue;
1987
+ const equals = item.indexOf("=");
1988
+ const key = equals < 0 ? item : item.slice(0, equals);
1989
+ const value = equals < 0 ? "" : item.slice(equals + 1);
1990
+ const name = yield* decodeComponent(key.replaceAll("+", " "), "searchParams", void 0, partial);
1991
+ entries.push([name, yield* decodeComponent(value.replaceAll("+", " "), "searchParams", name, partial)]);
1992
+ }
1993
+ return entries;
1994
+ });
1995
+ /** Parse a raw application-root-relative location before a URL implementation normalizes it. */
1996
+ const parseLocation = Effect.fn("Routing.parseLocation")(function* (input) {
1997
+ yield* Schema.decodeUnknownEffect(Schema.String)(input).pipe(Effect.mapError(() => invalid("params", "A location must be URL text.")));
1998
+ const fragmentAt = input.indexOf("#");
1999
+ const hash = fragmentAt < 0 ? "" : input.slice(fragmentAt);
2000
+ const beforeHash = fragmentAt < 0 ? input : input.slice(0, fragmentAt);
2001
+ const queryAt = beforeHash.indexOf("?");
2002
+ const search = queryAt < 0 ? "" : beforeHash.slice(queryAt);
2003
+ const rawPathname = queryAt < 0 ? beforeHash : beforeHash.slice(0, queryAt);
2004
+ const pathname = rawPathname.length > 1 && rawPathname.endsWith("/") ? rawPathname.slice(0, -1) : rawPathname;
2005
+ const segments = [];
2006
+ const partial = (pathComplete) => ({
2007
+ segments: [...segments],
2008
+ pathname,
2009
+ search,
2010
+ hash,
2011
+ canonicalUrl: `${pathname}${search}${hash}`,
2012
+ pathComplete
2013
+ });
2014
+ if (!rawPathname.startsWith("/") || rawPathname.startsWith("//")) return yield* invalid("params", "A location must start with exactly one application-root slash.", void 0, partial(false));
2015
+ if (pathname !== "/") for (const raw of pathname.slice(1).split("/")) {
2016
+ const segment = yield* decodeComponent(raw, "params", void 0, partial(false));
2017
+ yield* Schema.decodeEffect(SafeSegment)(segment).pipe(Effect.mapError(() => invalid("params", "Path segments must be nonempty and cannot contain slashes, dot navigation or control characters.", void 0, partial(false))));
2018
+ segments.push(segment);
2019
+ }
2020
+ yield* searchEntries(search, partial(true));
2021
+ yield* decodeComponent(hash.slice(1), "params", void 0, partial(true));
2022
+ return {
2023
+ segments,
2024
+ pathname,
2025
+ search,
2026
+ hash,
2027
+ canonicalUrl: `${pathname}${search}${hash}`
2028
+ };
2029
+ });
2030
+ const scalarSchema = (descriptor) => {
2031
+ switch (descriptor.type) {
2032
+ case "string": return Schema.String;
2033
+ case "id": return GeneratedId;
2034
+ case "number": return Schema.Finite;
2035
+ case "integer": return Schema.Int;
2036
+ case "boolean": return Schema.Boolean;
2037
+ case "literal": return Schema.Literal(descriptor.value);
2038
+ case "literals": return Schema.Literals(descriptor.values);
2039
+ }
2040
+ };
2041
+ const scalarText = Effect.fn("Routing.scalarText")(function* (descriptor, value, source, field) {
2042
+ switch (descriptor.type) {
2043
+ case "number":
2044
+ case "integer":
2045
+ yield* Schema.decodeEffect(DecimalText)(value).pipe(Effect.mapError(() => invalidField(source, field)));
2046
+ return yield* Schema.decodeUnknownEffect(scalarSchema(descriptor))(Number(value)).pipe(Effect.mapError(() => invalidField(source, field)));
2047
+ case "boolean": return (yield* Schema.decodeUnknownEffect(Schema.Literals(["true", "false"]))(value).pipe(Effect.mapError(() => invalidField(source, field)))) === "true";
2048
+ case "literal":
2049
+ yield* Schema.decodeEffect(Schema.Literal(String(descriptor.value)))(value).pipe(Effect.mapError(() => invalidField(source, field)));
2050
+ return descriptor.value;
2051
+ case "literals":
2052
+ yield* Schema.decodeEffect(Schema.Literals(descriptor.values.map(String)))(value).pipe(Effect.mapError(() => invalidField(source, field)));
2053
+ return descriptor.values.find((literal) => String(literal) === value);
2054
+ default: return yield* Schema.decodeUnknownEffect(scalarSchema(descriptor))(value).pipe(Effect.mapError(() => invalidField(source, field)));
2055
+ }
2056
+ });
2057
+ const suppliedValue = Effect.fn("Routing.suppliedValue")(function* (descriptor, input, source, field) {
2058
+ const schema = descriptor.type === "array" ? Schema.Array(scalarSchema(descriptor.value)) : scalarSchema(descriptor);
2059
+ const value = yield* Schema.decodeUnknownEffect(schema)(input).pipe(Effect.mapError(() => invalidField(source, field)));
2060
+ for (const item of Array$1.ensure(value)) yield* encodeComponent(String(item), source, field);
2061
+ return value;
2062
+ });
2063
+ const validateDefinition = Effect.fn("Routing.validateUrlDefinition")(function* (input) {
2064
+ const route = yield* Schema.decodeUnknownEffect(RouteDefinition)(input).pipe(Effect.mapError(() => invalid("params", "The route's URL declaration is invalid.")));
2065
+ const pattern = yield* validateRoutePattern(route).pipe(Effect.mapError((error) => invalid("params", error.message)));
2066
+ for (const field of route.searchParams) {
2067
+ yield* encodeComponent(field.name, "searchParams", field.name);
2068
+ if (field.value.type === "default") yield* suppliedValue(field.value.value, field.value.default, "searchParams", field.name);
2069
+ }
2070
+ return {
2071
+ route,
2072
+ pattern
2073
+ };
2074
+ });
2075
+ const inputRecord = Effect.fn("Routing.inputRecord")(function* (input, fields, source) {
2076
+ const values = yield* Schema.decodeUnknownEffect(InputRecord)(input === void 0 ? {} : input).pipe(Effect.mapError(() => invalid(source, `The ${source} input must be an object.`)));
2077
+ for (const name of Object.keys(values)) if (!fields.some((field) => field.name === name)) return yield* invalid(source, `The URL field '${name}' is not declared.`, name);
2078
+ return values;
2079
+ });
2080
+ const own = (record, field) => Object.hasOwn(record, field) ? record[field] : void 0;
2081
+ const pathValue = Effect.fn("Routing.pathValue")(function* (value, name) {
2082
+ const text = String(value);
2083
+ yield* Schema.decodeEffect(SafeSegment)(text).pipe(Effect.mapError(() => invalidField("params", name)));
2084
+ yield* encodeComponent(text, "params", name);
2085
+ return text;
2086
+ });
2087
+ /** rawParams already contains decoded segments from parseLocation and must not be decoded twice. */
2088
+ const decodeRouteInputs = Effect.fn("Routing.decodeRouteInputs")(function* (input, rawParams, search) {
2089
+ const { route } = yield* validateDefinition(input);
2090
+ const raw = yield* inputRecord(rawParams, route.params, "params");
2091
+ const params = [];
2092
+ for (const field of route.params) {
2093
+ const value = own(raw, field.name);
2094
+ if (field.value.type === "array") {
2095
+ const values = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.String).check(Schema.isMinLength(1)))(value).pipe(Effect.mapError(() => invalidField("params", field.name)));
2096
+ params.push([field.name, yield* Effect.forEach(values, (item) => pathValue(item, field.name))]);
2097
+ } else {
2098
+ const text = yield* Schema.decodeUnknownEffect(Schema.String)(value).pipe(Effect.mapError(() => invalidField("params", field.name)));
2099
+ yield* pathValue(text, field.name);
2100
+ params.push([field.name, yield* scalarText(field.value, text, "params", field.name)]);
2101
+ }
2102
+ }
2103
+ const query = yield* searchEntries(search);
2104
+ const searchParams = [];
2105
+ for (const field of route.searchParams) {
2106
+ const values = query.filter(([name]) => name === field.name).map(([, value]) => value);
2107
+ const descriptor = field.value;
2108
+ if (values.length === 0) {
2109
+ if (descriptor.type === "optional") {
2110
+ searchParams.push([field.name, void 0]);
2111
+ continue;
2112
+ }
2113
+ if (descriptor.type === "default") {
2114
+ searchParams.push([field.name, yield* suppliedValue(descriptor.value, descriptor.default, "searchParams", field.name)]);
2115
+ continue;
2116
+ }
2117
+ return yield* invalid("searchParams", `The required URL field '${field.name}' is missing.`, field.name);
2118
+ }
2119
+ const required = descriptor.type === "optional" || descriptor.type === "default" ? descriptor.value : descriptor;
2120
+ if (required.type === "array") searchParams.push([field.name, yield* Effect.forEach(values, (value) => scalarText(required.value, value, "searchParams", field.name))]);
2121
+ else {
2122
+ if (values.length !== 1) return yield* invalid("searchParams", `The scalar URL field '${field.name}' occurs more than once.`, field.name);
2123
+ searchParams.push([field.name, yield* scalarText(required, values[0], "searchParams", field.name)]);
2124
+ }
2125
+ }
2126
+ return {
2127
+ params: Object.fromEntries(params),
2128
+ searchParams: Object.fromEntries(searchParams)
2129
+ };
2130
+ });
2131
+ const sameValue = (left, right) => Array$1.isArray(left) && Array$1.isArray(right) ? left.length === right.length && left.every((value, index) => value === right[index]) : left === right;
2132
+ const buildUrl = Effect.fn("Routing.buildUrl")(function* (input, values = {}) {
2133
+ const { route, pattern } = yield* validateDefinition(input);
2134
+ const navigation = yield* Schema.decodeUnknownEffect(BuildInput)(values).pipe(Effect.mapError(() => invalid("params", "The URL navigation input is invalid.")));
2135
+ const params = yield* inputRecord(navigation.params, route.params, "params");
2136
+ const searchParams = yield* inputRecord(navigation.searchParams, route.searchParams, "searchParams");
2137
+ const segments = [];
2138
+ for (const segment of pattern.segments) {
2139
+ if (segment.kind === "Literal") {
2140
+ segments.push(yield* encodeComponent(segment.value, "params"));
2141
+ continue;
2142
+ }
2143
+ const field = route.params.find((candidate) => candidate.name === segment.name);
2144
+ const value = yield* suppliedValue(field.value, own(params, field.name), "params", field.name);
2145
+ const parts = Array$1.ensure(value);
2146
+ if (parts.length === 0) return yield* invalid("params", "Catch-all parameters must contain at least one segment.", field.name);
2147
+ for (const part of parts) segments.push(yield* encodeComponent(yield* pathValue(part, field.name), "params", field.name));
2148
+ }
2149
+ if (segments[0] === "_ignotum") {
2150
+ const first = pattern.segments[0];
2151
+ return yield* invalid("params", "The /_ignotum path is reserved for the platform.", first?.kind === "Parameter" || first?.kind === "CatchAll" ? first.name : void 0);
2152
+ }
2153
+ const query = new URLSearchParams();
2154
+ for (const field of route.searchParams) {
2155
+ const raw = own(searchParams, field.name);
2156
+ const descriptor = field.value;
2157
+ if (raw === void 0) {
2158
+ if (descriptor.type === "optional" || descriptor.type === "default") continue;
2159
+ return yield* invalid("searchParams", `The required URL field '${field.name}' is missing.`, field.name);
2160
+ }
2161
+ const required = descriptor.type === "optional" || descriptor.type === "default" ? descriptor.value : descriptor;
2162
+ const value = yield* suppliedValue(required, raw, "searchParams", field.name);
2163
+ if (descriptor.type === "default" && sameValue(value, descriptor.default)) continue;
2164
+ const items = Array$1.ensure(value);
2165
+ if (items.length === 0 && descriptor.type !== "optional" && descriptor.type !== "default") return yield* invalid("searchParams", "A required array cannot serialize as an absent search key.", field.name);
2166
+ for (const item of items) query.append(field.name, String(item));
2167
+ }
2168
+ const search = query.toString();
2169
+ const hash = navigation.hash === void 0 || navigation.hash === "" ? "" : `#${yield* encodeComponent(navigation.hash, "params")}`;
2170
+ return `/${segments.join("/")}${search === "" ? "" : `?${search}`}${hash}`;
2171
+ });
2172
+ //#endregion
2173
+ //#region src/client/router.ts
2174
+ const RouterContext = createContext(void 0);
2175
+ const useRouter = () => {
2176
+ const router = useContext(RouterContext);
2177
+ if (router === void 0) throw new Error("Ignotum routing hooks require a managed client mount.");
2178
+ return router;
2179
+ };
2180
+ const useRouterSnapshot = (router) => {
2181
+ const [snapshot, setSnapshot] = useState(router.getSnapshot);
2182
+ useLayoutEffect(() => {
2183
+ const update = () => setSnapshot(router.getSnapshot());
2184
+ const unsubscribe = router.subscribe(update);
2185
+ update();
2186
+ return unsubscribe;
2187
+ }, [router]);
2188
+ return snapshot;
2189
+ };
2190
+ function url(reference, ...args) {
2191
+ return Effect.runSync(buildUrl(getRouteDefinition(reference), args[0]));
2192
+ }
2193
+ const destinationHref = (reference, input = {}) => Effect.runSync(buildUrl(getRouteDefinition(reference), {
2194
+ params: input.params,
2195
+ searchParams: input.searchParams,
2196
+ hash: input.hash
2197
+ }));
2198
+ const NavigationInput = Schema.Struct({
2199
+ params: Schema.optional(Schema.Unknown),
2200
+ searchParams: Schema.optional(Schema.Unknown),
2201
+ hash: Schema.optional(Schema.String),
2202
+ replace: Schema.optional(Schema.Boolean),
2203
+ scroll: Schema.optional(Schema.Literals(["auto", "preserve"]))
2204
+ }).annotate({ parseOptions: { onExcessProperty: "error" } });
2205
+ const useNavigate = () => {
2206
+ const router = useRouter();
2207
+ const [navigate] = useState(() => Object.assign((reference, ...args) => {
2208
+ const options = Schema.decodeUnknownSync(NavigationInput)(args[0] ?? {});
2209
+ return router.navigate(destinationHref(reference, options), options);
2210
+ }, {
2211
+ back: router.back,
2212
+ forward: router.forward
2213
+ }));
2214
+ return navigate;
2215
+ };
2216
+ const useLocation = () => {
2217
+ const snapshot = useRouterSnapshot(useRouter());
2218
+ if (snapshot.location === void 0) throw new Error("The initial Ignotum location has not committed.");
2219
+ return snapshot.location;
2220
+ };
2221
+ const useNavigation = () => useRouterSnapshot(useRouter()).navigation;
2222
+ const useBlocker = ({ when }) => {
2223
+ const router = useRouter();
2224
+ const [id] = useState(() => Symbol("blocker"));
2225
+ useRouterSnapshot(router);
2226
+ useLayoutEffect(() => {
2227
+ router.setBlocker(id, when);
2228
+ }, [
2229
+ router,
2230
+ id,
2231
+ when
2232
+ ]);
2233
+ useLayoutEffect(() => () => router.setBlocker(id, false), [router, id]);
2234
+ return router.getBlocker(id);
2235
+ };
2236
+ const matchReference = (reference, snapshot, end) => {
2237
+ const location = snapshot.location;
2238
+ const outcome = snapshot.branch?.outcome;
2239
+ if (location === void 0 || outcome === void 0) return;
2240
+ const definition = getRouteDefinition(reference);
2241
+ if (end && (outcome._tag !== "Matched" || outcome.route.id !== definition.id)) return;
2242
+ const parsed = Effect.runSync(parseLocation(`${location.pathname}${location.search}`).pipe(Effect.result));
2243
+ if (parsed._tag === "Failure") return;
2244
+ const compiled = Effect.runSync(compilePatterns([{
2245
+ ...definition,
2246
+ layouts: []
2247
+ }], []));
2248
+ for (let length = parsed.success.segments.length; length >= 0; length--) {
2249
+ if (length === 0 && parsed.success.segments.length !== 0) break;
2250
+ const match = matchPattern(compiled, parsed.success.segments.slice(0, length));
2251
+ if (match.route !== void 0) {
2252
+ const decoded = Effect.runSync(decodeRouteInputs(definition, match.params, location.search).pipe(Effect.result));
2253
+ if (decoded._tag === "Failure") return;
2254
+ return {
2255
+ ...decoded.success,
2256
+ hash: location.hash
2257
+ };
2258
+ }
2259
+ if (end) break;
2260
+ }
2261
+ };
2262
+ function useMatch(reference, options) {
2263
+ return matchReference(reference, useRouterSnapshot(useRouter()), options?.end ?? true);
2264
+ }
2265
+ function useRoute(reference) {
2266
+ const router = useRouter();
2267
+ const snapshot = useRouterSnapshot(router);
2268
+ const match = matchReference(reference, snapshot, true);
2269
+ if (match === void 0) throw new Error(`useRoute requires page '${getRouteDefinition(reference).id}' to be active.`);
2270
+ return {
2271
+ ...match,
2272
+ setSearchParams: (next, options) => {
2273
+ const latest = matchReference(reference, router.getSnapshot(), true);
2274
+ const location = router.getSnapshot().location;
2275
+ if (latest === void 0 || location === void 0) throw new Error("Cannot update search parameters for an inactive route.");
2276
+ const inputs = Predicate.isFunction(next) ? next(latest.searchParams) : next;
2277
+ const href = destinationHref(reference, {
2278
+ params: latest.params,
2279
+ searchParams: inputs,
2280
+ hash: latest.hash
2281
+ });
2282
+ const destination = new URL(href, router.browser.location.origin);
2283
+ destination.pathname = location.pathname;
2284
+ const declared = new Set(getRouteDefinition(reference).searchParams.map((field) => field.name));
2285
+ for (const [name, value] of new URLSearchParams(location.search)) if (!declared.has(name)) destination.searchParams.append(name, value);
2286
+ return router.navigate(`${destination.pathname}${destination.search}${destination.hash}`, {
2287
+ scroll: "preserve",
2288
+ ...options
2289
+ });
2290
+ }
2291
+ };
2292
+ }
2293
+ const activePath = (current, destination, end) => {
2294
+ const source = Effect.runSync(parseLocation(current).pipe(Effect.result));
2295
+ const target = Effect.runSync(parseLocation(destination).pipe(Effect.result));
2296
+ if (source._tag === "Failure" || target._tag === "Failure") return false;
2297
+ const from = source.success.segments;
2298
+ const to = target.success.segments;
2299
+ if (from.length !== to.length && (end || to.length === 0 || from.length < to.length)) return false;
2300
+ return to.every((segment, index) => from[index] === segment);
2301
+ };
2302
+ function Anchor(props) {
2303
+ const router = useRouter();
2304
+ const snapshot = useRouterSnapshot(router);
2305
+ const { to, params, searchParams, hash, replace, scroll, preload = "intent", end = true, active = false, class: className, onClick, onMouseEnter, onFocus, ...attributes } = props;
2306
+ const href = destinationHref(to, {
2307
+ params,
2308
+ searchParams,
2309
+ hash
2310
+ });
2311
+ const isActive = active && snapshot.location !== void 0 && activePath(snapshot.location.pathname, href.split(/[?#]/)[0], end);
2312
+ const prepare = () => {
2313
+ if (preload !== "none") router.preload(href).catch(() => {});
2314
+ };
2315
+ return createElement("a", {
2316
+ ...attributes,
2317
+ href,
2318
+ class: Predicate.isFunction(className) ? className({ isActive }) : className,
2319
+ "aria-current": isActive ? "page" : attributes["aria-current"],
2320
+ onClick: (event) => {
2321
+ onClick?.(event);
2322
+ const anchor = event.currentTarget;
2323
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || anchor.target !== "" && anchor.target.toLowerCase() !== "_self" || anchor.hasAttribute("download") || anchor.relList.contains("external") || anchor.origin !== router.browser.location.origin || `${anchor.pathname}${anchor.search}${anchor.hash}` !== href) return;
2324
+ event.preventDefault();
2325
+ router.navigate(href, {
2326
+ replace,
2327
+ scroll
2328
+ });
2329
+ },
2330
+ onMouseEnter: (event) => {
2331
+ onMouseEnter?.(event);
2332
+ if (!event.defaultPrevented) prepare();
2333
+ },
2334
+ onFocus: (event) => {
2335
+ onFocus?.(event);
2336
+ if (!event.defaultPrevented) prepare();
2337
+ }
2338
+ });
2339
+ }
2340
+ function Link(props) {
2341
+ return createElement(Anchor, props);
2342
+ }
2343
+ function NavLink(props) {
2344
+ return createElement(Anchor, {
2345
+ ...props,
2346
+ active: true
2347
+ });
2348
+ }
2349
+ function Redirect(props) {
2350
+ const router = useRouter();
2351
+ const href = destinationHref(props.to, props);
2352
+ const previous = useRef();
2353
+ const replace = props.replace ?? true;
2354
+ useEffect(() => {
2355
+ const identity = JSON.stringify([
2356
+ href,
2357
+ replace,
2358
+ props.scroll
2359
+ ]);
2360
+ if (previous.current === identity) return;
2361
+ previous.current = identity;
2362
+ router.navigate(href, {
2363
+ replace,
2364
+ scroll: props.scroll
2365
+ }, true);
2366
+ }, [
2367
+ router,
2368
+ href,
2369
+ replace,
2370
+ props.scroll
2371
+ ]);
2372
+ return null;
2373
+ }
2374
+ //#endregion
2375
+ export { Files as C, BrowserSession as S, matchPattern as _, url as a, syncClientInternals as b, useMatch as c, useRoute as d, useRouterSnapshot as f, matchDirectory as g, compilePatterns as h, RouterContext as i, useNavigate as l, parseLocation as m, NavLink as n, useBlocker as o, decodeRouteInputs as p, Redirect as r, useLocation as s, Link as t, useNavigation as u, SyncClient as v, ClientInfrastructureError as w, syncRuntime as x, mountManagedSync as y };
2376
+
2377
+ //# sourceMappingURL=router-BSBI1oN1.js.map