@embedpdf/core 3.0.0-next.14 → 3.0.0-next.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/dist/index.cjs +879 -427
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +212 -292
- package/dist/index.d.ts +212 -292
- package/dist/index.js +797 -400
- package/dist/index.js.map +1 -1
- package/dist/settings-CMWFRjmQ.js +1837 -0
- package/dist/settings-CMWFRjmQ.js.map +1 -0
- package/dist/settings-maKtf237.cjs +2034 -0
- package/dist/settings-maKtf237.cjs.map +1 -0
- package/dist/testing.cjs +401 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +94 -0
- package/dist/testing.d.ts +94 -0
- package/dist/testing.js +393 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-t-DaU8VK.d.cts +1162 -0
- package/dist/types-t-DaU8VK.d.ts +1162 -0
- package/package.json +6 -2
|
@@ -0,0 +1,1837 @@
|
|
|
1
|
+
import { EngineError, encodePageKey, generateUuidV7, objectNumbersNamedBy, objectNumbersReferencedBy, pageRefsEqual } from "@embedpdf/engine-core/runtime";
|
|
2
|
+
//#region src/types.ts
|
|
3
|
+
/** Built-in token for the document registry capability (provided by the kernel). */
|
|
4
|
+
const DocumentsToken = { name: "documents" };
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/bursts.ts
|
|
7
|
+
const txOf = (event) => "origin" in event ? event.origin.tx : void 0;
|
|
8
|
+
function createBurstGate(store) {
|
|
9
|
+
let open = null;
|
|
10
|
+
const close = () => {
|
|
11
|
+
const held = open;
|
|
12
|
+
open = null;
|
|
13
|
+
held?.release();
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
first(event) {
|
|
17
|
+
const tx = txOf(event);
|
|
18
|
+
if (open && open.id !== tx?.id) close();
|
|
19
|
+
if (!open && tx && tx.index === 0 && tx.count > 1) open = {
|
|
20
|
+
id: tx.id,
|
|
21
|
+
release: store.hold()
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
last(event) {
|
|
25
|
+
const tx = txOf(event);
|
|
26
|
+
if (open && tx?.id === open.id && tx.index === tx.count - 1) close();
|
|
27
|
+
},
|
|
28
|
+
close
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/scope.ts
|
|
33
|
+
/**
|
|
34
|
+
* A lifecycle operation was cancelled — the tab was closed mid-open, or the
|
|
35
|
+
* kernel was destroyed mid-start. Not a failure: callers that race `open()`
|
|
36
|
+
* against `close()` should treat this rejection as "the close won".
|
|
37
|
+
*/
|
|
38
|
+
var CancelledError = class extends Error {
|
|
39
|
+
constructor(what) {
|
|
40
|
+
super(`[kernel] cancelled: ${what}`);
|
|
41
|
+
this.name = "CancelledError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const isCancelled = (error) => error instanceof CancelledError;
|
|
45
|
+
function createScope(report) {
|
|
46
|
+
const teardowns = [];
|
|
47
|
+
let disposal = null;
|
|
48
|
+
return {
|
|
49
|
+
get disposed() {
|
|
50
|
+
return disposal !== null;
|
|
51
|
+
},
|
|
52
|
+
defer(teardown) {
|
|
53
|
+
if (disposal) disposal.then(() => Promise.resolve().then(teardown).catch(report));
|
|
54
|
+
else teardowns.push(teardown);
|
|
55
|
+
},
|
|
56
|
+
dispose() {
|
|
57
|
+
return disposal ?? (disposal = (async () => {
|
|
58
|
+
while (teardowns.length) try {
|
|
59
|
+
await teardowns.pop()();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
report(error);
|
|
62
|
+
}
|
|
63
|
+
})());
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/errors.ts
|
|
69
|
+
var PluginError = class extends Error {
|
|
70
|
+
constructor(code, capability, message, options = {}) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "PluginError";
|
|
73
|
+
if (options.cause !== void 0) this.cause = options.cause;
|
|
74
|
+
this.code = code;
|
|
75
|
+
this.capability = capability;
|
|
76
|
+
this.permission = options.permission ?? null;
|
|
77
|
+
this.details = options.details;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
function toPluginErrorInfo(error) {
|
|
81
|
+
return {
|
|
82
|
+
code: error.code,
|
|
83
|
+
message: error.message,
|
|
84
|
+
capability: error.capability,
|
|
85
|
+
permission: error.permission
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function isPluginError(value, code) {
|
|
89
|
+
if (!(value instanceof PluginError)) return false;
|
|
90
|
+
return code === void 0 || value.code === code;
|
|
91
|
+
}
|
|
92
|
+
const ENGINE_CODE_MAP = {
|
|
93
|
+
InvalidArg: "invalid-input",
|
|
94
|
+
MalformedPdf: "invalid-input",
|
|
95
|
+
PayloadTooLarge: "invalid-input",
|
|
96
|
+
WireFormat: "operation-failed",
|
|
97
|
+
DocNotOpen: "not-ready",
|
|
98
|
+
DocPasswordRequired: "permission-denied",
|
|
99
|
+
DocPasswordIncorrect: "permission-denied",
|
|
100
|
+
SharePasswordRequired: "permission-denied",
|
|
101
|
+
Unauthenticated: "permission-denied",
|
|
102
|
+
Forbidden: "permission-denied",
|
|
103
|
+
ProtectedDocument: "permission-denied",
|
|
104
|
+
NotFound: "not-found",
|
|
105
|
+
NotImplemented: "unsupported",
|
|
106
|
+
Aborted: "operation-cancelled",
|
|
107
|
+
LayerVersionConflict: "conflict",
|
|
108
|
+
StaleBase: "conflict",
|
|
109
|
+
SigningPending: "conflict",
|
|
110
|
+
SigningExpired: "conflict",
|
|
111
|
+
SigningVersionMismatch: "conflict",
|
|
112
|
+
ObjectNumberUnavailable: "conflict",
|
|
113
|
+
LayerFull: "conflict",
|
|
114
|
+
ChangeConflict: "conflict",
|
|
115
|
+
UndoUnavailable: "conflict",
|
|
116
|
+
IdempotencyKeyReused: "invalid-input"
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The permission an engine refusal names. `PermissionDenied` puts it in `details.required`
|
|
120
|
+
* (the first of `details.anyOf` when any of several would have done), and `details` is what
|
|
121
|
+
* survives the trip from a worker or the server, where the refusal arrives as a plain
|
|
122
|
+
* `EngineError` with code `Forbidden`.
|
|
123
|
+
*/
|
|
124
|
+
function requiredPermissionOf(error) {
|
|
125
|
+
const required = error.details?.required;
|
|
126
|
+
return typeof required === "string" ? required : null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The one boundary mapping from whatever an engine call threw to a
|
|
130
|
+
* `PluginError`. Idempotent: an existing PluginError passes through.
|
|
131
|
+
* Cancellation (the kernel's CancelledError, a DOM AbortError, or an engine
|
|
132
|
+
* abort) is `operation-cancelled`, never `operation-failed`, so callers can
|
|
133
|
+
* tell "nobody wanted this anymore" from "this broke". A refusal names the
|
|
134
|
+
* missing permission in `permission`; `details` keeps the rest of what the
|
|
135
|
+
* engine sent (`anyOf`, `context`).
|
|
136
|
+
*/
|
|
137
|
+
function toPluginError(capability, error) {
|
|
138
|
+
if (error instanceof PluginError) return error;
|
|
139
|
+
if (isCancelled(error) || error instanceof Error && error.name === "AbortError") return new PluginError("operation-cancelled", capability, "operation cancelled", { cause: error });
|
|
140
|
+
if (EngineError.is(error)) {
|
|
141
|
+
const code = ENGINE_CODE_MAP[String(error.code)] ?? "operation-failed";
|
|
142
|
+
return new PluginError(code, capability, error.message, {
|
|
143
|
+
cause: error,
|
|
144
|
+
details: error.details,
|
|
145
|
+
permission: code === "permission-denied" ? requiredPermissionOf(error) : null
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return new PluginError("operation-failed", capability, error instanceof Error ? error.message : String(error), { cause: error });
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/event-hook.ts
|
|
152
|
+
/**
|
|
153
|
+
* Create one capability event.
|
|
154
|
+
*
|
|
155
|
+
* Subscribe with `{ signal }` to tie the subscription to an AbortSignal; a
|
|
156
|
+
* pre-aborted signal never registers.
|
|
157
|
+
*
|
|
158
|
+
* Delivery contract: synchronous fan-out over a snapshot of the listener set
|
|
159
|
+
* (listeners added or removed during an emit don't affect that emit); a
|
|
160
|
+
* throwing listener is isolated — reported through `onListenerError`, never
|
|
161
|
+
* allowed to break the emitting operation or its sibling listeners. After
|
|
162
|
+
* `dispose()`, `on` returns an inert unsubscribe (teardown races never throw).
|
|
163
|
+
* No value cache, no replay, no equality — state belongs to the store.
|
|
164
|
+
*/
|
|
165
|
+
function createEventHook(onListenerError) {
|
|
166
|
+
let listeners = /* @__PURE__ */ new Set();
|
|
167
|
+
return {
|
|
168
|
+
on(listener, options) {
|
|
169
|
+
if (!listeners || options?.signal?.aborted) return () => {};
|
|
170
|
+
listeners.add(listener);
|
|
171
|
+
const off = () => void listeners?.delete(listener);
|
|
172
|
+
options?.signal?.addEventListener("abort", off, { once: true });
|
|
173
|
+
return off;
|
|
174
|
+
},
|
|
175
|
+
emit(event) {
|
|
176
|
+
if (!listeners) return;
|
|
177
|
+
for (const listener of [...listeners]) try {
|
|
178
|
+
listener(event);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
onListenerError?.(error);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
dispose() {
|
|
184
|
+
listeners = null;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/changes.ts
|
|
190
|
+
/**
|
|
191
|
+
* The change queue (`ctx.changes`): one per open document, shared by every
|
|
192
|
+
* plugin of it.
|
|
193
|
+
*
|
|
194
|
+
* A user action is one change: a list of the engine's ops, applied all or
|
|
195
|
+
* nothing by `doc.apply`, sent as one request and undone as one step. A plugin
|
|
196
|
+
* stages it; every mirror's `view()` shows it at once (`predict`, mirror.ts),
|
|
197
|
+
* and the queue sends it after everything staged before it. The engine keeps
|
|
198
|
+
* that order, and publishes a change's events before it answers.
|
|
199
|
+
*
|
|
200
|
+
* A change leaves the views once the engine answered:
|
|
201
|
+
* - applied: once each mirror holds the answer. A mirror that folded the
|
|
202
|
+
* events already does; one that asked to read again keeps showing the
|
|
203
|
+
* change until that read lands (mirror.ts).
|
|
204
|
+
* - refused: at once, together with the changes that name an object only it
|
|
205
|
+
* would have created (its dependents), which can't apply either.
|
|
206
|
+
*
|
|
207
|
+
* Two ways a change is put together over time:
|
|
208
|
+
* - a hold can still be amended before it is sent (typing, a form commit
|
|
209
|
+
* waiting for its scripts). Any later stage sends the open holds first, so
|
|
210
|
+
* send order is always staging order;
|
|
211
|
+
* - a group makes everything staged inside it, by any plugin, one change.
|
|
212
|
+
*/
|
|
213
|
+
const changeOf = (entry) => entry.undoOf !== null ? { undoOf: entry.undoOf } : { ops: entry.ops ?? [] };
|
|
214
|
+
function createDocumentChanges(options) {
|
|
215
|
+
let entries = [];
|
|
216
|
+
let version = 0;
|
|
217
|
+
let nextSeq = 0;
|
|
218
|
+
let grouping = null;
|
|
219
|
+
let closed = false;
|
|
220
|
+
const staged = createEventHook(options.report);
|
|
221
|
+
const settled = createEventHook(options.report);
|
|
222
|
+
const handleFor = (capability) => {
|
|
223
|
+
const handle = options.handle();
|
|
224
|
+
if (!handle) throw new PluginError("not-ready", capability, "the document has not opened yet");
|
|
225
|
+
return handle;
|
|
226
|
+
};
|
|
227
|
+
const assertOpen = (capability) => {
|
|
228
|
+
if (closed) throw new PluginError("instance-closed", capability, "the document closed");
|
|
229
|
+
};
|
|
230
|
+
const changed = () => {
|
|
231
|
+
version += 1;
|
|
232
|
+
options.notify();
|
|
233
|
+
};
|
|
234
|
+
const createEntry = (init) => {
|
|
235
|
+
let resolve;
|
|
236
|
+
let reject;
|
|
237
|
+
const result = new Promise((onResolve, onReject) => {
|
|
238
|
+
resolve = onResolve;
|
|
239
|
+
reject = onReject;
|
|
240
|
+
});
|
|
241
|
+
result.catch(() => {});
|
|
242
|
+
return {
|
|
243
|
+
...init,
|
|
244
|
+
merge: init.merge ?? null,
|
|
245
|
+
seq: nextSeq++,
|
|
246
|
+
opId: generateUuidV7(),
|
|
247
|
+
result,
|
|
248
|
+
answered: false,
|
|
249
|
+
abort: null,
|
|
250
|
+
resolve,
|
|
251
|
+
reject
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
const remove = (entry) => {
|
|
255
|
+
entries = entries.filter((candidate) => candidate !== entry);
|
|
256
|
+
};
|
|
257
|
+
const applied = (entry, result) => {
|
|
258
|
+
if (entry.answered) return;
|
|
259
|
+
entry.answered = true;
|
|
260
|
+
remove(entry);
|
|
261
|
+
changed();
|
|
262
|
+
entry.resolve(result);
|
|
263
|
+
settled.emit({
|
|
264
|
+
change: entry,
|
|
265
|
+
status: "applied",
|
|
266
|
+
result
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
const refused = (entry, error) => {
|
|
270
|
+
if (entry.answered) return;
|
|
271
|
+
entry.answered = true;
|
|
272
|
+
remove(entry);
|
|
273
|
+
entry.reject(error);
|
|
274
|
+
settled.emit({
|
|
275
|
+
change: entry,
|
|
276
|
+
status: "refused",
|
|
277
|
+
error
|
|
278
|
+
});
|
|
279
|
+
refuseDependents(entry);
|
|
280
|
+
changed();
|
|
281
|
+
};
|
|
282
|
+
/** The changes staged after `entry` that name an object only it would have created. */
|
|
283
|
+
const refuseDependents = (entry) => {
|
|
284
|
+
const created = new Set(objectNumbersNamedBy(changeOf(entry)));
|
|
285
|
+
if (created.size === 0) return;
|
|
286
|
+
for (const later of entries) {
|
|
287
|
+
if (later.seq < entry.seq || later.answered) continue;
|
|
288
|
+
if (!objectNumbersReferencedBy(changeOf(later)).some((number) => created.has(number))) continue;
|
|
289
|
+
later.abort?.();
|
|
290
|
+
refused(later, new PluginError("conflict", later.capability, `a change it depends on (${entry.label.key}) was refused`, { details: {
|
|
291
|
+
reason: "dependency-refused",
|
|
292
|
+
dependsOn: entry.opId
|
|
293
|
+
} }));
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const send = (entry) => {
|
|
297
|
+
entry.held = false;
|
|
298
|
+
let answer;
|
|
299
|
+
try {
|
|
300
|
+
answer = handleFor(entry.capability).apply(changeOf(entry), { opId: entry.opId });
|
|
301
|
+
} catch (error) {
|
|
302
|
+
refused(entry, toPluginError(entry.capability, error));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (typeof answer.abort === "function") entry.abort = (reason) => answer.abort(reason);
|
|
306
|
+
answer.then((result) => applied(entry, result), (error) => refused(entry, toPluginError(entry.capability, error)));
|
|
307
|
+
};
|
|
308
|
+
const cancel = (entry, capability) => {
|
|
309
|
+
if (entry.answered) return;
|
|
310
|
+
entry.answered = true;
|
|
311
|
+
remove(entry);
|
|
312
|
+
entry.reject(new PluginError("operation-cancelled", capability, "the change was cancelled"));
|
|
313
|
+
changed();
|
|
314
|
+
};
|
|
315
|
+
/** Send the open holds staged before `before`, in staging order. */
|
|
316
|
+
const sendHolds = (before = Infinity) => {
|
|
317
|
+
for (const entry of entries) {
|
|
318
|
+
if (!entry.held || entry.seq >= before) continue;
|
|
319
|
+
if ((entry.ops ?? []).length === 0) {
|
|
320
|
+
cancel(entry, entry.capability);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
staged.emit(entry);
|
|
324
|
+
send(entry);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
const invalid = (capability, message) => new PluginError("invalid-input", capability, message);
|
|
328
|
+
/** One stage inside a group: its ops join the group's change. */
|
|
329
|
+
const join = (group, change) => {
|
|
330
|
+
const entry = group.entry;
|
|
331
|
+
if (!entry) {
|
|
332
|
+
group.entry = createEntry({
|
|
333
|
+
label: group.label,
|
|
334
|
+
ops: change.ops,
|
|
335
|
+
undoOf: null,
|
|
336
|
+
shows: change.ops,
|
|
337
|
+
undo: change.undo ?? [],
|
|
338
|
+
history: change.history !== false,
|
|
339
|
+
held: false,
|
|
340
|
+
capability: group.capability
|
|
341
|
+
});
|
|
342
|
+
entries.push(group.entry);
|
|
343
|
+
changed();
|
|
344
|
+
return group.entry;
|
|
345
|
+
}
|
|
346
|
+
entry.ops = [...entry.ops ?? [], ...change.ops];
|
|
347
|
+
entry.shows = entry.ops;
|
|
348
|
+
entry.undo = [...change.undo ?? [], ...entry.undo];
|
|
349
|
+
entry.history = entry.history || change.history !== false;
|
|
350
|
+
changed();
|
|
351
|
+
return entry;
|
|
352
|
+
};
|
|
353
|
+
const forPlugin = (capability) => ({
|
|
354
|
+
stage(change) {
|
|
355
|
+
assertOpen(capability);
|
|
356
|
+
if ("undoOf" in change) {
|
|
357
|
+
if (grouping) throw invalid(capability, "an undo can not join a group");
|
|
358
|
+
} else if (change.ops.length === 0) throw invalid(capability, "a change needs at least one op");
|
|
359
|
+
else if (grouping) return join(grouping, change);
|
|
360
|
+
sendHolds();
|
|
361
|
+
const entry = "undoOf" in change ? createEntry({
|
|
362
|
+
label: change.label,
|
|
363
|
+
ops: null,
|
|
364
|
+
undoOf: change.undoOf,
|
|
365
|
+
shows: change.shows,
|
|
366
|
+
undo: [],
|
|
367
|
+
history: false,
|
|
368
|
+
held: false,
|
|
369
|
+
capability
|
|
370
|
+
}) : createEntry({
|
|
371
|
+
label: change.label,
|
|
372
|
+
ops: change.ops,
|
|
373
|
+
undoOf: null,
|
|
374
|
+
shows: change.ops,
|
|
375
|
+
undo: change.undo ?? [],
|
|
376
|
+
history: change.history !== false,
|
|
377
|
+
held: false,
|
|
378
|
+
capability
|
|
379
|
+
});
|
|
380
|
+
entries.push(entry);
|
|
381
|
+
changed();
|
|
382
|
+
staged.emit(entry);
|
|
383
|
+
send(entry);
|
|
384
|
+
return entry;
|
|
385
|
+
},
|
|
386
|
+
hold(label, options = {}) {
|
|
387
|
+
assertOpen(capability);
|
|
388
|
+
if (grouping) throw invalid(capability, "a change can not be held inside a group");
|
|
389
|
+
let entry = null;
|
|
390
|
+
let cancelled = false;
|
|
391
|
+
const open = () => !closed && !cancelled && (entry === null || entry.held && !entry.answered);
|
|
392
|
+
return {
|
|
393
|
+
get open() {
|
|
394
|
+
return open();
|
|
395
|
+
},
|
|
396
|
+
get change() {
|
|
397
|
+
return entry;
|
|
398
|
+
},
|
|
399
|
+
set(ops, undo = []) {
|
|
400
|
+
if (!open()) return;
|
|
401
|
+
if (!entry) {
|
|
402
|
+
entry = createEntry({
|
|
403
|
+
label,
|
|
404
|
+
ops,
|
|
405
|
+
undoOf: null,
|
|
406
|
+
merge: options.merge ?? null,
|
|
407
|
+
shows: ops,
|
|
408
|
+
undo,
|
|
409
|
+
history: true,
|
|
410
|
+
held: true,
|
|
411
|
+
capability
|
|
412
|
+
});
|
|
413
|
+
entries.push(entry);
|
|
414
|
+
} else {
|
|
415
|
+
entry.ops = ops;
|
|
416
|
+
entry.shows = ops;
|
|
417
|
+
entry.undo = undo;
|
|
418
|
+
}
|
|
419
|
+
changed();
|
|
420
|
+
},
|
|
421
|
+
send() {
|
|
422
|
+
if (entry?.held) sendHolds(entry.seq + 1);
|
|
423
|
+
return entry;
|
|
424
|
+
},
|
|
425
|
+
cancel() {
|
|
426
|
+
cancelled = true;
|
|
427
|
+
if (entry?.held) cancel(entry, capability);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
},
|
|
431
|
+
group(label, run) {
|
|
432
|
+
assertOpen(capability);
|
|
433
|
+
if (grouping) return run();
|
|
434
|
+
sendHolds();
|
|
435
|
+
const group = {
|
|
436
|
+
label,
|
|
437
|
+
capability,
|
|
438
|
+
entry: null
|
|
439
|
+
};
|
|
440
|
+
grouping = group;
|
|
441
|
+
let value;
|
|
442
|
+
try {
|
|
443
|
+
value = run();
|
|
444
|
+
} catch (error) {
|
|
445
|
+
grouping = null;
|
|
446
|
+
if (group.entry) cancel(group.entry, capability);
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
grouping = null;
|
|
450
|
+
if (group.entry) {
|
|
451
|
+
staged.emit(group.entry);
|
|
452
|
+
send(group.entry);
|
|
453
|
+
}
|
|
454
|
+
return value;
|
|
455
|
+
},
|
|
456
|
+
sendHolds: () => sendHolds(),
|
|
457
|
+
takeObjectNumber: () => handleFor(capability).objectNumbers.take(),
|
|
458
|
+
async reserveObjectNumbers(count) {
|
|
459
|
+
try {
|
|
460
|
+
await handleFor(capability).objectNumbers.reserve(count);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
throw toPluginError(capability, error);
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
pending: () => entries,
|
|
466
|
+
hasPending: () => entries.length > 0,
|
|
467
|
+
whenSettled: () => whenSettled(),
|
|
468
|
+
onStaged: staged.on,
|
|
469
|
+
onSettled: settled.on
|
|
470
|
+
});
|
|
471
|
+
const whenSettled = async () => {
|
|
472
|
+
await Promise.allSettled(entries.filter((entry) => !entry.held).map((entry) => entry.result));
|
|
473
|
+
};
|
|
474
|
+
return {
|
|
475
|
+
shown: () => entries,
|
|
476
|
+
version: () => version,
|
|
477
|
+
find: (opId) => entries.find((entry) => entry.opId === opId),
|
|
478
|
+
forPlugin,
|
|
479
|
+
settle() {
|
|
480
|
+
sendHolds();
|
|
481
|
+
return whenSettled();
|
|
482
|
+
},
|
|
483
|
+
close() {
|
|
484
|
+
if (closed) return;
|
|
485
|
+
closed = true;
|
|
486
|
+
const open = entries;
|
|
487
|
+
entries = [];
|
|
488
|
+
version += 1;
|
|
489
|
+
for (const entry of open) {
|
|
490
|
+
entry.answered = true;
|
|
491
|
+
entry.reject(new PluginError("instance-closed", entry.capability, "the document closed"));
|
|
492
|
+
}
|
|
493
|
+
staged.dispose();
|
|
494
|
+
settled.dispose();
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/clock.ts
|
|
500
|
+
/** Timers only, no frames: the kernel's default. */
|
|
501
|
+
const timerClock = {
|
|
502
|
+
now: () => performance.now(),
|
|
503
|
+
after(ms, run) {
|
|
504
|
+
const timer = setTimeout(run, ms);
|
|
505
|
+
return () => clearTimeout(timer);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
/**
|
|
509
|
+
* The clock of one plugin instance, over the host's. What it schedules is
|
|
510
|
+
* cancelled when `lifetime` aborts (the instance closes), and nothing it
|
|
511
|
+
* schedules after that runs.
|
|
512
|
+
*/
|
|
513
|
+
function instanceClock(host, lifetime) {
|
|
514
|
+
const pending = /* @__PURE__ */ new Set();
|
|
515
|
+
lifetime.addEventListener("abort", () => {
|
|
516
|
+
for (const cancel of [...pending]) cancel();
|
|
517
|
+
}, { once: true });
|
|
518
|
+
/** Schedules `run` with the host and keeps it until it runs or is cancelled. */
|
|
519
|
+
const own = (schedule, run) => {
|
|
520
|
+
if (lifetime.aborted) return () => {};
|
|
521
|
+
let done = false;
|
|
522
|
+
let cancelWithHost = () => {};
|
|
523
|
+
const cancel = () => {
|
|
524
|
+
if (done) return;
|
|
525
|
+
done = true;
|
|
526
|
+
pending.delete(cancel);
|
|
527
|
+
cancelWithHost();
|
|
528
|
+
};
|
|
529
|
+
pending.add(cancel);
|
|
530
|
+
cancelWithHost = schedule((...args) => {
|
|
531
|
+
if (done) return;
|
|
532
|
+
done = true;
|
|
533
|
+
pending.delete(cancel);
|
|
534
|
+
run(...args);
|
|
535
|
+
});
|
|
536
|
+
return cancel;
|
|
537
|
+
};
|
|
538
|
+
const nextFrameOfHost = (run) => host.nextFrame ? host.nextFrame(run) : host.after(0, () => run(host.now()));
|
|
539
|
+
return {
|
|
540
|
+
after: (ms, run) => own((fire) => host.after(ms, fire), run),
|
|
541
|
+
nextFrame: (run) => own(nextFrameOfHost, run),
|
|
542
|
+
hasFrames: host.nextFrame !== void 0
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/store.ts
|
|
547
|
+
function createStore(report = console.error) {
|
|
548
|
+
let core = {
|
|
549
|
+
documents: {},
|
|
550
|
+
pending: {},
|
|
551
|
+
order: [],
|
|
552
|
+
activeId: null
|
|
553
|
+
};
|
|
554
|
+
/** Live slice states, as adapters read them (`getState().plugins`). */
|
|
555
|
+
const states = {};
|
|
556
|
+
const leases = /* @__PURE__ */ new Map();
|
|
557
|
+
const changeListeners = /* @__PURE__ */ new Set();
|
|
558
|
+
let leaseCounter = 0;
|
|
559
|
+
const guarded = (fn) => {
|
|
560
|
+
try {
|
|
561
|
+
fn();
|
|
562
|
+
} catch (error) {
|
|
563
|
+
report(error);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
let emitting = false;
|
|
567
|
+
let pending = false;
|
|
568
|
+
let holds = 0;
|
|
569
|
+
let missed = false;
|
|
570
|
+
const emitChange = () => {
|
|
571
|
+
if (holds > 0) {
|
|
572
|
+
missed = true;
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (emitting) {
|
|
576
|
+
pending = true;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
emitting = true;
|
|
580
|
+
try {
|
|
581
|
+
do {
|
|
582
|
+
pending = false;
|
|
583
|
+
changeListeners.forEach((listener) => guarded(listener));
|
|
584
|
+
} while (pending);
|
|
585
|
+
} finally {
|
|
586
|
+
emitting = false;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
function lease(key, initial, instanceId = `${key}#${++leaseCounter}`) {
|
|
590
|
+
leases.get(key)?.revoke();
|
|
591
|
+
const cell = {
|
|
592
|
+
live: true,
|
|
593
|
+
state: initial
|
|
594
|
+
};
|
|
595
|
+
const changed = createEventHook(report);
|
|
596
|
+
states[key] = initial;
|
|
597
|
+
const handle = {
|
|
598
|
+
key,
|
|
599
|
+
instanceId,
|
|
600
|
+
get live() {
|
|
601
|
+
return cell.live;
|
|
602
|
+
},
|
|
603
|
+
read: () => cell.state,
|
|
604
|
+
write(next) {
|
|
605
|
+
if (!cell.live) return false;
|
|
606
|
+
if (next === cell.state) return true;
|
|
607
|
+
const previous = cell.state;
|
|
608
|
+
cell.state = next;
|
|
609
|
+
states[key] = next;
|
|
610
|
+
emitChange();
|
|
611
|
+
changed.emit({
|
|
612
|
+
next,
|
|
613
|
+
previous
|
|
614
|
+
});
|
|
615
|
+
return true;
|
|
616
|
+
},
|
|
617
|
+
onChange: changed.on,
|
|
618
|
+
revoke() {
|
|
619
|
+
if (!cell.live) return;
|
|
620
|
+
cell.live = false;
|
|
621
|
+
changed.dispose();
|
|
622
|
+
if (leases.get(key) === handle) {
|
|
623
|
+
leases.delete(key);
|
|
624
|
+
delete states[key];
|
|
625
|
+
emitChange();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
leases.set(key, handle);
|
|
630
|
+
return handle;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
lease,
|
|
634
|
+
getCore: () => core,
|
|
635
|
+
getState: () => ({
|
|
636
|
+
core,
|
|
637
|
+
plugins: states
|
|
638
|
+
}),
|
|
639
|
+
setCore(patch) {
|
|
640
|
+
core = {
|
|
641
|
+
...core,
|
|
642
|
+
...patch
|
|
643
|
+
};
|
|
644
|
+
emitChange();
|
|
645
|
+
},
|
|
646
|
+
notify: () => emitChange(),
|
|
647
|
+
hold() {
|
|
648
|
+
holds += 1;
|
|
649
|
+
let released = false;
|
|
650
|
+
return () => {
|
|
651
|
+
if (released) return;
|
|
652
|
+
released = true;
|
|
653
|
+
holds -= 1;
|
|
654
|
+
if (holds > 0 || !missed) return;
|
|
655
|
+
missed = false;
|
|
656
|
+
emitChange();
|
|
657
|
+
};
|
|
658
|
+
},
|
|
659
|
+
subscribe(listener) {
|
|
660
|
+
changeListeners.add(listener);
|
|
661
|
+
return () => void changeListeners.delete(listener);
|
|
662
|
+
},
|
|
663
|
+
destroy() {
|
|
664
|
+
core = {
|
|
665
|
+
documents: {},
|
|
666
|
+
pending: {},
|
|
667
|
+
order: [],
|
|
668
|
+
activeId: null
|
|
669
|
+
};
|
|
670
|
+
for (const held of [...leases.values()]) held.revoke();
|
|
671
|
+
for (const key of Object.keys(states)) delete states[key];
|
|
672
|
+
changeListeners.clear();
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
//#endregion
|
|
677
|
+
//#region src/env.ts
|
|
678
|
+
function isDev() {
|
|
679
|
+
try {
|
|
680
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
681
|
+
} catch {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/guarded-handle.ts
|
|
687
|
+
const isPromiseLike = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
688
|
+
const isPlainWrappable = (value) => value !== null && typeof value === "object" && !isPromiseLike(value) && !(value instanceof ArrayBuffer) && !ArrayBuffer.isView(value) && !(value instanceof Date) && !(value instanceof RegExp) && !(value instanceof Map) && !(value instanceof Set) && !Array.isArray(value);
|
|
689
|
+
/**
|
|
690
|
+
* The guarded document handle a controller sees as `ctx.doc`. Every call made
|
|
691
|
+
* through it is:
|
|
692
|
+
*
|
|
693
|
+
* - refused (rejected `instance-closed`) once the instance has closed,
|
|
694
|
+
* - raced against the instance lifetime: a call that is still pending when
|
|
695
|
+
* the instance closes rejects `instance-closed`, its late value is dropped,
|
|
696
|
+
* and an abortable engine call is aborted so worker-side work stops,
|
|
697
|
+
* - mapped through `toPluginError`, so a controller sees one error vocabulary.
|
|
698
|
+
*
|
|
699
|
+
* Nested service objects (`doc.annotations`, `doc.page(ref).render`) are
|
|
700
|
+
* wrapped lazily and cached; synchronous reads (`doc.security.allows`) pass
|
|
701
|
+
* straight through. Methods run with the raw target as `this`, so classes
|
|
702
|
+
* with private fields keep working. The returned promises keep an `abort()`
|
|
703
|
+
* when the engine's promise had one, so caller-side cancellation still works.
|
|
704
|
+
*/
|
|
705
|
+
function guardHandle(handle, lifetime, capability) {
|
|
706
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
707
|
+
const closedError = () => new PluginError("instance-closed", capability, `document instance ${lifetime.instanceId} was closed`);
|
|
708
|
+
const guardPromise = (pending) => {
|
|
709
|
+
const abortable = pending;
|
|
710
|
+
const guarded = new Promise((resolve, reject) => {
|
|
711
|
+
const onAbort = () => {
|
|
712
|
+
abortable.abort?.(lifetime.signal.reason);
|
|
713
|
+
reject(closedError());
|
|
714
|
+
};
|
|
715
|
+
if (lifetime.signal.aborted) onAbort();
|
|
716
|
+
else lifetime.signal.addEventListener("abort", onAbort, { once: true });
|
|
717
|
+
Promise.resolve(pending).then((value) => {
|
|
718
|
+
lifetime.signal.removeEventListener("abort", onAbort);
|
|
719
|
+
if (!lifetime.signal.aborted) resolve(value);
|
|
720
|
+
}, (error) => {
|
|
721
|
+
lifetime.signal.removeEventListener("abort", onAbort);
|
|
722
|
+
if (!lifetime.signal.aborted) reject(toPluginError(capability, error));
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
if (typeof abortable.abort === "function") Object.defineProperty(guarded, "abort", {
|
|
726
|
+
value: (reason) => abortable.abort(reason),
|
|
727
|
+
enumerable: false
|
|
728
|
+
});
|
|
729
|
+
return guarded;
|
|
730
|
+
};
|
|
731
|
+
const wrapFunction = (fn, target) => function guardedCall(...args) {
|
|
732
|
+
let result;
|
|
733
|
+
try {
|
|
734
|
+
result = fn.apply(target, args);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
throw toPluginError(capability, error);
|
|
737
|
+
}
|
|
738
|
+
if (isPromiseLike(result)) return guardPromise(result);
|
|
739
|
+
return wrapValue(result, target);
|
|
740
|
+
};
|
|
741
|
+
const wrapValue = (value, target) => {
|
|
742
|
+
if (typeof value === "function") return wrapFunction(value, target);
|
|
743
|
+
if (isPlainWrappable(value)) return wrapObject(value);
|
|
744
|
+
return value;
|
|
745
|
+
};
|
|
746
|
+
const wrapObject = (target) => {
|
|
747
|
+
let proxy = cache.get(target);
|
|
748
|
+
if (!proxy) {
|
|
749
|
+
proxy = new Proxy(target, { get(proxyTarget, property) {
|
|
750
|
+
return wrapValue(Reflect.get(proxyTarget, property, proxyTarget), proxyTarget);
|
|
751
|
+
} });
|
|
752
|
+
cache.set(target, proxy);
|
|
753
|
+
}
|
|
754
|
+
return proxy;
|
|
755
|
+
};
|
|
756
|
+
return wrapObject(handle);
|
|
757
|
+
}
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/lanes.ts
|
|
760
|
+
function createLatestLane(lifetime, capability, name) {
|
|
761
|
+
let generation = 0;
|
|
762
|
+
let active = null;
|
|
763
|
+
const closed = () => new PluginError("instance-closed", capability, `${name}: instance closed`);
|
|
764
|
+
const cancelled = (id, why) => new PluginError("operation-cancelled", capability, `${name}: ${why}`, { details: {
|
|
765
|
+
runId: id,
|
|
766
|
+
reason: why
|
|
767
|
+
} });
|
|
768
|
+
return {
|
|
769
|
+
get running() {
|
|
770
|
+
return active !== null;
|
|
771
|
+
},
|
|
772
|
+
cancel(reason) {
|
|
773
|
+
active?.abort(reason ?? "cancelled");
|
|
774
|
+
active = null;
|
|
775
|
+
},
|
|
776
|
+
async run(fn, options) {
|
|
777
|
+
if (lifetime.aborted) throw closed();
|
|
778
|
+
if (options?.signal?.aborted) throw cancelled(`${name}#${generation + 1}`, "cancelled before start");
|
|
779
|
+
const mine = ++generation;
|
|
780
|
+
const id = `${name}#${mine}`;
|
|
781
|
+
const controller = new AbortController();
|
|
782
|
+
const previous = active;
|
|
783
|
+
active = controller;
|
|
784
|
+
const onLifetime = () => controller.abort("instance-closed");
|
|
785
|
+
const onCaller = () => controller.abort("cancelled");
|
|
786
|
+
lifetime.addEventListener("abort", onLifetime, { once: true });
|
|
787
|
+
options?.signal?.addEventListener("abort", onCaller, { once: true });
|
|
788
|
+
const current = () => mine === generation && !controller.signal.aborted;
|
|
789
|
+
const run = {
|
|
790
|
+
id,
|
|
791
|
+
signal: controller.signal,
|
|
792
|
+
get superseded() {
|
|
793
|
+
return mine !== generation;
|
|
794
|
+
},
|
|
795
|
+
commit(commitFn) {
|
|
796
|
+
if (!current()) return false;
|
|
797
|
+
commitFn();
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
try {
|
|
802
|
+
previous?.abort("superseded");
|
|
803
|
+
if (!current()) throw cancelled(id, "superseded");
|
|
804
|
+
const value = await fn(run);
|
|
805
|
+
if (!current()) throw cancelled(id, String(controller.signal.reason ?? "superseded"));
|
|
806
|
+
return value;
|
|
807
|
+
} catch (error) {
|
|
808
|
+
if (lifetime.aborted) throw closed();
|
|
809
|
+
if (!current()) throw cancelled(id, String(controller.signal.reason ?? "superseded"));
|
|
810
|
+
throw toPluginError(capability, error);
|
|
811
|
+
} finally {
|
|
812
|
+
lifetime.removeEventListener("abort", onLifetime);
|
|
813
|
+
options?.signal?.removeEventListener("abort", onCaller);
|
|
814
|
+
if (active === controller) active = null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region src/mirror.ts
|
|
821
|
+
const RELOAD = Symbol("mirror.reload");
|
|
822
|
+
/** Ask the mirror to reload instead of applying the event. */
|
|
823
|
+
function reload(options) {
|
|
824
|
+
return { [RELOAD]: options?.pages ?? "all" };
|
|
825
|
+
}
|
|
826
|
+
const isReload = (value) => typeof value === "object" && value !== null && RELOAD in value;
|
|
827
|
+
/** Events that invalidate every mirror: the stream skipped events, or the document moved to new bytes. */
|
|
828
|
+
const isResync = (event) => event.type === "stream.desynced" || event.type === "document.versioned";
|
|
829
|
+
const statusOfFailure = (error) => isPluginError(error, "permission-denied") ? "forbidden" : "error";
|
|
830
|
+
/** The `opId` of the change an event belongs to, or null for an event of none. */
|
|
831
|
+
const changeIdOf = (event) => "origin" in event ? event.origin.tx?.id ?? null : null;
|
|
832
|
+
/** The `opId` of the change whose last event this is, or null. */
|
|
833
|
+
const lastEventOf = (event) => {
|
|
834
|
+
const tx = "origin" in event ? event.origin.tx : void 0;
|
|
835
|
+
return tx && tx.index === tx.count - 1 ? tx.id : null;
|
|
836
|
+
};
|
|
837
|
+
/**
|
|
838
|
+
* The changes of this session whose events a mirror folded, every one of
|
|
839
|
+
* them: the mirror holds their answer, so its view stops replaying them, even
|
|
840
|
+
* before the answer itself arrives. An entry goes once its change left the
|
|
841
|
+
* queue.
|
|
842
|
+
*/
|
|
843
|
+
function createFoldedChanges(changes) {
|
|
844
|
+
const folded = /* @__PURE__ */ new Set();
|
|
845
|
+
return {
|
|
846
|
+
has: (opId) => folded.has(opId),
|
|
847
|
+
/** The event folded: when it was a change's last, the change is held. True when that's new. */
|
|
848
|
+
add(event) {
|
|
849
|
+
const opId = lastEventOf(event);
|
|
850
|
+
if (opId === null || folded.has(opId) || !changes.find(opId)) return false;
|
|
851
|
+
for (const each of folded) if (!changes.find(each)) folded.delete(each);
|
|
852
|
+
folded.add(opId);
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* The changes a view shows: the queue's, but those whose events the mirror
|
|
859
|
+
* folded already, and the answered ones a mirror keeps (it asked to read
|
|
860
|
+
* again), each once.
|
|
861
|
+
*/
|
|
862
|
+
function showingWith(changes, kept, folded) {
|
|
863
|
+
const shown = new Map(changes.shown().filter((change) => !folded.has(change.opId)).map((change) => [change.opId, change]));
|
|
864
|
+
for (const { change } of kept) shown.set(change.opId, change);
|
|
865
|
+
return [...shown.values()];
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Replay changes over a value, in staging order: the shown ones and the
|
|
869
|
+
* answered ones a mirror still shows. A `predict` that throws is reported,
|
|
870
|
+
* and the value shows the truth.
|
|
871
|
+
*/
|
|
872
|
+
function replay(value, changes, predict, report) {
|
|
873
|
+
const ordered = [...changes].sort((left, right) => left.seq - right.seq);
|
|
874
|
+
let next = value;
|
|
875
|
+
try {
|
|
876
|
+
for (const change of ordered) for (const op of change.shows) next = predict(next, op);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
report(error);
|
|
879
|
+
return value;
|
|
880
|
+
}
|
|
881
|
+
return next;
|
|
882
|
+
}
|
|
883
|
+
function createMirror(spec, env) {
|
|
884
|
+
const cell = env.cell(`mirror/${spec.name}`, {
|
|
885
|
+
value: spec.initial(),
|
|
886
|
+
status: "idle"
|
|
887
|
+
});
|
|
888
|
+
let started = false;
|
|
889
|
+
let running = null;
|
|
890
|
+
const pageReloads = /* @__PURE__ */ new Set();
|
|
891
|
+
let loadAgain = false;
|
|
892
|
+
/** Non-null while a full load runs: the events to replay after it lands. */
|
|
893
|
+
let queued = null;
|
|
894
|
+
/** How many full loads have started: a load clears what was kept before it started. */
|
|
895
|
+
let fullLoads = 0;
|
|
896
|
+
/**
|
|
897
|
+
* Changes this mirror still shows though the engine may have answered them: a
|
|
898
|
+
* fold asked to read again, and the read hasn't landed. `after`: the full
|
|
899
|
+
* loads started before; a later full load that lands holds the change too.
|
|
900
|
+
*/
|
|
901
|
+
const kept = /* @__PURE__ */ new Map();
|
|
902
|
+
let keptVersion = 0;
|
|
903
|
+
const folded = createFoldedChanges(env.changes);
|
|
904
|
+
/** Keep the change an event belongs to until a read lands, when it's one of ours. */
|
|
905
|
+
const keepUntilRead = (event, after) => {
|
|
906
|
+
const opId = changeIdOf(event);
|
|
907
|
+
const change = opId !== null && spec.predict ? env.changes.find(opId) : void 0;
|
|
908
|
+
if (!change) return null;
|
|
909
|
+
kept.set(change.opId, {
|
|
910
|
+
change,
|
|
911
|
+
after
|
|
912
|
+
});
|
|
913
|
+
keptVersion += 1;
|
|
914
|
+
return change.opId;
|
|
915
|
+
};
|
|
916
|
+
const release = (opIds) => {
|
|
917
|
+
let any = false;
|
|
918
|
+
for (const opId of opIds) any = kept.delete(opId) || any;
|
|
919
|
+
if (!any) return;
|
|
920
|
+
keptVersion += 1;
|
|
921
|
+
env.notify();
|
|
922
|
+
};
|
|
923
|
+
const releaseLoadedBy = (load) => release([...kept].filter(([, entry]) => entry.after < load).map(([opId]) => opId));
|
|
924
|
+
let viewMemo = null;
|
|
925
|
+
const view = () => {
|
|
926
|
+
const value = cell.read().value;
|
|
927
|
+
if (!spec.predict) return value;
|
|
928
|
+
const version = env.changes.version();
|
|
929
|
+
if (viewMemo && viewMemo.value === value && viewMemo.version === version && viewMemo.kept === keptVersion) return viewMemo.view;
|
|
930
|
+
const next = replay(value, showingWith(env.changes, kept.values(), folded), spec.predict, env.report);
|
|
931
|
+
viewMemo = {
|
|
932
|
+
value,
|
|
933
|
+
version,
|
|
934
|
+
kept: keptVersion,
|
|
935
|
+
view: next
|
|
936
|
+
};
|
|
937
|
+
return next;
|
|
938
|
+
};
|
|
939
|
+
const announce = (change) => {
|
|
940
|
+
try {
|
|
941
|
+
spec.changed?.(change);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
env.report(error);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
const commit = (next, change) => {
|
|
947
|
+
const previous = cell.read().value;
|
|
948
|
+
if (next === previous) return;
|
|
949
|
+
if (!cell.write({
|
|
950
|
+
...cell.read(),
|
|
951
|
+
value: next
|
|
952
|
+
})) return;
|
|
953
|
+
announce({
|
|
954
|
+
...change,
|
|
955
|
+
previous,
|
|
956
|
+
next
|
|
957
|
+
});
|
|
958
|
+
};
|
|
959
|
+
const reloadInBackground = () => {
|
|
960
|
+
refresh().catch(() => {});
|
|
961
|
+
};
|
|
962
|
+
const reloadPages = async (pages, keptFor) => {
|
|
963
|
+
try {
|
|
964
|
+
const merge = await spec.loadPages(env.doc, pages, env.lifetime);
|
|
965
|
+
if (!cell.live) return;
|
|
966
|
+
const previous = cell.read().value;
|
|
967
|
+
const next = merge(previous);
|
|
968
|
+
cell.write({
|
|
969
|
+
...cell.read(),
|
|
970
|
+
value: next
|
|
971
|
+
});
|
|
972
|
+
announce({
|
|
973
|
+
cause: "load",
|
|
974
|
+
event: null,
|
|
975
|
+
pages,
|
|
976
|
+
previous,
|
|
977
|
+
next
|
|
978
|
+
});
|
|
979
|
+
if (keptFor !== null) release([keptFor]);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
if (isPluginError(error, "instance-closed") || !cell.live) return;
|
|
982
|
+
cell.write({
|
|
983
|
+
...cell.read(),
|
|
984
|
+
status: statusOfFailure(error)
|
|
985
|
+
});
|
|
986
|
+
env.report(error);
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
const applyEvent = (event) => {
|
|
990
|
+
let result;
|
|
991
|
+
try {
|
|
992
|
+
result = spec.fold(cell.read().value, event);
|
|
993
|
+
} catch (error) {
|
|
994
|
+
env.report(error);
|
|
995
|
+
if (!spec.onDemand || started) reloadInBackground();
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (isReload(result)) {
|
|
999
|
+
if (spec.onDemand && !started) return;
|
|
1000
|
+
const pages = result[RELOAD];
|
|
1001
|
+
const keptFor = keepUntilRead(event, fullLoads);
|
|
1002
|
+
if (pages === "all" || !spec.loadPages) reloadInBackground();
|
|
1003
|
+
else {
|
|
1004
|
+
const reloading = reloadPages(pages, keptFor).finally(() => pageReloads.delete(reloading));
|
|
1005
|
+
pageReloads.add(reloading);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (spec.predict && folded.add(event)) keptVersion += 1;
|
|
1010
|
+
commit(result, {
|
|
1011
|
+
cause: "event",
|
|
1012
|
+
event,
|
|
1013
|
+
pages: null
|
|
1014
|
+
});
|
|
1015
|
+
};
|
|
1016
|
+
env.onDocumentEvent((event) => {
|
|
1017
|
+
if (!cell.live) return;
|
|
1018
|
+
if (isResync(event)) {
|
|
1019
|
+
if (started) reloadInBackground();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (queued) {
|
|
1023
|
+
queued.push(event);
|
|
1024
|
+
keepUntilRead(event, fullLoads - 1);
|
|
1025
|
+
} else applyEvent(event);
|
|
1026
|
+
});
|
|
1027
|
+
const loadOnce = async () => {
|
|
1028
|
+
if (spec.readable && !spec.readable()) {
|
|
1029
|
+
cell.write({
|
|
1030
|
+
...cell.read(),
|
|
1031
|
+
status: "forbidden"
|
|
1032
|
+
});
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (cell.read().status !== "ready") cell.write({
|
|
1036
|
+
...cell.read(),
|
|
1037
|
+
status: "loading"
|
|
1038
|
+
});
|
|
1039
|
+
const load = ++fullLoads;
|
|
1040
|
+
queued = [];
|
|
1041
|
+
let loaded;
|
|
1042
|
+
try {
|
|
1043
|
+
loaded = await spec.load(env.doc, env.lifetime);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
const pending = queued ?? [];
|
|
1046
|
+
queued = null;
|
|
1047
|
+
if (!cell.live || isPluginError(error, "instance-closed")) return;
|
|
1048
|
+
cell.write({
|
|
1049
|
+
...cell.read(),
|
|
1050
|
+
status: statusOfFailure(error)
|
|
1051
|
+
});
|
|
1052
|
+
for (const event of pending) applyEvent(event);
|
|
1053
|
+
throw error;
|
|
1054
|
+
}
|
|
1055
|
+
const pending = queued ?? [];
|
|
1056
|
+
queued = null;
|
|
1057
|
+
if (!cell.live) return;
|
|
1058
|
+
const previous = cell.read().value;
|
|
1059
|
+
cell.write({
|
|
1060
|
+
value: loaded.value,
|
|
1061
|
+
status: "ready"
|
|
1062
|
+
});
|
|
1063
|
+
announce({
|
|
1064
|
+
cause: "load",
|
|
1065
|
+
event: null,
|
|
1066
|
+
pages: "all",
|
|
1067
|
+
previous,
|
|
1068
|
+
next: loaded.value
|
|
1069
|
+
});
|
|
1070
|
+
const cursor = loaded.cursor ?? null;
|
|
1071
|
+
for (const event of pending) {
|
|
1072
|
+
const serverId = "origin" in event ? event.origin.serverId : null;
|
|
1073
|
+
if (cursor !== null && serverId !== null && serverId <= cursor) continue;
|
|
1074
|
+
applyEvent(event);
|
|
1075
|
+
}
|
|
1076
|
+
releaseLoadedBy(load);
|
|
1077
|
+
};
|
|
1078
|
+
function refresh() {
|
|
1079
|
+
started = true;
|
|
1080
|
+
if (running) {
|
|
1081
|
+
loadAgain = true;
|
|
1082
|
+
return running;
|
|
1083
|
+
}
|
|
1084
|
+
running = (async () => {
|
|
1085
|
+
let failure = null;
|
|
1086
|
+
do {
|
|
1087
|
+
loadAgain = false;
|
|
1088
|
+
try {
|
|
1089
|
+
await loadOnce();
|
|
1090
|
+
failure = null;
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
failure = error;
|
|
1093
|
+
}
|
|
1094
|
+
} while (loadAgain && cell.live);
|
|
1095
|
+
if (failure !== null) throw failure;
|
|
1096
|
+
})().finally(() => {
|
|
1097
|
+
running = null;
|
|
1098
|
+
});
|
|
1099
|
+
return running;
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
mirror: {
|
|
1103
|
+
get: () => cell.read().value,
|
|
1104
|
+
view,
|
|
1105
|
+
getStatus: () => cell.read().status,
|
|
1106
|
+
ensureLoaded: () => {
|
|
1107
|
+
if (running) return running;
|
|
1108
|
+
if (cell.read().status === "ready") return Promise.resolve();
|
|
1109
|
+
return refresh();
|
|
1110
|
+
},
|
|
1111
|
+
refresh,
|
|
1112
|
+
async settled() {
|
|
1113
|
+
while (running || pageReloads.size > 0) await Promise.allSettled([running, ...pageReloads]);
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
start() {
|
|
1117
|
+
if (!started && !spec.onDemand) reloadInBackground();
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
//#endregion
|
|
1122
|
+
//#region src/page-mirror.ts
|
|
1123
|
+
/**
|
|
1124
|
+
* Page mirrors: engine data that is loaded page by page, on demand, and kept
|
|
1125
|
+
* current from the document event stream.
|
|
1126
|
+
*
|
|
1127
|
+
* A page is `idle` until something calls `ensureLoaded(page)`. Once loaded it
|
|
1128
|
+
* stays current: an event the spec maps to that page either folds into the
|
|
1129
|
+
* page's value or re-reads it. Every read carries a per-page epoch, so a read
|
|
1130
|
+
* that started before a newer one can never overwrite it. Loads for the same
|
|
1131
|
+
* page are shared while in flight. Deleted pages are dropped, and
|
|
1132
|
+
* `stream.desynced` / `document.versioned` re-read every loaded page.
|
|
1133
|
+
*
|
|
1134
|
+
* `view(page)` is the page's value with this session's pending changes on
|
|
1135
|
+
* top (`predict`), as for whole-document mirrors (mirror.ts): an answered
|
|
1136
|
+
* change that made a page read again stays in that page's view until the
|
|
1137
|
+
* read lands.
|
|
1138
|
+
*/
|
|
1139
|
+
const IDLE = "idle";
|
|
1140
|
+
function createPageMirror(spec, env) {
|
|
1141
|
+
const cell = env.cell(`page-mirror/${spec.name}`, {});
|
|
1142
|
+
const epochs = /* @__PURE__ */ new Map();
|
|
1143
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1144
|
+
/**
|
|
1145
|
+
* Per page: answered changes the page still shows, because an event of
|
|
1146
|
+
* theirs made it read again. `epoch`: the page's epoch then; a later read
|
|
1147
|
+
* that lands clears them.
|
|
1148
|
+
*/
|
|
1149
|
+
const kept = /* @__PURE__ */ new Map();
|
|
1150
|
+
let keptVersion = 0;
|
|
1151
|
+
/** Changes every event of which reached the pages: each page holds its part, or keeps the change. */
|
|
1152
|
+
const folded = createFoldedChanges(env.changes);
|
|
1153
|
+
const keepUntilRead = (key, event) => {
|
|
1154
|
+
const opId = changeIdOf(event);
|
|
1155
|
+
const change = opId !== null && spec.predict ? env.changes.find(opId) : void 0;
|
|
1156
|
+
if (!change) return;
|
|
1157
|
+
let page = kept.get(key);
|
|
1158
|
+
if (!page) kept.set(key, page = /* @__PURE__ */ new Map());
|
|
1159
|
+
page.set(change.opId, {
|
|
1160
|
+
change,
|
|
1161
|
+
epoch: epochs.get(key) ?? 0
|
|
1162
|
+
});
|
|
1163
|
+
keptVersion += 1;
|
|
1164
|
+
};
|
|
1165
|
+
const releasePage = (key, epoch) => {
|
|
1166
|
+
const page = kept.get(key);
|
|
1167
|
+
if (!page) return;
|
|
1168
|
+
const before = page.size;
|
|
1169
|
+
for (const [opId, entry] of page) if (entry.epoch < epoch) page.delete(opId);
|
|
1170
|
+
if (page.size === 0) kept.delete(key);
|
|
1171
|
+
if (page.size === before) return;
|
|
1172
|
+
keptVersion += 1;
|
|
1173
|
+
env.notify();
|
|
1174
|
+
};
|
|
1175
|
+
const views = /* @__PURE__ */ new Map();
|
|
1176
|
+
const view = (page) => {
|
|
1177
|
+
const key = encodePageKey(page);
|
|
1178
|
+
const value = entryOf(key)?.value;
|
|
1179
|
+
if (value === void 0 || !spec.predict) return value;
|
|
1180
|
+
const version = env.changes.version();
|
|
1181
|
+
const memo = views.get(key);
|
|
1182
|
+
if (memo && memo.value === value && memo.version === version && memo.kept === keptVersion) return memo.view;
|
|
1183
|
+
const predict = spec.predict;
|
|
1184
|
+
const next = replay(value, showingWith(env.changes, kept.get(key)?.values() ?? [], folded), (current, op) => predict(current, op, page), env.report);
|
|
1185
|
+
views.set(key, {
|
|
1186
|
+
value,
|
|
1187
|
+
version,
|
|
1188
|
+
kept: keptVersion,
|
|
1189
|
+
view: next
|
|
1190
|
+
});
|
|
1191
|
+
return next;
|
|
1192
|
+
};
|
|
1193
|
+
const entryOf = (key) => cell.read()[key];
|
|
1194
|
+
const setEntry = (key, entry) => {
|
|
1195
|
+
const { [key]: _replaced, ...rest } = cell.read();
|
|
1196
|
+
return cell.write(entry ? {
|
|
1197
|
+
...rest,
|
|
1198
|
+
[key]: entry
|
|
1199
|
+
} : rest);
|
|
1200
|
+
};
|
|
1201
|
+
const announce = (change) => {
|
|
1202
|
+
if (change.cause !== "load" && change.previous === change.next) return;
|
|
1203
|
+
try {
|
|
1204
|
+
spec.changed?.(change);
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
env.report(error);
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
const load = (page) => {
|
|
1210
|
+
const key = encodePageKey(page);
|
|
1211
|
+
const epoch = (epochs.get(key) ?? 0) + 1;
|
|
1212
|
+
epochs.set(key, epoch);
|
|
1213
|
+
const before = entryOf(key);
|
|
1214
|
+
if (before?.status !== "ready") setEntry(key, {
|
|
1215
|
+
page,
|
|
1216
|
+
status: "loading",
|
|
1217
|
+
value: before?.value
|
|
1218
|
+
});
|
|
1219
|
+
const isCurrent = () => cell.live && epochs.get(key) === epoch;
|
|
1220
|
+
const request = spec.load(env.doc, page, env.lifetime).then((value) => {
|
|
1221
|
+
if (!isCurrent()) return;
|
|
1222
|
+
const previous = entryOf(key)?.value;
|
|
1223
|
+
setEntry(key, {
|
|
1224
|
+
page,
|
|
1225
|
+
status: "ready",
|
|
1226
|
+
value
|
|
1227
|
+
});
|
|
1228
|
+
announce({
|
|
1229
|
+
page,
|
|
1230
|
+
cause: "load",
|
|
1231
|
+
event: null,
|
|
1232
|
+
previous,
|
|
1233
|
+
next: value
|
|
1234
|
+
});
|
|
1235
|
+
if (kept.has(key)) releasePage(key, epoch);
|
|
1236
|
+
}, (error) => {
|
|
1237
|
+
if (!isCurrent() || isPluginError(error, "instance-closed")) return;
|
|
1238
|
+
setEntry(key, {
|
|
1239
|
+
page,
|
|
1240
|
+
status: statusOfFailure(error),
|
|
1241
|
+
value: entryOf(key)?.value
|
|
1242
|
+
});
|
|
1243
|
+
throw error;
|
|
1244
|
+
}).finally(() => {
|
|
1245
|
+
if (inFlight.get(key) === request) inFlight.delete(key);
|
|
1246
|
+
});
|
|
1247
|
+
inFlight.set(key, request);
|
|
1248
|
+
return request;
|
|
1249
|
+
};
|
|
1250
|
+
const reloadInBackground = (page) => {
|
|
1251
|
+
load(page).catch(() => {});
|
|
1252
|
+
};
|
|
1253
|
+
const loadedPages = () => Object.values(cell.read()).filter((entry) => entry.status !== IDLE).map((entry) => entry.page);
|
|
1254
|
+
env.onDocumentEvent((event) => {
|
|
1255
|
+
if (!cell.live) return;
|
|
1256
|
+
foldEvent(event);
|
|
1257
|
+
if (spec.predict && folded.add(event)) keptVersion += 1;
|
|
1258
|
+
});
|
|
1259
|
+
/** One event, into every loaded page it concerns. */
|
|
1260
|
+
function foldEvent(event) {
|
|
1261
|
+
if (isResync(event)) {
|
|
1262
|
+
for (const page of loadedPages()) reloadInBackground(page);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (event.type === "pages.deleted") {
|
|
1266
|
+
for (const page of event.pages) {
|
|
1267
|
+
const key = encodePageKey(page);
|
|
1268
|
+
const before = entryOf(key);
|
|
1269
|
+
if (!before) continue;
|
|
1270
|
+
epochs.set(key, (epochs.get(key) ?? 0) + 1);
|
|
1271
|
+
kept.delete(key);
|
|
1272
|
+
views.delete(key);
|
|
1273
|
+
setEntry(key, null);
|
|
1274
|
+
announce({
|
|
1275
|
+
page,
|
|
1276
|
+
cause: "drop",
|
|
1277
|
+
event,
|
|
1278
|
+
previous: before.value,
|
|
1279
|
+
next: void 0
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const affected = spec.affected(event);
|
|
1285
|
+
if (affected === null) return;
|
|
1286
|
+
const pages = affected === "all" ? loadedPages() : affected;
|
|
1287
|
+
for (const page of pages) {
|
|
1288
|
+
const key = encodePageKey(page);
|
|
1289
|
+
const entry = entryOf(key);
|
|
1290
|
+
if (!entry || entry.status === IDLE) continue;
|
|
1291
|
+
if (!spec.fold || entry.status !== "ready") {
|
|
1292
|
+
keepUntilRead(key, event);
|
|
1293
|
+
reloadInBackground(page);
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
let next;
|
|
1297
|
+
try {
|
|
1298
|
+
next = spec.fold(entry.value, event, page);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
env.report(error);
|
|
1301
|
+
next = "reload";
|
|
1302
|
+
}
|
|
1303
|
+
if (next === "reload") {
|
|
1304
|
+
keepUntilRead(key, event);
|
|
1305
|
+
reloadInBackground(page);
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (next === entry.value) continue;
|
|
1309
|
+
epochs.set(key, (epochs.get(key) ?? 0) + 1);
|
|
1310
|
+
setEntry(key, {
|
|
1311
|
+
page,
|
|
1312
|
+
status: "ready",
|
|
1313
|
+
value: next
|
|
1314
|
+
});
|
|
1315
|
+
announce({
|
|
1316
|
+
page,
|
|
1317
|
+
cause: "event",
|
|
1318
|
+
event,
|
|
1319
|
+
previous: entry.value,
|
|
1320
|
+
next
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
get: (page) => entryOf(encodePageKey(page))?.value,
|
|
1326
|
+
view,
|
|
1327
|
+
getStatus: (page) => entryOf(encodePageKey(page))?.status ?? IDLE,
|
|
1328
|
+
ensureLoaded(page) {
|
|
1329
|
+
const key = encodePageKey(page);
|
|
1330
|
+
const running = inFlight.get(key);
|
|
1331
|
+
if (running) return running;
|
|
1332
|
+
if (entryOf(key)?.status === "ready") return Promise.resolve();
|
|
1333
|
+
return load(page);
|
|
1334
|
+
},
|
|
1335
|
+
refresh(page) {
|
|
1336
|
+
if (page) return load(page);
|
|
1337
|
+
return Promise.all(loadedPages().map((loaded) => load(loaded))).then(() => void 0);
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
//#endregion
|
|
1342
|
+
//#region src/serial-queue.ts
|
|
1343
|
+
/** `capability` names the plugin in the error a skipped operation rejects with. */
|
|
1344
|
+
function createSerialQueue(capability) {
|
|
1345
|
+
let tail = Promise.resolve();
|
|
1346
|
+
const queue = (operation, options) => {
|
|
1347
|
+
const start = () => options?.signal?.aborted ? Promise.reject(new PluginError("operation-cancelled", capability, "cancelled before it started", { cause: options.signal.reason })) : operation();
|
|
1348
|
+
const result = tail.then(start, start);
|
|
1349
|
+
tail = result.then(() => void 0, () => void 0);
|
|
1350
|
+
return result;
|
|
1351
|
+
};
|
|
1352
|
+
return Object.assign(queue, { idle: () => tail });
|
|
1353
|
+
}
|
|
1354
|
+
//#endregion
|
|
1355
|
+
//#region src/cancellable.ts
|
|
1356
|
+
/**
|
|
1357
|
+
* What `ctx.cancellable` does: race an engine call against the caller's
|
|
1358
|
+
* signal. When the signal fires, the call is aborted, so the worker or the
|
|
1359
|
+
* server stops working on it (an `AbortablePromise`, and a call made through
|
|
1360
|
+
* the guarded `ctx.doc`, carry `abort()`), and the returned promise rejects
|
|
1361
|
+
* `operation-cancelled` at once instead of waiting for the engine to notice.
|
|
1362
|
+
* The guarded handle does the same for the instance's lifetime
|
|
1363
|
+
* (guarded-handle.ts). A signal that fires after the call settled changes
|
|
1364
|
+
* nothing.
|
|
1365
|
+
*/
|
|
1366
|
+
function cancellable(capability, signal, task) {
|
|
1367
|
+
if (!signal) return task;
|
|
1368
|
+
const abortable = task;
|
|
1369
|
+
return new Promise((resolve, reject) => {
|
|
1370
|
+
const onAbort = () => {
|
|
1371
|
+
abortable.abort?.(signal.reason);
|
|
1372
|
+
reject(new PluginError("operation-cancelled", capability, "operation cancelled", { cause: signal.reason }));
|
|
1373
|
+
};
|
|
1374
|
+
task.then((value) => {
|
|
1375
|
+
signal.removeEventListener("abort", onAbort);
|
|
1376
|
+
resolve(value);
|
|
1377
|
+
}, (error) => {
|
|
1378
|
+
signal.removeEventListener("abort", onAbort);
|
|
1379
|
+
reject(toPluginError(capability, error));
|
|
1380
|
+
});
|
|
1381
|
+
if (signal.aborted) onAbort();
|
|
1382
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
//#endregion
|
|
1386
|
+
//#region src/page-of.ts
|
|
1387
|
+
/**
|
|
1388
|
+
* A page argument as plugins take it, a `PageRef` or a zero-based index into
|
|
1389
|
+
* the document's page list (display order), looked up two ways. A read asks
|
|
1390
|
+
* {@link findPage} and gets null for a page that isn't there: reads run while
|
|
1391
|
+
* rendering, and a page deleted mid-render must not break a layer. A verb asks
|
|
1392
|
+
* {@link pageOf}, which refuses with `not-found`.
|
|
1393
|
+
*/
|
|
1394
|
+
/** What `ctx.getPage` reads: the page `page` names in `pages`, or null. */
|
|
1395
|
+
function findPage(pages, page) {
|
|
1396
|
+
if (typeof page === "number") return pages[page] ?? null;
|
|
1397
|
+
return pages.find((pageInfo) => pageRefsEqual(pageInfo.ref, page)) ?? null;
|
|
1398
|
+
}
|
|
1399
|
+
/** What `ctx.pageOf` resolves: the page `page` names in `pages`. Throws `not-found` when there is none. */
|
|
1400
|
+
function pageOf(pages, page, capability) {
|
|
1401
|
+
const found = findPage(pages, page);
|
|
1402
|
+
if (found) return found;
|
|
1403
|
+
throw new PluginError("not-found", capability, typeof page === "number" ? `there is no page ${page} in this document` : `page ${page.objectNumber} is not in this document`);
|
|
1404
|
+
}
|
|
1405
|
+
//#endregion
|
|
1406
|
+
//#region src/permissions.ts
|
|
1407
|
+
/**
|
|
1408
|
+
* What `ctx.allows` reads: the document session's own predicates, the ones the
|
|
1409
|
+
* engine enforces with, so a control gated on the answer never disagrees with
|
|
1410
|
+
* what the verb then does.
|
|
1411
|
+
*/
|
|
1412
|
+
function sessionAllows(security, permission) {
|
|
1413
|
+
return permission === "annotations:create" ? security.allowsAnnotation("create") : security.allows(permission);
|
|
1414
|
+
}
|
|
1415
|
+
/** What `ctx.assertAllowed` throws: a refusal that names what was missing. */
|
|
1416
|
+
function permissionDenied(capability, permission, operation) {
|
|
1417
|
+
return new PluginError("permission-denied", capability, `${operation} requires ${permission}`, { permission });
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* What a plugin throws for a form field the session may not fill in or sign:
|
|
1421
|
+
* a refusal that names the permission it would take, as the engine's does
|
|
1422
|
+
* (`fields:<action>:group=<group>`, or the broad one for a field in no group).
|
|
1423
|
+
*/
|
|
1424
|
+
function fieldPermissionDenied(capability, action, field, operation) {
|
|
1425
|
+
const permission = field.groupId !== null ? `fields:${action}:group=${field.groupId}` : action === "fill" ? "doc.forms.fill" : "doc.sign";
|
|
1426
|
+
return new PluginError("permission-denied", capability, `${operation} requires ${permission}`, { permission });
|
|
1427
|
+
}
|
|
1428
|
+
//#endregion
|
|
1429
|
+
//#region src/context.ts
|
|
1430
|
+
/** A plugin's slice key. Workspace plugins use their id; document-scoped plugins are per-document. */
|
|
1431
|
+
const sliceKey = (pluginId, documentId) => documentId ? `${pluginId}::${documentId}` : pluginId;
|
|
1432
|
+
/** `read`, inside every wrap: the first registered is the outermost. */
|
|
1433
|
+
const readWrapped = (wraps, read) => wraps.reduceRight((inner, wrap) => () => wrap(inner), read)();
|
|
1434
|
+
/**
|
|
1435
|
+
* The tokens a plugin may resolve: its declared `requires` and `optional`, its
|
|
1436
|
+
* own token, and the always-available documents token. Resolving anything else
|
|
1437
|
+
* is a lie in the dependency graph (the kernel cannot order or validate what it
|
|
1438
|
+
* does not know about), so in development it throws with the remedy.
|
|
1439
|
+
*/
|
|
1440
|
+
function declaredTokens(plugin) {
|
|
1441
|
+
const declared = /* @__PURE__ */ new Set([
|
|
1442
|
+
DocumentsToken,
|
|
1443
|
+
...plugin.requires ?? [],
|
|
1444
|
+
...plugin.optional ?? []
|
|
1445
|
+
]);
|
|
1446
|
+
if (plugin.token) declared.add(plugin.token);
|
|
1447
|
+
return declared;
|
|
1448
|
+
}
|
|
1449
|
+
const connectedHooks = /* @__PURE__ */ new WeakMap();
|
|
1450
|
+
/** Kernel-side: the plugin's `connect` ran; start what waits for it (mirror loads). */
|
|
1451
|
+
function markConnected(context) {
|
|
1452
|
+
connectedHooks.get(context)?.();
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Build the context a plugin's `create()` receives. With a `session` the
|
|
1456
|
+
* context is bound to that document: its state slice, `document()`, the
|
|
1457
|
+
* guarded `doc`, and `get()` resolving document-scoped capabilities for it.
|
|
1458
|
+
* `lifetime` aborts and `scope` disposes when the instance closes. One kind of
|
|
1459
|
+
* context serves every plugin, so `settings()` is always there and throws for
|
|
1460
|
+
* a plugin that declares none; the plugin's own types hide it (`definePlugin`).
|
|
1461
|
+
*/
|
|
1462
|
+
function createPluginContext(services, plugin, session, lifetime, scope) {
|
|
1463
|
+
const { engine, store } = services;
|
|
1464
|
+
const documentId = session?.id;
|
|
1465
|
+
const instanceId = session?.instanceId ?? `workspace:${plugin.id}`;
|
|
1466
|
+
const capability = plugin.id;
|
|
1467
|
+
const lease = (session?.leases ?? services.workspaceLeases).get(plugin);
|
|
1468
|
+
if (!lease) throw new Error(`[kernel] no state lease for plugin "${plugin.id}" (${documentId ?? "workspace"})`);
|
|
1469
|
+
const declared = declaredTokens(plugin);
|
|
1470
|
+
const guard = (token) => {
|
|
1471
|
+
if (isDev() && !plugin.resolvesAnyCapability && !declared.has(token)) throw new Error(`[kernel] plugin "${plugin.id}" resolved capability "${token.name}" without declaring it; add the token to its \`requires\` or \`optional\` list.`);
|
|
1472
|
+
};
|
|
1473
|
+
let reportedClosed = false;
|
|
1474
|
+
const reportClosed = (what) => {
|
|
1475
|
+
if (reportedClosed) return;
|
|
1476
|
+
reportedClosed = true;
|
|
1477
|
+
services.report(new PluginError("instance-closed", plugin.id, `${what} after the instance closed (${lease.instanceId}); dropped`));
|
|
1478
|
+
};
|
|
1479
|
+
let guarded = null;
|
|
1480
|
+
const doc = () => {
|
|
1481
|
+
if (!session) throw new Error(`[kernel] workspace plugin "${plugin.id}" has no bound document; use ctx.forDocument(token, documentId).`);
|
|
1482
|
+
if (!session.handle) throw new PluginError("not-ready", capability, `document ${session.id} has no engine handle yet`);
|
|
1483
|
+
return guarded ?? (guarded = guardHandle(session.handle, {
|
|
1484
|
+
signal: lifetime,
|
|
1485
|
+
instanceId
|
|
1486
|
+
}, capability));
|
|
1487
|
+
};
|
|
1488
|
+
const metaOf = () => session ? store.getCore().documents[session.id] ?? session.stagedMeta : null;
|
|
1489
|
+
const notFound = (ref) => new PluginError("not-found", capability, `page ${ref.objectNumber} is not in this document`);
|
|
1490
|
+
const queues = /* @__PURE__ */ new Map();
|
|
1491
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
1492
|
+
let instanceSettings = null;
|
|
1493
|
+
let changeQueue = null;
|
|
1494
|
+
const pendingStarts = [];
|
|
1495
|
+
const mirrorEnvironment = () => ({
|
|
1496
|
+
doc: doc(),
|
|
1497
|
+
changes: session.changes,
|
|
1498
|
+
notify: () => {
|
|
1499
|
+
if (lease.live) store.notify();
|
|
1500
|
+
},
|
|
1501
|
+
lifetime,
|
|
1502
|
+
cell(name, initial) {
|
|
1503
|
+
const cell = store.lease(`${sliceKey(plugin.id, session?.id)}/${name}`, initial, instanceId);
|
|
1504
|
+
if (lifetime.aborted) cell.revoke();
|
|
1505
|
+
else lifetime.addEventListener("abort", () => cell.revoke(), { once: true });
|
|
1506
|
+
return cell;
|
|
1507
|
+
},
|
|
1508
|
+
onDocumentEvent(listener) {
|
|
1509
|
+
const off = doc().events.subscribe(listener);
|
|
1510
|
+
scope.defer(off);
|
|
1511
|
+
},
|
|
1512
|
+
report: services.report
|
|
1513
|
+
});
|
|
1514
|
+
const context = {
|
|
1515
|
+
id: plugin.id,
|
|
1516
|
+
instanceId,
|
|
1517
|
+
documentId,
|
|
1518
|
+
engine,
|
|
1519
|
+
get doc() {
|
|
1520
|
+
return doc();
|
|
1521
|
+
},
|
|
1522
|
+
documentHandle: (requestedDocumentId) => requestedDocumentId ? services.documentHandle(requestedDocumentId) : session ? session.handle : services.documentHandle(void 0),
|
|
1523
|
+
document: () => {
|
|
1524
|
+
if (session) return metaOf();
|
|
1525
|
+
const id = store.getCore().activeId;
|
|
1526
|
+
return id ? store.getCore().documents[id] ?? null : null;
|
|
1527
|
+
},
|
|
1528
|
+
getPage: (page) => findPage(metaOf()?.pages ?? [], page),
|
|
1529
|
+
pageOf: (page) => pageOf(metaOf()?.pages ?? [], page, capability),
|
|
1530
|
+
assertPageRef: (ref) => {
|
|
1531
|
+
if (!metaOf()?.pages.some((pageInfo) => pageRefsEqual(pageInfo.ref, ref))) throw notFound(ref);
|
|
1532
|
+
},
|
|
1533
|
+
allows: (permission) => sessionAllows(doc().security, permission),
|
|
1534
|
+
assertAllowed: (permission, operation) => {
|
|
1535
|
+
if (!sessionAllows(doc().security, permission)) throw permissionDenied(capability, permission, operation);
|
|
1536
|
+
},
|
|
1537
|
+
allowsField: (action, field) => doc().security.allowsField(action, { groupId: field.groupId }),
|
|
1538
|
+
assertAllowedField: (action, field, operation) => {
|
|
1539
|
+
if (!doc().security.allowsField(action, { groupId: field.groupId })) throw fieldPermissionDenied(capability, action, field, operation);
|
|
1540
|
+
},
|
|
1541
|
+
get: (token) => (guard(token), services.resolveCapability(token, documentId)),
|
|
1542
|
+
tryGet: (token) => (guard(token), services.tryResolveCapability(token, documentId)),
|
|
1543
|
+
forDocument: (token, otherDocumentId) => (guard(token), services.resolveCapability(token, otherDocumentId)),
|
|
1544
|
+
tryForDocument: (token, otherDocumentId) => (guard(token), services.tryResolveCapability(token, otherDocumentId)),
|
|
1545
|
+
state: {
|
|
1546
|
+
get: () => lease.read(),
|
|
1547
|
+
update: (transition, ...args) => {
|
|
1548
|
+
if (!lease.write(transition(lease.read(), ...args))) reportClosed("a state update");
|
|
1549
|
+
},
|
|
1550
|
+
onChange: lease.onChange
|
|
1551
|
+
},
|
|
1552
|
+
settings: () => {
|
|
1553
|
+
if (instanceSettings) return instanceSettings;
|
|
1554
|
+
const registration = services.settingsStoreOf(plugin);
|
|
1555
|
+
if (!registration) throw new Error(`[kernel] plugin "${plugin.id}" read ctx.settings() but declares no settings; add \`settings: { defaults, registered }\` to its definition.`);
|
|
1556
|
+
return instanceSettings = registration.forInstance((teardown) => scope.defer(teardown));
|
|
1557
|
+
},
|
|
1558
|
+
notify: () => {
|
|
1559
|
+
if (lease.live) store.notify();
|
|
1560
|
+
},
|
|
1561
|
+
watch: (select, handler, isEqual = Object.is) => {
|
|
1562
|
+
let previous = select();
|
|
1563
|
+
const unsubscribe = store.subscribe(() => {
|
|
1564
|
+
const next = select();
|
|
1565
|
+
if (isEqual(previous, next)) return;
|
|
1566
|
+
const prior = previous;
|
|
1567
|
+
previous = next;
|
|
1568
|
+
handler(next, prior);
|
|
1569
|
+
});
|
|
1570
|
+
scope.defer(unsubscribe);
|
|
1571
|
+
return unsubscribe;
|
|
1572
|
+
},
|
|
1573
|
+
waitFor(predicate, options) {
|
|
1574
|
+
return new Promise((resolve, reject) => {
|
|
1575
|
+
let done = false;
|
|
1576
|
+
const finish = (fn) => {
|
|
1577
|
+
if (done) return;
|
|
1578
|
+
done = true;
|
|
1579
|
+
unsubscribe();
|
|
1580
|
+
lifetime.removeEventListener("abort", onClose);
|
|
1581
|
+
options?.signal?.removeEventListener("abort", onCancel);
|
|
1582
|
+
fn();
|
|
1583
|
+
};
|
|
1584
|
+
const onClose = () => finish(() => reject(new PluginError("instance-closed", capability, "closed while waiting")));
|
|
1585
|
+
const onCancel = () => finish(() => reject(new PluginError("operation-cancelled", capability, "cancelled while waiting")));
|
|
1586
|
+
const check = () => {
|
|
1587
|
+
let holds = false;
|
|
1588
|
+
try {
|
|
1589
|
+
holds = predicate();
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
finish(() => reject(error));
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (holds) finish(resolve);
|
|
1595
|
+
};
|
|
1596
|
+
const unsubscribe = store.subscribe(check);
|
|
1597
|
+
if (lifetime.aborted) return onClose();
|
|
1598
|
+
if (options?.signal?.aborted) return onCancel();
|
|
1599
|
+
lifetime.addEventListener("abort", onClose, { once: true });
|
|
1600
|
+
options?.signal?.addEventListener("abort", onCancel, { once: true });
|
|
1601
|
+
check();
|
|
1602
|
+
});
|
|
1603
|
+
},
|
|
1604
|
+
mirror(spec) {
|
|
1605
|
+
const controller = createMirror(spec, mirrorEnvironment());
|
|
1606
|
+
pendingStarts.push(controller.start);
|
|
1607
|
+
return controller.mirror;
|
|
1608
|
+
},
|
|
1609
|
+
pageMirror: (spec) => createPageMirror(spec, mirrorEnvironment()),
|
|
1610
|
+
events: { source() {
|
|
1611
|
+
const hook = createEventHook(services.report);
|
|
1612
|
+
scope.defer(() => hook.dispose());
|
|
1613
|
+
return hook;
|
|
1614
|
+
} },
|
|
1615
|
+
listen(source, listener) {
|
|
1616
|
+
const off = typeof source === "function" ? source(listener) : source.subscribe(listener);
|
|
1617
|
+
scope.defer(off);
|
|
1618
|
+
},
|
|
1619
|
+
get changes() {
|
|
1620
|
+
if (!session) throw new Error(`[kernel] workspace plugin "${plugin.id}" has no document to change; ctx.changes is for document-scoped plugins.`);
|
|
1621
|
+
return changeQueue ?? (changeQueue = session.changes.forPlugin(plugin.id));
|
|
1622
|
+
},
|
|
1623
|
+
cleanup: (teardown) => scope.defer(teardown),
|
|
1624
|
+
clock: instanceClock(services.clock, lifetime),
|
|
1625
|
+
onSettle(flush) {
|
|
1626
|
+
if (!session) throw new Error(`[kernel] workspace plugin "${plugin.id}" has no document to settle; ctx.onSettle() is for document-scoped plugins.`);
|
|
1627
|
+
session.settleFlushes.add(flush);
|
|
1628
|
+
scope.defer(() => {
|
|
1629
|
+
session.settleFlushes.delete(flush);
|
|
1630
|
+
});
|
|
1631
|
+
},
|
|
1632
|
+
aroundDownload(wrap) {
|
|
1633
|
+
if (!session) throw new Error(`[kernel] workspace plugin "${plugin.id}" has no document to download; ctx.aroundDownload() is for document-scoped plugins.`);
|
|
1634
|
+
session.downloadWraps.push(wrap);
|
|
1635
|
+
scope.defer(() => {
|
|
1636
|
+
const index = session.downloadWraps.indexOf(wrap);
|
|
1637
|
+
if (index >= 0) session.downloadWraps.splice(index, 1);
|
|
1638
|
+
});
|
|
1639
|
+
},
|
|
1640
|
+
async acquire(get, dispose) {
|
|
1641
|
+
const value = await get(lifetime);
|
|
1642
|
+
if (lifetime.aborted) {
|
|
1643
|
+
await dispose(value);
|
|
1644
|
+
throw new PluginError("instance-closed", capability, "closed while acquiring a resource");
|
|
1645
|
+
}
|
|
1646
|
+
scope.defer(() => dispose(value));
|
|
1647
|
+
return value;
|
|
1648
|
+
},
|
|
1649
|
+
cancellable: (signal, task) => cancellable(capability, signal, task),
|
|
1650
|
+
latest(key) {
|
|
1651
|
+
let lane = lanes.get(key);
|
|
1652
|
+
if (!lane) {
|
|
1653
|
+
lane = createLatestLane(lifetime, capability, key);
|
|
1654
|
+
lanes.set(key, lane);
|
|
1655
|
+
}
|
|
1656
|
+
return lane;
|
|
1657
|
+
},
|
|
1658
|
+
serialQueue(key = "default") {
|
|
1659
|
+
let queue = queues.get(key);
|
|
1660
|
+
if (!queue) {
|
|
1661
|
+
queue = createSerialQueue(capability);
|
|
1662
|
+
queues.set(key, queue);
|
|
1663
|
+
}
|
|
1664
|
+
return queue;
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
connectedHooks.set(context, () => {
|
|
1668
|
+
for (const start of pendingStarts.splice(0)) start();
|
|
1669
|
+
});
|
|
1670
|
+
return context;
|
|
1671
|
+
}
|
|
1672
|
+
//#endregion
|
|
1673
|
+
//#region src/settle.ts
|
|
1674
|
+
/**
|
|
1675
|
+
* Settling a document: before something reads the whole file (a download), everything its
|
|
1676
|
+
* plugins hold back from the engine is sent, and the engine has answered. Each plugin registers
|
|
1677
|
+
* a flush with `ctx.onSettle()` for what it holds back: text typed a moment ago, a drawing waiting
|
|
1678
|
+
* for its next stroke, the writes on their way in its write queue. The rules are in
|
|
1679
|
+
* `docs/conventions/state-and-sync.md`.
|
|
1680
|
+
*
|
|
1681
|
+
* A flush that fails doesn't stop the read: the file is then what the engine has, and the plugin
|
|
1682
|
+
* reports its own failure.
|
|
1683
|
+
*/
|
|
1684
|
+
/**
|
|
1685
|
+
* Run every flush and wait for all of them. Rejects with `operation-cancelled` as soon as one of
|
|
1686
|
+
* `signals` aborts, such as the caller's or the document's own (it aborts when the document
|
|
1687
|
+
* closes).
|
|
1688
|
+
*/
|
|
1689
|
+
async function settle(flushes, signals, report) {
|
|
1690
|
+
const stop = new AbortController();
|
|
1691
|
+
const listening = signals.filter((signal) => signal !== void 0);
|
|
1692
|
+
const abort = () => stop.abort();
|
|
1693
|
+
for (const signal of listening) if (signal.aborted) abort();
|
|
1694
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
1695
|
+
try {
|
|
1696
|
+
await whileNotAborted((async () => {
|
|
1697
|
+
const running = [...flushes].map(async (flush) => flush(stop.signal));
|
|
1698
|
+
for (const outcome of await Promise.allSettled(running)) if (outcome.status === "rejected") report(outcome.reason);
|
|
1699
|
+
})(), stop.signal);
|
|
1700
|
+
} finally {
|
|
1701
|
+
for (const signal of listening) signal.removeEventListener("abort", abort);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
/** `work`, or `operation-cancelled` as soon as `signal` aborts. */
|
|
1705
|
+
async function whileNotAborted(work, signal) {
|
|
1706
|
+
let onAbort;
|
|
1707
|
+
const aborted = new Promise((_, reject) => {
|
|
1708
|
+
onAbort = () => reject(new PluginError("operation-cancelled", "documents", "settling was cancelled"));
|
|
1709
|
+
if (signal.aborted) onAbort();
|
|
1710
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
1711
|
+
});
|
|
1712
|
+
try {
|
|
1713
|
+
await Promise.race([work, aborted]);
|
|
1714
|
+
} finally {
|
|
1715
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
//#endregion
|
|
1719
|
+
//#region src/settings.ts
|
|
1720
|
+
/**
|
|
1721
|
+
* A plugin's settings: what the app registered over the plugin's defaults, changed at runtime
|
|
1722
|
+
* with `updateSettings()`. A plugin declares them in its definition
|
|
1723
|
+
* (`definePlugin({ settings: { defaults, registered } })`), and the kernel builds one store per
|
|
1724
|
+
* plugin registration when it plans the plugin list. The store belongs to the registration, not
|
|
1725
|
+
* to a document: it exists before the first document opens, every instance of the plugin shares
|
|
1726
|
+
* it, and a change reaches every open document and the ones opened later. A plugin reads its
|
|
1727
|
+
* store with `ctx.settings()`; an adapter reads it with `kernel.settingsOf(token)`.
|
|
1728
|
+
*
|
|
1729
|
+
* Changes merge into the settings the same way everywhere (`registered` over the defaults, and
|
|
1730
|
+
* every `updateSettings()`): plain objects merge key by key, arrays and every other value
|
|
1731
|
+
* replace, and `undefined` leaves a setting as it is.
|
|
1732
|
+
*/
|
|
1733
|
+
/**
|
|
1734
|
+
* Build a registration's store. `wake` runs after every change, before the event, so readers
|
|
1735
|
+
* that poll the capability (selectors over `getSettings()`) read again.
|
|
1736
|
+
*/
|
|
1737
|
+
function createSettingsStore({ defaults, registered, whole = [] }, wake, report) {
|
|
1738
|
+
const initial = mergeSettings(defaults, registered, whole);
|
|
1739
|
+
let current = initial;
|
|
1740
|
+
const changedHook = createEventHook(report);
|
|
1741
|
+
const commit = (next) => {
|
|
1742
|
+
const changed = changedKeys(current, next);
|
|
1743
|
+
if (changed.length === 0) return;
|
|
1744
|
+
current = next;
|
|
1745
|
+
wake();
|
|
1746
|
+
changedHook.emit({
|
|
1747
|
+
settings: next,
|
|
1748
|
+
changed
|
|
1749
|
+
});
|
|
1750
|
+
};
|
|
1751
|
+
const api = {
|
|
1752
|
+
getSettings: () => current,
|
|
1753
|
+
updateSettings: (changes) => commit(mergeSettings(current, changes, whole)),
|
|
1754
|
+
resetSettings: () => commit(initial),
|
|
1755
|
+
onSettingsChanged: changedHook.on
|
|
1756
|
+
};
|
|
1757
|
+
return {
|
|
1758
|
+
api,
|
|
1759
|
+
forInstance(cleanup) {
|
|
1760
|
+
const instanceChanged = createEventHook(report);
|
|
1761
|
+
cleanup(changedHook.on((event) => instanceChanged.emit(event)));
|
|
1762
|
+
cleanup(() => instanceChanged.dispose());
|
|
1763
|
+
return {
|
|
1764
|
+
get: api.getSettings,
|
|
1765
|
+
api: {
|
|
1766
|
+
...api,
|
|
1767
|
+
onSettingsChanged: instanceChanged.on
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
},
|
|
1771
|
+
dispose: () => changedHook.dispose()
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* `changes` merged into `settings`, the `whole` ones replaced instead: the merge every settings
|
|
1776
|
+
* store applies, for code that keeps changes before a store exists (an adapter whose engine is
|
|
1777
|
+
* still loading). What a change leaves as it was keeps its object, so a reader that compares by
|
|
1778
|
+
* reference sees only what really changed.
|
|
1779
|
+
*/
|
|
1780
|
+
function mergeSettings(settings, changes, whole = []) {
|
|
1781
|
+
if (whole.length === 0 || !isPlainObject(changes)) return mergeValue(settings, changes);
|
|
1782
|
+
const merged = mergeValue(settings, omit(changes, whole));
|
|
1783
|
+
let replaced = null;
|
|
1784
|
+
for (const key of whole) {
|
|
1785
|
+
const change = changes[key];
|
|
1786
|
+
if (change === void 0 || sameValue(merged[key], change)) continue;
|
|
1787
|
+
replaced ?? (replaced = { ...merged });
|
|
1788
|
+
replaced[key] = change;
|
|
1789
|
+
}
|
|
1790
|
+
return replaced ?? merged;
|
|
1791
|
+
}
|
|
1792
|
+
/** `object` without `keys`. */
|
|
1793
|
+
function omit(object, keys) {
|
|
1794
|
+
const rest = {};
|
|
1795
|
+
for (const key of Object.keys(object)) if (!keys.includes(key)) rest[key] = object[key];
|
|
1796
|
+
return rest;
|
|
1797
|
+
}
|
|
1798
|
+
function mergeValue(base, change) {
|
|
1799
|
+
if (change === void 0) return base;
|
|
1800
|
+
if (!isPlainObject(change)) return sameValue(base, change) ? base : change;
|
|
1801
|
+
const into = isPlainObject(base) ? base : {};
|
|
1802
|
+
let merged = null;
|
|
1803
|
+
for (const key of Object.keys(change)) {
|
|
1804
|
+
const current = hasOwn(into, key) ? into[key] : void 0;
|
|
1805
|
+
const next = mergeValue(current, change[key]);
|
|
1806
|
+
if (next === current) continue;
|
|
1807
|
+
merged ?? (merged = { ...into });
|
|
1808
|
+
merged[key] = next;
|
|
1809
|
+
}
|
|
1810
|
+
return merged ?? base;
|
|
1811
|
+
}
|
|
1812
|
+
/** The top-level settings whose value differs between `previous` and `next`. */
|
|
1813
|
+
function changedKeys(previous, next) {
|
|
1814
|
+
const before = previous;
|
|
1815
|
+
const after = next;
|
|
1816
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((key) => !sameValue(before[key], after[key]));
|
|
1817
|
+
}
|
|
1818
|
+
const isPlainObject = (value) => {
|
|
1819
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1820
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1821
|
+
return prototype === Object.prototype || prototype === null;
|
|
1822
|
+
};
|
|
1823
|
+
const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
1824
|
+
/** Equal by value: the same primitive or object, or arrays and plain objects with equal entries. */
|
|
1825
|
+
function sameValue(left, right) {
|
|
1826
|
+
if (Object.is(left, right)) return true;
|
|
1827
|
+
if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((item, index) => sameValue(item, right[index]));
|
|
1828
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
1829
|
+
const keys = Object.keys(left);
|
|
1830
|
+
return keys.length === Object.keys(right).length && keys.every((key) => hasOwn(right, key) && sameValue(left[key], right[key]));
|
|
1831
|
+
}
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
//#endregion
|
|
1835
|
+
export { createScope as A, createDocumentChanges as C, toPluginError as D, isPluginError as E, createBurstGate as M, DocumentsToken as N, toPluginErrorInfo as O, timerClock as S, PluginError as T, reload as _, markConnected as a, createStore as b, fieldPermissionDenied as c, findPage as d, pageOf as f, createMirror as g, createPageMirror as h, createPluginContext as i, isCancelled as j, CancelledError as k, permissionDenied as l, createSerialQueue as m, mergeSettings as n, readWrapped as o, cancellable as p, settle as r, sliceKey as s, createSettingsStore as t, sessionAllows as u, createLatestLane as v, createEventHook as w, instanceClock as x, guardHandle as y };
|
|
1836
|
+
|
|
1837
|
+
//# sourceMappingURL=settings-CMWFRjmQ.js.map
|