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.
- package/README.md +6 -6
- package/dist/cli/bin.mjs +6111 -4448
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
- package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
- package/dist/cli/ffi-7IElU4Co.mjs +148 -0
- package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
- package/dist/cli/managed-release.d.mts +1 -0
- package/dist/cli/managed-release.mjs +14 -0
- package/dist/cli/managed-release.mjs.map +1 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
- package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
- package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
- package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
- package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
- package/dist/runtime/client.d.ts +50 -15
- package/dist/runtime/client.js +7 -1138
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +269 -0
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
- package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
- package/dist/runtime/id--1m0NuUL.js.map +1 -0
- package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
- package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
- package/dist/runtime/image-Djtjy4Z8.js +19 -0
- package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
- package/dist/runtime/index-D4seNHlX.d.ts +184 -0
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +30 -1
- package/dist/runtime/internal/api.js.map +1 -0
- package/dist/runtime/internal/bootstrap.d.ts +2 -0
- package/dist/runtime/internal/bootstrap.js +2 -0
- package/dist/runtime/internal/client.d.ts +2 -0
- package/dist/runtime/internal/client.js +75 -0
- package/dist/runtime/internal/client.js.map +1 -0
- package/dist/runtime/internal/host.d.ts +25 -8
- package/dist/runtime/internal/host.js +27 -6
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/routes.d.ts +2 -0
- package/dist/runtime/internal/routes.js +2 -0
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +2 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/pagination-D3qd6s9N.js +33 -0
- package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
- package/dist/runtime/result-BkziOG1L.d.ts +1 -0
- package/dist/runtime/router-BSBI1oN1.js +2377 -0
- package/dist/runtime/router-BSBI1oN1.js.map +1 -0
- package/dist/runtime/routes-Ce8TVP-T.js +690 -0
- package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
- package/dist/runtime/{schema-1Zs03-iS.js → schema-D1wOqiNH.js} +40 -12
- package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
- package/dist/runtime/server.d.ts +5 -5
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
- package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
- package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
- package/dist/runtime/types-DeCCyqLU-BIm7rU-0.d.ts +341 -0
- package/package.json +29 -4
- package/src/cli/agent-files.ts +14 -8
- package/src/cli/build/managed-client.ts +411 -0
- package/src/cli/build/managed-server.ts +294 -0
- package/src/cli/build/managed.ts +67 -0
- package/src/cli/build/output.ts +47 -0
- package/src/cli/build/server.ts +51 -220
- package/src/cli/client-config.ts +1 -140
- package/src/cli/client-styles.ts +1 -0
- package/src/cli/codegen.ts +9 -4
- package/src/cli/command.ts +28 -5
- package/src/cli/control-client.ts +84 -101
- package/src/cli/deploy.ts +133 -177
- package/src/cli/dev.ts +506 -107
- package/src/cli/image-assets.ts +499 -0
- package/src/cli/managed-client-boundaries.ts +103 -0
- package/src/cli/managed-dev-platform.ts +118 -0
- package/src/cli/managed-exports.ts +219 -0
- package/src/cli/managed-release-bin.ts +13 -0
- package/src/cli/managed-release.ts +260 -0
- package/src/cli/module-boundaries.ts +11 -1
- package/src/cli/new-app.ts +111 -87
- package/src/cli/route-codegen.ts +311 -0
- package/src/cli/route-static.ts +710 -0
- package/src/client/bootstrap.ts +110 -0
- package/src/client/errors.ts +13 -7
- package/src/client/files.ts +64 -0
- package/src/client/id.ts +9 -5
- package/src/client/image.ts +28 -0
- package/src/client/index.ts +25 -2
- package/src/client/managed-client.ts +729 -0
- package/src/client/managed-upgrade.ts +283 -0
- package/src/client/recovery-journal.ts +195 -0
- package/src/client/route-boundaries.ts +184 -0
- package/src/client/router-history.ts +152 -0
- package/src/client/router-store.ts +644 -0
- package/src/client/router.ts +324 -0
- package/src/client/routes.ts +28 -0
- package/src/client/sync.ts +66 -592
- package/src/dev-runtime/database.ts +77 -2
- package/src/dev-runtime/functions.ts +3 -3
- package/src/dev-runtime/id.ts +19 -4
- package/src/dev-runtime/managed-functions.ts +255 -0
- package/src/dev-runtime/managed-sync.ts +263 -0
- package/src/dev-runtime/managed-websocket.ts +71 -0
- package/src/dev-runtime/migrations.ts +20 -0
- package/src/dev-runtime/sync.ts +236 -236
- package/src/image-assets.d.ts +36 -0
- package/src/internal/client.ts +186 -0
- package/src/internal/host.ts +1 -1
- package/src/internal/http-paths.ts +0 -3
- package/src/internal/routes.ts +45 -0
- package/dist/runtime/api-CAgKDij7.js.map +0 -1
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
- package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
- package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/index-D54flWtH.d.ts +0 -402
- package/dist/runtime/pagination-DnKg3dkI-r5ZUxBBx.d.ts +0 -112
- package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
- package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
- package/src/cli/build/client.ts +0 -119
- package/src/cli/build/public.ts +0 -186
- package/src/cli/client-entry.ts +0 -152
- package/src/cli/client-plugin.ts +0 -136
- 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
|