fabricjs-document-engine 0.0.0 → 0.2.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 (68) hide show
  1. package/README.md +146 -12
  2. package/dist/document/document-format.d.cts +1 -0
  3. package/dist/document/document-format.d.ts +1 -0
  4. package/dist/document/validate-document.cjs +1 -0
  5. package/dist/document/validate-document.js +1 -0
  6. package/dist/engine/create-document-engine.cjs +70 -16
  7. package/dist/engine/create-document-engine.d.cts +28 -5
  8. package/dist/engine/create-document-engine.d.ts +28 -5
  9. package/dist/engine/create-document-engine.js +70 -16
  10. package/dist/engine/errors.cjs +5 -0
  11. package/dist/engine/errors.d.cts +4 -1
  12. package/dist/engine/errors.d.ts +4 -1
  13. package/dist/engine/errors.js +5 -1
  14. package/dist/fabric/fabric-adapter.cjs +7 -2
  15. package/dist/fabric/fabric-adapter.js +7 -3
  16. package/dist/history/apply-state.cjs +48 -0
  17. package/dist/history/apply-state.js +48 -0
  18. package/dist/history/create-history.cjs +193 -0
  19. package/dist/history/create-history.d.cts +12 -0
  20. package/dist/history/create-history.d.ts +12 -0
  21. package/dist/history/create-history.js +192 -0
  22. package/dist/history/history-stack.cjs +27 -0
  23. package/dist/history/history-stack.js +27 -0
  24. package/dist/history/keyboard-shortcuts.cjs +32 -0
  25. package/dist/history/keyboard-shortcuts.d.cts +10 -0
  26. package/dist/history/keyboard-shortcuts.d.ts +10 -0
  27. package/dist/history/keyboard-shortcuts.js +32 -0
  28. package/dist/history/snapshot.cjs +47 -0
  29. package/dist/history/snapshot.js +46 -0
  30. package/dist/index.cjs +5 -0
  31. package/dist/index.d.cts +10 -3
  32. package/dist/index.d.ts +10 -3
  33. package/dist/index.js +4 -2
  34. package/dist/save/autosave-scheduler.cjs +28 -0
  35. package/dist/save/autosave-scheduler.d.cts +6 -0
  36. package/dist/save/autosave-scheduler.d.ts +6 -0
  37. package/dist/save/autosave-scheduler.js +28 -0
  38. package/dist/save/retry.cjs +33 -0
  39. package/dist/save/retry.d.cts +7 -0
  40. package/dist/save/retry.d.ts +7 -0
  41. package/dist/save/retry.js +31 -0
  42. package/dist/save/save-controller.cjs +148 -0
  43. package/dist/save/save-controller.d.cts +20 -0
  44. package/dist/save/save-controller.d.ts +20 -0
  45. package/dist/save/save-controller.js +148 -0
  46. package/dist/save/unsaved-changes-warning.cjs +12 -0
  47. package/dist/save/unsaved-changes-warning.d.cts +6 -0
  48. package/dist/save/unsaved-changes-warning.d.ts +6 -0
  49. package/dist/save/unsaved-changes-warning.js +12 -0
  50. package/dist/storage/key-value-storage.cjs +35 -0
  51. package/dist/storage/key-value-storage.d.cts +14 -0
  52. package/dist/storage/key-value-storage.d.ts +14 -0
  53. package/dist/storage/key-value-storage.js +35 -0
  54. package/dist/storage/local-storage.cjs +24 -0
  55. package/dist/storage/local-storage.d.cts +8 -0
  56. package/dist/storage/local-storage.d.ts +8 -0
  57. package/dist/storage/local-storage.js +24 -0
  58. package/dist/storage/memory-storage.cjs +17 -0
  59. package/dist/storage/memory-storage.d.cts +4 -0
  60. package/dist/storage/memory-storage.d.ts +4 -0
  61. package/dist/storage/memory-storage.js +17 -0
  62. package/dist/storage/storage-contract.d.cts +14 -0
  63. package/dist/storage/storage-contract.d.ts +14 -0
  64. package/dist/storage.cjs +7 -0
  65. package/dist/storage.d.cts +5 -0
  66. package/dist/storage.d.ts +5 -0
  67. package/dist/storage.js +4 -0
  68. package/package.json +14 -3
