@embedpdf/core 3.0.0-next.14 → 3.0.0-next.16

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 CHANGED
@@ -1,198 +1,40 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_settings = require("./settings-BS97q3tq.cjs");
2
3
  let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
3
- //#region src/types.ts
4
- /** Kernel-emitted document-lifecycle actions; plugins may react via `onAction`. */
5
- const CORE_DOCUMENT_ADDED = "@@core/document-added";
6
- const CORE_DOCUMENT_REMOVED = "@@core/document-removed";
7
- const CORE_ACTIVE_CHANGED = "@@core/active-changed";
8
- const CORE_ORDER_CHANGED = "@@core/order-changed";
9
- /** A document's page registry was replaced by a mutation event (rotate/move/delete). */
10
- const CORE_DOCUMENT_PAGES_UPDATED = "@@core/document-pages-updated";
11
- /** A tab slot was reserved: `open()` was called; the document is loading. */
12
- const CORE_DOCUMENT_OPENING = "@@core/document-opening";
13
- /** The engine reports the document needs a password (`documents.unlock`). */
14
- const CORE_DOCUMENT_LOCKED = "@@core/document-locked";
15
- /** The open failed; the tab stays with `status: 'error'` until closed. */
16
- const CORE_DOCUMENT_OPEN_FAILED = "@@core/document-open-failed";
17
- /** Field-wise DocInfo equality — the ONE definition every adapter's reactive
18
- * `docs` read keys on, so a new lifecycle field can never silently stop
19
- * re-rendering one framework's tab bar. */
20
- const docInfoEquals = (a, b) => a.id === b.id && a.name === b.name && a.status === b.status && a.pageCount === b.pageCount && a.passwordProvided === b.passwordProvided;
21
- const docInfoListEquals = (a, b) => a === b || a.length === b.length && a.every((d, i) => docInfoEquals(d, b[i]));
22
- /** Built-in token for the document registry capability (provided by the kernel). */
23
- const DocumentsToken = { name: "documents" };
24
- //#endregion
25
- //#region src/store.ts
26
- function createStore(report = console.error) {
27
- let core = {
28
- documents: {},
29
- pending: {},
30
- order: [],
31
- activeId: null
32
- };
33
- const slices = {};
34
- const reducers = {};
35
- const changeListeners = /* @__PURE__ */ new Set();
36
- const actionListeners = /* @__PURE__ */ new Set();
37
- const guarded = (fn) => {
38
- try {
39
- fn();
40
- } catch (error) {
41
- report(error);
42
- }
43
- };
44
- let emitting = false;
45
- let pending = false;
46
- const emitChange = () => {
47
- if (emitting) {
48
- pending = true;
49
- return;
50
- }
51
- emitting = true;
52
- try {
53
- do {
54
- pending = false;
55
- changeListeners.forEach((listener) => guarded(listener));
56
- } while (pending);
57
- } finally {
58
- emitting = false;
59
- }
60
- };
61
- const emitAction = (action) => actionListeners.forEach((listener) => guarded(() => listener(action)));
62
- return {
63
- registerSlice(key, reducer, initial) {
64
- reducers[key] = reducer;
65
- slices[key] = initial;
66
- },
67
- removeSlice(key) {
68
- delete reducers[key];
69
- delete slices[key];
70
- emitChange();
71
- },
72
- getSlice: (key) => slices[key],
73
- getCore: () => core,
74
- getState: () => ({
75
- core,
76
- plugins: slices
77
- }),
78
- dispatchTo(key, action) {
79
- const r = reducers[key];
80
- if (!r) return;
81
- const next = r(slices[key], action);
82
- if (next !== slices[key]) {
83
- slices[key] = next;
84
- emitChange();
85
- }
86
- emitAction(action);
87
- },
88
- setCore(patch, action) {
89
- core = {
90
- ...core,
91
- ...patch
92
- };
93
- emitChange();
94
- emitAction(action);
95
- },
96
- subscribe(listener) {
97
- changeListeners.add(listener);
98
- return () => void changeListeners.delete(listener);
99
- },
100
- subscribeAction(listener) {
101
- actionListeners.add(listener);
102
- return () => void actionListeners.delete(listener);
103
- },
104
- destroy() {
105
- core = {
106
- documents: {},
107
- pending: {},
108
- order: [],
109
- activeId: null
110
- };
111
- for (const key of Object.keys(slices)) delete slices[key];
112
- for (const key of Object.keys(reducers)) delete reducers[key];
113
- changeListeners.clear();
114
- actionListeners.clear();
115
- }
116
- };
117
- }
118
- //#endregion
119
- //#region src/context.ts
120
- /** A plugin's slice key. Workspace plugins use their id; document-scoped plugins are per-document. */
121
- const sliceKey = (pluginId, documentId) => documentId ? `${pluginId}::${documentId}` : pluginId;
122
- /**
123
- * Build the context a plugin sees. When `session` is given the context is bound
124
- * to that document — `getState`/`dispatch` target its slice, `document()` returns
125
- * it, and `get()` resolves document-scoped capabilities for it.
126
- */
127
- function createPluginContext(services, plugin, session) {
128
- const { engine, store } = services;
129
- const documentId = session?.id;
130
- const key = sliceKey(plugin.id, documentId);
131
- const ownScope = session?.scope ?? services.workspaceScope;
132
- return {
133
- id: plugin.id,
134
- engine,
135
- documentId,
136
- doc: session ? session.handle : services.documentHandle(void 0),
137
- documentHandle: (requestedDocumentId) => requestedDocumentId ? services.documentHandle(requestedDocumentId) : session ? session.handle : services.documentHandle(void 0),
138
- getState: () => store.getSlice(key),
139
- dispatch: (action) => store.dispatchTo(key, action),
140
- subscribe: store.subscribe,
141
- core: store.getCore,
142
- document: () => {
143
- if (session) return store.getCore().documents[session.id] ?? session.stagedMeta;
144
- const id = store.getCore().activeId;
145
- return id ? store.getCore().documents[id] ?? null : null;
146
- },
147
- get: (token) => services.resolveCapability(token, documentId),
148
- forDocument: (token, otherDocumentId) => services.resolveCapability(token, otherDocumentId),
149
- tryGet: (token) => services.tryResolveCapability(token, documentId),
150
- tryForDocument: (token, otherDocumentId) => services.tryResolveCapability(token, otherDocumentId),
151
- cleanup: (teardown) => ownScope.defer(teardown)
152
- };
153
- }
154
- /** A plugin context plus the side-effect primitives (watch / onAction / cleanup). */
155
- function createEffectContext(services, plugin, session) {
156
- const { store } = services;
157
- const ownScope = session?.scope ?? services.workspaceScope;
158
- return {
159
- ...createPluginContext(services, plugin, session),
160
- watch: (select, handler, isEqual = Object.is) => {
161
- let previous = select();
162
- const unsubscribe = store.subscribe(() => {
163
- const next = select();
164
- if (!isEqual(previous, next)) {
165
- const prior = previous;
166
- previous = next;
167
- handler(next, prior);
168
- }
169
- });
170
- ownScope.defer(unsubscribe);
171
- return unsubscribe;
172
- },
173
- onAction: (type, handler) => {
174
- const unsubscribe = store.subscribeAction((action) => {
175
- if (action.type === type) handler(action);
176
- });
177
- ownScope.defer(unsubscribe);
178
- return unsubscribe;
179
- }
180
- };
181
- }
182
- //#endregion
183
4
  //#region src/order.ts
184
5
  /**
185
- * Validate `requires` (fail fast) and topologically sort plugins so dependencies
186
- * are initialised before dependents.
6
+ * Validate the composition (fail fast, before anything is constructed) and
7
+ * topologically sort plugins so dependencies are initialised before dependents.
8
+ *
9
+ * Rejected compositions, each with both names in the error:
10
+ * - the same plugin definition installed twice, or two definitions sharing an id;
11
+ * - two plugins providing the same capability token (no implicit last-wins —
12
+ * an intentional replacement is a composition-time choice, made by
13
+ * removing the old definition from the list);
14
+ * - a workspace plugin that `requires` a document-scoped token (it has no
15
+ * document to resolve it for; `ctx.forDocument()` is the explicit path);
16
+ * - a required token no plugin provides;
17
+ * - a dependency cycle.
187
18
  */
