fabricjs-document-engine 0.5.0 → 0.6.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 +37 -2
- package/dist/engine/create-document-engine.cjs +105 -4
- package/dist/engine/create-document-engine.d.cts +20 -0
- package/dist/engine/create-document-engine.d.ts +20 -0
- package/dist/engine/create-document-engine.js +105 -4
- package/dist/engine/errors.cjs +1 -0
- package/dist/engine/errors.d.cts +3 -1
- package/dist/engine/errors.d.ts +3 -1
- package/dist/engine/errors.js +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/migrations/migrate-document.cjs +65 -0
- package/dist/migrations/migrate-document.d.cts +15 -0
- package/dist/migrations/migrate-document.d.ts +15 -0
- package/dist/migrations/migrate-document.js +63 -0
- package/dist/storage/key-value-storage.cjs +27 -1
- package/dist/storage/key-value-storage.d.cts +2 -1
- package/dist/storage/key-value-storage.d.ts +2 -1
- package/dist/storage/key-value-storage.js +27 -1
- package/dist/storage.d.cts +2 -1
- package/dist/storage.d.ts +2 -1
- package/dist/versions/document-version.cjs +20 -0
- package/dist/versions/document-version.d.cts +25 -0
- package/dist/versions/document-version.d.ts +25 -0
- package/dist/versions/document-version.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Fabric already draws objects, handles interaction and serializes to JSON. This p
|
|
|
13
13
|
- **Assets and fonts.** Documents record the images and fonts they need. When a document is opened, every image and font is checked first. You get the exact list of what is missing, can offer replacements, and tab-only images are uploaded when you save.
|
|
14
14
|
- **Recovery.** Unsaved work is copied to IndexedDB while the user edits, and again at the moment the tab is closed or refreshed. After a crash or refresh you can offer to restore it, including images that only existed in the old tab.
|
|
15
15
|
- **Dependable export.** PNG, JPEG, WebP, SVG and editable JSON. You choose the area, scale and background. A preflight check means an export either succeeds or tells you exactly which image or font prevents it.
|
|
16
|
+
- **Versions and migration.** Keep named versions, restore any of them as a new revision, and open plain Fabric JSON or documents saved by older versions of this package.
|
|
16
17
|
- **Reliable undo and redo.** One user action is one undo step. Transactions group several code changes into one labelled step, and ids survive undo and redo.
|
|
17
18
|
- **Fabric 6 and 7.** Every release is tested against both.
|
|
18
19
|
|
|
@@ -218,6 +219,33 @@ const check = await engine.preflightExport({ format: 'png' });
|
|
|
218
219
|
if (!check.ok) showProblems(check.problems);
|
|
219
220
|
```
|
|
220
221
|
|
|
222
|
+
## Versions
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const version = await engine.createVersion('Sent to client');
|
|
226
|
+
const versions = await engine.listVersions();
|
|
227
|
+
await engine.restoreVersion(version.id);
|
|
228
|
+
await engine.deleteVersion(version.id);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- Versions are full copies of the document, kept in your storage adapter. The built-in adapters support them. A custom adapter adds four methods: `saveVersion(version)`, `listVersions(documentId)`, `loadVersion(documentId, versionId)` and `deleteVersion(documentId, versionId)`.
|
|
232
|
+
- `listVersions` returns summaries, newest first: `{ id, documentId, name, kind, createdAt, revision }`. `kind` is `named` or `auto`.
|
|
233
|
+
- **Restoring never loses work.** The engine first keeps an automatic version named `Before restoring "..."`. It then loads the old content as a new, unsaved revision of the same document. The next save stores it as the newest revision, and history stays linear. To undo a restore, restore the automatic version.
|
|
234
|
+
- **Automatic versions.** Use `versions: { autoEvery: 10, keepAuto: 20 }` to keep a version after every 10 successful saves. Named versions are never pruned. Only the newest `keepAuto` automatic versions are kept, 20 by default.
|
|
235
|
+
- Undo and redo cover recent edits in this session. Versions preserve chosen states for later.
|
|
236
|
+
|
|
237
|
+
## Migration and importing Fabric JSON
|
|
238
|
+
|
|
239
|
+
Plain Fabric JSON, such as the output of `canvas.toJSON()` from Fabric 5, 6 or 7, opens directly:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
await engine.importFabricJson(savedJsonText, { id: 'plan-42', metadata: { source: 'old editor' } });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`loadDocument` and `load(id)` also recognise plain Fabric JSON, so projects stored by an existing Fabric app open without a separate import step. A document loaded with `load(id)` keeps that id, and its next save stores it in the current format.
|
|
246
|
+
|
|
247
|
+
Every document records its `schemaVersion`. When the package format changes, older documents are upgraded step by step when they are opened. `load:success` reports `migratedFrom` when that happened. A failed step rejects with `MIGRATION_FAILED`, and `error.migrationFrom` names the version it started from. A document from a newer version of the package is refused with `UNSUPPORTED_SCHEMA` rather than being misread. `migrateDocument(value, context)` and `detectSchemaVersion(value)` are exported for tooling such as server-side batch upgrades.
|
|
248
|
+
|
|
221
249
|
## Recovery
|
|
222
250
|
|
|
223
251
|
```ts
|
|
@@ -342,6 +370,9 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
342
370
|
| `engine.toDocument()` | Serializes the canvas into the versioned document format. |
|
|
343
371
|
| `engine.loadDocument(document, { restoreCanvasSize? })` | Validates and loads a document. Resolves when the objects are on the canvas. |
|
|
344
372
|
| `engine.load(id)` / `engine.save({ overwrite? })` | Reads from or writes to your storage adapter. |
|
|
373
|
+
| `engine.importFabricJson(json, { id?, metadata? })` | Opens plain Fabric JSON, as text or an object. |
|
|
374
|
+
| `engine.createVersion(name?)` / `engine.listVersions()` | Keeps a named version, or lists versions newest first. |
|
|
375
|
+
| `engine.restoreVersion(id)` / `engine.deleteVersion(id)` | Restores a version as a new unsaved revision, or deletes it. |
|
|
345
376
|
| `engine.isDirty()` | Tells you whether there are unsaved changes. |
|
|
346
377
|
| `engine.getSaveState()` | Returns `{ status, isDirty, isSaving, revision, lastSavedAt, error }`. |
|
|
347
378
|
| `bindUnsavedChangesWarning(engine)` | Asks the browser to confirm before closing a page with unsaved changes. Returns an unbind function. |
|
|
@@ -367,7 +398,7 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
367
398
|
| `engine.getHistory()` | Returns `{ undo, redo }` label lists, newest first. |
|
|
368
399
|
| `engine.clearHistory()` | Forgets all steps. Loading a document or starting a new one also does this. |
|
|
369
400
|
| `bindKeyboardShortcuts(engine, { target? })` | Adds the undo and redo shortcuts. Returns an unbind function. |
|
|
370
|
-
| `engine.on(event, handler)` | Listens to `load:start`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `save:retry`, `save:status`, `assets:warning`, `recovery:checkpoint`, `recovery:restored`, `recovery:error`, `export:success`, `export:error`, `history:change` or `history:error`. Returns an unsubscribe function. |
|
|
401
|
+
| `engine.on(event, handler)` | Listens to `load:start`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `save:retry`, `save:status`, `assets:warning`, `recovery:checkpoint`, `recovery:restored`, `recovery:error`, `export:success`, `export:error`, `version:created`, `version:restored`, `version:error`, `history:change` or `history:error`. Returns an unsubscribe function. |
|
|
371
402
|
| `engine.destroy()` | Stops listening to the canvas and cancels a running load. |
|
|
372
403
|
| `validateDocument(value)` | Returns a list of issues with the exact path of each problem. |
|
|
373
404
|
|
|
@@ -390,6 +421,10 @@ Every failure is a `DocumentEngineError` with a `code` you can switch on:
|
|
|
390
421
|
| `SAVE_CANCELLED` | A queued save was dropped because another document was opened. |
|
|
391
422
|
| `DOCUMENT_NOT_FOUND` | The built-in adapters have no document with that id. |
|
|
392
423
|
| `HISTORY_FAILED` | Undo or redo could not rebuild an object, for example because an image is gone. The step is kept and the canvas is unchanged. |
|
|
424
|
+
| `MIGRATION_FAILED` | An older document could not be upgraded. `error.migrationFrom` is the schema version it started from. |
|
|
425
|
+
| `VERSIONS_UNSUPPORTED` | The storage adapter has no version methods. |
|
|
426
|
+
| `VERSION_NOT_FOUND` | There is no version with that id. |
|
|
427
|
+
| `VERSION_FAILED` | An automatic version could not be kept. It is delivered as a `version:error` event. |
|
|
393
428
|
| `EXPORT_BLOCKED` | The preflight found problems. `error.problems` lists each one with the objects involved. |
|
|
394
429
|
| `INVALID_EXPORT_OPTIONS` | The format, scale, quality, area or padding is not valid, or the area is empty. |
|
|
395
430
|
| `EXPORT_ABORTED` | The export was cancelled with its `signal`. |
|
|
@@ -413,7 +448,7 @@ A failed load never clears or half-fills your canvas.
|
|
|
413
448
|
| 4 | Assets and fonts | 0.3.0 |
|
|
414
449
|
| 5 | Recovery after a refresh or crash | 0.4.0 |
|
|
415
450
|
| 6 | PNG, JPEG, SVG and JSON export with preflight checks | 0.5.0 |
|
|
416
|
-
| 7 | Named versions and schema migrations | |
|
|
451
|
+
| 7 | Named versions and schema migrations | 0.6.0 |
|
|
417
452
|
| 8 | React adapter and examples | |
|
|
418
453
|
| 9 | Hardening and benchmarks | |
|
|
419
454
|
| 10 | Stable API | 1.0.0 |
|
|
@@ -15,7 +15,9 @@ const require_validate_document = require("../document/validate-document.cjs");
|
|
|
15
15
|
const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
|
|
16
16
|
const require_object_registry = require("../fabric/object-registry.cjs");
|
|
17
17
|
const require_create_history = require("../history/create-history.cjs");
|
|
18
|
+
const require_migrate_document = require("../migrations/migrate-document.cjs");
|
|
18
19
|
const require_recovery_controller = require("../recovery/recovery-controller.cjs");
|
|
20
|
+
const require_document_version = require("../versions/document-version.cjs");
|
|
19
21
|
const require_save_controller = require("../save/save-controller.cjs");
|
|
20
22
|
const require_event_emitter = require("./event-emitter.cjs");
|
|
21
23
|
//#region src/engine/create-document-engine.ts
|
|
@@ -91,6 +93,7 @@ function createDocumentEngine(options) {
|
|
|
91
93
|
onSuccess: (document) => {
|
|
92
94
|
if (recovery && !saving.state().isDirty) recovery.remove(document.id);
|
|
93
95
|
events.emit("save:success", { document });
|
|
96
|
+
createAutomaticVersionAfterSave(document);
|
|
94
97
|
},
|
|
95
98
|
onError: (error) => events.emit("save:error", { error }),
|
|
96
99
|
onRetry: (event) => events.emit("save:retry", event)
|
|
@@ -155,7 +158,10 @@ function createDocumentEngine(options) {
|
|
|
155
158
|
if (unknownTypes.length > 0) throw new require_errors.DocumentEngineError("UNKNOWN_OBJECT_TYPE", `The document uses object types that are not registered: ${unknownTypes.join(", ")}. Register them with customObjects or engine.registerObject before loading.`, { unknownTypes });
|
|
156
159
|
return document;
|
|
157
160
|
}
|
|
158
|
-
|
|
161
|
+
function loadDocument(input, loadOptions = {}) {
|
|
162
|
+
return loadMigratedDocument(input, loadOptions, {});
|
|
163
|
+
}
|
|
164
|
+
async function loadMigratedDocument(input, loadOptions, importDetails) {
|
|
159
165
|
ensureUsable();
|
|
160
166
|
activeLoad?.abort();
|
|
161
167
|
const controller = new AbortController();
|
|
@@ -165,7 +171,12 @@ function createDocumentEngine(options) {
|
|
|
165
171
|
try {
|
|
166
172
|
await recovery?.markLoadStarted(documentId);
|
|
167
173
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
168
|
-
const
|
|
174
|
+
const { document: migrated, migratedFrom } = require_migrate_document.migrateDocument(input, {
|
|
175
|
+
canvasWidth: canvas.getWidth(),
|
|
176
|
+
canvasHeight: canvas.getHeight(),
|
|
177
|
+
...importDetails
|
|
178
|
+
});
|
|
179
|
+
const checked = checkDocument(migrated);
|
|
169
180
|
const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal);
|
|
170
181
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
171
182
|
await history.withoutRecording(() => require_fabric_adapter.loadIntoCanvas(canvas, {
|
|
@@ -192,7 +203,8 @@ function createDocumentEngine(options) {
|
|
|
192
203
|
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
193
204
|
events.emit("load:success", {
|
|
194
205
|
document,
|
|
195
|
-
warnings
|
|
206
|
+
warnings,
|
|
207
|
+
migratedFrom
|
|
196
208
|
});
|
|
197
209
|
return document;
|
|
198
210
|
} catch (error) {
|
|
@@ -206,6 +218,90 @@ function createDocumentEngine(options) {
|
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
}
|
|
221
|
+
function importFabricJson(json, importOptions = {}) {
|
|
222
|
+
let parsed = json;
|
|
223
|
+
if (typeof json === "string") try {
|
|
224
|
+
parsed = JSON.parse(json);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
227
|
+
return Promise.reject(new require_errors.DocumentEngineError("INVALID_DOCUMENT", `The text is not valid JSON: ${reason}`, { cause: error }));
|
|
228
|
+
}
|
|
229
|
+
const { id, metadata, ...loadOptions } = importOptions;
|
|
230
|
+
return loadMigratedDocument(parsed, loadOptions, {
|
|
231
|
+
id,
|
|
232
|
+
metadata
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
let savesSinceAutomaticVersion = 0;
|
|
236
|
+
let lastVersionTime = 0;
|
|
237
|
+
function nextVersionTimestamp() {
|
|
238
|
+
lastVersionTime = Math.max(Date.now(), lastVersionTime + 1);
|
|
239
|
+
return new Date(lastVersionTime).toISOString();
|
|
240
|
+
}
|
|
241
|
+
function requireVersions() {
|
|
242
|
+
ensureUsable();
|
|
243
|
+
if (!require_document_version.supportsVersions(storage)) throw new require_errors.DocumentEngineError("VERSIONS_UNSUPPORTED", "The storage adapter needs saveVersion, listVersions, loadVersion and deleteVersion to keep versions");
|
|
244
|
+
return storage;
|
|
245
|
+
}
|
|
246
|
+
async function pruneAutomaticVersions(versionStorage, documentId) {
|
|
247
|
+
const keepAuto = options.versions?.keepAuto ?? 20;
|
|
248
|
+
const existing = await versionStorage.listVersions(documentId);
|
|
249
|
+
await Promise.all(require_document_version.versionsToPrune(existing, keepAuto).map((version) => versionStorage.deleteVersion(documentId, version.id)));
|
|
250
|
+
}
|
|
251
|
+
async function storeVersion(name, kind, document) {
|
|
252
|
+
const versionStorage = requireVersions();
|
|
253
|
+
const content = document ?? (await require_asset_pipeline.prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls)).document;
|
|
254
|
+
const version = {
|
|
255
|
+
id: require_ids.createId(),
|
|
256
|
+
documentId: content.id,
|
|
257
|
+
name,
|
|
258
|
+
kind,
|
|
259
|
+
createdAt: nextVersionTimestamp(),
|
|
260
|
+
revision: content.revision ?? saving.state().revision,
|
|
261
|
+
document: content
|
|
262
|
+
};
|
|
263
|
+
await versionStorage.saveVersion(version);
|
|
264
|
+
await pruneAutomaticVersions(versionStorage, content.id);
|
|
265
|
+
const summary = require_document_version.summarize(version);
|
|
266
|
+
events.emit("version:created", summary);
|
|
267
|
+
return summary;
|
|
268
|
+
}
|
|
269
|
+
function reportVersionError(error) {
|
|
270
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
271
|
+
const engineError = require_errors.isDocumentEngineError(error) ? error : new require_errors.DocumentEngineError("VERSION_FAILED", `Could not keep an automatic version: ${reason}`, { cause: error });
|
|
272
|
+
events.emit("version:error", { error: engineError });
|
|
273
|
+
}
|
|
274
|
+
function createAutomaticVersionAfterSave(document) {
|
|
275
|
+
const every = options.versions?.autoEvery ?? 0;
|
|
276
|
+
if (every <= 0 || !require_document_version.supportsVersions(storage)) return;
|
|
277
|
+
savesSinceAutomaticVersion += 1;
|
|
278
|
+
if (savesSinceAutomaticVersion < every) return;
|
|
279
|
+
savesSinceAutomaticVersion = 0;
|
|
280
|
+
storeVersion(`Autosave ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "auto", document).catch(reportVersionError);
|
|
281
|
+
}
|
|
282
|
+
async function restoreVersion(versionId) {
|
|
283
|
+
const versionStorage = requireVersions();
|
|
284
|
+
const current = documentInfo;
|
|
285
|
+
const version = await versionStorage.loadVersion(current.id, versionId);
|
|
286
|
+
await storeVersion(`Before restoring "${version.name}"`, "auto");
|
|
287
|
+
const { document: migrated } = require_migrate_document.migrateDocument(version.document, {
|
|
288
|
+
canvasWidth: canvas.getWidth(),
|
|
289
|
+
canvasHeight: canvas.getHeight()
|
|
290
|
+
});
|
|
291
|
+
const baseRevision = saving.state().revision;
|
|
292
|
+
const loaded = await loadDocument({
|
|
293
|
+
...migrated,
|
|
294
|
+
id: current.id,
|
|
295
|
+
createdAt: current.createdAt,
|
|
296
|
+
revision: baseRevision
|
|
297
|
+
});
|
|
298
|
+
noteContentChange();
|
|
299
|
+
events.emit("version:restored", {
|
|
300
|
+
version: require_document_version.summarize(version),
|
|
301
|
+
document: loaded
|
|
302
|
+
});
|
|
303
|
+
return loaded;
|
|
304
|
+
}
|
|
209
305
|
function requireStorage() {
|
|
210
306
|
if (!storage) throw new require_errors.DocumentEngineError("STORAGE_MISSING", "Pass a storage adapter to createDocumentEngine to use load and save");
|
|
211
307
|
return storage;
|
|
@@ -222,7 +318,7 @@ function createDocumentEngine(options) {
|
|
|
222
318
|
events.emit("load:error", { error: engineError });
|
|
223
319
|
throw engineError;
|
|
224
320
|
}
|
|
225
|
-
return
|
|
321
|
+
return loadMigratedDocument(stored, loadOptions ?? {}, { id: documentId });
|
|
226
322
|
}
|
|
227
323
|
function save(saveOptions) {
|
|
228
324
|
try {
|
|
@@ -419,6 +515,11 @@ function createDocumentEngine(options) {
|
|
|
419
515
|
toDocument,
|
|
420
516
|
loadDocument,
|
|
421
517
|
load,
|
|
518
|
+
importFabricJson,
|
|
519
|
+
createVersion: (name) => storeVersion(name ?? `Version ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "named"),
|
|
520
|
+
listVersions: async (documentId) => requireVersions().listVersions(documentId ?? documentInfo.id),
|
|
521
|
+
restoreVersion,
|
|
522
|
+
deleteVersion: async (versionId) => requireVersions().deleteVersion(documentInfo.id, versionId),
|
|
422
523
|
save,
|
|
423
524
|
isDirty: () => saving.state().isDirty,
|
|
424
525
|
getSaveState: () => saving.state(),
|
|
@@ -6,6 +6,7 @@ import { ExportPreflight } from "../export/preflight-export.cjs";
|
|
|
6
6
|
import { NewDocumentOptions } from "../document/create-document.cjs";
|
|
7
7
|
import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
|
|
8
8
|
import { HistoryOptions, HistoryState } from "../history/create-history.cjs";
|
|
9
|
+
import { VersionOptions, VersionSummary } from "../versions/document-version.cjs";
|
|
9
10
|
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.cjs";
|
|
10
11
|
import { AutosaveOptions } from "../save/autosave-scheduler.cjs";
|
|
11
12
|
import { RetryOptions } from "../save/retry.cjs";
|
|
@@ -25,10 +26,15 @@ export interface DocumentEngineOptions {
|
|
|
25
26
|
saveRetry?: RetryOptions;
|
|
26
27
|
assets?: AssetOptions;
|
|
27
28
|
recovery?: RecoveryOptions;
|
|
29
|
+
versions?: VersionOptions;
|
|
28
30
|
}
|
|
29
31
|
export interface LoadOptions {
|
|
30
32
|
restoreCanvasSize?: boolean;
|
|
31
33
|
}
|
|
34
|
+
export interface ImportOptions extends LoadOptions {
|
|
35
|
+
id?: string;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
}
|
|
32
38
|
export interface ExportResult {
|
|
33
39
|
format: ExportFormat;
|
|
34
40
|
mimeType: string;
|
|
@@ -45,6 +51,7 @@ export interface DocumentEngineEvents {
|
|
|
45
51
|
"load:success": {
|
|
46
52
|
document: FabricDocument;
|
|
47
53
|
warnings: AssetWarning[];
|
|
54
|
+
migratedFrom: number | undefined;
|
|
48
55
|
};
|
|
49
56
|
"assets:warning": {
|
|
50
57
|
warnings: AssetWarning[];
|
|
@@ -86,6 +93,14 @@ export interface DocumentEngineEvents {
|
|
|
86
93
|
"export:error": {
|
|
87
94
|
error: DocumentEngineError;
|
|
88
95
|
};
|
|
96
|
+
"version:created": VersionSummary;
|
|
97
|
+
"version:restored": {
|
|
98
|
+
version: VersionSummary;
|
|
99
|
+
document: FabricDocument;
|
|
100
|
+
};
|
|
101
|
+
"version:error": {
|
|
102
|
+
error: DocumentEngineError;
|
|
103
|
+
};
|
|
89
104
|
}
|
|
90
105
|
export interface DocumentEngine {
|
|
91
106
|
readonly canvas: StaticCanvas;
|
|
@@ -95,6 +110,11 @@ export interface DocumentEngine {
|
|
|
95
110
|
toDocument(): FabricDocument;
|
|
96
111
|
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
97
112
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
113
|
+
importFabricJson(json: string | Record<string, unknown>, options?: ImportOptions): Promise<FabricDocument>;
|
|
114
|
+
createVersion(name?: string): Promise<VersionSummary>;
|
|
115
|
+
listVersions(documentId?: string): Promise<VersionSummary[]>;
|
|
116
|
+
restoreVersion(versionId: string): Promise<FabricDocument>;
|
|
117
|
+
deleteVersion(versionId: string): Promise<void>;
|
|
98
118
|
save(options?: SaveOptions): Promise<FabricDocument>;
|
|
99
119
|
isDirty(): boolean;
|
|
100
120
|
getSaveState(): SaveState;
|
|
@@ -6,6 +6,7 @@ import { ExportPreflight } from "../export/preflight-export.js";
|
|
|
6
6
|
import { NewDocumentOptions } from "../document/create-document.js";
|
|
7
7
|
import { CustomObjectDefinition } from "../fabric/object-registry.js";
|
|
8
8
|
import { HistoryOptions, HistoryState } from "../history/create-history.js";
|
|
9
|
+
import { VersionOptions, VersionSummary } from "../versions/document-version.js";
|
|
9
10
|
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.js";
|
|
10
11
|
import { AutosaveOptions } from "../save/autosave-scheduler.js";
|
|
11
12
|
import { RetryOptions } from "../save/retry.js";
|
|
@@ -25,10 +26,15 @@ export interface DocumentEngineOptions {
|
|
|
25
26
|
saveRetry?: RetryOptions;
|
|
26
27
|
assets?: AssetOptions;
|
|
27
28
|
recovery?: RecoveryOptions;
|
|
29
|
+
versions?: VersionOptions;
|
|
28
30
|
}
|
|
29
31
|
export interface LoadOptions {
|
|
30
32
|
restoreCanvasSize?: boolean;
|
|
31
33
|
}
|
|
34
|
+
export interface ImportOptions extends LoadOptions {
|
|
35
|
+
id?: string;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
}
|
|
32
38
|
export interface ExportResult {
|
|
33
39
|
format: ExportFormat;
|
|
34
40
|
mimeType: string;
|
|
@@ -45,6 +51,7 @@ export interface DocumentEngineEvents {
|
|
|
45
51
|
"load:success": {
|
|
46
52
|
document: FabricDocument;
|
|
47
53
|
warnings: AssetWarning[];
|
|
54
|
+
migratedFrom: number | undefined;
|
|
48
55
|
};
|
|
49
56
|
"assets:warning": {
|
|
50
57
|
warnings: AssetWarning[];
|
|
@@ -86,6 +93,14 @@ export interface DocumentEngineEvents {
|
|
|
86
93
|
"export:error": {
|
|
87
94
|
error: DocumentEngineError;
|
|
88
95
|
};
|
|
96
|
+
"version:created": VersionSummary;
|
|
97
|
+
"version:restored": {
|
|
98
|
+
version: VersionSummary;
|
|
99
|
+
document: FabricDocument;
|
|
100
|
+
};
|
|
101
|
+
"version:error": {
|
|
102
|
+
error: DocumentEngineError;
|
|
103
|
+
};
|
|
89
104
|
}
|
|
90
105
|
export interface DocumentEngine {
|
|
91
106
|
readonly canvas: StaticCanvas;
|
|
@@ -95,6 +110,11 @@ export interface DocumentEngine {
|
|
|
95
110
|
toDocument(): FabricDocument;
|
|
96
111
|
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
97
112
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
113
|
+
importFabricJson(json: string | Record<string, unknown>, options?: ImportOptions): Promise<FabricDocument>;
|
|
114
|
+
createVersion(name?: string): Promise<VersionSummary>;
|
|
115
|
+
listVersions(documentId?: string): Promise<VersionSummary[]>;
|
|
116
|
+
restoreVersion(versionId: string): Promise<FabricDocument>;
|
|
117
|
+
deleteVersion(versionId: string): Promise<void>;
|
|
98
118
|
save(options?: SaveOptions): Promise<FabricDocument>;
|
|
99
119
|
isDirty(): boolean;
|
|
100
120
|
getSaveState(): SaveState;
|
|
@@ -15,7 +15,9 @@ import { validateDocument } from "../document/validate-document.js";
|
|
|
15
15
|
import { loadIntoCanvas, serializeCanvas } from "../fabric/fabric-adapter.js";
|
|
16
16
|
import { createObjectRegistry } from "../fabric/object-registry.js";
|
|
17
17
|
import { createHistory } from "../history/create-history.js";
|
|
18
|
+
import { migrateDocument } from "../migrations/migrate-document.js";
|
|
18
19
|
import { createRecoveryController, restoreRecordedFiles } from "../recovery/recovery-controller.js";
|
|
20
|
+
import { summarize, supportsVersions, versionsToPrune } from "../versions/document-version.js";
|
|
19
21
|
import { createSaveController } from "../save/save-controller.js";
|
|
20
22
|
import { createEventEmitter } from "./event-emitter.js";
|
|
21
23
|
//#region src/engine/create-document-engine.ts
|
|
@@ -91,6 +93,7 @@ function createDocumentEngine(options) {
|
|
|
91
93
|
onSuccess: (document) => {
|
|
92
94
|
if (recovery && !saving.state().isDirty) recovery.remove(document.id);
|
|
93
95
|
events.emit("save:success", { document });
|
|
96
|
+
createAutomaticVersionAfterSave(document);
|
|
94
97
|
},
|
|
95
98
|
onError: (error) => events.emit("save:error", { error }),
|
|
96
99
|
onRetry: (event) => events.emit("save:retry", event)
|
|
@@ -155,7 +158,10 @@ function createDocumentEngine(options) {
|
|
|
155
158
|
if (unknownTypes.length > 0) throw new DocumentEngineError("UNKNOWN_OBJECT_TYPE", `The document uses object types that are not registered: ${unknownTypes.join(", ")}. Register them with customObjects or engine.registerObject before loading.`, { unknownTypes });
|
|
156
159
|
return document;
|
|
157
160
|
}
|
|
158
|
-
|
|
161
|
+
function loadDocument(input, loadOptions = {}) {
|
|
162
|
+
return loadMigratedDocument(input, loadOptions, {});
|
|
163
|
+
}
|
|
164
|
+
async function loadMigratedDocument(input, loadOptions, importDetails) {
|
|
159
165
|
ensureUsable();
|
|
160
166
|
activeLoad?.abort();
|
|
161
167
|
const controller = new AbortController();
|
|
@@ -165,7 +171,12 @@ function createDocumentEngine(options) {
|
|
|
165
171
|
try {
|
|
166
172
|
await recovery?.markLoadStarted(documentId);
|
|
167
173
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
168
|
-
const
|
|
174
|
+
const { document: migrated, migratedFrom } = migrateDocument(input, {
|
|
175
|
+
canvasWidth: canvas.getWidth(),
|
|
176
|
+
canvasHeight: canvas.getHeight(),
|
|
177
|
+
...importDetails
|
|
178
|
+
});
|
|
179
|
+
const checked = checkDocument(migrated);
|
|
169
180
|
const { document, warnings } = await prepareAssetsForLoad(checked, assetOptions, controller.signal);
|
|
170
181
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
171
182
|
await history.withoutRecording(() => loadIntoCanvas(canvas, {
|
|
@@ -192,7 +203,8 @@ function createDocumentEngine(options) {
|
|
|
192
203
|
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
193
204
|
events.emit("load:success", {
|
|
194
205
|
document,
|
|
195
|
-
warnings
|
|
206
|
+
warnings,
|
|
207
|
+
migratedFrom
|
|
196
208
|
});
|
|
197
209
|
return document;
|
|
198
210
|
} catch (error) {
|
|
@@ -206,6 +218,90 @@ function createDocumentEngine(options) {
|
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
}
|
|
221
|
+
function importFabricJson(json, importOptions = {}) {
|
|
222
|
+
let parsed = json;
|
|
223
|
+
if (typeof json === "string") try {
|
|
224
|
+
parsed = JSON.parse(json);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
227
|
+
return Promise.reject(new DocumentEngineError("INVALID_DOCUMENT", `The text is not valid JSON: ${reason}`, { cause: error }));
|
|
228
|
+
}
|
|
229
|
+
const { id, metadata, ...loadOptions } = importOptions;
|
|
230
|
+
return loadMigratedDocument(parsed, loadOptions, {
|
|
231
|
+
id,
|
|
232
|
+
metadata
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
let savesSinceAutomaticVersion = 0;
|
|
236
|
+
let lastVersionTime = 0;
|
|
237
|
+
function nextVersionTimestamp() {
|
|
238
|
+
lastVersionTime = Math.max(Date.now(), lastVersionTime + 1);
|
|
239
|
+
return new Date(lastVersionTime).toISOString();
|
|
240
|
+
}
|
|
241
|
+
function requireVersions() {
|
|
242
|
+
ensureUsable();
|
|
243
|
+
if (!supportsVersions(storage)) throw new DocumentEngineError("VERSIONS_UNSUPPORTED", "The storage adapter needs saveVersion, listVersions, loadVersion and deleteVersion to keep versions");
|
|
244
|
+
return storage;
|
|
245
|
+
}
|
|
246
|
+
async function pruneAutomaticVersions(versionStorage, documentId) {
|
|
247
|
+
const keepAuto = options.versions?.keepAuto ?? 20;
|
|
248
|
+
const existing = await versionStorage.listVersions(documentId);
|
|
249
|
+
await Promise.all(versionsToPrune(existing, keepAuto).map((version) => versionStorage.deleteVersion(documentId, version.id)));
|
|
250
|
+
}
|
|
251
|
+
async function storeVersion(name, kind, document) {
|
|
252
|
+
const versionStorage = requireVersions();
|
|
253
|
+
const content = document ?? (await prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls)).document;
|
|
254
|
+
const version = {
|
|
255
|
+
id: createId(),
|
|
256
|
+
documentId: content.id,
|
|
257
|
+
name,
|
|
258
|
+
kind,
|
|
259
|
+
createdAt: nextVersionTimestamp(),
|
|
260
|
+
revision: content.revision ?? saving.state().revision,
|
|
261
|
+
document: content
|
|
262
|
+
};
|
|
263
|
+
await versionStorage.saveVersion(version);
|
|
264
|
+
await pruneAutomaticVersions(versionStorage, content.id);
|
|
265
|
+
const summary = summarize(version);
|
|
266
|
+
events.emit("version:created", summary);
|
|
267
|
+
return summary;
|
|
268
|
+
}
|
|
269
|
+
function reportVersionError(error) {
|
|
270
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
271
|
+
const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("VERSION_FAILED", `Could not keep an automatic version: ${reason}`, { cause: error });
|
|
272
|
+
events.emit("version:error", { error: engineError });
|
|
273
|
+
}
|
|
274
|
+
function createAutomaticVersionAfterSave(document) {
|
|
275
|
+
const every = options.versions?.autoEvery ?? 0;
|
|
276
|
+
if (every <= 0 || !supportsVersions(storage)) return;
|
|
277
|
+
savesSinceAutomaticVersion += 1;
|
|
278
|
+
if (savesSinceAutomaticVersion < every) return;
|
|
279
|
+
savesSinceAutomaticVersion = 0;
|
|
280
|
+
storeVersion(`Autosave ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "auto", document).catch(reportVersionError);
|
|
281
|
+
}
|
|
282
|
+
async function restoreVersion(versionId) {
|
|
283
|
+
const versionStorage = requireVersions();
|
|
284
|
+
const current = documentInfo;
|
|
285
|
+
const version = await versionStorage.loadVersion(current.id, versionId);
|
|
286
|
+
await storeVersion(`Before restoring "${version.name}"`, "auto");
|
|
287
|
+
const { document: migrated } = migrateDocument(version.document, {
|
|
288
|
+
canvasWidth: canvas.getWidth(),
|
|
289
|
+
canvasHeight: canvas.getHeight()
|
|
290
|
+
});
|
|
291
|
+
const baseRevision = saving.state().revision;
|
|
292
|
+
const loaded = await loadDocument({
|
|
293
|
+
...migrated,
|
|
294
|
+
id: current.id,
|
|
295
|
+
createdAt: current.createdAt,
|
|
296
|
+
revision: baseRevision
|
|
297
|
+
});
|
|
298
|
+
noteContentChange();
|
|
299
|
+
events.emit("version:restored", {
|
|
300
|
+
version: summarize(version),
|
|
301
|
+
document: loaded
|
|
302
|
+
});
|
|
303
|
+
return loaded;
|
|
304
|
+
}
|
|
209
305
|
function requireStorage() {
|
|
210
306
|
if (!storage) throw new DocumentEngineError("STORAGE_MISSING", "Pass a storage adapter to createDocumentEngine to use load and save");
|
|
211
307
|
return storage;
|
|
@@ -222,7 +318,7 @@ function createDocumentEngine(options) {
|
|
|
222
318
|
events.emit("load:error", { error: engineError });
|
|
223
319
|
throw engineError;
|
|
224
320
|
}
|
|
225
|
-
return
|
|
321
|
+
return loadMigratedDocument(stored, loadOptions ?? {}, { id: documentId });
|
|
226
322
|
}
|
|
227
323
|
function save(saveOptions) {
|
|
228
324
|
try {
|
|
@@ -419,6 +515,11 @@ function createDocumentEngine(options) {
|
|
|
419
515
|
toDocument,
|
|
420
516
|
loadDocument,
|
|
421
517
|
load,
|
|
518
|
+
importFabricJson,
|
|
519
|
+
createVersion: (name) => storeVersion(name ?? `Version ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "named"),
|
|
520
|
+
listVersions: async (documentId) => requireVersions().listVersions(documentId ?? documentInfo.id),
|
|
521
|
+
restoreVersion,
|
|
522
|
+
deleteVersion: async (versionId) => requireVersions().deleteVersion(documentInfo.id, versionId),
|
|
422
523
|
save,
|
|
423
524
|
isDirty: () => saving.state().isDirty,
|
|
424
525
|
getSaveState: () => saving.state(),
|
package/dist/engine/errors.cjs
CHANGED
|
@@ -9,6 +9,7 @@ var DocumentEngineError = class extends Error {
|
|
|
9
9
|
this.missingAssets = details.missingAssets ?? [];
|
|
10
10
|
this.missingFonts = details.missingFonts ?? [];
|
|
11
11
|
this.problems = details.problems ?? [];
|
|
12
|
+
this.migrationFrom = details.migrationFrom;
|
|
12
13
|
this.cause = details.cause;
|
|
13
14
|
this.retryable = details.retryable ?? false;
|
|
14
15
|
}
|
package/dist/engine/errors.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FontAsset, ImageAsset } from "../assets/asset-manifest.cjs";
|
|
2
2
|
import { ExportProblem } from "../export/preflight-export.cjs";
|
|
3
3
|
//#region src/engine/errors.d.ts
|
|
4
|
-
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" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "ENGINE_DESTROYED";
|
|
4
|
+
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" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "ENGINE_DESTROYED";
|
|
5
5
|
export interface DocumentIssue {
|
|
6
6
|
code: DocumentErrorCode;
|
|
7
7
|
path: string;
|
|
@@ -13,6 +13,7 @@ export interface DocumentEngineErrorDetails {
|
|
|
13
13
|
missingAssets?: ImageAsset[];
|
|
14
14
|
missingFonts?: FontAsset[];
|
|
15
15
|
problems?: ExportProblem[];
|
|
16
|
+
migrationFrom?: number;
|
|
16
17
|
cause?: unknown;
|
|
17
18
|
retryable?: boolean;
|
|
18
19
|
}
|
|
@@ -23,6 +24,7 @@ export declare class DocumentEngineError extends Error {
|
|
|
23
24
|
readonly missingAssets: ImageAsset[];
|
|
24
25
|
readonly missingFonts: FontAsset[];
|
|
25
26
|
readonly problems: ExportProblem[];
|
|
27
|
+
readonly migrationFrom: number | undefined;
|
|
26
28
|
readonly cause: unknown;
|
|
27
29
|
readonly retryable: boolean;
|
|
28
30
|
constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
|
package/dist/engine/errors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FontAsset, ImageAsset } from "../assets/asset-manifest.js";
|
|
2
2
|
import { ExportProblem } from "../export/preflight-export.js";
|
|
3
3
|
//#region src/engine/errors.d.ts
|
|
4
|
-
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" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "ENGINE_DESTROYED";
|
|
4
|
+
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" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "ENGINE_DESTROYED";
|
|
5
5
|
export interface DocumentIssue {
|
|
6
6
|
code: DocumentErrorCode;
|
|
7
7
|
path: string;
|
|
@@ -13,6 +13,7 @@ export interface DocumentEngineErrorDetails {
|
|
|
13
13
|
missingAssets?: ImageAsset[];
|
|
14
14
|
missingFonts?: FontAsset[];
|
|
15
15
|
problems?: ExportProblem[];
|
|
16
|
+
migrationFrom?: number;
|
|
16
17
|
cause?: unknown;
|
|
17
18
|
retryable?: boolean;
|
|
18
19
|
}
|
|
@@ -23,6 +24,7 @@ export declare class DocumentEngineError extends Error {
|
|
|
23
24
|
readonly missingAssets: ImageAsset[];
|
|
24
25
|
readonly missingFonts: FontAsset[];
|
|
25
26
|
readonly problems: ExportProblem[];
|
|
27
|
+
readonly migrationFrom: number | undefined;
|
|
26
28
|
readonly cause: unknown;
|
|
27
29
|
readonly retryable: boolean;
|
|
28
30
|
constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
|
package/dist/engine/errors.js
CHANGED
|
@@ -9,6 +9,7 @@ var DocumentEngineError = class extends Error {
|
|
|
9
9
|
this.missingAssets = details.missingAssets ?? [];
|
|
10
10
|
this.missingFonts = details.missingFonts ?? [];
|
|
11
11
|
this.problems = details.problems ?? [];
|
|
12
|
+
this.migrationFrom = details.migrationFrom;
|
|
12
13
|
this.cause = details.cause;
|
|
13
14
|
this.retryable = details.retryable ?? false;
|
|
14
15
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_errors = require("./engine/errors.cjs");
|
|
3
3
|
const require_document_format = require("./document/document-format.cjs");
|
|
4
4
|
const require_validate_document = require("./document/validate-document.cjs");
|
|
5
|
+
const require_migrate_document = require("./migrations/migrate-document.cjs");
|
|
5
6
|
const require_create_document_engine = require("./engine/create-document-engine.cjs");
|
|
6
7
|
const require_keyboard_shortcuts = require("./history/keyboard-shortcuts.cjs");
|
|
7
8
|
const require_unsaved_changes_warning = require("./save/unsaved-changes-warning.cjs");
|
|
@@ -12,6 +13,8 @@ exports.bindKeyboardShortcuts = require_keyboard_shortcuts.bindKeyboardShortcuts
|
|
|
12
13
|
exports.bindUnsavedChangesWarning = require_unsaved_changes_warning.bindUnsavedChangesWarning;
|
|
13
14
|
exports.createConflictError = require_errors.createConflictError;
|
|
14
15
|
exports.createDocumentEngine = require_create_document_engine.createDocumentEngine;
|
|
16
|
+
exports.detectSchemaVersion = require_migrate_document.detectSchemaVersion;
|
|
15
17
|
exports.downloadExport = require_download_export.downloadExport;
|
|
16
18
|
exports.isDocumentEngineError = require_errors.isDocumentEngineError;
|
|
19
|
+
exports.migrateDocument = require_migrate_document.migrateDocument;
|
|
17
20
|
exports.validateDocument = require_validate_document.validateDocument;
|
package/dist/index.d.cts
CHANGED
|
@@ -7,6 +7,7 @@ import { ExportPreflight, ExportProblem, ExportProblemCode } from "./export/pref
|
|
|
7
7
|
import { NewDocumentOptions } from "./document/create-document.cjs";
|
|
8
8
|
import { CustomObjectDefinition } from "./fabric/object-registry.cjs";
|
|
9
9
|
import { HistoryOptions, HistoryState } from "./history/create-history.cjs";
|
|
10
|
+
import { DocumentVersion, VersionKind, VersionOptions, VersionStorage, VersionSummary } from "./versions/document-version.cjs";
|
|
10
11
|
import { RecoveryStore } from "./recovery/recovery-store.cjs";
|
|
11
12
|
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "./recovery/recovery-controller.cjs";
|
|
12
13
|
import { AutosaveOptions } from "./save/autosave-scheduler.cjs";
|
|
@@ -15,9 +16,10 @@ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, Doc
|
|
|
15
16
|
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.cjs";
|
|
16
17
|
import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.cjs";
|
|
17
18
|
import { Unsubscribe } from "./engine/event-emitter.cjs";
|
|
18
|
-
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
19
|
+
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
19
20
|
import { validateDocument } from "./document/validate-document.cjs";
|
|
20
21
|
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.cjs";
|
|
21
22
|
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.cjs";
|
|
22
23
|
import { DownloadableExport, downloadExport } from "./export/download-export.cjs";
|
|
23
|
-
|
|
24
|
+
import { Migration, MigrationContext, MigrationResult, detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.cjs";
|
|
25
|
+
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ExportPreflight, ExportProblem, ExportProblemCode } from "./export/pref
|
|
|
7
7
|
import { NewDocumentOptions } from "./document/create-document.js";
|
|
8
8
|
import { CustomObjectDefinition } from "./fabric/object-registry.js";
|
|
9
9
|
import { HistoryOptions, HistoryState } from "./history/create-history.js";
|
|
10
|
+
import { DocumentVersion, VersionKind, VersionOptions, VersionStorage, VersionSummary } from "./versions/document-version.js";
|
|
10
11
|
import { RecoveryStore } from "./recovery/recovery-store.js";
|
|
11
12
|
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "./recovery/recovery-controller.js";
|
|
12
13
|
import { AutosaveOptions } from "./save/autosave-scheduler.js";
|
|
@@ -15,9 +16,10 @@ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, Doc
|
|
|
15
16
|
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.js";
|
|
16
17
|
import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.js";
|
|
17
18
|
import { Unsubscribe } from "./engine/event-emitter.js";
|
|
18
|
-
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
|
|
19
|
+
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
|
|
19
20
|
import { validateDocument } from "./document/validate-document.js";
|
|
20
21
|
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
21
22
|
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
|
|
22
23
|
import { DownloadableExport, downloadExport } from "./export/download-export.js";
|
|
23
|
-
|
|
24
|
+
import { Migration, MigrationContext, MigrationResult, detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.js";
|
|
25
|
+
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { DocumentEngineError, createConflictError, isDocumentEngineError } from "./engine/errors.js";
|
|
2
2
|
import { CURRENT_SCHEMA_VERSION } from "./document/document-format.js";
|
|
3
3
|
import { validateDocument } from "./document/validate-document.js";
|
|
4
|
+
import { detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.js";
|
|
4
5
|
import { createDocumentEngine } from "./engine/create-document-engine.js";
|
|
5
6
|
import { bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
6
7
|
import { bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
|
|
7
8
|
import { downloadExport } from "./export/download-export.js";
|
|
8
|
-
export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, downloadExport, isDocumentEngineError, validateDocument };
|
|
9
|
+
export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
require("../document/document-format.cjs");
|
|
3
|
+
const require_ids = require("../document/ids.cjs");
|
|
4
|
+
//#region src/migrations/migrate-document.ts
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function positiveNumberOr(value, fallback) {
|
|
9
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
function fromFabricJson(json, context) {
|
|
12
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13
|
+
const document = {
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
id: context.id ?? (typeof json.id === "string" && json.id.length > 0 ? json.id : require_ids.createId()),
|
|
16
|
+
createdAt: now,
|
|
17
|
+
updatedAt: now,
|
|
18
|
+
revision: 0,
|
|
19
|
+
canvas: {
|
|
20
|
+
width: positiveNumberOr(json.width, context.canvasWidth),
|
|
21
|
+
height: positiveNumberOr(json.height, context.canvasHeight),
|
|
22
|
+
...json.background === void 0 ? {} : { background: json.background }
|
|
23
|
+
},
|
|
24
|
+
objects: json.objects,
|
|
25
|
+
metadata: { ...context.metadata }
|
|
26
|
+
};
|
|
27
|
+
if (typeof json.version === "string") document.fabricVersion = json.version;
|
|
28
|
+
return document;
|
|
29
|
+
}
|
|
30
|
+
const migrations = { 0: fromFabricJson };
|
|
31
|
+
function detectSchemaVersion(value) {
|
|
32
|
+
if (!isPlainObject(value)) return void 0;
|
|
33
|
+
if (typeof value.schemaVersion === "number" && Number.isInteger(value.schemaVersion)) return value.schemaVersion;
|
|
34
|
+
if (value.schemaVersion === void 0 && Array.isArray(value.objects)) return 0;
|
|
35
|
+
}
|
|
36
|
+
function migrateDocument(value, context, steps = migrations, targetVersion = 1) {
|
|
37
|
+
const startVersion = detectSchemaVersion(value);
|
|
38
|
+
if (startVersion === void 0 || startVersion >= targetVersion || startVersion < 0) return {
|
|
39
|
+
document: value,
|
|
40
|
+
migratedFrom: void 0
|
|
41
|
+
};
|
|
42
|
+
let document = JSON.parse(JSON.stringify(value));
|
|
43
|
+
for (let version = startVersion; version < targetVersion; version += 1) {
|
|
44
|
+
const step = steps[version];
|
|
45
|
+
if (!step) throw new require_errors.DocumentEngineError("MIGRATION_FAILED", `There is no migration from schema version ${version}`, { migrationFrom: version });
|
|
46
|
+
try {
|
|
47
|
+
document = step(document, context);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new require_errors.DocumentEngineError("MIGRATION_FAILED", `Could not migrate the document from schema version ${version} to ${version + 1}: ${reason}`, {
|
|
51
|
+
cause: error,
|
|
52
|
+
migrationFrom: version
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (document.schemaVersion !== version + 1) throw new require_errors.DocumentEngineError("MIGRATION_FAILED", `The migration from schema version ${version} produced version ${String(document.schemaVersion)} instead of ${version + 1}`, { migrationFrom: version });
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
document,
|
|
59
|
+
migratedFrom: startVersion
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
exports.detectSchemaVersion = detectSchemaVersion;
|
|
64
|
+
exports.migrateDocument = migrateDocument;
|
|
65
|
+
exports.migrations = migrations;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/migrations/migrate-document.d.ts
|
|
2
|
+
export interface MigrationContext {
|
|
3
|
+
canvasWidth: number;
|
|
4
|
+
canvasHeight: number;
|
|
5
|
+
id?: string;
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export type Migration = (document: Record<string, unknown>, context: MigrationContext) => Record<string, unknown>;
|
|
9
|
+
export interface MigrationResult {
|
|
10
|
+
document: unknown;
|
|
11
|
+
migratedFrom: number | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare function detectSchemaVersion(value: unknown): number | undefined;
|
|
14
|
+
export declare function migrateDocument(value: unknown, context: MigrationContext, steps?: Readonly<Record<number, Migration>>, targetVersion?: number): MigrationResult;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/migrations/migrate-document.d.ts
|
|
2
|
+
export interface MigrationContext {
|
|
3
|
+
canvasWidth: number;
|
|
4
|
+
canvasHeight: number;
|
|
5
|
+
id?: string;
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export type Migration = (document: Record<string, unknown>, context: MigrationContext) => Record<string, unknown>;
|
|
9
|
+
export interface MigrationResult {
|
|
10
|
+
document: unknown;
|
|
11
|
+
migratedFrom: number | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare function detectSchemaVersion(value: unknown): number | undefined;
|
|
14
|
+
export declare function migrateDocument(value: unknown, context: MigrationContext, steps?: Readonly<Record<number, Migration>>, targetVersion?: number): MigrationResult;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DocumentEngineError } from "../engine/errors.js";
|
|
2
|
+
import "../document/document-format.js";
|
|
3
|
+
import { createId } from "../document/ids.js";
|
|
4
|
+
//#region src/migrations/migrate-document.ts
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function positiveNumberOr(value, fallback) {
|
|
9
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
function fromFabricJson(json, context) {
|
|
12
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13
|
+
const document = {
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
id: context.id ?? (typeof json.id === "string" && json.id.length > 0 ? json.id : createId()),
|
|
16
|
+
createdAt: now,
|
|
17
|
+
updatedAt: now,
|
|
18
|
+
revision: 0,
|
|
19
|
+
canvas: {
|
|
20
|
+
width: positiveNumberOr(json.width, context.canvasWidth),
|
|
21
|
+
height: positiveNumberOr(json.height, context.canvasHeight),
|
|
22
|
+
...json.background === void 0 ? {} : { background: json.background }
|
|
23
|
+
},
|
|
24
|
+
objects: json.objects,
|
|
25
|
+
metadata: { ...context.metadata }
|
|
26
|
+
};
|
|
27
|
+
if (typeof json.version === "string") document.fabricVersion = json.version;
|
|
28
|
+
return document;
|
|
29
|
+
}
|
|
30
|
+
const migrations = { 0: fromFabricJson };
|
|
31
|
+
function detectSchemaVersion(value) {
|
|
32
|
+
if (!isPlainObject(value)) return void 0;
|
|
33
|
+
if (typeof value.schemaVersion === "number" && Number.isInteger(value.schemaVersion)) return value.schemaVersion;
|
|
34
|
+
if (value.schemaVersion === void 0 && Array.isArray(value.objects)) return 0;
|
|
35
|
+
}
|
|
36
|
+
function migrateDocument(value, context, steps = migrations, targetVersion = 1) {
|
|
37
|
+
const startVersion = detectSchemaVersion(value);
|
|
38
|
+
if (startVersion === void 0 || startVersion >= targetVersion || startVersion < 0) return {
|
|
39
|
+
document: value,
|
|
40
|
+
migratedFrom: void 0
|
|
41
|
+
};
|
|
42
|
+
let document = JSON.parse(JSON.stringify(value));
|
|
43
|
+
for (let version = startVersion; version < targetVersion; version += 1) {
|
|
44
|
+
const step = steps[version];
|
|
45
|
+
if (!step) throw new DocumentEngineError("MIGRATION_FAILED", `There is no migration from schema version ${version}`, { migrationFrom: version });
|
|
46
|
+
try {
|
|
47
|
+
document = step(document, context);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new DocumentEngineError("MIGRATION_FAILED", `Could not migrate the document from schema version ${version} to ${version + 1}: ${reason}`, {
|
|
51
|
+
cause: error,
|
|
52
|
+
migrationFrom: version
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (document.schemaVersion !== version + 1) throw new DocumentEngineError("MIGRATION_FAILED", `The migration from schema version ${version} produced version ${String(document.schemaVersion)} instead of ${version + 1}`, { migrationFrom: version });
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
document,
|
|
59
|
+
migratedFrom: startVersion
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { detectSchemaVersion, migrateDocument, migrations };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
const require_document_version = require("../versions/document-version.cjs");
|
|
2
3
|
//#region src/storage/key-value-storage.ts
|
|
3
4
|
function readRevision(json) {
|
|
4
5
|
if (json === null) return 0;
|
|
@@ -6,7 +7,19 @@ function readRevision(json) {
|
|
|
6
7
|
return typeof revision === "number" ? revision : 0;
|
|
7
8
|
}
|
|
8
9
|
function createKeyValueStorage(store, prefix) {
|
|
10
|
+
const versionMarker = "::version::";
|
|
9
11
|
const keyFor = (id) => `${prefix}${id}`;
|
|
12
|
+
const versionPrefixFor = (documentId) => `${keyFor(documentId)}${versionMarker}`;
|
|
13
|
+
const versionKeyFor = (documentId, versionId) => `${versionPrefixFor(documentId)}${versionId}`;
|
|
14
|
+
function readVersion(documentId, versionId) {
|
|
15
|
+
const json = store.read(versionKeyFor(documentId, versionId));
|
|
16
|
+
if (json === null) throw new require_errors.DocumentEngineError("VERSION_NOT_FOUND", `Document "${documentId}" has no version "${versionId}"`);
|
|
17
|
+
return JSON.parse(json);
|
|
18
|
+
}
|
|
19
|
+
function versionKeysOf(documentId) {
|
|
20
|
+
const versionPrefix = versionPrefixFor(documentId);
|
|
21
|
+
return store.keys().filter((key) => key.startsWith(versionPrefix));
|
|
22
|
+
}
|
|
10
23
|
return {
|
|
11
24
|
async loadDocument(id) {
|
|
12
25
|
const json = store.read(keyFor(id));
|
|
@@ -24,10 +37,23 @@ function createKeyValueStorage(store, prefix) {
|
|
|
24
37
|
return { revision };
|
|
25
38
|
},
|
|
26
39
|
async listDocuments() {
|
|
27
|
-
return store.keys().filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
|
|
40
|
+
return store.keys().filter((key) => key.startsWith(prefix) && !key.includes(versionMarker)).map((key) => key.slice(prefix.length));
|
|
28
41
|
},
|
|
29
42
|
async deleteDocument(id) {
|
|
43
|
+
versionKeysOf(id).forEach((key) => store.remove(key));
|
|
30
44
|
store.remove(keyFor(id));
|
|
45
|
+
},
|
|
46
|
+
async saveVersion(version) {
|
|
47
|
+
store.write(versionKeyFor(version.documentId, version.id), JSON.stringify(version));
|
|
48
|
+
},
|
|
49
|
+
async listVersions(documentId) {
|
|
50
|
+
return versionKeysOf(documentId).map((key) => require_document_version.summarize(JSON.parse(store.read(key) ?? "null"))).sort(require_document_version.newestFirst);
|
|
51
|
+
},
|
|
52
|
+
async loadVersion(documentId, versionId) {
|
|
53
|
+
return readVersion(documentId, versionId);
|
|
54
|
+
},
|
|
55
|
+
async deleteVersion(documentId, versionId) {
|
|
56
|
+
store.remove(versionKeyFor(documentId, versionId));
|
|
31
57
|
}
|
|
32
58
|
};
|
|
33
59
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { VersionStorage } from "../versions/document-version.cjs";
|
|
1
2
|
import { DocumentStorage } from "./storage-contract.cjs";
|
|
2
3
|
//#region src/storage/key-value-storage.d.ts
|
|
3
4
|
export interface KeyValueStore {
|
|
@@ -6,7 +7,7 @@ export interface KeyValueStore {
|
|
|
6
7
|
remove(key: string): void;
|
|
7
8
|
keys(): string[];
|
|
8
9
|
}
|
|
9
|
-
export interface ManagedDocumentStorage extends DocumentStorage {
|
|
10
|
+
export interface ManagedDocumentStorage extends DocumentStorage, VersionStorage {
|
|
10
11
|
listDocuments(): Promise<string[]>;
|
|
11
12
|
deleteDocument(id: string): Promise<void>;
|
|
12
13
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { VersionStorage } from "../versions/document-version.js";
|
|
1
2
|
import { DocumentStorage } from "./storage-contract.js";
|
|
2
3
|
//#region src/storage/key-value-storage.d.ts
|
|
3
4
|
export interface KeyValueStore {
|
|
@@ -6,7 +7,7 @@ export interface KeyValueStore {
|
|
|
6
7
|
remove(key: string): void;
|
|
7
8
|
keys(): string[];
|
|
8
9
|
}
|
|
9
|
-
export interface ManagedDocumentStorage extends DocumentStorage {
|
|
10
|
+
export interface ManagedDocumentStorage extends DocumentStorage, VersionStorage {
|
|
10
11
|
listDocuments(): Promise<string[]>;
|
|
11
12
|
deleteDocument(id: string): Promise<void>;
|
|
12
13
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DocumentEngineError, createConflictError } from "../engine/errors.js";
|
|
2
|
+
import { newestFirst, summarize } from "../versions/document-version.js";
|
|
2
3
|
//#region src/storage/key-value-storage.ts
|
|
3
4
|
function readRevision(json) {
|
|
4
5
|
if (json === null) return 0;
|
|
@@ -6,7 +7,19 @@ function readRevision(json) {
|
|
|
6
7
|
return typeof revision === "number" ? revision : 0;
|
|
7
8
|
}
|
|
8
9
|
function createKeyValueStorage(store, prefix) {
|
|
10
|
+
const versionMarker = "::version::";
|
|
9
11
|
const keyFor = (id) => `${prefix}${id}`;
|
|
12
|
+
const versionPrefixFor = (documentId) => `${keyFor(documentId)}${versionMarker}`;
|
|
13
|
+
const versionKeyFor = (documentId, versionId) => `${versionPrefixFor(documentId)}${versionId}`;
|
|
14
|
+
function readVersion(documentId, versionId) {
|
|
15
|
+
const json = store.read(versionKeyFor(documentId, versionId));
|
|
16
|
+
if (json === null) throw new DocumentEngineError("VERSION_NOT_FOUND", `Document "${documentId}" has no version "${versionId}"`);
|
|
17
|
+
return JSON.parse(json);
|
|
18
|
+
}
|
|
19
|
+
function versionKeysOf(documentId) {
|
|
20
|
+
const versionPrefix = versionPrefixFor(documentId);
|
|
21
|
+
return store.keys().filter((key) => key.startsWith(versionPrefix));
|
|
22
|
+
}
|
|
10
23
|
return {
|
|
11
24
|
async loadDocument(id) {
|
|
12
25
|
const json = store.read(keyFor(id));
|
|
@@ -24,10 +37,23 @@ function createKeyValueStorage(store, prefix) {
|
|
|
24
37
|
return { revision };
|
|
25
38
|
},
|
|
26
39
|
async listDocuments() {
|
|
27
|
-
return store.keys().filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
|
|
40
|
+
return store.keys().filter((key) => key.startsWith(prefix) && !key.includes(versionMarker)).map((key) => key.slice(prefix.length));
|
|
28
41
|
},
|
|
29
42
|
async deleteDocument(id) {
|
|
43
|
+
versionKeysOf(id).forEach((key) => store.remove(key));
|
|
30
44
|
store.remove(keyFor(id));
|
|
45
|
+
},
|
|
46
|
+
async saveVersion(version) {
|
|
47
|
+
store.write(versionKeyFor(version.documentId, version.id), JSON.stringify(version));
|
|
48
|
+
},
|
|
49
|
+
async listVersions(documentId) {
|
|
50
|
+
return versionKeysOf(documentId).map((key) => summarize(JSON.parse(store.read(key) ?? "null"))).sort(newestFirst);
|
|
51
|
+
},
|
|
52
|
+
async loadVersion(documentId, versionId) {
|
|
53
|
+
return readVersion(documentId, versionId);
|
|
54
|
+
},
|
|
55
|
+
async deleteVersion(documentId, versionId) {
|
|
56
|
+
store.remove(versionKeyFor(documentId, versionId));
|
|
31
57
|
}
|
|
32
58
|
};
|
|
33
59
|
}
|
package/dist/storage.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { DocumentVersion, VersionStorage, VersionSummary } from "./versions/document-version.cjs";
|
|
1
2
|
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.cjs";
|
|
2
3
|
import { KeyValueStore, ManagedDocumentStorage, createKeyValueStorage } from "./storage/key-value-storage.cjs";
|
|
3
4
|
import { createMemoryStorage } from "./storage/memory-storage.cjs";
|
|
4
5
|
import { LocalStorageOptions, createLocalStorage } from "./storage/local-storage.cjs";
|
|
5
|
-
export { type DocumentStorage, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, createKeyValueStorage, createLocalStorage, createMemoryStorage };
|
|
6
|
+
export { type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage };
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { DocumentVersion, VersionStorage, VersionSummary } from "./versions/document-version.js";
|
|
1
2
|
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.js";
|
|
2
3
|
import { KeyValueStore, ManagedDocumentStorage, createKeyValueStorage } from "./storage/key-value-storage.js";
|
|
3
4
|
import { createMemoryStorage } from "./storage/memory-storage.js";
|
|
4
5
|
import { LocalStorageOptions, createLocalStorage } from "./storage/local-storage.js";
|
|
5
|
-
export { type DocumentStorage, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, createKeyValueStorage, createLocalStorage, createMemoryStorage };
|
|
6
|
+
export { type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/versions/document-version.ts
|
|
2
|
+
function summarize(version) {
|
|
3
|
+
const { document: _document, ...summary } = version;
|
|
4
|
+
return summary;
|
|
5
|
+
}
|
|
6
|
+
function newestFirst(first, second) {
|
|
7
|
+
return second.createdAt.localeCompare(first.createdAt);
|
|
8
|
+
}
|
|
9
|
+
function versionsToPrune(versions, keepAuto) {
|
|
10
|
+
return versions.filter((version) => version.kind === "auto").sort(newestFirst).slice(Math.max(0, keepAuto));
|
|
11
|
+
}
|
|
12
|
+
function supportsVersions(storage) {
|
|
13
|
+
const candidate = storage;
|
|
14
|
+
return typeof candidate?.saveVersion === "function" && typeof candidate.listVersions === "function" && typeof candidate.loadVersion === "function" && typeof candidate.deleteVersion === "function";
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
exports.newestFirst = newestFirst;
|
|
18
|
+
exports.summarize = summarize;
|
|
19
|
+
exports.supportsVersions = supportsVersions;
|
|
20
|
+
exports.versionsToPrune = versionsToPrune;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FabricDocument } from "../document/document-format.cjs";
|
|
2
|
+
//#region src/versions/document-version.d.ts
|
|
3
|
+
export type VersionKind = "named" | "auto";
|
|
4
|
+
export interface VersionSummary {
|
|
5
|
+
id: string;
|
|
6
|
+
documentId: string;
|
|
7
|
+
name: string;
|
|
8
|
+
kind: VersionKind;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
revision: number;
|
|
11
|
+
}
|
|
12
|
+
export interface DocumentVersion extends VersionSummary {
|
|
13
|
+
document: FabricDocument;
|
|
14
|
+
}
|
|
15
|
+
export interface VersionStorage {
|
|
16
|
+
saveVersion(version: DocumentVersion): Promise<void>;
|
|
17
|
+
listVersions(documentId: string): Promise<VersionSummary[]>;
|
|
18
|
+
loadVersion(documentId: string, versionId: string): Promise<DocumentVersion>;
|
|
19
|
+
deleteVersion(documentId: string, versionId: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface VersionOptions {
|
|
22
|
+
keepAuto?: number;
|
|
23
|
+
autoEvery?: number;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FabricDocument } from "../document/document-format.js";
|
|
2
|
+
//#region src/versions/document-version.d.ts
|
|
3
|
+
export type VersionKind = "named" | "auto";
|
|
4
|
+
export interface VersionSummary {
|
|
5
|
+
id: string;
|
|
6
|
+
documentId: string;
|
|
7
|
+
name: string;
|
|
8
|
+
kind: VersionKind;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
revision: number;
|
|
11
|
+
}
|
|
12
|
+
export interface DocumentVersion extends VersionSummary {
|
|
13
|
+
document: FabricDocument;
|
|
14
|
+
}
|
|
15
|
+
export interface VersionStorage {
|
|
16
|
+
saveVersion(version: DocumentVersion): Promise<void>;
|
|
17
|
+
listVersions(documentId: string): Promise<VersionSummary[]>;
|
|
18
|
+
loadVersion(documentId: string, versionId: string): Promise<DocumentVersion>;
|
|
19
|
+
deleteVersion(documentId: string, versionId: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface VersionOptions {
|
|
22
|
+
keepAuto?: number;
|
|
23
|
+
autoEvery?: number;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/versions/document-version.ts
|
|
2
|
+
function summarize(version) {
|
|
3
|
+
const { document: _document, ...summary } = version;
|
|
4
|
+
return summary;
|
|
5
|
+
}
|
|
6
|
+
function newestFirst(first, second) {
|
|
7
|
+
return second.createdAt.localeCompare(first.createdAt);
|
|
8
|
+
}
|
|
9
|
+
function versionsToPrune(versions, keepAuto) {
|
|
10
|
+
return versions.filter((version) => version.kind === "auto").sort(newestFirst).slice(Math.max(0, keepAuto));
|
|
11
|
+
}
|
|
12
|
+
function supportsVersions(storage) {
|
|
13
|
+
const candidate = storage;
|
|
14
|
+
return typeof candidate?.saveVersion === "function" && typeof candidate.listVersions === "function" && typeof candidate.loadVersion === "function" && typeof candidate.deleteVersion === "function";
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { newestFirst, summarize, supportsVersions, versionsToPrune };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabricjs-document-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Turn an existing Fabric.js canvas into a dependable editable document: stable object ids, versioned save and load, undo and redo, safe autosave with conflict protection, custom objects and clear errors.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fabric",
|