fabricjs-document-engine 0.3.0 → 0.5.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.
Files changed (50) hide show
  1. package/README.md +88 -3
  2. package/dist/assets/asset-pipeline.cjs +1 -0
  3. package/dist/assets/asset-pipeline.js +1 -1
  4. package/dist/engine/create-document-engine.cjs +150 -7
  5. package/dist/engine/create-document-engine.d.cts +40 -0
  6. package/dist/engine/create-document-engine.d.ts +40 -0
  7. package/dist/engine/create-document-engine.js +150 -7
  8. package/dist/engine/errors.cjs +1 -0
  9. package/dist/engine/errors.d.cts +4 -1
  10. package/dist/engine/errors.d.ts +4 -1
  11. package/dist/engine/errors.js +1 -0
  12. package/dist/export/download-export.cjs +14 -0
  13. package/dist/export/download-export.d.cts +7 -0
  14. package/dist/export/download-export.d.ts +7 -0
  15. package/dist/export/download-export.js +14 -0
  16. package/dist/export/export-area.cjs +46 -0
  17. package/dist/export/export-area.js +46 -0
  18. package/dist/export/export-options.cjs +47 -0
  19. package/dist/export/export-options.d.cts +20 -0
  20. package/dist/export/export-options.d.ts +20 -0
  21. package/dist/export/export-options.js +46 -0
  22. package/dist/export/preflight-export.cjs +105 -0
  23. package/dist/export/preflight-export.d.cts +17 -0
  24. package/dist/export/preflight-export.d.ts +17 -0
  25. package/dist/export/preflight-export.js +105 -0
  26. package/dist/export/render-export.cjs +69 -0
  27. package/dist/export/render-export.js +65 -0
  28. package/dist/history/apply-state.cjs +1 -1
  29. package/dist/history/apply-state.js +1 -1
  30. package/dist/index.cjs +2 -0
  31. package/dist/index.d.cts +7 -2
  32. package/dist/index.d.ts +7 -2
  33. package/dist/index.js +2 -1
  34. package/dist/recovery/indexed-db-recovery.cjs +80 -0
  35. package/dist/recovery/indexed-db-recovery.d.cts +8 -0
  36. package/dist/recovery/indexed-db-recovery.d.ts +8 -0
  37. package/dist/recovery/indexed-db-recovery.js +80 -0
  38. package/dist/recovery/recovery-controller.cjs +159 -0
  39. package/dist/recovery/recovery-controller.d.cts +19 -0
  40. package/dist/recovery/recovery-controller.d.ts +19 -0
  41. package/dist/recovery/recovery-controller.js +158 -0
  42. package/dist/recovery/recovery-store.cjs +19 -0
  43. package/dist/recovery/recovery-store.d.cts +10 -0
  44. package/dist/recovery/recovery-store.d.ts +10 -0
  45. package/dist/recovery/recovery-store.js +19 -0
  46. package/dist/recovery.cjs +5 -0
  47. package/dist/recovery.d.cts +4 -0
  48. package/dist/recovery.d.ts +4 -0
  49. package/dist/recovery.js +3 -0
  50. package/package.json +13 -2
package/README.md CHANGED
@@ -11,6 +11,8 @@ Fabric already draws objects, handles interaction and serializes to JSON. This p
11
11
  - **Safe saving.** It tracks unsaved changes and can autosave. Only one save runs at a time, so a slow older save can never overwrite newer work. Revision checks catch another tab or device saving the same document, and failed saves are retried with backoff.
12
12
  - **Your storage.** Plug in any backend with two functions, or use the built-in memory and localStorage adapters. No hosted service is needed.
13
13
  - **Assets and fonts.** Documents record the images and fonts they need. When a document is opened, every image and font is checked first. You get the exact list of what is missing, can offer replacements, and tab-only images are uploaded when you save.
14
+ - **Recovery.** Unsaved work is copied to IndexedDB while the user edits, and again at the moment the tab is closed or refreshed. After a crash or refresh you can offer to restore it, including images that only existed in the old tab.
15
+ - **Dependable export.** PNG, JPEG, WebP, SVG and editable JSON. You choose the area, scale and background. A preflight check means an export either succeeds or tells you exactly which image or font prevents it.
14
16
  - **Reliable undo and redo.** One user action is one undo step. Transactions group several code changes into one labelled step, and ids survive undo and redo.
