fabricjs-document-engine 0.8.0 → 1.0.1
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 +20 -93
- package/dist/engine/create-document-engine.cjs +28 -5
- package/dist/engine/create-document-engine.d.cts +5 -1
- package/dist/engine/create-document-engine.d.ts +5 -1
- package/dist/engine/create-document-engine.js +28 -5
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/history/create-history.cjs +1 -0
- package/dist/history/create-history.js +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/recovery/recovery-controller.cjs +1 -0
- package/dist/recovery/recovery-controller.js +1 -0
- package/dist/storage/verify-storage-adapter.cjs +125 -0
- package/dist/storage/verify-storage-adapter.d.cts +13 -0
- package/dist/storage/verify-storage-adapter.d.ts +13 -0
- package/dist/storage/verify-storage-adapter.js +125 -0
- package/dist/storage.cjs +2 -0
- package/dist/storage.d.cts +2 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +2 -1
- package/package.json +4 -1
- package/schema/document-v1.schema.json +69 -0
package/README.md
CHANGED
|
@@ -156,6 +156,7 @@ engine.on('save:status', ({ status, isDirty, revision, lastSavedAt, error }) =>
|
|
|
156
156
|
- **Stale responses.** If another document is loaded while a save is running, that save's response is ignored and any queued save is cancelled with `SAVE_CANCELLED`.
|
|
157
157
|
- **Conflicts.** When another tab or device saved first, the save fails with `SAVE_CONFLICT` and the status becomes `conflict`. Either reload the document with `engine.load(id)`, or keep your version with `engine.save({ overwrite: true })`.
|
|
158
158
|
- **Retries.** Temporary failures are retried with exponential backoff and jitter. Each retry emits `save:retry` with `{ attempt, delay, error }`.
|
|
159
|
+
- **Unsaved work is never replaced silently.** With a storage adapter, `load`, `loadDocument`, `importFabricJson` and `newDocument` refuse with `UNSAVED_CHANGES` while there are unsaved changes. Save first, or pass `{ discardUnsavedChanges: true }` when the user chose to throw the changes away.
|
|
159
160
|
|
|
160
161
|
### Warn before leaving
|
|
161
162
|
|
|
@@ -402,99 +403,25 @@ interface FabricDocument {
|
|
|
402
403
|
|
|
403
404
|
Keep project data such as titles, owners and tags in `metadata` with `engine.updateMetadata()`, rather than on Fabric objects.
|
|
404
405
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
| `engine.getAssetManifest()` | Lists the images and fonts used on the canvas. |
|
|
425
|
-
| `engine.checkAssets()` | Resolves to `{ manifest, missingImages, unavailableFonts, warnings }` for the current canvas. |
|
|
426
|
-
| `engine.replaceImage(oldUrl, newUrl)` | Replaces every image with that URL as one undo step. Resolves to the number of images replaced. |
|
|
427
|
-
| `engine.export(options)` | Exports PNG, JPEG, WebP, SVG or JSON. See [Export](#export). |
|
|
428
|
-
| `engine.preflightExport(options)` | Resolves to `{ ok, problems, warnings }` without exporting. |
|
|
429
|
-
| `downloadExport(result, fileName?)` | Starts a browser download of an export result. |
|
|
430
|
-
| `engine.getRecoverableDocuments()` | Lists recovery copies, newest first. |
|
|
431
|
-
| `engine.restoreRecovery(id?)` / `engine.discardRecovery(id?)` | Loads or deletes a recovery copy. The default is the current document. |
|
|
432
|
-
| `engine.getRecovery(id?)` / `engine.flushRecovery()` | Reads a copy, or writes one now. |
|
|
433
|
-
| `engine.getInterruptedLoad()` | Returns the load that was running when the tab last crashed, if any. |
|
|
434
|
-
| `engine.transaction(label, work)` | Runs `work` and records everything it changed as one undo step. Returns what `work` returns. |
|
|
435
|
-
| `engine.commit(label?)` | Records changes made since the last step. Returns `false` when nothing changed. |
|
|
436
|
-
| `engine.undo()` / `engine.redo()` | Resolves to `true` when a step was applied. Calls run one after another. |
|
|
437
|
-
| `engine.canUndo()` / `engine.canRedo()` | Tells you whether a step is available. |
|
|
438
|
-
| `engine.getHistory()` | Returns `{ undo, redo }` label lists, newest first. |
|
|
439
|
-
| `engine.clearHistory()` | Forgets all steps. Loading a document or starting a new one also does this. |
|
|
440
|
-
| `bindKeyboardShortcuts(engine, { target? })` | Adds the undo and redo shortcuts. Returns an unbind function. |
|
|
441
|
-
| `engine.on(event, handler)` | Listens to `load:start`, `document:change`, `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. |
|
|
442
|
-
| `engine.destroy()` | Stops listening to the canvas and cancels a running load. |
|
|
443
|
-
| `secureDocument(value, limits?)` / `refuseUnsafeImageUrls(objects, isAllowed?)` / `isSafeImageUrl(url)` | The safety checks, exported for server-side validation. |
|
|
444
|
-
| `createDocumentStateStore(engine)` | Framework-free `{ getSnapshot, subscribe, destroy }` state for toolbars. |
|
|
445
|
-
| `validateDocument(value)` | Returns a list of issues with the exact path of each problem. |
|
|
446
|
-
|
|
447
|
-
## Errors
|
|
448
|
-
|
|
449
|
-
Every failure is a `DocumentEngineError` with a `code` you can switch on:
|
|
450
|
-
|
|
451
|
-
| Code | Meaning |
|
|
452
|
-
| --- | --- |
|
|
453
|
-
| `UNSAFE_DOCUMENT` | The document broke a safety rule: an unsafe image address, too many objects, or nesting too deep. `error.issues` lists each one. |
|
|
454
|
-
| `INVALID_DOCUMENT` | The document shape is wrong. `error.issues` lists each path, such as `objects[3].objects[1].type`. |
|
|
455
|
-
| `UNSUPPORTED_SCHEMA` | The document was written by a newer version of this package. |
|
|
456
|
-
| `UNKNOWN_OBJECT_TYPE` | A type is not registered. `error.unknownTypes` lists them. |
|
|
457
|
-
| `LOAD_FAILED` | Fabric or your storage could not load the document, for example because an image is missing. `error.cause` holds the original error. |
|
|
458
|
-
| `LOAD_ABORTED` | A newer load started before this one finished. |
|
|
459
|
-
| `MISSING_ASSETS` | Images could not be loaded and had no replacement. `error.missingAssets` lists each `{ url, objectIds }`. |
|
|
460
|
-
| `MISSING_FONTS` | Fonts are not available and `requireFonts` is on. `error.missingFonts` lists them. |
|
|
461
|
-
| `ASSET_UPLOAD_FAILED` | Your `upload` handler failed while saving. |
|
|
462
|
-
| `SAVE_FAILED` | Your storage adapter rejected the save after all retries. `error.retryable` tells you whether trying again could help. |
|
|
463
|
-
| `SAVE_CONFLICT` | Another tab or device saved this document first. |
|
|
464
|
-
| `SAVE_CANCELLED` | A queued save was dropped because another document was opened. |
|
|
465
|
-
| `DOCUMENT_NOT_FOUND` | The built-in adapters have no document with that id. |
|
|
466
|
-
| `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. |
|
|
467
|
-
| `MIGRATION_FAILED` | An older document could not be upgraded. `error.migrationFrom` is the schema version it started from. |
|
|
468
|
-
| `VERSIONS_UNSUPPORTED` | The storage adapter has no version methods. |
|
|
469
|
-
| `VERSION_NOT_FOUND` | There is no version with that id. |
|
|
470
|
-
| `VERSION_FAILED` | An automatic version could not be kept. It is delivered as a `version:error` event. |
|
|
471
|
-
| `EXPORT_BLOCKED` | The preflight found problems. `error.problems` lists each one with the objects involved. |
|
|
472
|
-
| `INVALID_EXPORT_OPTIONS` | The format, scale, quality, area or padding is not valid, or the area is empty. |
|
|
473
|
-
| `EXPORT_ABORTED` | The export was cancelled with its `signal`. |
|
|
474
|
-
| `EXPORT_FAILED` | Fabric could not render the export. `error.cause` holds the original error. |
|
|
475
|
-
| `RECOVERY_MISSING` | A recovery method was called without `recovery: { store }`. |
|
|
476
|
-
| `RECOVERY_NOT_FOUND` | There is no recovery copy for that document. |
|
|
477
|
-
| `RECOVERY_FAILED` | Writing a recovery copy failed, for example because storage is full. It is delivered as a `recovery:error` event and never interrupts editing. |
|
|
478
|
-
| `STORAGE_MISSING` | `load` or `save` was called without a storage adapter. |
|
|
479
|
-
| `INVALID_CUSTOM_OBJECT` | A registered class has no static `type`, or it does not extend a Fabric class. |
|
|
480
|
-
| `ENGINE_DESTROYED` | The engine was used after `destroy()`. |
|
|
481
|
-
|
|
482
|
-
A failed load never clears or half-fills your canvas.
|
|
483
|
-
|
|
484
|
-
## Roadmap
|
|
485
|
-
|
|
486
|
-
| Stage | Focus | Released in |
|
|
487
|
-
| --- | --- | --- |
|
|
488
|
-
| 1 | Document foundation: ids, save and load, validation, custom objects | 0.0.0 |
|
|
489
|
-
| 2 | Undo and redo with transactions | 0.1.0 |
|
|
490
|
-
| 3 | Safe saving: dirty state, autosave, stale-response protection | 0.2.0 |
|
|
491
|
-
| 4 | Assets and fonts | 0.3.0 |
|
|
492
|
-
| 5 | Recovery after a refresh or crash | 0.4.0 |
|
|
493
|
-
| 6 | PNG, JPEG, SVG and JSON export with preflight checks | 0.5.0 |
|
|
494
|
-
| 7 | Named versions and schema migrations | 0.6.0 |
|
|
495
|
-
| 8 | React adapter and examples | 0.7.0 |
|
|
496
|
-
| 9 | Hardening and benchmarks | 0.8.0 |
|
|
497
|
-
| 10 | Stable API | 1.0.0 |
|
|
406
|
+
The format is described by a JSON Schema that ships with the package:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
import schema from 'fabricjs-document-engine/schema/document-v1.json';
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## API reference
|
|
413
|
+
|
|
414
|
+
Every function, option, event and error code is listed in [docs/api.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/api.md). Every failure is a `DocumentEngineError` with a stable `code` you can switch on, such as `SAVE_CONFLICT`, `MISSING_ASSETS` or `UNSAVED_CHANGES`. A failed load never clears or half-fills your canvas.
|
|
415
|
+
|
|
416
|
+
## Stability
|
|
417
|
+
|
|
418
|
+
Version 1.0 freezes the document format and the adapter contracts:
|
|
419
|
+
|
|
420
|
+
- Documents are validated by the published JSON Schema at `fabricjs-document-engine/schema/document-v1.json`. Every 1.x release reads every document written by earlier releases, as well as plain Fabric JSON from Fabric 5, 6 and 7.
|
|
421
|
+
- Public API names, options, events and error codes do not change within 1.x. New ones may be added.
|
|
422
|
+
- Storage, version and recovery adapters written for 1.0 keep working. `verifyStorageAdapter(storage)` from `fabricjs-document-engine/storage` checks that your adapter follows the save rules.
|
|
423
|
+
|
|
424
|
+
The full promise is in the [compatibility policy](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility-policy.md). Tested browsers, Fabric versions and performance results are in [docs/compatibility.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility.md), and a complete setup is in the [production guide](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/production.md).
|
|
498
425
|
|
|
499
426
|
## Imported content and limits
|
|
500
427
|
|
|
@@ -163,11 +163,20 @@ function createDocumentEngine(options) {
|
|
|
163
163
|
function loadDocument(input, loadOptions = {}) {
|
|
164
164
|
return loadMigratedDocument(input, loadOptions, {});
|
|
165
165
|
}
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
function protectUnsavedChanges(discardUnsavedChanges) {
|
|
167
|
+
if (!storage || discardUnsavedChanges || !saving.state().isDirty) return;
|
|
168
|
+
throw new require_errors.DocumentEngineError("UNSAVED_CHANGES", "The current document has unsaved changes. Save first, or pass { discardUnsavedChanges: true } to replace it anyway.");
|
|
169
|
+
}
|
|
170
|
+
function startLoad() {
|
|
168
171
|
activeLoad?.abort();
|
|
169
172
|
const controller = new AbortController();
|
|
170
173
|
activeLoad = controller;
|
|
174
|
+
return controller;
|
|
175
|
+
}
|
|
176
|
+
async function loadMigratedDocument(input, loadOptions, importDetails, startedLoad) {
|
|
177
|
+
ensureUsable();
|
|
178
|
+
protectUnsavedChanges(loadOptions.discardUnsavedChanges);
|
|
179
|
+
const controller = startedLoad ?? startLoad();
|
|
171
180
|
const documentId = describeDocumentId(input);
|
|
172
181
|
events.emit("load:start", { documentId });
|
|
173
182
|
try {
|
|
@@ -297,7 +306,7 @@ function createDocumentEngine(options) {
|
|
|
297
306
|
id: current.id,
|
|
298
307
|
createdAt: current.createdAt,
|
|
299
308
|
revision: baseRevision
|
|
300
|
-
});
|
|
309
|
+
}, { discardUnsavedChanges: true });
|
|
301
310
|
noteContentChange();
|
|
302
311
|
events.emit("version:restored", {
|
|
303
312
|
version: require_document_version.summarize(version),
|
|
@@ -312,16 +321,25 @@ function createDocumentEngine(options) {
|
|
|
312
321
|
async function load(documentId, loadOptions) {
|
|
313
322
|
ensureUsable();
|
|
314
323
|
const source = requireStorage();
|
|
324
|
+
protectUnsavedChanges(loadOptions?.discardUnsavedChanges);
|
|
325
|
+
const controller = startLoad();
|
|
315
326
|
let stored;
|
|
316
327
|
try {
|
|
317
328
|
stored = await source.loadDocument(documentId);
|
|
329
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
318
330
|
} catch (error) {
|
|
331
|
+
if (activeLoad === controller) activeLoad = null;
|
|
332
|
+
if (controller.signal.aborted) {
|
|
333
|
+
const aborted = toLoadError(error, controller.signal);
|
|
334
|
+
events.emit("load:error", { error: aborted });
|
|
335
|
+
throw aborted;
|
|
336
|
+
}
|
|
319
337
|
const reason = error instanceof Error ? error.message : String(error);
|
|
320
338
|
const engineError = require_errors.isDocumentEngineError(error) ? error : new require_errors.DocumentEngineError("LOAD_FAILED", `Storage could not load "${documentId}": ${reason}`, { cause: error });
|
|
321
339
|
events.emit("load:error", { error: engineError });
|
|
322
340
|
throw engineError;
|
|
323
341
|
}
|
|
324
|
-
return loadMigratedDocument(stored, loadOptions ?? {}, { id: documentId });
|
|
342
|
+
return loadMigratedDocument(stored, loadOptions ?? {}, { id: documentId }, controller);
|
|
325
343
|
}
|
|
326
344
|
function save(saveOptions) {
|
|
327
345
|
try {
|
|
@@ -329,10 +347,12 @@ function createDocumentEngine(options) {
|
|
|
329
347
|
} catch (error) {
|
|
330
348
|
return Promise.reject(error);
|
|
331
349
|
}
|
|
350
|
+
history.flush();
|
|
332
351
|
return saving.save(saveOptions);
|
|
333
352
|
}
|
|
334
|
-
function newDocument(newOptions) {
|
|
353
|
+
function newDocument(newOptions = {}) {
|
|
335
354
|
ensureUsable();
|
|
355
|
+
protectUnsavedChanges(newOptions.discardUnsavedChanges);
|
|
336
356
|
activeLoad?.abort();
|
|
337
357
|
history.withoutRecording(() => canvas.clear());
|
|
338
358
|
objectsById.clear();
|
|
@@ -488,6 +508,8 @@ function createDocumentEngine(options) {
|
|
|
488
508
|
}
|
|
489
509
|
function destroy() {
|
|
490
510
|
if (destroyed) return;
|
|
511
|
+
const { disposed } = canvas;
|
|
512
|
+
if (recovery && saving.state().isDirty && !disposed) recovery.writeNow();
|
|
491
513
|
destroyed = true;
|
|
492
514
|
activeLoad?.abort();
|
|
493
515
|
history.destroy();
|
|
@@ -561,6 +583,7 @@ function createDocumentEngine(options) {
|
|
|
561
583
|
getHistory: () => history.labels(),
|
|
562
584
|
clearHistory() {
|
|
563
585
|
ensureUsable();
|
|
586
|
+
history.flush();
|
|
564
587
|
history.reset();
|
|
565
588
|
},
|
|
566
589
|
on: (name, handler) => events.on(name, handler),
|
|
@@ -32,6 +32,10 @@ export interface DocumentEngineOptions {
|
|
|
32
32
|
}
|
|
33
33
|
export interface LoadOptions {
|
|
34
34
|
restoreCanvasSize?: boolean;
|
|
35
|
+
discardUnsavedChanges?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface NewDocumentRequest extends NewDocumentOptions {
|
|
38
|
+
discardUnsavedChanges?: boolean;
|
|
35
39
|
}
|
|
36
40
|
export interface ImportOptions extends LoadOptions {
|
|
37
41
|
id?: string;
|
|
@@ -111,7 +115,7 @@ export interface DocumentEngine {
|
|
|
111
115
|
readonly canvas: StaticCanvas;
|
|
112
116
|
getDocumentInfo(): DocumentInfo;
|
|
113
117
|
updateMetadata(changes: Record<string, unknown>): void;
|
|
114
|
-
newDocument(options?:
|
|
118
|
+
newDocument(options?: NewDocumentRequest): void;
|
|
115
119
|
toDocument(): FabricDocument;
|
|
116
120
|
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
117
121
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
@@ -32,6 +32,10 @@ export interface DocumentEngineOptions {
|
|
|
32
32
|
}
|
|
33
33
|
export interface LoadOptions {
|
|
34
34
|
restoreCanvasSize?: boolean;
|
|
35
|
+
discardUnsavedChanges?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface NewDocumentRequest extends NewDocumentOptions {
|
|
38
|
+
discardUnsavedChanges?: boolean;
|
|
35
39
|
}
|
|
36
40
|
export interface ImportOptions extends LoadOptions {
|
|
37
41
|
id?: string;
|
|
@@ -111,7 +115,7 @@ export interface DocumentEngine {
|
|
|
111
115
|
readonly canvas: StaticCanvas;
|
|
112
116
|
getDocumentInfo(): DocumentInfo;
|
|
113
117
|
updateMetadata(changes: Record<string, unknown>): void;
|
|
114
|
-
newDocument(options?:
|
|
118
|
+
newDocument(options?: NewDocumentRequest): void;
|
|
115
119
|
toDocument(): FabricDocument;
|
|
116
120
|
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
117
121
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
@@ -163,11 +163,20 @@ function createDocumentEngine(options) {
|
|
|
163
163
|
function loadDocument(input, loadOptions = {}) {
|
|
164
164
|
return loadMigratedDocument(input, loadOptions, {});
|
|
165
165
|
}
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
function protectUnsavedChanges(discardUnsavedChanges) {
|
|
167
|
+
if (!storage || discardUnsavedChanges || !saving.state().isDirty) return;
|
|
168
|
+
throw new DocumentEngineError("UNSAVED_CHANGES", "The current document has unsaved changes. Save first, or pass { discardUnsavedChanges: true } to replace it anyway.");
|
|
169
|
+
}
|
|
170
|
+
function startLoad() {
|
|
168
171
|
activeLoad?.abort();
|
|
169
172
|
const controller = new AbortController();
|
|
170
173
|
activeLoad = controller;
|
|
174
|
+
return controller;
|
|
175
|
+
}
|
|
176
|
+
async function loadMigratedDocument(input, loadOptions, importDetails, startedLoad) {
|
|
177
|
+
ensureUsable();
|
|
178
|
+
protectUnsavedChanges(loadOptions.discardUnsavedChanges);
|
|
179
|
+
const controller = startedLoad ?? startLoad();
|
|
171
180
|
const documentId = describeDocumentId(input);
|
|
172
181
|
events.emit("load:start", { documentId });
|
|
173
182
|
try {
|
|
@@ -297,7 +306,7 @@ function createDocumentEngine(options) {
|
|
|
297
306
|
id: current.id,
|
|
298
307
|
createdAt: current.createdAt,
|
|
299
308
|
revision: baseRevision
|
|
300
|
-
});
|
|
309
|
+
}, { discardUnsavedChanges: true });
|
|
301
310
|
noteContentChange();
|
|
302
311
|
events.emit("version:restored", {
|
|
303
312
|
version: summarize(version),
|
|
@@ -312,16 +321,25 @@ function createDocumentEngine(options) {
|
|
|
312
321
|
async function load(documentId, loadOptions) {
|
|
313
322
|
ensureUsable();
|
|
314
323
|
const source = requireStorage();
|
|
324
|
+
protectUnsavedChanges(loadOptions?.discardUnsavedChanges);
|
|
325
|
+
const controller = startLoad();
|
|
315
326
|
let stored;
|
|
316
327
|
try {
|
|
317
328
|
stored = await source.loadDocument(documentId);
|
|
329
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
318
330
|
} catch (error) {
|
|
331
|
+
if (activeLoad === controller) activeLoad = null;
|
|
332
|
+
if (controller.signal.aborted) {
|
|
333
|
+
const aborted = toLoadError(error, controller.signal);
|
|
334
|
+
events.emit("load:error", { error: aborted });
|
|
335
|
+
throw aborted;
|
|
336
|
+
}
|
|
319
337
|
const reason = error instanceof Error ? error.message : String(error);
|
|
320
338
|
const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("LOAD_FAILED", `Storage could not load "${documentId}": ${reason}`, { cause: error });
|
|
321
339
|
events.emit("load:error", { error: engineError });
|
|
322
340
|
throw engineError;
|
|
323
341
|
}
|
|
324
|
-
return loadMigratedDocument(stored, loadOptions ?? {}, { id: documentId });
|
|
342
|
+
return loadMigratedDocument(stored, loadOptions ?? {}, { id: documentId }, controller);
|
|
325
343
|
}
|
|
326
344
|
function save(saveOptions) {
|
|
327
345
|
try {
|
|
@@ -329,10 +347,12 @@ function createDocumentEngine(options) {
|
|
|
329
347
|
} catch (error) {
|
|
330
348
|
return Promise.reject(error);
|
|
331
349
|
}
|
|
350
|
+
history.flush();
|
|
332
351
|
return saving.save(saveOptions);
|
|
333
352
|
}
|
|
334
|
-
function newDocument(newOptions) {
|
|
353
|
+
function newDocument(newOptions = {}) {
|
|
335
354
|
ensureUsable();
|
|
355
|
+
protectUnsavedChanges(newOptions.discardUnsavedChanges);
|
|
336
356
|
activeLoad?.abort();
|
|
337
357
|
history.withoutRecording(() => canvas.clear());
|
|
338
358
|
objectsById.clear();
|
|
@@ -488,6 +508,8 @@ function createDocumentEngine(options) {
|
|
|
488
508
|
}
|
|
489
509
|
function destroy() {
|
|
490
510
|
if (destroyed) return;
|
|
511
|
+
const { disposed } = canvas;
|
|
512
|
+
if (recovery && saving.state().isDirty && !disposed) recovery.writeNow();
|
|
491
513
|
destroyed = true;
|
|
492
514
|
activeLoad?.abort();
|
|
493
515
|
history.destroy();
|
|
@@ -561,6 +583,7 @@ function createDocumentEngine(options) {
|
|
|
561
583
|
getHistory: () => history.labels(),
|
|
562
584
|
clearHistory() {
|
|
563
585
|
ensureUsable();
|
|
586
|
+
history.flush();
|
|
564
587
|
history.reset();
|
|
565
588
|
},
|
|
566
589
|
on: (name, handler) => events.on(name, handler),
|
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" | "UNSAFE_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";
|
|
4
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "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;
|
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" | "UNSAFE_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";
|
|
4
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "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;
|
package/dist/index.d.cts
CHANGED
|
@@ -17,11 +17,11 @@ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, Doc
|
|
|
17
17
|
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.cjs";
|
|
18
18
|
import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.cjs";
|
|
19
19
|
import { Unsubscribe } from "./engine/event-emitter.cjs";
|
|
20
|
-
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
20
|
+
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, NewDocumentRequest, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
21
21
|
import { validateDocument } from "./document/validate-document.cjs";
|
|
22
22
|
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.cjs";
|
|
23
23
|
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.cjs";
|
|
24
24
|
import { DownloadableExport, downloadExport } from "./export/download-export.cjs";
|
|
25
25
|
import { Migration, MigrationContext, MigrationResult, detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.cjs";
|
|
26
26
|
import { DocumentState, DocumentStateStore, createDocumentStateStore } from "./state/document-state-store.cjs";
|
|
27
|
-
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type ContentLimits, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, 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, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, isSafeImageUrl, migrateDocument, refuseUnsafeImageUrls, secureDocument, validateDocument };
|
|
27
|
+
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type ContentLimits, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, 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 NewDocumentRequest, 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, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, isSafeImageUrl, migrateDocument, refuseUnsafeImageUrls, secureDocument, validateDocument };
|
package/dist/index.d.ts
CHANGED
|
@@ -17,11 +17,11 @@ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, Doc
|
|
|
17
17
|
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.js";
|
|
18
18
|
import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.js";
|
|
19
19
|
import { Unsubscribe } from "./engine/event-emitter.js";
|
|
20
|
-
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
|
|
20
|
+
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, NewDocumentRequest, createDocumentEngine } from "./engine/create-document-engine.js";
|
|
21
21
|
import { validateDocument } from "./document/validate-document.js";
|
|
22
22
|
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
23
23
|
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
|
|
24
24
|
import { DownloadableExport, downloadExport } from "./export/download-export.js";
|
|
25
25
|
import { Migration, MigrationContext, MigrationResult, detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.js";
|
|
26
26
|
import { DocumentState, DocumentStateStore, createDocumentStateStore } from "./state/document-state-store.js";
|
|
27
|
-
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type ContentLimits, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, 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, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, isSafeImageUrl, migrateDocument, refuseUnsafeImageUrls, secureDocument, validateDocument };
|
|
27
|
+
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type ContentLimits, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, 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 NewDocumentRequest, 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, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, isSafeImageUrl, migrateDocument, refuseUnsafeImageUrls, secureDocument, validateDocument };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require("../document/document-format.cjs");
|
|
2
|
+
const require_ids = require("../document/ids.cjs");
|
|
3
|
+
const require_document_version = require("../versions/document-version.cjs");
|
|
4
|
+
//#region src/storage/verify-storage-adapter.ts
|
|
5
|
+
function sampleDocument(id, revision, label) {
|
|
6
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7
|
+
return {
|
|
8
|
+
schemaVersion: 1,
|
|
9
|
+
id,
|
|
10
|
+
createdAt: now,
|
|
11
|
+
updatedAt: now,
|
|
12
|
+
revision,
|
|
13
|
+
canvas: {
|
|
14
|
+
width: 100,
|
|
15
|
+
height: 100
|
|
16
|
+
},
|
|
17
|
+
objects: [{
|
|
18
|
+
type: "Rect",
|
|
19
|
+
id: `${id}-shape`,
|
|
20
|
+
width: 10,
|
|
21
|
+
height: 10,
|
|
22
|
+
fill: label
|
|
23
|
+
}],
|
|
24
|
+
metadata: { label }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function codeOf(error) {
|
|
28
|
+
return error?.code;
|
|
29
|
+
}
|
|
30
|
+
async function verifyStorageAdapter(storage) {
|
|
31
|
+
const checks = [];
|
|
32
|
+
const documentId = `adapter-check-${require_ids.createId()}`;
|
|
33
|
+
const signal = new AbortController().signal;
|
|
34
|
+
let revision = 0;
|
|
35
|
+
async function check(name, work) {
|
|
36
|
+
try {
|
|
37
|
+
const problem = await work();
|
|
38
|
+
checks.push(problem === void 0 ? {
|
|
39
|
+
name,
|
|
40
|
+
passed: true
|
|
41
|
+
} : {
|
|
42
|
+
name,
|
|
43
|
+
passed: false,
|
|
44
|
+
message: problem
|
|
45
|
+
});
|
|
46
|
+
return problem === void 0;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
checks.push({
|
|
49
|
+
name,
|
|
50
|
+
passed: false,
|
|
51
|
+
message: error instanceof Error ? error.message : String(error)
|
|
52
|
+
});
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function saveAt(expectedRevision, label) {
|
|
57
|
+
return (await storage.saveDocument(sampleDocument(documentId, (expectedRevision ?? revision) + 1, label), {
|
|
58
|
+
expectedRevision,
|
|
59
|
+
signal
|
|
60
|
+
}))?.revision ?? (expectedRevision ?? revision) + 1;
|
|
61
|
+
}
|
|
62
|
+
if (await check("saves a new document when expectedRevision is 0", async () => {
|
|
63
|
+
revision = await saveAt(0, "first");
|
|
64
|
+
return revision > 0 ? void 0 : `returned revision ${revision}, expected a number above 0`;
|
|
65
|
+
})) {
|
|
66
|
+
await check("loads the document it saved", async () => {
|
|
67
|
+
const loaded = await storage.loadDocument(documentId);
|
|
68
|
+
if (loaded?.id !== documentId) return `loaded id ${String(loaded?.id)}, expected ${documentId}`;
|
|
69
|
+
if (loaded.metadata?.label !== "first") return "loaded content differs from what was saved";
|
|
70
|
+
});
|
|
71
|
+
await check("refuses a save based on an old revision with code SAVE_CONFLICT", async () => {
|
|
72
|
+
try {
|
|
73
|
+
await saveAt(revision - 1, "stale");
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return codeOf(error) === "SAVE_CONFLICT" ? void 0 : `rejected with code ${String(codeOf(error))} instead of SAVE_CONFLICT`;
|
|
76
|
+
}
|
|
77
|
+
return "the stale save was accepted, so an older tab could overwrite newer work";
|
|
78
|
+
});
|
|
79
|
+
await check("accepts a save based on the current revision", async () => {
|
|
80
|
+
const next = await saveAt(revision, "second");
|
|
81
|
+
if (next <= revision) return `revision went from ${revision} to ${next}, it must increase`;
|
|
82
|
+
revision = next;
|
|
83
|
+
return (await storage.loadDocument(documentId))?.metadata?.label === "second" ? void 0 : "the newer content was not stored";
|
|
84
|
+
});
|
|
85
|
+
await check("overwrites when expectedRevision is null", async () => {
|
|
86
|
+
revision = await saveAt(null, "overwritten");
|
|
87
|
+
return (await storage.loadDocument(documentId))?.metadata?.label === "overwritten" ? void 0 : "the overwrite was not stored";
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
await check("rejects loading a document that does not exist", async () => {
|
|
91
|
+
try {
|
|
92
|
+
const missing = await storage.loadDocument(`adapter-check-missing-${require_ids.createId()}`);
|
|
93
|
+
return missing === void 0 || missing === null ? "resolved with nothing instead of rejecting, which hides a missing document" : "returned a document for an id that was never saved";
|
|
94
|
+
} catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if (require_document_version.supportsVersions(storage)) {
|
|
99
|
+
const version = {
|
|
100
|
+
id: `version-${require_ids.createId()}`,
|
|
101
|
+
documentId,
|
|
102
|
+
name: "Adapter check",
|
|
103
|
+
kind: "named",
|
|
104
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
|
+
revision,
|
|
106
|
+
document: sampleDocument(documentId, revision, "versioned")
|
|
107
|
+
};
|
|
108
|
+
await check("keeps, lists, loads and deletes versions", async () => {
|
|
109
|
+
await storage.saveVersion(version);
|
|
110
|
+
const listed = await storage.listVersions(documentId);
|
|
111
|
+
if (!listed.some((summary) => summary.id === version.id)) return "listVersions did not include the saved version";
|
|
112
|
+
if (listed.some((summary) => "document" in summary)) return "listVersions should return summaries without the document";
|
|
113
|
+
if ((await storage.loadVersion(documentId, version.id)).document.metadata.label !== "versioned") return "loadVersion returned different content";
|
|
114
|
+
await storage.deleteVersion(documentId, version.id);
|
|
115
|
+
return (await storage.listVersions(documentId)).some((summary) => summary.id === version.id) ? "deleteVersion did not remove the version" : void 0;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
await storage.deleteDocument?.(documentId).catch(() => void 0);
|
|
119
|
+
return {
|
|
120
|
+
ok: checks.every((result) => result.passed),
|
|
121
|
+
checks
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
exports.verifyStorageAdapter = verifyStorageAdapter;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DocumentStorage } from "./storage-contract.cjs";
|
|
2
|
+
//#region src/storage/verify-storage-adapter.d.ts
|
|
3
|
+
export interface AdapterCheck {
|
|
4
|
+
name: string;
|
|
5
|
+
passed: boolean;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AdapterReport {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
checks: AdapterCheck[];
|
|
11
|
+
}
|
|
12
|
+
export declare function verifyStorageAdapter(storage: DocumentStorage): Promise<AdapterReport>;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DocumentStorage } from "./storage-contract.js";
|
|
2
|
+
//#region src/storage/verify-storage-adapter.d.ts
|
|
3
|
+
export interface AdapterCheck {
|
|
4
|
+
name: string;
|
|
5
|
+
passed: boolean;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AdapterReport {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
checks: AdapterCheck[];
|
|
11
|
+
}
|
|
12
|
+
export declare function verifyStorageAdapter(storage: DocumentStorage): Promise<AdapterReport>;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import "../document/document-format.js";
|
|
2
|
+
import { createId } from "../document/ids.js";
|
|
3
|
+
import { supportsVersions } from "../versions/document-version.js";
|
|
4
|
+
//#region src/storage/verify-storage-adapter.ts
|
|
5
|
+
function sampleDocument(id, revision, label) {
|
|
6
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7
|
+
return {
|
|
8
|
+
schemaVersion: 1,
|
|
9
|
+
id,
|
|
10
|
+
createdAt: now,
|
|
11
|
+
updatedAt: now,
|
|
12
|
+
revision,
|
|
13
|
+
canvas: {
|
|
14
|
+
width: 100,
|
|
15
|
+
height: 100
|
|
16
|
+
},
|
|
17
|
+
objects: [{
|
|
18
|
+
type: "Rect",
|
|
19
|
+
id: `${id}-shape`,
|
|
20
|
+
width: 10,
|
|
21
|
+
height: 10,
|
|
22
|
+
fill: label
|
|
23
|
+
}],
|
|
24
|
+
metadata: { label }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function codeOf(error) {
|
|
28
|
+
return error?.code;
|
|
29
|
+
}
|
|
30
|
+
async function verifyStorageAdapter(storage) {
|
|
31
|
+
const checks = [];
|
|
32
|
+
const documentId = `adapter-check-${createId()}`;
|
|
33
|
+
const signal = new AbortController().signal;
|
|
34
|
+
let revision = 0;
|
|
35
|
+
async function check(name, work) {
|
|
36
|
+
try {
|
|
37
|
+
const problem = await work();
|
|
38
|
+
checks.push(problem === void 0 ? {
|
|
39
|
+
name,
|
|
40
|
+
passed: true
|
|
41
|
+
} : {
|
|
42
|
+
name,
|
|
43
|
+
passed: false,
|
|
44
|
+
message: problem
|
|
45
|
+
});
|
|
46
|
+
return problem === void 0;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
checks.push({
|
|
49
|
+
name,
|
|
50
|
+
passed: false,
|
|
51
|
+
message: error instanceof Error ? error.message : String(error)
|
|
52
|
+
});
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function saveAt(expectedRevision, label) {
|
|
57
|
+
return (await storage.saveDocument(sampleDocument(documentId, (expectedRevision ?? revision) + 1, label), {
|
|
58
|
+
expectedRevision,
|
|
59
|
+
signal
|
|
60
|
+
}))?.revision ?? (expectedRevision ?? revision) + 1;
|
|
61
|
+
}
|
|
62
|
+
if (await check("saves a new document when expectedRevision is 0", async () => {
|
|
63
|
+
revision = await saveAt(0, "first");
|
|
64
|
+
return revision > 0 ? void 0 : `returned revision ${revision}, expected a number above 0`;
|
|
65
|
+
})) {
|
|
66
|
+
await check("loads the document it saved", async () => {
|
|
67
|
+
const loaded = await storage.loadDocument(documentId);
|
|
68
|
+
if (loaded?.id !== documentId) return `loaded id ${String(loaded?.id)}, expected ${documentId}`;
|
|
69
|
+
if (loaded.metadata?.label !== "first") return "loaded content differs from what was saved";
|
|
70
|
+
});
|
|
71
|
+
await check("refuses a save based on an old revision with code SAVE_CONFLICT", async () => {
|
|
72
|
+
try {
|
|
73
|
+
await saveAt(revision - 1, "stale");
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return codeOf(error) === "SAVE_CONFLICT" ? void 0 : `rejected with code ${String(codeOf(error))} instead of SAVE_CONFLICT`;
|
|
76
|
+
}
|
|
77
|
+
return "the stale save was accepted, so an older tab could overwrite newer work";
|
|
78
|
+
});
|
|
79
|
+
await check("accepts a save based on the current revision", async () => {
|
|
80
|
+
const next = await saveAt(revision, "second");
|
|
81
|
+
if (next <= revision) return `revision went from ${revision} to ${next}, it must increase`;
|
|
82
|
+
revision = next;
|
|
83
|
+
return (await storage.loadDocument(documentId))?.metadata?.label === "second" ? void 0 : "the newer content was not stored";
|
|
84
|
+
});
|
|
85
|
+
await check("overwrites when expectedRevision is null", async () => {
|
|
86
|
+
revision = await saveAt(null, "overwritten");
|
|
87
|
+
return (await storage.loadDocument(documentId))?.metadata?.label === "overwritten" ? void 0 : "the overwrite was not stored";
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
await check("rejects loading a document that does not exist", async () => {
|
|
91
|
+
try {
|
|
92
|
+
const missing = await storage.loadDocument(`adapter-check-missing-${createId()}`);
|
|
93
|
+
return missing === void 0 || missing === null ? "resolved with nothing instead of rejecting, which hides a missing document" : "returned a document for an id that was never saved";
|
|
94
|
+
} catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if (supportsVersions(storage)) {
|
|
99
|
+
const version = {
|
|
100
|
+
id: `version-${createId()}`,
|
|
101
|
+
documentId,
|
|
102
|
+
name: "Adapter check",
|
|
103
|
+
kind: "named",
|
|
104
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
|
+
revision,
|
|
106
|
+
document: sampleDocument(documentId, revision, "versioned")
|
|
107
|
+
};
|
|
108
|
+
await check("keeps, lists, loads and deletes versions", async () => {
|
|
109
|
+
await storage.saveVersion(version);
|
|
110
|
+
const listed = await storage.listVersions(documentId);
|
|
111
|
+
if (!listed.some((summary) => summary.id === version.id)) return "listVersions did not include the saved version";
|
|
112
|
+
if (listed.some((summary) => "document" in summary)) return "listVersions should return summaries without the document";
|
|
113
|
+
if ((await storage.loadVersion(documentId, version.id)).document.metadata.label !== "versioned") return "loadVersion returned different content";
|
|
114
|
+
await storage.deleteVersion(documentId, version.id);
|
|
115
|
+
return (await storage.listVersions(documentId)).some((summary) => summary.id === version.id) ? "deleteVersion did not remove the version" : void 0;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
await storage.deleteDocument?.(documentId).catch(() => void 0);
|
|
119
|
+
return {
|
|
120
|
+
ok: checks.every((result) => result.passed),
|
|
121
|
+
checks
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
export { verifyStorageAdapter };
|
package/dist/storage.cjs
CHANGED
|
@@ -2,6 +2,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_key_value_storage = require("./storage/key-value-storage.cjs");
|
|
3
3
|
const require_memory_storage = require("./storage/memory-storage.cjs");
|
|
4
4
|
const require_local_storage = require("./storage/local-storage.cjs");
|
|
5
|
+
const require_verify_storage_adapter = require("./storage/verify-storage-adapter.cjs");
|
|
5
6
|
exports.createKeyValueStorage = require_key_value_storage.createKeyValueStorage;
|
|
6
7
|
exports.createLocalStorage = require_local_storage.createLocalStorage;
|
|
7
8
|
exports.createMemoryStorage = require_memory_storage.createMemoryStorage;
|
|
9
|
+
exports.verifyStorageAdapter = require_verify_storage_adapter.verifyStorageAdapter;
|
package/dist/storage.d.cts
CHANGED
|
@@ -3,4 +3,5 @@ import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-cont
|
|
|
3
3
|
import { KeyValueStore, ManagedDocumentStorage, createKeyValueStorage } from "./storage/key-value-storage.cjs";
|
|
4
4
|
import { createMemoryStorage } from "./storage/memory-storage.cjs";
|
|
5
5
|
import { LocalStorageOptions, createLocalStorage } from "./storage/local-storage.cjs";
|
|
6
|
-
|
|
6
|
+
import { AdapterCheck, AdapterReport, verifyStorageAdapter } from "./storage/verify-storage-adapter.cjs";
|
|
7
|
+
export { type AdapterCheck, type AdapterReport, type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage, verifyStorageAdapter };
|
package/dist/storage.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-cont
|
|
|
3
3
|
import { KeyValueStore, ManagedDocumentStorage, createKeyValueStorage } from "./storage/key-value-storage.js";
|
|
4
4
|
import { createMemoryStorage } from "./storage/memory-storage.js";
|
|
5
5
|
import { LocalStorageOptions, createLocalStorage } from "./storage/local-storage.js";
|
|
6
|
-
|
|
6
|
+
import { AdapterCheck, AdapterReport, verifyStorageAdapter } from "./storage/verify-storage-adapter.js";
|
|
7
|
+
export { type AdapterCheck, type AdapterReport, type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage, verifyStorageAdapter };
|
package/dist/storage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createKeyValueStorage } from "./storage/key-value-storage.js";
|
|
2
2
|
import { createMemoryStorage } from "./storage/memory-storage.js";
|
|
3
3
|
import { createLocalStorage } from "./storage/local-storage.js";
|
|
4
|
-
|
|
4
|
+
import { verifyStorageAdapter } from "./storage/verify-storage-adapter.js";
|
|
5
|
+
export { createKeyValueStorage, createLocalStorage, createMemoryStorage, verifyStorageAdapter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabricjs-document-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"sideEffects": false,
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
|
+
"schema",
|
|
32
33
|
"README.md",
|
|
33
34
|
"LICENSE"
|
|
34
35
|
],
|
|
@@ -76,6 +77,7 @@
|
|
|
76
77
|
"default": "./dist/react.cjs"
|
|
77
78
|
}
|
|
78
79
|
},
|
|
80
|
+
"./schema/document-v1.json": "./schema/document-v1.schema.json",
|
|
79
81
|
"./package.json": "./package.json"
|
|
80
82
|
},
|
|
81
83
|
"scripts": {
|
|
@@ -117,6 +119,7 @@
|
|
|
117
119
|
"@types/react": "^19.3.0",
|
|
118
120
|
"@types/react-dom": "^19.3.0",
|
|
119
121
|
"@vitest/browser-playwright": "^5.0.2",
|
|
122
|
+
"ajv": "^8.20.0",
|
|
120
123
|
"fabric": "^7.4.0",
|
|
121
124
|
"fabric6": "npm:fabric@^6.9.1",
|
|
122
125
|
"playwright": "^1.63.0",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://unpkg.com/fabricjs-document-engine/schema/document-v1.schema.json",
|
|
4
|
+
"title": "Fabric document, schema version 1",
|
|
5
|
+
"description": "The document format written by fabricjs-document-engine 1.x. Fabric object properties are kept as Fabric serializes them.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schemaVersion", "id", "createdAt", "updatedAt", "canvas", "objects", "metadata"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": 1 },
|
|
10
|
+
"id": { "type": "string", "minLength": 1 },
|
|
11
|
+
"createdAt": { "type": "string" },
|
|
12
|
+
"updatedAt": { "type": "string" },
|
|
13
|
+
"revision": { "type": "integer", "minimum": 0 },
|
|
14
|
+
"fabricVersion": { "type": "string" },
|
|
15
|
+
"canvas": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": ["width", "height"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"width": { "type": "number", "exclusiveMinimum": 0 },
|
|
20
|
+
"height": { "type": "number", "exclusiveMinimum": 0 },
|
|
21
|
+
"background": {}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"objects": { "type": "array", "items": { "$ref": "#/$defs/fabricObject" } },
|
|
25
|
+
"assets": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": ["images", "fonts"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"images": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"required": ["url", "objectIds"],
|
|
34
|
+
"properties": {
|
|
35
|
+
"url": { "type": "string" },
|
|
36
|
+
"objectIds": { "type": "array", "items": { "type": "string" } }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"fonts": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"required": ["family", "weight", "style", "objectIds"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"family": { "type": "string" },
|
|
47
|
+
"weight": { "type": "string" },
|
|
48
|
+
"style": { "type": "string" },
|
|
49
|
+
"objectIds": { "type": "array", "items": { "type": "string" } }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"metadata": { "type": "object" }
|
|
56
|
+
},
|
|
57
|
+
"$defs": {
|
|
58
|
+
"fabricObject": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"required": ["type"],
|
|
61
|
+
"properties": {
|
|
62
|
+
"type": { "type": "string", "minLength": 1 },
|
|
63
|
+
"id": { "type": "string" },
|
|
64
|
+
"objects": { "type": "array", "items": { "$ref": "#/$defs/fabricObject" } },
|
|
65
|
+
"clipPath": { "$ref": "#/$defs/fabricObject" }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|