fabricjs-document-engine 0.0.0 → 0.2.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 +146 -12
- package/dist/document/document-format.d.cts +1 -0
- package/dist/document/document-format.d.ts +1 -0
- package/dist/document/validate-document.cjs +1 -0
- package/dist/document/validate-document.js +1 -0
- package/dist/engine/create-document-engine.cjs +70 -16
- package/dist/engine/create-document-engine.d.cts +28 -5
- package/dist/engine/create-document-engine.d.ts +28 -5
- package/dist/engine/create-document-engine.js +70 -16
- package/dist/engine/errors.cjs +5 -0
- package/dist/engine/errors.d.cts +4 -1
- package/dist/engine/errors.d.ts +4 -1
- package/dist/engine/errors.js +5 -1
- package/dist/fabric/fabric-adapter.cjs +7 -2
- package/dist/fabric/fabric-adapter.js +7 -3
- package/dist/history/apply-state.cjs +48 -0
- package/dist/history/apply-state.js +48 -0
- package/dist/history/create-history.cjs +193 -0
- package/dist/history/create-history.d.cts +12 -0
- package/dist/history/create-history.d.ts +12 -0
- package/dist/history/create-history.js +192 -0
- package/dist/history/history-stack.cjs +27 -0
- package/dist/history/history-stack.js +27 -0
- package/dist/history/keyboard-shortcuts.cjs +32 -0
- package/dist/history/keyboard-shortcuts.d.cts +10 -0
- package/dist/history/keyboard-shortcuts.d.ts +10 -0
- package/dist/history/keyboard-shortcuts.js +32 -0
- package/dist/history/snapshot.cjs +47 -0
- package/dist/history/snapshot.js +46 -0
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +4 -2
- package/dist/save/autosave-scheduler.cjs +28 -0
- package/dist/save/autosave-scheduler.d.cts +6 -0
- package/dist/save/autosave-scheduler.d.ts +6 -0
- package/dist/save/autosave-scheduler.js +28 -0
- package/dist/save/retry.cjs +33 -0
- package/dist/save/retry.d.cts +7 -0
- package/dist/save/retry.d.ts +7 -0
- package/dist/save/retry.js +31 -0
- package/dist/save/save-controller.cjs +148 -0
- package/dist/save/save-controller.d.cts +20 -0
- package/dist/save/save-controller.d.ts +20 -0
- package/dist/save/save-controller.js +148 -0
- package/dist/save/unsaved-changes-warning.cjs +12 -0
- package/dist/save/unsaved-changes-warning.d.cts +6 -0
- package/dist/save/unsaved-changes-warning.d.ts +6 -0
- package/dist/save/unsaved-changes-warning.js +12 -0
- package/dist/storage/key-value-storage.cjs +35 -0
- package/dist/storage/key-value-storage.d.cts +14 -0
- package/dist/storage/key-value-storage.d.ts +14 -0
- package/dist/storage/key-value-storage.js +35 -0
- package/dist/storage/local-storage.cjs +24 -0
- package/dist/storage/local-storage.d.cts +8 -0
- package/dist/storage/local-storage.d.ts +8 -0
- package/dist/storage/local-storage.js +24 -0
- package/dist/storage/memory-storage.cjs +17 -0
- package/dist/storage/memory-storage.d.cts +4 -0
- package/dist/storage/memory-storage.d.ts +4 -0
- package/dist/storage/memory-storage.js +17 -0
- package/dist/storage/storage-contract.d.cts +14 -0
- package/dist/storage/storage-contract.d.ts +14 -0
- package/dist/storage.cjs +7 -0
- package/dist/storage.d.cts +5 -0
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +4 -0
- package/package.json +14 -3
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
const require_autosave_scheduler = require("./autosave-scheduler.cjs");
|
|
3
|
+
const require_retry = require("./retry.cjs");
|
|
4
|
+
//#region src/save/save-controller.ts
|
|
5
|
+
function cancelledError() {
|
|
6
|
+
return new require_errors.DocumentEngineError("SAVE_CANCELLED", "The save was cancelled because another document was opened");
|
|
7
|
+
}
|
|
8
|
+
function toSaveError(error, signal) {
|
|
9
|
+
if (require_errors.isDocumentEngineError(error)) return error;
|
|
10
|
+
if (signal.aborted) return cancelledError();
|
|
11
|
+
const hints = error ?? {};
|
|
12
|
+
const reason = typeof hints.message === "string" ? hints.message : String(error);
|
|
13
|
+
if (hints.code === "SAVE_CONFLICT") return new require_errors.DocumentEngineError("SAVE_CONFLICT", reason, { cause: error });
|
|
14
|
+
return new require_errors.DocumentEngineError("SAVE_FAILED", `Storage could not save the document: ${reason}`, {
|
|
15
|
+
cause: error,
|
|
16
|
+
retryable: require_retry.isRetryable(error)
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function createSaveController(options) {
|
|
20
|
+
const attempts = Math.max(0, options.retry?.attempts ?? 3);
|
|
21
|
+
let contentVersion = 0;
|
|
22
|
+
let savedVersion = 0;
|
|
23
|
+
let revision = 0;
|
|
24
|
+
let lastSavedAt;
|
|
25
|
+
let lastError;
|
|
26
|
+
let saving = false;
|
|
27
|
+
let session = 0;
|
|
28
|
+
let sessionAbort = new AbortController();
|
|
29
|
+
let inFlight = null;
|
|
30
|
+
let queued = null;
|
|
31
|
+
const isDirty = () => contentVersion !== savedVersion;
|
|
32
|
+
function state() {
|
|
33
|
+
let status = isDirty() ? "unsaved" : "saved";
|
|
34
|
+
if (lastError) status = lastError.code === "SAVE_CONFLICT" ? "conflict" : "error";
|
|
35
|
+
if (saving) status = "saving";
|
|
36
|
+
return {
|
|
37
|
+
status,
|
|
38
|
+
isDirty: isDirty(),
|
|
39
|
+
isSaving: saving,
|
|
40
|
+
revision,
|
|
41
|
+
lastSavedAt,
|
|
42
|
+
error: lastError
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const publish = () => options.onStateChange(state());
|
|
46
|
+
const autosave = options.autosave === false || options.autosave === void 0 ? null : require_autosave_scheduler.createAutosaveScheduler(() => {
|
|
47
|
+
if (isDirty()) save().catch(() => void 0);
|
|
48
|
+
}, options.autosave);
|
|
49
|
+
async function sendWithRetries(storage, document, expectedRevision, signal) {
|
|
50
|
+
for (let attempt = 0;; attempt += 1) try {
|
|
51
|
+
return await storage.saveDocument(document, {
|
|
52
|
+
expectedRevision,
|
|
53
|
+
signal
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (signal.aborted || !require_retry.isRetryable(error) || attempt >= attempts) throw error;
|
|
57
|
+
const delay = require_retry.retryDelay(attempt, options.retry);
|
|
58
|
+
options.onRetry({
|
|
59
|
+
attempt: attempt + 1,
|
|
60
|
+
delay,
|
|
61
|
+
error
|
|
62
|
+
});
|
|
63
|
+
await require_retry.wait(delay, signal);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function runSave(saveOptions) {
|
|
67
|
+
const storage = options.getStorage();
|
|
68
|
+
const sessionAtStart = session;
|
|
69
|
+
const { signal } = sessionAbort;
|
|
70
|
+
const versionAtSnapshot = contentVersion;
|
|
71
|
+
const document = {
|
|
72
|
+
...options.createDocument(),
|
|
73
|
+
revision: revision + 1
|
|
74
|
+
};
|
|
75
|
+
const expectedRevision = saveOptions.overwrite === true ? null : revision;
|
|
76
|
+
saving = true;
|
|
77
|
+
lastError = void 0;
|
|
78
|
+
publish();
|
|
79
|
+
options.onStart(document);
|
|
80
|
+
try {
|
|
81
|
+
const result = await sendWithRetries(storage, document, expectedRevision, signal);
|
|
82
|
+
if (sessionAtStart !== session) return document;
|
|
83
|
+
revision = result?.revision ?? document.revision;
|
|
84
|
+
document.revision = revision;
|
|
85
|
+
savedVersion = versionAtSnapshot;
|
|
86
|
+
lastSavedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
87
|
+
options.onSuccess(document);
|
|
88
|
+
return document;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const saveError = toSaveError(error, signal);
|
|
91
|
+
if (sessionAtStart === session) {
|
|
92
|
+
lastError = saveError;
|
|
93
|
+
options.onError(saveError);
|
|
94
|
+
}
|
|
95
|
+
throw saveError;
|
|
96
|
+
} finally {
|
|
97
|
+
if (sessionAtStart === session) {
|
|
98
|
+
saving = false;
|
|
99
|
+
publish();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function save(saveOptions = {}) {
|
|
104
|
+
if (inFlight === null) {
|
|
105
|
+
inFlight = runSave(saveOptions).finally(() => {
|
|
106
|
+
inFlight = null;
|
|
107
|
+
});
|
|
108
|
+
return inFlight;
|
|
109
|
+
}
|
|
110
|
+
if (queued === null) {
|
|
111
|
+
const sessionWhenQueued = session;
|
|
112
|
+
queued = inFlight.catch(() => void 0).then(() => {
|
|
113
|
+
queued = null;
|
|
114
|
+
if (sessionWhenQueued !== session) throw cancelledError();
|
|
115
|
+
return save(saveOptions);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return queued;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
save,
|
|
122
|
+
noteContentChange() {
|
|
123
|
+
contentVersion += 1;
|
|
124
|
+
autosave?.schedule();
|
|
125
|
+
publish();
|
|
126
|
+
},
|
|
127
|
+
startSession(newRevision) {
|
|
128
|
+
session += 1;
|
|
129
|
+
sessionAbort.abort();
|
|
130
|
+
sessionAbort = new AbortController();
|
|
131
|
+
autosave?.cancel();
|
|
132
|
+
saving = false;
|
|
133
|
+
savedVersion = contentVersion;
|
|
134
|
+
revision = newRevision;
|
|
135
|
+
lastSavedAt = void 0;
|
|
136
|
+
lastError = void 0;
|
|
137
|
+
publish();
|
|
138
|
+
},
|
|
139
|
+
state,
|
|
140
|
+
destroy() {
|
|
141
|
+
session += 1;
|
|
142
|
+
sessionAbort.abort();
|
|
143
|
+
autosave?.cancel();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
exports.createSaveController = createSaveController;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DocumentEngineError } from "../engine/errors.cjs";
|
|
2
|
+
//#region src/save/save-controller.d.ts
|
|
3
|
+
export type SaveStatus = "saved" | "unsaved" | "saving" | "error" | "conflict";
|
|
4
|
+
export interface SaveState {
|
|
5
|
+
status: SaveStatus;
|
|
6
|
+
isDirty: boolean;
|
|
7
|
+
isSaving: boolean;
|
|
8
|
+
revision: number;
|
|
9
|
+
lastSavedAt: string | undefined;
|
|
10
|
+
error: DocumentEngineError | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface SaveOptions {
|
|
13
|
+
overwrite?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface SaveRetryEvent {
|
|
16
|
+
attempt: number;
|
|
17
|
+
delay: number;
|
|
18
|
+
error: unknown;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DocumentEngineError } from "../engine/errors.js";
|
|
2
|
+
//#region src/save/save-controller.d.ts
|
|
3
|
+
export type SaveStatus = "saved" | "unsaved" | "saving" | "error" | "conflict";
|
|
4
|
+
export interface SaveState {
|
|
5
|
+
status: SaveStatus;
|
|
6
|
+
isDirty: boolean;
|
|
7
|
+
isSaving: boolean;
|
|
8
|
+
revision: number;
|
|
9
|
+
lastSavedAt: string | undefined;
|
|
10
|
+
error: DocumentEngineError | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface SaveOptions {
|
|
13
|
+
overwrite?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface SaveRetryEvent {
|
|
16
|
+
attempt: number;
|
|
17
|
+
delay: number;
|
|
18
|
+
error: unknown;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { DocumentEngineError, isDocumentEngineError } from "../engine/errors.js";
|
|
2
|
+
import { createAutosaveScheduler } from "./autosave-scheduler.js";
|
|
3
|
+
import { isRetryable, retryDelay, wait } from "./retry.js";
|
|
4
|
+
//#region src/save/save-controller.ts
|
|
5
|
+
function cancelledError() {
|
|
6
|
+
return new DocumentEngineError("SAVE_CANCELLED", "The save was cancelled because another document was opened");
|
|
7
|
+
}
|
|
8
|
+
function toSaveError(error, signal) {
|
|
9
|
+
if (isDocumentEngineError(error)) return error;
|
|
10
|
+
if (signal.aborted) return cancelledError();
|
|
11
|
+
const hints = error ?? {};
|
|
12
|
+
const reason = typeof hints.message === "string" ? hints.message : String(error);
|
|
13
|
+
if (hints.code === "SAVE_CONFLICT") return new DocumentEngineError("SAVE_CONFLICT", reason, { cause: error });
|
|
14
|
+
return new DocumentEngineError("SAVE_FAILED", `Storage could not save the document: ${reason}`, {
|
|
15
|
+
cause: error,
|
|
16
|
+
retryable: isRetryable(error)
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function createSaveController(options) {
|
|
20
|
+
const attempts = Math.max(0, options.retry?.attempts ?? 3);
|
|
21
|
+
let contentVersion = 0;
|
|
22
|
+
let savedVersion = 0;
|
|
23
|
+
let revision = 0;
|
|
24
|
+
let lastSavedAt;
|
|
25
|
+
let lastError;
|
|
26
|
+
let saving = false;
|
|
27
|
+
let session = 0;
|
|
28
|
+
let sessionAbort = new AbortController();
|
|
29
|
+
let inFlight = null;
|
|
30
|
+
let queued = null;
|
|
31
|
+
const isDirty = () => contentVersion !== savedVersion;
|
|
32
|
+
function state() {
|
|
33
|
+
let status = isDirty() ? "unsaved" : "saved";
|
|
34
|
+
if (lastError) status = lastError.code === "SAVE_CONFLICT" ? "conflict" : "error";
|
|
35
|
+
if (saving) status = "saving";
|
|
36
|
+
return {
|
|
37
|
+
status,
|
|
38
|
+
isDirty: isDirty(),
|
|
39
|
+
isSaving: saving,
|
|
40
|
+
revision,
|
|
41
|
+
lastSavedAt,
|
|
42
|
+
error: lastError
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const publish = () => options.onStateChange(state());
|
|
46
|
+
const autosave = options.autosave === false || options.autosave === void 0 ? null : createAutosaveScheduler(() => {
|
|
47
|
+
if (isDirty()) save().catch(() => void 0);
|
|
48
|
+
}, options.autosave);
|
|
49
|
+
async function sendWithRetries(storage, document, expectedRevision, signal) {
|
|
50
|
+
for (let attempt = 0;; attempt += 1) try {
|
|
51
|
+
return await storage.saveDocument(document, {
|
|
52
|
+
expectedRevision,
|
|
53
|
+
signal
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (signal.aborted || !isRetryable(error) || attempt >= attempts) throw error;
|
|
57
|
+
const delay = retryDelay(attempt, options.retry);
|
|
58
|
+
options.onRetry({
|
|
59
|
+
attempt: attempt + 1,
|
|
60
|
+
delay,
|
|
61
|
+
error
|
|
62
|
+
});
|
|
63
|
+
await wait(delay, signal);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function runSave(saveOptions) {
|
|
67
|
+
const storage = options.getStorage();
|
|
68
|
+
const sessionAtStart = session;
|
|
69
|
+
const { signal } = sessionAbort;
|
|
70
|
+
const versionAtSnapshot = contentVersion;
|
|
71
|
+
const document = {
|
|
72
|
+
...options.createDocument(),
|
|
73
|
+
revision: revision + 1
|
|
74
|
+
};
|
|
75
|
+
const expectedRevision = saveOptions.overwrite === true ? null : revision;
|
|
76
|
+
saving = true;
|
|
77
|
+
lastError = void 0;
|
|
78
|
+
publish();
|
|
79
|
+
options.onStart(document);
|
|
80
|
+
try {
|
|
81
|
+
const result = await sendWithRetries(storage, document, expectedRevision, signal);
|
|
82
|
+
if (sessionAtStart !== session) return document;
|
|
83
|
+
revision = result?.revision ?? document.revision;
|
|
84
|
+
document.revision = revision;
|
|
85
|
+
savedVersion = versionAtSnapshot;
|
|
86
|
+
lastSavedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
87
|
+
options.onSuccess(document);
|
|
88
|
+
return document;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const saveError = toSaveError(error, signal);
|
|
91
|
+
if (sessionAtStart === session) {
|
|
92
|
+
lastError = saveError;
|
|
93
|
+
options.onError(saveError);
|
|
94
|
+
}
|
|
95
|
+
throw saveError;
|
|
96
|
+
} finally {
|
|
97
|
+
if (sessionAtStart === session) {
|
|
98
|
+
saving = false;
|
|
99
|
+
publish();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function save(saveOptions = {}) {
|
|
104
|
+
if (inFlight === null) {
|
|
105
|
+
inFlight = runSave(saveOptions).finally(() => {
|
|
106
|
+
inFlight = null;
|
|
107
|
+
});
|
|
108
|
+
return inFlight;
|
|
109
|
+
}
|
|
110
|
+
if (queued === null) {
|
|
111
|
+
const sessionWhenQueued = session;
|
|
112
|
+
queued = inFlight.catch(() => void 0).then(() => {
|
|
113
|
+
queued = null;
|
|
114
|
+
if (sessionWhenQueued !== session) throw cancelledError();
|
|
115
|
+
return save(saveOptions);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return queued;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
save,
|
|
122
|
+
noteContentChange() {
|
|
123
|
+
contentVersion += 1;
|
|
124
|
+
autosave?.schedule();
|
|
125
|
+
publish();
|
|
126
|
+
},
|
|
127
|
+
startSession(newRevision) {
|
|
128
|
+
session += 1;
|
|
129
|
+
sessionAbort.abort();
|
|
130
|
+
sessionAbort = new AbortController();
|
|
131
|
+
autosave?.cancel();
|
|
132
|
+
saving = false;
|
|
133
|
+
savedVersion = contentVersion;
|
|
134
|
+
revision = newRevision;
|
|
135
|
+
lastSavedAt = void 0;
|
|
136
|
+
lastError = void 0;
|
|
137
|
+
publish();
|
|
138
|
+
},
|
|
139
|
+
state,
|
|
140
|
+
destroy() {
|
|
141
|
+
session += 1;
|
|
142
|
+
sessionAbort.abort();
|
|
143
|
+
autosave?.cancel();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { createSaveController };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/save/unsaved-changes-warning.ts
|
|
2
|
+
function bindUnsavedChangesWarning(engine, target = globalThis.window) {
|
|
3
|
+
const handleBeforeUnload = (event) => {
|
|
4
|
+
if (!engine.isDirty()) return;
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
event.returnValue = "";
|
|
7
|
+
};
|
|
8
|
+
target.addEventListener("beforeunload", handleBeforeUnload);
|
|
9
|
+
return () => target.removeEventListener("beforeunload", handleBeforeUnload);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
exports.bindUnsavedChangesWarning = bindUnsavedChangesWarning;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/save/unsaved-changes-warning.ts
|
|
2
|
+
function bindUnsavedChangesWarning(engine, target = globalThis.window) {
|
|
3
|
+
const handleBeforeUnload = (event) => {
|
|
4
|
+
if (!engine.isDirty()) return;
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
event.returnValue = "";
|
|
7
|
+
};
|
|
8
|
+
target.addEventListener("beforeunload", handleBeforeUnload);
|
|
9
|
+
return () => target.removeEventListener("beforeunload", handleBeforeUnload);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { bindUnsavedChangesWarning };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
//#region src/storage/key-value-storage.ts
|
|
3
|
+
function readRevision(json) {
|
|
4
|
+
if (json === null) return 0;
|
|
5
|
+
const revision = JSON.parse(json).revision;
|
|
6
|
+
return typeof revision === "number" ? revision : 0;
|
|
7
|
+
}
|
|
8
|
+
function createKeyValueStorage(store, prefix) {
|
|
9
|
+
const keyFor = (id) => `${prefix}${id}`;
|
|
10
|
+
return {
|
|
11
|
+
async loadDocument(id) {
|
|
12
|
+
const json = store.read(keyFor(id));
|
|
13
|
+
if (json === null) throw new require_errors.DocumentEngineError("DOCUMENT_NOT_FOUND", `No document is stored with id "${id}"`);
|
|
14
|
+
return JSON.parse(json);
|
|
15
|
+
},
|
|
16
|
+
async saveDocument(document, { expectedRevision }) {
|
|
17
|
+
const storedRevision = readRevision(store.read(keyFor(document.id)));
|
|
18
|
+
if (expectedRevision !== null && storedRevision !== expectedRevision) throw require_errors.createConflictError(document.id, expectedRevision, storedRevision);
|
|
19
|
+
const revision = expectedRevision === null ? storedRevision + 1 : document.revision ?? storedRevision + 1;
|
|
20
|
+
store.write(keyFor(document.id), JSON.stringify({
|
|
21
|
+
...document,
|
|
22
|
+
revision
|
|
23
|
+
}));
|
|
24
|
+
return { revision };
|
|
25
|
+
},
|
|
26
|
+
async listDocuments() {
|
|
27
|
+
return store.keys().filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
|
|
28
|
+
},
|
|
29
|
+
async deleteDocument(id) {
|
|
30
|
+
store.remove(keyFor(id));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
exports.createKeyValueStorage = createKeyValueStorage;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DocumentStorage } from "./storage-contract.cjs";
|
|
2
|
+
//#region src/storage/key-value-storage.d.ts
|
|
3
|
+
export interface KeyValueStore {
|
|
4
|
+
read(key: string): string | null;
|
|
5
|
+
write(key: string, value: string): void;
|
|
6
|
+
remove(key: string): void;
|
|
7
|
+
keys(): string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ManagedDocumentStorage extends DocumentStorage {
|
|
10
|
+
listDocuments(): Promise<string[]>;
|
|
11
|
+
deleteDocument(id: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createKeyValueStorage(store: KeyValueStore, prefix: string): ManagedDocumentStorage;
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DocumentStorage } from "./storage-contract.js";
|
|
2
|
+
//#region src/storage/key-value-storage.d.ts
|
|
3
|
+
export interface KeyValueStore {
|
|
4
|
+
read(key: string): string | null;
|
|
5
|
+
write(key: string, value: string): void;
|
|
6
|
+
remove(key: string): void;
|
|
7
|
+
keys(): string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ManagedDocumentStorage extends DocumentStorage {
|
|
10
|
+
listDocuments(): Promise<string[]>;
|
|
11
|
+
deleteDocument(id: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createKeyValueStorage(store: KeyValueStore, prefix: string): ManagedDocumentStorage;
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DocumentEngineError, createConflictError } from "../engine/errors.js";
|
|
2
|
+
//#region src/storage/key-value-storage.ts
|
|
3
|
+
function readRevision(json) {
|
|
4
|
+
if (json === null) return 0;
|
|
5
|
+
const revision = JSON.parse(json).revision;
|
|
6
|
+
return typeof revision === "number" ? revision : 0;
|
|
7
|
+
}
|
|
8
|
+
function createKeyValueStorage(store, prefix) {
|
|
9
|
+
const keyFor = (id) => `${prefix}${id}`;
|
|
10
|
+
return {
|
|
11
|
+
async loadDocument(id) {
|
|
12
|
+
const json = store.read(keyFor(id));
|
|
13
|
+
if (json === null) throw new DocumentEngineError("DOCUMENT_NOT_FOUND", `No document is stored with id "${id}"`);
|
|
14
|
+
return JSON.parse(json);
|
|
15
|
+
},
|
|
16
|
+
async saveDocument(document, { expectedRevision }) {
|
|
17
|
+
const storedRevision = readRevision(store.read(keyFor(document.id)));
|
|
18
|
+
if (expectedRevision !== null && storedRevision !== expectedRevision) throw createConflictError(document.id, expectedRevision, storedRevision);
|
|
19
|
+
const revision = expectedRevision === null ? storedRevision + 1 : document.revision ?? storedRevision + 1;
|
|
20
|
+
store.write(keyFor(document.id), JSON.stringify({
|
|
21
|
+
...document,
|
|
22
|
+
revision
|
|
23
|
+
}));
|
|
24
|
+
return { revision };
|
|
25
|
+
},
|
|
26
|
+
async listDocuments() {
|
|
27
|
+
return store.keys().filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
|
|
28
|
+
},
|
|
29
|
+
async deleteDocument(id) {
|
|
30
|
+
store.remove(keyFor(id));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { createKeyValueStorage };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
const require_key_value_storage = require("./key-value-storage.cjs");
|
|
3
|
+
//#region src/storage/local-storage.ts
|
|
4
|
+
function writeOrExplain(storage, key, value) {
|
|
5
|
+
try {
|
|
6
|
+
storage.setItem(key, value);
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new require_errors.DocumentEngineError("SAVE_FAILED", "The browser refused to store the document, it may be out of space", {
|
|
9
|
+
cause: error,
|
|
10
|
+
retryable: false
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function createLocalStorage(options = {}) {
|
|
15
|
+
const storage = options.storage ?? globalThis.localStorage;
|
|
16
|
+
return require_key_value_storage.createKeyValueStorage({
|
|
17
|
+
read: (key) => storage.getItem(key),
|
|
18
|
+
write: (key, value) => writeOrExplain(storage, key, value),
|
|
19
|
+
remove: (key) => storage.removeItem(key),
|
|
20
|
+
keys: () => Array.from({ length: storage.length }, (_, index) => storage.key(index) ?? "")
|
|
21
|
+
}, options.prefix ?? "fabric-document:");
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
exports.createLocalStorage = createLocalStorage;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ManagedDocumentStorage } from "./key-value-storage.cjs";
|
|
2
|
+
//#region src/storage/local-storage.d.ts
|
|
3
|
+
export interface LocalStorageOptions {
|
|
4
|
+
prefix?: string;
|
|
5
|
+
storage?: Storage;
|
|
6
|
+
}
|
|
7
|
+
export declare function createLocalStorage(options?: LocalStorageOptions): ManagedDocumentStorage;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ManagedDocumentStorage } from "./key-value-storage.js";
|
|
2
|
+
//#region src/storage/local-storage.d.ts
|
|
3
|
+
export interface LocalStorageOptions {
|
|
4
|
+
prefix?: string;
|
|
5
|
+
storage?: Storage;
|
|
6
|
+
}
|
|
7
|
+
export declare function createLocalStorage(options?: LocalStorageOptions): ManagedDocumentStorage;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DocumentEngineError } from "../engine/errors.js";
|
|
2
|
+
import { createKeyValueStorage } from "./key-value-storage.js";
|
|
3
|
+
//#region src/storage/local-storage.ts
|
|
4
|
+
function writeOrExplain(storage, key, value) {
|
|
5
|
+
try {
|
|
6
|
+
storage.setItem(key, value);
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new DocumentEngineError("SAVE_FAILED", "The browser refused to store the document, it may be out of space", {
|
|
9
|
+
cause: error,
|
|
10
|
+
retryable: false
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function createLocalStorage(options = {}) {
|
|
15
|
+
const storage = options.storage ?? globalThis.localStorage;
|
|
16
|
+
return createKeyValueStorage({
|
|
17
|
+
read: (key) => storage.getItem(key),
|
|
18
|
+
write: (key, value) => writeOrExplain(storage, key, value),
|
|
19
|
+
remove: (key) => storage.removeItem(key),
|
|
20
|
+
keys: () => Array.from({ length: storage.length }, (_, index) => storage.key(index) ?? "")
|
|
21
|
+
}, options.prefix ?? "fabric-document:");
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { createLocalStorage };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const require_key_value_storage = require("./key-value-storage.cjs");
|
|
2
|
+
//#region src/storage/memory-storage.ts
|
|
3
|
+
function createMemoryStorage() {
|
|
4
|
+
const records = /* @__PURE__ */ new Map();
|
|
5
|
+
return require_key_value_storage.createKeyValueStorage({
|
|
6
|
+
read: (key) => records.get(key) ?? null,
|
|
7
|
+
write: (key, value) => {
|
|
8
|
+
records.set(key, value);
|
|
9
|
+
},
|
|
10
|
+
remove: (key) => {
|
|
11
|
+
records.delete(key);
|
|
12
|
+
},
|
|
13
|
+
keys: () => [...records.keys()]
|
|
14
|
+
}, "");
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
exports.createMemoryStorage = createMemoryStorage;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createKeyValueStorage } from "./key-value-storage.js";
|
|
2
|
+
//#region src/storage/memory-storage.ts
|
|
3
|
+
function createMemoryStorage() {
|
|
4
|
+
const records = /* @__PURE__ */ new Map();
|
|
5
|
+
return createKeyValueStorage({
|
|
6
|
+
read: (key) => records.get(key) ?? null,
|
|
7
|
+
write: (key, value) => {
|
|
8
|
+
records.set(key, value);
|
|
9
|
+
},
|
|
10
|
+
remove: (key) => {
|
|
11
|
+
records.delete(key);
|
|
12
|
+
},
|
|
13
|
+
keys: () => [...records.keys()]
|
|
14
|
+
}, "");
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { createMemoryStorage };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FabricDocument } from "../document/document-format.cjs";
|
|
2
|
+
//#region src/storage/storage-contract.d.ts
|
|
3
|
+
export interface SaveContext {
|
|
4
|
+
expectedRevision: number | null;
|
|
5
|
+
signal: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
export interface SaveResult {
|
|
8
|
+
revision?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DocumentStorage {
|
|
11
|
+
loadDocument(id: string): Promise<unknown>;
|
|
12
|
+
saveDocument(document: FabricDocument, context: SaveContext): Promise<void | SaveResult>;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FabricDocument } from "../document/document-format.js";
|
|
2
|
+
//#region src/storage/storage-contract.d.ts
|
|
3
|
+
export interface SaveContext {
|
|
4
|
+
expectedRevision: number | null;
|
|
5
|
+
signal: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
export interface SaveResult {
|
|
8
|
+
revision?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DocumentStorage {
|
|
11
|
+
loadDocument(id: string): Promise<unknown>;
|
|
12
|
+
saveDocument(document: FabricDocument, context: SaveContext): Promise<void | SaveResult>;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
package/dist/storage.cjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_key_value_storage = require("./storage/key-value-storage.cjs");
|
|
3
|
+
const require_memory_storage = require("./storage/memory-storage.cjs");
|
|
4
|
+
const require_local_storage = require("./storage/local-storage.cjs");
|
|
5
|
+
exports.createKeyValueStorage = require_key_value_storage.createKeyValueStorage;
|
|
6
|
+
exports.createLocalStorage = require_local_storage.createLocalStorage;
|
|
7
|
+
exports.createMemoryStorage = require_memory_storage.createMemoryStorage;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.cjs";
|
|
2
|
+
import { KeyValueStore, ManagedDocumentStorage, createKeyValueStorage } from "./storage/key-value-storage.cjs";
|
|
3
|
+
import { createMemoryStorage } from "./storage/memory-storage.cjs";
|
|
4
|
+
import { LocalStorageOptions, createLocalStorage } from "./storage/local-storage.cjs";
|
|
5
|
+
export { type DocumentStorage, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, createKeyValueStorage, createLocalStorage, createMemoryStorage };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.js";
|
|
2
|
+
import { KeyValueStore, ManagedDocumentStorage, createKeyValueStorage } from "./storage/key-value-storage.js";
|
|
3
|
+
import { createMemoryStorage } from "./storage/memory-storage.js";
|
|
4
|
+
import { LocalStorageOptions, createLocalStorage } from "./storage/local-storage.js";
|
|
5
|
+
export { type DocumentStorage, type KeyValueStore, type LocalStorageOptions, type ManagedDocumentStorage, type SaveContext, type SaveResult, createKeyValueStorage, createLocalStorage, createMemoryStorage };
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createKeyValueStorage } from "./storage/key-value-storage.js";
|
|
2
|
+
import { createMemoryStorage } from "./storage/memory-storage.js";
|
|
3
|
+
import { createLocalStorage } from "./storage/local-storage.js";
|
|
4
|
+
export { createKeyValueStorage, createLocalStorage, createMemoryStorage };
|