@embedpdf/plugin-history 2.15.0 → 3.0.0-next.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +192 -21
  2. package/README.md +65 -0
  3. package/dist/contract-B7QL_Q3D.js +11 -0
  4. package/dist/contract-B7QL_Q3D.js.map +1 -0
  5. package/dist/contract-DHipVkAd.cjs +21 -0
  6. package/dist/contract-DHipVkAd.cjs.map +1 -0
  7. package/dist/contract-J_XQyhyq.d.cts +66 -0
  8. package/dist/contract-J_XQyhyq.d.ts +66 -0
  9. package/dist/contract.cjs +4 -0
  10. package/dist/contract.d.cts +2 -0
  11. package/dist/contract.d.ts +2 -0
  12. package/dist/contract.js +2 -0
  13. package/dist/index.cjs +356 -2
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +56 -0
  16. package/dist/index.d.ts +56 -1
  17. package/dist/index.js +348 -377
  18. package/dist/index.js.map +1 -1
  19. package/package.json +25 -57
  20. package/dist/lib/actions.d.ts +0 -34
  21. package/dist/lib/history-plugin.d.ts +0 -33
  22. package/dist/lib/index.d.ts +0 -8
  23. package/dist/lib/manifest.d.ts +0 -4
  24. package/dist/lib/reducer.d.ts +0 -5
  25. package/dist/lib/types.d.ts +0 -151
  26. package/dist/preact/adapter.d.ts +0 -1
  27. package/dist/preact/core.d.ts +0 -1
  28. package/dist/preact/index.cjs +0 -2
  29. package/dist/preact/index.cjs.map +0 -1
  30. package/dist/preact/index.d.ts +0 -1
  31. package/dist/preact/index.js +0 -10
  32. package/dist/preact/index.js.map +0 -1
  33. package/dist/react/adapter.d.ts +0 -1
  34. package/dist/react/core.d.ts +0 -1
  35. package/dist/react/index.cjs +0 -2
  36. package/dist/react/index.cjs.map +0 -1
  37. package/dist/react/index.d.ts +0 -1
  38. package/dist/react/index.js +0 -10
  39. package/dist/react/index.js.map +0 -1
  40. package/dist/shared/hooks/index.d.ts +0 -1
  41. package/dist/shared/hooks/use-history.d.ts +0 -11
  42. package/dist/shared/index.d.ts +0 -2
  43. package/dist/shared-preact/hooks/index.d.ts +0 -1
  44. package/dist/shared-preact/hooks/use-history.d.ts +0 -11
  45. package/dist/shared-preact/index.d.ts +0 -2
  46. package/dist/shared-react/hooks/index.d.ts +0 -1
  47. package/dist/shared-react/hooks/use-history.d.ts +0 -11
  48. package/dist/shared-react/index.d.ts +0 -2
  49. package/dist/svelte/hooks/index.d.ts +0 -1
  50. package/dist/svelte/hooks/use-history.d.ts +0 -11
  51. package/dist/svelte/index.cjs +0 -2
  52. package/dist/svelte/index.cjs.map +0 -1
  53. package/dist/svelte/index.d.ts +0 -2
  54. package/dist/svelte/index.js +0 -10
  55. package/dist/svelte/index.js.map +0 -1
  56. package/dist/vue/hooks/index.d.ts +0 -1
  57. package/dist/vue/hooks/use-history.d.ts +0 -3
  58. package/dist/vue/index.cjs +0 -2
  59. package/dist/vue/index.cjs.map +0 -1
  60. package/dist/vue/index.d.ts +0 -2
  61. package/dist/vue/index.js +0 -10
  62. package/dist/vue/index.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,381 +1,352 @@