@@ -0,0 +1,192 @@
1
+ import { applyStateChange } from "./apply-state.js";
2
+ import { createHistoryStack } from "./history-stack.js";
3
+ import { createSnapshot, diffSnapshots } from "./snapshot.js";
4
+ //#region src/history/create-history.ts
5
+ const verbsByAction = {
6
+ drag: "Move",
7
+ rotate: "Rotate",
8
+ scale: "Resize",
9
+ scaleX: "Resize",
10
+ scaleY: "Resize",
11
+ resizing: "Resize",
12
+ skewX: "Skew",
13
+ skewY: "Skew"
14
+ };
15
+ function nameOf(object) {
16
+ return (object.constructor.type ?? object.type).toLowerCase();
17
+ }
18
+ function isPromiseLike(value) {
19
+ return typeof value?.then === "function";
20
+ }
21
+ function describePendingChanges(changes) {
22
+ if (new Set(changes.map((change) => change.verb)).size > 1) return "Edit objects";
23
+ const verb = changes[0]?.verb ?? "Edit";
24
+ return changes.length === 1 ? `${verb} ${changes[0].noun}` : `${verb} ${changes.length} objects`;
25
+ }
26
+ function createHistory(options) {
27
+ const { canvas, serializeObjects, onChange, onContentChange } = options;
28
+ const stack = createHistoryStack(Math.max(1, options.limit ?? 100));
29
+ let snapshot = createSnapshot(serializeObjects());
30
+ let pendingChanges = [];
31
+ let flushScheduled = false;
32
+ let transactionDepth = 0;
33
+ let pausedDepth = 0;
34
+ let applying = false;
35
+ let queue = Promise.resolve();
36
+ function state() {
37
+ const undoLabels = stack.undoLabels();
38
+ const redoLabels = stack.redoLabels();
39
+ return {
40
+ canUndo: undoLabels.length > 0,
41
+ canRedo: redoLabels.length > 0,
42
+ undoLabel: undoLabels[0],
43
+ redoLabel: redoLabels[0]
44
+ };
45
+ }
46
+ function commit(label) {
47
+ pendingChanges = [];
48
+ const nextSnapshot = createSnapshot(serializeObjects());
49
+ const difference = diffSnapshots(snapshot, nextSnapshot);
50
+ snapshot = nextSnapshot;
51
+ if (difference === null) return false;
52
+ stack.record({
53
+ label,
54
+ ...difference
55
+ });
56
+ onContentChange();
57
+ onChange(state());
58
+ return true;
59
+ }
60
+ function isRecording() {
61
+ return !applying && pausedDepth === 0 && transactionDepth === 0;
62
+ }
63
+ function commitPendingChanges() {
64
+ flushScheduled = false;
65
+ if (pendingChanges.length === 0) return;
66
+ if (isRecording()) commit(describePendingChanges(pendingChanges));
67
+ pendingChanges = [];
68
+ }
69
+ function notice(verb, target) {
70
+ if (!isRecording()) return;
71
+ pendingChanges.push({
72
+ verb,
73
+ noun: nameOf(target)
74
+ });
75
+ if (flushScheduled) return;
76
+ flushScheduled = true;
77
+ queueMicrotask(commitPendingChanges);
78
+ }
79
+ const handleAdded = ({ target }) => notice("Add", target);
80
+ const handleRemoved = ({ target }) => notice("Delete", target);
81
+ const handleModified = (event) => {
82
+ const action = event.action ?? event.transform?.action ?? "";
83
+ notice(verbsByAction[action] ?? "Transform", event.target);
84
+ };
85
+ const handleTextEdited = ({ target }) => notice("Edit", target);
86
+ const listeners = [
87
+ ["object:added", handleAdded],
88
+ ["object:removed", handleRemoved],
89
+ ["object:modified", handleModified],
90
+ ["text:editing:exited", handleTextEdited]
91
+ ];
92
+ const eventTarget = canvas;
93
+ listeners.forEach(([name, handler]) => eventTarget.on(name, handler));
94
+ function transaction(label, work) {
95
+ if (transactionDepth === 0) commitPendingChanges();
96
+ transactionDepth += 1;
97
+ const finish = () => {
98
+ transactionDepth -= 1;
99
+ if (transactionDepth === 0) commit(label);
100
+ };
101
+ let result;
102
+ try {
103
+ result = work();
104
+ } catch (error) {
105
+ finish();
106
+ throw error;
107
+ }
108
+ if (!isPromiseLike(result)) {
109
+ finish();
110
+ return result;
111
+ }
112
+ return Promise.resolve(result).then((value) => {
113
+ finish();
114
+ return value;
115
+ }, (error) => {
116
+ finish();
117
+ throw error;
118
+ });
119
+ }
120
+ function withoutRecording(work) {
121
+ pausedDepth += 1;
122
+ let result;
123
+ try {
124
+ result = work();
125
+ } catch (error) {
126
+ pausedDepth -= 1;
127
+ throw error;
128
+ }
129
+ if (!isPromiseLike(result)) {
130
+ pausedDepth -= 1;
131
+ return result;
132
+ }
133
+ return Promise.resolve(result).finally(() => {
134
+ pausedDepth -= 1;
135
+ });
136
+ }
137
+ function runInOrder(task) {
138
+ const run = queue.then(task);
139
+ queue = run.catch(() => void 0);
140
+ return run;
141
+ }
142
+ function travel(takeStep, pickChange, putBack, moveTo) {
143
+ return runInOrder(async () => {
144
+ if (transactionDepth > 0) return false;
145
+ commitPendingChanges();
146
+ const step = takeStep();
147
+ if (step === void 0) return false;
148
+ applying = true;
149
+ try {
150
+ await applyStateChange(canvas, pickChange(step));
151
+ } catch (error) {
152
+ putBack(step);
153
+ throw error;
154
+ } finally {
155
+ applying = false;
156
+ }
157
+ snapshot = createSnapshot(serializeObjects());
158
+ moveTo(step);
159
+ onContentChange();
160
+ onChange(state());
161
+ return true;
162
+ });
163
+ }
164
+ return {
165
+ commit(label) {
166
+ if (applying) return false;
167
+ return commit(label);
168
+ },
169
+ transaction,
170
+ undo: () => travel(() => stack.takeUndo(), (step) => step.before, (step) => stack.returnUndo(step), (step) => stack.returnRedo(step)),
171
+ redo: () => travel(() => stack.takeRedo(), (step) => step.after, (step) => stack.returnRedo(step), (step) => stack.returnUndo(step)),
172
+ state,
173
+ labels: () => ({
174
+ undo: stack.undoLabels(),
175
+ redo: stack.redoLabels()
176
+ }),
177
+ withoutRecording,
178
+ reset() {
179
+ pendingChanges = [];
180
+ stack.clear();
181
+ snapshot = createSnapshot(serializeObjects());
182
+ onChange(state());
183
+ },
184
+ destroy() {
185
+ listeners.forEach(([name, handler]) => eventTarget.off(name, handler));
186
+ pendingChanges = [];
187
+ stack.clear();
188
+ }
189
+ };
190
+ }
191
+ //#endregion
192
+ export { createHistory, describePendingChanges };
@@ -0,0 +1,27 @@
1
+ //#region src/history/history-stack.ts
2
+ function createHistoryStack(limit) {
3
+ const undoSteps = [];
4
+ const redoSteps = [];
5
+ function pushWithinLimit(steps, step) {
6
+ steps.push(step);
7
+ if (steps.length > limit) steps.shift();
8
+ }
9
+ return {
10
+ record(step) {
11
+ pushWithinLimit(undoSteps, step);
12
+ redoSteps.length = 0;
13
+ },
14
+ takeUndo: () => undoSteps.pop(),
15
+ takeRedo: () => redoSteps.pop(),
16
+ returnUndo: (step) => pushWithinLimit(undoSteps, step),
17
+ returnRedo: (step) => pushWithinLimit(redoSteps, step),
18
+ undoLabels: () => undoSteps.map((step) => step.label).reverse(),
19
+ redoLabels: () => redoSteps.map((step) => step.label).reverse(),
20
+ clear() {
21
+ undoSteps.length = 0;
22
+ redoSteps.length = 0;
23
+ }
24
+ };
25
+ }
26
+ //#endregion
27
+ exports.createHistoryStack = createHistoryStack;
@@ -0,0 +1,27 @@
1
+ //#region src/history/history-stack.ts
2
+ function createHistoryStack(limit) {
3
+ const undoSteps = [];
4
+ const redoSteps = [];
5
+ function pushWithinLimit(steps, step) {
6
+ steps.push(step);
7
+ if (steps.length > limit) steps.shift();
8
+ }
9
+ return {
10
+ record(step) {
11
+ pushWithinLimit(undoSteps, step);
12
+ redoSteps.length = 0;
13
+ },
14
+ takeUndo: () => undoSteps.pop(),
15
+ takeRedo: () => redoSteps.pop(),
16
+ returnUndo: (step) => pushWithinLimit(undoSteps, step),
17
+ returnRedo: (step) => pushWithinLimit(redoSteps, step),
18
+ undoLabels: () => undoSteps.map((step) => step.label).reverse(),
19
+ redoLabels: () => redoSteps.map((step) => step.label).reverse(),
20
+ clear() {
21
+ undoSteps.length = 0;
22
+ redoSteps.length = 0;
23
+ }
24
+ };
25
+ }
26
+ //#endregion
27
+ export { createHistoryStack };
@@ -0,0 +1,32 @@
1
+ //#region src/history/keyboard-shortcuts.ts
2
+ function isTypingInto(element) {
3
+ if (!(element instanceof HTMLElement)) return false;
4
+ if (element.isContentEditable) return true;
5
+ return [
6
+ "INPUT",
7
+ "TEXTAREA",
8
+ "SELECT"
9
+ ].includes(element.tagName);
10
+ }
11
+ function chooseAction(event) {
12
+ if (!(event.metaKey || event.ctrlKey) || event.altKey) return null;
13
+ const key = event.key.toLowerCase();
14
+ if (key === "z") return event.shiftKey ? "redo" : "undo";
15
+ if (key === "y" && event.ctrlKey && !event.shiftKey) return "redo";
16
+ return null;
17
+ }
18
+ function bindKeyboardShortcuts(engine, options = {}) {
19
+ const target = options.target ?? globalThis.window;
20
+ const handleKeyDown = (event) => {
21
+ const keyboardEvent = event;
22
+ if (keyboardEvent.defaultPrevented || isTypingInto(keyboardEvent.target)) return;
23
+ const action = chooseAction(keyboardEvent);
24
+ if (action === null) return;
25
+ keyboardEvent.preventDefault();
26
+ engine[action]().catch(() => void 0);
27
+ };
28
+ target.addEventListener("keydown", handleKeyDown);
29
+ return () => target.removeEventListener("keydown", handleKeyDown);
30
+ }
31
+ //#endregion
32
+ exports.bindKeyboardShortcuts = bindKeyboardShortcuts;
@@ -0,0 +1,10 @@
1
+ //#region src/history/keyboard-shortcuts.d.ts
2
+ export interface UndoRedoTarget {
3
+ undo(): Promise<boolean>;
4
+ redo(): Promise<boolean>;
5
+ }
6
+ export interface KeyboardShortcutOptions {
7
+ target?: EventTarget;
8
+ }
9
+ export declare function bindKeyboardShortcuts(engine: UndoRedoTarget, options?: KeyboardShortcutOptions): () => void;
10
+ //#endregion
@@ -0,0 +1,10 @@
1
+ //#region src/history/keyboard-shortcuts.d.ts
2
+ export interface UndoRedoTarget {
3
+ undo(): Promise<boolean>;
4
+ redo(): Promise<boolean>;
5
+ }
6
+ export interface KeyboardShortcutOptions {
7
+ target?: EventTarget;
8
+ }
9
+ export declare function bindKeyboardShortcuts(engine: UndoRedoTarget, options?: KeyboardShortcutOptions): () => void;
10
+ //#endregion
@@ -0,0 +1,32 @@
1
+ //#region src/history/keyboard-shortcuts.ts
2
+ function isTypingInto(element) {
3
+ if (!(element instanceof HTMLElement)) return false;
4
+ if (element.isContentEditable) return true;
5
+ return [
6
+ "INPUT",
7
+ "TEXTAREA",
8
+ "SELECT"
9
+ ].includes(element.tagName);
10
+ }
11
+ function chooseAction(event) {
12
+ if (!(event.metaKey || event.ctrlKey) || event.altKey) return null;
13
+ const key = event.key.toLowerCase();
14
+ if (key === "z") return event.shiftKey ? "redo" : "undo";
15
+ if (key === "y" && event.ctrlKey && !event.shiftKey) return "redo";
16
+ return null;
17
+ }
18
+ function bindKeyboardShortcuts(engine, options = {}) {
19
+ const target = options.target ?? globalThis.window;
20
+ const handleKeyDown = (event) => {
21
+ const keyboardEvent = event;
22
+ if (keyboardEvent.defaultPrevented || isTypingInto(keyboardEvent.target)) return;
23
+ const action = chooseAction(keyboardEvent);
24
+ if (action === null) return;
25
+ keyboardEvent.preventDefault();
26
+ engine[action]().catch(() => void 0);
27
+ };
28
+ target.addEventListener("keydown", handleKeyDown);
29
+ return () => target.removeEventListener("keydown", handleKeyDown);
30
+ }
31
+ //#endregion
32
+ export { bindKeyboardShortcuts };
@@ -0,0 +1,47 @@
1
+ //#region src/history/snapshot.ts
2
+ function createSnapshot(objects) {
3
+ const objectsById = /* @__PURE__ */ new Map();
4
+ const order = [];
5
+ for (const object of objects) {
6
+ const id = object.id ?? "";
7
+ objectsById.set(id, JSON.stringify(object));
8
+ order.push(id);
9
+ }
10
+ return {
11
+ objectsById,
12
+ order
13
+ };
14
+ }
15
+ function haveSameOrder(first, second) {
16
+ return first.length === second.length && first.every((id, index) => id === second[index]);
17
+ }
18
+ function diffSnapshots(previous, next) {
19
+ const before = /* @__PURE__ */ new Map();
20
+ const after = /* @__PURE__ */ new Map();
21
+ for (const [id, json] of next.objectsById) {
22
+ const previousJson = previous.objectsById.get(id);
23
+ if (previousJson === json) continue;
24
+ before.set(id, previousJson ?? null);
25
+ after.set(id, json);
26
+ }
27
+ for (const [id, json] of previous.objectsById) {
28
+ if (next.objectsById.has(id)) continue;
29
+ before.set(id, json);
30
+ after.set(id, null);
31
+ }
32
+ const orderChanged = !haveSameOrder(previous.order, next.order);
33
+ if (before.size === 0 && !orderChanged) return null;
34
+ return {
35
+ before: {
36
+ objects: before,
37
+ order: orderChanged ? previous.order : null
38
+ },
39
+ after: {
40
+ objects: after,
41
+ order: orderChanged ? next.order : null
42
+ }
43
+ };
44
+ }
45
+ //#endregion
46
+ exports.createSnapshot = createSnapshot;
47
+ exports.diffSnapshots = diffSnapshots;
@@ -0,0 +1,46 @@
1
+ //#region src/history/snapshot.ts
2
+ function createSnapshot(objects) {
3
+ const objectsById = /* @__PURE__ */ new Map();
4
+ const order = [];
5
+ for (const object of objects) {
6
+ const id = object.id ?? "";
7
+ objectsById.set(id, JSON.stringify(object));
8
+ order.push(id);
9
+ }
10
+ return {
11
+ objectsById,
12
+ order
13
+ };
14
+ }
15
+ function haveSameOrder(first, second) {
16
+ return first.length === second.length && first.every((id, index) => id === second[index]);
17
+ }
18
+ function diffSnapshots(previous, next) {
19
+ const before = /* @__PURE__ */ new Map();
20
+ const after = /* @__PURE__ */ new Map();
21
+ for (const [id, json] of next.objectsById) {
22
+ const previousJson = previous.objectsById.get(id);
23
+ if (previousJson === json) continue;
24
+ before.set(id, previousJson ?? null);
25
+ after.set(id, json);
26
+ }
27
+ for (const [id, json] of previous.objectsById) {
28
+ if (next.objectsById.has(id)) continue;
29
+ before.set(id, json);
30
+ after.set(id, null);
31
+ }
32
+ const orderChanged = !haveSameOrder(previous.order, next.order);
33
+ if (before.size === 0 && !orderChanged) return null;
34
+ return {
35
+ before: {
36
+ objects: before,
37
+ order: orderChanged ? previous.order : null
38
+ },
39
+ after: {
40
+ objects: after,
41
+ order: orderChanged ? next.order : null
42
+ }
43
+ };
44
+ }
45
+ //#endregion
46
+ export { createSnapshot, diffSnapshots };
package/dist/index.cjs CHANGED
@@ -3,8 +3,13 @@ const require_document_format = require("./document/document-format.cjs");
3
3
  const require_validate_document = require("./document/validate-document.cjs");