15
17
  - **Fabric 6 and 7.** Every release is tested against both.
16
18
 
@@ -175,6 +177,75 @@ await engine.replaceImage('/old-logo.png', '/new-logo.png');
175
177
 
176
178
  `replaceImage` swaps every image that uses a URL. Each image keeps its size on the page, and the change is one undo step. `engine.getAssetManifest()` returns the manifest for the current canvas.
177
179
 
180
+ ## Export
181
+
182
+ ```ts
183
+ import { downloadExport } from 'fabricjs-document-engine';
184
+
185
+ const result = await engine.export({ format: 'png', scale: 2 });
186
+ downloadExport(result, 'poster.png');
187
+ ```
188
+
189
+ `result` is `{ format, mimeType, blob, width, height, warnings }`. JSON exports also include `document`.
190
+
191
+ | Option | Values | Default |
192
+ | --- | --- | --- |
193
+ | `format` | `'png'`, `'jpeg'`, `'webp'`, `'svg'` or `'json'` | required |
194
+ | `scale` | Output size multiplier, such as `2` for retina | `1` |
195
+ | `quality` | 0 to 1, for JPEG and WebP | `0.92` |
196
+ | `area` | `'canvas'`, `'content'` (every object), `'selection'`, or `{ left, top, width, height }` | `'canvas'` |
197
+ | `padding` | Extra space around `content` or `selection` | `0` |
198
+ | `background` | `'keep'`, `'transparent'` or any CSS color | `'keep'` |
199
+ | `signal` | An `AbortSignal` to cancel | |
200
+
201
+ - The current zoom and pan do not matter. Exports always use document coordinates, and the view is restored afterwards.
202
+ - A JPEG has no transparency, so an empty or transparent background becomes white instead of black.
203
+ - The export never changes the canvas, the history or the unsaved state.
204
+ - A JSON export is the same portable document a save produces, including uploaded images when `assets.upload` is set.
205
+
206
+ ### Preflight and errors
207
+
208
+ Before rendering, the engine checks the objects on the canvas:
209
+
210
+ - **`MISSING_IMAGE`**: an image failed to load.
211
+ - **`CROSS_ORIGIN_IMAGE`**: an image from another site without CORS would make the browser block a PNG, JPEG or WebP export. SVG and JSON are not affected.
212
+ - **`MISSING_FONT`**: a font is not available and `assets.requireFonts` is on. Otherwise you get a `FONT_UNAVAILABLE` warning.
213
+
214
+ If any problem is found, `export` rejects with `EXPORT_BLOCKED`, and `error.problems` lists each `{ code, message, url?, family?, objectIds }`. You can run the same check first to show it in your UI:
215
+
216
+ ```ts
217
+ const check = await engine.preflightExport({ format: 'png' });
218
+ if (!check.ok) showProblems(check.problems);
219
+ ```
220
+
221
+ ## Recovery
222
+
223
+ ```ts
224
+ import { createIndexedDbRecovery } from 'fabricjs-document-engine/recovery';
225
+
226
+ const engine = createDocumentEngine({
227
+ canvas,
228
+ storage,
229
+ recovery: { store: createIndexedDbRecovery(), interval: 2000 },
230
+ });
231
+
232
+ const [latest] = await engine.getRecoverableDocuments();
233
+ if (latest && confirm(`Restore unsaved work from ${new Date(latest.savedAt).toLocaleString()}?`)) {
234
+ await engine.restoreRecovery(latest.documentId);
235
+ } else if (latest) {
236
+ await engine.discardRecovery(latest.documentId);
237
+ }
238
+ ```
239
+
240
+ - **Checkpoints.** While there are unsaved changes, a copy is written at most once every `interval` ms (default 2000). Nothing is written while the document is saved.
241
+ - **Closing or refreshing.** Browsers do not let IndexedDB finish writing while a page unloads. So when the tab is hidden or closed, the engine also writes an immediate copy to localStorage. The newest copy wins when you read it back.
242
+ - **Tab-only images.** Images with `blob:` URLs vanish on refresh. Checkpoints keep the image data, and restoring creates fresh URLs for it.
243
+ - **After a save.** When a save covers every change, the copy is removed. If the tab closes during a save, the copy stays, so work is never lost between the edit and the server.
244
+ - **Restoring.** The restored document is marked as unsaved and keeps the revision it was based on. If the server moved on meanwhile, the next save reports `SAVE_CONFLICT` instead of overwriting newer work.
245
+ - **Interrupted loads.** A marker is kept while a document loads. If the tab crashes during loading, `engine.getInterruptedLoad()` returns `{ documentId, startedAt }` on the next start, so you can skip or discard that document instead of crashing again.
246
+ - `engine.flushRecovery()` writes a copy right now. `engine.getRecovery(id?)` reads one.
247
+ - `createMemoryRecovery()` keeps copies in memory, which is useful for tests. To use your own storage, implement `{ get, set, delete, keys }`, plus an optional synchronous `setNow` for the moment the page closes.
248
+
178
249
  ## Custom objects