188
19
  function planPlugins(plugins) {
20
+ const byId = /* @__PURE__ */ new Map();
189
21
  const providerByToken = /* @__PURE__ */ new Map();
190
- const scopeByToken = /* @__PURE__ */ new Map([[DocumentsToken, "workspace"]]);
191
- for (const plugin of plugins) if (plugin.token && plugin.capability) {
192
- providerByToken.set(plugin.token, plugin);
193
- scopeByToken.set(plugin.token, plugin.scope ?? "workspace");
22
+ const scopeByToken = /* @__PURE__ */ new Map([[require_settings.DocumentsToken, "workspace"]]);
23
+ for (const plugin of plugins) {
24
+ const prior = byId.get(plugin.id);
25
+ if (prior) throw new Error(prior === plugin ? `[kernel] plugin "${plugin.id}" is installed twice.` : `[kernel] two different plugins use the id "${plugin.id}".`);
26
+ byId.set(plugin.id, plugin);
27
+ if (plugin.token) {
28
+ const other = providerByToken.get(plugin.token);
29
+ if (other) throw new Error(`[kernel] capability "${plugin.token.name}" is provided by both "${other.id}" and "${plugin.id}".`);
30
+ providerByToken.set(plugin.token, plugin);
31
+ scopeByToken.set(plugin.token, plugin.scope ?? "workspace");
32
+ }
33
+ }
34
+ for (const plugin of plugins) for (const required of plugin.requires ?? []) {
35
+ if (required !== require_settings.DocumentsToken && !providerByToken.has(required)) throw new Error(`Plugin "${plugin.id}" requires capability "${required.name}", which no plugin provides` + (required.hint ? ` — ${required.hint}.` : "."));
36
+ if (plugin.scope !== "document" && providerByToken.get(required)?.scope === "document") throw new Error(`[kernel] workspace plugin "${plugin.id}" requires document-scoped capability "${required.name}"; resolve it per document with ctx.forDocument() and declare it as optional.`);
194
37
  }
195
- for (const plugin of plugins) for (const required of plugin.requires ?? []) if (required !== DocumentsToken && !providerByToken.has(required)) throw new Error(`Plugin "${plugin.id}" requires capability "${required.name}", which no plugin provides` + (required.hint ? ` — ${required.hint}.` : "."));
196
38
  const ordered = [];
197
39
  const status = /* @__PURE__ */ new Map();
198
40
  const visit = (plugin) => {
@@ -215,74 +57,129 @@ function planPlugins(plugins) {
215
57
  };
216
58
  }
217
59
  //#endregion
218
- //#region src/scope.ts
60
+ //#region src/viewer-settings.ts
61
+ /** The viewer's settings when the app gives none; the colors are the theming page's defaults. */
62
+ const VIEWER_DEFAULTS = Object.freeze({
63
+ identity: null,
64
+ scope: null,
65
+ accent: "#3858e9",
66
+ page: Object.freeze({
67
+ background: "#ffffff",
68
+ shadow: "0 6px 18px rgb(0 0 0 / 0.18)"
69
+ })
70
+ });
71
+ /** An identity is one person: a new one replaces the last instead of merging into it. */
72
+ const VIEWER_WHOLE_SETTINGS = ["identity"];
219
73
  /**
220
- * A lifecycle operation was cancelled — the tab was closed mid-open, or the
221
- * kernel was destroyed mid-start. NOT a failure: callers that race `open()`
222
- * against `close()` should treat this rejection as "the close won".
74
+ * The viewer's settings as a framework's viewer component gives them: a setting left out is its
75
+ * default, and `page` fills in the side it leaves out. Every adapter's viewer starts the kernel
76
+ * with this and passes it again when its props change.
223
77
  */
224
- var CancelledError = class extends Error {
225
- constructor(what) {
226
- super(`[kernel] cancelled: ${what}`);
227
- this.name = "CancelledError";
228
- }
229
- };
230
- const isCancelled = (error) => error instanceof CancelledError;
231
- function createScope(report) {
232
- const teardowns = [];
233
- let disposal = null;
78
+ function viewerSettingsOf({ identity, scope, accent, page }) {
234
79
  return {
235
- get disposed() {
236
- return disposal !== null;
237
- },
238
- defer(teardown) {
239
- if (disposal) disposal.then(() => Promise.resolve().then(teardown).catch(report));
240
- else teardowns.push(teardown);
241
- },
242
- dispose() {
243
- return disposal ?? (disposal = (async () => {
244
- while (teardowns.length) try {
245
- await teardowns.pop()();
246
- } catch (error) {
247
- report(error);
248
- }
249
- })());
80
+ identity: identity ?? null,
81
+ scope: scope ?? null,
82
+ accent: accent ?? VIEWER_DEFAULTS.accent,
83
+ page: {
84
+ ...VIEWER_DEFAULTS.page,
85
+ ...page
250
86
  }
251
87
  };
252
88
  }
253
89
  //#endregion
254
90
  //#region src/kernel.ts
91
+ const EMPTY_PAGES = Object.freeze([]);
255
92
  /** The new page registry a document mutation carries, or null for events that
256
93
  * don't change page structure (annotations, metadata). The snapshot is the
257
94
  * same shape `pages.list()` returns, so callers swap it in directly. */
258
95
  function layoutFromEvent(event) {
259
96
  switch (event.type) {
260
- case "pages.moved":
97
+ case "pages.reordered":
261
98
  case "pages.rotated":
262
99
  case "pages.deleted":
263
- case "pages.inserted": return event.layout;
100
+ case "pages.inserted":
101
+ case "pages.restored": return event.layout;
264
102
  default: return null;
265
103
  }
266
104
  }
267
105
  const isDocumentScoped = (plugin) => plugin.scope === "document";
268
- const initialStateOf = (plugin) => typeof plugin.initialState === "function" ? plugin.initialState() : plugin.initialState ?? {};
269
- const reducerOf = (plugin) => plugin.reduce ?? ((state) => state);
270
- const toDocInfo = (meta) => ({
106
+ const initialStateOf = (plugin) => plugin.state?.();
107
+ /**
108
+ * A document's public fields, one object per registry entry: the entries are replaced, never
109
+ * changed, so the same entry gives the same object, and readers re-render only for their own.
110
+ */
111
+ const infoByEntry = /* @__PURE__ */ new WeakMap();
112
+ function documentInfoOf(entry) {
113
+ let info = infoByEntry.get(entry);
114
+ if (!info) {
115
+ info = "pages" in entry ? readyInfo(entry) : pendingInfo(entry);
116
+ infoByEntry.set(entry, info);
117
+ }
118
+ return info;
119
+ }
120
+ const readyInfo = (meta) => Object.freeze({
271
121
  id: meta.id,
272
122
  name: meta.name,
273
123
  status: "ready",
274
- pageCount: meta.pageCount
124
+ pageCount: meta.pageCount,
125
+ hasUnsavedChanges: meta.hasUnsavedChanges
275
126
  });
276
- const pendingToDocInfo = (meta) => ({
127
+ const pendingInfo = (meta) => Object.freeze({
277
128
  id: meta.id,
278
129
  name: meta.name,
279
130
  status: meta.status,
280
131
  pageCount: 0,
281
- passwordProvided: meta.passwordProvided
132
+ hasUnsavedChanges: false,
133
+ ...meta.status === "locked" ? { passwordProvided: meta.passwordProvided ?? false } : {},
134
+ ...meta.status === "error" && meta.error !== void 0 ? { error: require_settings.toPluginErrorInfo(require_settings.toPluginError("documents", meta.error)) } : {}
135
+ });
136
+ /** Whether an event is a change to the document, one a download would carry. */
137
+ const changesDocument = (event) => event.type !== "stream.desynced" && event.type !== "document.versioned" && event.type !== "signatures.prepared" && event.type !== "signatures.cancelled";
138
+ /** Download a URL source's file; its tab shows the failure when the server says no. */
139
+ async function fetchUrlSource(source, signal) {
140
+ const fetchFile = globalThis.fetch;
141
+ if (!fetchFile) throw new require_settings.PluginError("unsupported", "documents", `can't download ${source.url}: no fetch()`);
142
+ const response = await fetchFile(source.url, { signal });
143
+ if (!response.ok) throw new require_settings.PluginError(response.status === 404 ? "not-found" : "operation-failed", "documents", `couldn't download ${source.url} (HTTP ${response.status})`);
144
+ return {
145
+ kind: "bytes",
146
+ bytes: new Uint8Array(await response.arrayBuffer())
147
+ };
148
+ }
149
+ const isUrlSource = (source) => source.kind === "url";
150
+ const noLayer = () => new require_settings.PluginError("unsupported", "documents", "only the local engine gives a layer to download");
151
+ /** How a print comes out at each tier, one object each, so readers can compare them with `Object.is`. */
152
+ const PRINT_STANDARD = Object.freeze({
153
+ kind: "images",
154
+ dpi: 150
155
+ });
156
+ const PRINT_HIGH = Object.freeze({
157
+ kind: "images",
158
+ dpi: 300
159
+ });
160
+ const PRINT_PDF = Object.freeze({ kind: "pdf" });
161
+ /**
162
+ * The documents capability as seen from inside one document's scope: every call whose document
163
+ * is optional uses that document instead of the active one. The rest is the registry itself.
164
+ */
165
+ const documentsInScope = (documents, documentId) => ({
166
+ ...documents,
167
+ get: (id = documentId) => documents.get(id),
168
+ download: (id = documentId, options) => documents.download(id, options),
169
+ downloadLayer: (id = documentId, options) => documents.downloadLayer(id, options),
170
+ canDownload: (id = documentId) => documents.canDownload(id),
171
+ preparePrint: (id = documentId, options) => documents.preparePrint(id, options),
172
+ print: (id = documentId, options) => documents.print(id, options),
173
+ getPrintQuality: (id = documentId) => documents.getPrintQuality(id),
174
+ canPrint: (id = documentId) => documents.canPrint(id),
175
+ listPages: (id = documentId) => documents.listPages(id),
176
+ getPage: (page, id = documentId) => documents.getPage(page, id),
177
+ getPageIndex: (ref, id = documentId) => documents.getPageIndex(ref, id),
178
+ getRevision: (id = documentId) => documents.getRevision(id)
282
179
  });
283
180
  /** The stable id an input implies, if it carries one ('bytes'/'layerBytes'/'id'). */
284
181
  const idOfInput = (input) => "id" in input && typeof input.id === "string" ? input.id : null;
285
- const passwordOfInput = (input) => "password" in input ? input.password : void 0;
182
+ let instanceCounter = 0;
286
183
  /** Engine rejections carry AbortError when close() aborts the live call;
287
184
  * match structurally so test fakes with plain promises still work. */
288
185
  const isAbortLike = (error) => error instanceof Error && error.name === "AbortError";
@@ -290,7 +187,10 @@ const isAbortLike = (error) => error instanceof Error && error.name === "AbortEr
290
187
  * Assemble a kernel from an engine + plugins.
291
188
  *
292
189
  * planPlugins — validate dependencies, order them
293
- * resolveCapability — workspace singletons, or per-document instances built lazily
190
+ * resolveCapability — workspace singletons (or their view from a document's scope),
191
+ * or per-document instances built lazily
192
+ * settings — one store per plugin registration, built up front, shared
193
+ * by its instances and readable with no document
294
194
  * document lifecycle — one DocumentSession per document: transactional open
295
195
  * (publish-last), one idempotent close for every phase
296
196
  * start / destroy — explicit status machine; destroy closes everything
@@ -298,14 +198,49 @@ const isAbortLike = (error) => error instanceof Error && error.name === "AbortEr
298
198
  * The kernel closes every handle it opened; it never destroys the engine —
299
199
  * ownership follows acquisition, and the engine was handed in by the caller.
300
200
  */
301
- function createKernel(opts) {
302
- const { engine, plugins } = opts;
303
- const report = opts.report ?? ((error) => console.error("[kernel]", error));
304
- const store = createStore(report);
201
+ function createKernel(config) {
202
+ const { engine, plugins } = config;
203
+ const report = config.report ?? ((error) => console.error("[kernel]", error));
204
+ const store = require_settings.createStore(report);
305
205
  const plan = planPlugins(plugins);
306
206
  const documentScopedPlugins = plan.ordered.filter(isDocumentScoped);
307
207
  const workspaceCapabilities = /* @__PURE__ */ new Map();
308
- const workspaceScope = createScope(report);
208
+ const workspaceLeases = /* @__PURE__ */ new Map();
209
+ const workspaceConnectors = /* @__PURE__ */ new Map();
210
+ const workspaceCancel = new AbortController();
211
+ const workspaceScope = require_settings.createScope(report);
212
+ /** How each workspace capability looks from inside a document's scope, by token. The
213
+ * documents capability is built in, so its view is declared here; plugins declare theirs. */
214
+ const inScopeByToken = /* @__PURE__ */ new Map([[require_settings.DocumentsToken, documentsInScope]]);
215
+ for (const plugin of plan.ordered) if (plugin.token && plugin.inScope && !isDocumentScoped(plugin)) inScopeByToken.set(plugin.token, plugin.inScope);
216
+ const opened = require_settings.createEventHook(report);
217
+ const openFailed = require_settings.createEventHook(report);
218
+ const locked = require_settings.createEventHook(report);
219
+ const closed = require_settings.createEventHook(report);
220
+ const activeChanged = require_settings.createEventHook(report);
221
+ const pagesChanged = require_settings.createEventHook(report);
222
+ const unsavedChangesChanged = require_settings.createEventHook(report);
223
+ workspaceScope.defer(() => {
224
+ for (const hook of [
225
+ opened,
226
+ openFailed,
227
+ locked,
228
+ closed,
229
+ activeChanged,
230
+ pagesChanged,
231
+ unsavedChangesChanged
232
+ ]) hook.dispose();
233
+ });
234
+ /** Every core write goes through here so an active-tab change is observed exactly once. */
235
+ const setCore = (patch) => {
236
+ const before = store.getCore().activeId;
237
+ store.setCore(patch);
238
+ const after = store.getCore().activeId;
239
+ if (before !== after) activeChanged.emit({
240
+ documentId: after,
241
+ previousDocumentId: before
242
+ });
243
+ };
309
244
  const sessions = /* @__PURE__ */ new Map();
310
245
  let status = "created";
311
246
  let startPromise = null;
@@ -314,18 +249,18 @@ function createKernel(opts) {
314
249
  if (status === "failed" || status === "destroying" || status === "destroyed") throw new Error(`[kernel] ${what} on a ${status} kernel`);
315
250
  };
316
251
  const checkpoint = (session) => {
317
- if (session.cancel.signal.aborted) throw new CancelledError(`closed while opening: ${session.id}`);
252
+ if (session.cancel.signal.aborted) throw new require_settings.CancelledError(`closed while opening: ${session.id}`);
318
253
  };
319
254
  /**
320
255
  * Await an engine/network call under the session's cancellation:
321
256
  * - the call is retained so close() can abort real worker-side work
322
257
  * (`AbortablePromise`), and
323
- * - the await RACES the cancellation, so close()'s join never blocks on a
258
+ * - the await races the cancellation, so close()'s join never blocks on a
324
259
  * call that cannot be aborted (a plain-promise engine, a stuck fetch).
325
260
  * When cancellation wins but the call later lands anyway, `onLateResult`
326
261
  * routes the result into the session scope — whose late-defer rule runs it
327
262
  * immediately after disposal — so a late-arriving resource cannot leak.
328
- * (Plugin inits are deliberately NOT raced: they are first-party code that
263
+ * (Plugin inits are deliberately not raced: they are first-party code that
329
264
  * close() joins to completion; only unbounded external waits are raced.)
330
265
  */
331
266
  async function engineCall(session, call, onLateResult) {
@@ -334,14 +269,14 @@ function createKernel(opts) {
334
269
  const signal = session.cancel.signal;
335
270
  let onAbort;
336
271
  const cancellation = new Promise((_, reject) => {
337
- onAbort = () => reject(new CancelledError(`closed while opening: ${session.id}`));
272
+ onAbort = () => reject(new require_settings.CancelledError(`closed while opening: ${session.id}`));
338
273
  if (signal.aborted) onAbort();
339
274
  else signal.addEventListener("abort", onAbort, { once: true });
340
275
  });
341
276
  try {
342
277
  return await Promise.race([call, cancellation]);
343
278
  } catch (error) {
344
- if (isCancelled(error)) call.then((value) => {
279
+ if (require_settings.isCancelled(error)) call.then((value) => {
345
280
  if (onLateResult) session.scope.defer(() => onLateResult(value));
346
281
  }, () => {});
347
282
  throw error;
@@ -351,19 +286,40 @@ function createKernel(opts) {
351
286
  }
352
287
  }
353
288
  function createSession(id, name) {
289
+ const cancel = new AbortController();
354
290
  const session = {
355
291
  id,
292
+ instanceId: `${id}#${++instanceCounter}`,
356
293
  name,
294
+ source: null,
295
+ openOptions: void 0,
357
296
  phase: "opening",
358
297
  handle: null,
359
298
  stagedMeta: null,
360
- scope: createScope(report),
299
+ scope: require_settings.createScope(report),
361
300
  operation: null,
362
301
  engineOp: null,
363
- cancel: new AbortController(),
302
+ cancel,
303
+ signal: cancel.signal,
364
304
  capabilities: /* @__PURE__ */ new Map(),
305
+ building: /* @__PURE__ */ new Set(),
306
+ views: /* @__PURE__ */ new Map(),
307
+ connectors: /* @__PURE__ */ new Map(),
308
+ leases: /* @__PURE__ */ new Map(),
309
+ settleFlushes: /* @__PURE__ */ new Set(),
310
+ downloadWraps: [],
311
+ printWraps: [],
312
+ changes: require_settings.createDocumentChanges({
313
+ handle: () => session.handle,
314
+ notify: () => store.notify(),
315
+ report
316
+ }),
317
+ changeCount: 0,
318
+ changesSeen: 0,
365
319
  close: () => closeSession(session)
366
320
  };
321
+ session.settleFlushes.add(() => session.changes.settle());
322
+ session.scope.defer(() => session.changes.close());
367
323
  return session;
368
324
  }
369
325
  /** One transition at a time per session (open, then possibly unlock). */
@@ -381,14 +337,15 @@ function createKernel(opts) {
381
337
  const previousId = session.id;
382
338
  if (previousId === nextId) return;
383
339
  const core = store.getCore();
384
- if (sessions.has(nextId) || core.documents[nextId] || core.pending[nextId]) throw new Error(`[documents] duplicate document id: ${nextId}`);
340
+ if (sessions.has(nextId) || core.documents[nextId] || core.pending[nextId]) throw new require_settings.PluginError("conflict", "documents", `duplicate document id: ${nextId}`);
385
341
  sessions.delete(previousId);
386
342
  session.id = nextId;
387
343
  sessions.set(nextId, session);
344
+ session.views.clear();
388
345
  const slot = core.pending[previousId];
389
346
  if (slot) {
390
347
  const { [previousId]: _moved, ...pending } = core.pending;
391
- store.setCore({
348
+ setCore({
392
349
  pending: {
393
350
  ...pending,
394
351
  [nextId]: {
@@ -398,7 +355,7 @@ function createKernel(opts) {
398
355
  },
399
356
  order: core.order.map((id) => id === previousId ? nextId : id),
400
357
  activeId: core.activeId === previousId ? nextId : core.activeId
401
- }, { type: CORE_ORDER_CHANGED });
358
+ });
402
359
  }
403
360
  }
404
361
  const closingSessions = /* @__PURE__ */ new WeakMap();
@@ -408,14 +365,17 @@ function createKernel(opts) {
408
365
  const closing = (async () => {
409
366
  session.phase = "closing";
410
367
  unpublishSlot(session.id);
411
- const reason = new CancelledError(`closed while opening: ${session.id}`);
368
+ for (const lease of session.leases.values()) lease.revoke();
369
+ const reason = new require_settings.CancelledError(`closed while opening: ${session.id}`);
412
370
  session.cancel.abort(reason);
413
371
  session.engineOp?.abort(reason);
414
372
  await session.operation?.catch((error) => {
415
- if (!isCancelled(error) && !isAbortLike(error)) report(error);
373
+ if (!require_settings.isCancelled(error) && !isAbortLike(error)) report(error);
416
374
  });
417
375
  await session.scope.dispose();
418
376
  if (sessions.get(session.id) === session) sessions.delete(session.id);
377
+ session.views.clear();
378
+ closed.emit({ documentId: session.id });
419
379
  })();
420
380
  closingSessions.set(session, closing);
421
381
  return closing;
@@ -428,7 +388,7 @@ function createKernel(opts) {
428
388
  }
429
389
  function publishPendingSlot(session, activate) {
430
390
  const core = store.getCore();
431
- store.setCore({
391
+ setCore({
432
392
  pending: {
433
393
  ...core.pending,
434
394
  [session.id]: {
@@ -437,13 +397,13 @@ function createKernel(opts) {
437
397
  status: "loading"
438
398
  }
439
399
  },
440
- order: [...core.order, session.id],
400
+ order: core.order.includes(session.id) ? core.order : [...core.order, session.id],
441
401
  activeId: activate || core.activeId === null ? session.id : core.activeId
442
- }, { type: CORE_DOCUMENT_OPENING });
402
+ });
443
403
  }
444
404
  function publishLocked(session, passwordProvided) {
445
405
  const core = store.getCore();
446
- store.setCore({ pending: {
406
+ setCore({ pending: {
447
407
  ...core.pending,
448
408
  [session.id]: {
449
409
  id: session.id,
@@ -451,11 +411,15 @@ function createKernel(opts) {
451
411
  status: "locked",
452
412
  passwordProvided
453
413
  }
454
- } }, { type: CORE_DOCUMENT_LOCKED });
414
+ } });
415
+ locked.emit({
416
+ documentId: session.id,
417
+ passwordProvided
418
+ });
455
419
  }
456
420
  function publishError(session, error) {
457
421
  const core = store.getCore();
458
- store.setCore({ pending: {
422
+ setCore({ pending: {
459
423
  ...core.pending,
460
424
  [session.id]: {
461
425
  id: session.id,
@@ -463,35 +427,70 @@ function createKernel(opts) {
463
427
  status: "error",
464
428
  error
465
429
  }
466
- } }, { type: CORE_DOCUMENT_OPEN_FAILED });
430
+ } });
431
+ openFailed.emit({
432
+ documentId: session.id,
433
+ error: require_settings.toPluginErrorInfo(require_settings.toPluginError("documents", error))
434
+ });
467
435
  }
468
436
  function unpublishSlot(id) {
469
437
  const core = store.getCore();
470
438
  if (!core.pending[id] && !core.documents[id]) return;
471
439
  const { [id]: _pending, ...pending } = core.pending;
472
440
  const { [id]: _document, ...documents } = core.documents;
473
- store.setCore({
441
+ setCore({
474
442
  pending,
475
443
  documents,
476
444
  order: core.order.filter((other) => other !== id),
477
445
  activeId: nextActiveDocument(core, id)
478
- }, { type: CORE_DOCUMENT_REMOVED });
446
+ });
479
447
  }
480
- /** The ONE ready transition: swap the pending slot for the staged meta and
481
- * fire CORE_DOCUMENT_ADDED. Everything before this is unpublished and rolls
448
+ /** The one ready transition: swap the pending slot for the staged meta and
449
+ * announce `onOpened`. Everything before this is unpublished and rolls
482
450
  * back by disposing the session scope; nothing after this can fail. */
483
451
  function commitReady(session) {
484
452
  session.phase = "ready";
485
453
  const meta = session.stagedMeta;
486
454
  const core = store.getCore();
487
455
  const { [session.id]: _resolved, ...pending } = core.pending;
488
- store.setCore({
456
+ setCore({
489
457
  documents: {
490
458
  ...core.documents,
491
459
  [session.id]: meta
492
460
  },
493
461
  pending
494
- }, { type: CORE_DOCUMENT_ADDED });
462
+ });
463
+ opened.emit({
464
+ documentId: session.id,
465
+ info: documentInfoOf(meta)
466
+ });
467
+ }
468
+ /**
469
+ * The document changed, or a download read every change it had: set `hasUnsavedChanges` and
470
+ * announce a flip. Before the document is published, its staged meta carries the flag.
471
+ */
472
+ function setUnsavedChanges(session, hasUnsavedChanges) {
473
+ const core = store.getCore();
474
+ const published = core.documents[session.id];
475
+ if (!published) {
476
+ if (session.stagedMeta && session.stagedMeta.hasUnsavedChanges !== hasUnsavedChanges) session.stagedMeta = {
477
+ ...session.stagedMeta,
478
+ hasUnsavedChanges
479
+ };
480
+ return;
481
+ }
482
+ if (published.hasUnsavedChanges === hasUnsavedChanges) return;
483
+ setCore({ documents: {
484
+ ...core.documents,
485
+ [session.id]: {
486
+ ...published,
487
+ hasUnsavedChanges
488
+ }
489
+ } });
490
+ unsavedChangesChanged.emit({
491
+ documentId: session.id,
492
+ hasUnsavedChanges
493
+ });
495
494
  }
496
495
  const sessionOf = (documentId) => {
497
496
  const id = documentId ?? store.getCore().activeId;
@@ -504,18 +503,278 @@ function createKernel(opts) {
504
503
  if (!session) return null;
505
504
  return session.phase === "bringup" || session.phase === "ready" ? session.handle : null;
506
505
  };
506
+ /** The session a per-document verb acts on: `documentId`, or the active document. */
507
+ function readySession(documentId, verb) {
508
+ const session = sessionOf(documentId);
509
+ if (!session || !documentHandle(session.id)) throw new require_settings.PluginError("not-ready", "documents", `no open document to ${verb}`);
510
+ return session;
511
+ }
512
+ const allowsOn = (documentId, permission) => {
513
+ const handle = documentHandle(documentId);
514
+ return handle ? require_settings.sessionAllows(handle.security, permission) : false;
515
+ };
516
+ /**
517
+ * Read the document's file, with everything the user sees: refuse without `doc.download`, let
518
+ * it settle (what its plugins held back is sent and answered, settle.ts), then read it inside
519
+ * every `aroundDownload` wrap (the document's own save actions). A read that saw every change
520
+ * the document had clears `hasUnsavedChanges`; a change during the read, or after it (a
521
+ * did-save script), keeps it. Closing the document, or `signal`, cancels it.
522
+ */
523
+ async function downloadFile(documentId, signal, verb, read, refuse) {
524
+ const session = readySession(documentId, verb);
525
+ if (!allowsOn(session.id, "doc.download")) throw require_settings.permissionDenied("documents", "doc.download", `documents.${verb}`);
526
+ const refusal = refuse?.(session.handle);
527
+ if (refusal) throw refusal;
528
+ try {
529
+ await require_settings.settle(session.settleFlushes, [signal, session.signal], report);
530
+ const handle = documentHandle(session.id);
531
+ if (!handle) throw new require_settings.PluginError("not-ready", "documents", `no open document to ${verb}`);
532
+ let readAt = -1;
533
+ const readFile = () => {
534
+ readAt = session.changeCount;
535
+ return require_settings.cancellable("documents", signal, read(handle));
536
+ };
537
+ const bytes = await require_settings.runWrapped(session.downloadWraps, readFile);
538
+ if (readAt === session.changeCount) setUnsavedChanges(session, false);
539
+ return bytes;
540
+ } catch (error) {
541
+ throw require_settings.toPluginError("documents", error);
542
+ }
543
+ }
544
+ const printer = config.printer ?? null;
545
+ const canPrintOn = (documentId) => printer !== null && allowsOn(documentId, "doc.print");
546
+ /**
547
+ * How a print comes out: the PDF copy with `doc.print.high`, a right to keep the copy (every
548
+ * print dialog can save it as a file) and a printer that prints a PDF; else pictures, at the
549
+ * resolution `doc.print.high` or `doc.print` allows.
550
+ */
551
+ const printQualityOf = (documentId) => {
552
+ if (!canPrintOn(documentId)) return null;
553
+ if (!allowsOn(documentId, "doc.print.high")) return PRINT_STANDARD;
554
+ return (allowsOn(documentId, "doc.download") || allowsOn(documentId, "doc.download.flattened")) && printer?.stagePdf ? PRINT_PDF : PRINT_HIGH;
555
+ };
556
+ /** The pages a print asks for, in its order: every page when it names none. */
557
+ function printPagesOf(session, pages) {
558
+ const all = metaOf(session.id)?.pages ?? EMPTY_PAGES;
559
+ if (pages === void 0) return all;
560
+ if (pages.length === 0) throw new require_settings.PluginError("invalid-input", "documents", "a print needs at least one page");
561
+ const found = pages.map((ref) => {
562
+ const page = require_settings.findPage(all, ref);
563
+ if (!page) throw new require_settings.PluginError("not-found", "documents", `page ${ref.objectNumber} is not in this document`);
564
+ return page;
565
+ });
566
+ if (new Set(found).size !== found.length) throw new require_settings.PluginError("invalid-input", "documents", "a print names a page twice");
567
+ return found;
568
+ }
569
+ /**
570
+ * What prints beside the page content: the annotations as the options ask, by default when
571
+ * the user may read them; the form fields whenever the user may read the form.
572
+ */
573
+ const printLayersOf = (documentId, options) => ({
574
+ includeAnnotations: options.includeAnnotations ?? allowsOn(documentId, "doc.annotate.read"),
575
+ includeFormFields: allowsOn(documentId, "doc.forms.read")
576
+ });
577
+ /**
578
+ * Make the pages as they print and hand them to the printer, once the document settled: the
579
+ * PDF copy, or a picture of each page. The pages are looked up again each time, so one deleted
580
+ * meanwhile refuses `not-found`. Returns the staged print, how it came out, and the change it
581
+ * saw last, so a print can tell it went stale.
582
+ */
583
+ async function stagePrint(session, activePrinter, quality, options) {
584
+ const { signal } = options;
585
+ await require_settings.settle(session.settleFlushes, [signal, session.signal], report);
586
+ const handle = documentHandle(session.id);
587
+ if (!handle) throw new require_settings.PluginError("not-ready", "documents", "no open document to print");
588
+ const changesSeen = session.changesSeen;
589
+ const job = {
590
+ handle,
591
+ pages: printPagesOf(session, options.pages),
592
+ layers: printLayersOf(session.id, options),
593
+ name: session.name ?? "document",
594
+ options
595
+ };
596
+ options.onProgress?.({
597
+ pagesRendered: 0,
598
+ pageCount: job.pages.length
599
+ });
600
+ if (quality.kind === "images") return {
601
+ staged: await stagePictures(job, activePrinter, quality.dpi),
602
+ quality,
603
+ changesSeen
604
+ };
605
+ try {
606
+ return {
607
+ staged: await stagePdfCopy(job, activePrinter),
608
+ quality,
609
+ changesSeen
610
+ };
611
+ } catch (error) {
612
+ if (signal?.aborted) throw error;
613
+ return {
614
+ staged: await stagePictures(job, activePrinter, PRINT_HIGH.dpi),
615
+ quality: PRINT_HIGH,
616
+ changesSeen
617
+ };
618
+ }
619
+ }
620
+ /** The PDF copy of the pages as they print, staged with the printer. */
621
+ async function stagePdfCopy(job, activePrinter) {
622
+ const { handle, pages, layers, name, options } = job;
623
+ const { signal } = options;
624
+ if (!activePrinter.stagePdf) throw new require_settings.PluginError("unsupported", "documents", "this printer prints no PDF");
625
+ const pdf = await require_settings.cancellable("documents", signal, handle.pages.extract(pages.map((page) => page.ref), {
626
+ flatten: { usage: "print" },
627
+ ...layers
628
+ }));
629
+ const staged = await activePrinter.stagePdf(pdf, {
630
+ name,
631
+ ...signal ? { signal } : {}
632
+ });
633
+ options.onProgress?.({
634
+ pagesRendered: pages.length,
635
+ pageCount: pages.length
636
+ });
637
+ return staged;
638
+ }
639
+ /** A picture of each page, drawn by the print rules at `dpi`, staged with the printer. */
640
+ async function stagePictures(job, activePrinter, dpi) {
641
+ const { handle, pages, layers, name, options } = job;
642
+ const { signal } = options;
643
+ const sheets = [];
644
+ for (const page of pages) {
645
+ const blob = await require_settings.cancellable("documents", signal, (await require_settings.cancellable("documents", signal, handle.page(page.ref).render.image({
646
+ usage: "print",
647
+ viewport: {
648
+ kind: "dpi",
649
+ dpi
650
+ },
651
+ format: "png",
652
+ ...layers
653
+ }))).blob());
654
+ sheets.push({
655
+ image: blob,
656
+ widthPt: page.size.width,
657
+ heightPt: page.size.height
658
+ });
659
+ options.onProgress?.({
660
+ pagesRendered: sheets.length,
661
+ pageCount: pages.length
662
+ });
663
+ }
664
+ return activePrinter.stage(sheets, {
665
+ name,
666
+ ...signal ? { signal } : {}
667
+ });
668
+ }
669
+ /**
670
+ * Get a print ready. Refused before anything runs without `doc.print` or a printer, or for
671
+ * pages the document doesn't have. Its `print()` runs inside every `aroundPrint` wrap (the
672
+ * document's own print actions), and prepares again first when the document changed since:
673
+ * a will-print action that fills in a date is on the paper. When the printer turns the PDF
674
+ * copy down, preparing or printing, the same pages print as pictures, and `quality` says so.
675
+ */
676
+ async function preparePrint(documentId, options = {}) {
677
+ const session = readySession(documentId, "preparePrint");
678
+ if (!allowsOn(session.id, "doc.print")) throw require_settings.permissionDenied("documents", "doc.print", "documents.preparePrint");
679
+ const quality = printQualityOf(session.id);
680
+ if (!printer || !quality) throw new require_settings.PluginError("unsupported", "documents", "no printer: pass one to createKernel({ printer }), such as browserPrinter() from @embedpdf/web");
681
+ const pageCount = printPagesOf(session, options.pages).length;
682
+ try {
683
+ let { staged, changesSeen, quality: printed } = await stagePrint(session, printer, quality, options);
684
+ let disposed = false;
685
+ const dispose = () => {
686
+ if (disposed) return;
687
+ disposed = true;
688
+ staged.dispose();
689
+ };
690
+ session.scope.defer(dispose);
691
+ /** Stage the print again, as `as`, in place of what was staged. */
692
+ const restage = async (as, signal) => {
693
+ const fresh = await stagePrint(session, printer, as, {
694
+ ...options.pages ? { pages: options.pages } : {},
695
+ ...options.includeAnnotations !== void 0 ? { includeAnnotations: options.includeAnnotations } : {},
696
+ ...signal ? { signal } : {}
697
+ });
698
+ staged.dispose();
699
+ ({staged, changesSeen, quality: printed} = fresh);
700
+ };
701
+ return {
702
+ get quality() {
703
+ return printed;
704
+ },
705
+ pageCount,
706
+ needsTap: printer.needsTap === true,
707
+ async print(printOptions) {
708
+ if (disposed) throw new require_settings.PluginError("invalid-input", "documents", "this print was disposed");
709
+ const signal = printOptions?.signal;
710
+ const run = async () => {
711
+ if (session.changesSeen !== changesSeen) await restage(quality, signal);
712
+ if (signal?.aborted) throw new require_settings.PluginError("operation-cancelled", "documents", "print cancelled", { cause: signal.reason });
713
+ try {
714
+ await staged.print();
715
+ } catch (error) {
716
+ if (printed.kind !== "pdf") throw error;
717
+ await restage(PRINT_HIGH, signal);
718
+ await staged.print();
719
+ }
720
+ };
721
+ try {
722
+ await require_settings.runWrapped(session.printWraps, run);
723
+ } catch (error) {
724
+ throw require_settings.toPluginError("documents", error);
725
+ }
726
+ },
727
+ dispose
728
+ };
729
+ } catch (error) {
730
+ throw require_settings.toPluginError("documents", error);
731
+ }
732
+ }
733
+ /** Whether the plugin's `create()` is running for this document: it has no capability yet. */
734
+ const isBuilding = (plugin, session) => session.building.has(plugin) && !session.capabilities.has(plugin);
507
735
  function buildDocumentCapability(plugin, session) {
508
736
  let capability = session.capabilities.get(plugin);
509
737
  if (!capability) {
510
- capability = plugin.capability(createPluginContext(services, plugin, session));
738
+ if (session.building.has(plugin)) throw new require_settings.PluginError("not-ready", plugin.id, `"${plugin.id}" was asked for while it is being created`);
739
+ const ctx = require_settings.createPluginContext(services, plugin, session, session.signal, session.scope);
740
+ session.building.add(plugin);
741
+ let created;
742
+ try {
743
+ created = plugin.create(ctx);
744
+ } finally {
745
+ session.building.delete(plugin);
746
+ }
747
+ const { api, connect } = created;
748
+ capability = api;
749
+ session.connectors.set(plugin, () => {
750
+ connect?.();
751
+ require_settings.markConnected(ctx);
752
+ });
511
753
  session.capabilities.set(plugin, capability);
512
754
  }
513
755
  return capability;
514
756
  }
757
+ /**
758
+ * A workspace capability as a document's scope sees it: the view its plugin declares
759
+ * (`inScope`), built once per document and kept on the session, so it is the same object on
760
+ * every call and goes when the document closes. Without a document, or for a document the
761
+ * kernel doesn't know, it is the capability itself.
762
+ */
763
+ function workspaceViewOf(token, capability, documentId) {
764
+ const inScope = inScopeByToken.get(token);
765
+ const session = documentId === void 0 ? void 0 : sessions.get(documentId);
766
+ if (!inScope || !session) return capability;
767
+ let view = session.views.get(token);
768
+ if (view === void 0) {
769
+ view = inScope(capability, session.id);
770
+ session.views.set(token, view);
771
+ }
772
+ return view;
773
+ }
515
774
  function resolveCapability(token, documentId) {
516
775
  guardUsable(`capability("${token.name}")`);
517
776
  const workspaceCapability = workspaceCapabilities.get(token);
518
- if (workspaceCapability) return workspaceCapability;
777
+ if (workspaceCapability) return workspaceViewOf(token, workspaceCapability, documentId);
519
778
  const provider = plan.providerOf(token);
520
779
  if (!provider) throw new Error(`No capability "${token.name}".`);
521
780
  const id = documentId ?? store.getCore().activeId;
@@ -531,55 +790,94 @@ function createKernel(opts) {
531
790
  function tryResolveInternal(token, documentId) {
532
791
  if (status === "destroying" || status === "destroyed") return null;
533
792
  const workspaceCapability = workspaceCapabilities.get(token);
534
- if (workspaceCapability) return workspaceCapability;
793
+ if (workspaceCapability) return workspaceViewOf(token, workspaceCapability, documentId);
535
794
  const provider = plan.providerOf(token);
536
795
  if (!provider) return null;
537
796
  const session = sessionOf(documentId);
538
797
  if (!session || session.phase !== "bringup" && session.phase !== "ready") return null;
798
+ if (isBuilding(provider, session)) return null;
539
799
  return buildDocumentCapability(provider, session);
540
800
  }
541
801
  /** Public total resolver — see `Kernel.tryCapability`. `ready` only: the
542
- * null→instance flip at commit time IS the adapters' re-render signal. */
802
+ * null→instance flip at commit time is the adapters' re-render signal. */
543
803
  function tryResolveCapability(token, documentId) {
544
804
  if (status === "destroying" || status === "destroyed") return null;
545
805
  const workspaceCapability = workspaceCapabilities.get(token);
546
- if (workspaceCapability) return workspaceCapability;
806
+ if (workspaceCapability) return workspaceViewOf(token, workspaceCapability, documentId);
547
807
  const provider = plan.providerOf(token);
548
808
  if (!provider) return null;
549
809
  const session = sessionOf(documentId);
550
810
  if (!session || session.phase !== "ready") return null;
811
+ if (isBuilding(provider, session)) return null;
551
812
  return buildDocumentCapability(provider, session);
552
813
  }
814
+ const viewerSettings = require_settings.createSettingsStore({
815
+ defaults: VIEWER_DEFAULTS,
816
+ registered: config.settings,
817
+ whole: VIEWER_WHOLE_SETTINGS
818
+ }, store.notify, report);
819
+ workspaceScope.defer(() => viewerSettings.dispose());
820
+ const settingsStores = /* @__PURE__ */ new Map();
821
+ for (const plugin of plan.ordered) if (plugin.settings) settingsStores.set(plugin, require_settings.createSettingsStore(plugin.settings, store.notify, report));
822
+ workspaceScope.defer(() => {
823
+ for (const settings of settingsStores.values()) settings.dispose();
824
+ });
825
+ function settingsOf(token) {
826
+ const provider = plan.providerOf(token);
827
+ if (!provider) throw new Error(`No capability "${token.name}".`);
828
+ const settings = settingsStores.get(provider);
829
+ if (!settings) throw new Error(`Plugin "${provider.id}" has no settings.`);
830
+ return settings.api;
831
+ }
553
832
  const services = {
554
833
  engine,
555
834
  store,
556
835
  workspaceScope,
836
+ workspaceSignal: workspaceCancel.signal,
837
+ workspaceLeases,
838
+ report,
839
+ clock: config.clock ?? require_settings.timerClock,
557
840
  resolveCapability,
558
841
  tryResolveCapability: tryResolveInternal,
559
- documentHandle
842
+ documentHandle,
843
+ settingsStoreOf: (plugin) => settingsStores.get(plugin)
560
844
  };
561
845
  let ticketCounter = 0;
562
846
  const nextTicket = () => `pending:${++ticketCounter}`;
563
- /** Slices + event subscription + plugin inits + effect SETUP — every step's
564
- * release deferred into the session scope, every await followed by a
565
- * checkpoint. Runs entirely pre-commit: a failure anywhere rolls the whole
566
- * session back and the document was never `ready`. */
847
+ /** Slices, the registry's event subscription, and every plugin's
848
+ * construction and connection — every step's release deferred into the
849
+ * session scope, every await followed by a checkpoint. Runs entirely
850
+ * pre-commit: a failure anywhere rolls the whole session back and the
851
+ * document was never `ready`. */
567
852
  async function bringUp(session, snapshot) {
568
853
  session.phase = "bringup";
569
854
  let renderPolicy = _embedpdf_engine_core_runtime.CONTINUOUS_RENDER_POLICY;
570
855
  try {
571
- renderPolicy = await session.handle.render?.policy() ?? _embedpdf_engine_core_runtime.CONTINUOUS_RENDER_POLICY;
856
+ renderPolicy = await session.handle.render?.getPolicy() ?? _embedpdf_engine_core_runtime.CONTINUOUS_RENDER_POLICY;
572
857
  } catch {}
573
858
  checkpoint(session);
574
859
  session.stagedMeta = {
575
860
  id: session.id,
861
+ instanceId: session.instanceId,
576
862
  name: session.name,
577
863
  pageCount: snapshot.pageCount,
578
864
  pages: snapshot.pages,
579
865
  revision: 0,
580
- renderPolicy
866
+ renderPolicy,
867
+ hasUnsavedChanges: false
581
868
  };
869
+ const tracksChanges = (0, _embedpdf_engine_core_runtime.isLocalDocument)(session.handle);
870
+ const bursts = require_settings.createBurstGate(store);
871
+ session.scope.defer(bursts.close);
582
872
  const unsubscribeEvents = session.handle.events.subscribe((event) => {
873
+ bursts.first(event);
874
+ if (changesDocument(event)) {
875
+ session.changesSeen += 1;
876
+ if (tracksChanges) {
877
+ session.changeCount += 1;
878
+ setUnsavedChanges(session, true);
879
+ }
880
+ }
583
881
  const layout = layoutFromEvent(event);
584
882
  if (!layout) return;
585
883
  const now = store.getCore();
@@ -591,74 +889,133 @@ function createKernel(opts) {
591
889
  pages: layout.pages,
592
890
  revision: existing.revision + 1
593
891
  };
594
- store.setCore({ documents: {
892
+ setCore({ documents: {
595
893
  ...now.documents,
596
894
  [session.id]: updated
597
- } }, { type: CORE_DOCUMENT_PAGES_UPDATED });
895
+ } });
896
+ pagesChanged.emit({
897
+ documentId: session.id,
898
+ revision: updated.revision,
899
+ pages: updated.pages
900
+ });
598
901
  });
599
902
  session.scope.defer(unsubscribeEvents);
600
903
  for (const plugin of documentScopedPlugins) {
601
- const key = sliceKey(plugin.id, session.id);
602
- store.registerSlice(key, reducerOf(plugin), initialStateOf(plugin));
603
- session.scope.defer(() => store.removeSlice(key));
904
+ const lease = store.lease(require_settings.sliceKey(plugin.id, session.id), initialStateOf(plugin), session.instanceId);
905
+ session.leases.set(plugin, lease);
906
+ session.scope.defer(() => lease.revoke());
604
907
  }
605
- for (const plugin of documentScopedPlugins) {
606
- await plugin.init?.(createPluginContext(services, plugin, session));
607
- checkpoint(session);
908
+ const release = store.hold();
909
+ try {
910
+ for (const plugin of documentScopedPlugins) {
911
+ buildDocumentCapability(plugin, session);
912
+ session.connectors.get(plugin)?.();
913
+ }
914
+ } finally {
915
+ release();
608
916
  }
609
- for (const plugin of documentScopedPlugins) plugin.effects?.(createEffectContext(services, plugin, session));
917
+ session.scope.defer(session.handle.events.subscribe((event) => bursts.last(event)));
610
918
  checkpoint(session);
611
919
  }
612
- async function openDocument(input, options) {
920
+ /** What `open()` and `retry()` resolve: the document as its tab shows it, ready or locked. */
921
+ function openedResult(session) {
922
+ const core = store.getCore();
923
+ const entry = core.documents[session.id] ?? core.pending[session.id];
924
+ if (!entry) throw new require_settings.PluginError("instance-closed", "documents", `${session.id} was closed`);
925
+ return { document: documentInfoOf(entry) };
926
+ }
927
+ async function openDocument(input, options, slotId) {
928
+ const signal = options?.signal;
929
+ if (signal?.aborted) throw new require_settings.PluginError("operation-cancelled", "documents", "the open was cancelled");
930
+ try {
931
+ const { session, done } = reserveAndOpen(input, options, slotId);
932
+ const cancel = () => void session.close();
933
+ signal?.addEventListener("abort", cancel, { once: true });
934
+ try {
935
+ return openedResult(await done);
936
+ } finally {
937
+ signal?.removeEventListener("abort", cancel);
938
+ }
939
+ } catch (error) {
940
+ throw require_settings.toPluginError("documents", error);
941
+ }
942
+ }
943
+ /**
944
+ * The engine's open options for a document: its own, and the viewer's `scope` and
945
+ * `identity` where it has none. A document's own replace the viewer's, never merge.
946
+ */
947
+ function engineOptionsOf(options) {
948
+ const { activate: _activate, name: _name, signal: _signal, ...own } = options ?? {};
949
+ const viewer = viewerSettings.api.getSettings();
950
+ const scope = own.scope ?? viewer.scope ?? void 0;
951
+ const identity = own.identity ?? viewer.identity ?? void 0;
952
+ return {
953
+ ...own,
954
+ ...scope !== void 0 ? { scope } : {},
955
+ ...identity !== void 0 ? { identity } : {}
956
+ };
957
+ }
958
+ /**
959
+ * Reserve the tab slot synchronously and start the open; returns the slot id at once.
960
+ * `slotId` is a failed document's tab, which `retry()` opens again in place.
961
+ */
962
+ function reserveAndOpen(input, options, slotId) {
613
963
  guardUsable("documents.open()");
614
- const { activate, name, ...engineOptions } = options ?? {};
615
- const requestedId = typeof input === "function" ? nextTicket() : idOfInput(input) ?? nextTicket();
616
- if (sessions.has(requestedId)) throw new Error(`[documents] document already open: ${requestedId}`);
617
- const session = createSession(requestedId, name);
964
+ const engineOptions = engineOptionsOf(options);
965
+ const requestedId = slotId ?? (typeof input === "function" ? nextTicket() : idOfInput(input) ?? nextTicket());
966
+ if (sessions.has(requestedId)) throw new require_settings.PluginError("conflict", "documents", `document already open: ${requestedId}`);
967
+ const session = createSession(requestedId, options?.name);
968
+ session.source = input;
969
+ session.openOptions = options;
618
970
  sessions.set(session.id, session);
619
- publishPendingSlot(session, activate ?? true);
620
- return beginOperation(session, async () => {
621
- try {
622
- const source = typeof input === "function" ? await engineCall(session, Promise.resolve(input(session.cancel.signal))) : input;
623
- checkpoint(session);
624
- const sourceId = idOfInput(source);
625
- if (sourceId) rekeySession(session, sourceId);
626
- const handle = await engineCall(session, engine.open(source, engineOptions), (late) => late.close());
627
- session.handle = handle;
628
- session.scope.defer(() => handle.close());
629
- checkpoint(session);
630
- if (handle.id !== session.id) rekeySession(session, handle.id);
631
- if (handle.security?.passwordPrompt?.state === "required") {
632
- const passwordProvided = "password" in engineOptions && engineOptions.password != null || passwordOfInput(source) != null;
633
- session.phase = "locked";
634
- publishLocked(session, passwordProvided);
635
- return session.id;
971
+ publishPendingSlot(session, options?.activate ?? true);
972
+ return {
973
+ session,
974
+ done: beginOperation(session, async () => {
975
+ try {
976
+ const given = typeof input === "function" ? await engineCall(session, Promise.resolve(input(session.cancel.signal))) : input;
977
+ checkpoint(session);
978
+ const source = isUrlSource(given) ? await engineCall(session, fetchUrlSource(given, session.cancel.signal)) : given;
979
+ checkpoint(session);
980
+ const sourceId = idOfInput(source);
981
+ if (sourceId) rekeySession(session, sourceId);
982
+ const handle = await engineCall(session, engine.open(source, engineOptions), (late) => late.close());
983
+ session.handle = handle;
984
+ session.scope.defer(() => handle.close());
985
+ checkpoint(session);
986
+ if (handle.id !== session.id) rekeySession(session, handle.id);
987
+ const prompt = handle.security?.passwordPrompt;
988
+ if (prompt?.state === "required") {
989
+ session.phase = "locked";
990
+ publishLocked(session, prompt.incorrect);
991
+ return session;
992
+ }
993
+ const snapshot = await engineCall(session, handle.pages.list());
994
+ checkpoint(session);
995
+ await bringUp(session, snapshot);
996
+ commitReady(session);
997
+ return session;
998
+ } catch (error) {
999
+ if (require_settings.isCancelled(error)) throw error;
1000
+ if (session.phase === "closing" || isAbortLike(error)) throw new require_settings.CancelledError(`closed while opening: ${session.id}`);
1001
+ await session.scope.dispose();
1002
+ session.phase = "error";
1003
+ publishError(session, error);
1004
+ throw error;
636
1005
  }
637
- const snapshot = await engineCall(session, handle.pages.list());
638
- checkpoint(session);
639
- await bringUp(session, snapshot);
640
- commitReady(session);
641
- return session.id;
642
- } catch (error) {
643
- if (isCancelled(error)) throw error;
644
- if (session.phase === "closing" || isAbortLike(error)) throw new CancelledError(`closed while opening: ${session.id}`);
645
- await session.scope.dispose();
646
- session.phase = "error";
647
- publishError(session, error);
648
- throw error;
649
- }
650
- });
1006
+ })
1007
+ };
651
1008
  }
652
- async function unlockDocument(id, input) {
1009
+ async function unlockDocument(id, options) {
653
1010
  guardUsable("documents.unlock()");
654
1011
  const session = sessions.get(id);
655
- if (!session || session.phase !== "locked" || !session.handle) throw new Error(`[documents] document is not locked: ${id}`);
1012
+ if (!session || session.phase !== "locked" || !session.handle) throw new require_settings.PluginError("invalid-input", "documents", `document is not locked: ${id}`);
656
1013
  const handle = session.handle;
657
1014
  return beginOperation(session, async () => {
658
1015
  try {
659
- await engineCall(session, handle.security.unlock({ password: input.password }));
1016
+ await require_settings.cancellable("documents", options.signal, engineCall(session, handle.security.unlock({ password: options.password })));
660
1017
  } catch (error) {
661
- if (session.phase === "closing" || isAbortLike(error)) throw new CancelledError(`closed while opening: ${session.id}`);
1018
+ if (session.phase === "closing" || isAbortLike(error)) throw new require_settings.CancelledError(`closed while opening: ${session.id}`);
662
1019
  throw error;
663
1020
  }
664
1021
  checkpoint(session);
@@ -668,13 +1025,15 @@ function createKernel(opts) {
668
1025
  await bringUp(session, snapshot);
669
1026
  commitReady(session);
670
1027
  } catch (error) {
671
- if (isCancelled(error)) throw error;
672
- if (session.phase === "closing" || isAbortLike(error)) throw new CancelledError(`closed while opening: ${session.id}`);
1028
+ if (require_settings.isCancelled(error)) throw error;
1029
+ if (session.phase === "closing" || isAbortLike(error)) throw new require_settings.CancelledError(`closed while opening: ${session.id}`);
673
1030
  await session.scope.dispose();
674
1031
  session.phase = "error";
675
1032
  publishError(session, error);
676
1033
  throw error;
677
1034
  }
1035
+ }).catch((error) => {
1036
+ throw require_settings.toPluginError("documents", error);
678
1037
  });
679
1038
  }
680
1039
  async function closeDocument(documentId) {
@@ -683,18 +1042,81 @@ function createKernel(opts) {
683
1042
  await session.close();
684
1043
  }
685
1044
  function reorder(next) {
686
- store.setCore({ order: next }, { type: CORE_ORDER_CHANGED });
1045
+ setCore({ order: next });
687
1046
  }
1047
+ const metaOf = (documentId) => {
1048
+ const core = store.getCore();
1049
+ const id = documentId ?? core.activeId;
1050
+ return id ? core.documents[id] ?? null : null;
1051
+ };
1052
+ /** The tab list: the same array until one of its documents changes, or the order does. */
1053
+ let listMemo = null;
1054
+ const listDocuments = () => {
1055
+ const core = store.getCore();
1056
+ if (listMemo?.core === core) return listMemo.value;
1057
+ const next = core.order.map((id) => documentInfoOf(core.documents[id] ?? core.pending[id]));
1058
+ const previous = listMemo?.value;
1059
+ const value = previous !== void 0 && previous.length === next.length && previous.every((info, i) => info === next[i]) ? previous : Object.freeze(next);
1060
+ listMemo = {
1061
+ core,
1062
+ value
1063
+ };
1064
+ return value;
1065
+ };
1066
+ /** A document's fields: `documentId`, or the active document's. */
1067
+ const getDocument = (documentId) => {
1068
+ const core = store.getCore();
1069
+ const id = documentId ?? core.activeId;
1070
+ const entry = id ? core.documents[id] ?? core.pending[id] : void 0;
1071
+ return entry ? documentInfoOf(entry) : null;
1072
+ };
688
1073
  const documents = {
689
1074
  open: openDocument,
690
- openAll: (docs) => {
1075
+ canOpenFiles: () => engine.canOpenFiles,
1076
+ retry: async (id, options) => {
1077
+ const session = sessions.get(id);
1078
+ if (!session || session.phase !== "error" || session.source === null) throw new require_settings.PluginError("invalid-input", "documents", `${id} didn't fail to open; nothing to retry`);
1079
+ const { source, openOptions } = session;
1080
+ sessions.delete(id);
1081
+ return openDocument(source, {
1082
+ ...openOptions,
1083
+ activate: false,
1084
+ signal: options?.signal
1085
+ }, id);
1086
+ },
1087
+ rename: (id, name) => {
1088
+ const core = store.getCore();
1089
+ if (core.documents[id]) setCore({ documents: {
1090
+ ...core.documents,
1091
+ [id]: {
1092
+ ...core.documents[id],
1093
+ name
1094
+ }
1095
+ } });
1096
+ else if (core.pending[id]) setCore({ pending: {
1097
+ ...core.pending,
1098
+ [id]: {
1099
+ ...core.pending[id],
1100
+ name
1101
+ }
1102
+ } });
1103
+ const session = sessions.get(id);
1104
+ if (session) session.name = name;
1105
+ },
1106
+ openAll: (initialDocuments) => {
691
1107
  guardUsable("documents.openAll()");
692
- const activeIndex = Math.max(0, docs.findIndex((d) => d.active));
693
- docs.forEach(({ source, active: _active, ...options }, index) => {
694
- openDocument(source, {
695
- ...options,
696
- activate: index === activeIndex
697
- }).catch(() => {});
1108
+ const activeIndex = Math.max(0, initialDocuments.findIndex((initialDocument) => initialDocument.active));
1109
+ return initialDocuments.map(({ source, active: _active, ...options }, index) => {
1110
+ try {
1111
+ const { session, done } = reserveAndOpen(source, {
1112
+ ...options,
1113
+ activate: index === activeIndex
1114
+ });
1115
+ done.catch(() => {});
1116
+ return session.id;
1117
+ } catch {
1118
+ return typeof source === "function" ? "" : idOfInput(source) ?? "";
1119
+ }
698
1120
  });
699
1121
  },
700
1122
  unlock: unlockDocument,
@@ -704,67 +1126,87 @@ function createKernel(opts) {
704
1126
  },
705
1127
  setActive: (id) => {
706
1128
  const core = store.getCore();
707
- if (core.documents[id] || core.pending[id]) store.setCore({ activeId: id }, { type: CORE_ACTIVE_CHANGED });
708
- },
709
- activeId: () => store.getCore().activeId,
710
- list: () => store.getCore().order.map((id) => {
711
- const core = store.getCore();
712
- const meta = core.documents[id];
713
- return meta ? toDocInfo(meta) : pendingToDocInfo(core.pending[id]);
714
- }),
715
- get: (id) => {
716
- const core = store.getCore();
717
- const meta = core.documents[id];
718
- if (meta) return toDocInfo(meta);
719
- const pending = core.pending[id];
720
- return pending ? pendingToDocInfo(pending) : null;
1129
+ if (core.documents[id] || core.pending[id]) setCore({ activeId: id });
721
1130
  },
1131
+ getActiveId: () => store.getCore().activeId,
1132
+ getActive: () => getDocument(),
1133
+ list: listDocuments,
1134
+ get: getDocument,
722
1135
  has: (id) => {
723
1136
  const core = store.getCore();
724
1137
  return core.documents[id] !== void 0 || core.pending[id] !== void 0;
725
1138
  },
726
- count: () => store.getCore().order.length,
727
- order: () => [...store.getCore().order],
1139
+ getCount: () => store.getCore().order.length,
1140
+ getOrder: () => store.getCore().order,
1141
+ setOrder: (ids) => {
1142
+ const current = store.getCore().order;
1143
+ if (!(ids.length === current.length && [...ids].sort().join("\0") === [...current].sort().join("\0"))) throw new require_settings.PluginError("invalid-input", "documents", "setOrder() needs a permutation of the current order");
1144
+ reorder([...ids]);
1145
+ },
728
1146
  move: (id, toIndex) => {
729
1147
  const core = store.getCore();
730
1148
  if (!core.documents[id] && !core.pending[id]) return;
731
- const without = core.order.filter((x) => x !== id);
1149
+ const without = core.order.filter((documentId) => documentId !== id);
732
1150
  const clamped = Math.max(0, Math.min(toIndex, without.length));
733
1151
  without.splice(clamped, 0, id);
734
1152
  reorder(without);
735
1153
  },
736
- swap: (a, b) => {
1154
+ swap: (id, otherId) => {
737
1155
  const core = store.getCore();
738
- const indexA = core.order.indexOf(a);
739
- const indexB = core.order.indexOf(b);
740
- if (indexA < 0 || indexB < 0) return;
1156
+ const index = core.order.indexOf(id);
1157
+ const otherIndex = core.order.indexOf(otherId);
1158
+ if (index < 0 || otherIndex < 0) return;
741
1159
  const next = [...core.order];
742
- next[indexA] = b;
743
- next[indexB] = a;
1160
+ next[index] = otherId;
1161
+ next[otherIndex] = id;
744
1162
  reorder(next);
745
1163
  },
746
- download: (id, opts) => {
747
- const handle = documentHandle(id);
748
- if (!handle) return Promise.reject(/* @__PURE__ */ new Error("[documents] no document to download"));
749
- return handle.download(opts);
750
- },
751
- downloadLayer: (id) => {
752
- const handle = documentHandle(id);
753
- if (!handle) return Promise.reject(/* @__PURE__ */ new Error("[documents] no document to download"));
754
- if (!handle.downloadLayer) return Promise.reject(/* @__PURE__ */ new Error("[documents] this engine cannot export a layer (open with a layer on the local engine)"));
755
- return handle.downloadLayer();
1164
+ download: (id, options) => downloadFile(id, options?.signal, "download", (handle) => handle.download(options?.mode !== void 0 ? { mode: options.mode } : void 0)),
1165
+ downloadLayer: (id, options) => downloadFile(id, options?.signal, "downloadLayer", (handle) => (0, _embedpdf_engine_core_runtime.isLocalDocument)(handle) ? handle.downloadLayer() : Promise.reject(noLayer()), (handle) => (0, _embedpdf_engine_core_runtime.isLocalDocument)(handle) ? null : noLayer()),
1166
+ preparePrint: (id, options) => preparePrint(id, options),
1167
+ async print(id, options = {}) {
1168
+ const prepared = await preparePrint(id, options);
1169
+ try {
1170
+ await prepared.print(options.signal ? { signal: options.signal } : void 0);
1171
+ } finally {
1172
+ prepared.dispose();
1173
+ }
756
1174
  },
757
- allows: (cap, id) => documentHandle(id)?.security.allows(cap) ?? false
1175
+ getPrintQuality: (id) => printQualityOf(id),
1176
+ canDownload: (id) => allowsOn(id, "doc.download"),
1177
+ canPrint: (id) => canPrintOn(id),
1178
+ listPages: (id) => metaOf(id)?.pages ?? EMPTY_PAGES,
1179
+ getPage: (page, id) => require_settings.findPage(metaOf(id)?.pages ?? EMPTY_PAGES, page),
1180
+ getPageIndex: (ref, id) => metaOf(id)?.pages.findIndex((pageInfo) => (0, _embedpdf_engine_core_runtime.pageRefsEqual)(pageInfo.ref, ref)) ?? -1,
1181
+ getRevision: (id) => metaOf(id)?.revision ?? -1,
1182
+ onOpened: opened.on,
1183
+ onOpenFailed: openFailed.on,
1184
+ onLocked: locked.on,
1185
+ onClosed: closed.on,
1186
+ onActiveChanged: activeChanged.on,
1187
+ onPagesChanged: pagesChanged.on,
1188
+ onUnsavedChangesChanged: unsavedChangesChanged.on
758
1189
  };
759
- workspaceCapabilities.set(DocumentsToken, documents);
760
- for (const plugin of plan.ordered) if (!isDocumentScoped(plugin)) store.registerSlice(plugin.id, reducerOf(plugin), initialStateOf(plugin));
761
- for (const plugin of plan.ordered) if (!isDocumentScoped(plugin) && plugin.token && plugin.capability) workspaceCapabilities.set(plugin.token, plugin.capability(createPluginContext(services, plugin)));
1190
+ workspaceCapabilities.set(require_settings.DocumentsToken, documents);
1191
+ for (const plugin of plan.ordered) if (!isDocumentScoped(plugin)) workspaceLeases.set(plugin, store.lease(plugin.id, initialStateOf(plugin)));
1192
+ for (const plugin of plan.ordered) {
1193
+ if (isDocumentScoped(plugin)) continue;
1194
+ const ctx = require_settings.createPluginContext(services, plugin, void 0, workspaceCancel.signal, workspaceScope);
1195
+ const { api, connect } = plugin.create(ctx);
1196
+ if (plugin.token) workspaceCapabilities.set(plugin.token, api);
1197
+ workspaceConnectors.set(plugin, () => {
1198
+ connect?.();
1199
+ require_settings.markConnected(ctx);
1200
+ });
1201
+ }
762
1202
  return {
1203
+ ...viewerSettings.api,
763
1204
  engine,
764
1205
  documents,
765
1206
  capability: resolveCapability,
766
1207
  tryCapability: tryResolveCapability,
767
1208
  scopeOf: plan.scopeOf,
1209
+ settingsOf,
768
1210
  subscribe: store.subscribe,
769
1211
  getState: store.getState,
770
1212
  status: () => status,
@@ -775,10 +1217,8 @@ function createKernel(opts) {
775
1217
  try {
776
1218
  for (const plugin of plan.ordered) {
777
1219
  if (status !== "starting") return;
778
- if (!isDocumentScoped(plugin)) await plugin.init?.(createPluginContext(services, plugin));
1220
+ if (!isDocumentScoped(plugin)) workspaceConnectors.get(plugin)?.();
779
1221
  }
780
- if (status !== "starting") return;
781
- for (const plugin of plan.ordered) if (!isDocumentScoped(plugin)) plugin.effects?.(createEffectContext(services, plugin));
782
1222
  status = "started";
783
1223
  } catch (error) {
784
1224
  status = "failed";
@@ -792,8 +1232,9 @@ function createKernel(opts) {
792
1232
  return destroyPromise ?? (destroyPromise = (async () => {
793
1233
  const wasFailed = status === "failed";
794
1234
  status = "destroying";
1235
+ workspaceCancel.abort(new require_settings.CancelledError("kernel destroyed"));
795
1236
  await startPromise?.catch((error) => {
796
- if (!isCancelled(error) && !wasFailed) report(error);
1237
+ if (!require_settings.isCancelled(error) && !wasFailed) report(error);
797
1238
  });
798
1239
  await Promise.allSettled([...sessions.values()].map((session) => session.close()));
799
1240
  sessions.clear();
@@ -806,56 +1247,213 @@ function createKernel(opts) {
806
1247
  };
807
1248
  }
808
1249
  //#endregion
809
- //#region src/event-hook.ts
1250
+ //#region src/compose.ts
1251
+ function composeApi(capability, slices) {
1252
+ const seen = /* @__PURE__ */ new Set();
1253
+ const api = {};
1254
+ for (const slice of slices) for (const key of Object.keys(slice)) {
1255
+ if (seen.has(key)) throw new Error(`[${capability}] api member '${key}' is defined twice`);
1256
+ seen.add(key);
1257
+ api[key] = slice[key];
1258
+ }
1259
+ return api;
1260
+ }
1261
+ //#endregion
1262
+ //#region src/memo.ts
1263
+ const sameInputs = (left, right) => left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
810
1264
  /**
811
- * Create one capability event.
812
- *
813
- * Delivery contract: synchronous fan-out over a snapshot of the listener set
814
- * (listeners added or removed during an emit don't affect that emit); a
815
- * throwing listener is isolated — reported through `onListenerError`, never
816
- * allowed to break the emitting operation or its sibling listeners. After
817
- * `dispose()`, `on` returns an inert unsubscribe (teardown races never throw).
818
- * No value cache, no replay, no equality — state belongs to the store.
1265
+ * Recompute `compute(...inputs())` only when an input changes (compared with
1266
+ * `Object.is`, element by element); otherwise return the previous result.
819
1267
  */
820
- function createEventHook(onListenerError) {
821
- let listeners = /* @__PURE__ */ new Set();
822
- return {
823
- on(listener) {
824
- if (!listeners) return () => {};
825
- listeners.add(listener);
826
- return () => void listeners?.delete(listener);
827
- },
828
- emit(event) {
829
- if (!listeners) return;
830
- for (const listener of [...listeners]) try {
831
- listener(event);
832
- } catch (error) {
833
- onListenerError?.(error);
834
- }
835
- },
836
- dispose() {
837
- listeners = null;
1268
+ function memo(inputs, compute) {
1269
+ let last = null;
1270
+ return () => {
1271
+ const next = inputs();
1272
+ if (last && sameInputs(last.inputs, next)) return last.result;
1273
+ const result = compute(...next);
1274
+ last = {
1275
+ inputs: next,
1276
+ result
1277
+ };
1278
+ return result;
1279
+ };
1280
+ }
1281
+ /**
1282
+ * `memo` per key: each key keeps its own last inputs and result. Keys are
1283
+ * compared with `Map` semantics, so use primitives (a page object number, an
1284
+ * encoded key), not freshly built objects. `maxEntries` bounds the cache;
1285
+ * the oldest key is evicted first.
1286
+ */
1287
+ function memoByKey(inputs, compute, options = {}) {
1288
+ const cache = /* @__PURE__ */ new Map();
1289
+ return (key) => {
1290
+ const next = inputs(key);
1291
+ const cached = cache.get(key);
1292
+ if (cached && sameInputs(cached.inputs, next)) return cached.result;
1293
+ const result = compute(key, ...next);
1294
+ if (cached) cache.delete(key);
1295
+ else if (options.maxEntries !== void 0 && cache.size >= options.maxEntries) {
1296
+ const oldest = cache.keys().next();
1297
+ if (!oldest.done) cache.delete(oldest.value);
838
1298
  }
1299
+ cache.set(key, {
1300
+ inputs: next,
1301
+ result
1302
+ });
1303
+ return result;
839
1304
  };
840
1305
  }
841
1306
  //#endregion
842
- //#region src/serial-queue.ts
1307
+ //#region src/state.ts
1308
+ /**
1309
+ * Declare a plugin's state. The fields come from `read`, and `empty` is checked
1310
+ * against them (`NoInfer` keeps it out of the inference): a missing field or a
1311
+ * value of the wrong type is a type error. Write `empty` inline, because
1312
+ * TypeScript refuses an extra field only in an object literal.
1313
+ */
1314
+ function defineState(token, declaration) {
1315
+ return {
1316
+ token,
1317
+ read: declaration.read,
1318
+ empty: Object.freeze(declaration.empty),
1319
+ ...declaration.ensure ? { ensure: declaration.ensure } : {}
1320
+ };
1321
+ }
1322
+ /**
1323
+ * One level of identity: equal when the two values are the same, or are
1324
+ * objects (or arrays) holding the same values under the same keys. Declared
1325
+ * state compares this way, and so does a value a selector picks from it.
1326
+ */
1327
+ function shallowEqual(left, right) {
1328
+ if (Object.is(left, right)) return true;
1329
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
1330
+ if (Array.isArray(left) !== Array.isArray(right)) return false;
1331
+ const leftFields = left;
1332
+ const rightFields = right;
1333
+ const keys = Object.keys(leftFields);
1334
+ return keys.length === Object.keys(rightFields).length && keys.every((key) => key in rightFields && Object.is(leftFields[key], rightFields[key]));
1335
+ }
1336
+ //#endregion
1337
+ //#region src/documents-state.ts
1338
+ /**
1339
+ * The documents' State tables as code: the document a component talks to (React's
1340
+ * `useDocument()`), and every open document with the active one (`useDocumentsState()`). Each
1341
+ * adapter turns these into its own reactive form, as it does a plugin's.
1342
+ */
1343
+ /** No document: none is open, or none is in scope. It reads as one still on its way. */
1344
+ const NO_DOCUMENT = Object.freeze({
1345
+ id: "",
1346
+ name: void 0,
1347
+ status: "loading",
1348
+ pageCount: 0,
1349
+ hasUnsavedChanges: false,
1350
+ passwordProvided: void 0,
1351
+ error: void 0
1352
+ });
1353
+ /**
1354
+ * The document in scope (the capability resolved for a document, or else the active one): its
1355
+ * id, name, status, page count, unsaved changes, and why it's locked or failed.
1356
+ */
1357
+ const documentState = defineState(require_settings.DocumentsToken, {
1358
+ read: (documents) => documents.get() ?? NO_DOCUMENT,
1359
+ empty: NO_DOCUMENT
1360
+ });
1361
+ const documentsState = defineState(require_settings.DocumentsToken, {
1362
+ read: (documents) => ({
1363
+ documents: documents.list(),
1364
+ activeId: documents.getActiveId()
1365
+ }),
1366
+ empty: {
1367
+ documents: [],
1368
+ activeId: null
1369
+ }
1370
+ });
1371
+ //#endregion
1372
+ //#region src/capability-handle.ts
843
1373
  /**
844
- * Small promise-tail queue: operations run strictly one at a time, in
845
- * submission order, and failures never poison later operations. The shared
846
- * serializer for per-document mutation/dispatch pipelines.
1374
+ * What every framework adapter's plugin API shares (`useStage()`, `inject(EpdfSearch)`): which
1375
+ * members are reads, and what a document plugin's API reaches while its part of the app has no
1376
+ * ready document.
847
1377
  *
848
- * Direction law for nested queues: an operation running on queue A may
849
- * enqueue into queue B and await it, but never the reverse on the same pair —
850
- * a B-operation enqueueing back into A self-deadlocks behind its caller.
1378
+ * Reads follow the capability vocabulary (docs/conventions/naming.md): a member named `get*`,
1379
+ * `list*`, `is*`, `has*` or `can*` reads state, so an adapter makes a call to it in a template or
1380
+ * a derived value update when its answer changes, and every other member tracks nothing.
1381
+ *
1382
+ * Without a document there is no capability to call, so the API reaches a stand-in: reading a
1383
+ * member never throws, so chrome renders before the first document opens, and calling one refuses
1384
+ * with `not-ready`. A member the token lists in `promises` returns the refusal as a rejected
1385
+ * promise, as the capability itself would, so a `.catch()` sees it; every other member throws.
1386
+ * The settings calls work: a plugin's settings belong to the plugin, not to a document.
851
1387
  */
852
- function createSerialQueue() {
853
- let tail = Promise.resolve();
854
- return (operation) => {
855
- const result = tail.then(operation, operation);
856
- tail = result.then(() => void 0, () => void 0);
857
- return result;
1388
+ const READ = /^(?:get|list|is|has|can)(?:[A-Z]|$)/;
1389
+ /** Whether a capability member named `name` is a read: `get*`, `list*`, `is*`, `has*` or `can*`. */
1390
+ function isReadMember(name) {
1391
+ return READ.test(name);
1392
+ }
1393
+ /** Whether the member at `path` (`'setValue'`, `'comments.reply'`) returns a promise, as `token` declares. */
1394
+ function returnsPromise(token, path) {
1395
+ return token.promises?.[path] === true;
1396
+ }
1397
+ /** The capability members that work without a document: a plugin's settings are the plugin's own. */
1398
+ const SETTINGS_CALLS = /* @__PURE__ */ new Set([
1399
+ "getSettings",
1400
+ "updateSettings",
1401
+ "resetSettings",
1402
+ "onSettingsChanged"
1403
+ ]);
1404
+ const standIns = /* @__PURE__ */ new WeakMap();
1405
+ /**
1406
+ * What a framework adapter's API reaches for a document plugin while its part of the app has no
1407
+ * ready document. One per kernel and token, so it is the same object on every read.
1408
+ */
1409
+ function standInFor(kernel, token) {
1410
+ let byToken = standIns.get(kernel);
1411
+ if (!byToken) {
1412
+ byToken = /* @__PURE__ */ new WeakMap();
1413
+ standIns.set(kernel, byToken);
1414
+ }
1415
+ let standIn = byToken.get(token);
1416
+ if (!standIn) {
1417
+ standIn = createStandIn(token, () => kernel.settingsOf(token));
1418
+ byToken.set(token, standIn);
1419
+ }
1420
+ return standIn;
1421
+ }
1422
+ /**
1423
+ * A Proxy whose every member is a function that refuses with `not-ready`. A member is itself such
1424
+ * a Proxy, so namespaces work too (`annotation.comments.reply`). Members are cached by path, so
1425
+ * `stage.zoomIn` is the same function on every read. `then`, symbol keys and keys that start with
1426
+ * `__` (a framework's own flags, such as Vue's `__v_isRef`) read as undefined: the stand-in is no
1427
+ * thenable, and inspecting it doesn't throw. The four settings calls forward to `settings()` when
1428
+ * called; for a plugin without settings, they refuse too.
1429
+ */
1430
+ function createStandIn(token, settings) {
1431
+ const refusal = () => new require_settings.PluginError("not-ready", token.name, "no document is open");
1432
+ const refuse = (path) => () => {
1433
+ if (returnsPromise(token, path)) return Promise.reject(refusal());
1434
+ throw refusal();
858
1435
  };
1436
+ const forward = (name) => (...args) => {
1437
+ let api;
1438
+ try {
1439
+ api = settings();
1440
+ } catch {
1441
+ throw refusal();
1442
+ }
1443
+ return api[name](...args);
1444
+ };
1445
+ const members = /* @__PURE__ */ new Map();
1446
+ const memberOf = (parentPath, key) => {
1447
+ if (typeof key === "symbol" || key === "then" || key.startsWith("__")) return void 0;
1448
+ const path = parentPath ? `${parentPath}.${key}` : key;
1449
+ let member = members.get(path);
1450
+ if (!member) {
1451
+ member = !parentPath && SETTINGS_CALLS.has(key) ? forward(key) : new Proxy(refuse(path), { get: (_refuse, next) => memberOf(path, next) });
1452
+ members.set(path, member);
1453
+ }
1454
+ return member;
1455
+ };
1456
+ return new Proxy({}, { get: (_capability, key) => memberOf("", key) });
859
1457
  }
860
1458
  //#endregion
861
1459
  //#region src/index.ts
@@ -863,16 +1461,33 @@ function createSerialQueue() {
863
1461
  * Create a typed capability token. `name` is the token's identity (debugging,
864
1462
  * error messages); `options.hint` is the authored remedy the kernel appends to
865
1463
  * the missing-dependency error, so the error contains its own fix.
1464
+ * `options.promises` lists the members that return a promise, so a framework
1465
+ * adapter's stand-in rejects them, rather than throwing, while no document is
1466
+ * open; a document plugin whose capability has such members gives it.
866
1467
  */
867
1468
  function createCapabilityToken(name, options) {
868
1469
  return {
869
1470
  name,
870
- hint: options?.hint
1471
+ hint: options?.hint,
1472
+ ...options?.promises ? { promises: options.promises } : {}
871
1473
  };
872
1474
  }
873
- /** Identity helper that pins a plugin's generics. The real win is inference. */
874
- function definePlugin(def) {
875
- return def;
1475
+ /**
1476
+ * The host lens over a plugin's token: the same runtime object, typed with
1477
+ * the wider capability sibling plugins use. The one place that cast lives.
1478
+ */
1479
+ function createHostToken(token) {
1480
+ return token;
1481
+ }
1482
+ /**
1483
+ * Define a plugin. It returns the definition as it is, and reads the types from it, so a
1484
+ * plugin passes no type arguments: the capability from `token`, the state from `state`, the
1485
+ * settings from `settings.defaults`. `create` receives a `PluginContext<State, Settings>`
1486
+ * and must return the capability. A plugin without `state` is stateless (`void`); one
1487
+ * without `settings` has `NoSettings`, and `ctx.settings()` throws there.
1488
+ */
1489
+ function definePlugin(definition) {
1490
+ return definition;
876
1491
  }
877
1492
  //#endregion
878
1493
  Object.defineProperty(exports, "AbortablePromise", {
@@ -887,30 +1502,74 @@ Object.defineProperty(exports, "CONTINUOUS_RENDER_POLICY", {
887
1502
  return _embedpdf_engine_core_runtime.CONTINUOUS_RENDER_POLICY;
888
1503
  }
889
1504
  });
890
- exports.CORE_ACTIVE_CHANGED = CORE_ACTIVE_CHANGED;
891
- exports.CORE_DOCUMENT_ADDED = CORE_DOCUMENT_ADDED;
892
- exports.CORE_DOCUMENT_LOCKED = CORE_DOCUMENT_LOCKED;
893
- exports.CORE_DOCUMENT_OPENING = CORE_DOCUMENT_OPENING;
894
- exports.CORE_DOCUMENT_OPEN_FAILED = CORE_DOCUMENT_OPEN_FAILED;
895
- exports.CORE_DOCUMENT_PAGES_UPDATED = CORE_DOCUMENT_PAGES_UPDATED;
896
- exports.CORE_DOCUMENT_REMOVED = CORE_DOCUMENT_REMOVED;
897
- exports.CORE_ORDER_CHANGED = CORE_ORDER_CHANGED;
898
- exports.CancelledError = CancelledError;
899
- exports.DocumentsToken = DocumentsToken;
1505
+ exports.CancelledError = require_settings.CancelledError;
1506
+ exports.DocumentsToken = require_settings.DocumentsToken;
900
1507
  Object.defineProperty(exports, "PermissionDenied", {
901
1508
  enumerable: true,
902
1509
  get: function() {
903
1510
  return _embedpdf_engine_core_runtime.PermissionDenied;
904
1511
  }
905
1512
  });
1513
+ exports.PluginError = require_settings.PluginError;
1514
+ exports.VIEWER_DEFAULTS = VIEWER_DEFAULTS;
1515
+ exports.VIEWER_WHOLE_SETTINGS = VIEWER_WHOLE_SETTINGS;
1516
+ Object.defineProperty(exports, "annotationKey", {
1517
+ enumerable: true,
1518
+ get: function() {
1519
+ return _embedpdf_engine_core_runtime.annotationKey;
1520
+ }
1521
+ });
1522
+ exports.composeApi = composeApi;
906
1523
  exports.createCapabilityToken = createCapabilityToken;
907
- exports.createEventHook = createEventHook;
1524
+ exports.createEventHook = require_settings.createEventHook;
1525
+ exports.createHostToken = createHostToken;
908
1526
  exports.createKernel = createKernel;
909
- exports.createSerialQueue = createSerialQueue;
1527
+ exports.createLatestLane = require_settings.createLatestLane;
1528
+ exports.createSerialQueue = require_settings.createSerialQueue;
1529
+ Object.defineProperty(exports, "decodePageKey", {
1530
+ enumerable: true,
1531
+ get: function() {
1532
+ return _embedpdf_engine_core_runtime.decodePageKey;
1533
+ }
1534
+ });
910
1535
  exports.definePlugin = definePlugin;
911
- exports.docInfoEquals = docInfoEquals;
912
- exports.docInfoListEquals = docInfoListEquals;
913
- exports.isCancelled = isCancelled;
1536
+ exports.defineState = defineState;
1537
+ exports.documentState = documentState;
1538
+ exports.documentsState = documentsState;
1539
+ Object.defineProperty(exports, "encodePageKey", {
1540
+ enumerable: true,
1541
+ get: function() {
1542
+ return _embedpdf_engine_core_runtime.encodePageKey;
1543
+ }
1544
+ });
1545
+ exports.guardHandle = require_settings.guardHandle;
1546
+ exports.isCancelled = require_settings.isCancelled;
1547
+ Object.defineProperty(exports, "isLocalEngine", {
1548
+ enumerable: true,
1549
+ get: function() {
1550
+ return _embedpdf_engine_core_runtime.isLocalEngine;
1551
+ }
1552
+ });
1553
+ exports.isPluginError = require_settings.isPluginError;
1554
+ exports.isReadMember = isReadMember;
1555
+ exports.memo = memo;
1556
+ exports.memoByKey = memoByKey;
1557
+ exports.mergeSettings = require_settings.mergeSettings;
1558
+ Object.defineProperty(exports, "pageRefsEqual", {
1559
+ enumerable: true,
1560
+ get: function() {
1561
+ return _embedpdf_engine_core_runtime.pageRefsEqual;
1562
+ }
1563
+ });
1564
+ exports.reload = require_settings.reload;
1565
+ Object.defineProperty(exports, "resolvePageLayers", {
1566
+ enumerable: true,
1567
+ get: function() {
1568
+ return _embedpdf_engine_core_runtime.resolvePageLayers;
1569
+ }
1570
+ });
1571
+ exports.returnsPromise = returnsPromise;
1572
+ exports.shallowEqual = shallowEqual;
914
1573
  Object.defineProperty(exports, "snapAppearanceScale", {
915
1574
  enumerable: true,
916
1575
  get: function() {
@@ -929,5 +1588,16 @@ Object.defineProperty(exports, "snapTileScale", {
929
1588
  return _embedpdf_engine_core_runtime.snapTileScale;
930
1589
  }
931
1590
  });
1591
+ exports.standInFor = standInFor;
1592
+ exports.timerClock = require_settings.timerClock;
1593
+ Object.defineProperty(exports, "toPageRef", {
1594
+ enumerable: true,
1595
+ get: function() {
1596
+ return _embedpdf_engine_core_runtime.toPageRef;
1597
+ }
1598
+ });
1599
+ exports.toPluginError = require_settings.toPluginError;
1600
+ exports.toPluginErrorInfo = require_settings.toPluginErrorInfo;
1601
+ exports.viewerSettingsOf = viewerSettingsOf;
932
1602
 
933
1603
  //# sourceMappingURL=index.cjs.map