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