fabricjs-document-engine 0.2.0 → 0.4.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 +113 -13
- package/dist/assets/asset-manifest.cjs +43 -0
- package/dist/assets/asset-manifest.d.cts +16 -0
- package/dist/assets/asset-manifest.d.ts +16 -0
- package/dist/assets/asset-manifest.js +41 -0
- package/dist/assets/asset-pipeline.cjs +175 -0
- package/dist/assets/asset-pipeline.d.cts +31 -0
- package/dist/assets/asset-pipeline.d.ts +31 -0
- package/dist/assets/asset-pipeline.js +172 -0
- package/dist/assets/asset-references.cjs +83 -0
- package/dist/assets/asset-references.js +82 -0
- package/dist/assets/font-check.cjs +59 -0
- package/dist/assets/font-check.d.cts +4 -0
- package/dist/assets/font-check.d.ts +4 -0
- package/dist/assets/font-check.js +57 -0
- package/dist/assets/image-check.cjs +36 -0
- package/dist/assets/image-check.js +33 -0
- package/dist/document/document-format.d.cts +2 -0
- package/dist/document/document-format.d.ts +2 -0
- package/dist/document/validate-document.cjs +4 -0
- package/dist/document/validate-document.js +4 -0
- package/dist/engine/create-document-engine.cjs +112 -10
- package/dist/engine/create-document-engine.d.cts +28 -0
- package/dist/engine/create-document-engine.d.ts +28 -0
- package/dist/engine/create-document-engine.js +112 -10
- package/dist/engine/errors.cjs +2 -0
- package/dist/engine/errors.d.cts +6 -1
- package/dist/engine/errors.d.ts +6 -1
- package/dist/engine/errors.js +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1 -1
- package/dist/recovery/indexed-db-recovery.cjs +80 -0
- package/dist/recovery/indexed-db-recovery.d.cts +8 -0
- package/dist/recovery/indexed-db-recovery.d.ts +8 -0
- package/dist/recovery/indexed-db-recovery.js +80 -0
- package/dist/recovery/recovery-controller.cjs +159 -0
- package/dist/recovery/recovery-controller.d.cts +19 -0
- package/dist/recovery/recovery-controller.d.ts +19 -0
- package/dist/recovery/recovery-controller.js +158 -0
- package/dist/recovery/recovery-store.cjs +19 -0
- package/dist/recovery/recovery-store.d.cts +10 -0
- package/dist/recovery/recovery-store.d.ts +10 -0
- package/dist/recovery/recovery-store.js +19 -0
- package/dist/recovery.cjs +5 -0
- package/dist/recovery.d.cts +4 -0
- package/dist/recovery.d.ts +4 -0
- package/dist/recovery.js +3 -0
- package/dist/save/save-controller.cjs +6 -5
- package/dist/save/save-controller.js +6 -5
- package/package.json +13 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { CURRENT_SCHEMA_VERSION, DocumentCanvas, DocumentInfo, FabricDocument, SerializedFabricObject } from "./document/document-format.cjs";
|
|
2
|
+
import { AssetManifest, FontAsset, ImageAsset } from "./assets/asset-manifest.cjs";
|
|
3
|
+
import { FontLoader } from "./assets/font-check.cjs";
|
|
4
|
+
import { AssetOptions, AssetReport, AssetWarning, AssetWarningCode, UploadRequest } from "./assets/asset-pipeline.cjs";
|
|
2
5
|
import { NewDocumentOptions } from "./document/create-document.cjs";
|
|
3
6
|
import { CustomObjectDefinition } from "./fabric/object-registry.cjs";
|
|
4
7
|
import { HistoryOptions, HistoryState } from "./history/create-history.cjs";
|
|
8
|
+
import { RecoveryStore } from "./recovery/recovery-store.cjs";
|
|
9
|
+
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "./recovery/recovery-controller.cjs";
|
|
5
10
|
import { AutosaveOptions } from "./save/autosave-scheduler.cjs";
|
|
6
11
|
import { RetryOptions } from "./save/retry.cjs";
|
|
7
12
|
import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, createConflictError, isDocumentEngineError } from "./engine/errors.cjs";
|
|
@@ -12,4 +17,4 @@ import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, LoadOption
|
|
|
12
17
|
import { validateDocument } from "./document/validate-document.cjs";
|
|
13
18
|
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.cjs";
|
|
14
19
|
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.cjs";
|
|
15
|
-
export { type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type HistoryOptions, type HistoryState, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
|
|
20
|
+
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, 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, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { CURRENT_SCHEMA_VERSION, DocumentCanvas, DocumentInfo, FabricDocument, SerializedFabricObject } from "./document/document-format.js";
|
|
2
|
+
import { AssetManifest, FontAsset, ImageAsset } from "./assets/asset-manifest.js";
|
|
3
|
+
import { FontLoader } from "./assets/font-check.js";
|
|
4
|
+
import { AssetOptions, AssetReport, AssetWarning, AssetWarningCode, UploadRequest } from "./assets/asset-pipeline.js";
|
|
2
5
|
import { NewDocumentOptions } from "./document/create-document.js";
|
|
3
6
|
import { CustomObjectDefinition } from "./fabric/object-registry.js";
|
|
4
7
|
import { HistoryOptions, HistoryState } from "./history/create-history.js";
|
|
8
|
+
import { RecoveryStore } from "./recovery/recovery-store.js";
|
|
9
|
+
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "./recovery/recovery-controller.js";
|
|
5
10
|
import { AutosaveOptions } from "./save/autosave-scheduler.js";
|
|
6
11
|
import { RetryOptions } from "./save/retry.js";
|
|
7
12
|
import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, createConflictError, isDocumentEngineError } from "./engine/errors.js";
|
|
@@ -12,4 +17,4 @@ import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, LoadOption
|
|
|
12
17
|
import { validateDocument } from "./document/validate-document.js";
|
|
13
18
|
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
14
19
|
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
|
|
15
|
-
export { type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type HistoryOptions, type HistoryState, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
|
|
20
|
+
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, 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, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { DocumentEngineError, createConflictError, isDocumentEngineError } from "./engine/errors.js";
|
|
1
2
|
import { CURRENT_SCHEMA_VERSION } from "./document/document-format.js";
|
|
2
3
|
import { validateDocument } from "./document/validate-document.js";
|
|
3
|
-
import { DocumentEngineError, createConflictError, isDocumentEngineError } from "./engine/errors.js";
|
|
4
4
|
import { createDocumentEngine } from "./engine/create-document-engine.js";
|
|
5
5
|
import { bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
6
6
|
import { bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/recovery/indexed-db-recovery.ts
|
|
2
|
+
function requestToPromise(request) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
request.onsuccess = () => resolve(request.result);
|
|
5
|
+
request.onerror = () => reject(request.error);
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
function browserStorage() {
|
|
9
|
+
try {
|
|
10
|
+
return globalThis.localStorage;
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function createIndexedDbRecovery(options = {}) {
|
|
16
|
+
const databaseName = options.databaseName ?? "fabricjs-document-engine";
|
|
17
|
+
const storeName = options.storeName ?? "recovery";
|
|
18
|
+
let opening;
|
|
19
|
+
let database;
|
|
20
|
+
const emergencyPrefix = `${databaseName}/${storeName}:`;
|
|
21
|
+
const emergencyStorage = browserStorage();
|
|
22
|
+
function readEmergency(key) {
|
|
23
|
+
const json = emergencyStorage?.getItem(`${emergencyPrefix}${key}`);
|
|
24
|
+
return json === null || json === void 0 ? void 0 : JSON.parse(json);
|
|
25
|
+
}
|
|
26
|
+
function emergencyKeys() {
|
|
27
|
+
if (!emergencyStorage) return [];
|
|
28
|
+
return Array.from({ length: emergencyStorage.length }, (_, index) => emergencyStorage.key(index) ?? "").filter((key) => key.startsWith(emergencyPrefix)).map((key) => key.slice(emergencyPrefix.length));
|
|
29
|
+
}
|
|
30
|
+
function openDatabase() {
|
|
31
|
+
opening ?? (opening = new Promise((resolve, reject) => {
|
|
32
|
+
const request = indexedDB.open(databaseName, 1);
|
|
33
|
+
request.onupgradeneeded = () => {
|
|
34
|
+
if (!request.result.objectStoreNames.contains(storeName)) request.result.createObjectStore(storeName);
|
|
35
|
+
};
|
|
36
|
+
request.onsuccess = () => {
|
|
37
|
+
database = request.result;
|
|
38
|
+
resolve(request.result);
|
|
39
|
+
};
|
|
40
|
+
request.onerror = () => {
|
|
41
|
+
opening = void 0;
|
|
42
|
+
reject(request.error);
|
|
43
|
+
};
|
|
44
|
+
}));
|
|
45
|
+
return opening;
|
|
46
|
+
}
|
|
47
|
+
async function run(mode, work) {
|
|
48
|
+
const transaction = (database ?? await openDatabase()).transaction(storeName, mode);
|
|
49
|
+
const result = requestToPromise(work(transaction.objectStore(storeName)));
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
transaction.oncomplete = () => resolve();
|
|
52
|
+
transaction.onerror = () => reject(transaction.error);
|
|
53
|
+
transaction.onabort = () => reject(transaction.error);
|
|
54
|
+
});
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
get: async (key) => await run("readonly", (store) => store.get(key)) ?? readEmergency(key),
|
|
59
|
+
set: async (key, value) => {
|
|
60
|
+
await run("readwrite", (store) => store.put(value, key));
|
|
61
|
+
},
|
|
62
|
+
setNow(key, value) {
|
|
63
|
+
try {
|
|
64
|
+
emergencyStorage?.setItem(`${emergencyPrefix}${key}`, JSON.stringify(value));
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
delete: async (key) => {
|
|
70
|
+
emergencyStorage?.removeItem(`${emergencyPrefix}${key}`);
|
|
71
|
+
await run("readwrite", (store) => store.delete(key));
|
|
72
|
+
},
|
|
73
|
+
keys: async () => {
|
|
74
|
+
const stored = (await run("readonly", (store) => store.getAllKeys())).map(String);
|
|
75
|
+
return [.../* @__PURE__ */ new Set([...stored, ...emergencyKeys()])];
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
exports.createIndexedDbRecovery = createIndexedDbRecovery;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RecoveryStore } from "./recovery-store.cjs";
|
|
2
|
+
//#region src/recovery/indexed-db-recovery.d.ts
|
|
3
|
+
export interface IndexedDbRecoveryOptions {
|
|
4
|
+
databaseName?: string;
|
|
5
|
+
storeName?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createIndexedDbRecovery(options?: IndexedDbRecoveryOptions): RecoveryStore;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RecoveryStore } from "./recovery-store.js";
|
|
2
|
+
//#region src/recovery/indexed-db-recovery.d.ts
|
|
3
|
+
export interface IndexedDbRecoveryOptions {
|
|
4
|
+
databaseName?: string;
|
|
5
|
+
storeName?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createIndexedDbRecovery(options?: IndexedDbRecoveryOptions): RecoveryStore;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/recovery/indexed-db-recovery.ts
|
|
2
|
+
function requestToPromise(request) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
request.onsuccess = () => resolve(request.result);
|
|
5
|
+
request.onerror = () => reject(request.error);
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
function browserStorage() {
|
|
9
|
+
try {
|
|
10
|
+
return globalThis.localStorage;
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function createIndexedDbRecovery(options = {}) {
|
|
16
|
+
const databaseName = options.databaseName ?? "fabricjs-document-engine";
|
|
17
|
+
const storeName = options.storeName ?? "recovery";
|
|
18
|
+
let opening;
|
|
19
|
+
let database;
|
|
20
|
+
const emergencyPrefix = `${databaseName}/${storeName}:`;
|
|
21
|
+
const emergencyStorage = browserStorage();
|
|
22
|
+
function readEmergency(key) {
|
|
23
|
+
const json = emergencyStorage?.getItem(`${emergencyPrefix}${key}`);
|
|
24
|
+
return json === null || json === void 0 ? void 0 : JSON.parse(json);
|
|
25
|
+
}
|
|
26
|
+
function emergencyKeys() {
|
|
27
|
+
if (!emergencyStorage) return [];
|
|
28
|
+
return Array.from({ length: emergencyStorage.length }, (_, index) => emergencyStorage.key(index) ?? "").filter((key) => key.startsWith(emergencyPrefix)).map((key) => key.slice(emergencyPrefix.length));
|
|
29
|
+
}
|
|
30
|
+
function openDatabase() {
|
|
31
|
+
opening ?? (opening = new Promise((resolve, reject) => {
|
|
32
|
+
const request = indexedDB.open(databaseName, 1);
|
|
33
|
+
request.onupgradeneeded = () => {
|
|
34
|
+
if (!request.result.objectStoreNames.contains(storeName)) request.result.createObjectStore(storeName);
|
|
35
|
+
};
|
|
36
|
+
request.onsuccess = () => {
|
|
37
|
+
database = request.result;
|
|
38
|
+
resolve(request.result);
|
|
39
|
+
};
|
|
40
|
+
request.onerror = () => {
|
|
41
|
+
opening = void 0;
|
|
42
|
+
reject(request.error);
|
|
43
|
+
};
|
|
44
|
+
}));
|
|
45
|
+
return opening;
|
|
46
|
+
}
|
|
47
|
+
async function run(mode, work) {
|
|
48
|
+
const transaction = (database ?? await openDatabase()).transaction(storeName, mode);
|
|
49
|
+
const result = requestToPromise(work(transaction.objectStore(storeName)));
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
transaction.oncomplete = () => resolve();
|
|
52
|
+
transaction.onerror = () => reject(transaction.error);
|
|
53
|
+
transaction.onabort = () => reject(transaction.error);
|
|
54
|
+
});
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
get: async (key) => await run("readonly", (store) => store.get(key)) ?? readEmergency(key),
|
|
59
|
+
set: async (key, value) => {
|
|
60
|
+
await run("readwrite", (store) => store.put(value, key));
|
|
61
|
+
},
|
|
62
|
+
setNow(key, value) {
|
|
63
|
+
try {
|
|
64
|
+
emergencyStorage?.setItem(`${emergencyPrefix}${key}`, JSON.stringify(value));
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
delete: async (key) => {
|
|
70
|
+
emergencyStorage?.removeItem(`${emergencyPrefix}${key}`);
|
|
71
|
+
await run("readwrite", (store) => store.delete(key));
|
|
72
|
+
},
|
|
73
|
+
keys: async () => {
|
|
74
|
+
const stored = (await run("readonly", (store) => store.getAllKeys())).map(String);
|
|
75
|
+
return [.../* @__PURE__ */ new Set([...stored, ...emergencyKeys()])];
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { createIndexedDbRecovery };
|
|
@@ -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 };
|