fabricjs-document-engine 0.1.0 → 0.3.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 (70) hide show
  1. package/README.md +162 -21
  2. package/dist/assets/asset-manifest.cjs +43 -0
  3. package/dist/assets/asset-manifest.d.cts +16 -0
  4. package/dist/assets/asset-manifest.d.ts +16 -0
  5. package/dist/assets/asset-manifest.js +41 -0
  6. package/dist/assets/asset-pipeline.cjs +174 -0
  7. package/dist/assets/asset-pipeline.d.cts +31 -0
  8. package/dist/assets/asset-pipeline.d.ts +31 -0
  9. package/dist/assets/asset-pipeline.js +172 -0
  10. package/dist/assets/asset-references.cjs +83 -0
  11. package/dist/assets/asset-references.js +82 -0
  12. package/dist/assets/font-check.cjs +59 -0
  13. package/dist/assets/font-check.d.cts +4 -0
  14. package/dist/assets/font-check.d.ts +4 -0
  15. package/dist/assets/font-check.js +57 -0
  16. package/dist/assets/image-check.cjs +36 -0
  17. package/dist/assets/image-check.js +33 -0
  18. package/dist/document/document-format.d.cts +3 -0
  19. package/dist/document/document-format.d.ts +3 -0
  20. package/dist/document/validate-document.cjs +5 -0
  21. package/dist/document/validate-document.js +5 -0
  22. package/dist/engine/create-document-engine.cjs +82 -18
  23. package/dist/engine/create-document-engine.d.cts +21 -5
  24. package/dist/engine/create-document-engine.d.ts +21 -5
  25. package/dist/engine/create-document-engine.js +82 -18
  26. package/dist/engine/errors.cjs +7 -0
  27. package/dist/engine/errors.d.cts +9 -1
  28. package/dist/engine/errors.d.ts +9 -1
  29. package/dist/engine/errors.js +7 -1
  30. package/dist/history/create-history.cjs +3 -1
  31. package/dist/history/create-history.js +3 -1
  32. package/dist/index.cjs +4 -1
  33. package/dist/index.d.cts +11 -3
  34. package/dist/index.d.ts +11 -3
  35. package/dist/index.js +3 -2
  36. package/dist/save/autosave-scheduler.cjs +28 -0
  37. package/dist/save/autosave-scheduler.d.cts +6 -0
  38. package/dist/save/autosave-scheduler.d.ts +6 -0
  39. package/dist/save/autosave-scheduler.js +28 -0
  40. package/dist/save/retry.cjs +33 -0
  41. package/dist/save/retry.d.cts +7 -0
  42. package/dist/save/retry.d.ts +7 -0
  43. package/dist/save/retry.js +31 -0
  44. package/dist/save/save-controller.cjs +149 -0
  45. package/dist/save/save-controller.d.cts +20 -0
  46. package/dist/save/save-controller.d.ts +20 -0
  47. package/dist/save/save-controller.js +149 -0
  48. package/dist/save/unsaved-changes-warning.cjs +12 -0
  49. package/dist/save/unsaved-changes-warning.d.cts +6 -0
  50. package/dist/save/unsaved-changes-warning.d.ts +6 -0
  51. package/dist/save/unsaved-changes-warning.js +12 -0
  52. package/dist/storage/key-value-storage.cjs +35 -0
  53. package/dist/storage/key-value-storage.d.cts +14 -0
  54. package/dist/storage/key-value-storage.d.ts +14 -0
  55. package/dist/storage/key-value-storage.js +35 -0
  56. package/dist/storage/local-storage.cjs +24 -0
  57. package/dist/storage/local-storage.d.cts +8 -0
  58. package/dist/storage/local-storage.d.ts +8 -0
  59. package/dist/storage/local-storage.js +24 -0
  60. package/dist/storage/memory-storage.cjs +17 -0
  61. package/dist/storage/memory-storage.d.cts +4 -0
  62. package/dist/storage/memory-storage.d.ts +4 -0
  63. package/dist/storage/memory-storage.js +17 -0
  64. package/dist/storage/storage-contract.d.cts +14 -0
  65. package/dist/storage/storage-contract.d.ts +14 -0
  66. package/dist/storage.cjs +7 -0
  67. package/dist/storage.d.cts +5 -0
  68. package/dist/storage.d.ts +5 -0
  69. package/dist/storage.js +4 -0
  70. package/package.json +14 -3
@@ -1,3 +1,7 @@
1
+ const require_asset_manifest = require("../assets/asset-manifest.cjs");
2
+ const require_errors = require("./errors.cjs");
3
+ const require_image_check = require("../assets/image-check.cjs");
4
+ const require_asset_pipeline = require("../assets/asset-pipeline.cjs");
1
5
  require("../document/document-format.cjs");