4
4
  const require_errors = require("./engine/errors.cjs");
5
5
  const require_create_document_engine = require("./engine/create-document-engine.cjs");
6
+ const require_keyboard_shortcuts = require("./history/keyboard-shortcuts.cjs");
7
+ const require_unsaved_changes_warning = require("./save/unsaved-changes-warning.cjs");
6
8
  exports.CURRENT_SCHEMA_VERSION = require_document_format.CURRENT_SCHEMA_VERSION;
7
9
  exports.DocumentEngineError = require_errors.DocumentEngineError;
10
+ exports.bindKeyboardShortcuts = require_keyboard_shortcuts.bindKeyboardShortcuts;
11
+ exports.bindUnsavedChangesWarning = require_unsaved_changes_warning.bindUnsavedChangesWarning;
12
+ exports.createConflictError = require_errors.createConflictError;
8
13
  exports.createDocumentEngine = require_create_document_engine.createDocumentEngine;
9
14
  exports.isDocumentEngineError = require_errors.isDocumentEngineError;
10
15
  exports.validateDocument = require_validate_document.validateDocument;
package/dist/index.d.cts CHANGED
@@ -1,8 +1,15 @@
1
1
  import { CURRENT_SCHEMA_VERSION, DocumentCanvas, DocumentInfo, FabricDocument, SerializedFabricObject } from "./document/document-format.cjs";
