fabricjs-document-engine 0.2.0 → 0.4.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 +113 -13
- 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 +175 -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 +2 -0
- package/dist/document/document-format.d.ts +2 -0
- package/dist/document/validate-document.cjs +4 -0
- package/dist/document/validate-document.js +4 -0
- package/dist/engine/create-document-engine.cjs +112 -10
- package/dist/engine/create-document-engine.d.cts +28 -0
- package/dist/engine/create-document-engine.d.ts +28 -0
- package/dist/engine/create-document-engine.js +112 -10
- package/dist/engine/errors.cjs +2 -0
- package/dist/engine/errors.d.cts +6 -1
- package/dist/engine/errors.d.ts +6 -1
- package/dist/engine/errors.js +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1 -1
- package/dist/recovery/indexed-db-recovery.cjs +80 -0
- package/dist/recovery/indexed-db-recovery.d.cts +8 -0
- package/dist/recovery/indexed-db-recovery.d.ts +8 -0
- package/dist/recovery/indexed-db-recovery.js +80 -0
- package/dist/recovery/recovery-controller.cjs +159 -0
- package/dist/recovery/recovery-controller.d.cts +19 -0
- package/dist/recovery/recovery-controller.d.ts +19 -0
- package/dist/recovery/recovery-controller.js +158 -0
- package/dist/recovery/recovery-store.cjs +19 -0
- package/dist/recovery/recovery-store.d.cts +10 -0
- package/dist/recovery/recovery-store.d.ts +10 -0
- package/dist/recovery/recovery-store.js +19 -0
- package/dist/recovery.cjs +5 -0
- package/dist/recovery.d.cts +4 -0
- package/dist/recovery.d.ts +4 -0
- package/dist/recovery.js +3 -0
- package/dist/save/save-controller.cjs +6 -5
- package/dist/save/save-controller.js +6 -5
- package/package.json +13 -2
|
@@ -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,12 +9,16 @@ 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_recovery_controller = require("../recovery/recovery-controller.cjs");
|
|
11
15
|
const require_save_controller = require("../save/save-controller.cjs");
|
|
12
16
|
const require_event_emitter = require("./event-emitter.cjs");
|
|
13
17
|
//#region src/engine/create-document-engine.ts
|
|
18
|
+
function isReplaceableImage(object) {
|
|
19
|
+
const candidate = object;
|
|
20
|
+
return typeof candidate.getSrc === "function" && typeof candidate.setSrc === "function";
|
|
21
|
+
}
|
|
14
22
|
function describeDocumentId(value) {
|
|
15
23
|
const id = value?.id;
|
|
16
24
|
return typeof id === "string" ? id : void 0;
|
|
@@ -21,6 +29,8 @@ function createDocumentEngine(options) {
|
|
|
21
29
|
const registry = require_object_registry.createObjectRegistry(options.customObjects);
|
|
22
30
|
const events = require_event_emitter.createEventEmitter();
|
|
23
31
|
const objectsById = /* @__PURE__ */ new Map();
|
|
32
|
+
const assetOptions = options.assets ?? {};
|
|
33
|
+
const uploadedUrls = /* @__PURE__ */ new Map();
|
|
24
34
|
let documentInfo = require_create_document.createDocumentInfo(options.document);
|
|
25
35
|
let activeLoad = null;
|
|
26
36
|
let destroyed = false;
|
|
@@ -61,19 +71,43 @@ function createDocumentEngine(options) {
|
|
|
61
71
|
return require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude()).objects;
|
|
62
72
|
},
|
|
63
73
|
onChange: (state) => events.emit("history:change", state),
|
|
64
|
-
onContentChange: () =>
|
|
74
|
+
onContentChange: () => noteContentChange()
|
|
65
75
|
});
|
|
66
76
|
const saving = require_save_controller.createSaveController({
|
|
67
77
|
getStorage: () => requireStorage(),
|
|
68
|
-
createDocument: () =>
|
|
78
|
+
createDocument: async () => {
|
|
79
|
+
const prepared = await require_asset_pipeline.prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
|
|
80
|
+
if (prepared.warnings.length > 0) events.emit("assets:warning", { warnings: prepared.warnings });
|
|
81
|
+
return prepared.document;
|
|
82
|
+
},
|
|
69
83
|
retry: options.saveRetry,
|
|
70
84
|
autosave: options.autosave === true ? {} : options.autosave ?? false,
|
|
71
85
|
onStateChange: (state) => events.emit("save:status", state),
|
|
72
86
|
onStart: (document) => events.emit("save:start", { document }),
|
|
73
|
-
onSuccess: (document) =>
|
|
87
|
+
onSuccess: (document) => {
|
|
88
|
+
if (recovery && !saving.state().isDirty) recovery.remove(document.id);
|
|
89
|
+
events.emit("save:success", { document });
|
|
90
|
+
},
|
|
74
91
|
onError: (error) => events.emit("save:error", { error }),
|
|
75
92
|
onRetry: (event) => events.emit("save:retry", event)
|
|
76
93
|
});
|
|
94
|
+
const recovery = options.recovery ? require_recovery_controller.createRecoveryController({
|
|
95
|
+
...options.recovery,
|
|
96
|
+
createDocument: () => toDocument(),
|
|
97
|
+
shouldWrite: () => !destroyed && saving.state().isDirty,
|
|
98
|
+
onCheckpoint: (record) => events.emit("recovery:checkpoint", {
|
|
99
|
+
documentId: record.documentId,
|
|
100
|
+
savedAt: record.savedAt
|
|
101
|
+
}),
|
|
102
|
+
onError: (error) => {
|
|
103
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
104
|
+
events.emit("recovery:error", { error: new require_errors.DocumentEngineError("RECOVERY_FAILED", `Could not write the recovery copy: ${reason}`, { cause: error }) });
|
|
105
|
+
}
|
|
106
|
+
}) : void 0;
|
|
107
|
+
function noteContentChange() {
|
|
108
|
+
saving.noteContentChange();
|
|
109
|
+
recovery?.schedule();
|
|
110
|
+
}
|
|
77
111
|
function toDocument() {
|
|
78
112
|
ensureUsable();
|
|
79
113
|
rebuildIndex();
|
|
@@ -93,6 +127,7 @@ function createDocumentEngine(options) {
|
|
|
93
127
|
height: canvas.getHeight()
|
|
94
128
|
},
|
|
95
129
|
objects: serialized.objects,
|
|
130
|
+
assets: require_asset_manifest.buildAssetManifest(serialized.objects),
|
|
96
131
|
metadata: { ...documentInfo.metadata }
|
|
97
132
|
};
|
|
98
133
|
if (serialized.version !== void 0) document.fabricVersion = serialized.version;
|
|
@@ -100,8 +135,8 @@ function createDocumentEngine(options) {
|
|
|
100
135
|
return document;
|
|
101
136
|
}
|
|
102
137
|
function toLoadError(error, signal) {
|
|
103
|
-
if (require_errors.isDocumentEngineError(error)) return error;
|
|
104
138
|
if (signal.aborted) return new require_errors.DocumentEngineError("LOAD_ABORTED", "Loading stopped because a newer load started", { cause: error });
|
|
139
|
+
if (require_errors.isDocumentEngineError(error)) return error;
|
|
105
140
|
const reason = error instanceof Error ? error.message : String(error);
|
|
106
141
|
return new require_errors.DocumentEngineError("LOAD_FAILED", `Fabric could not load the document: ${reason}`, { cause: error });
|
|
107
142
|
}
|
|
@@ -121,9 +156,14 @@ function createDocumentEngine(options) {
|
|
|
121
156
|
activeLoad?.abort();
|
|
122
157
|
const controller = new AbortController();
|
|
123
158
|
activeLoad = controller;
|
|
124
|
-
|
|
159
|
+
const documentId = describeDocumentId(input);
|
|
160
|
+
events.emit("load:start", { documentId });
|
|
125
161
|
try {
|
|
126
|
-
|
|
162
|
+
await recovery?.markLoadStarted(documentId);
|
|
163
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
164
|
+
const checked = checkDocument(input);
|
|
165
|
+
const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal);
|
|
166
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
127
167
|
await history.withoutRecording(() => require_fabric_adapter.loadIntoCanvas(canvas, {
|
|
128
168
|
version: document.fabricVersion,
|
|
129
169
|
background: document.canvas.background,
|
|
@@ -142,16 +182,24 @@ function createDocumentEngine(options) {
|
|
|
142
182
|
};
|
|
143
183
|
rebuildIndex();
|
|
144
184
|
history.reset();
|
|
185
|
+
recovery?.cancel();
|
|
145
186
|
saving.startSession(document.revision ?? 0);
|
|
146
187
|
canvas.requestRenderAll();
|
|
147
|
-
events.emit("
|
|
188
|
+
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
189
|
+
events.emit("load:success", {
|
|
190
|
+
document,
|
|
191
|
+
warnings
|
|
192
|
+
});
|
|
148
193
|
return document;
|
|
149
194
|
} catch (error) {
|
|
150
195
|
const engineError = toLoadError(error, controller.signal);
|
|
151
196
|
events.emit("load:error", { error: engineError });
|
|
152
197
|
throw engineError;
|
|
153
198
|
} finally {
|
|
154
|
-
if (activeLoad === controller)
|
|
199
|
+
if (activeLoad === controller) {
|
|
200
|
+
activeLoad = null;
|
|
201
|
+
await recovery?.markLoadFinished();
|
|
202
|
+
}
|
|
155
203
|
}
|
|
156
204
|
}
|
|
157
205
|
function requireStorage() {
|
|
@@ -186,7 +234,9 @@ function createDocumentEngine(options) {
|
|
|
186
234
|
history.withoutRecording(() => canvas.clear());
|
|
187
235
|
objectsById.clear();
|
|
188
236
|
documentInfo = require_create_document.createDocumentInfo(newOptions);
|
|
237
|
+
uploadedUrls.clear();
|
|
189
238
|
history.reset();
|
|
239
|
+
recovery?.cancel();
|
|
190
240
|
saving.startSession(0);
|
|
191
241
|
}
|
|
192
242
|
function getObjectById(id) {
|
|
@@ -196,6 +246,45 @@ function createDocumentEngine(options) {
|
|
|
196
246
|
rebuildIndex();
|
|
197
247
|
return objectsById.get(id);
|
|
198
248
|
}
|
|
249
|
+
function checkAssets() {
|
|
250
|
+
ensureUsable();
|
|
251
|
+
return require_asset_pipeline.inspectAssets(toDocument(), assetOptions, new AbortController().signal);
|
|
252
|
+
}
|
|
253
|
+
async function replaceImage(oldUrl, newUrl) {
|
|
254
|
+
ensureUsable();
|
|
255
|
+
const images = [];
|
|
256
|
+
require_walk_objects.walkObjects(canvas.getObjects(), (object) => {
|
|
257
|
+
if (isReplaceableImage(object) && require_image_check.isSameUrl(object.getSrc(), oldUrl)) images.push(object);
|
|
258
|
+
});
|
|
259
|
+
if (images.length === 0) return 0;
|
|
260
|
+
await history.transaction("Replace image", () => Promise.all(images.map(async (image) => {
|
|
261
|
+
const displayedWidth = image.width * image.scaleX;
|
|
262
|
+
const displayedHeight = image.height * image.scaleY;
|
|
263
|
+
await image.setSrc(newUrl, { crossOrigin: image.crossOrigin ?? null });
|
|
264
|
+
image.set({
|
|
265
|
+
scaleX: displayedWidth / image.width,
|
|
266
|
+
scaleY: displayedHeight / image.height
|
|
267
|
+
});
|
|
268
|
+
image.setCoords();
|
|
269
|
+
})));
|
|
270
|
+
canvas.requestRenderAll();
|
|
271
|
+
return images.length;
|
|
272
|
+
}
|
|
273
|
+
function requireRecovery() {
|
|
274
|
+
ensureUsable();
|
|
275
|
+
if (!recovery) throw new require_errors.DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
|
|
276
|
+
return recovery;
|
|
277
|
+
}
|
|
278
|
+
async function restoreRecovery(documentId, loadOptions) {
|
|
279
|
+
const source = requireRecovery();
|
|
280
|
+
const id = documentId ?? documentInfo.id;
|
|
281
|
+
const record = await source.read(id);
|
|
282
|
+
if (!record) throw new require_errors.DocumentEngineError("RECOVERY_NOT_FOUND", `There is no recovery copy for document "${id}"`);
|
|
283
|
+
const loaded = await loadDocument(await require_recovery_controller.restoreRecordedFiles(record), loadOptions);
|
|
284
|
+
noteContentChange();
|
|
285
|
+
events.emit("recovery:restored", { document: loaded });
|
|
286
|
+
return loaded;
|
|
287
|
+
}
|
|
199
288
|
async function travelThroughHistory(direction) {
|
|
200
289
|
ensureUsable();
|
|
201
290
|
try {
|
|
@@ -213,6 +302,7 @@ function createDocumentEngine(options) {
|
|
|
213
302
|
activeLoad?.abort();
|
|
214
303
|
history.destroy();
|
|
215
304
|
saving.destroy();
|
|
305
|
+
recovery?.destroy();
|
|
216
306
|
canvas.off("object:added", handleObjectAdded);
|
|
217
307
|
canvas.off("object:removed", handleObjectRemoved);
|
|
218
308
|
objectsById.clear();
|
|
@@ -233,7 +323,7 @@ function createDocumentEngine(options) {
|
|
|
233
323
|
...changes
|
|
234
324
|
}
|
|
235
325
|
};
|
|
236
|
-
|
|
326
|
+
noteContentChange();
|
|
237
327
|
},
|
|
238
328
|
newDocument,
|
|
239
329
|
toDocument,
|
|
@@ -247,6 +337,18 @@ function createDocumentEngine(options) {
|
|
|
247
337
|
registry.register(definition);
|
|
248
338
|
},
|
|
249
339
|
getObjectById,
|
|
340
|
+
getAssetManifest: () => toDocument().assets ?? {
|
|
341
|
+
images: [],
|
|
342
|
+
fonts: []
|
|
343
|
+
},
|
|
344
|
+
checkAssets,
|
|
345
|
+
replaceImage,
|
|
346
|
+
flushRecovery: async () => requireRecovery().flush(),
|
|
347
|
+
getRecoverableDocuments: async () => requireRecovery().list(),
|
|
348
|
+
getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
|
|
349
|
+
restoreRecovery,
|
|
350
|
+
discardRecovery: async (documentId) => requireRecovery().remove(documentId ?? documentInfo.id),
|
|
351
|
+
getInterruptedLoad: async () => requireRecovery().interruptedLoad(),
|
|
250
352
|
transaction(label, work) {
|
|
251
353
|
ensureUsable();
|
|
252
354
|
return history.transaction(label, work);
|
|
@@ -1,7 +1,10 @@
|
|
|
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 { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.cjs";
|
|
5
8
|
import { AutosaveOptions } from "../save/autosave-scheduler.cjs";
|
|
6
9
|
import { RetryOptions } from "../save/retry.cjs";
|
|
7
10
|
import { DocumentEngineError } from "./errors.cjs";
|
|
@@ -18,6 +21,8 @@ export interface DocumentEngineOptions {
|
|
|
18
21
|
history?: HistoryOptions;
|
|
19
22
|
autosave?: boolean | AutosaveOptions;
|
|
20
23
|
saveRetry?: RetryOptions;
|
|
24
|
+
assets?: AssetOptions;
|
|
25
|
+
recovery?: RecoveryOptions;
|
|
21
26
|
}
|
|
22
27
|
export interface LoadOptions {
|
|
23
28
|
restoreCanvasSize?: boolean;
|
|
@@ -28,6 +33,10 @@ export interface DocumentEngineEvents {
|
|
|
28
33
|
};
|
|
29
34
|
"load:success": {
|
|
30
35
|
document: FabricDocument;
|
|
36
|
+
warnings: AssetWarning[];
|
|
37
|
+
};
|
|
38
|
+
"assets:warning": {
|
|
39
|
+
warnings: AssetWarning[];
|
|
31
40
|
};
|
|
32
41
|
"load:error": {
|
|
33
42
|
error: DocumentEngineError;
|
|
@@ -47,6 +56,16 @@ export interface DocumentEngineEvents {
|
|
|
47
56
|
"history:error": {
|
|
48
57
|
error: DocumentEngineError;
|
|
49
58
|
};
|
|
59
|
+
"recovery:checkpoint": {
|
|
60
|
+
documentId: string;
|
|
61
|
+
savedAt: string;
|
|
62
|
+
};
|
|
63
|
+
"recovery:restored": {
|
|
64
|
+
document: FabricDocument;
|
|
65
|
+
};
|
|
66
|
+
"recovery:error": {
|
|
67
|
+
error: DocumentEngineError;
|
|
68
|
+
};
|
|
50
69
|
}
|
|
51
70
|
export interface DocumentEngine {
|
|
52
71
|
readonly canvas: StaticCanvas;
|
|
@@ -72,6 +91,15 @@ export interface DocumentEngine {
|
|
|
72
91
|
};
|
|
73
92
|
clearHistory(): void;
|
|
74
93
|
getObjectById(id: string): FabricObject | undefined;
|
|
94
|
+
getAssetManifest(): AssetManifest;
|
|
95
|
+
checkAssets(): Promise<AssetReport>;
|
|
96
|
+
replaceImage(oldUrl: string, newUrl: string): Promise<number>;
|
|
97
|
+
flushRecovery(): Promise<void>;
|
|
98
|
+
getRecoverableDocuments(): Promise<RecoveryRecord[]>;
|
|
99
|
+
getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
|
|
100
|
+
restoreRecovery(documentId?: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
101
|
+
discardRecovery(documentId?: string): Promise<void>;
|
|
102
|
+
getInterruptedLoad(): Promise<InterruptedLoad | undefined>;
|
|
75
103
|
on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
|
|
76
104
|
destroy(): void;
|
|
77
105
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
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 { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.js";
|
|
5
8
|
import { AutosaveOptions } from "../save/autosave-scheduler.js";
|
|
6
9
|
import { RetryOptions } from "../save/retry.js";
|
|
7
10
|
import { DocumentEngineError } from "./errors.js";
|
|
@@ -18,6 +21,8 @@ export interface DocumentEngineOptions {
|
|
|
18
21
|
history?: HistoryOptions;
|
|
19
22
|
autosave?: boolean | AutosaveOptions;
|
|
20
23
|
saveRetry?: RetryOptions;
|
|
24
|
+
assets?: AssetOptions;
|
|
25
|
+
recovery?: RecoveryOptions;
|
|
21
26
|
}
|
|
22
27
|
export interface LoadOptions {
|
|
23
28
|
restoreCanvasSize?: boolean;
|
|
@@ -28,6 +33,10 @@ export interface DocumentEngineEvents {
|
|
|
28
33
|
};
|
|
29
34
|
"load:success": {
|
|
30
35
|
document: FabricDocument;
|
|
36
|
+
warnings: AssetWarning[];
|
|
37
|
+
};
|
|
38
|
+
"assets:warning": {
|
|
39
|
+
warnings: AssetWarning[];
|
|
31
40
|
};
|
|
32
41
|
"load:error": {
|
|
33
42
|
error: DocumentEngineError;
|
|
@@ -47,6 +56,16 @@ export interface DocumentEngineEvents {
|
|
|
47
56
|
"history:error": {
|
|
48
57
|
error: DocumentEngineError;
|
|
49
58
|
};
|
|
59
|
+
"recovery:checkpoint": {
|
|
60
|
+
documentId: string;
|
|
61
|
+
savedAt: string;
|
|
62
|
+
};
|
|
63
|
+
"recovery:restored": {
|
|
64
|
+
document: FabricDocument;
|
|
65
|
+
};
|
|
66
|
+
"recovery:error": {
|
|
67
|
+
error: DocumentEngineError;
|
|
68
|
+
};
|
|
50
69
|
}
|
|
51
70
|
export interface DocumentEngine {
|
|
52
71
|
readonly canvas: StaticCanvas;
|
|
@@ -72,6 +91,15 @@ export interface DocumentEngine {
|
|
|
72
91
|
};
|
|
73
92
|
clearHistory(): void;
|
|
74
93
|
getObjectById(id: string): FabricObject | undefined;
|
|
94
|
+
getAssetManifest(): AssetManifest;
|
|
95
|
+
checkAssets(): Promise<AssetReport>;
|
|
96
|
+
replaceImage(oldUrl: string, newUrl: string): Promise<number>;
|
|
97
|
+
flushRecovery(): Promise<void>;
|
|
98
|
+
getRecoverableDocuments(): Promise<RecoveryRecord[]>;
|
|
99
|
+
getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
|
|
100
|
+
restoreRecovery(documentId?: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
101
|
+
discardRecovery(documentId?: string): Promise<void>;
|
|
102
|
+
getInterruptedLoad(): Promise<InterruptedLoad | undefined>;
|
|
75
103
|
on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
|
|
76
104
|
destroy(): void;
|
|
77
105
|
}
|
|
@@ -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,12 +9,16 @@ 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 { createRecoveryController, restoreRecordedFiles } from "../recovery/recovery-controller.js";
|
|
11
15
|
import { createSaveController } from "../save/save-controller.js";
|
|
12
16
|
import { createEventEmitter } from "./event-emitter.js";
|
|
13
17
|
//#region src/engine/create-document-engine.ts
|
|
18
|
+
function isReplaceableImage(object) {
|
|
19
|
+
const candidate = object;
|
|
20
|
+
return typeof candidate.getSrc === "function" && typeof candidate.setSrc === "function";
|
|
21
|
+
}
|
|
14
22
|
function describeDocumentId(value) {
|
|
15
23
|
const id = value?.id;
|
|
16
24
|
return typeof id === "string" ? id : void 0;
|
|
@@ -21,6 +29,8 @@ function createDocumentEngine(options) {
|
|
|
21
29
|
const registry = createObjectRegistry(options.customObjects);
|
|
22
30
|
const events = createEventEmitter();
|
|
23
31
|
const objectsById = /* @__PURE__ */ new Map();
|
|
32
|
+
const assetOptions = options.assets ?? {};
|
|
33
|
+
const uploadedUrls = /* @__PURE__ */ new Map();
|
|
24
34
|
let documentInfo = createDocumentInfo(options.document);
|
|
25
35
|
let activeLoad = null;
|
|
26
36
|
let destroyed = false;
|
|
@@ -61,19 +71,43 @@ function createDocumentEngine(options) {
|
|
|
61
71
|
return serializeCanvas(canvas, registry.propertiesToInclude()).objects;
|
|
62
72
|
},
|
|
63
73
|
onChange: (state) => events.emit("history:change", state),
|
|
64
|
-
onContentChange: () =>
|
|
74
|
+
onContentChange: () => noteContentChange()
|
|
65
75
|
});
|
|
66
76
|
const saving = createSaveController({
|
|
67
77
|
getStorage: () => requireStorage(),
|
|
68
|
-
createDocument: () =>
|
|
78
|
+
createDocument: async () => {
|
|
79
|
+
const prepared = await prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
|
|
80
|
+
if (prepared.warnings.length > 0) events.emit("assets:warning", { warnings: prepared.warnings });
|
|
81
|
+
return prepared.document;
|
|
82
|
+
},
|
|
69
83
|
retry: options.saveRetry,
|
|
70
84
|
autosave: options.autosave === true ? {} : options.autosave ?? false,
|
|
71
85
|
onStateChange: (state) => events.emit("save:status", state),
|
|
72
86
|
onStart: (document) => events.emit("save:start", { document }),
|
|
73
|
-
onSuccess: (document) =>
|
|
87
|
+
onSuccess: (document) => {
|
|
88
|
+
if (recovery && !saving.state().isDirty) recovery.remove(document.id);
|
|
89
|
+
events.emit("save:success", { document });
|
|
90
|
+
},
|
|
74
91
|
onError: (error) => events.emit("save:error", { error }),
|
|
75
92
|
onRetry: (event) => events.emit("save:retry", event)
|
|
76
93
|
});
|
|
94
|
+
const recovery = options.recovery ? createRecoveryController({
|
|
95
|
+
...options.recovery,
|
|
96
|
+
createDocument: () => toDocument(),
|
|
97
|
+
shouldWrite: () => !destroyed && saving.state().isDirty,
|
|
98
|
+
onCheckpoint: (record) => events.emit("recovery:checkpoint", {
|
|
99
|
+
documentId: record.documentId,
|
|
100
|
+
savedAt: record.savedAt
|
|
101
|
+
}),
|
|
102
|
+
onError: (error) => {
|
|
103
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
104
|
+
events.emit("recovery:error", { error: new DocumentEngineError("RECOVERY_FAILED", `Could not write the recovery copy: ${reason}`, { cause: error }) });
|
|
105
|
+
}
|
|
106
|
+
}) : void 0;
|
|
107
|
+
function noteContentChange() {
|
|
108
|
+
saving.noteContentChange();
|
|
109
|
+
recovery?.schedule();
|
|
110
|
+
}
|
|
77
111
|
function toDocument() {
|
|
78
112
|
ensureUsable();
|
|
79
113
|
rebuildIndex();
|
|
@@ -93,6 +127,7 @@ function createDocumentEngine(options) {
|
|
|
93
127
|
height: canvas.getHeight()
|
|
94
128
|
},
|
|
95
129
|
objects: serialized.objects,
|
|
130
|
+
assets: buildAssetManifest(serialized.objects),
|
|
96
131
|
metadata: { ...documentInfo.metadata }
|
|
97
132
|
};
|
|
98
133
|
if (serialized.version !== void 0) document.fabricVersion = serialized.version;
|
|
@@ -100,8 +135,8 @@ function createDocumentEngine(options) {
|
|
|
100
135
|
return document;
|
|
101
136
|
}
|
|
102
137
|
function toLoadError(error, signal) {
|
|
103
|
-
if (isDocumentEngineError(error)) return error;
|
|
104
138
|
if (signal.aborted) return new DocumentEngineError("LOAD_ABORTED", "Loading stopped because a newer load started", { cause: error });
|
|
139
|
+
if (isDocumentEngineError(error)) return error;
|
|
105
140
|
const reason = error instanceof Error ? error.message : String(error);
|
|
106
141
|
return new DocumentEngineError("LOAD_FAILED", `Fabric could not load the document: ${reason}`, { cause: error });
|
|
107
142
|
}
|
|
@@ -121,9 +156,14 @@ function createDocumentEngine(options) {
|
|
|
121
156
|
activeLoad?.abort();
|
|
122
157
|
const controller = new AbortController();
|
|
123
158
|
activeLoad = controller;
|
|
124
|
-
|
|
159
|
+
const documentId = describeDocumentId(input);
|
|
160
|
+
events.emit("load:start", { documentId });
|
|
125
161
|
try {
|
|
126
|
-
|
|
162
|
+
await recovery?.markLoadStarted(documentId);
|
|
163
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
164
|
+
const checked = checkDocument(input);
|
|
165
|
+
const { document, warnings } = await prepareAssetsForLoad(checked, assetOptions, controller.signal);
|
|
166
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
127
167
|
await history.withoutRecording(() => loadIntoCanvas(canvas, {
|
|
128
168
|
version: document.fabricVersion,
|
|
129
169
|
background: document.canvas.background,
|
|
@@ -142,16 +182,24 @@ function createDocumentEngine(options) {
|
|
|
142
182
|
};
|
|
143
183
|
rebuildIndex();
|
|
144
184
|
history.reset();
|
|
185
|
+
recovery?.cancel();
|
|
145
186
|
saving.startSession(document.revision ?? 0);
|
|
146
187
|
canvas.requestRenderAll();
|
|
147
|
-
events.emit("
|
|
188
|
+
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
189
|
+
events.emit("load:success", {
|
|
190
|
+
document,
|
|
191
|
+
warnings
|
|
192
|
+
});
|
|
148
193
|
return document;
|
|
149
194
|
} catch (error) {
|
|
150
195
|
const engineError = toLoadError(error, controller.signal);
|
|
151
196
|
events.emit("load:error", { error: engineError });
|
|
152
197
|
throw engineError;
|
|
153
198
|
} finally {
|
|
154
|
-
if (activeLoad === controller)
|
|
199
|
+
if (activeLoad === controller) {
|
|
200
|
+
activeLoad = null;
|
|
201
|
+
await recovery?.markLoadFinished();
|
|
202
|
+
}
|
|
155
203
|
}
|
|
156
204
|
}
|
|
157
205
|
function requireStorage() {
|
|
@@ -186,7 +234,9 @@ function createDocumentEngine(options) {
|
|
|
186
234
|
history.withoutRecording(() => canvas.clear());
|
|
187
235
|
objectsById.clear();
|
|
188
236
|
documentInfo = createDocumentInfo(newOptions);
|
|
237
|
+
uploadedUrls.clear();
|
|
189
238
|
history.reset();
|
|
239
|
+
recovery?.cancel();
|
|
190
240
|
saving.startSession(0);
|
|
191
241
|
}
|
|
192
242
|
function getObjectById(id) {
|
|
@@ -196,6 +246,45 @@ function createDocumentEngine(options) {
|
|
|
196
246
|
rebuildIndex();
|
|
197
247
|
return objectsById.get(id);
|
|
198
248
|
}
|
|
249
|
+
function checkAssets() {
|
|
250
|
+
ensureUsable();
|
|
251
|
+
return inspectAssets(toDocument(), assetOptions, new AbortController().signal);
|
|
252
|
+
}
|
|
253
|
+
async function replaceImage(oldUrl, newUrl) {
|
|
254
|
+
ensureUsable();
|
|
255
|
+
const images = [];
|
|
256
|
+
walkObjects(canvas.getObjects(), (object) => {
|
|
257
|
+
if (isReplaceableImage(object) && isSameUrl(object.getSrc(), oldUrl)) images.push(object);
|
|
258
|
+
});
|
|
259
|
+
if (images.length === 0) return 0;
|
|
260
|
+
await history.transaction("Replace image", () => Promise.all(images.map(async (image) => {
|
|
261
|
+
const displayedWidth = image.width * image.scaleX;
|
|
262
|
+
const displayedHeight = image.height * image.scaleY;
|
|
263
|
+
await image.setSrc(newUrl, { crossOrigin: image.crossOrigin ?? null });
|
|
264
|
+
image.set({
|
|
265
|
+
scaleX: displayedWidth / image.width,
|
|
266
|
+
scaleY: displayedHeight / image.height
|
|
267
|
+
});
|
|
268
|
+
image.setCoords();
|
|
269
|
+
})));
|
|
270
|
+
canvas.requestRenderAll();
|
|
271
|
+
return images.length;
|
|
272
|
+
}
|
|
273
|
+
function requireRecovery() {
|
|
274
|
+
ensureUsable();
|
|
275
|
+
if (!recovery) throw new DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
|
|
276
|
+
return recovery;
|
|
277
|
+
}
|
|
278
|
+
async function restoreRecovery(documentId, loadOptions) {
|
|
279
|
+
const source = requireRecovery();
|
|
280
|
+
const id = documentId ?? documentInfo.id;
|
|
281
|
+
const record = await source.read(id);
|
|
282
|
+
if (!record) throw new DocumentEngineError("RECOVERY_NOT_FOUND", `There is no recovery copy for document "${id}"`);
|
|
283
|
+
const loaded = await loadDocument(await restoreRecordedFiles(record), loadOptions);
|
|
284
|
+
noteContentChange();
|
|
285
|
+
events.emit("recovery:restored", { document: loaded });
|
|
286
|
+
return loaded;
|
|
287
|
+
}
|
|
199
288
|
async function travelThroughHistory(direction) {
|
|
200
289
|
ensureUsable();
|
|
201
290
|
try {
|
|
@@ -213,6 +302,7 @@ function createDocumentEngine(options) {
|
|
|
213
302
|
activeLoad?.abort();
|
|
214
303
|
history.destroy();
|
|
215
304
|
saving.destroy();
|
|
305
|
+
recovery?.destroy();
|
|
216
306
|
canvas.off("object:added", handleObjectAdded);
|
|
217
307
|
canvas.off("object:removed", handleObjectRemoved);
|
|
218
308
|
objectsById.clear();
|
|
@@ -233,7 +323,7 @@ function createDocumentEngine(options) {
|
|
|
233
323
|
...changes
|
|
234
324
|
}
|
|
235
325
|
};
|
|
236
|
-
|
|
326
|
+
noteContentChange();
|
|
237
327
|
},
|
|
238
328
|
newDocument,
|
|
239
329
|
toDocument,
|
|
@@ -247,6 +337,18 @@ function createDocumentEngine(options) {
|
|
|
247
337
|
registry.register(definition);
|
|
248
338
|
},
|
|
249
339
|
getObjectById,
|
|
340
|
+
getAssetManifest: () => toDocument().assets ?? {
|
|
341
|
+
images: [],
|
|
342
|
+
fonts: []
|
|
343
|
+
},
|
|
344
|
+
checkAssets,
|
|
345
|
+
replaceImage,
|
|
346
|
+
flushRecovery: async () => requireRecovery().flush(),
|
|
347
|
+
getRecoverableDocuments: async () => requireRecovery().list(),
|
|
348
|
+
getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
|
|
349
|
+
restoreRecovery,
|
|
350
|
+
discardRecovery: async (documentId) => requireRecovery().remove(documentId ?? documentInfo.id),
|
|
351
|
+
getInterruptedLoad: async () => requireRecovery().interruptedLoad(),
|
|
250
352
|
transaction(label, work) {
|
|
251
353
|
ensureUsable();
|
|
252
354
|
return history.transaction(label, work);
|
package/dist/engine/errors.cjs
CHANGED
|
@@ -6,6 +6,8 @@ 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;
|
|
10
12
|
this.retryable = details.retryable ?? false;
|
|
11
13
|
}
|
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" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "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" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "ENGINE_DESTROYED";
|
|
3
4
|
export interface DocumentIssue {
|
|
4
5
|
code: DocumentErrorCode;
|
|
5
6
|
path: string;
|
|
@@ -8,6 +9,8 @@ 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;
|
|
12
15
|
retryable?: boolean;
|
|
13
16
|
}
|
|
@@ -15,6 +18,8 @@ export declare class DocumentEngineError extends Error {
|
|
|
15
18
|
readonly code: DocumentErrorCode;
|
|
16
19
|
readonly issues: DocumentIssue[];
|
|
17
20
|
readonly unknownTypes: string[];
|
|
21
|
+
readonly missingAssets: ImageAsset[];
|
|
22
|
+
readonly missingFonts: FontAsset[];
|
|
18
23
|
readonly cause: unknown;
|
|
19
24
|
readonly retryable: boolean;
|
|
20
25
|
constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
|
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" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "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" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "ENGINE_DESTROYED";
|
|
3
4
|
export interface DocumentIssue {
|
|
4
5
|
code: DocumentErrorCode;
|
|
5
6
|
path: string;
|
|
@@ -8,6 +9,8 @@ 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;
|
|
12
15
|
retryable?: boolean;
|
|
13
16
|
}
|
|
@@ -15,6 +18,8 @@ export declare class DocumentEngineError extends Error {
|
|
|
15
18
|
readonly code: DocumentErrorCode;
|
|
16
19
|
readonly issues: DocumentIssue[];
|
|
17
20
|
readonly unknownTypes: string[];
|
|
21
|
+
readonly missingAssets: ImageAsset[];
|
|
22
|
+
readonly missingFonts: FontAsset[];
|
|
18
23
|
readonly cause: unknown;
|
|
19
24
|
readonly retryable: boolean;
|
|
20
25
|
constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
|
package/dist/engine/errors.js
CHANGED
|
@@ -6,6 +6,8 @@ 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;
|
|
10
12
|
this.retryable = details.retryable ?? false;
|
|
11
13
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_errors = require("./engine/errors.cjs");
|
|
2
3
|
const require_document_format = require("./document/document-format.cjs");
|
|
3
4
|
const require_validate_document = require("./document/validate-document.cjs");
|
|
4
|
-
const require_errors = require("./engine/errors.cjs");
|
|
5
5
|
const require_create_document_engine = require("./engine/create-document-engine.cjs");
|
|
6
6
|
const require_keyboard_shortcuts = require("./history/keyboard-shortcuts.cjs");
|
|
7
7
|
const require_unsaved_changes_warning = require("./save/unsaved-changes-warning.cjs");
|