2
6
  const require_ids = require("../document/ids.cjs");
3
7
  const require_create_document = require("../document/create-document.cjs");
@@ -5,20 +9,27 @@ const require_validate_document = require("../document/validate-document.cjs");
5
9
  const require_walk_objects = require("../fabric/walk-objects.cjs");
6
10
  const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
7
11
  const require_object_ids = require("../fabric/object-ids.cjs");
8
- const require_errors = require("./errors.cjs");
9
12
  const require_object_registry = require("../fabric/object-registry.cjs");
10
13
  const require_create_history = require("../history/create-history.cjs");
14
+ const require_save_controller = require("../save/save-controller.cjs");
11
15
  const require_event_emitter = require("./event-emitter.cjs");
12
16
  //#region src/engine/create-document-engine.ts
17
+ function isReplaceableImage(object) {
18
+ const candidate = object;
19
+ return typeof candidate.getSrc === "function" && typeof candidate.setSrc === "function";
20
+ }
13
21
  function describeDocumentId(value) {
14
22
  const id = value?.id;
15
23
  return typeof id === "string" ? id : void 0;
16
24
  }
17
25
  function createDocumentEngine(options) {
18
26
  const { canvas, storage } = options;
27
+ if (options.autosave && !storage) throw new require_errors.DocumentEngineError("STORAGE_MISSING", "Autosave needs a storage adapter passed to createDocumentEngine");
19
28
  const registry = require_object_registry.createObjectRegistry(options.customObjects);
20
29
  const events = require_event_emitter.createEventEmitter();
21
30
  const objectsById = /* @__PURE__ */ new Map();
31
+ const assetOptions = options.assets ?? {};
32
+ const uploadedUrls = /* @__PURE__ */ new Map();
22
33
  let documentInfo = require_create_document.createDocumentInfo(options.document);
23
34
  let activeLoad = null;
24
35
  let destroyed = false;
@@ -58,7 +69,23 @@ function createDocumentEngine(options) {
58
69
  rebuildIndex();
59
70
  return require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude()).objects;
60
71
  },
61
- onChange: (state) => events.emit("history:change", state)
72
+ onChange: (state) => events.emit("history:change", state),
73
+ onContentChange: () => saving.noteContentChange()
74
+ });
75
+ const saving = require_save_controller.createSaveController({
76
+ getStorage: () => requireStorage(),
77
+ createDocument: async () => {
78
+ const prepared = await require_asset_pipeline.prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
79
+ if (prepared.warnings.length > 0) events.emit("assets:warning", { warnings: prepared.warnings });
80
+ return prepared.document;
81
+ },
82
+ retry: options.saveRetry,
83
+ autosave: options.autosave === true ? {} : options.autosave ?? false,
84
+ onStateChange: (state) => events.emit("save:status", state),
85
+ onStart: (document) => events.emit("save:start", { document }),
86
+ onSuccess: (document) => events.emit("save:success", { document }),
87
+ onError: (error) => events.emit("save:error", { error }),
88
+ onRetry: (event) => events.emit("save:retry", event)
62
89
  });