2
2
  import { NewDocumentOptions } from "./document/create-document.cjs";
3
3
  import { CustomObjectDefinition } from "./fabric/object-registry.cjs";
4
- import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, isDocumentEngineError } from "./engine/errors.cjs";
4
+ import { HistoryOptions, HistoryState } from "./history/create-history.cjs";
5
+ import { AutosaveOptions } from "./save/autosave-scheduler.cjs";
6
+ import { RetryOptions } from "./save/retry.cjs";
7
+ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, createConflictError, isDocumentEngineError } from "./engine/errors.cjs";
8
+ import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.cjs";
9
+ import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.cjs";
5
10
  import { Unsubscribe } from "./engine/event-emitter.cjs";
6
- import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, DocumentStorage, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
11
+ import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
7
12
  import { validateDocument } from "./document/validate-document.cjs";
8
- export { CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type LoadOptions, type NewDocumentOptions, type SerializedFabricObject, type Unsubscribe, createDocumentEngine, isDocumentEngineError, validateDocument };
13
+ import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.cjs";
14
+ import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.cjs";
15
+ export { type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type HistoryOptions, type HistoryState, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import { CURRENT_SCHEMA_VERSION, DocumentCanvas, DocumentInfo, FabricDocument, SerializedFabricObject } from "./document/document-format.js";
2
2
  import { NewDocumentOptions } from "./document/create-document.js";
3
3
  import { CustomObjectDefinition } from "./fabric/object-registry.js";
4
- import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, isDocumentEngineError } from "./engine/errors.js";
4
+ import { HistoryOptions, HistoryState } from "./history/create-history.js";
5
+ import { AutosaveOptions } from "./save/autosave-scheduler.js";
6
+ import { RetryOptions } from "./save/retry.js";
7
+ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, createConflictError, isDocumentEngineError } from "./engine/errors.js";
8
+ import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.js";
9
+ import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.js";
5
10
  import { Unsubscribe } from "./engine/event-emitter.js";