179
250
 
180
251
  ```ts
@@ -282,6 +353,13 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
282
353
  | `engine.getAssetManifest()` | Lists the images and fonts used on the canvas. |
283
354
  | `engine.checkAssets()` | Resolves to `{ manifest, missingImages, unavailableFonts, warnings }` for the current canvas. |
284
355
  | `engine.replaceImage(oldUrl, newUrl)` | Replaces every image with that URL as one undo step. Resolves to the number of images replaced. |
356
+ | `engine.export(options)` | Exports PNG, JPEG, WebP, SVG or JSON. See [Export](#export). |
357
+ | `engine.preflightExport(options)` | Resolves to `{ ok, problems, warnings }` without exporting. |
358
+ | `downloadExport(result, fileName?)` | Starts a browser download of an export result. |
359
+ | `engine.getRecoverableDocuments()` | Lists recovery copies, newest first. |
360
+ | `engine.restoreRecovery(id?)` / `engine.discardRecovery(id?)` | Loads or deletes a recovery copy. The default is the current document. |
361
+ | `engine.getRecovery(id?)` / `engine.flushRecovery()` | Reads a copy, or writes one now. |
362
+ | `engine.getInterruptedLoad()` | Returns the load that was running when the tab last crashed, if any. |
285
363
  | `engine.transaction(label, work)` | Runs `work` and records everything it changed as one undo step. Returns what `work` returns. |
286
364
  | `engine.commit(label?)` | Records changes made since the last step. Returns `false` when nothing changed. |
287
365
  | `engine.undo()` / `engine.redo()` | Resolves to `true` when a step was applied. Calls run one after another. |
@@ -289,7 +367,7 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
289
367
  | `engine.getHistory()` | Returns `{ undo, redo }` label lists, newest first. |
290
368
  | `engine.clearHistory()` | Forgets all steps. Loading a document or starting a new one also does this. |
291
369
  | `bindKeyboardShortcuts(engine, { target? })` | Adds the undo and redo shortcuts. Returns an unbind function. |
292
- | `engine.on(event, handler)` | Listens to `load:start`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `save:retry`, `save:status`, `assets:warning`, `history:change` or `history:error`. Returns an unsubscribe function. |
370
+ | `engine.on(event, handler)` | Listens to `load:start`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `save:retry`, `save:status`, `assets:warning`, `recovery:checkpoint`, `recovery:restored`, `recovery:error`, `export:success`, `export:error`, `history:change` or `history:error`. Returns an unsubscribe function. |
293
371
  | `engine.destroy()` | Stops listening to the canvas and cancels a running load. |
294
372
  | `validateDocument(value)` | Returns a list of issues with the exact path of each problem. |
295
373
 
@@ -312,6 +390,13 @@ Every failure is a `DocumentEngineError` with a `code` you can switch on:
312
390
  | `SAVE_CANCELLED` | A queued save was dropped because another document was opened. |
313
391
  | `DOCUMENT_NOT_FOUND` | The built-in adapters have no document with that id. |
314
392
  | `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. |
