@embedpdf/core 2.14.4 → 3.0.0-next.0

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.
Files changed (117) hide show
  1. package/LICENSE +192 -21
  2. package/dist/index.cjs +864 -2
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +313 -0
  5. package/dist/index.d.ts +313 -1
  6. package/dist/index.js +816 -2124
  7. package/dist/index.js.map +1 -1
  8. package/package.json +22 -52
  9. package/README.md +0 -40
  10. package/dist/lib/base/base-plugin.d.ts +0 -190
  11. package/dist/lib/index.d.ts +0 -17
  12. package/dist/lib/plugin/builder.d.ts +0 -11
  13. package/dist/lib/registry/plugin-registry.d.ts +0 -104
  14. package/dist/lib/store/actions.d.ts +0 -154
  15. package/dist/lib/store/index.d.ts +0 -6
  16. package/dist/lib/store/initial-state.d.ts +0 -31
  17. package/dist/lib/store/plugin-store.d.ts +0 -48
  18. package/dist/lib/store/reducer-helpers.d.ts +0 -11
  19. package/dist/lib/store/reducer.d.ts +0 -4
  20. package/dist/lib/store/selectors.d.ts +0 -41
  21. package/dist/lib/store/store.d.ts +0 -106
  22. package/dist/lib/store/types.d.ts +0 -28
  23. package/dist/lib/types/errors.d.ts +0 -21
  24. package/dist/lib/types/permissions.d.ts +0 -55
  25. package/dist/lib/types/plugin.d.ts +0 -75
  26. package/dist/lib/utils/dependency-resolver.d.ts +0 -6
  27. package/dist/lib/utils/event-control.d.ts +0 -58
  28. package/dist/lib/utils/eventing.d.ts +0 -19
  29. package/dist/lib/utils/math.d.ts +0 -22
  30. package/dist/lib/utils/plugin-helpers.d.ts +0 -6
  31. package/dist/lib/utils/scoped-eventing.d.ts +0 -71
  32. package/dist/lib/utils/typed-object.d.ts +0 -12
  33. package/dist/preact/adapter.d.ts +0 -11
  34. package/dist/preact/index.cjs +0 -2
  35. package/dist/preact/index.cjs.map +0 -1
  36. package/dist/preact/index.d.ts +0 -1
  37. package/dist/preact/index.js +0 -323
  38. package/dist/preact/index.js.map +0 -1
  39. package/dist/react/adapter.d.ts +0 -2
  40. package/dist/react/index.cjs +0 -2
  41. package/dist/react/index.cjs.map +0 -1
  42. package/dist/react/index.d.ts +0 -1
  43. package/dist/react/index.js +0 -322
  44. package/dist/react/index.js.map +0 -1
  45. package/dist/shared/components/auto-mount.d.ts +0 -8
  46. package/dist/shared/components/embed-pdf.d.ts +0 -37
  47. package/dist/shared/components/index.d.ts +0 -1
  48. package/dist/shared/context.d.ts +0 -12
  49. package/dist/shared/hooks/index.d.ts +0 -7
  50. package/dist/shared/hooks/use-capability.d.ts +0 -16
  51. package/dist/shared/hooks/use-core-state.d.ts +0 -8
  52. package/dist/shared/hooks/use-document-permissions.d.ts +0 -35
  53. package/dist/shared/hooks/use-document-state.d.ts +0 -8
  54. package/dist/shared/hooks/use-plugin.d.ts +0 -16
  55. package/dist/shared/hooks/use-registry.d.ts +0 -6
  56. package/dist/shared/hooks/use-store-state.d.ts +0 -6
  57. package/dist/shared/index.d.ts +0 -3
  58. package/dist/shared-preact/components/auto-mount.d.ts +0 -8
  59. package/dist/shared-preact/components/embed-pdf.d.ts +0 -37
  60. package/dist/shared-preact/components/index.d.ts +0 -1
  61. package/dist/shared-preact/context.d.ts +0 -12
  62. package/dist/shared-preact/hooks/index.d.ts +0 -7
  63. package/dist/shared-preact/hooks/use-capability.d.ts +0 -16
  64. package/dist/shared-preact/hooks/use-core-state.d.ts +0 -8
  65. package/dist/shared-preact/hooks/use-document-permissions.d.ts +0 -35
  66. package/dist/shared-preact/hooks/use-document-state.d.ts +0 -8
  67. package/dist/shared-preact/hooks/use-plugin.d.ts +0 -16
  68. package/dist/shared-preact/hooks/use-registry.d.ts +0 -6
  69. package/dist/shared-preact/hooks/use-store-state.d.ts +0 -6
  70. package/dist/shared-preact/index.d.ts +0 -3
  71. package/dist/shared-react/components/auto-mount.d.ts +0 -8
  72. package/dist/shared-react/components/embed-pdf.d.ts +0 -37
  73. package/dist/shared-react/components/index.d.ts +0 -1
  74. package/dist/shared-react/context.d.ts +0 -12
  75. package/dist/shared-react/hooks/index.d.ts +0 -7
  76. package/dist/shared-react/hooks/use-capability.d.ts +0 -16
  77. package/dist/shared-react/hooks/use-core-state.d.ts +0 -8
  78. package/dist/shared-react/hooks/use-document-permissions.d.ts +0 -35
  79. package/dist/shared-react/hooks/use-document-state.d.ts +0 -8
  80. package/dist/shared-react/hooks/use-plugin.d.ts +0 -16
  81. package/dist/shared-react/hooks/use-registry.d.ts +0 -6
  82. package/dist/shared-react/hooks/use-store-state.d.ts +0 -6
  83. package/dist/shared-react/index.d.ts +0 -3
  84. package/dist/svelte/components/AutoMount.svelte.d.ts +0 -9
  85. package/dist/svelte/components/EmbedPDF.svelte.d.ts +0 -40
  86. package/dist/svelte/components/NestedWrapper.svelte.d.ts +0 -10
  87. package/dist/svelte/components/index.d.ts +0 -1
  88. package/dist/svelte/hooks/index.d.ts +0 -6
  89. package/dist/svelte/hooks/use-capability.svelte.d.ts +0 -14
  90. package/dist/svelte/hooks/use-core-state.svelte.d.ts +0 -9
  91. package/dist/svelte/hooks/use-document-permissions.svelte.d.ts +0 -35
  92. package/dist/svelte/hooks/use-document-state.svelte.d.ts +0 -9
  93. package/dist/svelte/hooks/use-plugin.svelte.d.ts +0 -14
  94. package/dist/svelte/hooks/use-registry.svelte.d.ts +0 -17
  95. package/dist/svelte/index.cjs +0 -2
  96. package/dist/svelte/index.cjs.map +0 -1
  97. package/dist/svelte/index.d.ts +0 -2
  98. package/dist/svelte/index.js +0 -430
  99. package/dist/svelte/index.js.map +0 -1
  100. package/dist/vue/components/auto-mount.vue.d.ts +0 -19
  101. package/dist/vue/components/embed-pdf.vue.d.ts +0 -104
  102. package/dist/vue/components/index.d.ts +0 -1
  103. package/dist/vue/components/nested-wrapper.vue.d.ts +0 -19
  104. package/dist/vue/composables/index.d.ts +0 -7
  105. package/dist/vue/composables/use-capability.d.ts +0 -15
  106. package/dist/vue/composables/use-core-state.d.ts +0 -9
  107. package/dist/vue/composables/use-document-permissions.d.ts +0 -36
  108. package/dist/vue/composables/use-document-state.d.ts +0 -8
  109. package/dist/vue/composables/use-plugin.d.ts +0 -8
  110. package/dist/vue/composables/use-registry.d.ts +0 -1
  111. package/dist/vue/composables/use-store-state.d.ts +0 -10
  112. package/dist/vue/context.d.ts +0 -13
  113. package/dist/vue/index.cjs +0 -2
  114. package/dist/vue/index.cjs.map +0 -1
  115. package/dist/vue/index.d.ts +0 -2
  116. package/dist/vue/index.js +0 -384
  117. package/dist/vue/index.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,2126 +1,818 @@
