@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.
@@ -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