1
- import { BasePlugin, createEmitter } from "@embedpdf/core";
2
- const HISTORY_PLUGIN_ID = "history";
3
- const manifest = {
4
- id: HISTORY_PLUGIN_ID,
5
- name: "History Plugin",
6
- version: "1.0.0",
7
- provides: ["history"],
8
- requires: [],
9
- optional: [],
10
- defaultConfig: {}
11
- };
12
- const INIT_HISTORY_STATE = "HISTORY/INIT_STATE";
13
- const CLEANUP_HISTORY_STATE = "HISTORY/CLEANUP_STATE";
14
- const SET_HISTORY_DOCUMENT_STATE = "HISTORY/SET_DOCUMENT_STATE";
15
- const SET_ACTIVE_HISTORY_DOCUMENT = "HISTORY/SET_ACTIVE_DOCUMENT";
16
- const initHistoryState = (documentId) => ({
17
- type: INIT_HISTORY_STATE,
18
- payload: { documentId }
19
- });
20
- const cleanupHistoryState = (documentId) => ({
21
- type: CLEANUP_HISTORY_STATE,
22
- payload: { documentId }
1
+ import { n as HistoryToken, t as HISTORY_DEFAULTS } from "./contract-B7QL_Q3D.js";
2
+ import { definePlugin, defineState } from "@embedpdf/core";
3
+ import { EngineError, EngineErrorCode } from "@embedpdf/engine-core/runtime";
4
+ //#region src/model.ts
5
+ const emptyHistory = () => ({
6
+ undo: [],
7
+ redo: [],
8
+ merging: null,
9
+ nextSeq: 0
23
10
  });
24
- const setHistoryDocumentState = (documentId, state) => ({
25
- type: SET_HISTORY_DOCUMENT_STATE,
26
- payload: { documentId, state }
27
- });
28
- const _HistoryPlugin = class _HistoryPlugin extends BasePlugin {
29
- constructor(id, registry) {
30
- super(id, registry);
31
- this.documentHistories = /* @__PURE__ */ new Map();
32
- this.historyChange$ = createEmitter();
33
- }
34
- async initialize(_) {
35
- this.logger.info("HistoryPlugin", "Initialize", "History plugin initialized");
36
- }
37
- // ─────────────────────────────────────────────────────────
38
- // Document Lifecycle (from BasePlugin)
39
- // ─────────────────────────────────────────────────────────
40
- onDocumentLoadingStarted(documentId) {
41
- this.dispatch(initHistoryState(documentId));
42
- this.documentHistories.set(documentId, {
43
- topicHistories: /* @__PURE__ */ new Map(),
44
- globalTimeline: [],
45
- globalIndex: -1
46
- });
47
- this.logger.debug(
48
- "HistoryPlugin",
49
- "DocumentOpened",
50
- `Initialized history state for document: ${documentId}`
51
- );
52
- }
53
- onDocumentClosed(documentId) {
54
- this.dispatch(cleanupHistoryState(documentId));
55
- this.documentHistories.delete(documentId);
56
- this.logger.debug(
57
- "HistoryPlugin",
58
- "DocumentClosed",
59
- `Cleaned up history state for document: ${documentId}`
60
- );
61
- }
62
- // ─────────────────────────────────────────────────────────
63
- // Helper Methods
64
- // ─────────────────────────────────────────────────────────
65
- getDocumentHistoryData(documentId) {
66
- const id = documentId ?? this.getActiveDocumentId();
67
- const data = this.documentHistories.get(id);
68
- if (!data) {
69
- throw new Error(`History data not found for document: ${id}`);
70
- }
71
- return data;
72
- }
73
- getDocumentHistoryState(documentId) {
74
- const data = this.documentHistories.get(documentId);
75
- if (!data) {
76
- throw new Error(`History data not found for document: ${documentId}`);
77
- }
78
- const topics = {};
79
- Array.from(data.topicHistories.entries()).forEach(([topic, history]) => {
80
- topics[topic] = {
81
- canUndo: history.currentIndex > -1,
82
- canRedo: history.currentIndex < history.commands.length - 1
83
- };
84
- });
85
- return {
86
- global: {
87
- canUndo: data.globalIndex > -1,
88
- canRedo: data.globalIndex < data.globalTimeline.length - 1
89
- },
90
- topics
91
- };
92
- }
93
- emitHistoryChange(documentId, topic) {
94
- const state = this.getDocumentHistoryState(documentId);
95
- this.dispatch(setHistoryDocumentState(documentId, state));
96
- this.historyChange$.emit({
97
- documentId,
98
- topic,
99
- state
100
- });
101
- }
102
- // ─────────────────────────────────────────────────────────
103
- // History Operations (per document)
104
- // ─────────────────────────────────────────────────────────
105
- register(command, topic, documentId) {
106
- const data = this.getDocumentHistoryData(documentId);
107
- if (!data.topicHistories.has(topic)) {
108
- data.topicHistories.set(topic, { commands: [], currentIndex: -1 });
109
- }
110
- const topicHistory = data.topicHistories.get(topic);
111
- topicHistory.commands.splice(topicHistory.currentIndex + 1);
112
- topicHistory.commands.push(command);
113
- topicHistory.currentIndex++;
114
- const historyEntry = { command, topic };
115
- data.globalTimeline.splice(data.globalIndex + 1);
116
- data.globalTimeline.push(historyEntry);
117
- data.globalIndex++;
118
- command.execute();
119
- this.emitHistoryChange(documentId, topic);
120
- }
121
- undo(topic, documentId) {
122
- const data = this.getDocumentHistoryData(documentId);
123
- let affectedTopic;
124
- if (topic) {
125
- const topicHistory = data.topicHistories.get(topic);
126
- if (topicHistory && topicHistory.currentIndex > -1) {
127
- topicHistory.commands[topicHistory.currentIndex].undo();
128
- topicHistory.currentIndex--;
129
- affectedTopic = topic;
130
- }
131
- } else {
132
- if (data.globalIndex > -1) {
133
- const entry = data.globalTimeline[data.globalIndex];
134
- entry.command.undo();
135
- data.topicHistories.get(entry.topic).currentIndex--;
136
- data.globalIndex--;
137
- affectedTopic = entry.topic;
138
- }
139
- }
140
- if (affectedTopic) {
141
- this.emitHistoryChange(documentId, affectedTopic);
142
- }
143
- }
144
- redo(topic, documentId) {
145
- const data = this.getDocumentHistoryData(documentId);
146
- let affectedTopic;
147
- if (topic) {
148
- const topicHistory = data.topicHistories.get(topic);
149
- if (topicHistory && topicHistory.currentIndex < topicHistory.commands.length - 1) {
150
- topicHistory.currentIndex++;
151
- topicHistory.commands[topicHistory.currentIndex].execute();
152
- affectedTopic = topic;
153
- }
154
- } else {
155
- if (data.globalIndex < data.globalTimeline.length - 1) {
156
- data.globalIndex++;
157
- const entry = data.globalTimeline[data.globalIndex];
158
- entry.command.execute();
159
- data.topicHistories.get(entry.topic).currentIndex++;
160
- affectedTopic = entry.topic;
161
- }
162
- }
163
- if (affectedTopic) {
164
- this.emitHistoryChange(documentId, affectedTopic);
165
- }
166
- }
167
- canUndo(topic, documentId) {
168
- const data = this.getDocumentHistoryData(documentId);
169
- if (topic) {
170
- const history = data.topicHistories.get(topic);
171
- return !!history && history.currentIndex > -1;
172
- }
173
- return data.globalIndex > -1;
174
- }
175
- canRedo(topic, documentId) {
176
- const data = this.getDocumentHistoryData(documentId);
177
- if (topic) {
178
- const history = data.topicHistories.get(topic);
179
- return !!history && history.currentIndex < history.commands.length - 1;
180
- }
181
- return data.globalIndex < data.globalTimeline.length - 1;
182
- }
183
- /**
184
- * Purges history entries that match the given predicate based on command metadata.
185
- * Used to remove commands that are no longer valid (e.g., after a permanent redaction commit).
186
- *
187
- * @param predicate A function that returns true for commands that should be purged
188
- * @param topic If provided, only purges entries for that specific topic
189
- * @param documentId The document to purge history for
190
- * @returns The number of entries that were purged
191
- */
192
- purgeByMetadata(predicate, topic, documentId) {
193
- const data = this.getDocumentHistoryData(documentId);
194
- let purgedCount = 0;
195
- const shouldPurge = (command) => {
196
- return predicate(command.metadata);
197
- };
198
- const topicsToProcess = topic ? [topic] : Array.from(data.topicHistories.keys());
199
- for (const topicName of topicsToProcess) {
200
- const topicHistory = data.topicHistories.get(topicName);
201
- if (!topicHistory) continue;
202
- const newCommands = [];
203
- let indexAdjustment = 0;
204
- for (let i = 0; i < topicHistory.commands.length; i++) {
205
- const command = topicHistory.commands[i];
206
- if (shouldPurge(command)) {
207
- if (i <= topicHistory.currentIndex) {
208
- indexAdjustment++;
209
- }
210
- purgedCount++;
211
- } else {
212
- newCommands.push(command);
213
- }
214
- }
215
- topicHistory.commands = newCommands;
216
- topicHistory.currentIndex = Math.max(-1, topicHistory.currentIndex - indexAdjustment);
217
- }
218
- const newTimeline = [];
219
- let globalIndexAdjustment = 0;
220
- for (let i = 0; i < data.globalTimeline.length; i++) {
221
- const entry = data.globalTimeline[i];
222
- const matchesTopic = !topic || entry.topic === topic;
223
- if (matchesTopic && shouldPurge(entry.command)) {
224
- if (i <= data.globalIndex) {
225
- globalIndexAdjustment++;
226
- }
227
- } else {
228
- newTimeline.push(entry);
229
- }
230
- }
231
- data.globalTimeline = newTimeline;
232
- data.globalIndex = Math.max(-1, data.globalIndex - globalIndexAdjustment);
233
- if (purgedCount > 0) {
234
- this.emitHistoryChange(documentId, topic);
235
- this.logger.debug(
236
- "HistoryPlugin",
237
- "PurgeByMetadata",
238
- `Purged ${purgedCount} history entries for document: ${documentId}${topic ? `, topic: ${topic}` : ""}`
239
- );
240
- }
241
- return purgedCount;
242
- }
243
- // ─────────────────────────────────────────────────────────
244
- // Document Scoping
245
- // ─────────────────────────────────────────────────────────
246
- createHistoryScope(documentId) {
247
- return {
248
- register: (command, topic) => this.register(command, topic, documentId),
249
- undo: (topic) => this.undo(topic, documentId),
250
- redo: (topic) => this.redo(topic, documentId),
251
- canUndo: (topic) => this.canUndo(topic, documentId),
252
- canRedo: (topic) => this.canRedo(topic, documentId),
253
- getHistoryState: () => this.getDocumentHistoryState(documentId),
254
- onHistoryChange: (listener) => this.historyChange$.on((event) => {
255
- if (event.documentId === documentId) {
256
- listener(event.topic);
257
- }
258
- }),
259
- purgeByMetadata: (predicate, topic) => this.purgeByMetadata(predicate, topic, documentId)
260
- };
261
- }
262
- // ─────────────────────────────────────────────────────────
263
- // Capability
264
- // ─────────────────────────────────────────────────────────
265
- buildCapability() {
266
- return {
267
- // Active document operations
268
- register: (command, topic) => {
269
- const documentId = this.getActiveDocumentId();
270
- this.register(command, topic, documentId);
271
- },
272
- undo: (topic) => {
273
- const documentId = this.getActiveDocumentId();
274
- this.undo(topic, documentId);
275
- },
276
- redo: (topic) => {
277
- const documentId = this.getActiveDocumentId();
278
- this.redo(topic, documentId);
279
- },
280
- canUndo: (topic) => {
281
- const documentId = this.getActiveDocumentId();
282
- return this.canUndo(topic, documentId);
283
- },
284
- canRedo: (topic) => {
285
- const documentId = this.getActiveDocumentId();
286
- return this.canRedo(topic, documentId);
287
- },
288
- getHistoryState: () => {
289
- const documentId = this.getActiveDocumentId();
290
- return this.getDocumentHistoryState(documentId);
291
- },
292
- // Document-scoped operations
293
- forDocument: (documentId) => this.createHistoryScope(documentId),
294
- // Events
295
- onHistoryChange: this.historyChange$.on,
296
- // Purge operations
297
- purgeByMetadata: (predicate, topic) => {
298
- const documentId = this.getActiveDocumentId();
299
- return this.purgeByMetadata(predicate, topic, documentId);
300
- }
301
- };
302
- }
303
- // ─────────────────────────────────────────────────────────
304
- // Lifecycle
305
- // ─────────────────────────────────────────────────────────
306
- async destroy() {
307
- this.historyChange$.clear();
308
- this.documentHistories.clear();
309
- super.destroy();
310
- }
311
- };
312
- _HistoryPlugin.id = "history";
313
- let HistoryPlugin = _HistoryPlugin;
314
- const initialDocumentState = {
315
- global: {
316
- canUndo: false,
317
- canRedo: false
318
- },
319
- topics: {}
11
+ /** The stack a step takes its entry from, and the one it puts it on. */
12
+ const stacksOf = (direction) => direction === "undo" ? {
13
+ from: "undo",
14
+ to: "redo"
15
+ } : {
16
+ from: "redo",
17
+ to: "undo"
320
18
  };
321
- const initialState = {
322
- documents: {},
323
- activeDocumentId: null
19
+ /** The entry the next step of `direction` takes, or `null`. */
20
+ const nextOf = (history, direction) => history[direction].at(-1) ?? null;
21
+ /**
22
+ * A new change: it joins the newest entry when typing goes on under the same key, else it is a
23
+ * new entry on top, and the oldest go past `limit`. Anything that could be redone is forgotten.
24
+ */
25
+ function recorded(history, change, limit) {
26
+ const part = {
27
+ seq: history.nextSeq,
28
+ id: change.opId,
29
+ opId: change.opId,
30
+ forward: change.shows,
31
+ backward: change.undo
32
+ };
33
+ const top = history.undo.at(-1);
34
+ if (top && change.merge !== null && history.merging === change.merge) return {
35
+ undo: [...history.undo.slice(0, -1), {
36
+ ...top,
37
+ parts: [...top.parts, part]
38
+ }],
39
+ redo: [],
40
+ merging: change.merge,
41
+ nextSeq: history.nextSeq + 1
42
+ };
43
+ const entry = {
44
+ seq: part.seq,
45
+ label: change.label,
46
+ merge: change.merge,
47
+ parts: [part]
48
+ };
49
+ return {
50
+ undo: [...history.undo, entry].slice(-Math.max(1, limit)),
51
+ redo: [],
52
+ merging: change.merge,
53
+ nextSeq: history.nextSeq + 1
54
+ };
55
+ }
56
+ /**
57
+ * A step was staged: the entry `seq` leaves the top of its stack for the top of the other, each
58
+ * part now named by the change that staged it (`opIds`, by part id).
59
+ */
60
+ function stepped(history, direction, seq, opIds) {
61
+ const { from, to } = stacksOf(direction);
62
+ const entry = history[from].find((candidate) => candidate.seq === seq);
63
+ if (!entry) return history;
64
+ const moved = {
65
+ ...entry,
66
+ parts: entry.parts.map((part) => ({
67
+ ...part,
68
+ opId: opIds.get(part.id) ?? part.opId
69
+ }))
70
+ };
71
+ return {
72
+ ...history,
73
+ [from]: history[from].filter((candidate) => candidate !== entry),
74
+ [to]: [...history[to], moved],
75
+ merging: null
76
+ };
77
+ }
78
+ /** Every entry without the parts `drop` picks, and without the entries left with none. */
79
+ const withoutParts = (entries, drop) => {
80
+ let changed = false;
81
+ const kept = [];
82
+ for (const entry of entries) {
83
+ const parts = entry.parts.filter((part) => !drop(part));
84
+ if (parts.length === entry.parts.length) {
85
+ kept.push(entry);
86
+ continue;
87
+ }
88
+ changed = true;
89
+ if (parts.length) kept.push({
90
+ ...entry,
91
+ parts
92
+ });
93
+ }
94
+ return changed ? kept : entries;
324
95
  };
325
- const reducer = (state = initialState, action) => {
326
- switch (action.type) {
327
- case INIT_HISTORY_STATE: {
328
- const { documentId } = action.payload;
329
- return {
330
- ...state,
331
- documents: {
332
- ...state.documents,
333
- [documentId]: { ...initialDocumentState }
334
- }
335
- };
336
- }
337
- case CLEANUP_HISTORY_STATE: {
338
- const { documentId } = action.payload;
339
- const { [documentId]: removed, ...remainingDocs } = state.documents;
340
- return {
341
- ...state,
342
- documents: remainingDocs,
343
- activeDocumentId: state.activeDocumentId === documentId ? null : state.activeDocumentId
344
- };
345
- }
346
- case SET_HISTORY_DOCUMENT_STATE: {
347
- const { documentId, state: docState } = action.payload;
348
- if (!state.documents[documentId]) {
349
- return state;
350
- }
351
- return {
352
- ...state,
353
- documents: {
354
- ...state.documents,
355
- [documentId]: docState
356
- }
357
- };
358
- }
359
- case SET_ACTIVE_HISTORY_DOCUMENT: {
360
- return {
361
- ...state,
362
- activeDocumentId: action.payload
363
- };
364
- }
365
- default:
366
- return state;
367
- }
96
+ /**
97
+ * The change `id` never happened (the engine refused it), or did nothing that can be undone:
98
+ * its part goes, from whichever stack holds it.
99
+ */
100
+ function forgotten(history, id) {
101
+ const undo = withoutParts(history.undo, (part) => part.id === id);
102
+ const redo = withoutParts(history.redo, (part) => part.id === id);
103
+ return undo === history.undo && redo === history.redo ? history : {
104
+ ...history,
105
+ undo,
106
+ redo
107
+ };
108
+ }
109
+ /**
110
+ * The engine refused one change of a step, so that part didn't move: it goes back where it
111
+ * was, as `part` was before the step, into its entry there (or a new one with the entry's
112
+ * number, in its place), wherever a later step had put it since. Its other parts stay where
113
+ * the step put them.
114
+ */
115
+ function stepRefused(history, direction, entry, part) {
116
+ const { from } = stacksOf(direction);
117
+ const { undo, redo } = forgotten(history, part.id);
118
+ const left = {
119
+ undo,
120
+ redo
121
+ };
122
+ const back = [...left[from]];
123
+ const at = back.findIndex((candidate) => candidate.seq === entry.seq);
124
+ if (at >= 0) {
125
+ const parts = [...back[at].parts, part].sort((left, right) => left.seq - right.seq);
126
+ back[at] = {
127
+ ...back[at],
128
+ parts
129
+ };
130
+ } else {
131
+ const before = (candidate) => from === "undo" ? candidate.seq < entry.seq : candidate.seq > entry.seq;
132
+ const index = back.filter(before).length;
133
+ back.splice(index, 0, {
134
+ ...entry,
135
+ parts: [part]
136
+ });
137
+ }
138
+ return {
139
+ ...history,
140
+ ...left,
141
+ [from]: back,
142
+ merging: null
143
+ };
144
+ }
145
+ /**
146
+ * The engine can no longer undo the entry `seq` (a final change or a new version came after
147
+ * it, or it is too old): it goes, and so does everything recorded before it.
148
+ */
149
+ function unavailable(history, seq) {
150
+ const newer = (entry) => entry.seq > seq;
151
+ return {
152
+ ...history,
153
+ undo: history.undo.filter(newer),
154
+ redo: history.redo.filter(newer),
155
+ merging: null
156
+ };
157
+ }
158
+ /** Nothing can be undone or redone. */
159
+ const cleared = (history) => history.undo.length === 0 && history.redo.length === 0 && history.merging === null ? history : {
160
+ ...history,
161
+ undo: [],
162
+ redo: [],
163
+ merging: null
368
164
  };
369
- const HistoryPluginPackage = {
370
- manifest,
371
- create: (registry) => new HistoryPlugin(HISTORY_PLUGIN_ID, registry),
372
- reducer,
373
- initialState
374
- };
375
- export {
376
- HISTORY_PLUGIN_ID,
377
- HistoryPlugin,
378
- HistoryPluginPackage,
379
- manifest
380
- };
381
- //# sourceMappingURL=index.js.map
165
+ //#endregion
166
+ //#region src/controller.ts
167
+ /** The document events after which nothing before them can be undone. */
168
+ const ENDS_UNDO = /* @__PURE__ */ new Set([
169
+ "redaction.applied",
170
+ "pages.flattened",
171
+ "signatures.completed",
172
+ "forms.repaired",
173
+ "document.versioned"
174
+ ]);
175
+ /** Whether the engine refused an undo because it can't be undone any more. */
176
+ const isUnavailable = (error) => EngineError.is(error.cause, EngineErrorCode.UndoUnavailable);
177
+ /** How many of an undo's parts were left alone, in whole or in part. */
178
+ const skippedIn = (result) => result.items.filter((item) => item.type === "skipped" || "skipped" in item && (item.skipped?.length ?? 0) > 0).length;
179
+ function createHistoryController(ctx) {
180
+ const settings = ctx.settings();
181
+ const undone = ctx.events.source();
182
+ const undoFailed = ctx.events.source();
183
+ /**
184
+ * The changes the engine will never apply, while a step is on its way: a refused change, and
185
+ * a step that undid one. A step that names one is refused too, and changes nothing more.
186
+ */
187
+ const dead = /* @__PURE__ */ new Set();
188
+ /** The parts forgotten while a step is on its way: a refusal of their step puts nothing back. */
189
+ const gone = /* @__PURE__ */ new Set();
190
+ let inFlight = 0;
191
+ const forget = (ids) => {
192
+ if (inFlight === 0) return;
193
+ for (const id of ids) gone.add(id);
194
+ };
195
+ const partsOf = (entries) => entries.flatMap((entry) => entry.parts.map((part) => part.id));
196
+ const clear = () => {
197
+ const history = ctx.state.get();
198
+ forget(partsOf([...history.undo, ...history.redo]));
199
+ ctx.state.update(cleared);
200
+ };
201
+ /**
202
+ * One step's change for one part, and what its answer does. `moot`: it named a change the
203
+ * engine never applied, or a part the history forgot, so its refusal says nothing new.
204
+ */
205
+ const answerOf = (direction, entry, part, change) => change.result.then((result) => {
206
+ if (!result.meta.undoable) {
207
+ forget([part.id]);
208
+ ctx.state.update(forgotten, part.id);
209
+ }
210
+ return { skipped: skippedIn(result) };
211
+ }, (error) => {
212
+ dead.add(change.opId);
213
+ if (dead.has(part.opId) || gone.has(part.id)) return { moot: true };
214
+ if (isUnavailable(error)) {
215
+ const history = ctx.state.get();
216
+ const older = [...history.undo, ...history.redo].filter((other) => other.seq <= entry.seq);
217
+ forget(partsOf(older));
218
+ ctx.state.update(unavailable, entry.seq);
219
+ } else ctx.state.update(stepRefused, direction, entry, part);
220
+ return { error };
221
+ });
222
+ /**
223
+ * Take one step: the undo of each part of the next entry, as one change each (undo goes
224
+ * newest first, redo oldest first), staged now, so the views show it at once.
225
+ */
226
+ const step = (direction) => {
227
+ ctx.changes.sendHolds();
228
+ const entry = nextOf(ctx.state.get(), direction);
229
+ if (!entry) return;
230
+ const parts = direction === "undo" ? [...entry.parts].reverse() : entry.parts;
231
+ const staged = [];
232
+ for (const part of parts) try {
233
+ const change = ctx.changes.stage({
234
+ label: entry.label,
235
+ undoOf: part.opId,
236
+ shows: direction === "undo" ? part.backward : part.forward
237
+ });
238
+ staged.push({
239
+ part,
240
+ change
241
+ });
242
+ } catch (error) {
243
+ if (staged.length === 0) throw error;
244
+ break;
245
+ }
246
+ ctx.state.update(stepped, direction, entry.seq, new Map(staged.map(({ part, change }) => [part.id, change.opId])));
247
+ inFlight += 1;
248
+ Promise.all(staged.map(({ part, change }) => answerOf(direction, entry, part, change))).then((answers) => {
249
+ inFlight -= 1;
250
+ if (inFlight === 0) {
251
+ dead.clear();
252
+ gone.clear();
253
+ }
254
+ const redo = direction === "redo";
255
+ if (answers.every((answer) => "moot" in answer)) return;
256
+ const failure = answers.find((answer) => "error" in answer);
257
+ if (failure && "error" in failure) {
258
+ undoFailed.emit({
259
+ label: entry.label,
260
+ redo,
261
+ reason: isUnavailable(failure.error) ? "unavailable" : "refused",
262
+ error: failure.error
263
+ });
264
+ return;
265
+ }
266
+ const skipped = answers.reduce((sum, answer) => sum + ("skipped" in answer ? answer.skipped : 0), 0);
267
+ undone.emit({
268
+ label: entry.label,
269
+ redo,
270
+ skipped
271
+ });
272
+ });
273
+ };
274
+ /** A change still being typed: what undo would undo before anything in the history. */
275
+ const openHold = () => ctx.changes.pending().find((change) => change.held && change.history) ?? null;
276
+ return {
277
+ api: {
278
+ ...settings.api,
279
+ undo: () => step("undo"),
280
+ redo: () => step("redo"),
281
+ canUndo: () => openHold() !== null || ctx.state.get().undo.length > 0,
282
+ canRedo: () => openHold() === null && ctx.state.get().redo.length > 0,
283
+ getUndoLabel: () => openHold()?.label ?? nextOf(ctx.state.get(), "undo")?.label ?? null,
284
+ getRedoLabel: () => openHold() === null ? nextOf(ctx.state.get(), "redo")?.label ?? null : null,
285
+ clear,
286
+ onUndone: undone.on,
287
+ onUndoFailed: undoFailed.on
288
+ },
289
+ connect() {
290
+ ctx.listen(ctx.changes.onStaged, (change) => {
291
+ if (change.undoOf !== null || !change.history) return;
292
+ ctx.state.update(recorded, change, settings.get().limit);
293
+ });
294
+ ctx.listen(ctx.changes.onSettled, (settled) => {
295
+ const { change } = settled;
296
+ if (change.undoOf !== null || !change.history) return;
297
+ if (settled.status === "refused" || !settled.result.meta.undoable) {
298
+ if (settled.status === "refused" && inFlight > 0) dead.add(change.opId);
299
+ forget([change.opId]);
300
+ ctx.state.update(forgotten, change.opId);
301
+ }
302
+ });
303
+ ctx.listen(ctx.doc.events, (event) => {
304
+ if (ENDS_UNDO.has(event.type)) clear();
305
+ });
306
+ }
307
+ };
308
+ }
309
+ //#endregion
310
+ //#region src/history.plugin.ts
311
+ /**
312
+ * Undo and redo, one history per open document, of what this session changes through the
313
+ * viewer's plugins: every change they stage on the document's change queue is a step. No
314
+ * dependencies and no engine state of its own. `config` is the settings the app registers,
315
+ * over {@link HISTORY_DEFAULTS}.
316
+ */
317
+ const historyPlugin = (config) => definePlugin({
318
+ id: "history",
319
+ token: HistoryToken,
320
+ scope: "document",
321
+ state: emptyHistory,
322
+ settings: {
323
+ defaults: HISTORY_DEFAULTS,
324
+ registered: config
325
+ },
326
+ create: createHistoryController
327
+ });
328
+ //#endregion
329
+ //#region src/state.ts
330
+ /**
331
+ * The history page's State table as code: what `useHistoryState()` returns,
332
+ * and the same fields in every other framework. What the undo and redo
333
+ * buttons read.
334
+ */
335
+ const historyState = defineState(HistoryToken, {
336
+ read: (history) => ({
337
+ canUndo: history.canUndo(),
338
+ canRedo: history.canRedo(),
339
+ undoLabel: history.getUndoLabel(),
340
+ redoLabel: history.getRedoLabel()
341
+ }),
342
+ empty: {
343
+ canUndo: false,
344
+ canRedo: false,
345
+ undoLabel: null,
346
+ redoLabel: null
347
+ }
348
+ });
349
+ //#endregion
350
+ export { HISTORY_DEFAULTS, HistoryToken, historyPlugin, historyState };
351
+
352
+ //# sourceMappingURL=index.js.map