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
@@ -0,0 +1,159 @@
1
+ const require_asset_pipeline = require("../assets/asset-pipeline.cjs");
2
+ //#region src/recovery/recovery-controller.ts
3
+ const documentKeyPrefix = "document:";
4
+ const unloadKeyPrefix = "unload:";
5
+ const loadingKey = "loading";
6
+ function documentKey(documentId) {
7
+ return `${documentKeyPrefix}${documentId}`;
8
+ }
9
+ function unloadKey(documentId) {
10
+ return `${unloadKeyPrefix}${documentId}`;
11
+ }
12
+ function newerRecord(checkpoint, unloadCopy) {
13
+ if (!unloadCopy) return checkpoint;
14
+ if (!checkpoint || unloadCopy.savedAt > checkpoint.savedAt) return {
15
+ ...unloadCopy,
16
+ files: {
17
+ ...checkpoint?.files,
18
+ ...unloadCopy.files
19
+ }
20
+ };
21
+ return checkpoint;
22
+ }
23
+ function isRecoveryRecord(value) {
24
+ const record = value;
25
+ return typeof record?.documentId === "string" && typeof record.document === "object" && record.document !== null;
26
+ }
27
+ async function restoreRecordedFiles(record) {
28
+ const document = JSON.parse(JSON.stringify(record.document));
29
+ const recreatedUrls = /* @__PURE__ */ new Map();
30
+ await require_asset_pipeline.rewriteUrls(document, async (url) => {
31
+ const blob = record.files[url];
32
+ if (!blob) return url;
33
+ const recreated = recreatedUrls.get(url) ?? URL.createObjectURL(blob);
34
+ recreatedUrls.set(url, recreated);
35
+ return recreated;
36
+ });
37
+ return document;
38
+ }
39
+ function createRecoveryController(options) {
40
+ const { store } = options;
41
+ const interval = options.interval ?? 2e3;
42
+ const capturedFiles = /* @__PURE__ */ new Map();
43
+ const readyFiles = /* @__PURE__ */ new Map();
44
+ let timer;
45
+ let queue = Promise.resolve();
46
+ let checkpointWaiting;
47
+ function enqueue(task) {
48
+ const run = queue.then(task).catch(options.onError);
49
+ queue = run;
50
+ return run;
51
+ }
52
+ async function captureTabOnlyFiles(document) {
53
+ const files = {};
54
+ await require_asset_pipeline.rewriteUrls(document, async (url) => {
55
+ if (!url.startsWith("blob:")) return url;
56
+ let capturing = capturedFiles.get(url);
57
+ if (!capturing) {
58
+ capturing = fetch(url).then((response) => response.blob());
59
+ capturedFiles.set(url, capturing);
60
+ capturing.then((blob) => readyFiles.set(url, blob), () => void 0);
61
+ capturing.catch(() => capturedFiles.delete(url));
62
+ }
63
+ files[url] = await capturing;
64
+ return url;
65
+ });
66
+ return files;
67
+ }
68
+ function createRecord(document, files) {
69
+ return {
70
+ documentId: document.id,
71
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
72
+ baseRevision: document.revision ?? 0,
73
+ document,
74
+ files
75
+ };
76
+ }
77
+ async function writeCheckpoint() {
78
+ checkpointWaiting = void 0;
79
+ if (!options.shouldWrite()) return;
80
+ const document = options.createDocument();
81
+ const record = createRecord(document, await captureTabOnlyFiles(document));
82
+ await store.set(documentKey(document.id), record);
83
+ options.onCheckpoint(record);
84
+ }
85
+ function writeCheckpointBeforeThePageCloses() {
86
+ cancel();
87
+ if (!options.shouldWrite()) return;
88
+ const document = options.createDocument();
89
+ if (store.setNow) {
90
+ store.setNow(unloadKey(document.id), createRecord(document, {}));
91
+ return;
92
+ }
93
+ const record = createRecord(document, Object.fromEntries(readyFiles));
94
+ store.set(documentKey(document.id), record).then(() => options.onCheckpoint(record), options.onError);
95
+ }
96
+ async function read(documentId) {
97
+ await queue;
98
+ const [checkpoint, unloadCopy] = await Promise.all([store.get(documentKey(documentId)), store.get(unloadKey(documentId))]);
99
+ return newerRecord(isRecoveryRecord(checkpoint) ? checkpoint : void 0, isRecoveryRecord(unloadCopy) ? unloadCopy : void 0);
100
+ }
101
+ function cancel() {
102
+ if (timer !== void 0) clearTimeout(timer);
103
+ timer = void 0;
104
+ }
105
+ function flush() {
106
+ cancel();
107
+ checkpointWaiting ?? (checkpointWaiting = enqueue(writeCheckpoint));
108
+ return checkpointWaiting;
109
+ }
110
+ const handlePageHidden = () => writeCheckpointBeforeThePageCloses();
111
+ const handleVisibilityChange = () => {
112
+ if (document.visibilityState === "hidden") writeCheckpointBeforeThePageCloses();
113
+ };
114
+ const pageEvents = typeof window === "undefined" ? void 0 : window;
115
+ pageEvents?.addEventListener("pagehide", handlePageHidden);
116
+ pageEvents?.document.addEventListener("visibilitychange", handleVisibilityChange);
117
+ return {
118
+ schedule() {
119
+ if (timer !== void 0) return;
120
+ timer = setTimeout(() => {
121
+ timer = void 0;
122
+ flush();
123
+ }, interval);
124
+ },
125
+ flush,
126
+ cancel,
127
+ remove: (documentId) => enqueue(async () => {
128
+ await store.delete(unloadKey(documentId));
129
+ await store.delete(documentKey(documentId));
130
+ }),
131
+ read,
132
+ async list() {
133
+ await queue;
134
+ const documentIds = /* @__PURE__ */ new Set();
135
+ for (const key of await store.keys()) {
136
+ if (key.startsWith(documentKeyPrefix)) documentIds.add(key.slice(9));
137
+ if (key.startsWith(unloadKeyPrefix)) documentIds.add(key.slice(7));
138
+ }
139
+ return (await Promise.all([...documentIds].map(read))).filter((record) => record !== void 0).sort((first, second) => second.savedAt.localeCompare(first.savedAt));
140
+ },
141
+ markLoadStarted: (documentId) => store.set(loadingKey, {
142
+ documentId,
143
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
144
+ }).catch(options.onError),
145
+ markLoadFinished: () => store.delete(loadingKey).catch(options.onError),
146
+ async interruptedLoad() {
147
+ const marker = await store.get(loadingKey);
148
+ return marker && typeof marker.startedAt === "string" ? marker : void 0;
149
+ },
150
+ destroy() {
151
+ cancel();
152
+ pageEvents?.removeEventListener("pagehide", handlePageHidden);
153
+ pageEvents?.document.removeEventListener("visibilitychange", handleVisibilityChange);
154
+ }
155
+ };
156
+ }
157
+ //#endregion
158
+ exports.createRecoveryController = createRecoveryController;
159
+ exports.restoreRecordedFiles = restoreRecordedFiles;
@@ -0,0 +1,19 @@
1
+ import { FabricDocument } from "../document/document-format.cjs";
2
+ import { RecoveryStore } from "./recovery-store.cjs";
3
+ //#region src/recovery/recovery-controller.d.ts
4
+ export interface RecoveryOptions {
5
+ store: RecoveryStore;
6
+ interval?: number;
7
+ }
8
+ export interface RecoveryRecord {
9
+ documentId: string;
10
+ savedAt: string;
11
+ baseRevision: number;
12
+ document: FabricDocument;
13
+ files: Record<string, Blob>;
14
+ }
15
+ export interface InterruptedLoad {
16
+ documentId: string | undefined;
17
+ startedAt: string;
18
+ }
19
+ //#endregion
@@ -0,0 +1,19 @@
1
+ import { FabricDocument } from "../document/document-format.js";
2
+ import { RecoveryStore } from "./recovery-store.js";
3
+ //#region src/recovery/recovery-controller.d.ts
4
+ export interface RecoveryOptions {
5
+ store: RecoveryStore;
6
+ interval?: number;
7
+ }
8
+ export interface RecoveryRecord {
9
+ documentId: string;
10
+ savedAt: string;
11
+ baseRevision: number;
12
+ document: FabricDocument;
13
+ files: Record<string, Blob>;
14
+ }
15
+ export interface InterruptedLoad {
16
+ documentId: string | undefined;
17
+ startedAt: string;
18
+ }
19
+ //#endregion
@@ -0,0 +1,158 @@
1
+ import { rewriteUrls } from "../assets/asset-pipeline.js";
2
+ //#region src/recovery/recovery-controller.ts
3
+ const documentKeyPrefix = "document:";
4
+ const unloadKeyPrefix = "unload:";
5
+ const loadingKey = "loading";
6
+ function documentKey(documentId) {
7
+ return `${documentKeyPrefix}${documentId}`;
8
+ }
9
+ function unloadKey(documentId) {
10
+ return `${unloadKeyPrefix}${documentId}`;
11
+ }
12
+ function newerRecord(checkpoint, unloadCopy) {
13
+ if (!unloadCopy) return checkpoint;
14
+ if (!checkpoint || unloadCopy.savedAt > checkpoint.savedAt) return {
15
+ ...unloadCopy,
16
+ files: {
17
+ ...checkpoint?.files,
18
+ ...unloadCopy.files
19
+ }
20
+ };
21
+ return checkpoint;
22
+ }
23
+ function isRecoveryRecord(value) {
24
+ const record = value;
25
+ return typeof record?.documentId === "string" && typeof record.document === "object" && record.document !== null;
26
+ }
27
+ async function restoreRecordedFiles(record) {
28
+ const document = JSON.parse(JSON.stringify(record.document));
29
+ const recreatedUrls = /* @__PURE__ */ new Map();
30
+ await rewriteUrls(document, async (url) => {
31
+ const blob = record.files[url];
32
+ if (!blob) return url;
33
+ const recreated = recreatedUrls.get(url) ?? URL.createObjectURL(blob);
34
+ recreatedUrls.set(url, recreated);
35
+ return recreated;
36
+ });
37
+ return document;
38
+ }
39
+ function createRecoveryController(options) {
40
+ const { store } = options;
41
+ const interval = options.interval ?? 2e3;
42
+ const capturedFiles = /* @__PURE__ */ new Map();
43
+ const readyFiles = /* @__PURE__ */ new Map();
44
+ let timer;
45
+ let queue = Promise.resolve();
46
+ let checkpointWaiting;
47
+ function enqueue(task) {
48
+ const run = queue.then(task).catch(options.onError);
49
+ queue = run;
50
+ return run;
51
+ }
52
+ async function captureTabOnlyFiles(document) {
53
+ const files = {};
54
+ await rewriteUrls(document, async (url) => {
55
+ if (!url.startsWith("blob:")) return url;
56
+ let capturing = capturedFiles.get(url);
57
+ if (!capturing) {
58
+ capturing = fetch(url).then((response) => response.blob());
59
+ capturedFiles.set(url, capturing);
60
+ capturing.then((blob) => readyFiles.set(url, blob), () => void 0);
61
+ capturing.catch(() => capturedFiles.delete(url));
62
+ }
63
+ files[url] = await capturing;
64
+ return url;
65
+ });
66
+ return files;
67
+ }
68
+ function createRecord(document, files) {
69
+ return {
70
+ documentId: document.id,
71
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
72
+ baseRevision: document.revision ?? 0,
73
+ document,
74
+ files
75
+ };
76
+ }
77
+ async function writeCheckpoint() {
78
+ checkpointWaiting = void 0;
79
+ if (!options.shouldWrite()) return;
80
+ const document = options.createDocument();
81
+ const record = createRecord(document, await captureTabOnlyFiles(document));
82
+ await store.set(documentKey(document.id), record);
83
+ options.onCheckpoint(record);
84
+ }
85
+ function writeCheckpointBeforeThePageCloses() {
86
+ cancel();
87
+ if (!options.shouldWrite()) return;
88
+ const document = options.createDocument();
89
+ if (store.setNow) {
90
+ store.setNow(unloadKey(document.id), createRecord(document, {}));
91
+ return;
92
+ }
93
+ const record = createRecord(document, Object.fromEntries(readyFiles));
94
+ store.set(documentKey(document.id), record).then(() => options.onCheckpoint(record), options.onError);
95
+ }
96
+ async function read(documentId) {
97
+ await queue;
98
+ const [checkpoint, unloadCopy] = await Promise.all([store.get(documentKey(documentId)), store.get(unloadKey(documentId))]);
99
+ return newerRecord(isRecoveryRecord(checkpoint) ? checkpoint : void 0, isRecoveryRecord(unloadCopy) ? unloadCopy : void 0);
100
+ }
101
+ function cancel() {
102
+ if (timer !== void 0) clearTimeout(timer);
103
+ timer = void 0;
104
+ }
105
+ function flush() {
106
+ cancel();
107
+ checkpointWaiting ?? (checkpointWaiting = enqueue(writeCheckpoint));
108
+ return checkpointWaiting;
109
+ }
110
+ const handlePageHidden = () => writeCheckpointBeforeThePageCloses();
111
+ const handleVisibilityChange = () => {
112
+ if (document.visibilityState === "hidden") writeCheckpointBeforeThePageCloses();
113
+ };
114
+ const pageEvents = typeof window === "undefined" ? void 0 : window;
115
+ pageEvents?.addEventListener("pagehide", handlePageHidden);
116
+ pageEvents?.document.addEventListener("visibilitychange", handleVisibilityChange);
117
+ return {
118
+ schedule() {
119
+ if (timer !== void 0) return;
120
+ timer = setTimeout(() => {
121
+ timer = void 0;
122
+ flush();
123
+ }, interval);
124
+ },
125
+ flush,
126
+ cancel,
127
+ remove: (documentId) => enqueue(async () => {
128
+ await store.delete(unloadKey(documentId));
129
+ await store.delete(documentKey(documentId));
130
+ }),
131
+ read,
132
+ async list() {
133
+ await queue;
134
+ const documentIds = /* @__PURE__ */ new Set();
135
+ for (const key of await store.keys()) {
136
+ if (key.startsWith(documentKeyPrefix)) documentIds.add(key.slice(9));
137
+ if (key.startsWith(unloadKeyPrefix)) documentIds.add(key.slice(7));
138
+ }
139
+ return (await Promise.all([...documentIds].map(read))).filter((record) => record !== void 0).sort((first, second) => second.savedAt.localeCompare(first.savedAt));
140
+ },
141
+ markLoadStarted: (documentId) => store.set(loadingKey, {
142
+ documentId,
143
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
144
+ }).catch(options.onError),
145
+ markLoadFinished: () => store.delete(loadingKey).catch(options.onError),
146
+ async interruptedLoad() {
147
+ const marker = await store.get(loadingKey);
148
+ return marker && typeof marker.startedAt === "string" ? marker : void 0;
149
+ },
150
+ destroy() {
151
+ cancel();
152
+ pageEvents?.removeEventListener("pagehide", handlePageHidden);
153
+ pageEvents?.document.removeEventListener("visibilitychange", handleVisibilityChange);
154
+ }
155
+ };
156
+ }
157
+ //#endregion
158
+ export { createRecoveryController, restoreRecordedFiles };
@@ -0,0 +1,19 @@
1
+ //#region src/recovery/recovery-store.ts
2
+ function createMemoryRecovery() {
3
+ const values = /* @__PURE__ */ new Map();
4
+ return {
5
+ get: async (key) => values.get(key),
6
+ set: async (key, value) => {
7
+ values.set(key, value);
8
+ },
9
+ setNow: (key, value) => {
10
+ values.set(key, value);
11
+ },
12
+ delete: async (key) => {
13
+ values.delete(key);
14
+ },
15
+ keys: async () => [...values.keys()]
16
+ };
17
+ }
18
+ //#endregion
19
+ exports.createMemoryRecovery = createMemoryRecovery;
@@ -0,0 +1,10 @@
1
+ //#region src/recovery/recovery-store.d.ts
2
+ export interface RecoveryStore {
3
+ get(key: string): Promise<unknown>;
4
+ set(key: string, value: unknown): Promise<void>;
5
+ setNow?(key: string, value: unknown): void;
6
+ delete(key: string): Promise<void>;
7
+ keys(): Promise<string[]>;
8
+ }
9
+ export declare function createMemoryRecovery(): RecoveryStore;
10
+ //#endregion
@@ -0,0 +1,10 @@
1
+ //#region src/recovery/recovery-store.d.ts
2
+ export interface RecoveryStore {
3
+ get(key: string): Promise<unknown>;
4
+ set(key: string, value: unknown): Promise<void>;
5
+ setNow?(key: string, value: unknown): void;
6
+ delete(key: string): Promise<void>;
7
+ keys(): Promise<string[]>;
8
+ }
9
+ export declare function createMemoryRecovery(): RecoveryStore;
10
+ //#endregion
@@ -0,0 +1,19 @@
1
+ //#region src/recovery/recovery-store.ts
2
+ function createMemoryRecovery() {
3
+ const values = /* @__PURE__ */ new Map();
4
+ return {
5
+ get: async (key) => values.get(key),
6
+ set: async (key, value) => {
7
+ values.set(key, value);
8
+ },
9
+ setNow: (key, value) => {
10
+ values.set(key, value);
11
+ },
12
+ delete: async (key) => {
13
+ values.delete(key);
14
+ },
15
+ keys: async () => [...values.keys()]
16
+ };
17
+ }
18
+ //#endregion
19
+ export { createMemoryRecovery };
@@ -0,0 +1,5 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_indexed_db_recovery = require("./recovery/indexed-db-recovery.cjs");
3
+ const require_recovery_store = require("./recovery/recovery-store.cjs");
4
+ exports.createIndexedDbRecovery = require_indexed_db_recovery.createIndexedDbRecovery;
5
+ exports.createMemoryRecovery = require_recovery_store.createMemoryRecovery;
@@ -0,0 +1,4 @@
1
+ import { RecoveryStore, createMemoryRecovery } from "./recovery/recovery-store.cjs";
2
+ import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "./recovery/recovery-controller.cjs";
3
+ import { IndexedDbRecoveryOptions, createIndexedDbRecovery } from "./recovery/indexed-db-recovery.cjs";
4
+ export { type IndexedDbRecoveryOptions, type InterruptedLoad, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, createIndexedDbRecovery, createMemoryRecovery };
@@ -0,0 +1,4 @@
1
+ import { RecoveryStore, createMemoryRecovery } from "./recovery/recovery-store.js";
2
+ import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "./recovery/recovery-controller.js";
3
+ import { IndexedDbRecoveryOptions, createIndexedDbRecovery } from "./recovery/indexed-db-recovery.js";
4
+ export { type IndexedDbRecoveryOptions, type InterruptedLoad, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, createIndexedDbRecovery, createMemoryRecovery };
@@ -0,0 +1,3 @@
1
+ import { createIndexedDbRecovery } from "./recovery/indexed-db-recovery.js";
2
+ import { createMemoryRecovery } from "./recovery/recovery-store.js";
3
+ export { createIndexedDbRecovery, createMemoryRecovery };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabricjs-document-engine",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",
@@ -18,7 +18,8 @@
18
18
  "autosave",
19
19
  "editor",
20
20
  "typescript",
21
- "storage"
21
+ "storage",
22
+ "recovery"
22
23
  ],
23
24
  "license": "MIT",
24
25
  "author": "Sohail Khan (https://me.jscrate.dev)",
@@ -53,6 +54,16 @@
53
54
  "default": "./dist/storage.cjs"
54
55
  }
55
56
  },
57
+ "./recovery": {
58
+ "import": {
59
+ "types": "./dist/recovery.d.ts",
60
+ "default": "./dist/recovery.js"
61
+ },
62
+ "require": {
63
+ "types": "./dist/recovery.d.cts",
64
+ "default": "./dist/recovery.cjs"
65
+ }
66
+ },
56
67
  "./package.json": "./package.json"
57
68
  },
58
69
  "scripts": {