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.
- package/README.md +162 -21
- package/dist/assets/asset-manifest.cjs +43 -0
- package/dist/assets/asset-manifest.d.cts +16 -0
- package/dist/assets/asset-manifest.d.ts +16 -0
- package/dist/assets/asset-manifest.js +41 -0
- package/dist/assets/asset-pipeline.cjs +174 -0
- package/dist/assets/asset-pipeline.d.cts +31 -0
- package/dist/assets/asset-pipeline.d.ts +31 -0
- package/dist/assets/asset-pipeline.js +172 -0
- package/dist/assets/asset-references.cjs +83 -0
- package/dist/assets/asset-references.js +82 -0
- package/dist/assets/font-check.cjs +59 -0
- package/dist/assets/font-check.d.cts +4 -0
- package/dist/assets/font-check.d.ts +4 -0
- package/dist/assets/font-check.js +57 -0
- package/dist/assets/image-check.cjs +36 -0
- package/dist/assets/image-check.js +33 -0
- package/dist/document/document-format.d.cts +3 -0
- package/dist/document/document-format.d.ts +3 -0
- package/dist/document/validate-document.cjs +5 -0
- package/dist/document/validate-document.js +5 -0
- package/dist/engine/create-document-engine.cjs +82 -18
- package/dist/engine/create-document-engine.d.cts +21 -5
- package/dist/engine/create-document-engine.d.ts +21 -5
- package/dist/engine/create-document-engine.js +82 -18
- package/dist/engine/errors.cjs +7 -0
- package/dist/engine/errors.d.cts +9 -1
- package/dist/engine/errors.d.ts +9 -1
- package/dist/engine/errors.js +7 -1
- package/dist/history/create-history.cjs +3 -1
- package/dist/history/create-history.js +3 -1
- package/dist/index.cjs +4 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +3 -2
- package/dist/save/autosave-scheduler.cjs +28 -0
- package/dist/save/autosave-scheduler.d.cts +6 -0
- package/dist/save/autosave-scheduler.d.ts +6 -0
- package/dist/save/autosave-scheduler.js +28 -0
- package/dist/save/retry.cjs +33 -0
- package/dist/save/retry.d.cts +7 -0
- package/dist/save/retry.d.ts +7 -0
- package/dist/save/retry.js +31 -0
- package/dist/save/save-controller.cjs +149 -0
- package/dist/save/save-controller.d.cts +20 -0
- package/dist/save/save-controller.d.ts +20 -0
- package/dist/save/save-controller.js +149 -0
- package/dist/save/unsaved-changes-warning.cjs +12 -0
- package/dist/save/unsaved-changes-warning.d.cts +6 -0
- package/dist/save/unsaved-changes-warning.d.ts +6 -0
- package/dist/save/unsaved-changes-warning.js +12 -0
- package/dist/storage/key-value-storage.cjs +35 -0
- package/dist/storage/key-value-storage.d.cts +14 -0
- package/dist/storage/key-value-storage.d.ts +14 -0
- package/dist/storage/key-value-storage.js +35 -0
- package/dist/storage/local-storage.cjs +24 -0
- package/dist/storage/local-storage.d.cts +8 -0
- package/dist/storage/local-storage.d.ts +8 -0
- package/dist/storage/local-storage.js +24 -0
- package/dist/storage/memory-storage.cjs +17 -0
- package/dist/storage/memory-storage.d.cts +4 -0
- package/dist/storage/memory-storage.d.ts +4 -0
- package/dist/storage/memory-storage.js +17 -0
- package/dist/storage/storage-contract.d.cts +14 -0
- package/dist/storage/storage-contract.d.ts +14 -0
- package/dist/storage.cjs +7 -0
- package/dist/storage.d.cts +5 -0
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +4 -0
- 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
|
|
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("
|
|
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
|
-
|
|
160
|
-
ensureUsable();
|
|
161
|
-
const target = requireStorage();
|
|
162
|
-
const document = toDocument();
|
|
163
|
-
events.emit("save:start", { document });
|
|
195
|
+
function save(saveOptions) {
|
|
164
196
|
try {
|
|
165
|
-
|
|
197
|
+
ensureUsable();
|
|
166
198
|
} catch (error) {
|
|
167
|
-
|
|
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
|
-
|
|
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
|
|
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("
|
|
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
|
-
|
|
160
|
-
ensureUsable();
|
|
161
|
-
const target = requireStorage();
|
|
162
|
-
const document = toDocument();
|
|
163
|
-
events.emit("save:start", { document });
|
|
195
|
+
function save(saveOptions) {
|
|
164
196
|
try {
|
|
165
|
-
|
|
197
|
+
ensureUsable();
|
|
166
198
|
} catch (error) {
|
|
167
|
-
|
|
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
|
-
|
|
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);
|
package/dist/engine/errors.cjs
CHANGED
|
@@ -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;
|
package/dist/engine/errors.d.cts
CHANGED
|
@@ -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
|
package/dist/engine/errors.d.ts
CHANGED
|
@@ -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
|
package/dist/engine/errors.js
CHANGED
|
@@ -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 };
|