fabricjs-document-engine 0.8.0 → 1.0.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 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
- ## API
406
-
407
- | Member | What it does |
408
- | --- | --- |
409
- | `createDocumentEngine({ canvas, storage?, customObjects?, document? })` | Connects the engine to your canvas. |
410
- | `engine.toDocument()` | Serializes the canvas into the versioned document format. |
411
- | `engine.loadDocument(document, { restoreCanvasSize? })` | Validates and loads a document. Resolves when the objects are on the canvas. |
412
- | `engine.load(id)` / `engine.save({ overwrite? })` | Reads from or writes to your storage adapter. |
413
- | `engine.importFabricJson(json, { id?, metadata? })` | Opens plain Fabric JSON, as text or an object. |
414
- | `engine.createVersion(name?)` / `engine.listVersions()` | Keeps a named version, or lists versions newest first. |
415
- | `engine.restoreVersion(id)` / `engine.deleteVersion(id)` | Restores a version as a new unsaved revision, or deletes it. |
416
- | `engine.isDirty()` | Tells you whether there are unsaved changes. |
417
- | `engine.getSaveState()` | Returns `{ status, isDirty, isSaving, revision, lastSavedAt, error }`. |
418
- | `bindUnsavedChangesWarning(engine)` | Asks the browser to confirm before closing a page with unsaved changes. Returns an unbind function. |
419
- | `engine.newDocument({ id?, metadata? })` | Clears the canvas and starts a fresh document. |
420
- | `engine.getDocumentInfo()` | Returns the current document's id, dates and metadata. |
421
- | `engine.updateMetadata(changes)` | Merges changes into the document metadata. |
422
- | `engine.getObjectById(id)` | Finds any object by id, including objects inside groups. |
423
- | `engine.registerObject({ fabricClass, properties })` | Registers a custom class after the engine is created. |
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,8 +163,13 @@ function createDocumentEngine(options) {
163
163
  function loadDocument(input, loadOptions = {}) {
164
164
  return loadMigratedDocument(input, loadOptions, {});
165
165
  }
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
+ }
166
170
  async function loadMigratedDocument(input, loadOptions, importDetails) {
167
171
  ensureUsable();
172
+ protectUnsavedChanges(loadOptions.discardUnsavedChanges);
168
173
  activeLoad?.abort();
169
174
  const controller = new AbortController();
170
175
  activeLoad = controller;
@@ -297,7 +302,7 @@ function createDocumentEngine(options) {
297
302
  id: current.id,
298
303
  createdAt: current.createdAt,
299
304
  revision: baseRevision
300
- });
305
+ }, { discardUnsavedChanges: true });
301
306
  noteContentChange();
302
307
  events.emit("version:restored", {
303
308
  version: require_document_version.summarize(version),
@@ -312,6 +317,7 @@ function createDocumentEngine(options) {
312
317
  async function load(documentId, loadOptions) {
313
318
  ensureUsable();
314
319
  const source = requireStorage();
320
+ protectUnsavedChanges(loadOptions?.discardUnsavedChanges);
315
321
  let stored;
316
322
  try {
317
323
  stored = await source.loadDocument(documentId);
@@ -331,8 +337,9 @@ function createDocumentEngine(options) {
331
337
  }
332
338
  return saving.save(saveOptions);
333
339
  }
334
- function newDocument(newOptions) {
340
+ function newDocument(newOptions = {}) {
335
341
  ensureUsable();
342
+ protectUnsavedChanges(newOptions.discardUnsavedChanges);
336
343
  activeLoad?.abort();
337
344
  history.withoutRecording(() => canvas.clear());
338
345
  objectsById.clear();
@@ -488,6 +495,7 @@ function createDocumentEngine(options) {
488
495
  }
489
496
  function destroy() {
490
497
  if (destroyed) return;
498
+ if (recovery && saving.state().isDirty) recovery.writeNow();
491
499
  destroyed = true;
492
500
  activeLoad?.abort();
493
501
  history.destroy();
@@ -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?: NewDocumentOptions): void;
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?: NewDocumentOptions): void;
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,8 +163,13 @@ function createDocumentEngine(options) {
163
163
  function loadDocument(input, loadOptions = {}) {
164
164
  return loadMigratedDocument(input, loadOptions, {});
165
165
  }
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
+ }
166
170
  async function loadMigratedDocument(input, loadOptions, importDetails) {
167
171
  ensureUsable();
172
+ protectUnsavedChanges(loadOptions.discardUnsavedChanges);
168
173
  activeLoad?.abort();
169
174
  const controller = new AbortController();
170
175
  activeLoad = controller;
@@ -297,7 +302,7 @@ function createDocumentEngine(options) {
297
302
  id: current.id,
298
303
  createdAt: current.createdAt,
299
304
  revision: baseRevision
300
- });
305
+ }, { discardUnsavedChanges: true });
301
306
  noteContentChange();
302
307
  events.emit("version:restored", {
303
308
  version: summarize(version),
@@ -312,6 +317,7 @@ function createDocumentEngine(options) {
312
317
  async function load(documentId, loadOptions) {
313
318
  ensureUsable();
314
319
  const source = requireStorage();
320
+ protectUnsavedChanges(loadOptions?.discardUnsavedChanges);
315
321
  let stored;
316
322
  try {
317
323
  stored = await source.loadDocument(documentId);
@@ -331,8 +337,9 @@ function createDocumentEngine(options) {
331
337
  }
332
338
  return saving.save(saveOptions);
333
339
  }
334
- function newDocument(newOptions) {
340
+ function newDocument(newOptions = {}) {
335
341
  ensureUsable();
342
+ protectUnsavedChanges(newOptions.discardUnsavedChanges);
336
343
  activeLoad?.abort();
337
344
  history.withoutRecording(() => canvas.clear());
338
345
  objectsById.clear();
@@ -488,6 +495,7 @@ function createDocumentEngine(options) {
488
495
  }
489
496
  function destroy() {
490
497
  if (destroyed) return;
498
+ if (recovery && saving.state().isDirty) recovery.writeNow();
491
499
  destroyed = true;
492
500
  activeLoad?.abort();
493
501
  history.destroy();
@@ -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;
@@ -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 };
@@ -149,6 +149,7 @@ function createRecoveryController(options) {
149
149
  }, interval);
150
150
  },
151
151
  flush,
152
+ writeNow: writeCheckpointBeforeThePageCloses,
152
153
  cancel,
153
154
  remove: (documentId) => enqueue(async () => {
154
155
  await store.delete(unloadKey(documentId));
@@ -149,6 +149,7 @@ function createRecoveryController(options) {
149
149
  }, interval);
150
150
  },
151
151
  flush,
152
+ writeNow: writeCheckpointBeforeThePageCloses,
152
153
  cancel,
153
154
  remove: (documentId) => enqueue(async () => {
154
155
  await store.delete(unloadKey(documentId));
@@ -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;
@@ -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
- export { type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage };
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
- export { type DocumentStorage, type DocumentVersion, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, type VersionStorage, type VersionSummary, createKeyValueStorage, createLocalStorage, createMemoryStorage };
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
- export { createKeyValueStorage, createLocalStorage, createMemoryStorage };
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.8.0",
3
+ "version": "1.0.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",
@@ -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
+ }