1
- import { Rotation, PdfPermissionFlag, NoopLogger, PermissionDeniedError } from "@embedpdf/models";
2
- class DependencyResolver {
3
- constructor() {
4
- this.dependencyGraph = /* @__PURE__ */ new Map();
5
- }
6
- addNode(id, dependencies = []) {
7
- this.dependencyGraph.set(id, new Set(dependencies));
8
- }
9
- hasCircularDependencies() {
10
- const visited = /* @__PURE__ */ new Set();
11
- const recursionStack = /* @__PURE__ */ new Set();
12
- const dfs = (id) => {
13
- visited.add(id);
14
- recursionStack.add(id);
15
- const dependencies = this.dependencyGraph.get(id) || /* @__PURE__ */ new Set();
16
- for (const dep of dependencies) {
17
- if (!visited.has(dep)) {
18
- if (dfs(dep)) return true;
19
- } else if (recursionStack.has(dep)) {
20
- return true;
21
- }
22
- }
23
- recursionStack.delete(id);
24
- return false;
25
- };
26
- for (const id of this.dependencyGraph.keys()) {
27
- if (!visited.has(id)) {
28
- if (dfs(id)) return true;
29
- }
30
- }
31
- return false;
32
- }
33
- resolveLoadOrder() {
34
- if (this.hasCircularDependencies()) {
35
- throw new Error("Circular dependencies detected");
36
- }
37
- const result = [];
38
- const visited = /* @__PURE__ */ new Set();
39
- const temp = /* @__PURE__ */ new Set();
40
- const visit = (id) => {
41
- if (temp.has(id)) throw new Error("Circular dependency");
42
- if (visited.has(id)) return;
43
- temp.add(id);
44
- const dependencies = this.dependencyGraph.get(id) || /* @__PURE__ */ new Set();
45
- for (const dep of dependencies) {
46
- visit(dep);
47
- }
48
- temp.delete(id);
49
- visited.add(id);
50
- result.push(id);
51
- };
52
- for (const id of this.dependencyGraph.keys()) {
53
- if (!visited.has(id)) {
54
- visit(id);
55
- }
56
- }
57
- return result;
58
- }
59
- }
60
- class PluginRegistrationError extends Error {
61
- constructor(message) {
62
- super(message);
63
- this.name = "PluginRegistrationError";
64
- }
65
- }
66
- class PluginNotFoundError extends Error {
67
- constructor(message) {
68
- super(message);
69
- this.name = "PluginNotFoundError";
70
- }
71
- }
72
- class CircularDependencyError extends Error {
73
- constructor(message) {
74
- super(message);
75
- this.name = "CircularDependencyError";
76
- }
77
- }
78
- class CapabilityNotFoundError extends Error {
79
- constructor(message) {
80
- super(message);
81
- this.name = "CapabilityNotFoundError";
82
- }
83
- }
84
- class CapabilityConflictError extends Error {
85
- constructor(message) {
86
- super(message);
87
- this.name = "CapabilityConflictError";
88
- }
89
- }
90
- class PluginInitializationError extends Error {
91
- constructor(message) {
92
- super(message);
93
- this.name = "PluginInitializationError";
94
- }
95
- }
96
- class PluginConfigurationError extends Error {
97
- constructor(message) {
98
- super(message);
99
- this.name = "PluginConfigurationError";
100
- }
101
- }
102
- class PluginStore {
103
- /**
104
- * Initializes the PluginStore with the main store and plugin ID.
105
- * @param store The main store instance.
106
- * @param pluginId The unique identifier for the plugin.
107
- */
108
- constructor(store, pluginId) {
109
- this.store = store;
110
- this.pluginId = pluginId;
111
- }
112
- /**
113
- * Gets the current state of the plugin.
114
- * @returns The plugin's state.
115
- */
116
- getState() {
117
- return this.store.getState().plugins[this.pluginId];
118
- }
119
- /**
120
- * Dispatches an action for the plugin and returns the *new* global state.
121
- * If you only need the plugin’s updated state, call `getState()` afterward.
122
- * @param action The action to dispatch.
123
- * @returns The updated global store state (after plugin reducer).
124
- */
125
- dispatch(action) {
126
- return this.store.dispatchToPlugin(this.pluginId, action);
127
- }
128
- /**
129
- * Subscribes to state changes only for this specific plugin.
130
- * You now receive (action, newPluginState, oldPluginState) in the callback.
131
- *
132
- * @param listener The callback to invoke when plugin state changes.
133
- * @returns A function to unsubscribe the listener.
134
- */
135
- subscribeToState(listener) {
136
- return this.store.subscribeToPlugin(this.pluginId, (action, newPluginState, oldPluginState) => {
137
- listener(
138
- action,
139
- newPluginState,
140
- oldPluginState
141
- );
142
- });
143
- }
144
- /**
145
- * Subscribes to a specific action type for the plugin.
146
- * This still uses the main store's `onAction`, so you get the *global*
147
- * old/new store states there. If you specifically want old/new plugin state,
148
- * use `subscribeToState` instead.
149
- *
150
- * @param type The action type to listen for.
151
- * @param handler The callback to invoke when the action occurs.
152
- * @returns A function to unsubscribe the handler.
153
- */
154
- onAction(type, handler) {
155
- return this.store.onAction(type, (action, state, oldState) => {
156
- handler(
157
- action,
158
- state.plugins[this.pluginId],
159
- oldState.plugins[this.pluginId]
160
- );
161
- });
162
- }
163
- }
164
- const START_LOADING_DOCUMENT = "START_LOADING_DOCUMENT";
165
- const UPDATE_DOCUMENT_LOADING_PROGRESS = "UPDATE_DOCUMENT_LOADING_PROGRESS";
166
- const SET_DOCUMENT_LOADED = "SET_DOCUMENT_LOADED";
167
- const SET_DOCUMENT_ERROR = "SET_DOCUMENT_ERROR";
168
- const RETRY_LOADING_DOCUMENT = "RETRY_LOADING_DOCUMENT";
169
- const CLOSE_DOCUMENT = "CLOSE_DOCUMENT";
170
- const SET_ACTIVE_DOCUMENT = "SET_ACTIVE_DOCUMENT";
171
- const REORDER_DOCUMENTS = "REORDER_DOCUMENTS";
172
- const MOVE_DOCUMENT = "MOVE_DOCUMENT";
173
- const UPDATE_DOCUMENT_SECURITY = "UPDATE_DOCUMENT_SECURITY";
174
- const REFRESH_DOCUMENT = "REFRESH_DOCUMENT";
175
- const REFRESH_PAGES = "REFRESH_PAGES";
176
- const SET_PAGES = "SET_PAGES";
177
- const SET_SCALE = "SET_SCALE";
178
- const SET_ROTATION = "SET_ROTATION";
179
- const SET_DEFAULT_SCALE = "SET_DEFAULT_SCALE";
180
- const SET_DEFAULT_ROTATION = "SET_DEFAULT_ROTATION";
181
- const CORE_ACTION_TYPES = [
182
- START_LOADING_DOCUMENT,
183
- UPDATE_DOCUMENT_LOADING_PROGRESS,
184
- SET_DOCUMENT_LOADED,
185
- CLOSE_DOCUMENT,
186
- SET_ACTIVE_DOCUMENT,
187
- SET_DOCUMENT_ERROR,
188
- RETRY_LOADING_DOCUMENT,
189
- REFRESH_DOCUMENT,
190
- REFRESH_PAGES,
191
- SET_PAGES,
192
- SET_SCALE,
193
- SET_ROTATION,
194
- SET_DEFAULT_SCALE,
195
- SET_DEFAULT_ROTATION,
196
- REORDER_DOCUMENTS,
197
- MOVE_DOCUMENT,
198
- UPDATE_DOCUMENT_SECURITY
199
- ];
200
- const startLoadingDocument = (documentId, name, scale, rotation, passwordProvided, autoActivate, permissions) => ({
201
- type: START_LOADING_DOCUMENT,
202
- payload: { documentId, name, scale, rotation, passwordProvided, autoActivate, permissions }
203
- });
204
- const updateDocumentLoadingProgress = (documentId, progress) => ({
205
- type: UPDATE_DOCUMENT_LOADING_PROGRESS,
206
- payload: { documentId, progress }
207
- });
208
- const setDocumentLoaded = (documentId, document) => ({
209
- type: SET_DOCUMENT_LOADED,
210
- payload: { documentId, document }
211
- });
212
- const setDocumentError = (documentId, error, errorCode, errorDetails) => ({
213
- type: SET_DOCUMENT_ERROR,
214
- payload: { documentId, error, errorCode, errorDetails }
215
- });
216
- const retryLoadingDocument = (documentId, passwordProvided) => ({
217
- type: RETRY_LOADING_DOCUMENT,
218
- payload: { documentId, passwordProvided }
219
- });
220
- const closeDocument = (documentId, nextActiveDocumentId) => ({
221
- type: CLOSE_DOCUMENT,
222
- payload: { documentId, nextActiveDocumentId }
223
- });
224
- const setActiveDocument = (documentId) => ({
225
- type: SET_ACTIVE_DOCUMENT,
226
- payload: documentId
227
- });
228
- const refreshDocument = (documentId, document) => ({
229
- type: REFRESH_DOCUMENT,
230
- payload: { documentId, document }
231
- });
232
- const refreshPages = (documentId, pageIndexes) => ({
233
- type: REFRESH_PAGES,
234
- payload: { documentId, pageIndexes }
235
- });
236
- const setPages = (documentId, pages) => ({
237
- type: SET_PAGES,
238
- payload: { documentId, pages }
239
- });
240
- const setScale = (scale, documentId) => ({
241
- type: SET_SCALE,
242
- payload: { scale, documentId }
243
- });
244
- const setRotation = (rotation, documentId) => ({
245
- type: SET_ROTATION,
246
- payload: { rotation, documentId }
247
- });
248
- const setDefaultScale = (scale) => ({
249
- type: SET_DEFAULT_SCALE,
250
- payload: scale
251
- });
252
- const setDefaultRotation = (rotation) => ({
253
- type: SET_DEFAULT_ROTATION,
254
- payload: rotation
255
- });
256
- const reorderDocuments = (order) => ({
257
- type: REORDER_DOCUMENTS,
258
- payload: order
259
- });
260
- const moveDocument = (documentId, toIndex) => ({
261
- type: MOVE_DOCUMENT,
262
- payload: { documentId, toIndex }
263
- });
264
- const updateDocumentSecurity = (documentId, permissions, isOwnerUnlocked) => ({
265
- type: UPDATE_DOCUMENT_SECURITY,
266
- payload: { documentId, permissions, isOwnerUnlocked }
1
+ import { AbortablePromise, CONTINUOUS_RENDER_POLICY, CONTINUOUS_RENDER_POLICY as CONTINUOUS_RENDER_POLICY$1, 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
+ cleanup: (teardown) => ownScope.defer(teardown)
150
+ };
151
+ }
152
+ /** A plugin context plus the side-effect primitives (watch / onAction / cleanup). */
153
+ function createEffectContext(services, plugin, session) {
154
+ const { store } = services;
155
+ const ownScope = session?.scope ?? services.workspaceScope;
156
+ return {
157
+ ...createPluginContext(services, plugin, session),
158
+ watch: (select, handler, isEqual = Object.is) => {
159
+ let previous = select();
160
+ const unsubscribe = store.subscribe(() => {
161
+ const next = select();
162
+ if (!isEqual(previous, next)) {
163
+ const prior = previous;
164
+ previous = next;
165
+ handler(next, prior);
166
+ }
167
+ });
168
+ ownScope.defer(unsubscribe);
169
+ return unsubscribe;
170
+ },
171
+ onAction: (type, handler) => {
172
+ const unsubscribe = store.subscribeAction((action) => {
173
+ if (action.type === type) handler(action);
174
+ });
175
+ ownScope.defer(unsubscribe);
176
+ return unsubscribe;
177
+ }
178
+ };
179
+ }
180
+ //#endregion
181
+ //#region src/order.ts
182
+ /**
183
+ * Validate `requires` (fail fast) and topologically sort plugins so dependencies
184
+ * are initialised before dependents.
185
+ */
186
+ function planPlugins(plugins) {
187
+ const providerByToken = /* @__PURE__ */ new Map();
188
+ const scopeByToken = /* @__PURE__ */ new Map([[DocumentsToken, "workspace"]]);
189
+ for (const plugin of plugins) if (plugin.token && plugin.capability) {
190
+ providerByToken.set(plugin.token, plugin);
191
+ scopeByToken.set(plugin.token, plugin.scope ?? "workspace");
192
+ }
193
+ 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.`);
194
+ const ordered = [];
195
+ const status = /* @__PURE__ */ new Map();
196
+ const visit = (plugin) => {
197
+ const state = status.get(plugin.id);
198
+ if (state === "done") return;
199
+ if (state === "visiting") throw new Error(`Dependency cycle involving plugin "${plugin.id}".`);
200
+ status.set(plugin.id, "visiting");
201
+ for (const token of [...plugin.requires ?? [], ...plugin.optional ?? []]) {
202
+ const dependency = providerByToken.get(token);
203
+ if (dependency && dependency !== plugin) visit(dependency);
204
+ }
205
+ status.set(plugin.id, "done");
206
+ ordered.push(plugin);
207
+ };
208
+ for (const plugin of plugins) visit(plugin);
209
+ return {
210
+ ordered,
211
+ providerOf: (token) => providerByToken.get(token),
212
+ scopeOf: (token) => scopeByToken.get(token) ?? "workspace"
213
+ };
214
+ }
215
+ //#endregion
216
+ //#region src/scope.ts
217
+ /**
218
+ * A lifecycle operation was cancelled — the tab was closed mid-open, or the
219
+ * kernel was destroyed mid-start. NOT a failure: callers that race `open()`
220
+ * against `close()` should treat this rejection as "the close won".
221
+ */
222
+ var CancelledError = class extends Error {
223
+ constructor(what) {
224
+ super(`[kernel] cancelled: ${what}`);
225
+ this.name = "CancelledError";
226
+ }
227
+ };
228
+ const isCancelled = (error) => error instanceof CancelledError;
229
+ function createScope(report) {
230
+ const teardowns = [];
231
+ let disposal = null;
232
+ return {
233
+ get disposed() {
234
+ return disposal !== null;
235
+ },
236
+ defer(teardown) {
237
+ if (disposal) disposal.then(() => Promise.resolve().then(teardown).catch(report));
238
+ else teardowns.push(teardown);
239
+ },
240
+ dispose() {
241
+ return disposal ?? (disposal = (async () => {
242
+ while (teardowns.length) try {
243
+ await teardowns.pop()();
244
+ } catch (error) {
245
+ report(error);
246
+ }
247
+ })());
248
+ }
249
+ };
250
+ }
251
+ //#endregion
252
+ //#region src/kernel.ts
253
+ /** The new page registry a document mutation carries, or null for events that
254
+ * don't change page structure (annotations, metadata). The snapshot is the
255
+ * same shape `pages.list()` returns, so callers swap it in directly. */
256
+ function layoutFromEvent(event) {
257
+ switch (event.type) {
258
+ case "pages.moved":
259
+ case "pages.rotated":
260
+ case "pages.deleted":
261
+ case "pages.inserted": return event.layout;
262
+ default: return null;
263
+ }
264
+ }
265
+ const isDocumentScoped = (plugin) => plugin.scope === "document";
266
+ const initialStateOf = (plugin) => typeof plugin.initialState === "function" ? plugin.initialState() : plugin.initialState ?? {};
267
+ const reducerOf = (plugin) => plugin.reduce ?? ((state) => state);
268
+ const toDocInfo = (meta) => ({
269
+ id: meta.id,
270
+ name: meta.name,
271
+ status: "ready",
272
+ pageCount: meta.pageCount
267
273
  });
268
- class Store {
269
- /**
270
- * Initializes the store with the provided core state.
271
- * @param reducer The core reducer function
272
- * @param initialCoreState The initial core state
273
- */
274
- constructor(reducer, initialCoreState2) {
275
- this.initialCoreState = initialCoreState2;
276
- this.pluginReducers = {};
277
- this.listeners = [];
278
- this.pluginListeners = {};
279
- this.isDispatching = false;
280
- this.state = { core: initialCoreState2, plugins: {} };
281
- this.coreReducer = reducer;
282
- }
283
- /**
284
- * Adds a reducer for a plugin-specific state.
285
- * @param pluginId The unique identifier for the plugin.
286
- * @param reducer The reducer function for the plugin state.
287
- * @param initialState The initial state for the plugin.
288
- */
289
- addPluginReducer(pluginId, reducer, initialState) {
290
- this.state.plugins[pluginId] = initialState;
291
- this.pluginReducers[pluginId] = reducer;
292
- }
293
- /**
294
- * Dispatches an action *only* to the core reducer.
295
- * Notifies the global store listeners with (action, newState, oldState).
296
- *
297
- * @param action The action to dispatch, typed as CoreAction
298
- * @returns The updated *global* store state
299
- */
300
- dispatchToCore(action) {
301
- if (!this.coreReducer) {
302
- return this.getState();
303
- }
304
- if (this.isDispatching) {
305
- throw new Error(
306
- "Reducers may not dispatch actions. To trigger cascading actions, dispatch from a listener callback instead."
307
- );
308
- }
309
- const oldState = this.getState();
310
- try {
311
- this.isDispatching = true;
312
- this.state.core = this.coreReducer(this.state.core, action);
313
- } finally {
314
- this.isDispatching = false;
315
- }
316
- this.listeners.forEach((listener) => {
317
- const currentState = this.getState();
318
- listener(action, currentState, oldState);
319
- });
320
- return this.getState();
321
- }
322
- /**
323
- * Dispatches an action *only* to a specific plugin.
324
- * Optionally notifies global store listeners if `notifyGlobal` is true.
325
- * Always notifies plugin-specific listeners with (action, newPluginState, oldPluginState).
326
- *
327
- * @param pluginId The plugin identifier
328
- * @param action The plugin action to dispatch
329
- * @param notifyGlobal Whether to also notify global store listeners
330
- * @returns The updated plugin state
331
- */
332
- dispatchToPlugin(pluginId, action, notifyGlobal = true) {
333
- if (this.isDispatching) {
334
- throw new Error(
335
- "Reducers may not dispatch actions. To trigger cascading actions, dispatch from a listener callback instead."
336
- );
337
- }
338
- const oldGlobalState = this.getState();
339
- const reducer = this.pluginReducers[pluginId];
340
- if (!reducer) {
341
- return oldGlobalState.plugins[pluginId];
342
- }
343
- const oldPluginState = oldGlobalState.plugins[pluginId];
344
- try {
345
- this.isDispatching = true;
346
- const newPluginState = reducer(oldPluginState, action);
347
- this.state.plugins[pluginId] = newPluginState;
348
- } finally {
349
- this.isDispatching = false;
350
- }
351
- if (notifyGlobal) {
352
- this.listeners.forEach((listener) => {
353
- const currentGlobalState = this.getState();
354
- listener(action, currentGlobalState, oldGlobalState);
355
- });
356
- }
357
- if (this.pluginListeners[pluginId]) {
358
- this.pluginListeners[pluginId].forEach((listener) => {
359
- const currentPluginState = this.getState().plugins[pluginId];
360
- listener(action, currentPluginState, oldPluginState);
361
- });
362
- }
363
- return this.getState().plugins[pluginId];
364
- }
365
- /**
366
- * Dispatches an action to update the state using:
367
- * - the core reducer (if it's a CoreAction)
368
- * - *all* plugin reducers (regardless of action type), with no global notify for each plugin
369
- *
370
- * Returns the new *global* store state after all reducers have processed the action.
371
- *
372
- * @param action The action to dispatch (can be CoreAction or any Action).
373
- */
374
- dispatch(action) {
375
- if (this.isDispatching) {
376
- throw new Error(
377
- "Reducers may not dispatch actions. To trigger cascading actions, dispatch from a listener callback instead."
378
- );
379
- }
380
- const oldState = this.getState();
381
- try {
382
- this.isDispatching = true;
383
- if (this.isCoreAction(action)) {
384
- this.state.core = this.coreReducer(this.state.core, action);
385
- }
386
- for (const pluginId in this.pluginReducers) {
387
- const reducer = this.pluginReducers[pluginId];
388
- const oldPluginState = oldState.plugins[pluginId];
389
- if (reducer) {
390
- this.state.plugins[pluginId] = reducer(oldPluginState, action);
391
- }
392
- }
393
- } finally {
394
- this.isDispatching = false;
395
- }
396
- this.listeners.forEach((listener) => {
397
- const currentState = this.getState();
398
- listener(action, currentState, oldState);
399
- });
400
- return this.getState();
401
- }
402
- /**
403
- * Returns a shallow copy of the current state.
404
- * @returns The current store state.
405
- */
406
- getState() {
407
- if (this.isDispatching) {
408
- throw new Error(
409
- "You may not call store.getState() while the reducer is executing. The reducer has already received the state as an argument. Pass it down from the top reducer instead of reading it from the store."
410
- );
411
- }
412
- return {
413
- core: { ...this.state.core },
414
- plugins: { ...this.state.plugins }
415
- };
416
- }
417
- /**
418
- * Subscribes a listener to *global* state changes.
419
- * The callback signature is now (action, newState, oldState).
420
- *
421
- * @param listener The callback to invoke on state changes
422
- * @returns A function to unsubscribe the listener
423
- */
424
- subscribe(listener) {
425
- if (this.isDispatching) {
426
- throw new Error(
427
- "You may not call store.subscribe() while the reducer is executing. If you would like to be notified after the store has been updated, subscribe from a component and invoke store.getState() in the callback to access the latest state."
428
- );
429
- }
430
- this.listeners.push(listener);
431
- return () => {
432
- if (this.isDispatching) {
433
- throw new Error(
434
- "You may not unsubscribe from a store listener while the reducer is executing."
435
- );
436
- }
437
- this.listeners = this.listeners.filter((l) => l !== listener);
438
- };
439
- }
440
- /**
441
- * Subscribes a listener to *plugin-specific* state changes.
442
- * The callback signature is now (action, newPluginState, oldPluginState).
443
- *
444
- * @param pluginId The unique identifier for the plugin.
445
- * @param listener The callback to invoke on plugin state changes.
446
- * @returns A function to unsubscribe the listener.
447
- */
448
- subscribeToPlugin(pluginId, listener) {
449
- if (!(pluginId in this.state.plugins)) {
450
- throw new Error(
451
- `Plugin state not found for plugin "${pluginId}". Did you forget to call addPluginReducer?`
452
- );
453
- }
454
- if (this.isDispatching) {
455
- throw new Error("You may not call store.subscribeToPlugin() while the reducer is executing.");
456
- }
457
- if (!this.pluginListeners[pluginId]) {
458
- this.pluginListeners[pluginId] = [];
459
- }
460
- this.pluginListeners[pluginId].push(listener);
461
- return () => {
462
- if (this.isDispatching) {
463
- throw new Error(
464
- "You may not unsubscribe from a store listener while the reducer is executing."
465
- );
466
- }
467
- this.pluginListeners[pluginId] = this.pluginListeners[pluginId].filter((l) => l !== listener);
468
- if (this.pluginListeners[pluginId].length === 0) {
469
- delete this.pluginListeners[pluginId];
470
- }
471
- };
472
- }
473
- /**
474
- * Subscribes to a specific action type (only from the core's action union).
475
- * The callback signature is (action, newState, oldState).
476
- *
477
- * @param type The action type to listen for.
478
- * @param handler The callback to invoke when the action occurs.
479
- * @returns A function to unsubscribe the handler.
480
- */
481
- onAction(type, handler) {
482
- return this.subscribe((action, newState, oldState) => {
483
- if (action.type === type) {
484
- handler(action, newState, oldState);
485
- }
486
- });
487
- }
488
- /**
489
- * Gets a PluginStore handle for a specific plugin.
490
- * @param pluginId The unique identifier for the plugin.
491
- * @returns A PluginStore instance for the plugin.
492
- */
493
- getPluginStore(pluginId) {
494
- if (!(pluginId in this.state.plugins)) {
495
- throw new Error(
496
- `Plugin state not found for plugin "${pluginId}". Did you forget to call addPluginReducer?`
497
- );
498
- }
499
- return new PluginStore(this, pluginId);
500
- }
501
- /**
502
- * Helper method to check if an action is a CoreAction.
503
- * Adjust if you have a more refined way to differentiate CoreAction vs. any other Action.
504
- */
505
- isCoreAction(action) {
506
- return CORE_ACTION_TYPES.includes(action.type);
507
- }
508
- /**
509
- * Destroy the store: drop every listener and plugin reducer
510
- */
511
- destroy() {
512
- var _a, _b;
513
- this.listeners.length = 0;
514
- for (const id in this.pluginListeners) {
515
- (_b = (_a = this.pluginListeners[id]) == null ? void 0 : _a.splice) == null ? void 0 : _b.call(_a, 0);
516
- }
517
- this.pluginListeners = {};
518
- this.pluginReducers = {};
519
- this.state.plugins = {};
520
- this.state.core = { ...this.initialCoreState };
521
- }
522
- }
523
- const initialCoreState = (config) => ({
524
- documents: {},
525
- documentOrder: [],
526
- activeDocumentId: null,
527
- defaultScale: (config == null ? void 0 : config.defaultScale) ?? 1,
528
- defaultRotation: (config == null ? void 0 : config.defaultRotation) ?? Rotation.Degree0,
529
- globalPermissions: config == null ? void 0 : config.permissions
274
+ const pendingToDocInfo = (meta) => ({
275
+ id: meta.id,
276
+ name: meta.name,
277
+ status: meta.status,
278
+ pageCount: 0,
279
+ passwordProvided: meta.passwordProvided
530
280
  });
531
- const PERMISSION_NAME_TO_FLAG = {
532
- print: PdfPermissionFlag.Print,
533
- modifyContents: PdfPermissionFlag.ModifyContents,
534
- copyContents: PdfPermissionFlag.CopyContents,
535
- modifyAnnotations: PdfPermissionFlag.ModifyAnnotations,
536
- fillForms: PdfPermissionFlag.FillForms,
537
- extractForAccessibility: PdfPermissionFlag.ExtractForAccessibility,
538
- assembleDocument: PdfPermissionFlag.AssembleDocument,
539
- printHighQuality: PdfPermissionFlag.PrintHighQuality
540
- };
541
- const ALL_PERMISSION_FLAGS = [
542
- PdfPermissionFlag.Print,
543
- PdfPermissionFlag.ModifyContents,
544
- PdfPermissionFlag.CopyContents,
545
- PdfPermissionFlag.ModifyAnnotations,
546
- PdfPermissionFlag.FillForms,
547
- PdfPermissionFlag.ExtractForAccessibility,
548
- PdfPermissionFlag.AssembleDocument,
549
- PdfPermissionFlag.PrintHighQuality
550
- ];
551
- const ALL_PERMISSION_NAMES = [
552
- "print",
553
- "modifyContents",
554
- "copyContents",
555
- "modifyAnnotations",
556
- "fillForms",
557
- "extractForAccessibility",
558
- "assembleDocument",
559
- "printHighQuality"
560
- ];
561
- const PERMISSION_FLAG_TO_NAME = {
562
- [PdfPermissionFlag.Print]: "print",
563
- [PdfPermissionFlag.ModifyContents]: "modifyContents",
564
- [PdfPermissionFlag.CopyContents]: "copyContents",
565
- [PdfPermissionFlag.ModifyAnnotations]: "modifyAnnotations",
566
- [PdfPermissionFlag.FillForms]: "fillForms",
567
- [PdfPermissionFlag.ExtractForAccessibility]: "extractForAccessibility",
568
- [PdfPermissionFlag.AssembleDocument]: "assembleDocument",
569
- [PdfPermissionFlag.PrintHighQuality]: "printHighQuality"
570
- };
571
- function getPermissionOverride(overrides, flag) {
572
- if (!overrides) return void 0;
573
- if (flag in overrides) {
574
- return overrides[flag];
575
- }
576
- const name = PERMISSION_FLAG_TO_NAME[flag];
577
- if (name && name in overrides) {
578
- return overrides[name];
579
- }
580
- return void 0;
581
- }
582
- const getActiveDocumentState = (state) => {
583
- if (!state.activeDocumentId) return null;
584
- return state.documents[state.activeDocumentId] ?? null;
585
- };
586
- const getDocumentState = (state, documentId) => {
587
- return state.documents[documentId] ?? null;
588
- };
589
- const getDocumentIds = (state) => {
590
- return Object.keys(state.documents);
591
- };
592
- const isDocumentLoaded = (state, documentId) => {
593
- return !!state.documents[documentId];
594
- };
595
- const getDocumentCount = (state) => {
596
- return Object.keys(state.documents).length;
597
- };
598
- function getEffectivePermission(state, documentId, flag) {
599
- var _a;
600
- const docState = state.documents[documentId];
601
- const docConfig = docState == null ? void 0 : docState.permissions;
602
- const globalConfig = state.globalPermissions;
603
- const pdfPermissions = ((_a = docState == null ? void 0 : docState.document) == null ? void 0 : _a.permissions) ?? PdfPermissionFlag.AllowAll;
604
- const docOverride = getPermissionOverride(docConfig == null ? void 0 : docConfig.overrides, flag);
605
- if (docOverride !== void 0) {
606
- return docOverride;
607
- }
608
- const globalOverride = getPermissionOverride(globalConfig == null ? void 0 : globalConfig.overrides, flag);
609
- if (globalOverride !== void 0) {
610
- return globalOverride;
611
- }
612
- const enforce = (docConfig == null ? void 0 : docConfig.enforceDocumentPermissions) ?? (globalConfig == null ? void 0 : globalConfig.enforceDocumentPermissions) ?? true;
613
- if (!enforce) return true;
614
- return (pdfPermissions & flag) !== 0;
615
- }
616
- function getEffectivePermissions(state, documentId) {
617
- return ALL_PERMISSION_FLAGS.reduce((acc, flag) => {
618
- return getEffectivePermission(state, documentId, flag) ? acc | flag : acc;
619
- }, 0);
620
- }
621
- function calculateNextActiveDocument(state, closingDocumentId, explicitNext) {
622
- const currentActiveId = state.activeDocumentId;
623
- if (currentActiveId !== closingDocumentId) {
624
- return currentActiveId;
625
- }
626
- if (explicitNext !== void 0) {
627
- return explicitNext && state.documents[explicitNext] ? explicitNext : null;
628
- }
629
- const closingIndex = state.documentOrder.indexOf(closingDocumentId);
630
- if (closingIndex === -1) {
631
- return null;
632
- }
633
- if (closingIndex > 0) {
634
- return state.documentOrder[closingIndex - 1];
635
- }
636
- if (closingIndex < state.documentOrder.length - 1) {
637
- return state.documentOrder[closingIndex + 1];
638
- }
639
- return null;
640
- }
641
- function moveDocumentInOrder(currentOrder, documentId, toIndex) {
642
- const fromIndex = currentOrder.indexOf(documentId);
643
- if (fromIndex === -1) return null;
644
- if (toIndex < 0 || toIndex >= currentOrder.length) return null;
645
- if (fromIndex === toIndex) return null;
646
- const newOrder = [...currentOrder];
647
- newOrder.splice(fromIndex, 1);
648
- newOrder.splice(toIndex, 0, documentId);
649
- return newOrder;
650
- }
651
- const coreReducer = (state, action) => {
652
- switch (action.type) {
653
- case START_LOADING_DOCUMENT: {
654
- const {
655
- documentId,
656
- name,
657
- scale,
658
- rotation,
659
- passwordProvided,
660
- autoActivate = true,
661
- permissions
662
- } = action.payload;
663
- const newDocState = {
664
- id: documentId,
665
- name,
666
- status: "loading",
667
- loadingProgress: 0,
668
- error: null,
669
- document: null,
670
- scale: scale ?? state.defaultScale,
671
- rotation: rotation ?? state.defaultRotation,
672
- passwordProvided: passwordProvided ?? false,
673
- pageRefreshVersions: {},
674
- permissions,
675
- loadStartedAt: Date.now()
676
- };
677
- return {
678
- ...state,
679
- documents: {
680
- ...state.documents,
681
- [documentId]: newDocState
682
- },
683
- documentOrder: [...state.documentOrder, documentId],
684
- // Only activate if autoActivate is true (default), or if no document is currently active
685
- activeDocumentId: autoActivate || !state.activeDocumentId ? documentId : state.activeDocumentId
686
- };
687
- }
688
- case UPDATE_DOCUMENT_LOADING_PROGRESS: {
689
- const { documentId, progress } = action.payload;
690
- const docState = state.documents[documentId];
691
- if (!docState || docState.status !== "loading") return state;
692
- return {
693
- ...state,
694
- documents: {
695
- ...state.documents,
696
- [documentId]: {
697
- ...docState,
698
- loadingProgress: progress
699
- }
700
- }
701
- };
702
- }
703
- case SET_DOCUMENT_LOADED: {
704
- const { documentId, document } = action.payload;
705
- const docState = state.documents[documentId];
706
- if (!docState) return state;
707
- return {
708
- ...state,
709
- documents: {
710
- ...state.documents,
711
- [documentId]: {
712
- ...docState,
713
- status: "loaded",
714
- document,
715
- error: null,
716
- errorCode: void 0,
717
- errorDetails: void 0,
718
- passwordProvided: void 0,
719
- loadedAt: Date.now()
720
- }
721
- }
722
- };
723
- }
724
- case SET_DOCUMENT_ERROR: {
725
- const { documentId, error, errorCode, errorDetails } = action.payload;
726
- const docState = state.documents[documentId];
727
- if (!docState) return state;
728
- return {
729
- ...state,
730
- documents: {
731
- ...state.documents,
732
- [documentId]: {
733
- ...docState,
734
- status: "error",
735
- error,
736
- errorCode,
737
- errorDetails
738
- }
739
- }
740
- };
741
- }
742
- case RETRY_LOADING_DOCUMENT: {
743
- const { documentId, passwordProvided } = action.payload;
744
- const docState = state.documents[documentId];
745
- if (!docState || docState.status !== "error") return state;
746
- return {
747
- ...state,
748
- documents: {
749
- ...state.documents,
750
- [documentId]: {
751
- ...docState,
752
- status: "loading",
753
- loadingProgress: 0,
754
- error: null,
755
- errorCode: void 0,
756
- errorDetails: void 0,
757
- passwordProvided: passwordProvided ?? false,
758
- loadStartedAt: Date.now()
759
- }
760
- }
761
- };
762
- }
763
- case CLOSE_DOCUMENT: {
764
- const { documentId, nextActiveDocumentId } = action.payload;
765
- const { [documentId]: removed, ...remainingDocs } = state.documents;
766
- return {
767
- ...state,
768
- documents: remainingDocs,
769
- documentOrder: state.documentOrder.filter((id) => id !== documentId),
770
- activeDocumentId: calculateNextActiveDocument(state, documentId, nextActiveDocumentId)
771
- };
772
- }
773
- case MOVE_DOCUMENT: {
774
- const { documentId, toIndex } = action.payload;
775
- const newOrder = moveDocumentInOrder(state.documentOrder, documentId, toIndex);
776
- if (!newOrder) return state;
777
- return {
778
- ...state,
779
- documentOrder: newOrder
780
- };
781
- }
782
- case REORDER_DOCUMENTS: {
783
- return {
784
- ...state,
785
- documentOrder: action.payload
786
- };
787
- }
788
- case SET_ACTIVE_DOCUMENT: {
789
- return {
790
- ...state,
791
- activeDocumentId: action.payload
792
- };
793
- }
794
- case SET_SCALE: {
795
- const { scale, documentId } = action.payload;
796
- const targetId = documentId ?? state.activeDocumentId;
797
- if (!targetId) return state;
798
- const docState = state.documents[targetId];
799
- if (!docState) return state;
800
- return {
801
- ...state,
802
- documents: {
803
- ...state.documents,
804
- [targetId]: {
805
- ...docState,
806
- scale
807
- }
808
- }
809
- };
810
- }
811
- case SET_ROTATION: {
812
- const { rotation, documentId } = action.payload;
813
- const targetId = documentId ?? state.activeDocumentId;
814
- if (!targetId) return state;
815
- const docState = state.documents[targetId];
816
- if (!docState) return state;
817
- return {
818
- ...state,
819
- documents: {
820
- ...state.documents,
821
- [targetId]: {
822
- ...docState,
823
- rotation
824
- }
825
- }
826
- };
827
- }
828
- case REFRESH_PAGES: {
829
- const { documentId, pageIndexes } = action.payload;
830
- const docState = state.documents[documentId];
831
- if (!docState) return state;
832
- const newVersions = { ...docState.pageRefreshVersions };
833
- for (const pageIndex of pageIndexes) {
834
- newVersions[pageIndex] = (newVersions[pageIndex] || 0) + 1;
835
- }
836
- return {
837
- ...state,
838
- documents: {
839
- ...state.documents,
840
- [documentId]: {
841
- ...docState,
842
- pageRefreshVersions: newVersions
843
- }
844
- }
845
- };
846
- }
847
- case UPDATE_DOCUMENT_SECURITY: {
848
- const { documentId, permissions, isOwnerUnlocked } = action.payload;
849
- const docState = state.documents[documentId];
850
- if (!(docState == null ? void 0 : docState.document)) return state;
851
- return {
852
- ...state,
853
- documents: {
854
- ...state.documents,
855
- [documentId]: {
856
- ...docState,
857
- document: {
858
- ...docState.document,
859
- permissions,
860
- isOwnerUnlocked
861
- }
862
- }
863
- }
864
- };
865
- }
866
- default:
867
- return state;
868
- }
869
- };
870
- class PluginRegistry {
871
- constructor(engine, config) {
872
- this.plugins = /* @__PURE__ */ new Map();
873
- this.manifests = /* @__PURE__ */ new Map();
874
- this.capabilities = /* @__PURE__ */ new Map();
875
- this.status = /* @__PURE__ */ new Map();
876
- this.configurations = /* @__PURE__ */ new Map();
877
- this.initPromise = null;
878
- this.pendingRegistrations = [];
879
- this.processingRegistrations = [];
880
- this.initialized = false;
881
- this.isInitializing = false;
882
- this.pluginsReadyPromise = null;
883
- this.destroyed = false;
884
- this.resolver = new DependencyResolver();
885
- this.engine = engine;
886
- this.initialCoreState = initialCoreState(config);
887
- this.store = new Store(coreReducer, this.initialCoreState);
888
- this.logger = (config == null ? void 0 : config.logger) ?? new NoopLogger();
889
- }
890
- /**
891
- * Get the logger instance
892
- */
893
- getLogger() {
894
- return this.logger;
895
- }
896
- /**
897
- * Register a plugin without initializing it
898
- */
899
- registerPlugin(pluginPackage, config) {
900
- if (this.initialized && !this.isInitializing) {
901
- throw new PluginRegistrationError("Cannot register plugins after initialization");
902
- }
903
- this.validateManifest(pluginPackage.manifest);
904
- this.store.addPluginReducer(
905
- pluginPackage.manifest.id,
906
- // We need one type assertion here since we can't fully reconcile TAction with Action
907
- // due to TypeScript's type system limitations with generic variance
908
- pluginPackage.reducer,
909
- "function" === typeof pluginPackage.initialState ? pluginPackage.initialState(
910
- this.initialCoreState,
911
- {
912
- ...pluginPackage.manifest.defaultConfig,
913
- ...config
914
- }
915
- ) : pluginPackage.initialState
916
- );
917
- this.pendingRegistrations.push({
918
- package: pluginPackage,
919
- config
920
- });
921
- }
922
- /**
923
- * Get the central store instance
924
- */
925
- getStore() {
926
- return this.store;
927
- }
928
- /**
929
- * Get the engine instance
930
- */
931
- getEngine() {
932
- return this.engine;
933
- }
934
- /**
935
- * Get a promise that resolves when all plugins are ready
936
- */
937
- pluginsReady() {
938
- if (this.pluginsReadyPromise) {
939
- return this.pluginsReadyPromise;
940
- }
941
- this.pluginsReadyPromise = (async () => {
942
- if (!this.initialized) {
943
- await this.initialize();
944
- }
945
- const readyPromises = Array.from(this.plugins.values()).map(
946
- (p) => typeof p.ready === "function" ? p.ready() : Promise.resolve()
947
- );
948
- await Promise.all(readyPromises);
949
- })();
950
- return this.pluginsReadyPromise;
951
- }
952
- /**
953
- * INITIALISE THE REGISTRY – runs once no-matter-how-many calls *
954
- */
955
- async initialize() {
956
- if (this.destroyed) {
957
- throw new PluginRegistrationError("Registry has been destroyed");
958
- }
959
- if (this.initPromise) {
960
- return this.initPromise;
961
- }
962
- this.initPromise = (async () => {
963
- if (this.initialized) {
964
- throw new PluginRegistrationError("Registry is already initialized");
965
- }
966
- this.isInitializing = true;
967
- try {
968
- if (this.destroyed) return;
969
- while (this.pendingRegistrations.length > 0) {
970
- if (this.destroyed) return;
971
- this.processingRegistrations = [...this.pendingRegistrations];
972
- this.pendingRegistrations = [];
973
- for (const reg of this.processingRegistrations) {
974
- const dependsOn = /* @__PURE__ */ new Set();
975
- const allDeps = [...reg.package.manifest.requires, ...reg.package.manifest.optional];
976
- for (const cap of allDeps) {
977
- const provider = this.processingRegistrations.find(
978
- (r) => r.package.manifest.provides.includes(cap)
979
- );
980
- if (provider) {
981
- dependsOn.add(provider.package.manifest.id);
982
- }
983
- }
984
- this.resolver.addNode(reg.package.manifest.id, [...dependsOn]);
985
- }
986
- const loadOrder = this.resolver.resolveLoadOrder();
987
- for (const id of loadOrder) {
988
- const reg = this.processingRegistrations.find((r) => r.package.manifest.id === id);
989
- this.instantiatePlugin(reg.package.manifest, reg.package.create, reg.config);
990
- }
991
- for (const id of loadOrder) {
992
- await this.runPluginInitialization(id);
993
- }
994
- this.processingRegistrations = [];
995
- this.resolver = new DependencyResolver();
996
- }
997
- this.initialized = true;
998
- } catch (err) {
999
- if (err instanceof Error) {
1000
- throw new CircularDependencyError(
1001
- `Failed to resolve plugin dependencies: ${err.message}`
1002
- );
1003
- }
1004
- throw err;
1005
- } finally {
1006
- this.isInitializing = false;
1007
- }
1008
- })();
1009
- return this.initPromise;
1010
- }
1011
- /**
1012
- * Phase 2: Create instance and register capabilities
1013
- */
1014
- instantiatePlugin(manifest, packageCreator, config) {
1015
- const finalConfig = {
1016
- ...manifest.defaultConfig,
1017
- ...config
1018
- };
1019
- this.validateConfig(manifest.id, finalConfig, manifest.defaultConfig);
1020
- const plugin = packageCreator(this, finalConfig);
1021
- this.validatePlugin(plugin);
1022
- for (const capability of manifest.provides) {
1023
- if (this.capabilities.has(capability)) {
1024
- throw new PluginRegistrationError(
1025
- `Capability ${capability} is already provided by plugin ${this.capabilities.get(capability)}`
1026
- );
1027
- }
1028
- this.capabilities.set(capability, manifest.id);
1029
- }
1030
- this.plugins.set(manifest.id, plugin);
1031
- this.manifests.set(manifest.id, manifest);
1032
- this.status.set(manifest.id, "registered");
1033
- this.configurations.set(manifest.id, finalConfig);
1034
- }
1035
- /**
1036
- * Phase 3: Run the initialize method
1037
- */
1038
- async runPluginInitialization(pluginId) {
1039
- const plugin = this.plugins.get(pluginId);
1040
- if (!plugin) return;
1041
- const manifest = this.manifests.get(pluginId);
1042
- const config = this.configurations.get(pluginId);
1043
- for (const capability of manifest.requires) {
1044
- if (!this.capabilities.has(capability)) {
1045
- throw new PluginRegistrationError(
1046
- `Missing required capability: ${capability} for plugin ${pluginId}`
1047
- );
1048
- }
1049
- }
1050
- this.logger.debug("PluginRegistry", "InitializePlugin", `Initializing plugin ${pluginId}`);
1051
- try {
1052
- if (plugin.initialize) {
1053
- await plugin.initialize(config);
1054
- }
1055
- this.status.set(pluginId, "active");
1056
- this.logger.info(
1057
- "PluginRegistry",
1058
- "PluginInitialized",
1059
- `Plugin ${pluginId} initialized successfully`
1060
- );
1061
- } catch (error) {
1062
- this.status.set(pluginId, "error");
1063
- this.logger.error(
1064
- "PluginRegistry",
1065
- "InitializationFailed",
1066
- `Plugin ${pluginId} initialization failed`,
1067
- { error }
1068
- );
1069
- throw error;
1070
- }
1071
- }
1072
- getPluginConfig(pluginId) {
1073
- const config = this.configurations.get(pluginId);
1074
- if (!config) {
1075
- throw new PluginNotFoundError(`Configuration for plugin ${pluginId} not found`);
1076
- }
1077
- return config;
1078
- }
1079
- validateConfig(pluginId, config, defaultConfig) {
1080
- const requiredKeys = Object.keys(defaultConfig);
1081
- const missingKeys = requiredKeys.filter((key) => !config.hasOwnProperty(key));
1082
- if (missingKeys.length > 0) {
1083
- throw new PluginConfigurationError(
1084
- `Missing required configuration keys for plugin ${pluginId}: ${missingKeys.join(", ")}`
1085
- );
1086
- }
1087
- }
1088
- async updatePluginConfig(pluginId, config) {
1089
- const plugin = this.getPlugin(pluginId);
1090
- if (!plugin) {
1091
- throw new PluginNotFoundError(`Plugin ${pluginId} not found`);
1092
- }
1093
- const manifest = this.manifests.get(pluginId);
1094
- const currentConfig = this.configurations.get(pluginId);
1095
- if (!manifest || !currentConfig) {
1096
- throw new PluginNotFoundError(`Plugin ${pluginId} not found`);
1097
- }
1098
- const newConfig = {
1099
- ...currentConfig,
1100
- ...config
1101
- };
1102
- this.validateConfig(pluginId, newConfig, manifest.defaultConfig);
1103
- this.configurations.set(pluginId, newConfig);
1104
- if (plugin.initialize) {
1105
- await plugin.initialize(newConfig);
1106
- }
1107
- }
1108
- /**
1109
- * Register multiple plugins at once
1110
- */
1111
- registerPluginBatch(registrations) {
1112
- for (const reg of registrations) {
1113
- this.registerPlugin(reg.package, reg.config);
1114
- }
1115
- }
1116
- /**
1117
- * Unregister a plugin
1118
- */
1119
- async unregisterPlugin(pluginId) {
1120
- const plugin = this.plugins.get(pluginId);
1121
- if (!plugin) {
1122
- throw new PluginNotFoundError(`Plugin ${pluginId} is not registered`);
1123
- }
1124
- const manifest = this.manifests.get(pluginId);
1125
- if (!manifest) {
1126
- throw new PluginNotFoundError(`Manifest for plugin ${pluginId} not found`);
1127
- }
1128
- for (const [otherId, otherManifest] of this.manifests.entries()) {
1129
- if (otherId === pluginId) continue;
1130
- const dependsOnThis = [...otherManifest.requires, ...otherManifest.optional].some(
1131
- (cap) => manifest.provides.includes(cap)
1132
- );
1133
- if (dependsOnThis) {
1134
- throw new PluginRegistrationError(
1135
- `Cannot unregister plugin ${pluginId}: plugin ${otherId} depends on it`
1136
- );
1137
- }
1138
- }
1139
- try {
1140
- if (plugin.destroy) {
1141
- await plugin.destroy();
1142
- }
1143
- for (const capability of manifest.provides) {
1144
- this.capabilities.delete(capability);
1145
- }
1146
- this.plugins.delete(pluginId);
1147
- this.manifests.delete(pluginId);
1148
- this.status.delete(pluginId);
1149
- } catch (error) {
1150
- if (error instanceof Error) {
1151
- throw new Error(`Failed to unregister plugin ${pluginId}: ${error.message}`);
1152
- }
1153
- throw error;
1154
- }
1155
- }
1156
- /**
1157
- * Get a plugin instance
1158
- * @param pluginId The ID of the plugin to get
1159
- * @returns The plugin instance or null if not found
1160
- */
1161
- getPlugin(pluginId) {
1162
- const plugin = this.plugins.get(pluginId);
1163
- if (!plugin) {
1164
- return null;
1165
- }
1166
- return plugin;
1167
- }
1168
- /**
1169
- * Get a plugin that provides a specific capability
1170
- * @param capability The capability to get a provider for
1171
- * @returns The plugin providing the capability or null if not found
1172
- */
1173
- getCapabilityProvider(capability) {
1174
- const pluginId = this.capabilities.get(capability);
1175
- if (!pluginId) {
1176
- return null;
1177
- }
1178
- return this.getPlugin(pluginId);
1179
- }
1180
- /**
1181
- * Check if a capability is available
1182
- */
1183
- hasCapability(capability) {
1184
- return this.capabilities.has(capability);
1185
- }
1186
- /**
1187
- * Get all registered plugins
1188
- */
1189
- getAllPlugins() {
1190
- return Array.from(this.plugins.values());
1191
- }
1192
- /**
1193
- * Get plugin status
1194
- */
1195
- getPluginStatus(pluginId) {
1196
- const status = this.status.get(pluginId);
1197
- if (!status) {
1198
- throw new PluginNotFoundError(`Plugin ${pluginId} not found`);
1199
- }
1200
- return status;
1201
- }
1202
- /**
1203
- * Validate plugin object
1204
- */
1205
- validatePlugin(plugin) {
1206
- if (!plugin.id) {
1207
- throw new PluginRegistrationError("Plugin must have an id");
1208
- }
1209
- }
1210
- /**
1211
- * Validate plugin manifest
1212
- */
1213
- validateManifest(manifest) {
1214
- if (!manifest.id) {
1215
- throw new PluginRegistrationError("Manifest must have an id");
1216
- }
1217
- if (!manifest.name) {
1218
- throw new PluginRegistrationError("Manifest must have a name");
1219
- }
1220
- if (!manifest.version) {
1221
- throw new PluginRegistrationError("Manifest must have a version");
1222
- }
1223
- if (!Array.isArray(manifest.provides)) {
1224
- throw new PluginRegistrationError("Manifest must have a provides array");
1225
- }
1226
- if (!Array.isArray(manifest.requires)) {
1227
- throw new PluginRegistrationError("Manifest must have a requires array");
1228
- }
1229
- if (!Array.isArray(manifest.optional)) {
1230
- throw new PluginRegistrationError("Manifest must have an optional array");
1231
- }
1232
- }
1233
- isDestroyed() {
1234
- return this.destroyed;
1235
- }
1236
- /**
1237
- * DESTROY EVERYTHING – waits for any ongoing initialise(), once *
1238
- */
1239
- async destroy() {
1240
- var _a;
1241
- if (this.destroyed) throw new PluginRegistrationError("Registry has already been destroyed");
1242
- this.destroyed = true;
1243
- try {
1244
- await this.initPromise;
1245
- } catch {
1246
- }
1247
- for (const plugin of Array.from(this.plugins.values()).reverse()) {
1248
- await ((_a = plugin.destroy) == null ? void 0 : _a.call(plugin));
1249
- }
1250
- this.store.destroy();
1251
- this.plugins.clear();
1252
- this.manifests.clear();
1253
- this.capabilities.clear();
1254
- this.status.clear();
1255
- this.pendingRegistrations.length = 0;
1256
- this.processingRegistrations.length = 0;
1257
- }
1258
- }
1259
- function createPluginRegistration(pluginPackage, config) {
1260
- return {
1261
- package: pluginPackage,
1262
- config
1263
- };
1264
- }
1265
- function hasAutoMountElements(pkg) {
1266
- return "autoMountElements" in pkg && typeof pkg.autoMountElements === "function";
1267
- }
1268
- class BasePlugin {
1269
- constructor(id, registry) {
1270
- this.id = id;
1271
- this.registry = registry;
1272
- this.cooldownActions = {};
1273
- this.debouncedTimeouts = {};
1274
- this.unsubscribeFromState = null;
1275
- this.unsubscribeFromCoreStore = null;
1276
- this.unsubscribeFromStartLoadingDocument = null;
1277
- this.unsubscribeFromSetDocumentLoaded = null;
1278
- this.unsubscribeFromCloseDocument = null;
1279
- this.unsubscribeFromSetScale = null;
1280
- this.unsubscribeFromSetRotation = null;
1281
- if (id !== this.constructor.id) {
1282
- throw new Error(
1283
- `Plugin ID mismatch: ${id} !== ${this.constructor.id}`
1284
- );
1285
- }
1286
- this.engine = this.registry.getEngine();
1287
- this.logger = this.registry.getLogger();
1288
- this.coreStore = this.registry.getStore();
1289
- this.pluginStore = this.coreStore.getPluginStore(this.id);
1290
- this.unsubscribeFromState = this.pluginStore.subscribeToState((action, newState, oldState) => {
1291
- this.onStoreUpdated(oldState, newState);
1292
- });
1293
- this.unsubscribeFromCoreStore = this.coreStore.subscribe((action, newState, oldState) => {
1294
- this.onCoreStoreUpdated(oldState, newState);
1295
- if (newState.core.activeDocumentId !== oldState.core.activeDocumentId) {
1296
- this.onActiveDocumentChanged(
1297
- oldState.core.activeDocumentId,
1298
- newState.core.activeDocumentId
1299
- );
1300
- }
1301
- });
1302
- this.unsubscribeFromStartLoadingDocument = this.coreStore.onAction(
1303
- START_LOADING_DOCUMENT,
1304
- (action) => {
1305
- this.onDocumentLoadingStarted(action.payload.documentId);
1306
- }
1307
- );
1308
- this.unsubscribeFromSetDocumentLoaded = this.coreStore.onAction(
1309
- SET_DOCUMENT_LOADED,
1310
- (action) => {
1311
- this.onDocumentLoaded(action.payload.documentId);
1312
- }
1313
- );
1314
- this.unsubscribeFromCloseDocument = this.coreStore.onAction(CLOSE_DOCUMENT, (action) => {
1315
- this.onDocumentClosed(action.payload.documentId);
1316
- });
1317
- this.unsubscribeFromSetScale = this.coreStore.onAction(SET_SCALE, (action, state) => {
1318
- const targetId = action.payload.documentId ?? state.core.activeDocumentId;
1319
- if (targetId) {
1320
- this.onScaleChanged(targetId, action.payload.scale);
1321
- }
1322
- });
1323
- this.unsubscribeFromSetRotation = this.coreStore.onAction(SET_ROTATION, (action, state) => {
1324
- const targetId = action.payload.documentId ?? state.core.activeDocumentId;
1325
- if (targetId) {
1326
- this.onRotationChanged(targetId, action.payload.rotation);
1327
- }
1328
- });
1329
- this.readyPromise = new Promise((resolve) => {
1330
- this.readyResolve = resolve;
1331
- });
1332
- this.readyResolve();
1333
- }
1334
- provides() {
1335
- if (!this._capability) {
1336
- const cap = this.buildCapability();
1337
- this._capability = Object.freeze(cap);
1338
- }
1339
- return this._capability;
1340
- }
1341
- /**
1342
- * Get a copy of the current state
1343
- */
1344
- get state() {
1345
- return this.pluginStore.getState();
1346
- }
1347
- /**
1348
- * Get a copy of the current core state
1349
- */
1350
- get coreState() {
1351
- return this.coreStore.getState();
1352
- }
1353
- /**
1354
- * @deprecated use `this.state` Get a copy of the current state
1355
- */
1356
- getState() {
1357
- return this.pluginStore.getState();
1358
- }
1359
- /**
1360
- * @deprecated use `this.coreState` Get a copy of the current core state
1361
- */
1362
- getCoreState() {
1363
- return this.coreStore.getState();
1364
- }
1365
- /**
1366
- * Core Dispatch
1367
- */
1368
- dispatchCoreAction(action) {
1369
- return this.coreStore.dispatchToCore(action);
1370
- }
1371
- /**
1372
- * Dispatch an action to all plugins
1373
- */
1374
- dispatchToAllPlugins(action) {
1375
- return this.coreStore.dispatch(action);
1376
- }
1377
- /**
1378
- * Dispatch an action
1379
- */
1380
- dispatch(action) {
1381
- return this.pluginStore.dispatch(action);
1382
- }
1383
- /**
1384
- * Dispatch an action with a cooldown to prevent rapid repeated calls
1385
- * This executes immediately if cooldown has expired, then blocks subsequent calls
1386
- * @param action The action to dispatch
1387
- * @param cooldownTime Time in ms for cooldown (default: 100ms)
1388
- * @returns boolean indicating whether the action was dispatched or blocked
1389
- */
1390
- cooldownDispatch(action, cooldownTime = 100) {
1391
- const now = Date.now();
1392
- const lastActionTime = this.cooldownActions[action.type] || 0;
1393
- if (now - lastActionTime >= cooldownTime) {
1394
- this.cooldownActions[action.type] = now;
1395
- this.dispatch(action);
1396
- return true;
1397
- }
1398
- return false;
1399
- }
1400
- /**
1401
- * Dispatch an action with true debouncing - waits for the delay after the last call
1402
- * Each new call resets the timer. Action only executes after no calls for the specified time.
1403
- * @param action The action to dispatch
1404
- * @param debounceTime Time in ms to wait after the last call
1405
- */
1406
- debouncedDispatch(action, debounceTime = 100) {
1407
- const actionKey = action.type;
1408
- if (this.debouncedTimeouts[actionKey]) {
1409
- clearTimeout(this.debouncedTimeouts[actionKey]);
1410
- }
1411
- this.debouncedTimeouts[actionKey] = setTimeout(() => {
1412
- this.dispatch(action);
1413
- delete this.debouncedTimeouts[actionKey];
1414
- }, debounceTime);
1415
- }
1416
- /**
1417
- * Cancel a pending debounced action
1418
- * @param actionType The action type to cancel
1419
- */
1420
- cancelDebouncedDispatch(actionType) {
1421
- if (this.debouncedTimeouts[actionType]) {
1422
- clearTimeout(this.debouncedTimeouts[actionType]);
1423
- delete this.debouncedTimeouts[actionType];
1424
- }
1425
- }
1426
- /**
1427
- * Subscribe to state changes
1428
- */
1429
- subscribe(listener) {
1430
- return this.pluginStore.subscribeToState(listener);
1431
- }
1432
- /**
1433
- * Subscribe to core store changes
1434
- */
1435
- subscribeToCoreStore(listener) {
1436
- return this.coreStore.subscribe(listener);
1437
- }
1438
- /**
1439
- * Called when the plugin store state is updated
1440
- * @param oldState Previous state
1441
- * @param newState New state
1442
- */
1443
- onStoreUpdated(oldState, newState) {
1444
- }
1445
- /**
1446
- * Called when the core store state is updated
1447
- * @param oldState Previous state
1448
- * @param newState New state
1449
- */
1450
- onCoreStoreUpdated(oldState, newState) {
1451
- }
1452
- /**
1453
- * Called when a document is opened
1454
- * Override to initialize per-document state
1455
- * @param documentId The ID of the document that was opened
1456
- */
1457
- onDocumentLoadingStarted(documentId) {
1458
- }
1459
- /**
1460
- * Called when a document is loaded
1461
- * @param documentId The ID of the document that is loaded
1462
- */
1463
- onDocumentLoaded(documentId) {
1464
- }
1465
- /**
1466
- * Called when a document is closed
1467
- * Override to cleanup per-document state
1468
- * @param documentId The ID of the document that was closed
1469
- */
1470
- onDocumentClosed(documentId) {
1471
- }
1472
- /**
1473
- * Called when the active document changes
1474
- * @param previousId The ID of the previous active document
1475
- * @param currentId The ID of the new active document
1476
- */
1477
- onActiveDocumentChanged(previousId, currentId) {
1478
- }
1479
- onScaleChanged(documentId, scale) {
1480
- }
1481
- onRotationChanged(documentId, rotation) {
1482
- }
1483
- /**
1484
- * Cleanup method to be called when plugin is being destroyed
1485
- */
1486
- destroy() {
1487
- Object.values(this.debouncedTimeouts).forEach((timeout) => {
1488
- clearTimeout(timeout);
1489
- });
1490
- this.debouncedTimeouts = {};
1491
- if (this.unsubscribeFromState) {
1492
- this.unsubscribeFromState();
1493
- this.unsubscribeFromState = null;
1494
- }
1495
- if (this.unsubscribeFromCoreStore) {
1496
- this.unsubscribeFromCoreStore();
1497
- this.unsubscribeFromCoreStore = null;
1498
- }
1499
- if (this.unsubscribeFromStartLoadingDocument) {
1500
- this.unsubscribeFromStartLoadingDocument();
1501
- this.unsubscribeFromStartLoadingDocument = null;
1502
- }
1503
- if (this.unsubscribeFromSetDocumentLoaded) {
1504
- this.unsubscribeFromSetDocumentLoaded();
1505
- this.unsubscribeFromSetDocumentLoaded = null;
1506
- }
1507
- if (this.unsubscribeFromCloseDocument) {
1508
- this.unsubscribeFromCloseDocument();
1509
- this.unsubscribeFromCloseDocument = null;
1510
- }
1511
- if (this.unsubscribeFromSetScale) {
1512
- this.unsubscribeFromSetScale();
1513
- this.unsubscribeFromSetScale = null;
1514
- }
1515
- if (this.unsubscribeFromSetRotation) {
1516
- this.unsubscribeFromSetRotation();
1517
- this.unsubscribeFromSetRotation = null;
1518
- }
1519
- }
1520
- /**
1521
- * Returns a promise that resolves when the plugin is ready
1522
- */
1523
- ready() {
1524
- return this.readyPromise;
1525
- }
1526
- /**
1527
- * Mark the plugin as ready
1528
- */
1529
- markReady() {
1530
- this.readyResolve();
1531
- }
1532
- /**
1533
- * Reset the ready state (useful for plugins that need to reinitialize)
1534
- */
1535
- resetReady() {
1536
- this.readyPromise = new Promise((resolve) => {
1537
- this.readyResolve = resolve;
1538
- });
1539
- }
1540
- /**
1541
- * Get the active document ID
1542
- * @throws Error if no active document exists
1543
- */
1544
- getActiveDocumentId() {
1545
- const id = this.coreState.core.activeDocumentId;
1546
- if (!id) {
1547
- throw new Error("No active document");
1548
- }
1549
- return id;
1550
- }
1551
- /**
1552
- * Get the active document ID or null if none exists
1553
- */
1554
- getActiveDocumentIdOrNull() {
1555
- return this.coreState.core.activeDocumentId;
1556
- }
1557
- /**
1558
- * Get core document state for a specific document
1559
- * @param documentId Document ID (optional, defaults to active document)
1560
- * @returns Document state or null if not found
1561
- */
1562
- getCoreDocument(documentId) {
1563
- const id = documentId ?? this.getActiveDocumentIdOrNull();
1564
- if (!id) return null;
1565
- return this.coreState.core.documents[id] ?? null;
1566
- }
1567
- /**
1568
- * Get core document state for a specific document
1569
- * @param documentId Document ID (optional, defaults to active document)
1570
- * @throws Error if document not found
1571
- */
1572
- getCoreDocumentOrThrow(documentId) {
1573
- const doc = this.getCoreDocument(documentId);
1574
- if (!doc) {
1575
- throw new Error(`Document not found: ${documentId ?? "active"}`);
1576
- }
1577
- return doc;
1578
- }
1579
- // ─────────────────────────────────────────────────────────
1580
- // Permission Helpers
1581
- // ─────────────────────────────────────────────────────────
1582
- /**
1583
- * Get the effective permission flags for a document.
1584
- * Applies layered resolution: per-document override → global override → PDF permission.
1585
- * Returns AllowAll if document not found.
1586
- * @param documentId Document ID (optional, defaults to active document)
1587
- */
1588
- getDocumentPermissions(documentId) {
1589
- const docId = documentId ?? this.coreState.core.activeDocumentId;
1590
- if (!docId) return PdfPermissionFlag.AllowAll;
1591
- return getEffectivePermissions(this.coreState.core, docId);
1592
- }
1593
- /**
1594
- * Check if a document has the required permissions (returns boolean).
1595
- * Applies layered resolution: per-document override → global override → PDF permission.
1596
- * Useful for conditional UI logic.
1597
- * @param documentId Document ID (optional, defaults to active document)
1598
- * @param flags Permission flags to check
1599
- */
1600
- checkPermission(documentId, ...flags) {
1601
- const docId = documentId ?? this.coreState.core.activeDocumentId;
1602
- if (!docId) return true;
1603
- return flags.every((flag) => getEffectivePermission(this.coreState.core, docId, flag));
1604
- }
1605
- /**
1606
- * Assert that a document has the required permissions.
1607
- * Applies layered resolution: per-document override → global override → PDF permission.
1608
- * Throws PermissionDeniedError if any flag is missing.
1609
- * @param documentId Document ID (optional, defaults to active document)
1610
- * @param flags Permission flags to require
1611
- */
1612
- requirePermission(documentId, ...flags) {
1613
- const docId = documentId ?? this.coreState.core.activeDocumentId;
1614
- if (!docId) return;
1615
- const missingFlags = [];
1616
- for (const flag of flags) {
1617
- if (!getEffectivePermission(this.coreState.core, docId, flag)) {
1618
- missingFlags.push(flag);
1619
- }
1620
- }
1621
- if (missingFlags.length > 0) {
1622
- const effectivePermissions = getEffectivePermissions(this.coreState.core, docId);
1623
- throw new PermissionDeniedError(missingFlags, effectivePermissions);
1624
- }
1625
- }
1626
- }
1627
- class EventControl {
1628
- constructor(handler, options) {
1629
- this.handler = handler;
1630
- this.options = options;
1631
- this.lastRun = 0;
1632
- this.handle = (data) => {
1633
- if (this.options.mode === "debounce") {
1634
- this.debounce(data);
1635
- } else {
1636
- this.throttle(data);
1637
- }
1638
- };
1639
- }
1640
- debounce(data) {
1641
- if (this.timeoutId) {
1642
- window.clearTimeout(this.timeoutId);
1643
- }
1644
- this.timeoutId = window.setTimeout(() => {
1645
- this.handler(data);
1646
- this.timeoutId = void 0;
1647
- }, this.options.wait);
1648
- }
1649
- throttle(data) {
1650
- if (this.options.mode === "debounce") return;
1651
- const now = Date.now();
1652
- const throttleMode = this.options.throttleMode || "leading-trailing";
1653
- if (now - this.lastRun >= this.options.wait) {
1654
- if (throttleMode === "leading-trailing") {
1655
- this.handler(data);
1656
- }
1657
- this.lastRun = now;
1658
- }
1659
- if (this.timeoutId) {
1660
- window.clearTimeout(this.timeoutId);
1661
- }
1662
- this.timeoutId = window.setTimeout(
1663
- () => {
1664
- this.handler(data);
1665
- this.lastRun = Date.now();
1666
- this.timeoutId = void 0;
1667
- },
1668
- this.options.wait - (now - this.lastRun)
1669
- );
1670
- }
1671
- destroy() {
1672
- if (this.timeoutId) {
1673
- window.clearTimeout(this.timeoutId);
1674
- }
1675
- }
1676
- }
1677
- class KeyedEventControl {
1678
- constructor(handler, options) {
1679
- this.handler = handler;
1680
- this.options = options;
1681
- this.controls = /* @__PURE__ */ new Map();
1682
- this.handle = (data) => {
1683
- const key = String(this.options.keyExtractor(data));
1684
- let control = this.controls.get(key);
1685
- if (!control) {
1686
- control = new EventControl(this.handler, this.baseOptions);
1687
- this.controls.set(key, control);
1688
- }
1689
- control.handle(data);
1690
- };
1691
- this.baseOptions = {
1692
- mode: options.mode,
1693
- wait: options.wait,
1694
- ...options.mode === "throttle" && "throttleMode" in options ? { throttleMode: options.throttleMode } : {}
1695
- };
1696
- }
1697
- destroy() {
1698
- for (const control of this.controls.values()) {
1699
- control.destroy();
1700
- }
1701
- this.controls.clear();
1702
- }
1703
- }
1704
- function isKeyedOptions(options) {
1705
- return "keyExtractor" in options;
1706
- }
1707
- function clamp(value, min, max) {
1708
- return value < min ? min : value > max ? max : value;
1709
- }
1710
- function arePropsEqual(a, b, visited) {
1711
- if (a === b) {
1712
- return true;
1713
- }
1714
- if (a == null || b == null) {
1715
- return a === b;
1716
- }
1717
- const aType = typeof a;
1718
- const bType = typeof b;
1719
- if (aType !== bType) return false;
1720
- if (aType === "object") {
1721
- if (!visited) visited = /* @__PURE__ */ new Set();
1722
- const pairId = getPairId(a, b);
1723
- if (visited.has(pairId)) {
1724
- return true;
1725
- }
1726
- visited.add(pairId);
1727
- const aIsArray = Array.isArray(a);
1728
- const bIsArray = Array.isArray(b);
1729
- if (aIsArray && bIsArray) {
1730
- return arraysEqualUnordered(a, b, visited);
1731
- } else if (!aIsArray && !bIsArray) {
1732
- return objectsEqual(a, b, visited);
1733
- } else {
1734
- return false;
1735
- }
1736
- }
1737
- return false;
1738
- }
1739
- function getPairId(a, b) {
1740
- return `${objectId(a)}__${objectId(b)}`;
1741
- }
1742
- let objectIdCounter = 0;
1743
- const objectIds = /* @__PURE__ */ new WeakMap();
1744
- function objectId(obj) {
1745
- if (!objectIds.has(obj)) {
1746
- objectIds.set(obj, ++objectIdCounter);
1747
- }
1748
- return objectIds.get(obj);
1749
- }
1750
- function arraysEqualUnordered(a, b, visited) {
1751
- if (a.length !== b.length) return false;
1752
- const used = new Array(b.length).fill(false);
1753
- outer: for (let i = 0; i < a.length; i++) {
1754
- const elemA = a[i];
1755
- for (let j = 0; j < b.length; j++) {
1756
- if (used[j]) continue;
1757
- if (arePropsEqual(elemA, b[j], visited)) {
1758
- used[j] = true;
1759
- continue outer;
1760
- }
1761
- }
1762
- return false;
1763
- }
1764
- return true;
1765
- }
1766
- function objectsEqual(a, b, visited) {
1767
- const aKeys = Object.keys(a).sort();
1768
- const bKeys = Object.keys(b).sort();
1769
- if (aKeys.length !== bKeys.length) return false;
1770
- for (let i = 0; i < aKeys.length; i++) {
1771
- if (aKeys[i] !== bKeys[i]) return false;
1772
- }
1773
- for (const key of aKeys) {
1774
- const valA = a[key];
1775
- const valB = b[key];
1776
- if (!arePropsEqual(valA, valB, visited)) {
1777
- return false;
1778
- }
1779
- }
1780
- return true;
1781
- }
1782
- function createEmitter() {
1783
- const listeners = /* @__PURE__ */ new Set();
1784
- const on = (l) => {
1785
- listeners.add(l);
1786
- return () => listeners.delete(l);
1787
- };
1788
- return {
1789
- emit: (v = void 0) => listeners.forEach((l) => l(v)),
1790
- on,
1791
- off: (l) => listeners.delete(l),
1792
- clear: () => listeners.clear()
1793
- };
1794
- }
1795
- function createBehaviorEmitter(initial, equality = arePropsEqual) {
1796
- const listeners = /* @__PURE__ */ new Set();
1797
- const proxyMap = /* @__PURE__ */ new Map();
1798
- let _value = initial;
1799
- const notify = (v) => listeners.forEach((l) => l(v));
1800
- const baseOn = (listener, options) => {
1801
- let realListener = listener;
1802
- let destroy = () => {
1803
- };
1804
- if (options) {
1805
- if (isKeyedOptions(options)) {
1806
- const ctl = new KeyedEventControl(listener, options);
1807
- realListener = ctl.handle;
1808
- destroy = () => ctl.destroy();
1809
- } else {
1810
- const ctl = new EventControl(listener, options);
1811
- realListener = ctl.handle;
1812
- destroy = () => ctl.destroy();
1813
- }
1814
- proxyMap.set(listener, { wrapped: realListener, destroy });
1815
- }
1816
- if (_value !== void 0) realListener(_value);
1817
- listeners.add(realListener);
1818
- return () => {
1819
- listeners.delete(realListener);
1820
- destroy();
1821
- proxyMap.delete(listener);
1822
- };
1823
- };
1824
- return {
1825
- /* emitter behaviour ---------------------------------------- */
1826
- get value() {
1827
- return _value;
1828
- },
1829
- emit(v = void 0) {
1830
- if (_value === void 0 || !equality(_value, v)) {
1831
- _value = v;
1832
- notify(v);
1833
- }
1834
- },
1835
- on: baseOn,
1836
- off(listener) {
1837
- const proxy = proxyMap.get(listener);
1838
- if (proxy) {
1839
- listeners.delete(proxy.wrapped);
1840
- proxy.destroy();
1841
- proxyMap.delete(listener);
1842
- } else {
1843
- listeners.delete(listener);
1844
- }
1845
- },
1846
- clear() {
1847
- listeners.clear();
1848
- proxyMap.forEach((p) => p.destroy());
1849
- proxyMap.clear();
1850
- },
1851
- /* derived hook --------------------------------------------- */
1852
- select(selector, eq = arePropsEqual) {
1853
- return (listener, options) => {
1854
- let prev;
1855
- if (_value !== void 0) {
1856
- const mapped = selector(_value);
1857
- prev = mapped;
1858
- listener(mapped);
1859
- }
1860
- return baseOn(
1861
- (next) => {
1862
- const mapped = selector(next);
1863
- if (prev === void 0 || !eq(prev, mapped)) {
1864
- prev = mapped;
1865
- listener(mapped);
1866
- }
1867
- },
1868
- options
1869
- );
1870
- };
1871
- }
1872
- };
1873
- }
1874
- function createScopedEmitter(toGlobalEvent, options) {
1875
- const shouldCache = (options == null ? void 0 : options.cache) ?? true;
1876
- const equality = (options == null ? void 0 : options.equality) ?? arePropsEqual;
1877
- const scopeCaches = /* @__PURE__ */ new Map();
1878
- const scopeListeners = /* @__PURE__ */ new Map();
1879
- const scopeProxyMaps = /* @__PURE__ */ new Map();
1880
- const globalListeners = /* @__PURE__ */ new Set();
1881
- const globalProxyMap = /* @__PURE__ */ new Map();
1882
- const normalizeKey = (key) => String(key);
1883
- const getOrCreateListeners = (key) => {
1884
- let listeners = scopeListeners.get(key);
1885
- if (!listeners) {
1886
- listeners = /* @__PURE__ */ new Set();
1887
- scopeListeners.set(key, listeners);
1888
- }
1889
- return listeners;
1890
- };
1891
- const getOrCreateProxyMap = (key) => {
1892
- let proxyMap = scopeProxyMaps.get(key);
1893
- if (!proxyMap) {
1894
- proxyMap = /* @__PURE__ */ new Map();
1895
- scopeProxyMaps.set(key, proxyMap);
1896
- }
1897
- return proxyMap;
1898
- };
1899
- const onGlobal = (listener, options2) => {
1900
- let realListener = listener;
1901
- let destroy = () => {
1902
- };
1903
- if (options2) {
1904
- if (isKeyedOptions(options2)) {
1905
- const ctl = new KeyedEventControl(listener, options2);
1906
- realListener = ctl.handle;
1907
- destroy = () => ctl.destroy();
1908
- } else {
1909
- const ctl = new EventControl(listener, options2);
1910
- realListener = ctl.handle;
1911
- destroy = () => ctl.destroy();
1912
- }
1913
- globalProxyMap.set(listener, { wrapped: realListener, destroy });
1914
- }
1915
- globalListeners.add(realListener);
1916
- return () => {
1917
- globalListeners.delete(realListener);
1918
- destroy();
1919
- globalProxyMap.delete(listener);
1920
- };
1921
- };
1922
- return {
1923
- emit(key, data) {
1924
- const normalizedKey = normalizeKey(key);
1925
- if (shouldCache) {
1926
- const cached = scopeCaches.get(normalizedKey);
1927
- if (cached !== void 0 && equality(cached, data)) {
1928
- return;
1929
- }
1930
- scopeCaches.set(normalizedKey, data);
1931
- }
1932
- const listeners = scopeListeners.get(normalizedKey);
1933
- if (listeners) {
1934
- listeners.forEach((l) => l(data));
1935
- }
1936
- const globalEvent = toGlobalEvent(key, data);
1937
- globalListeners.forEach((l) => l(globalEvent));
1938
- },
1939
- forScope(key) {
1940
- const normalizedKey = normalizeKey(key);
1941
- return (listener, options2) => {
1942
- const listeners = getOrCreateListeners(normalizedKey);
1943
- const proxyMap = getOrCreateProxyMap(normalizedKey);
1944
- let realListener = listener;
1945
- let destroy = () => {
1946
- };
1947
- if (options2) {
1948
- if (isKeyedOptions(options2)) {
1949
- const ctl = new KeyedEventControl(listener, options2);
1950
- realListener = ctl.handle;
1951
- destroy = () => ctl.destroy();
1952
- } else {
1953
- const ctl = new EventControl(listener, options2);
1954
- realListener = ctl.handle;
1955
- destroy = () => ctl.destroy();
1956
- }
1957
- proxyMap.set(listener, { wrapped: realListener, destroy });
1958
- }
1959
- if (shouldCache) {
1960
- const cached = scopeCaches.get(normalizedKey);
1961
- if (cached !== void 0) {
1962
- realListener(cached);
1963
- }
1964
- }
1965
- listeners.add(realListener);
1966
- return () => {
1967
- listeners.delete(realListener);
1968
- destroy();
1969
- proxyMap.delete(listener);
1970
- if (listeners.size === 0) {
1971
- scopeListeners.delete(normalizedKey);
1972
- }
1973
- if (proxyMap.size === 0) {
1974
- scopeProxyMaps.delete(normalizedKey);
1975
- }
1976
- };
1977
- };
1978
- },
1979
- onGlobal,
1980
- getValue(key) {
1981
- return shouldCache ? scopeCaches.get(normalizeKey(key)) : void 0;
1982
- },
1983
- getScopes() {
1984
- if (shouldCache) {
1985
- return Array.from(scopeCaches.keys());
1986
- }
1987
- return Array.from(scopeListeners.keys());
1988
- },
1989
- clearScope(key) {
1990
- const normalizedKey = normalizeKey(key);
1991
- if (shouldCache) {
1992
- scopeCaches.delete(normalizedKey);
1993
- }
1994
- const listeners = scopeListeners.get(normalizedKey);
1995
- if (listeners) {
1996
- listeners.clear();
1997
- scopeListeners.delete(normalizedKey);
1998
- }
1999
- const proxyMap = scopeProxyMaps.get(normalizedKey);
2000
- if (proxyMap) {
2001
- proxyMap.forEach((p) => p.destroy());
2002
- proxyMap.clear();
2003
- scopeProxyMaps.delete(normalizedKey);
2004
- }
2005
- },
2006
- clear() {
2007
- if (shouldCache) {
2008
- scopeCaches.clear();
2009
- }
2010
- scopeListeners.forEach((set) => set.clear());
2011
- scopeListeners.clear();
2012
- scopeProxyMaps.forEach((map) => {
2013
- map.forEach((p) => p.destroy());
2014
- map.clear();
2015
- });
2016
- scopeProxyMaps.clear();
2017
- globalListeners.clear();
2018
- globalProxyMap.forEach((p) => p.destroy());
2019
- globalProxyMap.clear();
2020
- }
2021
- };
2022
- }
2023
- function enumEntries(record) {
2024
- return Object.entries(record).map(([k, v]) => {
2025
- const maybeNum = Number(k);
2026
- const typedKey = Number.isFinite(maybeNum) && k.trim() !== "" ? maybeNum : k;
2027
- return [typedKey, v];
2028
- });
2029
- }
2030
- class PluginPackageBuilder {
2031
- constructor(basePackage) {
2032
- this.autoMountElements = [];
2033
- this.package = basePackage;
2034
- }
2035
- addUtility(component) {
2036
- this.autoMountElements.push({ component, type: "utility" });
2037
- return this;
2038
- }
2039
- addWrapper(component) {
2040
- this.autoMountElements.push({ component, type: "wrapper" });
2041
- return this;
2042
- }
2043
- build() {
2044
- return {
2045
- ...this.package,
2046
- autoMountElements: () => this.autoMountElements
2047
- };
2048
- }
2049
- }
2050
- function createPluginPackage(basePackage) {
2051
- return new PluginPackageBuilder(basePackage);
2052
- }
2053
- export {
2054
- ALL_PERMISSION_FLAGS,
2055
- ALL_PERMISSION_NAMES,
2056
- BasePlugin,
2057
- CLOSE_DOCUMENT,
2058
- CORE_ACTION_TYPES,
2059
- CapabilityConflictError,
2060
- CapabilityNotFoundError,
2061
- CircularDependencyError,
2062
- DependencyResolver,
2063
- EventControl,
2064
- KeyedEventControl,
2065
- MOVE_DOCUMENT,
2066
- PERMISSION_FLAG_TO_NAME,
2067
- PERMISSION_NAME_TO_FLAG,
2068
- PluginConfigurationError,
2069
- PluginInitializationError,
2070
- PluginNotFoundError,
2071
- PluginPackageBuilder,
2072
- PluginRegistrationError,
2073
- PluginRegistry,
2074
- REFRESH_DOCUMENT,
2075
- REFRESH_PAGES,
2076
- REORDER_DOCUMENTS,
2077
- RETRY_LOADING_DOCUMENT,
2078
- SET_ACTIVE_DOCUMENT,
2079
- SET_DEFAULT_ROTATION,
2080
- SET_DEFAULT_SCALE,
2081
- SET_DOCUMENT_ERROR,
2082
- SET_DOCUMENT_LOADED,
2083
- SET_PAGES,
2084
- SET_ROTATION,
2085
- SET_SCALE,
2086
- START_LOADING_DOCUMENT,
2087
- UPDATE_DOCUMENT_LOADING_PROGRESS,
2088
- UPDATE_DOCUMENT_SECURITY,
2089
- arePropsEqual,
2090
- clamp,
2091
- closeDocument,
2092
- createBehaviorEmitter,
2093
- createEmitter,
2094
- createPluginPackage,
2095
- createPluginRegistration,
2096
- createScopedEmitter,
2097
- enumEntries,
2098
- getActiveDocumentState,
2099
- getDocumentCount,
2100
- getDocumentIds,
2101
- getDocumentState,
2102
- getEffectivePermission,
2103
- getEffectivePermissions,
2104
- getPermissionOverride,
2105
- hasAutoMountElements,
2106
- initialCoreState,
2107
- isDocumentLoaded,
2108
- isKeyedOptions,
2109
- moveDocument,
2110
- refreshDocument,
2111
- refreshPages,
2112
- reorderDocuments,
2113
- retryLoadingDocument,
2114
- setActiveDocument,
2115
- setDefaultRotation,
2116
- setDefaultScale,
2117
- setDocumentError,
2118
- setDocumentLoaded,
2119
- setPages,
2120
- setRotation,
2121
- setScale,
2122
- startLoadingDocument,
2123
- updateDocumentLoadingProgress,
2124
- updateDocumentSecurity
2125
- };
2126
- //# sourceMappingURL=index.js.map
281
+ /** The stable id an input implies, if it carries one ('bytes'/'layerBytes'/'id'). */
282
+ const idOfInput = (input) => "id" in input && typeof input.id === "string" ? input.id : null;
283
+ const passwordOfInput = (input) => "password" in input ? input.password : void 0;
284
+ /** Engine rejections carry AbortError when close() aborts the live call;
285
+ * match structurally so test fakes with plain promises still work. */
286
+ const isAbortLike = (error) => error instanceof Error && error.name === "AbortError";
287
+ /**
288
+ * Assemble a kernel from an engine + plugins.
289
+ *
290
+ * planPlugins — validate dependencies, order them
291
+ * resolveCapability — workspace singletons, or per-document instances built lazily
292
+ * document lifecycle — one DocumentSession per document: transactional open
293
+ * (publish-last), one idempotent close for every phase
294
+ * start / destroy — explicit status machine; destroy closes everything
295
+ *
296
+ * The kernel closes every handle it opened; it never destroys the engine —
297
+ * ownership follows acquisition, and the engine was handed in by the caller.
298
+ */
299
+ function createKernel(opts) {
300
+ const { engine, plugins } = opts;
301
+ const report = opts.report ?? ((error) => console.error("[kernel]", error));
302
+ const store = createStore(report);
303
+ const plan = planPlugins(plugins);
304
+ const documentScopedPlugins = plan.ordered.filter(isDocumentScoped);
305
+ const workspaceCapabilities = /* @__PURE__ */ new Map();
306
+ const workspaceScope = createScope(report);
307
+ const sessions = /* @__PURE__ */ new Map();
308
+ let status = "created";
309
+ let startPromise = null;
310
+ let destroyPromise = null;
311
+ const guardUsable = (what) => {
312
+ if (status === "failed" || status === "destroying" || status === "destroyed") throw new Error(`[kernel] ${what} on a ${status} kernel`);
313
+ };
314
+ const checkpoint = (session) => {
315
+ if (session.cancel.signal.aborted) throw new CancelledError(`closed while opening: ${session.id}`);
316
+ };
317
+ /**
318
+ * Await an engine/network call under the session's cancellation:
319
+ * - the call is retained so close() can abort real worker-side work
320
+ * (`AbortablePromise`), and
321
+ * - the await RACES the cancellation, so close()'s join never blocks on a
322
+ * call that cannot be aborted (a plain-promise engine, a stuck fetch).
323
+ * When cancellation wins but the call later lands anyway, `onLateResult`
324
+ * routes the result into the session scope — whose late-defer rule runs it
325
+ * immediately after disposal — so a late-arriving resource cannot leak.
326
+ * (Plugin inits are deliberately NOT raced: they are first-party code that
327
+ * close() joins to completion; only unbounded external waits are raced.)
328
+ */
329
+ async function engineCall(session, call, onLateResult) {
330
+ const abortable = call;
331
+ session.engineOp = typeof abortable.abort === "function" ? abortable : null;
332
+ const signal = session.cancel.signal;
333
+ let onAbort;
334
+ const cancellation = new Promise((_, reject) => {
335
+ onAbort = () => reject(new CancelledError(`closed while opening: ${session.id}`));
336
+ if (signal.aborted) onAbort();
337
+ else signal.addEventListener("abort", onAbort, { once: true });
338
+ });
339
+ try {
340
+ return await Promise.race([call, cancellation]);
341
+ } catch (error) {
342
+ if (isCancelled(error)) call.then((value) => {
343
+ if (onLateResult) session.scope.defer(() => onLateResult(value));
344
+ }, () => {});
345
+ throw error;
346
+ } finally {
347
+ if (onAbort) signal.removeEventListener("abort", onAbort);
348
+ session.engineOp = null;
349
+ }
350
+ }
351
+ function createSession(id, name) {
352
+ const session = {
353
+ id,
354
+ name,
355
+ phase: "opening",
356
+ handle: null,
357
+ stagedMeta: null,
358
+ scope: createScope(report),
359
+ operation: null,
360
+ engineOp: null,
361
+ cancel: new AbortController(),
362
+ capabilities: /* @__PURE__ */ new Map(),
363
+ close: () => closeSession(session)
364
+ };
365
+ return session;
366
+ }
367
+ /** One transition at a time per session (open, then possibly unlock). */
368
+ function beginOperation(session, run) {
369
+ if (session.operation) throw new Error(`[documents] "${session.id}" already has an active transition`);
370
+ const operation = run().finally(() => {
371
+ if (session.operation === operation) session.operation = null;
372
+ });
373
+ session.operation = operation;
374
+ return operation;
375
+ }
376
+ /** Atomic ticket → real-id reconciliation: the sessions map, the store slot,
377
+ * order position, and activation all move together, or not at all. */
378
+ function rekeySession(session, nextId) {
379
+ const previousId = session.id;
380
+ if (previousId === nextId) return;
381
+ const core = store.getCore();
382
+ if (sessions.has(nextId) || core.documents[nextId] || core.pending[nextId]) throw new Error(`[documents] duplicate document id: ${nextId}`);
383
+ sessions.delete(previousId);
384
+ session.id = nextId;
385
+ sessions.set(nextId, session);
386
+ const slot = core.pending[previousId];
387
+ if (slot) {
388
+ const { [previousId]: _moved, ...pending } = core.pending;
389
+ store.setCore({
390
+ pending: {
391
+ ...pending,
392
+ [nextId]: {
393
+ ...slot,
394
+ id: nextId
395
+ }
396
+ },
397
+ order: core.order.map((id) => id === previousId ? nextId : id),
398
+ activeId: core.activeId === previousId ? nextId : core.activeId
399
+ }, { type: CORE_ORDER_CHANGED });
400
+ }
401
+ }
402
+ const closingSessions = /* @__PURE__ */ new WeakMap();
403
+ function closeSession(session) {
404
+ const inFlight = closingSessions.get(session);
405
+ if (inFlight) return inFlight;
406
+ const closing = (async () => {
407
+ session.phase = "closing";
408
+ unpublishSlot(session.id);
409
+ const reason = new CancelledError(`closed while opening: ${session.id}`);
410
+ session.cancel.abort(reason);
411
+ session.engineOp?.abort(reason);
412
+ await session.operation?.catch((error) => {
413
+ if (!isCancelled(error) && !isAbortLike(error)) report(error);
414
+ });
415
+ await session.scope.dispose();
416
+ if (sessions.get(session.id) === session) sessions.delete(session.id);
417
+ })();
418
+ closingSessions.set(session, closing);
419
+ return closing;
420
+ }
421
+ function nextActiveDocument(core, removedId) {
422
+ if (core.activeId !== removedId) return core.activeId;
423
+ const index = core.order.indexOf(removedId);
424
+ const remaining = core.order.filter((id) => id !== removedId);
425
+ return remaining.length === 0 ? null : remaining[Math.max(0, index - 1)] ?? remaining[0];
426
+ }
427
+ function publishPendingSlot(session, activate) {
428
+ const core = store.getCore();
429
+ store.setCore({
430
+ pending: {
431
+ ...core.pending,
432
+ [session.id]: {
433
+ id: session.id,
434
+ name: session.name,
435
+ status: "loading"
436
+ }
437
+ },
438
+ order: [...core.order, session.id],
439
+ activeId: activate || core.activeId === null ? session.id : core.activeId
440
+ }, { type: CORE_DOCUMENT_OPENING });
441
+ }
442
+ function publishLocked(session, passwordProvided) {
443
+ const core = store.getCore();
444
+ store.setCore({ pending: {
445
+ ...core.pending,
446
+ [session.id]: {
447
+ id: session.id,
448
+ name: session.name,
449
+ status: "locked",
450
+ passwordProvided
451
+ }
452
+ } }, { type: CORE_DOCUMENT_LOCKED });
453
+ }
454
+ function publishError(session, error) {
455
+ const core = store.getCore();
456
+ store.setCore({ pending: {
457
+ ...core.pending,
458
+ [session.id]: {
459
+ id: session.id,
460
+ name: session.name,
461
+ status: "error",
462
+ error
463
+ }
464
+ } }, { type: CORE_DOCUMENT_OPEN_FAILED });
465
+ }
466
+ function unpublishSlot(id) {
467
+ const core = store.getCore();
468
+ if (!core.pending[id] && !core.documents[id]) return;
469
+ const { [id]: _pending, ...pending } = core.pending;
470
+ const { [id]: _document, ...documents } = core.documents;
471
+ store.setCore({
472
+ pending,
473
+ documents,
474
+ order: core.order.filter((other) => other !== id),
475
+ activeId: nextActiveDocument(core, id)
476
+ }, { type: CORE_DOCUMENT_REMOVED });
477
+ }
478
+ /** The ONE ready transition: swap the pending slot for the staged meta and
479
+ * fire CORE_DOCUMENT_ADDED. Everything before this is unpublished and rolls
480
+ * back by disposing the session scope; nothing after this can fail. */
481
+ function commitReady(session) {
482
+ session.phase = "ready";
483
+ const meta = session.stagedMeta;
484
+ const core = store.getCore();
485
+ const { [session.id]: _resolved, ...pending } = core.pending;
486
+ store.setCore({
487
+ documents: {
488
+ ...core.documents,
489
+ [session.id]: meta
490
+ },
491
+ pending
492
+ }, { type: CORE_DOCUMENT_ADDED });
493
+ }
494
+ const sessionOf = (documentId) => {
495
+ const id = documentId ?? store.getCore().activeId;
496
+ return id ? sessions.get(id) ?? null : null;
497
+ };
498
+ /** The handle plugins may touch: bring-up or ready — never locked, never
499
+ * a handle whose close already won. */
500
+ const documentHandle = (documentId) => {
501
+ const session = sessionOf(documentId);
502
+ if (!session) return null;
503
+ return session.phase === "bringup" || session.phase === "ready" ? session.handle : null;
504
+ };
505
+ function buildDocumentCapability(plugin, session) {
506
+ let capability = session.capabilities.get(plugin);
507
+ if (!capability) {
508
+ capability = plugin.capability(createPluginContext(services, plugin, session));
509
+ session.capabilities.set(plugin, capability);
510
+ }
511
+ return capability;
512
+ }
513
+ function resolveCapability(token, documentId) {
514
+ guardUsable(`capability("${token.name}")`);
515
+ const workspaceCapability = workspaceCapabilities.get(token);
516
+ if (workspaceCapability) return workspaceCapability;
517
+ const provider = plan.providerOf(token);
518
+ if (!provider) throw new Error(`No capability "${token.name}".`);
519
+ const id = documentId ?? store.getCore().activeId;
520
+ if (!id) throw new Error(`Capability "${token.name}" requires an active document.`);
521
+ const session = sessions.get(id);
522
+ if (!session) throw new Error(`Capability "${token.name}" unavailable: no document "${id}".`);
523
+ if (session.phase === "bringup" || session.phase === "ready") return buildDocumentCapability(provider, session);
524
+ const shown = session.phase === "opening" ? "loading" : session.phase;
525
+ throw new Error(`Capability "${token.name}" unavailable: document "${id}" is ${shown}.`);
526
+ }
527
+ /** Internal total resolver: bring-up counts, so a plugin's `ctx.tryGet`
528
+ * works during its own document's init. */
529
+ function tryResolveInternal(token, documentId) {
530
+ if (status === "destroying" || status === "destroyed") return null;
531
+ const workspaceCapability = workspaceCapabilities.get(token);
532
+ if (workspaceCapability) return workspaceCapability;
533
+ const provider = plan.providerOf(token);
534
+ if (!provider) return null;
535
+ const session = sessionOf(documentId);
536
+ if (!session || session.phase !== "bringup" && session.phase !== "ready") return null;
537
+ return buildDocumentCapability(provider, session);
538
+ }
539
+ /** Public total resolver — see `Kernel.tryCapability`. `ready` only: the
540
+ * null→instance flip at commit time IS the adapters' re-render signal. */
541
+ function tryResolveCapability(token, documentId) {
542
+ if (status === "destroying" || status === "destroyed") return null;
543
+ const workspaceCapability = workspaceCapabilities.get(token);
544
+ if (workspaceCapability) return workspaceCapability;
545
+ const provider = plan.providerOf(token);
546
+ if (!provider) return null;
547
+ const session = sessionOf(documentId);
548
+ if (!session || session.phase !== "ready") return null;
549
+ return buildDocumentCapability(provider, session);
550
+ }
551
+ const services = {
552
+ engine,
553
+ store,
554
+ workspaceScope,
555
+ resolveCapability,
556
+ tryResolveCapability: tryResolveInternal,
557
+ documentHandle
558
+ };
559
+ let ticketCounter = 0;
560
+ const nextTicket = () => `pending:${++ticketCounter}`;
561
+ /** Slices + event subscription + plugin inits + effect SETUP — every step's
562
+ * release deferred into the session scope, every await followed by a
563
+ * checkpoint. Runs entirely pre-commit: a failure anywhere rolls the whole
564
+ * session back and the document was never `ready`. */
565
+ async function bringUp(session, snapshot) {
566
+ session.phase = "bringup";
567
+ let renderPolicy = CONTINUOUS_RENDER_POLICY$1;
568
+ try {
569
+ renderPolicy = await session.handle.render?.policy() ?? CONTINUOUS_RENDER_POLICY$1;
570
+ } catch {}
571
+ checkpoint(session);
572
+ session.stagedMeta = {
573
+ id: session.id,
574
+ name: session.name,
575
+ pageCount: snapshot.pageCount,
576
+ pages: snapshot.pages,
577
+ revision: 0,
578
+ renderPolicy
579
+ };
580
+ const unsubscribeEvents = session.handle.events.subscribe((event) => {
581
+ const layout = layoutFromEvent(event);
582
+ if (!layout) return;
583
+ const now = store.getCore();
584
+ const existing = now.documents[session.id];
585
+ if (!existing) return;
586
+ const updated = {
587
+ ...existing,
588
+ pageCount: layout.pageCount,
589
+ pages: layout.pages,
590
+ revision: existing.revision + 1
591
+ };
592
+ store.setCore({ documents: {
593
+ ...now.documents,
594
+ [session.id]: updated
595
+ } }, { type: CORE_DOCUMENT_PAGES_UPDATED });
596
+ });
597
+ session.scope.defer(unsubscribeEvents);
598
+ for (const plugin of documentScopedPlugins) {
599
+ const key = sliceKey(plugin.id, session.id);
600
+ store.registerSlice(key, reducerOf(plugin), initialStateOf(plugin));
601
+ session.scope.defer(() => store.removeSlice(key));
602
+ }
603
+ for (const plugin of documentScopedPlugins) {
604
+ await plugin.init?.(createPluginContext(services, plugin, session));
605
+ checkpoint(session);
606
+ }
607
+ for (const plugin of documentScopedPlugins) plugin.effects?.(createEffectContext(services, plugin, session));
608
+ checkpoint(session);
609
+ }
610
+ async function openDocument(input, options) {
611
+ guardUsable("documents.open()");
612
+ const { activate, name, ...engineOptions } = options ?? {};
613
+ const requestedId = typeof input === "function" ? nextTicket() : idOfInput(input) ?? nextTicket();
614
+ if (sessions.has(requestedId)) throw new Error(`[documents] document already open: ${requestedId}`);
615
+ const session = createSession(requestedId, name);
616
+ sessions.set(session.id, session);
617
+ publishPendingSlot(session, activate ?? true);
618
+ return beginOperation(session, async () => {
619
+ try {
620
+ const source = typeof input === "function" ? await engineCall(session, Promise.resolve(input(session.cancel.signal))) : input;
621
+ checkpoint(session);
622
+ const sourceId = idOfInput(source);
623
+ if (sourceId) rekeySession(session, sourceId);
624
+ const handle = await engineCall(session, engine.open(source, engineOptions), (late) => late.close());
625
+ session.handle = handle;
626
+ session.scope.defer(() => handle.close());
627
+ checkpoint(session);
628
+ if (handle.id !== session.id) rekeySession(session, handle.id);
629
+ if (handle.security?.passwordPrompt?.state === "required") {
630
+ const passwordProvided = "password" in engineOptions && engineOptions.password != null || passwordOfInput(source) != null;
631
+ session.phase = "locked";
632
+ publishLocked(session, passwordProvided);
633
+ return session.id;
634
+ }
635
+ const snapshot = await engineCall(session, handle.pages.list());
636
+ checkpoint(session);
637
+ await bringUp(session, snapshot);
638
+ commitReady(session);
639
+ return session.id;
640
+ } catch (error) {
641
+ if (isCancelled(error)) throw error;
642
+ if (session.phase === "closing" || isAbortLike(error)) throw new CancelledError(`closed while opening: ${session.id}`);
643
+ await session.scope.dispose();
644
+ session.phase = "error";
645
+ publishError(session, error);
646
+ throw error;
647
+ }
648
+ });
649
+ }
650
+ async function unlockDocument(id, input) {
651
+ guardUsable("documents.unlock()");
652
+ const session = sessions.get(id);
653
+ if (!session || session.phase !== "locked" || !session.handle) throw new Error(`[documents] document is not locked: ${id}`);
654
+ const handle = session.handle;
655
+ return beginOperation(session, async () => {
656
+ try {
657
+ await engineCall(session, handle.security.unlock({ password: input.password }));
658
+ } catch (error) {
659
+ if (session.phase === "closing" || isAbortLike(error)) throw new CancelledError(`closed while opening: ${session.id}`);
660
+ throw error;
661
+ }
662
+ checkpoint(session);
663
+ try {
664
+ const snapshot = await engineCall(session, handle.pages.list());
665
+ checkpoint(session);
666
+ await bringUp(session, snapshot);
667
+ commitReady(session);
668
+ } catch (error) {
669
+ if (isCancelled(error)) throw error;
670
+ if (session.phase === "closing" || isAbortLike(error)) throw new CancelledError(`closed while opening: ${session.id}`);
671
+ await session.scope.dispose();
672
+ session.phase = "error";
673
+ publishError(session, error);
674
+ throw error;
675
+ }
676
+ });
677
+ }
678
+ async function closeDocument(documentId) {
679
+ const session = sessions.get(documentId);
680
+ if (!session) return;
681
+ await session.close();
682
+ }
683
+ function reorder(next) {
684
+ store.setCore({ order: next }, { type: CORE_ORDER_CHANGED });
685
+ }
686
+ const documents = {
687
+ open: openDocument,
688
+ openAll: (docs) => {
689
+ guardUsable("documents.openAll()");
690
+ const activeIndex = Math.max(0, docs.findIndex((d) => d.active));
691
+ docs.forEach(({ source, active: _active, ...options }, index) => {
692
+ openDocument(source, {
693
+ ...options,
694
+ activate: index === activeIndex
695
+ }).catch(() => {});
696
+ });
697
+ },
698
+ unlock: unlockDocument,
699
+ close: closeDocument,
700
+ closeAll: async () => {
701
+ for (const id of [...store.getCore().order]) await closeDocument(id);
702
+ },
703
+ setActive: (id) => {
704
+ const core = store.getCore();
705
+ if (core.documents[id] || core.pending[id]) store.setCore({ activeId: id }, { type: CORE_ACTIVE_CHANGED });
706
+ },
707
+ activeId: () => store.getCore().activeId,
708
+ list: () => store.getCore().order.map((id) => {
709
+ const core = store.getCore();
710
+ const meta = core.documents[id];
711
+ return meta ? toDocInfo(meta) : pendingToDocInfo(core.pending[id]);
712
+ }),
713
+ get: (id) => {
714
+ const core = store.getCore();
715
+ const meta = core.documents[id];
716
+ if (meta) return toDocInfo(meta);
717
+ const pending = core.pending[id];
718
+ return pending ? pendingToDocInfo(pending) : null;
719
+ },
720
+ has: (id) => {
721
+ const core = store.getCore();
722
+ return core.documents[id] !== void 0 || core.pending[id] !== void 0;
723
+ },
724
+ count: () => store.getCore().order.length,
725
+ order: () => [...store.getCore().order],
726
+ move: (id, toIndex) => {
727
+ const core = store.getCore();
728
+ if (!core.documents[id] && !core.pending[id]) return;
729
+ const without = core.order.filter((x) => x !== id);
730
+ const clamped = Math.max(0, Math.min(toIndex, without.length));
731
+ without.splice(clamped, 0, id);
732
+ reorder(without);
733
+ },
734
+ swap: (a, b) => {
735
+ const core = store.getCore();
736
+ const indexA = core.order.indexOf(a);
737
+ const indexB = core.order.indexOf(b);
738
+ if (indexA < 0 || indexB < 0) return;
739
+ const next = [...core.order];
740
+ next[indexA] = b;
741
+ next[indexB] = a;
742
+ reorder(next);
743
+ },
744
+ download: (id, opts) => {
745
+ const handle = documentHandle(id);
746
+ if (!handle) return Promise.reject(/* @__PURE__ */ new Error("[documents] no document to download"));
747
+ return handle.download(opts);
748
+ },
749
+ downloadLayer: (id) => {
750
+ const handle = documentHandle(id);
751
+ if (!handle) return Promise.reject(/* @__PURE__ */ new Error("[documents] no document to download"));
752
+ if (!handle.downloadLayer) return Promise.reject(/* @__PURE__ */ new Error("[documents] this engine cannot export a layer (open with a layer on the local engine)"));
753
+ return handle.downloadLayer();
754
+ }
755
+ };
756
+ workspaceCapabilities.set(DocumentsToken, documents);
757
+ for (const plugin of plan.ordered) if (!isDocumentScoped(plugin)) store.registerSlice(plugin.id, reducerOf(plugin), initialStateOf(plugin));
758
+ for (const plugin of plan.ordered) if (!isDocumentScoped(plugin) && plugin.token && plugin.capability) workspaceCapabilities.set(plugin.token, plugin.capability(createPluginContext(services, plugin)));
759
+ return {
760
+ engine,
761
+ documents,
762
+ capability: resolveCapability,
763
+ tryCapability: tryResolveCapability,
764
+ scopeOf: plan.scopeOf,
765
+ subscribe: store.subscribe,
766
+ getState: store.getState,
767
+ status: () => status,
768
+ start: () => {
769
+ if (status === "destroying" || status === "destroyed" || status === "failed") return Promise.reject(/* @__PURE__ */ new Error(`[kernel] start() on a ${status} kernel`));
770
+ return startPromise ?? (startPromise = (async () => {
771
+ status = "starting";
772
+ try {
773
+ for (const plugin of plan.ordered) {
774
+ if (status !== "starting") return;
775
+ if (!isDocumentScoped(plugin)) await plugin.init?.(createPluginContext(services, plugin));
776
+ }
777
+ if (status !== "starting") return;
778
+ for (const plugin of plan.ordered) if (!isDocumentScoped(plugin)) plugin.effects?.(createEffectContext(services, plugin));
779
+ status = "started";
780
+ } catch (error) {
781
+ status = "failed";
782
+ await workspaceScope.dispose();
783
+ workspaceCapabilities.clear();
784
+ throw error;
785
+ }
786
+ })());
787
+ },
788
+ destroy: () => {
789
+ return destroyPromise ?? (destroyPromise = (async () => {
790
+ const wasFailed = status === "failed";
791
+ status = "destroying";
792
+ await startPromise?.catch((error) => {
793
+ if (!isCancelled(error) && !wasFailed) report(error);
794
+ });
795
+ await Promise.allSettled([...sessions.values()].map((session) => session.close()));
796
+ sessions.clear();
797
+ await workspaceScope.dispose();
798
+ workspaceCapabilities.clear();
799
+ store.destroy();
800
+ status = "destroyed";
801
+ })());
802
+ }
803
+ };
804
+ }
805
+ //#endregion
806
+ //#region src/index.ts
807
+ /** Create a typed capability token. `name` is for debugging only. */
808
+ function createCapabilityToken(name) {
809
+ return { name };
810
+ }
811
+ /** Identity helper that pins a plugin's generics. The real win is inference. */
812
+ function definePlugin(def) {
813
+ return def;
814
+ }
815
+ //#endregion
816
+ 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, createCapabilityToken, createKernel, definePlugin, docInfoEquals, docInfoListEquals, isCancelled, snapAppearanceScale, snapFullPageViewport, snapTileScale };
817
+
818
+ //# sourceMappingURL=index.js.map