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