63
90
  function toDocument() {
64
91
  ensureUsable();
@@ -73,11 +100,13 @@ function createDocumentEngine(options) {
73
100
  id: documentInfo.id,
74
101
  createdAt: documentInfo.createdAt,
75
102
  updatedAt: documentInfo.updatedAt,
103
+ revision: saving.state().revision,
76
104
  canvas: {
77
105
  width: canvas.getWidth(),
78
106
  height: canvas.getHeight()
79
107
  },
80
108
  objects: serialized.objects,
109
+ assets: require_asset_manifest.buildAssetManifest(serialized.objects),
81
110
  metadata: { ...documentInfo.metadata }
82
111
  };
83
112
  if (serialized.version !== void 0) document.fabricVersion = serialized.version;
@@ -85,8 +114,8 @@ function createDocumentEngine(options) {
85
114
  return document;
86
115
  }
87
116
  function toLoadError(error, signal) {
88
- if (require_errors.isDocumentEngineError(error)) return error;
89
117
  if (signal.aborted) return new require_errors.DocumentEngineError("LOAD_ABORTED", "Loading stopped because a newer load started", { cause: error });
118
+ if (require_errors.isDocumentEngineError(error)) return error;
90
119
  const reason = error instanceof Error ? error.message : String(error);
91
120
  return new require_errors.DocumentEngineError("LOAD_FAILED", `Fabric could not load the document: ${reason}`, { cause: error });
92
121
  }
@@ -108,7 +137,9 @@ function createDocumentEngine(options) {
108
137
  activeLoad = controller;
109
138
  events.emit("load:start", { documentId: describeDocumentId(input) });
110
139
  try {
111
- const document = checkDocument(input);
140
+ const checked = checkDocument(input);
141
+ const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal);
142
+ if (controller.signal.aborted) throw new Error("aborted");
112
143
  await history.withoutRecording(() => require_fabric_adapter.loadIntoCanvas(canvas, {
113
144
  version: document.fabricVersion,
114
145
  background: document.canvas.background,
@@ -127,8 +158,13 @@ function createDocumentEngine(options) {
127
158
  };
128
159
  rebuildIndex();
129
160
  history.reset();
161
+ saving.startSession(document.revision ?? 0);
130
162
  canvas.requestRenderAll();
131
- events.emit("load:success", { document });
163
+ if (warnings.length > 0) events.emit("assets:warning", { warnings });
164
+ events.emit("load:success", {
165
+ document,
166
+ warnings
167
+ });
132
168
  return document;
133
169
  } catch (error) {
134
170
  const engineError = toLoadError(error, controller.signal);
@@ -150,27 +186,19 @@ function createDocumentEngine(options) {
150
186
  stored = await source.loadDocument(documentId);
151
187
  } catch (error) {
152
188
  const reason = error instanceof Error ? error.message : String(error);
153
- const engineError = new require_errors.DocumentEngineError("LOAD_FAILED", `Storage could not load "${documentId}": ${reason}`, { cause: error });
189
+ const engineError = require_errors.isDocumentEngineError(error) ? error : new require_errors.DocumentEngineError("LOAD_FAILED", `Storage could not load "${documentId}": ${reason}`, { cause: error });
154
190
  events.emit("load:error", { error: engineError });
155
191
  throw engineError;
156
192
  }
157
193
  return loadDocument(stored, loadOptions);
158
194
  }
159
- async function save() {
160
- ensureUsable();
161
- const target = requireStorage();
162
- const document = toDocument();
163
- events.emit("save:start", { document });
195
+ function save(saveOptions) {
164
196
  try {
165
- await target.saveDocument(document);
197
+ ensureUsable();
166
198
  } catch (error) {
167
- const reason = error instanceof Error ? error.message : String(error);
168
- const engineError = new require_errors.DocumentEngineError("SAVE_FAILED", `Storage could not save the document: ${reason}`, { cause: error });
169
- events.emit("save:error", { error: engineError });
170
- throw engineError;
199
+ return Promise.reject(error);
171
200
  }
172
- events.emit("save:success", { document });
173
- return document;
201
+ return saving.save(saveOptions);
174
202
  }
175
203
  function newDocument(newOptions) {
176
204
  ensureUsable();
@@ -178,7 +206,9 @@ function createDocumentEngine(options) {
178
206
  history.withoutRecording(() => canvas.clear());
179
207
  objectsById.clear();
180
208
  documentInfo = require_create_document.createDocumentInfo(newOptions);
209
+ uploadedUrls.clear();
181
210
  history.reset();
211
+ saving.startSession(0);
182
212
  }
183
213
  function getObjectById(id) {
184
214
  ensureUsable();
@@ -187,6 +217,30 @@ function createDocumentEngine(options) {
187
217
  rebuildIndex();
188
218
  return objectsById.get(id);
189
219
  }
220
+ function checkAssets() {
221
+ ensureUsable();
222
+ return require_asset_pipeline.inspectAssets(toDocument(), assetOptions, new AbortController().signal);
223
+ }
224
+ async function replaceImage(oldUrl, newUrl) {
225
+ ensureUsable();
226
+ const images = [];
227
+ require_walk_objects.walkObjects(canvas.getObjects(), (object) => {
228
+ if (isReplaceableImage(object) && require_image_check.isSameUrl(object.getSrc(), oldUrl)) images.push(object);
229
+ });
230
+ if (images.length === 0) return 0;
231
+ await history.transaction("Replace image", () => Promise.all(images.map(async (image) => {
232
+ const displayedWidth = image.width * image.scaleX;
233
+ const displayedHeight = image.height * image.scaleY;
234
+ await image.setSrc(newUrl, { crossOrigin: image.crossOrigin ?? null });
235
+ image.set({
236
+ scaleX: displayedWidth / image.width,
237
+ scaleY: displayedHeight / image.height
238
+ });
239
+ image.setCoords();
240
+ })));
241
+ canvas.requestRenderAll();
242
+ return images.length;
243
+ }
190
244
  async function travelThroughHistory(direction) {
191
245
  ensureUsable();
192
246
  try {
@@ -203,6 +257,7 @@ function createDocumentEngine(options) {
203
257
  destroyed = true;
204
258
  activeLoad?.abort();
205
259
  history.destroy();
260
+ saving.destroy();
206
261
  canvas.off("object:added", handleObjectAdded);
207
262
  canvas.off("object:removed", handleObjectRemoved);
208
263
  objectsById.clear();
@@ -223,17 +278,26 @@ function createDocumentEngine(options) {
223
278
  ...changes
224
279
  }
225
280
  };
281
+ saving.noteContentChange();
226
282
  },
227
283
  newDocument,
228
284
  toDocument,
229
285
  loadDocument,
230
286
  load,
231
287
  save,
288
+ isDirty: () => saving.state().isDirty,
289
+ getSaveState: () => saving.state(),
232
290
  registerObject(definition) {
233
291
  ensureUsable();
234
292
  registry.register(definition);
235
293
  },
236
294
  getObjectById,
295
+ getAssetManifest: () => toDocument().assets ?? {
296
+ images: [],
297
+ fonts: []
298
+ },
299
+ checkAssets,
300
+ replaceImage,
237
301
  transaction(label, work) {
238
302
  ensureUsable();
239
303
  return history.transaction(label, work);
@@ -1,21 +1,26 @@
1
1
  import { DocumentInfo, FabricDocument } from "../document/document-format.cjs";
2
+ import { AssetManifest } from "../assets/asset-manifest.cjs";
3
+ import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.cjs";
2
4
  import { NewDocumentOptions } from "../document/create-document.cjs";
3
5
  import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
4
6
  import { HistoryOptions, HistoryState } from "../history/create-history.cjs";
7
+ import { AutosaveOptions } from "../save/autosave-scheduler.cjs";
8
+ import { RetryOptions } from "../save/retry.cjs";
5
9
  import { DocumentEngineError } from "./errors.cjs";
10
+ import { DocumentStorage } from "../storage/storage-contract.cjs";
11
+ import { SaveOptions, SaveRetryEvent, SaveState } from "../save/save-controller.cjs";
6
12
  import { Unsubscribe } from "./event-emitter.cjs";
7
13
  import { FabricObject, StaticCanvas } from "fabric";
8
14
  //#region src/engine/create-document-engine.d.ts
9
- export interface DocumentStorage {
10
- loadDocument(id: string): Promise<unknown>;
11
- saveDocument(document: FabricDocument): Promise<void>;
12
- }
13
15
  export interface DocumentEngineOptions {
14
16
  canvas: StaticCanvas;
15
17
  storage?: DocumentStorage;
16
18
  customObjects?: CustomObjectDefinition[];
17
19
  document?: NewDocumentOptions;
18
20
  history?: HistoryOptions;
21
+ autosave?: boolean | AutosaveOptions;
22
+ saveRetry?: RetryOptions;
23
+ assets?: AssetOptions;
19
24
  }
20
25
  export interface LoadOptions {
21
26
  restoreCanvasSize?: boolean;
@@ -26,6 +31,10 @@ export interface DocumentEngineEvents {
26
31
  };
27
32
  "load:success": {
28
33
  document: FabricDocument;
34
+ warnings: AssetWarning[];
35
+ };
36
+ "assets:warning": {
37
+ warnings: AssetWarning[];
29
38
  };
30
39
  "load:error": {
31
40
  error: DocumentEngineError;
@@ -39,6 +48,8 @@ export interface DocumentEngineEvents {
39
48
  "save:error": {
40
49
  error: DocumentEngineError;
41
50
  };
51
+ "save:retry": SaveRetryEvent;
52
+ "save:status": SaveState;
42
53
  "history:change": HistoryState;
43
54
  "history:error": {
44
55
  error: DocumentEngineError;
@@ -52,7 +63,9 @@ export interface DocumentEngine {
52
63
  toDocument(): FabricDocument;
53
64
  loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
54
65
  load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
55
- save(): Promise<FabricDocument>;
66
+ save(options?: SaveOptions): Promise<FabricDocument>;
67
+ isDirty(): boolean;
68
+ getSaveState(): SaveState;
56
69
  registerObject(definition: CustomObjectDefinition): void;
57
70
  transaction<Result>(label: string, work: () => Result): Result;
58
71
  commit(label?: string): boolean;
@@ -66,6 +79,9 @@ export interface DocumentEngine {
66
79
  };
67
80
  clearHistory(): void;
68
81
  getObjectById(id: string): FabricObject | undefined;
82
+ getAssetManifest(): AssetManifest;
83
+ checkAssets(): Promise<AssetReport>;
84
+ replaceImage(oldUrl: string, newUrl: string): Promise<number>;
69
85
  on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
70
86
  destroy(): void;
71
87
  }
@@ -1,21 +1,26 @@
1
1
  import { DocumentInfo, FabricDocument } from "../document/document-format.js";
2
+ import { AssetManifest } from "../assets/asset-manifest.js";
3
+ import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.js";
2
4
  import { NewDocumentOptions } from "../document/create-document.js";
3
5
  import { CustomObjectDefinition } from "../fabric/object-registry.js";
4
6
  import { HistoryOptions, HistoryState } from "../history/create-history.js";
7
+ import { AutosaveOptions } from "../save/autosave-scheduler.js";
8
+ import { RetryOptions } from "../save/retry.js";
5
9
  import { DocumentEngineError } from "./errors.js";
10
+ import { DocumentStorage } from "../storage/storage-contract.js";
11
+ import { SaveOptions, SaveRetryEvent, SaveState } from "../save/save-controller.js";
6
12
  import { Unsubscribe } from "./event-emitter.js";
7
13
  import { FabricObject, StaticCanvas } from "fabric";
8
14
  //#region src/engine/create-document-engine.d.ts
9
- export interface DocumentStorage {
10
- loadDocument(id: string): Promise<unknown>;
11
- saveDocument(document: FabricDocument): Promise<void>;
12
- }
13
15
  export interface DocumentEngineOptions {
14
16
  canvas: StaticCanvas;
15
17
  storage?: DocumentStorage;
16
18
  customObjects?: CustomObjectDefinition[];
17
19
  document?: NewDocumentOptions;
18
20
  history?: HistoryOptions;
21
+ autosave?: boolean | AutosaveOptions;
22
+ saveRetry?: RetryOptions;
23
+ assets?: AssetOptions;
19
24
  }
20
25
  export interface LoadOptions {
21
26
  restoreCanvasSize?: boolean;
@@ -26,6 +31,10 @@ export interface DocumentEngineEvents {
26
31
  };
27
32
  "load:success": {
28
33
  document: FabricDocument;
34
+ warnings: AssetWarning[];
35
+ };
36
+ "assets:warning": {
37
+ warnings: AssetWarning[];
29
38
  };
30
39
  "load:error": {
31
40
  error: DocumentEngineError;
@@ -39,6 +48,8 @@ export interface DocumentEngineEvents {
39
48
  "save:error": {
40
49
  error: DocumentEngineError;
41
50
  };
51
+ "save:retry": SaveRetryEvent;
52
+ "save:status": SaveState;
42
53
  "history:change": HistoryState;
43
54
  "history:error": {
44
55
  error: DocumentEngineError;
@@ -52,7 +63,9 @@ export interface DocumentEngine {
52
63
  toDocument(): FabricDocument;
53
64
  loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
54
65
  load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
55
- save(): Promise<FabricDocument>;
66
+ save(options?: SaveOptions): Promise<FabricDocument>;
67
+ isDirty(): boolean;
68
+ getSaveState(): SaveState;
56
69
  registerObject(definition: CustomObjectDefinition): void;
57
70
  transaction<Result>(label: string, work: () => Result): Result;
58
71
  commit(label?: string): boolean;
@@ -66,6 +79,9 @@ export interface DocumentEngine {
66
79
  };
67
80
  clearHistory(): void;
68
81
  getObjectById(id: string): FabricObject | undefined;
82
+ getAssetManifest(): AssetManifest;
83
+ checkAssets(): Promise<AssetReport>;
84
+ replaceImage(oldUrl: string, newUrl: string): Promise<number>;
69
85
  on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
70
86
  destroy(): void;
71
87
  }
@@ -1,3 +1,7 @@
1
+ import { buildAssetManifest } from "../assets/asset-manifest.js";
2
+ import { DocumentEngineError, isDocumentEngineError } from "./errors.js";
3
+ import { isSameUrl } from "../assets/image-check.js";
4
+ import { inspectAssets, prepareAssetsForLoad, prepareAssetsForSave } from "../assets/asset-pipeline.js";
1
5
  import "../document/document-format.js";
2
6
  import { createId } from "../document/ids.js";
3
7
  import { createDocumentInfo } from "../document/create-document.js";
@@ -5,20 +9,27 @@ import { validateDocument } from "../document/validate-document.js";
5
9
  import { collectSerializedTypes, walkObjects } from "../fabric/walk-objects.js";
6
10
  import { loadIntoCanvas, serializeCanvas } from "../fabric/fabric-adapter.js";
7
11
  import { readObjectId, writeObjectId } from "../fabric/object-ids.js";
8
- import { DocumentEngineError, isDocumentEngineError } from "./errors.js";
9
12
  import { createObjectRegistry } from "../fabric/object-registry.js";
10
13
  import { createHistory } from "../history/create-history.js";
14
+ import { createSaveController } from "../save/save-controller.js";
11
15
  import { createEventEmitter } from "./event-emitter.js";
12
16
  //#region src/engine/create-document-engine.ts
17
+ function isReplaceableImage(object) {
18
+ const candidate = object;
19
+ return typeof candidate.getSrc === "function" && typeof candidate.setSrc === "function";
20
+ }
13
21
  function describeDocumentId(value) {
14
22
  const id = value?.id;
15
23
  return typeof id === "string" ? id : void 0;
16
24
  }
17
25
  function createDocumentEngine(options) {
18
26
  const { canvas, storage } = options;
27
+ if (options.autosave && !storage) throw new DocumentEngineError("STORAGE_MISSING", "Autosave needs a storage adapter passed to createDocumentEngine");
19
28
  const registry = createObjectRegistry(options.customObjects);
20
29
  const events = createEventEmitter();
21
30
  const objectsById = /* @__PURE__ */ new Map();
31
+ const assetOptions = options.assets ?? {};
32
+ const uploadedUrls = /* @__PURE__ */ new Map();
22
33
  let documentInfo = createDocumentInfo(options.document);
23
34
  let activeLoad = null;
24
35
  let destroyed = false;
@@ -58,7 +69,23 @@ function createDocumentEngine(options) {
58
69
  rebuildIndex();
59
70
  return serializeCanvas(canvas, registry.propertiesToInclude()).objects;
60
71
  },
61
- onChange: (state) => events.emit("history:change", state)
72
+ onChange: (state) => events.emit("history:change", state),
73
+ onContentChange: () => saving.noteContentChange()
74
+ });
75
+ const saving = createSaveController({
76
+ getStorage: () => requireStorage(),
77
+ createDocument: async () => {
78
+ const prepared = await prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
79
+ if (prepared.warnings.length > 0) events.emit("assets:warning", { warnings: prepared.warnings });
80
+ return prepared.document;
81
+ },
82
+ retry: options.saveRetry,
83
+ autosave: options.autosave === true ? {} : options.autosave ?? false,
84
+ onStateChange: (state) => events.emit("save:status", state),
85
+ onStart: (document) => events.emit("save:start", { document }),
86
+ onSuccess: (document) => events.emit("save:success", { document }),
87
+ onError: (error) => events.emit("save:error", { error }),
88
+ onRetry: (event) => events.emit("save:retry", event)
62
89
  });
63
90
  function toDocument() {
64
91
  ensureUsable();
@@ -73,11 +100,13 @@ function createDocumentEngine(options) {
73
100
  id: documentInfo.id,
74
101
  createdAt: documentInfo.createdAt,
75
102
  updatedAt: documentInfo.updatedAt,
103
+ revision: saving.state().revision,
76
104
  canvas: {
77
105
  width: canvas.getWidth(),
78
106
  height: canvas.getHeight()
79
107
  },
80
108
  objects: serialized.objects,
109
+ assets: buildAssetManifest(serialized.objects),
81
110
  metadata: { ...documentInfo.metadata }
82
111
  };
83
112
  if (serialized.version !== void 0) document.fabricVersion = serialized.version;
@@ -85,8 +114,8 @@ function createDocumentEngine(options) {
85
114
  return document;
86
115
  }
87
116
  function toLoadError(error, signal) {
88
- if (isDocumentEngineError(error)) return error;
89
117
  if (signal.aborted) return new DocumentEngineError("LOAD_ABORTED", "Loading stopped because a newer load started", { cause: error });
118
+ if (isDocumentEngineError(error)) return error;
90
119
  const reason = error instanceof Error ? error.message : String(error);
91
120
  return new DocumentEngineError("LOAD_FAILED", `Fabric could not load the document: ${reason}`, { cause: error });
92
121
  }
@@ -108,7 +137,9 @@ function createDocumentEngine(options) {
108
137
  activeLoad = controller;
109
138
  events.emit("load:start", { documentId: describeDocumentId(input) });
110
139
  try {
111
- const document = checkDocument(input);
140
+ const checked = checkDocument(input);
141
+ const { document, warnings } = await prepareAssetsForLoad(checked, assetOptions, controller.signal);
142
+ if (controller.signal.aborted) throw new Error("aborted");
112
143
  await history.withoutRecording(() => loadIntoCanvas(canvas, {
113
144
  version: document.fabricVersion,
114
145
  background: document.canvas.background,
@@ -127,8 +158,13 @@ function createDocumentEngine(options) {
127
158
  };
128
159
  rebuildIndex();
129
160
  history.reset();
161
+ saving.startSession(document.revision ?? 0);
130
162
  canvas.requestRenderAll();
131
- events.emit("load:success", { document });
163
+ if (warnings.length > 0) events.emit("assets:warning", { warnings });
164
+ events.emit("load:success", {
165
+ document,
166
+ warnings
167
+ });
132
168
  return document;
133
169
  } catch (error) {
134
170
  const engineError = toLoadError(error, controller.signal);
@@ -150,27 +186,19 @@ function createDocumentEngine(options) {
150
186
  stored = await source.loadDocument(documentId);
151
187
  } catch (error) {
152
188
  const reason = error instanceof Error ? error.message : String(error);
153
- const engineError = new DocumentEngineError("LOAD_FAILED", `Storage could not load "${documentId}": ${reason}`, { cause: error });
189
+ const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("LOAD_FAILED", `Storage could not load "${documentId}": ${reason}`, { cause: error });
154
190
  events.emit("load:error", { error: engineError });
155
191
  throw engineError;
156
192
  }
157
193
  return loadDocument(stored, loadOptions);
158
194
  }
159
- async function save() {
160
- ensureUsable();
161
- const target = requireStorage();
162
- const document = toDocument();
163
- events.emit("save:start", { document });
195
+ function save(saveOptions) {
164
196
  try {
165
- await target.saveDocument(document);
197
+ ensureUsable();
166
198
  } catch (error) {
167
- const reason = error instanceof Error ? error.message : String(error);
168
- const engineError = new DocumentEngineError("SAVE_FAILED", `Storage could not save the document: ${reason}`, { cause: error });
169
- events.emit("save:error", { error: engineError });
170
- throw engineError;
199
+ return Promise.reject(error);
171
200
  }
172
- events.emit("save:success", { document });
173
- return document;
201
+ return saving.save(saveOptions);
174
202
  }
175
203
  function newDocument(newOptions) {
176
204
  ensureUsable();
@@ -178,7 +206,9 @@ function createDocumentEngine(options) {
178
206
  history.withoutRecording(() => canvas.clear());
179
207
  objectsById.clear();
180
208
  documentInfo = createDocumentInfo(newOptions);
209
+ uploadedUrls.clear();
181
210
  history.reset();
211
+ saving.startSession(0);
182
212
  }
183
213
  function getObjectById(id) {
184
214
  ensureUsable();
@@ -187,6 +217,30 @@ function createDocumentEngine(options) {
187
217
  rebuildIndex();
188
218
  return objectsById.get(id);
189
219
  }
220
+ function checkAssets() {
221
+ ensureUsable();
222
+ return inspectAssets(toDocument(), assetOptions, new AbortController().signal);
223
+ }
224
+ async function replaceImage(oldUrl, newUrl) {
225
+ ensureUsable();
226
+ const images = [];
227
+ walkObjects(canvas.getObjects(), (object) => {
228
+ if (isReplaceableImage(object) && isSameUrl(object.getSrc(), oldUrl)) images.push(object);
229
+ });
230
+ if (images.length === 0) return 0;
231
+ await history.transaction("Replace image", () => Promise.all(images.map(async (image) => {
232
+ const displayedWidth = image.width * image.scaleX;
233
+ const displayedHeight = image.height * image.scaleY;
234
+ await image.setSrc(newUrl, { crossOrigin: image.crossOrigin ?? null });
235
+ image.set({
236
+ scaleX: displayedWidth / image.width,
237
+ scaleY: displayedHeight / image.height
238
+ });
239
+ image.setCoords();
240
+ })));
241
+ canvas.requestRenderAll();
242
+ return images.length;
243
+ }
190
244
  async function travelThroughHistory(direction) {
191
245
  ensureUsable();
192
246
  try {
@@ -203,6 +257,7 @@ function createDocumentEngine(options) {
203
257
  destroyed = true;
204
258
  activeLoad?.abort();
205
259
  history.destroy();
260
+ saving.destroy();
206
261
  canvas.off("object:added", handleObjectAdded);
207
262
  canvas.off("object:removed", handleObjectRemoved);
208
263
  objectsById.clear();
@@ -223,17 +278,26 @@ function createDocumentEngine(options) {
223
278
  ...changes
224
279
  }
225
280
  };
281
+ saving.noteContentChange();
226
282
  },
227
283
  newDocument,
228
284
  toDocument,
229
285
  loadDocument,
230
286
  load,
231
287
  save,
288
+ isDirty: () => saving.state().isDirty,
289
+ getSaveState: () => saving.state(),
232
290
  registerObject(definition) {
233
291
  ensureUsable();
234
292
  registry.register(definition);
235
293
  },
236
294
  getObjectById,
295
+ getAssetManifest: () => toDocument().assets ?? {
296
+ images: [],
297
+ fonts: []
298
+ },
299
+ checkAssets,
300
+ replaceImage,
237
301
  transaction(label, work) {
238
302
  ensureUsable();
239
303
  return history.transaction(label, work);
@@ -6,12 +6,19 @@ var DocumentEngineError = class extends Error {
6
6
  this.code = code;
7
7
  this.issues = details.issues ?? [];
8
8
  this.unknownTypes = details.unknownTypes ?? [];
9
+ this.missingAssets = details.missingAssets ?? [];
10
+ this.missingFonts = details.missingFonts ?? [];
9
11
  this.cause = details.cause;
12
+ this.retryable = details.retryable ?? false;
10
13
  }
11
14
  };
12
15
  function isDocumentEngineError(value) {
13
16
  return value instanceof DocumentEngineError;
14
17
  }
18
+ function createConflictError(documentId, expectedRevision, actualRevision) {
19
+ return new DocumentEngineError("SAVE_CONFLICT", `Document "${documentId}" was saved somewhere else: expected revision ${expectedRevision} but storage has ${actualRevision}`);
20
+ }
15
21
  //#endregion
16
22
  exports.DocumentEngineError = DocumentEngineError;
23
+ exports.createConflictError = createConflictError;
17
24
  exports.isDocumentEngineError = isDocumentEngineError;
@@ -1,5 +1,6 @@
1
+ import { FontAsset, ImageAsset } from "../assets/asset-manifest.cjs";
1
2
  //#region src/engine/errors.d.ts
2
- export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
3
+ export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
3
4
  export interface DocumentIssue {
4
5
  code: DocumentErrorCode;
5
6
  path: string;
@@ -8,14 +9,21 @@ export interface DocumentIssue {
8
9
  export interface DocumentEngineErrorDetails {
9
10
  issues?: DocumentIssue[];
10
11
  unknownTypes?: string[];
12
+ missingAssets?: ImageAsset[];
13
+ missingFonts?: FontAsset[];
11
14
  cause?: unknown;
15
+ retryable?: boolean;
12
16
  }
13
17
  export declare class DocumentEngineError extends Error {
14
18
  readonly code: DocumentErrorCode;
15
19
  readonly issues: DocumentIssue[];
16
20
  readonly unknownTypes: string[];
21
+ readonly missingAssets: ImageAsset[];
22
+ readonly missingFonts: FontAsset[];
17
23
  readonly cause: unknown;
24
+ readonly retryable: boolean;
18
25
  constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
19
26
  }
20
27
  export declare function isDocumentEngineError(value: unknown): value is DocumentEngineError;
28
+ export declare function createConflictError(documentId: string, expectedRevision: number, actualRevision: number): DocumentEngineError;
21
29
  //#endregion
@@ -1,5 +1,6 @@
1
+ import { FontAsset, ImageAsset } from "../assets/asset-manifest.js";
1
2
  //#region src/engine/errors.d.ts
2
- export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
3
+ export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
3
4
  export interface DocumentIssue {
4
5
  code: DocumentErrorCode;
5
6
  path: string;
@@ -8,14 +9,21 @@ export interface DocumentIssue {
8
9
  export interface DocumentEngineErrorDetails {
9
10
  issues?: DocumentIssue[];
10
11
  unknownTypes?: string[];
12
+ missingAssets?: ImageAsset[];
13
+ missingFonts?: FontAsset[];
11
14
  cause?: unknown;
15
+ retryable?: boolean;
12
16
  }
13
17
  export declare class DocumentEngineError extends Error {
14
18
  readonly code: DocumentErrorCode;
15
19
  readonly issues: DocumentIssue[];
16
20
  readonly unknownTypes: string[];
21
+ readonly missingAssets: ImageAsset[];
22
+ readonly missingFonts: FontAsset[];
17
23
  readonly cause: unknown;
24
+ readonly retryable: boolean;
18
25
  constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
19
26
  }
20
27
  export declare function isDocumentEngineError(value: unknown): value is DocumentEngineError;
28
+ export declare function createConflictError(documentId: string, expectedRevision: number, actualRevision: number): DocumentEngineError;
21
29
  //#endregion
@@ -6,11 +6,17 @@ var DocumentEngineError = class extends Error {
6
6
  this.code = code;
7
7
  this.issues = details.issues ?? [];
8
8
  this.unknownTypes = details.unknownTypes ?? [];
9
+ this.missingAssets = details.missingAssets ?? [];
10
+ this.missingFonts = details.missingFonts ?? [];
9
11
  this.cause = details.cause;
12
+ this.retryable = details.retryable ?? false;
10
13
  }
11
14
  };
12
15
  function isDocumentEngineError(value) {
13
16
  return value instanceof DocumentEngineError;
14
17
  }
18
+ function createConflictError(documentId, expectedRevision, actualRevision) {
19
+ return new DocumentEngineError("SAVE_CONFLICT", `Document "${documentId}" was saved somewhere else: expected revision ${expectedRevision} but storage has ${actualRevision}`);
20
+ }
15
21
  //#endregion
16
- export { DocumentEngineError, isDocumentEngineError };
22
+ export { DocumentEngineError, createConflictError, isDocumentEngineError };