393
+ | `EXPORT_BLOCKED` | The preflight found problems. `error.problems` lists each one with the objects involved. |
394
+ | `INVALID_EXPORT_OPTIONS` | The format, scale, quality, area or padding is not valid, or the area is empty. |
395
+ | `EXPORT_ABORTED` | The export was cancelled with its `signal`. |
396
+ | `EXPORT_FAILED` | Fabric could not render the export. `error.cause` holds the original error. |
397
+ | `RECOVERY_MISSING` | A recovery method was called without `recovery: { store }`. |
398
+ | `RECOVERY_NOT_FOUND` | There is no recovery copy for that document. |
399
+ | `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. |
315
400
  | `STORAGE_MISSING` | `load` or `save` was called without a storage adapter. |
316
401
  | `INVALID_CUSTOM_OBJECT` | A registered class has no static `type`, or it does not extend a Fabric class. |
317
402
  | `ENGINE_DESTROYED` | The engine was used after `destroy()`. |
@@ -326,8 +411,8 @@ A failed load never clears or half-fills your canvas.
326
411
  | 2 | Undo and redo with transactions | 0.1.0 |
327
412
  | 3 | Safe saving: dirty state, autosave, stale-response protection | 0.2.0 |
328
413
  | 4 | Assets and fonts | 0.3.0 |
329
- | 5 | Recovery after a refresh or crash | |
330
- | 6 | PNG, JPEG, SVG and JSON export with preflight checks | |
414
+ | 5 | Recovery after a refresh or crash | 0.4.0 |
415
+ | 6 | PNG, JPEG, SVG and JSON export with preflight checks | 0.5.0 |
331
416
  | 7 | Named versions and schema migrations | |
332
417
  | 8 | React adapter and examples | |
333
418
  | 9 | Hardening and benchmarks | |
@@ -172,3 +172,4 @@ async function prepareAssetsForSave(document, options, uploadedUrls) {
172
172
  exports.inspectAssets = inspectAssets;
173
173
  exports.prepareAssetsForLoad = prepareAssetsForLoad;
174
174
  exports.prepareAssetsForSave = prepareAssetsForSave;
175
+ exports.rewriteUrls = rewriteUrls;
@@ -169,4 +169,4 @@ async function prepareAssetsForSave(document, options, uploadedUrls) {
169
169
  };
170
170
  }
171
171
  //#endregion
172
- export { inspectAssets, prepareAssetsForLoad, prepareAssetsForSave };
172
+ export { inspectAssets, prepareAssetsForLoad, prepareAssetsForSave, rewriteUrls };
@@ -2,15 +2,20 @@ const require_asset_manifest = require("../assets/asset-manifest.cjs");
2
2
  const require_errors = require("./errors.cjs");
3
3
  const require_image_check = require("../assets/image-check.cjs");
4
4
  const require_asset_pipeline = require("../assets/asset-pipeline.cjs");
5
+ const require_export_area = require("../export/export-area.cjs");
6
+ const require_export_options = require("../export/export-options.cjs");
7
+ const require_object_ids = require("../fabric/object-ids.cjs");
8
+ const require_walk_objects = require("../fabric/walk-objects.cjs");
9
+ const require_preflight_export = require("../export/preflight-export.cjs");
10
+ const require_render_export = require("../export/render-export.cjs");
5
11
  require("../document/document-format.cjs");
6
12
  const require_ids = require("../document/ids.cjs");
7
13
  const require_create_document = require("../document/create-document.cjs");
8
14
  const require_validate_document = require("../document/validate-document.cjs");
9
- const require_walk_objects = require("../fabric/walk-objects.cjs");
10
15
  const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
11
- const require_object_ids = require("../fabric/object-ids.cjs");
12
16
  const require_object_registry = require("../fabric/object-registry.cjs");
13
17
  const require_create_history = require("../history/create-history.cjs");
18
+ const require_recovery_controller = require("../recovery/recovery-controller.cjs");
14
19
  const require_save_controller = require("../save/save-controller.cjs");
15
20
  const require_event_emitter = require("./event-emitter.cjs");
16
21
  //#region src/engine/create-document-engine.ts
@@ -70,7 +75,7 @@ function createDocumentEngine(options) {
70
75
  return require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude()).objects;
71
76
  },
72
77
  onChange: (state) => events.emit("history:change", state),
73
- onContentChange: () => saving.noteContentChange()
78
+ onContentChange: () => noteContentChange()
74
79
  });
75
80
  const saving = require_save_controller.createSaveController({
76
81
  getStorage: () => requireStorage(),
@@ -83,10 +88,30 @@ function createDocumentEngine(options) {
83
88
  autosave: options.autosave === true ? {} : options.autosave ?? false,
84
89
  onStateChange: (state) => events.emit("save:status", state),
85
90
  onStart: (document) => events.emit("save:start", { document }),
86
- onSuccess: (document) => events.emit("save:success", { document }),
91
+ onSuccess: (document) => {
92
+ if (recovery && !saving.state().isDirty) recovery.remove(document.id);
93
+ events.emit("save:success", { document });
94
+ },
87
95
  onError: (error) => events.emit("save:error", { error }),
88
96
  onRetry: (event) => events.emit("save:retry", event)
89
97
  });
98
+ const recovery = options.recovery ? require_recovery_controller.createRecoveryController({
99
+ ...options.recovery,
100
+ createDocument: () => toDocument(),
101
+ shouldWrite: () => !destroyed && saving.state().isDirty,
102
+ onCheckpoint: (record) => events.emit("recovery:checkpoint", {
103
+ documentId: record.documentId,
104
+ savedAt: record.savedAt
105
+ }),
106
+ onError: (error) => {
107
+ const reason = error instanceof Error ? error.message : String(error);
108
+ events.emit("recovery:error", { error: new require_errors.DocumentEngineError("RECOVERY_FAILED", `Could not write the recovery copy: ${reason}`, { cause: error }) });
109
+ }
110
+ }) : void 0;
111
+ function noteContentChange() {
112
+ saving.noteContentChange();
113
+ recovery?.schedule();
114
+ }
90
115
  function toDocument() {
91
116
  ensureUsable();
92
117
  rebuildIndex();
@@ -135,8 +160,11 @@ function createDocumentEngine(options) {
135
160
  activeLoad?.abort();
136
161
  const controller = new AbortController();
137
162
  activeLoad = controller;
138
- events.emit("load:start", { documentId: describeDocumentId(input) });
163
+ const documentId = describeDocumentId(input);
164
+ events.emit("load:start", { documentId });
139
165
  try {
166
+ await recovery?.markLoadStarted(documentId);
167
+ if (controller.signal.aborted) throw new Error("aborted");
140
168
  const checked = checkDocument(input);
141
169
  const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal);
142
170
  if (controller.signal.aborted) throw new Error("aborted");
@@ -158,6 +186,7 @@ function createDocumentEngine(options) {
158
186
  };
159
187
  rebuildIndex();
160
188
  history.reset();
189
+ recovery?.cancel();
161
190
  saving.startSession(document.revision ?? 0);
162
191
  canvas.requestRenderAll();
163
192
  if (warnings.length > 0) events.emit("assets:warning", { warnings });
@@ -171,7 +200,10 @@ function createDocumentEngine(options) {
171
200
  events.emit("load:error", { error: engineError });
172
201
  throw engineError;
173
202
  } finally {
174
- if (activeLoad === controller) activeLoad = null;
203
+ if (activeLoad === controller) {
204
+ activeLoad = null;
205
+ await recovery?.markLoadFinished();
206
+ }
175
207
  }
176
208
  }
177
209
  function requireStorage() {
@@ -208,6 +240,7 @@ function createDocumentEngine(options) {
208
240
  documentInfo = require_create_document.createDocumentInfo(newOptions);
209
241
  uploadedUrls.clear();
210
242
  history.reset();
243
+ recovery?.cancel();
211
244
  saving.startSession(0);
212
245
  }
213
246
  function getObjectById(id) {
@@ -241,6 +274,107 @@ function createDocumentEngine(options) {
241
274
  canvas.requestRenderAll();
242
275
  return images.length;
243
276
  }
277
+ function fontsOnCanvas() {
278
+ rebuildIndex();
279
+ return require_asset_manifest.buildAssetManifest(require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude()).objects).fonts;
280
+ }
281
+ async function checkBeforeExport(exportOptions) {
282
+ ensureUsable();
283
+ const { format } = require_export_options.normalizeExportOptions(exportOptions);
284
+ return require_preflight_export.preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions);
285
+ }
286
+ function describeProblems(check) {
287
+ return check.problems.map((problem) => problem.message).join("; ");
288
+ }
289
+ async function exportContent(exportOptions) {
290
+ const settings = require_export_options.normalizeExportOptions(exportOptions);
291
+ const stopIfCancelled = () => {
292
+ if (settings.signal?.aborted) throw new require_errors.DocumentEngineError("EXPORT_ABORTED", "The export was cancelled");
293
+ };
294
+ stopIfCancelled();
295
+ if (settings.format === "json") {
296
+ const prepared = await require_asset_pipeline.prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
297
+ stopIfCancelled();
298
+ return {
299
+ format: "json",
300
+ mimeType: require_render_export.mimeTypes.json,
301
+ blob: new Blob([JSON.stringify(prepared.document)], { type: require_render_export.mimeTypes.json }),
302
+ width: prepared.document.canvas.width,
303
+ height: prepared.document.canvas.height,
304
+ warnings: prepared.warnings,
305
+ document: prepared.document
306
+ };
307
+ }
308
+ const check = await checkBeforeExport(settings);
309
+ stopIfCancelled();
310
+ if (!check.ok) throw new require_errors.DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
311
+ const format = settings.format;
312
+ try {
313
+ const rendered = require_render_export.withExportView(canvas, settings.background, format, () => {
314
+ const area = require_export_area.resolveExportArea(canvas, settings.area, settings.padding);
315
+ return {
316
+ area,
317
+ content: format === "svg" ? require_render_export.renderSvg(canvas, area, settings.scale) : require_render_export.renderRaster(canvas, area, format, settings.scale, settings.quality)
318
+ };
319
+ });
320
+ const blob = format === "svg" ? new Blob([rendered.content], { type: require_render_export.mimeTypes.svg }) : require_render_export.dataUrlToBlob(rendered.content);
321
+ return {
322
+ format,
323
+ mimeType: blob.type || require_render_export.mimeTypes[format],
324
+ blob,
325
+ width: Math.round(rendered.area.width * settings.scale),
326
+ height: Math.round(rendered.area.height * settings.scale),
327
+ warnings: check.warnings
328
+ };
329
+ } catch (error) {
330
+ if (require_errors.isDocumentEngineError(error)) throw error;
331
+ if (error?.name === "SecurityError") {
332
+ const problem = {
333
+ code: "CROSS_ORIGIN_IMAGE",
334
+ message: "The browser blocked the export because the canvas shows an image from another site without CORS",
335
+ objectIds: []
336
+ };
337
+ throw new require_errors.DocumentEngineError("EXPORT_BLOCKED", problem.message, {
338
+ problems: [problem],
339
+ cause: error
340
+ });
341
+ }
342
+ const reason = error instanceof Error ? error.message : String(error);
343
+ throw new require_errors.DocumentEngineError("EXPORT_FAILED", `Fabric could not export the canvas: ${reason}`, { cause: error });
344
+ }
345
+ }
346
+ async function exportDocument(exportOptions) {
347
+ ensureUsable();
348
+ try {
349
+ const result = await exportContent(exportOptions);
350
+ events.emit("export:success", {
351
+ format: result.format,
352
+ width: result.width,
353
+ height: result.height,
354
+ warnings: result.warnings
355
+ });
356
+ return result;
357
+ } catch (error) {
358
+ const engineError = require_errors.isDocumentEngineError(error) ? error : new require_errors.DocumentEngineError("EXPORT_FAILED", String(error), { cause: error });
359
+ events.emit("export:error", { error: engineError });
360
+ throw engineError;
361
+ }
362
+ }
363
+ function requireRecovery() {
364
+ ensureUsable();
365
+ if (!recovery) throw new require_errors.DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
366
+ return recovery;
367
+ }
368
+ async function restoreRecovery(documentId, loadOptions) {
369
+ const source = requireRecovery();
370
+ const id = documentId ?? documentInfo.id;
371
+ const record = await source.read(id);
372
+ if (!record) throw new require_errors.DocumentEngineError("RECOVERY_NOT_FOUND", `There is no recovery copy for document "${id}"`);
373
+ const loaded = await loadDocument(await require_recovery_controller.restoreRecordedFiles(record), loadOptions);
374
+ noteContentChange();
375
+ events.emit("recovery:restored", { document: loaded });
376
+ return loaded;
377
+ }
244
378
  async function travelThroughHistory(direction) {
245
379
  ensureUsable();
246
380
  try {
@@ -258,6 +392,7 @@ function createDocumentEngine(options) {
258
392
  activeLoad?.abort();
259
393
  history.destroy();
260
394
  saving.destroy();
395
+ recovery?.destroy();
261
396
  canvas.off("object:added", handleObjectAdded);
262
397
  canvas.off("object:removed", handleObjectRemoved);
263
398
  objectsById.clear();
@@ -278,7 +413,7 @@ function createDocumentEngine(options) {
278
413
  ...changes
279
414
  }
280
415
  };
281
- saving.noteContentChange();
416
+ noteContentChange();
282
417
  },
283
418
  newDocument,
284
419
  toDocument,
@@ -298,6 +433,14 @@ function createDocumentEngine(options) {
298
433
  },
299
434
  checkAssets,
300
435
  replaceImage,
436
+ export: exportDocument,
437
+ preflightExport: checkBeforeExport,
438
+ flushRecovery: async () => requireRecovery().flush(),
439
+ getRecoverableDocuments: async () => requireRecovery().list(),
440
+ getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
441
+ restoreRecovery,
442
+ discardRecovery: async (documentId) => requireRecovery().remove(documentId ?? documentInfo.id),
443
+ getInterruptedLoad: async () => requireRecovery().interruptedLoad(),
301
444
  transaction(label, work) {
302
445
  ensureUsable();
303
446
  return history.transaction(label, work);
@@ -1,9 +1,12 @@
1
1
  import { DocumentInfo, FabricDocument } from "../document/document-format.cjs";
2
2
  import { AssetManifest } from "../assets/asset-manifest.cjs";
3
3
  import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.cjs";
4
+ import { ExportFormat, ExportOptions } from "../export/export-options.cjs";
5
+ import { ExportPreflight } from "../export/preflight-export.cjs";
4
6
  import { NewDocumentOptions } from "../document/create-document.cjs";
5
7
  import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
6
8
  import { HistoryOptions, HistoryState } from "../history/create-history.cjs";
9
+ import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.cjs";
7
10
  import { AutosaveOptions } from "../save/autosave-scheduler.cjs";
8
11
  import { RetryOptions } from "../save/retry.cjs";
9
12
  import { DocumentEngineError } from "./errors.cjs";
@@ -21,10 +24,20 @@ export interface DocumentEngineOptions {
21
24
  autosave?: boolean | AutosaveOptions;
22
25
  saveRetry?: RetryOptions;
23
26
  assets?: AssetOptions;
27
+ recovery?: RecoveryOptions;
24
28
  }
25
29
  export interface LoadOptions {
26
30
  restoreCanvasSize?: boolean;
27
31
  }
32
+ export interface ExportResult {
33
+ format: ExportFormat;
34
+ mimeType: string;
35
+ blob: Blob;
36
+ width: number;
37
+ height: number;
38
+ warnings: AssetWarning[];
39
+ document?: FabricDocument;
40
+ }
28
41
  export interface DocumentEngineEvents {
29
42
  "load:start": {
30
43
  documentId: string | undefined;
@@ -54,6 +67,25 @@ export interface DocumentEngineEvents {
54
67
  "history:error": {
55
68
  error: DocumentEngineError;
56
69
  };
70
+ "recovery:checkpoint": {
71
+ documentId: string;
72
+ savedAt: string;
73
+ };
74
+ "recovery:restored": {
75
+ document: FabricDocument;
76
+ };
77
+ "recovery:error": {
78
+ error: DocumentEngineError;
79
+ };
80
+ "export:success": {
81
+ format: ExportFormat;
82
+ width: number;
83
+ height: number;
84
+ warnings: AssetWarning[];
85
+ };
86
+ "export:error": {
87
+ error: DocumentEngineError;
88
+ };
57
89
  }
58
90
  export interface DocumentEngine {
59
91
  readonly canvas: StaticCanvas;
@@ -82,6 +114,14 @@ export interface DocumentEngine {
82
114
  getAssetManifest(): AssetManifest;
83
115
  checkAssets(): Promise<AssetReport>;
84
116
  replaceImage(oldUrl: string, newUrl: string): Promise<number>;
117
+ export(options: ExportOptions): Promise<ExportResult>;
118
+ preflightExport(options: ExportOptions): Promise<ExportPreflight>;
119
+ flushRecovery(): Promise<void>;
120
+ getRecoverableDocuments(): Promise<RecoveryRecord[]>;
121
+ getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
122
+ restoreRecovery(documentId?: string, options?: LoadOptions): Promise<FabricDocument>;
123
+ discardRecovery(documentId?: string): Promise<void>;
124
+ getInterruptedLoad(): Promise<InterruptedLoad | undefined>;
85
125
  on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
86
126
  destroy(): void;
87
127
  }
@@ -1,9 +1,12 @@
1
1
  import { DocumentInfo, FabricDocument } from "../document/document-format.js";
2
2
  import { AssetManifest } from "../assets/asset-manifest.js";
3
3
  import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.js";
4
+ import { ExportFormat, ExportOptions } from "../export/export-options.js";
5
+ import { ExportPreflight } from "../export/preflight-export.js";
4
6
  import { NewDocumentOptions } from "../document/create-document.js";
5
7
  import { CustomObjectDefinition } from "../fabric/object-registry.js";
6
8
  import { HistoryOptions, HistoryState } from "../history/create-history.js";
9
+ import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.js";
7
10
  import { AutosaveOptions } from "../save/autosave-scheduler.js";
8
11
  import { RetryOptions } from "../save/retry.js";
9
12
  import { DocumentEngineError } from "./errors.js";
@@ -21,10 +24,20 @@ export interface DocumentEngineOptions {
21
24
  autosave?: boolean | AutosaveOptions;
22
25
  saveRetry?: RetryOptions;
23
26
  assets?: AssetOptions;
27
+ recovery?: RecoveryOptions;
24
28
  }
25
29
  export interface LoadOptions {
26
30
  restoreCanvasSize?: boolean;
27
31
  }
32
+ export interface ExportResult {
33
+ format: ExportFormat;
34
+ mimeType: string;
35
+ blob: Blob;
36
+ width: number;
37
+ height: number;
38
+ warnings: AssetWarning[];
39
+ document?: FabricDocument;
40
+ }
28
41
  export interface DocumentEngineEvents {
29
42
  "load:start": {
30
43
  documentId: string | undefined;
@@ -54,6 +67,25 @@ export interface DocumentEngineEvents {
54
67
  "history:error": {
55
68
  error: DocumentEngineError;
56
69
  };
70
+ "recovery:checkpoint": {
71
+ documentId: string;
72
+ savedAt: string;
73
+ };
74
+ "recovery:restored": {
75
+ document: FabricDocument;
76
+ };
77
+ "recovery:error": {
78
+ error: DocumentEngineError;
79
+ };
80
+ "export:success": {
81
+ format: ExportFormat;
82
+ width: number;
83
+ height: number;
84
+ warnings: AssetWarning[];
85
+ };
86
+ "export:error": {
87
+ error: DocumentEngineError;
88
+ };
57
89
  }
58
90
  export interface DocumentEngine {
59
91
  readonly canvas: StaticCanvas;
@@ -82,6 +114,14 @@ export interface DocumentEngine {
82
114
  getAssetManifest(): AssetManifest;
83
115
  checkAssets(): Promise<AssetReport>;
84
116
  replaceImage(oldUrl: string, newUrl: string): Promise<number>;
117
+ export(options: ExportOptions): Promise<ExportResult>;
118
+ preflightExport(options: ExportOptions): Promise<ExportPreflight>;
119
+ flushRecovery(): Promise<void>;
120
+ getRecoverableDocuments(): Promise<RecoveryRecord[]>;
121
+ getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
122
+ restoreRecovery(documentId?: string, options?: LoadOptions): Promise<FabricDocument>;
123
+ discardRecovery(documentId?: string): Promise<void>;
124
+ getInterruptedLoad(): Promise<InterruptedLoad | undefined>;
85
125
  on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
86
126
  destroy(): void;
87
127
  }