6
- import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, DocumentStorage, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
11
+ import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
7
12
  import { validateDocument } from "./document/validate-document.js";
8
- export { CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type LoadOptions, type NewDocumentOptions, type SerializedFabricObject, type Unsubscribe, createDocumentEngine, isDocumentEngineError, validateDocument };
13
+ import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
14
+ import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
15
+ export { type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type HistoryOptions, type HistoryState, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { CURRENT_SCHEMA_VERSION } from "./document/document-format.js";
2
2
  import { validateDocument } from "./document/validate-document.js";
3
- import { DocumentEngineError, isDocumentEngineError } from "./engine/errors.js";
3
+ import { DocumentEngineError, createConflictError, isDocumentEngineError } from "./engine/errors.js";
4
4
  import { createDocumentEngine } from "./engine/create-document-engine.js";
5
- export { CURRENT_SCHEMA_VERSION, DocumentEngineError, createDocumentEngine, isDocumentEngineError, validateDocument };
5
+ import { bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
6
+ import { bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
7
+ export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
@@ -0,0 +1,28 @@
1
+ //#region src/save/autosave-scheduler.ts
2
+ function createAutosaveScheduler(run, options = {}) {
3
+ const delay = options.delay ?? 1e3;
4
+ const maxWait = Math.max(delay, options.maxWait ?? 1e4);
5
+ let timer;
6
+ let firstRequestAt;
7
+ function cancel() {
8
+ if (timer !== void 0) clearTimeout(timer);
9
+ timer = void 0;
10
+ firstRequestAt = void 0;
11
+ }
12
+ function fire() {
13
+ cancel();
14
+ run();
15
+ }
16
+ return {
17
+ schedule() {
18
+ const now = Date.now();
19
+ firstRequestAt ?? (firstRequestAt = now);
20
+ const timeLeftBeforeMaxWait = maxWait - (now - firstRequestAt);
21
+ if (timer !== void 0) clearTimeout(timer);
22
+ timer = setTimeout(fire, Math.max(0, Math.min(delay, timeLeftBeforeMaxWait)));
23
+ },
24
+ cancel
25
+ };
26
+ }
27
+ //#endregion
28
+ exports.createAutosaveScheduler = createAutosaveScheduler;
@@ -0,0 +1,6 @@
1
+ //#region src/save/autosave-scheduler.d.ts
2
+ export interface AutosaveOptions {
3
+ delay?: number;
4
+ maxWait?: number;
5
+ }
6
+ //#endregion
@@ -0,0 +1,6 @@
1
+ //#region src/save/autosave-scheduler.d.ts
2
+ export interface AutosaveOptions {
3
+ delay?: number;
4
+ maxWait?: number;
5
+ }
6
+ //#endregion
@@ -0,0 +1,28 @@
1
+ //#region src/save/autosave-scheduler.ts
2
+ function createAutosaveScheduler(run, options = {}) {
3
+ const delay = options.delay ?? 1e3;
4
+ const maxWait = Math.max(delay, options.maxWait ?? 1e4);
5
+ let timer;
6
+ let firstRequestAt;
7
+ function cancel() {
8
+ if (timer !== void 0) clearTimeout(timer);
9
+ timer = void 0;
10
+ firstRequestAt = void 0;
11
+ }
12
+ function fire() {
13
+ cancel();
14
+ run();
15
+ }
16
+ return {
17
+ schedule() {
18
+ const now = Date.now();
19
+ firstRequestAt ?? (firstRequestAt = now);
20
+ const timeLeftBeforeMaxWait = maxWait - (now - firstRequestAt);
21
+ if (timer !== void 0) clearTimeout(timer);
22
+ timer = setTimeout(fire, Math.max(0, Math.min(delay, timeLeftBeforeMaxWait)));
23
+ },
24
+ cancel
25
+ };
26
+ }
27
+ //#endregion
28
+ export { createAutosaveScheduler };
@@ -0,0 +1,33 @@
1
+ //#region src/save/retry.ts
2
+ function isRetryable(error) {
3
+ const hints = error ?? {};
4
+ if (hints.code === "SAVE_CONFLICT" || hints.code === "SAVE_CANCELLED") return false;
5
+ return hints.retryable !== false;
6
+ }
7
+ function retryDelay(attempt, options = {}) {
8
+ const baseDelay = options.baseDelay ?? 500;
9
+ const maxDelay = options.maxDelay ?? 8e3;
10
+ const ceiling = Math.min(maxDelay, baseDelay * 2 ** attempt);
11
+ return ceiling / 2 + Math.random() * (ceiling / 2);
12
+ }
13
+ function wait(milliseconds, signal) {
14
+ return new Promise((resolve, reject) => {
15
+ if (signal.aborted) {
16
+ reject(/* @__PURE__ */ new Error("aborted"));
17
+ return;
18
+ }
19
+ const stop = () => {
20
+ clearTimeout(timer);
21
+ reject(/* @__PURE__ */ new Error("aborted"));
22
+ };
23
+ const timer = setTimeout(() => {
24
+ signal.removeEventListener("abort", stop);
25
+ resolve();
26
+ }, milliseconds);
27
+ signal.addEventListener("abort", stop, { once: true });
28
+ });
29
+ }
30
+ //#endregion
31
+ exports.isRetryable = isRetryable;
32
+ exports.retryDelay = retryDelay;
33
+ exports.wait = wait;
@@ -0,0 +1,7 @@
1
+ //#region src/save/retry.d.ts
2
+ export interface RetryOptions {
3
+ attempts?: number;
4
+ baseDelay?: number;
5
+ maxDelay?: number;
6
+ }
7
+ //#endregion
@@ -0,0 +1,7 @@
1
+ //#region src/save/retry.d.ts
2
+ export interface RetryOptions {
3
+ attempts?: number;
4
+ baseDelay?: number;
5
+ maxDelay?: number;
6
+ }
7
+ //#endregion
@@ -0,0 +1,31 @@
1
+ //#region src/save/retry.ts
2
+ function isRetryable(error) {
3
+ const hints = error ?? {};
4
+ if (hints.code === "SAVE_CONFLICT" || hints.code === "SAVE_CANCELLED") return false;
5
+ return hints.retryable !== false;
6
+ }
7
+ function retryDelay(attempt, options = {}) {
8
+ const baseDelay = options.baseDelay ?? 500;
9
+ const maxDelay = options.maxDelay ?? 8e3;
10
+ const ceiling = Math.min(maxDelay, baseDelay * 2 ** attempt);
11
+ return ceiling / 2 + Math.random() * (ceiling / 2);
12
+ }
13
+ function wait(milliseconds, signal) {
14
+ return new Promise((resolve, reject) => {
15
+ if (signal.aborted) {
16
+ reject(/* @__PURE__ */ new Error("aborted"));
17
+ return;
18
+ }
19
+ const stop = () => {
20
+ clearTimeout(timer);
21
+ reject(/* @__PURE__ */ new Error("aborted"));
22
+ };
23
+ const timer = setTimeout(() => {
24
+ signal.removeEventListener("abort", stop);
25
+ resolve();
26
+ }, milliseconds);
27
+ signal.addEventListener("abort", stop, { once: true });
28
+ });
29
+ }
30
+ //#endregion
31
+ export { isRetryable, retryDelay, wait };