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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,198 +1,40 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_settings = require("./settings-maKtf237.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,49 +57,44 @@ 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
100
  case "pages.inserted": return event.layout;
@@ -265,24 +102,70 @@ function layoutFromEvent(event) {
265
102
  }
266
103
  }
267
104
  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) => ({
105
+ const initialStateOf = (plugin) => plugin.state?.();
106
+ /**
107
+ * A document's public fields, one object per registry entry: the entries are replaced, never
108
+ * changed, so the same entry gives the same object, and readers re-render only for their own.
109
+ */
110
+ const infoByEntry = /* @__PURE__ */ new WeakMap();
111
+ function documentInfoOf(entry) {
112
+ let info = infoByEntry.get(entry);
113
+ if (!info) {
114
+ info = "pages" in entry ? readyInfo(entry) : pendingInfo(entry);
115
+ infoByEntry.set(entry, info);
116
+ }
117
+ return info;
118
+ }
119
+ const readyInfo = (meta) => Object.freeze({
271
120
  id: meta.id,
272
121
  name: meta.name,
273
122
  status: "ready",
274
- pageCount: meta.pageCount
123
+ pageCount: meta.pageCount,
124
+ hasUnsavedChanges: meta.hasUnsavedChanges
275
125
  });
276
- const pendingToDocInfo = (meta) => ({
126
+ const pendingInfo = (meta) => Object.freeze({
277
127
  id: meta.id,
278
128
  name: meta.name,
279
129
  status: meta.status,
280
130
  pageCount: 0,
281
- passwordProvided: meta.passwordProvided
131
+ hasUnsavedChanges: false,
132
+ ...meta.status === "locked" ? { passwordProvided: meta.passwordProvided ?? false } : {},
133
+ ...meta.status === "error" && meta.error !== void 0 ? { error: require_settings.toPluginErrorInfo(require_settings.toPluginError("documents", meta.error)) } : {}
134
+ });
135
+ /** Whether an event is a change to the document, one a download would carry. */
136
+ const changesDocument = (event) => event.type !== "stream.desynced" && event.type !== "document.versioned" && event.type !== "signatures.prepared" && event.type !== "signatures.cancelled";
137
+ /** Download a URL source's file; its tab shows the failure when the server says no. */
138
+ async function fetchUrlSource(source, signal) {
139
+ const fetchFile = globalThis.fetch;
140
+ if (!fetchFile) throw new require_settings.PluginError("unsupported", "documents", `can't download ${source.url}: no fetch()`);
141
+ const response = await fetchFile(source.url, { signal });
142
+ if (!response.ok) throw new require_settings.PluginError(response.status === 404 ? "not-found" : "operation-failed", "documents", `couldn't download ${source.url} (HTTP ${response.status})`);
143
+ return {
144
+ kind: "bytes",
145
+ bytes: new Uint8Array(await response.arrayBuffer())
146
+ };
147
+ }
148
+ const isUrlSource = (source) => source.kind === "url";
149
+ const noLayer = () => new require_settings.PluginError("unsupported", "documents", "only the local engine gives a layer to download");
150
+ /**
151
+ * The documents capability as seen from inside one document's scope: every call whose document
152
+ * is optional uses that document instead of the active one. The rest is the registry itself.
153
+ */
154
+ const documentsInScope = (documents, documentId) => ({
155
+ ...documents,
156
+ get: (id = documentId) => documents.get(id),
157
+ download: (id = documentId, options) => documents.download(id, options),
158
+ downloadLayer: (id = documentId, options) => documents.downloadLayer(id, options),
159
+ canDownload: (id = documentId) => documents.canDownload(id),
160
+ canPrint: (id = documentId) => documents.canPrint(id),
161
+ listPages: (id = documentId) => documents.listPages(id),
162
+ getPage: (page, id = documentId) => documents.getPage(page, id),
163
+ getPageIndex: (ref, id = documentId) => documents.getPageIndex(ref, id),
164
+ getRevision: (id = documentId) => documents.getRevision(id)
282
165
  });
283
166
  /** The stable id an input implies, if it carries one ('bytes'/'layerBytes'/'id'). */
284
167
  const idOfInput = (input) => "id" in input && typeof input.id === "string" ? input.id : null;
285
- const passwordOfInput = (input) => "password" in input ? input.password : void 0;
168
+ let instanceCounter = 0;
286
169
  /** Engine rejections carry AbortError when close() aborts the live call;
287
170
  * match structurally so test fakes with plain promises still work. */
288
171
  const isAbortLike = (error) => error instanceof Error && error.name === "AbortError";
@@ -290,7 +173,10 @@ const isAbortLike = (error) => error instanceof Error && error.name === "AbortEr
290
173
  * Assemble a kernel from an engine + plugins.
291
174
  *
292
175
  * planPlugins — validate dependencies, order them
293
- * resolveCapability — workspace singletons, or per-document instances built lazily
176
+ * resolveCapability — workspace singletons (or their view from a document's scope),
177
+ * or per-document instances built lazily
178
+ * settings — one store per plugin registration, built up front, shared
179
+ * by its instances and readable with no document
294
180
  * document lifecycle — one DocumentSession per document: transactional open
295
181
  * (publish-last), one idempotent close for every phase
296
182
  * start / destroy — explicit status machine; destroy closes everything
@@ -298,14 +184,49 @@ const isAbortLike = (error) => error instanceof Error && error.name === "AbortEr
298
184
  * The kernel closes every handle it opened; it never destroys the engine —
299
185
  * ownership follows acquisition, and the engine was handed in by the caller.
300
186
  */
301
- function createKernel(opts) {
302
- const { engine, plugins } = opts;
303
- const report = opts.report ?? ((error) => console.error("[kernel]", error));
304
- const store = createStore(report);
187
+ function createKernel(config) {
188
+ const { engine, plugins } = config;
189
+ const report = config.report ?? ((error) => console.error("[kernel]", error));
190
+ const store = require_settings.createStore(report);
305
191
  const plan = planPlugins(plugins);
306
192
  const documentScopedPlugins = plan.ordered.filter(isDocumentScoped);
307
193
  const workspaceCapabilities = /* @__PURE__ */ new Map();
308
- const workspaceScope = createScope(report);
194
+ const workspaceLeases = /* @__PURE__ */ new Map();
195
+ const workspaceConnectors = /* @__PURE__ */ new Map();
196
+ const workspaceCancel = new AbortController();
197
+ const workspaceScope = require_settings.createScope(report);
198
+ /** How each workspace capability looks from inside a document's scope, by token. The
199
+ * documents capability is built in, so its view is declared here; plugins declare theirs. */
200
+ const inScopeByToken = /* @__PURE__ */ new Map([[require_settings.DocumentsToken, documentsInScope]]);
201
+ for (const plugin of plan.ordered) if (plugin.token && plugin.inScope && !isDocumentScoped(plugin)) inScopeByToken.set(plugin.token, plugin.inScope);
202
+ const opened = require_settings.createEventHook(report);
203
+ const openFailed = require_settings.createEventHook(report);
204
+ const locked = require_settings.createEventHook(report);
205
+ const closed = require_settings.createEventHook(report);
206
+ const activeChanged = require_settings.createEventHook(report);
207
+ const pagesChanged = require_settings.createEventHook(report);
208
+ const unsavedChangesChanged = require_settings.createEventHook(report);
209
+ workspaceScope.defer(() => {
210
+ for (const hook of [
211
+ opened,
212
+ openFailed,
213
+ locked,
214
+ closed,
215
+ activeChanged,
216
+ pagesChanged,
217
+ unsavedChangesChanged
218
+ ]) hook.dispose();
219
+ });
220
+ /** Every core write goes through here so an active-tab change is observed exactly once. */
221
+ const setCore = (patch) => {
222
+ const before = store.getCore().activeId;
223
+ store.setCore(patch);
224
+ const after = store.getCore().activeId;
225
+ if (before !== after) activeChanged.emit({
226
+ documentId: after,
227
+ previousDocumentId: before
228
+ });
229
+ };
309
230
  const sessions = /* @__PURE__ */ new Map();
310
231
  let status = "created";
311
232
  let startPromise = null;
@@ -314,18 +235,18 @@ function createKernel(opts) {
314
235
  if (status === "failed" || status === "destroying" || status === "destroyed") throw new Error(`[kernel] ${what} on a ${status} kernel`);
315
236
  };
316
237
  const checkpoint = (session) => {
317
- if (session.cancel.signal.aborted) throw new CancelledError(`closed while opening: ${session.id}`);
238
+ if (session.cancel.signal.aborted) throw new require_settings.CancelledError(`closed while opening: ${session.id}`);
318
239
  };
319
240
  /**
320
241
  * Await an engine/network call under the session's cancellation:
321
242
  * - the call is retained so close() can abort real worker-side work
322
243
  * (`AbortablePromise`), and
323
- * - the await RACES the cancellation, so close()'s join never blocks on a
244
+ * - the await races the cancellation, so close()'s join never blocks on a
324
245
  * call that cannot be aborted (a plain-promise engine, a stuck fetch).
325
246
  * When cancellation wins but the call later lands anyway, `onLateResult`
326
247
  * routes the result into the session scope — whose late-defer rule runs it
327
248
  * immediately after disposal — so a late-arriving resource cannot leak.
328
- * (Plugin inits are deliberately NOT raced: they are first-party code that
249
+ * (Plugin inits are deliberately not raced: they are first-party code that
329
250
  * close() joins to completion; only unbounded external waits are raced.)
330
251
  */
331
252
  async function engineCall(session, call, onLateResult) {
@@ -334,14 +255,14 @@ function createKernel(opts) {
334
255
  const signal = session.cancel.signal;
335
256
  let onAbort;
336
257
  const cancellation = new Promise((_, reject) => {
337
- onAbort = () => reject(new CancelledError(`closed while opening: ${session.id}`));
258
+ onAbort = () => reject(new require_settings.CancelledError(`closed while opening: ${session.id}`));
338
259
  if (signal.aborted) onAbort();
339
260
  else signal.addEventListener("abort", onAbort, { once: true });
340
261
  });
341
262
  try {
342
263
  return await Promise.race([call, cancellation]);
343
264
  } catch (error) {
344
- if (isCancelled(error)) call.then((value) => {
265
+ if (require_settings.isCancelled(error)) call.then((value) => {
345
266
  if (onLateResult) session.scope.defer(() => onLateResult(value));
346
267
  }, () => {});
347
268
  throw error;
@@ -351,19 +272,38 @@ function createKernel(opts) {
351
272
  }
352
273
  }
353
274
  function createSession(id, name) {
275
+ const cancel = new AbortController();
354
276
  const session = {
355
277
  id,
278
+ instanceId: `${id}#${++instanceCounter}`,
356
279
  name,
280
+ source: null,
281
+ openOptions: void 0,
357
282
  phase: "opening",
358
283
  handle: null,
359
284
  stagedMeta: null,
360
- scope: createScope(report),
285
+ scope: require_settings.createScope(report),
361
286
  operation: null,
362
287
  engineOp: null,
363
- cancel: new AbortController(),
288
+ cancel,
289
+ signal: cancel.signal,
364
290
  capabilities: /* @__PURE__ */ new Map(),
291
+ building: /* @__PURE__ */ new Set(),
292
+ views: /* @__PURE__ */ new Map(),
293
+ connectors: /* @__PURE__ */ new Map(),
294
+ leases: /* @__PURE__ */ new Map(),
295
+ settleFlushes: /* @__PURE__ */ new Set(),
296
+ downloadWraps: [],
297
+ changes: require_settings.createDocumentChanges({
298
+ handle: () => session.handle,
299
+ notify: () => store.notify(),
300
+ report
301
+ }),
302
+ changeCount: 0,
365
303
  close: () => closeSession(session)
366
304
  };
305
+ session.settleFlushes.add(() => session.changes.settle());
306
+ session.scope.defer(() => session.changes.close());
367
307
  return session;
368
308
  }
369
309
  /** One transition at a time per session (open, then possibly unlock). */
@@ -381,14 +321,15 @@ function createKernel(opts) {
381
321
  const previousId = session.id;
382
322
  if (previousId === nextId) return;
383
323
  const core = store.getCore();
384
- if (sessions.has(nextId) || core.documents[nextId] || core.pending[nextId]) throw new Error(`[documents] duplicate document id: ${nextId}`);
324
+ if (sessions.has(nextId) || core.documents[nextId] || core.pending[nextId]) throw new require_settings.PluginError("conflict", "documents", `duplicate document id: ${nextId}`);
385
325
  sessions.delete(previousId);
386
326
  session.id = nextId;
387
327
  sessions.set(nextId, session);
328
+ session.views.clear();
388
329
  const slot = core.pending[previousId];
389
330
  if (slot) {
390
331
  const { [previousId]: _moved, ...pending } = core.pending;
391
- store.setCore({
332
+ setCore({
392
333
  pending: {
393
334
  ...pending,
394
335
  [nextId]: {
@@ -398,7 +339,7 @@ function createKernel(opts) {
398
339
  },
399
340
  order: core.order.map((id) => id === previousId ? nextId : id),
400
341
  activeId: core.activeId === previousId ? nextId : core.activeId
401
- }, { type: CORE_ORDER_CHANGED });
342
+ });
402
343
  }
403
344
  }
404
345
  const closingSessions = /* @__PURE__ */ new WeakMap();
@@ -408,14 +349,17 @@ function createKernel(opts) {
408
349
  const closing = (async () => {
409
350
  session.phase = "closing";
410
351
  unpublishSlot(session.id);
411
- const reason = new CancelledError(`closed while opening: ${session.id}`);
352
+ for (const lease of session.leases.values()) lease.revoke();
353
+ const reason = new require_settings.CancelledError(`closed while opening: ${session.id}`);
412
354
  session.cancel.abort(reason);
413
355
  session.engineOp?.abort(reason);
414
356
  await session.operation?.catch((error) => {
415
- if (!isCancelled(error) && !isAbortLike(error)) report(error);
357
+ if (!require_settings.isCancelled(error) && !isAbortLike(error)) report(error);
416
358
  });
417
359
  await session.scope.dispose();
418
360
  if (sessions.get(session.id) === session) sessions.delete(session.id);
361
+ session.views.clear();
362
+ closed.emit({ documentId: session.id });
419
363
  })();
420
364
  closingSessions.set(session, closing);
421
365
  return closing;
@@ -428,7 +372,7 @@ function createKernel(opts) {
428
372
  }
429
373
  function publishPendingSlot(session, activate) {
430
374
  const core = store.getCore();
431
- store.setCore({
375
+ setCore({
432
376
  pending: {
433
377
  ...core.pending,
434
378
  [session.id]: {
@@ -437,13 +381,13 @@ function createKernel(opts) {
437
381
  status: "loading"
438
382
  }
439
383
  },
440
- order: [...core.order, session.id],
384
+ order: core.order.includes(session.id) ? core.order : [...core.order, session.id],
441
385
  activeId: activate || core.activeId === null ? session.id : core.activeId
442
- }, { type: CORE_DOCUMENT_OPENING });
386
+ });
443
387
  }
444
388
  function publishLocked(session, passwordProvided) {
445
389
  const core = store.getCore();
446
- store.setCore({ pending: {
390
+ setCore({ pending: {
447
391
  ...core.pending,
448
392
  [session.id]: {
449
393
  id: session.id,
@@ -451,11 +395,15 @@ function createKernel(opts) {
451
395
  status: "locked",
452
396
  passwordProvided
453
397
  }
454
- } }, { type: CORE_DOCUMENT_LOCKED });
398
+ } });
399
+ locked.emit({
400
+ documentId: session.id,
401
+ passwordProvided
402
+ });
455
403
  }
456
404
  function publishError(session, error) {
457
405
  const core = store.getCore();
458
- store.setCore({ pending: {
406
+ setCore({ pending: {
459
407
  ...core.pending,
460
408
  [session.id]: {
461
409
  id: session.id,
@@ -463,35 +411,70 @@ function createKernel(opts) {
463
411
  status: "error",
464
412
  error
465
413
  }
466
- } }, { type: CORE_DOCUMENT_OPEN_FAILED });
414
+ } });
415
+ openFailed.emit({
416
+ documentId: session.id,
417
+ error: require_settings.toPluginErrorInfo(require_settings.toPluginError("documents", error))
418
+ });
467
419
  }
468
420
  function unpublishSlot(id) {
469
421
  const core = store.getCore();
470
422
  if (!core.pending[id] && !core.documents[id]) return;
471
423
  const { [id]: _pending, ...pending } = core.pending;
472
424
  const { [id]: _document, ...documents } = core.documents;
473
- store.setCore({
425
+ setCore({
474
426
  pending,
475
427
  documents,
476
428
  order: core.order.filter((other) => other !== id),
477
429
  activeId: nextActiveDocument(core, id)
478
- }, { type: CORE_DOCUMENT_REMOVED });
430
+ });
479
431
  }
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
432
+ /** The one ready transition: swap the pending slot for the staged meta and
433
+ * announce `onOpened`. Everything before this is unpublished and rolls
482
434
  * back by disposing the session scope; nothing after this can fail. */
483
435
  function commitReady(session) {
484
436
  session.phase = "ready";
485
437
  const meta = session.stagedMeta;
486
438
  const core = store.getCore();
487
439
  const { [session.id]: _resolved, ...pending } = core.pending;
488
- store.setCore({
440
+ setCore({
489
441
  documents: {
490
442
  ...core.documents,
491
443
  [session.id]: meta
492
444
  },
493
445
  pending
494
- }, { type: CORE_DOCUMENT_ADDED });
446
+ });
447
+ opened.emit({
448
+ documentId: session.id,
449
+ info: documentInfoOf(meta)
450
+ });
451
+ }
452
+ /**
453
+ * The document changed, or a download read every change it had: set `hasUnsavedChanges` and
454
+ * announce a flip. Before the document is published, its staged meta carries the flag.
455
+ */
456
+ function setUnsavedChanges(session, hasUnsavedChanges) {
457
+ const core = store.getCore();
458
+ const published = core.documents[session.id];
459
+ if (!published) {
460
+ if (session.stagedMeta && session.stagedMeta.hasUnsavedChanges !== hasUnsavedChanges) session.stagedMeta = {
461
+ ...session.stagedMeta,
462
+ hasUnsavedChanges
463
+ };
464
+ return;
465
+ }
466
+ if (published.hasUnsavedChanges === hasUnsavedChanges) return;
467
+ setCore({ documents: {
468
+ ...core.documents,
469
+ [session.id]: {
470
+ ...published,
471
+ hasUnsavedChanges
472
+ }
473
+ } });
474
+ unsavedChangesChanged.emit({
475
+ documentId: session.id,
476
+ hasUnsavedChanges
477
+ });
495
478
  }
496
479
  const sessionOf = (documentId) => {
497
480
  const id = documentId ?? store.getCore().activeId;
@@ -504,18 +487,89 @@ function createKernel(opts) {
504
487
  if (!session) return null;
505
488
  return session.phase === "bringup" || session.phase === "ready" ? session.handle : null;
506
489
  };
490
+ /** The session a per-document verb acts on: `documentId`, or the active document. */
491
+ function readySession(documentId, verb) {
492
+ const session = sessionOf(documentId);
493
+ if (!session || !documentHandle(session.id)) throw new require_settings.PluginError("not-ready", "documents", `no open document to ${verb}`);
494
+ return session;
495
+ }
496
+ const allowsOn = (documentId, permission) => {
497
+ const handle = documentHandle(documentId);
498
+ return handle ? require_settings.sessionAllows(handle.security, permission) : false;
499
+ };
500
+ /**
501
+ * Read the document's file, with everything the user sees: refuse without `doc.download`, let
502
+ * it settle (what its plugins held back is sent and answered, settle.ts), then read it inside
503
+ * every `aroundDownload` wrap (the document's own save actions). A read that saw every change
504
+ * the document had clears `hasUnsavedChanges`; a change during the read, or after it (a
505
+ * did-save script), keeps it. Closing the document, or `signal`, cancels it.
506
+ */
507
+ async function downloadFile(documentId, signal, verb, read, refuse) {
508
+ const session = readySession(documentId, verb);
509
+ if (!allowsOn(session.id, "doc.download")) throw require_settings.permissionDenied("documents", "doc.download", `documents.${verb}`);
510
+ const refusal = refuse?.(session.handle);
511
+ if (refusal) throw refusal;
512
+ try {
513
+ await require_settings.settle(session.settleFlushes, [signal, session.signal], report);
514
+ const handle = documentHandle(session.id);
515
+ if (!handle) throw new require_settings.PluginError("not-ready", "documents", `no open document to ${verb}`);
516
+ let readAt = -1;
517
+ const readFile = () => {
518
+ readAt = session.changeCount;
519
+ return require_settings.cancellable("documents", signal, read(handle));
520
+ };
521
+ const bytes = await require_settings.readWrapped(session.downloadWraps, readFile);
522
+ if (readAt === session.changeCount) setUnsavedChanges(session, false);
523
+ return bytes;
524
+ } catch (error) {
525
+ throw require_settings.toPluginError("documents", error);
526
+ }
527
+ }
528
+ /** Whether the plugin's `create()` is running for this document: it has no capability yet. */
529
+ const isBuilding = (plugin, session) => session.building.has(plugin) && !session.capabilities.has(plugin);
507
530
  function buildDocumentCapability(plugin, session) {
508
531
  let capability = session.capabilities.get(plugin);
509
532
  if (!capability) {
510
- capability = plugin.capability(createPluginContext(services, plugin, session));
533
+ if (session.building.has(plugin)) throw new require_settings.PluginError("not-ready", plugin.id, `"${plugin.id}" was asked for while it is being created`);
534
+ const ctx = require_settings.createPluginContext(services, plugin, session, session.signal, session.scope);
535
+ session.building.add(plugin);
536
+ let created;
537
+ try {
538
+ created = plugin.create(ctx);
539
+ } finally {
540
+ session.building.delete(plugin);
541
+ }
542
+ const { api, connect } = created;
543
+ capability = api;
544
+ session.connectors.set(plugin, () => {
545
+ connect?.();
546
+ require_settings.markConnected(ctx);
547
+ });
511
548
  session.capabilities.set(plugin, capability);
512
549
  }
513
550
  return capability;
514
551
  }
552
+ /**
553
+ * A workspace capability as a document's scope sees it: the view its plugin declares
554
+ * (`inScope`), built once per document and kept on the session, so it is the same object on
555
+ * every call and goes when the document closes. Without a document, or for a document the
556
+ * kernel doesn't know, it is the capability itself.
557
+ */
558
+ function workspaceViewOf(token, capability, documentId) {
559
+ const inScope = inScopeByToken.get(token);
560
+ const session = documentId === void 0 ? void 0 : sessions.get(documentId);
561
+ if (!inScope || !session) return capability;
562
+ let view = session.views.get(token);
563
+ if (view === void 0) {
564
+ view = inScope(capability, session.id);
565
+ session.views.set(token, view);
566
+ }
567
+ return view;
568
+ }
515
569
  function resolveCapability(token, documentId) {
516
570
  guardUsable(`capability("${token.name}")`);
517
571
  const workspaceCapability = workspaceCapabilities.get(token);
518
- if (workspaceCapability) return workspaceCapability;
572
+ if (workspaceCapability) return workspaceViewOf(token, workspaceCapability, documentId);
519
573
  const provider = plan.providerOf(token);
520
574
  if (!provider) throw new Error(`No capability "${token.name}".`);
521
575
  const id = documentId ?? store.getCore().activeId;
@@ -531,55 +585,91 @@ function createKernel(opts) {
531
585
  function tryResolveInternal(token, documentId) {
532
586
  if (status === "destroying" || status === "destroyed") return null;
533
587
  const workspaceCapability = workspaceCapabilities.get(token);
534
- if (workspaceCapability) return workspaceCapability;
588
+ if (workspaceCapability) return workspaceViewOf(token, workspaceCapability, documentId);
535
589
  const provider = plan.providerOf(token);
536
590
  if (!provider) return null;
537
591
  const session = sessionOf(documentId);
538
592
  if (!session || session.phase !== "bringup" && session.phase !== "ready") return null;
593
+ if (isBuilding(provider, session)) return null;
539
594
  return buildDocumentCapability(provider, session);
540
595
  }
541
596
  /** Public total resolver — see `Kernel.tryCapability`. `ready` only: the
542
- * null→instance flip at commit time IS the adapters' re-render signal. */
597
+ * null→instance flip at commit time is the adapters' re-render signal. */
543
598
  function tryResolveCapability(token, documentId) {
544
599
  if (status === "destroying" || status === "destroyed") return null;
545
600
  const workspaceCapability = workspaceCapabilities.get(token);
546
- if (workspaceCapability) return workspaceCapability;
601
+ if (workspaceCapability) return workspaceViewOf(token, workspaceCapability, documentId);
547
602
  const provider = plan.providerOf(token);
548
603
  if (!provider) return null;
549
604
  const session = sessionOf(documentId);
550
605
  if (!session || session.phase !== "ready") return null;
606
+ if (isBuilding(provider, session)) return null;
551
607
  return buildDocumentCapability(provider, session);
552
608
  }
609
+ const viewerSettings = require_settings.createSettingsStore({
610
+ defaults: VIEWER_DEFAULTS,
611
+ registered: config.settings,
612
+ whole: VIEWER_WHOLE_SETTINGS
613
+ }, store.notify, report);
614
+ workspaceScope.defer(() => viewerSettings.dispose());
615
+ const settingsStores = /* @__PURE__ */ new Map();
616
+ for (const plugin of plan.ordered) if (plugin.settings) settingsStores.set(plugin, require_settings.createSettingsStore(plugin.settings, store.notify, report));
617
+ workspaceScope.defer(() => {
618
+ for (const settings of settingsStores.values()) settings.dispose();
619
+ });
620
+ function settingsOf(token) {
621
+ const provider = plan.providerOf(token);
622
+ if (!provider) throw new Error(`No capability "${token.name}".`);
623
+ const settings = settingsStores.get(provider);
624
+ if (!settings) throw new Error(`Plugin "${provider.id}" has no settings.`);
625
+ return settings.api;
626
+ }
553
627
  const services = {
554
628
  engine,
555
629
  store,
556
630
  workspaceScope,
631
+ workspaceSignal: workspaceCancel.signal,
632
+ workspaceLeases,
633
+ report,
634
+ clock: config.clock ?? require_settings.timerClock,
557
635
  resolveCapability,
558
636
  tryResolveCapability: tryResolveInternal,
559
- documentHandle
637
+ documentHandle,
638
+ settingsStoreOf: (plugin) => settingsStores.get(plugin)
560
639
  };
561
640
  let ticketCounter = 0;
562
641
  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`. */
642
+ /** Slices, the registry's event subscription, and every plugin's
643
+ * construction and connection — every step's release deferred into the
644
+ * session scope, every await followed by a checkpoint. Runs entirely
645
+ * pre-commit: a failure anywhere rolls the whole session back and the
646
+ * document was never `ready`. */
567
647
  async function bringUp(session, snapshot) {
568
648
  session.phase = "bringup";
569
649
  let renderPolicy = _embedpdf_engine_core_runtime.CONTINUOUS_RENDER_POLICY;
570
650
  try {
571
- renderPolicy = await session.handle.render?.policy() ?? _embedpdf_engine_core_runtime.CONTINUOUS_RENDER_POLICY;
651
+ renderPolicy = await session.handle.render?.getPolicy() ?? _embedpdf_engine_core_runtime.CONTINUOUS_RENDER_POLICY;
572
652
  } catch {}
573
653
  checkpoint(session);
574
654
  session.stagedMeta = {
575
655
  id: session.id,
656
+ instanceId: session.instanceId,
576
657
  name: session.name,
577
658
  pageCount: snapshot.pageCount,
578
659
  pages: snapshot.pages,
579
660
  revision: 0,
580
- renderPolicy
661
+ renderPolicy,
662
+ hasUnsavedChanges: false
581
663
  };
664
+ const tracksChanges = (0, _embedpdf_engine_core_runtime.isLocalDocument)(session.handle);
665
+ const bursts = require_settings.createBurstGate(store);
666
+ session.scope.defer(bursts.close);
582
667
  const unsubscribeEvents = session.handle.events.subscribe((event) => {
668
+ bursts.first(event);
669
+ if (tracksChanges && changesDocument(event)) {
670
+ session.changeCount += 1;
671
+ setUnsavedChanges(session, true);
672
+ }
583
673
  const layout = layoutFromEvent(event);
584
674
  if (!layout) return;
585
675
  const now = store.getCore();
@@ -591,74 +681,133 @@ function createKernel(opts) {
591
681
  pages: layout.pages,
592
682
  revision: existing.revision + 1
593
683
  };
594
- store.setCore({ documents: {
684
+ setCore({ documents: {
595
685
  ...now.documents,
596
686
  [session.id]: updated
597
- } }, { type: CORE_DOCUMENT_PAGES_UPDATED });
687
+ } });
688
+ pagesChanged.emit({
689
+ documentId: session.id,
690
+ revision: updated.revision,
691
+ pages: updated.pages
692
+ });
598
693
  });
599
694
  session.scope.defer(unsubscribeEvents);
600
695
  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));
696
+ const lease = store.lease(require_settings.sliceKey(plugin.id, session.id), initialStateOf(plugin), session.instanceId);
697
+ session.leases.set(plugin, lease);
698
+ session.scope.defer(() => lease.revoke());
604
699
  }
605
- for (const plugin of documentScopedPlugins) {
606
- await plugin.init?.(createPluginContext(services, plugin, session));
607
- checkpoint(session);
700
+ const release = store.hold();
701
+ try {
702
+ for (const plugin of documentScopedPlugins) {
703
+ buildDocumentCapability(plugin, session);
704
+ session.connectors.get(plugin)?.();
705
+ }
706
+ } finally {
707
+ release();
608
708
  }
609
- for (const plugin of documentScopedPlugins) plugin.effects?.(createEffectContext(services, plugin, session));
709
+ session.scope.defer(session.handle.events.subscribe((event) => bursts.last(event)));
610
710
  checkpoint(session);
611
711
  }
612
- async function openDocument(input, options) {
712
+ /** What `open()` and `retry()` resolve: the document as its tab shows it, ready or locked. */
713
+ function openedResult(session) {
714
+ const core = store.getCore();
715
+ const entry = core.documents[session.id] ?? core.pending[session.id];
716
+ if (!entry) throw new require_settings.PluginError("instance-closed", "documents", `${session.id} was closed`);
717
+ return { document: documentInfoOf(entry) };
718
+ }
719
+ async function openDocument(input, options, slotId) {
720
+ const signal = options?.signal;
721
+ if (signal?.aborted) throw new require_settings.PluginError("operation-cancelled", "documents", "the open was cancelled");
722
+ try {
723
+ const { session, done } = reserveAndOpen(input, options, slotId);
724
+ const cancel = () => void session.close();
725
+ signal?.addEventListener("abort", cancel, { once: true });
726
+ try {
727
+ return openedResult(await done);
728
+ } finally {
729
+ signal?.removeEventListener("abort", cancel);
730
+ }
731
+ } catch (error) {
732
+ throw require_settings.toPluginError("documents", error);
733
+ }
734
+ }
735
+ /**
736
+ * The engine's open options for a document: its own, and the viewer's `scope` and
737
+ * `identity` where it has none. A document's own replace the viewer's, never merge.
738
+ */
739
+ function engineOptionsOf(options) {
740
+ const { activate: _activate, name: _name, signal: _signal, ...own } = options ?? {};
741
+ const viewer = viewerSettings.api.getSettings();
742
+ const scope = own.scope ?? viewer.scope ?? void 0;
743
+ const identity = own.identity ?? viewer.identity ?? void 0;
744
+ return {
745
+ ...own,
746
+ ...scope !== void 0 ? { scope } : {},
747
+ ...identity !== void 0 ? { identity } : {}
748
+ };
749
+ }
750
+ /**
751
+ * Reserve the tab slot synchronously and start the open; returns the slot id at once.
752
+ * `slotId` is a failed document's tab, which `retry()` opens again in place.
753
+ */
754
+ function reserveAndOpen(input, options, slotId) {
613
755
  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);
756
+ const engineOptions = engineOptionsOf(options);
757
+ const requestedId = slotId ?? (typeof input === "function" ? nextTicket() : idOfInput(input) ?? nextTicket());
758
+ if (sessions.has(requestedId)) throw new require_settings.PluginError("conflict", "documents", `document already open: ${requestedId}`);
759
+ const session = createSession(requestedId, options?.name);
760
+ session.source = input;
761
+ session.openOptions = options;
618
762
  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;
763
+ publishPendingSlot(session, options?.activate ?? true);
764
+ return {
765
+ session,
766
+ done: beginOperation(session, async () => {
767
+ try {
768
+ const given = typeof input === "function" ? await engineCall(session, Promise.resolve(input(session.cancel.signal))) : input;
769
+ checkpoint(session);
770
+ const source = isUrlSource(given) ? await engineCall(session, fetchUrlSource(given, session.cancel.signal)) : given;
771
+ checkpoint(session);
772
+ const sourceId = idOfInput(source);
773
+ if (sourceId) rekeySession(session, sourceId);
774
+ const handle = await engineCall(session, engine.open(source, engineOptions), (late) => late.close());
775
+ session.handle = handle;
776
+ session.scope.defer(() => handle.close());
777
+ checkpoint(session);
778
+ if (handle.id !== session.id) rekeySession(session, handle.id);
779
+ const prompt = handle.security?.passwordPrompt;
780
+ if (prompt?.state === "required") {
781
+ session.phase = "locked";
782
+ publishLocked(session, prompt.incorrect);
783
+ return session;
784
+ }
785
+ const snapshot = await engineCall(session, handle.pages.list());
786
+ checkpoint(session);
787
+ await bringUp(session, snapshot);
788
+ commitReady(session);
789
+ return session;
790
+ } catch (error) {
791
+ if (require_settings.isCancelled(error)) throw error;
792
+ if (session.phase === "closing" || isAbortLike(error)) throw new require_settings.CancelledError(`closed while opening: ${session.id}`);
793
+ await session.scope.dispose();
794
+ session.phase = "error";
795
+ publishError(session, error);
796
+ throw error;
636
797
  }
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
- });
798
+ })
799
+ };
651
800
  }
652
- async function unlockDocument(id, input) {
801
+ async function unlockDocument(id, options) {
653
802
  guardUsable("documents.unlock()");
654
803
  const session = sessions.get(id);
655
- if (!session || session.phase !== "locked" || !session.handle) throw new Error(`[documents] document is not locked: ${id}`);
804
+ if (!session || session.phase !== "locked" || !session.handle) throw new require_settings.PluginError("invalid-input", "documents", `document is not locked: ${id}`);
656
805
  const handle = session.handle;
657
806
  return beginOperation(session, async () => {
658
807
  try {
659
- await engineCall(session, handle.security.unlock({ password: input.password }));
808
+ await require_settings.cancellable("documents", options.signal, engineCall(session, handle.security.unlock({ password: options.password })));
660
809
  } catch (error) {
661
- if (session.phase === "closing" || isAbortLike(error)) throw new CancelledError(`closed while opening: ${session.id}`);
810
+ if (session.phase === "closing" || isAbortLike(error)) throw new require_settings.CancelledError(`closed while opening: ${session.id}`);
662
811
  throw error;
663
812
  }
664
813
  checkpoint(session);
@@ -668,13 +817,15 @@ function createKernel(opts) {
668
817
  await bringUp(session, snapshot);
669
818
  commitReady(session);
670
819
  } catch (error) {
671
- if (isCancelled(error)) throw error;
672
- if (session.phase === "closing" || isAbortLike(error)) throw new CancelledError(`closed while opening: ${session.id}`);
820
+ if (require_settings.isCancelled(error)) throw error;
821
+ if (session.phase === "closing" || isAbortLike(error)) throw new require_settings.CancelledError(`closed while opening: ${session.id}`);
673
822
  await session.scope.dispose();
674
823
  session.phase = "error";
675
824
  publishError(session, error);
676
825
  throw error;
677
826
  }
827
+ }).catch((error) => {
828
+ throw require_settings.toPluginError("documents", error);
678
829
  });
679
830
  }
680
831
  async function closeDocument(documentId) {
@@ -683,18 +834,81 @@ function createKernel(opts) {
683
834
  await session.close();
684
835
  }
685
836
  function reorder(next) {
686
- store.setCore({ order: next }, { type: CORE_ORDER_CHANGED });
837
+ setCore({ order: next });
687
838
  }
839
+ const metaOf = (documentId) => {
840
+ const core = store.getCore();
841
+ const id = documentId ?? core.activeId;
842
+ return id ? core.documents[id] ?? null : null;
843
+ };
844
+ /** The tab list: the same array until one of its documents changes, or the order does. */
845
+ let listMemo = null;
846
+ const listDocuments = () => {
847
+ const core = store.getCore();
848
+ if (listMemo?.core === core) return listMemo.value;
849
+ const next = core.order.map((id) => documentInfoOf(core.documents[id] ?? core.pending[id]));
850
+ const previous = listMemo?.value;
851
+ const value = previous !== void 0 && previous.length === next.length && previous.every((info, i) => info === next[i]) ? previous : Object.freeze(next);
852
+ listMemo = {
853
+ core,
854
+ value
855
+ };
856
+ return value;
857
+ };
858
+ /** A document's fields: `documentId`, or the active document's. */
859
+ const getDocument = (documentId) => {
860
+ const core = store.getCore();
861
+ const id = documentId ?? core.activeId;
862
+ const entry = id ? core.documents[id] ?? core.pending[id] : void 0;
863
+ return entry ? documentInfoOf(entry) : null;
864
+ };
688
865
  const documents = {
689
866
  open: openDocument,
690
- openAll: (docs) => {
867
+ canOpenFiles: () => engine.canOpenFiles,
868
+ retry: async (id, options) => {
869
+ const session = sessions.get(id);
870
+ 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`);
871
+ const { source, openOptions } = session;
872
+ sessions.delete(id);
873
+ return openDocument(source, {
874
+ ...openOptions,
875
+ activate: false,
876
+ signal: options?.signal
877
+ }, id);
878
+ },
879
+ rename: (id, name) => {
880
+ const core = store.getCore();
881
+ if (core.documents[id]) setCore({ documents: {
882
+ ...core.documents,
883
+ [id]: {
884
+ ...core.documents[id],
885
+ name
886
+ }
887
+ } });
888
+ else if (core.pending[id]) setCore({ pending: {
889
+ ...core.pending,
890
+ [id]: {
891
+ ...core.pending[id],
892
+ name
893
+ }
894
+ } });
895
+ const session = sessions.get(id);
896
+ if (session) session.name = name;
897
+ },
898
+ openAll: (initialDocuments) => {
691
899
  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(() => {});
900
+ const activeIndex = Math.max(0, initialDocuments.findIndex((initialDocument) => initialDocument.active));
901
+ return initialDocuments.map(({ source, active: _active, ...options }, index) => {
902
+ try {
903
+ const { session, done } = reserveAndOpen(source, {
904
+ ...options,
905
+ activate: index === activeIndex
906
+ });
907
+ done.catch(() => {});
908
+ return session.id;
909
+ } catch {
910
+ return typeof source === "function" ? "" : idOfInput(source) ?? "";
911
+ }
698
912
  });
699
913
  },
700
914
  unlock: unlockDocument,
@@ -704,67 +918,77 @@ function createKernel(opts) {
704
918
  },
705
919
  setActive: (id) => {
706
920
  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;
921
+ if (core.documents[id] || core.pending[id]) setCore({ activeId: id });
721
922
  },
923
+ getActiveId: () => store.getCore().activeId,
924
+ getActive: () => getDocument(),
925
+ list: listDocuments,
926
+ get: getDocument,
722
927
  has: (id) => {
723
928
  const core = store.getCore();
724
929
  return core.documents[id] !== void 0 || core.pending[id] !== void 0;
725
930
  },
726
- count: () => store.getCore().order.length,
727
- order: () => [...store.getCore().order],
931
+ getCount: () => store.getCore().order.length,
932
+ getOrder: () => store.getCore().order,
933
+ setOrder: (ids) => {
934
+ const current = store.getCore().order;
935
+ 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");
936
+ reorder([...ids]);
937
+ },
728
938
  move: (id, toIndex) => {
729
939
  const core = store.getCore();
730
940
  if (!core.documents[id] && !core.pending[id]) return;
731
- const without = core.order.filter((x) => x !== id);
941
+ const without = core.order.filter((documentId) => documentId !== id);
732
942
  const clamped = Math.max(0, Math.min(toIndex, without.length));
733
943
  without.splice(clamped, 0, id);
734
944
  reorder(without);
735
945
  },
736
- swap: (a, b) => {
946
+ swap: (id, otherId) => {
737
947
  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;
948
+ const index = core.order.indexOf(id);
949
+ const otherIndex = core.order.indexOf(otherId);
950
+ if (index < 0 || otherIndex < 0) return;
741
951
  const next = [...core.order];
742
- next[indexA] = b;
743
- next[indexB] = a;
952
+ next[index] = otherId;
953
+ next[otherIndex] = id;
744
954
  reorder(next);
745
955
  },
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();
756
- },
757
- allows: (cap, id) => documentHandle(id)?.security.allows(cap) ?? false
956
+ download: (id, options) => downloadFile(id, options?.signal, "download", (handle) => handle.download(options?.mode !== void 0 ? { mode: options.mode } : void 0)),
957
+ 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()),
958
+ canDownload: (id) => allowsOn(id, "doc.download"),
959
+ canPrint: (id) => allowsOn(id, "doc.print"),
960
+ listPages: (id) => metaOf(id)?.pages ?? EMPTY_PAGES,
961
+ getPage: (page, id) => require_settings.findPage(metaOf(id)?.pages ?? EMPTY_PAGES, page),
962
+ getPageIndex: (ref, id) => metaOf(id)?.pages.findIndex((pageInfo) => (0, _embedpdf_engine_core_runtime.pageRefsEqual)(pageInfo.ref, ref)) ?? -1,
963
+ getRevision: (id) => metaOf(id)?.revision ?? -1,
964
+ onOpened: opened.on,
965
+ onOpenFailed: openFailed.on,
966
+ onLocked: locked.on,
967
+ onClosed: closed.on,
968
+ onActiveChanged: activeChanged.on,
969
+ onPagesChanged: pagesChanged.on,
970
+ onUnsavedChangesChanged: unsavedChangesChanged.on
758
971
  };
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)));
972
+ workspaceCapabilities.set(require_settings.DocumentsToken, documents);
973
+ for (const plugin of plan.ordered) if (!isDocumentScoped(plugin)) workspaceLeases.set(plugin, store.lease(plugin.id, initialStateOf(plugin)));
974
+ for (const plugin of plan.ordered) {
975
+ if (isDocumentScoped(plugin)) continue;
976
+ const ctx = require_settings.createPluginContext(services, plugin, void 0, workspaceCancel.signal, workspaceScope);
977
+ const { api, connect } = plugin.create(ctx);
978
+ if (plugin.token) workspaceCapabilities.set(plugin.token, api);
979
+ workspaceConnectors.set(plugin, () => {
980
+ connect?.();
981
+ require_settings.markConnected(ctx);
982
+ });
983
+ }
762
984
  return {
985
+ ...viewerSettings.api,
763
986
  engine,
764
987
  documents,
765
988
  capability: resolveCapability,
766
989
  tryCapability: tryResolveCapability,
767
990
  scopeOf: plan.scopeOf,
991
+ settingsOf,
768
992
  subscribe: store.subscribe,
769
993
  getState: store.getState,
770
994
  status: () => status,
@@ -775,10 +999,8 @@ function createKernel(opts) {
775
999
  try {
776
1000
  for (const plugin of plan.ordered) {
777
1001
  if (status !== "starting") return;
778
- if (!isDocumentScoped(plugin)) await plugin.init?.(createPluginContext(services, plugin));
1002
+ if (!isDocumentScoped(plugin)) workspaceConnectors.get(plugin)?.();
779
1003
  }
780
- if (status !== "starting") return;
781
- for (const plugin of plan.ordered) if (!isDocumentScoped(plugin)) plugin.effects?.(createEffectContext(services, plugin));
782
1004
  status = "started";
783
1005
  } catch (error) {
784
1006
  status = "failed";
@@ -792,8 +1014,9 @@ function createKernel(opts) {
792
1014
  return destroyPromise ?? (destroyPromise = (async () => {
793
1015
  const wasFailed = status === "failed";
794
1016
  status = "destroying";
1017
+ workspaceCancel.abort(new require_settings.CancelledError("kernel destroyed"));
795
1018
  await startPromise?.catch((error) => {
796
- if (!isCancelled(error) && !wasFailed) report(error);
1019
+ if (!require_settings.isCancelled(error) && !wasFailed) report(error);
797
1020
  });
798
1021
  await Promise.allSettled([...sessions.values()].map((session) => session.close()));
799
1022
  sessions.clear();
@@ -806,56 +1029,213 @@ function createKernel(opts) {
806
1029
  };
807
1030
  }
808
1031
  //#endregion
809
- //#region src/event-hook.ts
1032
+ //#region src/compose.ts
1033
+ function composeApi(capability, slices) {
1034
+ const seen = /* @__PURE__ */ new Set();
1035
+ const api = {};
1036
+ for (const slice of slices) for (const key of Object.keys(slice)) {
1037
+ if (seen.has(key)) throw new Error(`[${capability}] api member '${key}' is defined twice`);
1038
+ seen.add(key);
1039
+ api[key] = slice[key];
1040
+ }
1041
+ return api;
1042
+ }
1043
+ //#endregion
1044
+ //#region src/memo.ts
1045
+ const sameInputs = (left, right) => left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
810
1046
  /**
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.
1047
+ * Recompute `compute(...inputs())` only when an input changes (compared with
1048
+ * `Object.is`, element by element); otherwise return the previous result.
819
1049
  */
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;
1050
+ function memo(inputs, compute) {
1051
+ let last = null;
1052
+ return () => {
1053
+ const next = inputs();
1054
+ if (last && sameInputs(last.inputs, next)) return last.result;
1055
+ const result = compute(...next);
1056
+ last = {
1057
+ inputs: next,
1058
+ result
1059
+ };
1060
+ return result;
1061
+ };
1062
+ }
1063
+ /**
1064
+ * `memo` per key: each key keeps its own last inputs and result. Keys are
1065
+ * compared with `Map` semantics, so use primitives (a page object number, an
1066
+ * encoded key), not freshly built objects. `maxEntries` bounds the cache;
1067
+ * the oldest key is evicted first.
1068
+ */
1069
+ function memoByKey(inputs, compute, options = {}) {
1070
+ const cache = /* @__PURE__ */ new Map();
1071
+ return (key) => {
1072
+ const next = inputs(key);
1073
+ const cached = cache.get(key);
1074
+ if (cached && sameInputs(cached.inputs, next)) return cached.result;
1075
+ const result = compute(key, ...next);
1076
+ if (cached) cache.delete(key);
1077
+ else if (options.maxEntries !== void 0 && cache.size >= options.maxEntries) {
1078
+ const oldest = cache.keys().next();
1079
+ if (!oldest.done) cache.delete(oldest.value);
838
1080
  }
1081
+ cache.set(key, {
1082
+ inputs: next,
1083
+ result
1084
+ });
1085
+ return result;
839
1086
  };
840
1087
  }
841
1088
  //#endregion
842
- //#region src/serial-queue.ts
1089
+ //#region src/state.ts
1090
+ /**
1091
+ * Declare a plugin's state. The fields come from `read`, and `empty` is checked
1092
+ * against them (`NoInfer` keeps it out of the inference): a missing field or a
1093
+ * value of the wrong type is a type error. Write `empty` inline, because
1094
+ * TypeScript refuses an extra field only in an object literal.
1095
+ */
1096
+ function defineState(token, declaration) {
1097
+ return {
1098
+ token,
1099
+ read: declaration.read,
1100
+ empty: Object.freeze(declaration.empty),
1101
+ ...declaration.ensure ? { ensure: declaration.ensure } : {}
1102
+ };
1103
+ }
843
1104
  /**
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.
1105
+ * One level of identity: equal when the two values are the same, or are
1106
+ * objects (or arrays) holding the same values under the same keys. Declared
1107
+ * state compares this way, and so does a value a selector picks from it.
1108
+ */
1109
+ function shallowEqual(left, right) {
1110
+ if (Object.is(left, right)) return true;
1111
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
1112
+ if (Array.isArray(left) !== Array.isArray(right)) return false;
1113
+ const leftFields = left;
1114
+ const rightFields = right;
1115
+ const keys = Object.keys(leftFields);
1116
+ return keys.length === Object.keys(rightFields).length && keys.every((key) => key in rightFields && Object.is(leftFields[key], rightFields[key]));
1117
+ }
1118
+ //#endregion
1119
+ //#region src/documents-state.ts
1120
+ /**
1121
+ * The documents' State tables as code: the document a component talks to (React's
1122
+ * `useDocument()`), and every open document with the active one (`useDocumentsState()`). Each
1123
+ * adapter turns these into its own reactive form, as it does a plugin's.
1124
+ */
1125
+ /** No document: none is open, or none is in scope. It reads as one still on its way. */
1126
+ const NO_DOCUMENT = Object.freeze({
1127
+ id: "",
1128
+ name: void 0,
1129
+ status: "loading",
1130
+ pageCount: 0,
1131
+ hasUnsavedChanges: false,
1132
+ passwordProvided: void 0,
1133
+ error: void 0
1134
+ });
1135
+ /**
1136
+ * The document in scope (the capability resolved for a document, or else the active one): its
1137
+ * id, name, status, page count, unsaved changes, and why it's locked or failed.
1138
+ */
1139
+ const documentState = defineState(require_settings.DocumentsToken, {
1140
+ read: (documents) => documents.get() ?? NO_DOCUMENT,
1141
+ empty: NO_DOCUMENT
1142
+ });
1143
+ const documentsState = defineState(require_settings.DocumentsToken, {
1144
+ read: (documents) => ({
1145
+ documents: documents.list(),
1146
+ activeId: documents.getActiveId()
1147
+ }),
1148
+ empty: {
1149
+ documents: [],
1150
+ activeId: null
1151
+ }
1152
+ });
1153
+ //#endregion
1154
+ //#region src/capability-handle.ts
1155
+ /**
1156
+ * What every framework adapter's plugin API shares (`useStage()`, `inject(EpdfSearch)`): which
1157
+ * members are reads, and what a document plugin's API reaches while its part of the app has no
1158
+ * ready document.
1159
+ *
1160
+ * Reads follow the capability vocabulary (docs/conventions/naming.md): a member named `get*`,
1161
+ * `list*`, `is*`, `has*` or `can*` reads state, so an adapter makes a call to it in a template or
1162
+ * a derived value update when its answer changes, and every other member tracks nothing.
847
1163
  *
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.
1164
+ * Without a document there is no capability to call, so the API reaches a stand-in: reading a
1165
+ * member never throws, so chrome renders before the first document opens, and calling one refuses
1166
+ * with `not-ready`. A member the token lists in `promises` returns the refusal as a rejected
1167
+ * promise, as the capability itself would, so a `.catch()` sees it; every other member throws.
1168
+ * The settings calls work: a plugin's settings belong to the plugin, not to a document.
851
1169
  */
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;
1170
+ const READ = /^(?:get|list|is|has|can)(?:[A-Z]|$)/;
1171
+ /** Whether a capability member named `name` is a read: `get*`, `list*`, `is*`, `has*` or `can*`. */
1172
+ function isReadMember(name) {
1173
+ return READ.test(name);
1174
+ }
1175
+ /** Whether the member at `path` (`'setValue'`, `'comments.reply'`) returns a promise, as `token` declares. */
1176
+ function returnsPromise(token, path) {
1177
+ return token.promises?.[path] === true;
1178
+ }
1179
+ /** The capability members that work without a document: a plugin's settings are the plugin's own. */
1180
+ const SETTINGS_CALLS = /* @__PURE__ */ new Set([
1181
+ "getSettings",
1182
+ "updateSettings",
1183
+ "resetSettings",
1184
+ "onSettingsChanged"
1185
+ ]);
1186
+ const standIns = /* @__PURE__ */ new WeakMap();
1187
+ /**
1188
+ * What a framework adapter's API reaches for a document plugin while its part of the app has no
1189
+ * ready document. One per kernel and token, so it is the same object on every read.
1190
+ */
1191
+ function standInFor(kernel, token) {
1192
+ let byToken = standIns.get(kernel);
1193
+ if (!byToken) {
1194
+ byToken = /* @__PURE__ */ new WeakMap();
1195
+ standIns.set(kernel, byToken);
1196
+ }
1197
+ let standIn = byToken.get(token);
1198
+ if (!standIn) {
1199
+ standIn = createStandIn(token, () => kernel.settingsOf(token));
1200
+ byToken.set(token, standIn);
1201
+ }
1202
+ return standIn;
1203
+ }
1204
+ /**
1205
+ * A Proxy whose every member is a function that refuses with `not-ready`. A member is itself such
1206
+ * a Proxy, so namespaces work too (`annotation.comments.reply`). Members are cached by path, so
1207
+ * `stage.zoomIn` is the same function on every read. `then`, symbol keys and keys that start with
1208
+ * `__` (a framework's own flags, such as Vue's `__v_isRef`) read as undefined: the stand-in is no
1209
+ * thenable, and inspecting it doesn't throw. The four settings calls forward to `settings()` when
1210
+ * called; for a plugin without settings, they refuse too.
1211
+ */
1212
+ function createStandIn(token, settings) {
1213
+ const refusal = () => new require_settings.PluginError("not-ready", token.name, "no document is open");
1214
+ const refuse = (path) => () => {
1215
+ if (returnsPromise(token, path)) return Promise.reject(refusal());
1216
+ throw refusal();
1217
+ };
1218
+ const forward = (name) => (...args) => {
1219
+ let api;
1220
+ try {
1221
+ api = settings();
1222
+ } catch {
1223
+ throw refusal();
1224
+ }
1225
+ return api[name](...args);
1226
+ };
1227
+ const members = /* @__PURE__ */ new Map();
1228
+ const memberOf = (parentPath, key) => {
1229
+ if (typeof key === "symbol" || key === "then" || key.startsWith("__")) return void 0;
1230
+ const path = parentPath ? `${parentPath}.${key}` : key;
1231
+ let member = members.get(path);
1232
+ if (!member) {
1233
+ member = !parentPath && SETTINGS_CALLS.has(key) ? forward(key) : new Proxy(refuse(path), { get: (_refuse, next) => memberOf(path, next) });
1234
+ members.set(path, member);
1235
+ }
1236
+ return member;
858
1237
  };
1238
+ return new Proxy({}, { get: (_capability, key) => memberOf("", key) });
859
1239
  }
860
1240
  //#endregion
861
1241
  //#region src/index.ts
@@ -863,16 +1243,33 @@ function createSerialQueue() {
863
1243
  * Create a typed capability token. `name` is the token's identity (debugging,
864
1244
  * error messages); `options.hint` is the authored remedy the kernel appends to
865
1245
  * the missing-dependency error, so the error contains its own fix.
1246
+ * `options.promises` lists the members that return a promise, so a framework
1247
+ * adapter's stand-in rejects them, rather than throwing, while no document is
1248
+ * open; a document plugin whose capability has such members gives it.
866
1249
  */
867
1250
  function createCapabilityToken(name, options) {
868
1251
  return {
869
1252
  name,
870
- hint: options?.hint
1253
+ hint: options?.hint,
1254
+ ...options?.promises ? { promises: options.promises } : {}
871
1255
  };
872
1256
  }
873
- /** Identity helper that pins a plugin's generics. The real win is inference. */
874
- function definePlugin(def) {
875
- return def;
1257
+ /**
1258
+ * The host lens over a plugin's token: the same runtime object, typed with
1259
+ * the wider capability sibling plugins use. The one place that cast lives.
1260
+ */
1261
+ function createHostToken(token) {
1262
+ return token;
1263
+ }
1264
+ /**
1265
+ * Define a plugin. It returns the definition as it is, and reads the types from it, so a
1266
+ * plugin passes no type arguments: the capability from `token`, the state from `state`, the
1267
+ * settings from `settings.defaults`. `create` receives a `PluginContext<State, Settings>`
1268
+ * and must return the capability. A plugin without `state` is stateless (`void`); one
1269
+ * without `settings` has `NoSettings`, and `ctx.settings()` throws there.
1270
+ */
1271
+ function definePlugin(definition) {
1272
+ return definition;
876
1273
  }
877
1274
  //#endregion
878
1275
  Object.defineProperty(exports, "AbortablePromise", {
@@ -887,30 +1284,74 @@ Object.defineProperty(exports, "CONTINUOUS_RENDER_POLICY", {
887
1284
  return _embedpdf_engine_core_runtime.CONTINUOUS_RENDER_POLICY;
888
1285
  }
889
1286
  });
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;
1287
+ exports.CancelledError = require_settings.CancelledError;
1288
+ exports.DocumentsToken = require_settings.DocumentsToken;
900
1289
  Object.defineProperty(exports, "PermissionDenied", {
901
1290
  enumerable: true,
902
1291
  get: function() {
903
1292
  return _embedpdf_engine_core_runtime.PermissionDenied;
904
1293
  }
905
1294
  });
1295
+ exports.PluginError = require_settings.PluginError;
1296
+ exports.VIEWER_DEFAULTS = VIEWER_DEFAULTS;
1297
+ exports.VIEWER_WHOLE_SETTINGS = VIEWER_WHOLE_SETTINGS;
1298
+ Object.defineProperty(exports, "annotationKey", {
1299
+ enumerable: true,
1300
+ get: function() {
1301
+ return _embedpdf_engine_core_runtime.annotationKey;
1302
+ }
1303
+ });
1304
+ exports.composeApi = composeApi;
906
1305
  exports.createCapabilityToken = createCapabilityToken;
907
- exports.createEventHook = createEventHook;
1306
+ exports.createEventHook = require_settings.createEventHook;
1307
+ exports.createHostToken = createHostToken;
908
1308
  exports.createKernel = createKernel;
909
- exports.createSerialQueue = createSerialQueue;
1309
+ exports.createLatestLane = require_settings.createLatestLane;
1310
+ exports.createSerialQueue = require_settings.createSerialQueue;
1311
+ Object.defineProperty(exports, "decodePageKey", {
1312
+ enumerable: true,
1313
+ get: function() {
1314
+ return _embedpdf_engine_core_runtime.decodePageKey;
1315
+ }
1316
+ });
910
1317
  exports.definePlugin = definePlugin;
911
- exports.docInfoEquals = docInfoEquals;
912
- exports.docInfoListEquals = docInfoListEquals;
913
- exports.isCancelled = isCancelled;
1318
+ exports.defineState = defineState;
1319
+ exports.documentState = documentState;
1320
+ exports.documentsState = documentsState;
1321
+ Object.defineProperty(exports, "encodePageKey", {
1322
+ enumerable: true,
1323
+ get: function() {
1324
+ return _embedpdf_engine_core_runtime.encodePageKey;
1325
+ }
1326
+ });
1327
+ exports.guardHandle = require_settings.guardHandle;
1328
+ exports.isCancelled = require_settings.isCancelled;
1329
+ Object.defineProperty(exports, "isLocalEngine", {
1330
+ enumerable: true,
1331
+ get: function() {
1332
+ return _embedpdf_engine_core_runtime.isLocalEngine;
1333
+ }
1334
+ });
1335
+ exports.isPluginError = require_settings.isPluginError;
1336
+ exports.isReadMember = isReadMember;
1337
+ exports.memo = memo;
1338
+ exports.memoByKey = memoByKey;
1339
+ exports.mergeSettings = require_settings.mergeSettings;
1340
+ Object.defineProperty(exports, "pageRefsEqual", {
1341
+ enumerable: true,
1342
+ get: function() {
1343
+ return _embedpdf_engine_core_runtime.pageRefsEqual;
1344
+ }
1345
+ });
1346
+ exports.reload = require_settings.reload;
1347
+ Object.defineProperty(exports, "resolvePageLayers", {
1348
+ enumerable: true,
1349
+ get: function() {
1350
+ return _embedpdf_engine_core_runtime.resolvePageLayers;
1351
+ }
1352
+ });
1353
+ exports.returnsPromise = returnsPromise;
1354
+ exports.shallowEqual = shallowEqual;
914
1355
  Object.defineProperty(exports, "snapAppearanceScale", {
915
1356
  enumerable: true,
916
1357
  get: function() {
@@ -929,5 +1370,16 @@ Object.defineProperty(exports, "snapTileScale", {
929
1370
  return _embedpdf_engine_core_runtime.snapTileScale;
930
1371
  }
931
1372
  });
1373
+ exports.standInFor = standInFor;
1374
+ exports.timerClock = require_settings.timerClock;
1375
+ Object.defineProperty(exports, "toPageRef", {
1376
+ enumerable: true,
1377
+ get: function() {
1378
+ return _embedpdf_engine_core_runtime.toPageRef;
1379
+ }
1380
+ });
1381
+ exports.toPluginError = require_settings.toPluginError;
1382
+ exports.toPluginErrorInfo = require_settings.toPluginErrorInfo;
1383
+ exports.viewerSettingsOf = viewerSettingsOf;
932
1384
 
933
1385
  //# sourceMappingURL=index.cjs.map