react-next-editor-js 0.1.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/LICENSE +21 -0
- package/README.md +877 -0
- package/dist/chunk-3QWXTDLY.cjs +486 -0
- package/dist/chunk-3QWXTDLY.cjs.map +1 -0
- package/dist/chunk-5F6SPYCN.cjs +180 -0
- package/dist/chunk-5F6SPYCN.cjs.map +1 -0
- package/dist/chunk-6NTSXJX4.js +174 -0
- package/dist/chunk-6NTSXJX4.js.map +1 -0
- package/dist/chunk-7VYJDBH7.js +261 -0
- package/dist/chunk-7VYJDBH7.js.map +1 -0
- package/dist/chunk-DBSFCCBG.cjs +1712 -0
- package/dist/chunk-DBSFCCBG.cjs.map +1 -0
- package/dist/chunk-EFE6RHDL.cjs +4 -0
- package/dist/chunk-EFE6RHDL.cjs.map +1 -0
- package/dist/chunk-G6YRIEK4.js +3 -0
- package/dist/chunk-G6YRIEK4.js.map +1 -0
- package/dist/chunk-GFNFJ3FL.cjs +119 -0
- package/dist/chunk-GFNFJ3FL.cjs.map +1 -0
- package/dist/chunk-IG2YLUFW.js +114 -0
- package/dist/chunk-IG2YLUFW.js.map +1 -0
- package/dist/chunk-JQXTWLHL.js +176 -0
- package/dist/chunk-JQXTWLHL.js.map +1 -0
- package/dist/chunk-NJCEHQV3.cjs +454 -0
- package/dist/chunk-NJCEHQV3.cjs.map +1 -0
- package/dist/chunk-O4GTLC3T.js +478 -0
- package/dist/chunk-O4GTLC3T.js.map +1 -0
- package/dist/chunk-ODHABIIC.cjs +82 -0
- package/dist/chunk-ODHABIIC.cjs.map +1 -0
- package/dist/chunk-PZ5AY32C.js +9 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-Q7SFCCGT.cjs +11 -0
- package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
- package/dist/chunk-QIUIYBCZ.js +80 -0
- package/dist/chunk-QIUIYBCZ.js.map +1 -0
- package/dist/chunk-QROUNVQK.js +450 -0
- package/dist/chunk-QROUNVQK.js.map +1 -0
- package/dist/chunk-T6FR37IC.js +41 -0
- package/dist/chunk-T6FR37IC.js.map +1 -0
- package/dist/chunk-TI44I654.cjs +265 -0
- package/dist/chunk-TI44I654.cjs.map +1 -0
- package/dist/chunk-TXPLBAH5.cjs +47 -0
- package/dist/chunk-TXPLBAH5.cjs.map +1 -0
- package/dist/chunk-U3O54IYI.cjs +187 -0
- package/dist/chunk-U3O54IYI.cjs.map +1 -0
- package/dist/chunk-VLC7SZMT.js +1669 -0
- package/dist/chunk-VLC7SZMT.js.map +1 -0
- package/dist/core/index.cjs +232 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +122 -0
- package/dist/core/index.d.ts +122 -0
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -0
- package/dist/defaults-EQD5QKCU.js +4 -0
- package/dist/defaults-EQD5QKCU.js.map +1 -0
- package/dist/defaults-MLYXD2BG.cjs +49 -0
- package/dist/defaults-MLYXD2BG.cjs.map +1 -0
- package/dist/docx-BUrf4PFj.d.ts +49 -0
- package/dist/docx-DLfSdvXm.d.cts +49 -0
- package/dist/docx-LDETXV3L.js +5 -0
- package/dist/docx-LDETXV3L.js.map +1 -0
- package/dist/docx-N2LKIOK3.cjs +14 -0
- package/dist/docx-N2LKIOK3.cjs.map +1 -0
- package/dist/export/index.cjs +54 -0
- package/dist/export/index.cjs.map +1 -0
- package/dist/export/index.d.cts +60 -0
- package/dist/export/index.d.ts +60 -0
- package/dist/export/index.js +9 -0
- package/dist/export/index.js.map +1 -0
- package/dist/html-5BXJPQU3.js +7 -0
- package/dist/html-5BXJPQU3.js.map +1 -0
- package/dist/html-KU2KHLRF.cjs +24 -0
- package/dist/html-KU2KHLRF.cjs.map +1 -0
- package/dist/import/index.cjs +15 -0
- package/dist/import/index.cjs.map +1 -0
- package/dist/import/index.d.cts +37 -0
- package/dist/import/index.d.ts +37 -0
- package/dist/import/index.js +6 -0
- package/dist/import/index.js.map +1 -0
- package/dist/index.cjs +1035 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +248 -0
- package/dist/index.d.ts +248 -0
- package/dist/index.js +885 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/index.cjs +37 -0
- package/dist/persistence/index.cjs.map +1 -0
- package/dist/persistence/index.d.cts +279 -0
- package/dist/persistence/index.d.ts +279 -0
- package/dist/persistence/index.js +4 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/sanitize-7IZ-SW1f.d.ts +361 -0
- package/dist/sanitize-CvmgqbsA.d.cts +361 -0
- package/dist/server/index.cjs +400 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +229 -0
- package/dist/server/index.d.ts +229 -0
- package/dist/server/index.js +390 -0
- package/dist/server/index.js.map +1 -0
- package/dist/styles.css +680 -0
- package/dist/types-B4z0Quvv.d.cts +193 -0
- package/dist/types-B4z0Quvv.d.ts +193 -0
- package/package.json +183 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var idb = require('idb');
|
|
4
|
+
|
|
5
|
+
// src/persistence/types.ts
|
|
6
|
+
var ConflictError = class extends Error {
|
|
7
|
+
constructor(message, remote) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.remote = remote;
|
|
10
|
+
this.name = "ConflictError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/persistence/memory.ts
|
|
15
|
+
var MemoryStore = class {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.docs = /* @__PURE__ */ new Map();
|
|
18
|
+
this.outbox = /* @__PURE__ */ new Map();
|
|
19
|
+
this.assets = /* @__PURE__ */ new Map();
|
|
20
|
+
}
|
|
21
|
+
async getDocument(id) {
|
|
22
|
+
return this.docs.get(id) ?? null;
|
|
23
|
+
}
|
|
24
|
+
async putDocument(record) {
|
|
25
|
+
this.docs.set(record.id, structuredCloneSafe(record));
|
|
26
|
+
}
|
|
27
|
+
async deleteDocument(id) {
|
|
28
|
+
this.docs.delete(id);
|
|
29
|
+
}
|
|
30
|
+
async listDocuments() {
|
|
31
|
+
return [...this.docs.values()];
|
|
32
|
+
}
|
|
33
|
+
async enqueue(entry) {
|
|
34
|
+
this.outbox.set(entry.id, { ...entry });
|
|
35
|
+
}
|
|
36
|
+
async dequeue(id) {
|
|
37
|
+
this.outbox.delete(id);
|
|
38
|
+
}
|
|
39
|
+
async listOutbox() {
|
|
40
|
+
return [...this.outbox.values()];
|
|
41
|
+
}
|
|
42
|
+
async putAsset(key, blob) {
|
|
43
|
+
this.assets.set(key, blob);
|
|
44
|
+
}
|
|
45
|
+
async getAsset(key) {
|
|
46
|
+
return this.assets.get(key) ?? null;
|
|
47
|
+
}
|
|
48
|
+
async deleteAsset(key) {
|
|
49
|
+
this.assets.delete(key);
|
|
50
|
+
}
|
|
51
|
+
async clear() {
|
|
52
|
+
this.docs.clear();
|
|
53
|
+
this.outbox.clear();
|
|
54
|
+
this.assets.clear();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function structuredCloneSafe(value) {
|
|
58
|
+
if (typeof structuredClone === "function") {
|
|
59
|
+
try {
|
|
60
|
+
return structuredClone(value);
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(JSON.stringify(value));
|
|
65
|
+
}
|
|
66
|
+
var DB_VERSION = 1;
|
|
67
|
+
var STORE_DOCS = "documents";
|
|
68
|
+
var STORE_OUTBOX = "outbox";
|
|
69
|
+
var STORE_ASSETS = "assets";
|
|
70
|
+
var IndexedDBStore = class _IndexedDBStore {
|
|
71
|
+
constructor(dbName = "react-next-editor") {
|
|
72
|
+
this.dbPromise = null;
|
|
73
|
+
this.fallback = null;
|
|
74
|
+
this.dbName = dbName;
|
|
75
|
+
}
|
|
76
|
+
/** Whether IndexedDB is usable in the current environment. */
|
|
77
|
+
static isSupported() {
|
|
78
|
+
return typeof indexedDB !== "undefined";
|
|
79
|
+
}
|
|
80
|
+
async db() {
|
|
81
|
+
if (!_IndexedDBStore.isSupported()) {
|
|
82
|
+
if (!this.fallback) this.fallback = new MemoryStore();
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (!this.dbPromise) {
|
|
86
|
+
this.dbPromise = idb.openDB(this.dbName, DB_VERSION, {
|
|
87
|
+
upgrade(db) {
|
|
88
|
+
if (!db.objectStoreNames.contains(STORE_DOCS)) {
|
|
89
|
+
db.createObjectStore(STORE_DOCS, { keyPath: "id" });
|
|
90
|
+
}
|
|
91
|
+
if (!db.objectStoreNames.contains(STORE_OUTBOX)) {
|
|
92
|
+
db.createObjectStore(STORE_OUTBOX, { keyPath: "id" });
|
|
93
|
+
}
|
|
94
|
+
if (!db.objectStoreNames.contains(STORE_ASSETS)) {
|
|
95
|
+
db.createObjectStore(STORE_ASSETS);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}).catch((err) => {
|
|
99
|
+
console.error("[react-next-editor] IndexedDB unavailable, using memory store.", err);
|
|
100
|
+
this.fallback = new MemoryStore();
|
|
101
|
+
throw err;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return await this.dbPromise;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async getDocument(id) {
|
|
111
|
+
const db = await this.db();
|
|
112
|
+
if (!db) return this.fallback.getDocument(id);
|
|
113
|
+
return await db.get(STORE_DOCS, id) ?? null;
|
|
114
|
+
}
|
|
115
|
+
async putDocument(record) {
|
|
116
|
+
const db = await this.db();
|
|
117
|
+
if (!db) return this.fallback.putDocument(record);
|
|
118
|
+
await db.put(STORE_DOCS, record);
|
|
119
|
+
}
|
|
120
|
+
async deleteDocument(id) {
|
|
121
|
+
const db = await this.db();
|
|
122
|
+
if (!db) return this.fallback.deleteDocument(id);
|
|
123
|
+
await db.delete(STORE_DOCS, id);
|
|
124
|
+
}
|
|
125
|
+
async listDocuments() {
|
|
126
|
+
const db = await this.db();
|
|
127
|
+
if (!db) return this.fallback.listDocuments();
|
|
128
|
+
return db.getAll(STORE_DOCS);
|
|
129
|
+
}
|
|
130
|
+
async enqueue(entry) {
|
|
131
|
+
const db = await this.db();
|
|
132
|
+
if (!db) return this.fallback.enqueue(entry);
|
|
133
|
+
await db.put(STORE_OUTBOX, entry);
|
|
134
|
+
}
|
|
135
|
+
async dequeue(id) {
|
|
136
|
+
const db = await this.db();
|
|
137
|
+
if (!db) return this.fallback.dequeue(id);
|
|
138
|
+
await db.delete(STORE_OUTBOX, id);
|
|
139
|
+
}
|
|
140
|
+
async listOutbox() {
|
|
141
|
+
const db = await this.db();
|
|
142
|
+
if (!db) return this.fallback.listOutbox();
|
|
143
|
+
return db.getAll(STORE_OUTBOX);
|
|
144
|
+
}
|
|
145
|
+
async putAsset(key, blob) {
|
|
146
|
+
const db = await this.db();
|
|
147
|
+
if (!db) return this.fallback.putAsset(key, blob);
|
|
148
|
+
await db.put(STORE_ASSETS, blob, key);
|
|
149
|
+
}
|
|
150
|
+
async getAsset(key) {
|
|
151
|
+
const db = await this.db();
|
|
152
|
+
if (!db) return this.fallback.getAsset(key);
|
|
153
|
+
return await db.get(STORE_ASSETS, key) ?? null;
|
|
154
|
+
}
|
|
155
|
+
async deleteAsset(key) {
|
|
156
|
+
const db = await this.db();
|
|
157
|
+
if (!db) return this.fallback.deleteAsset(key);
|
|
158
|
+
await db.delete(STORE_ASSETS, key);
|
|
159
|
+
}
|
|
160
|
+
async clear() {
|
|
161
|
+
const db = await this.db();
|
|
162
|
+
if (!db) return this.fallback.clear();
|
|
163
|
+
await Promise.all([
|
|
164
|
+
db.clear(STORE_DOCS),
|
|
165
|
+
db.clear(STORE_OUTBOX),
|
|
166
|
+
db.clear(STORE_ASSETS)
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
async function requestPersistentStorage() {
|
|
171
|
+
try {
|
|
172
|
+
if (typeof navigator !== "undefined" && navigator.storage?.persist) {
|
|
173
|
+
return await navigator.storage.persist();
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/persistence/autosave.ts
|
|
181
|
+
var DocumentPersistence = class {
|
|
182
|
+
constructor(options) {
|
|
183
|
+
this.current = null;
|
|
184
|
+
this.timer = null;
|
|
185
|
+
this.pending = null;
|
|
186
|
+
this.destroyed = false;
|
|
187
|
+
this.writing = false;
|
|
188
|
+
this.id = options.documentId;
|
|
189
|
+
this.store = options.store;
|
|
190
|
+
this.debounceMs = options.debounceMs ?? 800;
|
|
191
|
+
this.onStatus = options.onStatus;
|
|
192
|
+
this.metadata = options.metadata;
|
|
193
|
+
}
|
|
194
|
+
emit(status, detail) {
|
|
195
|
+
this.onStatus?.(status, detail);
|
|
196
|
+
}
|
|
197
|
+
/** Load the latest locally-persisted document (crash/reload recovery, F-11.9). */
|
|
198
|
+
async load() {
|
|
199
|
+
const record = await this.store.getDocument(this.id);
|
|
200
|
+
this.current = record;
|
|
201
|
+
return record;
|
|
202
|
+
}
|
|
203
|
+
/** The current stored record, if loaded/saved. */
|
|
204
|
+
getRecord() {
|
|
205
|
+
return this.current;
|
|
206
|
+
}
|
|
207
|
+
/** Whether there are unsynced local changes. */
|
|
208
|
+
isDirty() {
|
|
209
|
+
return this.current?.dirty ?? false;
|
|
210
|
+
}
|
|
211
|
+
/** Schedule a debounced save of the latest document JSON (F-4.8). */
|
|
212
|
+
scheduleSave(doc) {
|
|
213
|
+
if (this.destroyed) return;
|
|
214
|
+
this.pending = doc;
|
|
215
|
+
this.emit("savingLocal");
|
|
216
|
+
if (this.timer) clearTimeout(this.timer);
|
|
217
|
+
this.timer = setTimeout(() => {
|
|
218
|
+
void this.flush();
|
|
219
|
+
}, this.debounceMs);
|
|
220
|
+
}
|
|
221
|
+
/** Immediately persist any pending document (e.g. on blur/unmount). */
|
|
222
|
+
async flush() {
|
|
223
|
+
if (this.timer) {
|
|
224
|
+
clearTimeout(this.timer);
|
|
225
|
+
this.timer = null;
|
|
226
|
+
}
|
|
227
|
+
if (this.pending == null) return;
|
|
228
|
+
const doc = this.pending;
|
|
229
|
+
this.pending = null;
|
|
230
|
+
await this.saveNow(doc);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Persist a document to the local store, atomically bump the revision, mark it
|
|
234
|
+
* dirty and enqueue it in the outbox for later upload. Writes are serialized to
|
|
235
|
+
* avoid partial saves (NF-9).
|
|
236
|
+
*/
|
|
237
|
+
async saveNow(doc) {
|
|
238
|
+
if (this.writing) {
|
|
239
|
+
this.pending = doc;
|
|
240
|
+
return this.current ?? this.makeRecord(doc);
|
|
241
|
+
}
|
|
242
|
+
this.writing = true;
|
|
243
|
+
try {
|
|
244
|
+
const record = this.makeRecord(doc);
|
|
245
|
+
await this.store.putDocument(record);
|
|
246
|
+
await this.store.enqueue({
|
|
247
|
+
id: this.id,
|
|
248
|
+
rev: record.rev,
|
|
249
|
+
queuedAt: record.updatedAt,
|
|
250
|
+
attempts: 0
|
|
251
|
+
});
|
|
252
|
+
this.current = record;
|
|
253
|
+
this.emit("savedLocal");
|
|
254
|
+
return record;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
this.emit("syncFailed", { error: err?.message });
|
|
257
|
+
throw err;
|
|
258
|
+
} finally {
|
|
259
|
+
this.writing = false;
|
|
260
|
+
if (this.pending != null) {
|
|
261
|
+
const next = this.pending;
|
|
262
|
+
this.pending = null;
|
|
263
|
+
await this.saveNow(next);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
makeRecord(doc) {
|
|
268
|
+
const prev = this.current;
|
|
269
|
+
return {
|
|
270
|
+
id: this.id,
|
|
271
|
+
doc,
|
|
272
|
+
rev: (prev?.rev ?? 0) + 1,
|
|
273
|
+
baseVersion: prev?.baseVersion ?? null,
|
|
274
|
+
dirty: true,
|
|
275
|
+
updatedAt: Date.now(),
|
|
276
|
+
metadata: this.metadata ?? prev?.metadata
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** Mark the document as synced after a successful remote save (NF-10). */
|
|
280
|
+
async markSynced(version, serverDoc) {
|
|
281
|
+
if (!this.current) return;
|
|
282
|
+
const record = {
|
|
283
|
+
...this.current,
|
|
284
|
+
doc: serverDoc ?? this.current.doc,
|
|
285
|
+
baseVersion: version,
|
|
286
|
+
dirty: false
|
|
287
|
+
};
|
|
288
|
+
await this.store.putDocument(record);
|
|
289
|
+
await this.store.dequeue(this.id);
|
|
290
|
+
this.current = record;
|
|
291
|
+
this.emit("synced");
|
|
292
|
+
}
|
|
293
|
+
/** Purge this document's locally-persisted data and outbox entry (F-12.7). */
|
|
294
|
+
async clearLocal() {
|
|
295
|
+
await this.store.deleteDocument(this.id);
|
|
296
|
+
await this.store.dequeue(this.id);
|
|
297
|
+
this.current = null;
|
|
298
|
+
}
|
|
299
|
+
/** Stop the autosave timer and flush pending work. */
|
|
300
|
+
async destroy() {
|
|
301
|
+
this.destroyed = true;
|
|
302
|
+
await this.flush();
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/sync/connectivity.ts
|
|
307
|
+
var ConnectivityMonitor = class {
|
|
308
|
+
constructor(options = {}) {
|
|
309
|
+
this.timer = null;
|
|
310
|
+
this.started = false;
|
|
311
|
+
this.handleOnline = () => void this.check();
|
|
312
|
+
this.handleOffline = () => this.set(false);
|
|
313
|
+
this.ping = options.ping;
|
|
314
|
+
this.intervalMs = options.intervalMs ?? 3e4;
|
|
315
|
+
this.onChange = options.onChange;
|
|
316
|
+
this.online = typeof navigator !== "undefined" ? navigator.onLine : true;
|
|
317
|
+
}
|
|
318
|
+
isOnline() {
|
|
319
|
+
return this.online;
|
|
320
|
+
}
|
|
321
|
+
start() {
|
|
322
|
+
if (this.started || typeof window === "undefined") return;
|
|
323
|
+
this.started = true;
|
|
324
|
+
window.addEventListener("online", this.handleOnline);
|
|
325
|
+
window.addEventListener("offline", this.handleOffline);
|
|
326
|
+
if (this.intervalMs > 0) {
|
|
327
|
+
this.timer = setInterval(() => void this.check(), this.intervalMs);
|
|
328
|
+
}
|
|
329
|
+
void this.check();
|
|
330
|
+
}
|
|
331
|
+
stop() {
|
|
332
|
+
if (!this.started || typeof window === "undefined") return;
|
|
333
|
+
this.started = false;
|
|
334
|
+
window.removeEventListener("online", this.handleOnline);
|
|
335
|
+
window.removeEventListener("offline", this.handleOffline);
|
|
336
|
+
if (this.timer) {
|
|
337
|
+
clearInterval(this.timer);
|
|
338
|
+
this.timer = null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/** Re-evaluate connectivity now, confirming reachability via ping when set. */
|
|
342
|
+
async check() {
|
|
343
|
+
const navOnline = typeof navigator !== "undefined" ? navigator.onLine : true;
|
|
344
|
+
if (!navOnline) {
|
|
345
|
+
this.set(false);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (!this.ping) {
|
|
349
|
+
this.set(true);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const reachable = await this.ping();
|
|
354
|
+
this.set(reachable);
|
|
355
|
+
return reachable;
|
|
356
|
+
} catch {
|
|
357
|
+
this.set(false);
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
set(online) {
|
|
362
|
+
if (online !== this.online) {
|
|
363
|
+
this.online = online;
|
|
364
|
+
this.onChange?.(online);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/sync/engine.ts
|
|
370
|
+
var MAX_BACKOFF_MS = 5 * 6e4;
|
|
371
|
+
var SyncEngine = class {
|
|
372
|
+
constructor(options) {
|
|
373
|
+
this.flushing = false;
|
|
374
|
+
this.abortController = null;
|
|
375
|
+
this.store = options.store;
|
|
376
|
+
this.remote = options.remote;
|
|
377
|
+
this.maxAttempts = options.maxAttempts ?? 6;
|
|
378
|
+
this.baseDelayMs = options.baseDelayMs ?? 1e3;
|
|
379
|
+
this.onStatus = options.onStatus;
|
|
380
|
+
this.onConflict = options.onConflict;
|
|
381
|
+
}
|
|
382
|
+
emit(status, detail) {
|
|
383
|
+
this.onStatus?.(status, detail);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Process every queued document once. Re-entrancy-safe: concurrent calls are
|
|
387
|
+
* coalesced. Returns the number of documents successfully synced.
|
|
388
|
+
*/
|
|
389
|
+
async flush() {
|
|
390
|
+
if (this.flushing) return 0;
|
|
391
|
+
this.flushing = true;
|
|
392
|
+
this.abortController = new AbortController();
|
|
393
|
+
const signal = this.abortController.signal;
|
|
394
|
+
let synced = 0;
|
|
395
|
+
try {
|
|
396
|
+
const entries = await this.store.listOutbox();
|
|
397
|
+
if (entries.length === 0) return 0;
|
|
398
|
+
this.emit("syncing");
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
for (const entry of entries) {
|
|
401
|
+
if (signal.aborted) break;
|
|
402
|
+
if (entry.nextAttemptAt && entry.nextAttemptAt > now) continue;
|
|
403
|
+
const record = await this.store.getDocument(entry.id);
|
|
404
|
+
if (!record || !record.dirty || record.rev !== entry.rev) {
|
|
405
|
+
await this.store.dequeue(entry.id);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const result = await this.remote.save(record, signal);
|
|
410
|
+
const latest = await this.store.getDocument(entry.id);
|
|
411
|
+
if (latest && latest.rev === record.rev) {
|
|
412
|
+
await this.store.putDocument({
|
|
413
|
+
...latest,
|
|
414
|
+
doc: result.doc ?? latest.doc,
|
|
415
|
+
baseVersion: result.version,
|
|
416
|
+
dirty: false
|
|
417
|
+
});
|
|
418
|
+
await this.store.dequeue(entry.id);
|
|
419
|
+
} else {
|
|
420
|
+
await this.store.dequeue(entry.id);
|
|
421
|
+
}
|
|
422
|
+
synced++;
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (err instanceof ConflictError) {
|
|
425
|
+
await this.store.enqueue({
|
|
426
|
+
...entry,
|
|
427
|
+
attempts: entry.attempts + 1,
|
|
428
|
+
lastError: "conflict",
|
|
429
|
+
nextAttemptAt: Number.MAX_SAFE_INTEGER
|
|
430
|
+
// park until resolved
|
|
431
|
+
});
|
|
432
|
+
this.onConflict?.(record, err.remote);
|
|
433
|
+
this.emit("syncFailed", { error: "conflict" });
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const attempts = entry.attempts + 1;
|
|
437
|
+
const backoff = Math.min(MAX_BACKOFF_MS, this.baseDelayMs * 2 ** entry.attempts);
|
|
438
|
+
await this.store.enqueue({
|
|
439
|
+
...entry,
|
|
440
|
+
attempts,
|
|
441
|
+
lastError: err?.message ?? "upload failed",
|
|
442
|
+
nextAttemptAt: attempts >= this.maxAttempts ? Number.MAX_SAFE_INTEGER : Date.now() + backoff
|
|
443
|
+
});
|
|
444
|
+
this.emit("syncFailed", { error: err?.message });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const remaining = await this.store.listOutbox();
|
|
448
|
+
this.emit(remaining.length === 0 ? "synced" : "savedLocal");
|
|
449
|
+
return synced;
|
|
450
|
+
} finally {
|
|
451
|
+
this.flushing = false;
|
|
452
|
+
this.abortController = null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/** Abort an in-flight flush (e.g. on going offline or unmount). */
|
|
456
|
+
cancel() {
|
|
457
|
+
this.abortController?.abort();
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Re-queue a parked/conflicted document for another attempt (used by a
|
|
461
|
+
* host-defined conflict resolution flow after the user chooses to overwrite).
|
|
462
|
+
*/
|
|
463
|
+
async retry(id, baseVersion) {
|
|
464
|
+
const record = await this.store.getDocument(id);
|
|
465
|
+
if (!record) return;
|
|
466
|
+
if (baseVersion !== void 0) {
|
|
467
|
+
await this.store.putDocument({ ...record, baseVersion });
|
|
468
|
+
}
|
|
469
|
+
await this.store.enqueue({
|
|
470
|
+
id,
|
|
471
|
+
rev: record.rev,
|
|
472
|
+
queuedAt: Date.now(),
|
|
473
|
+
attempts: 0
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
exports.ConflictError = ConflictError;
|
|
479
|
+
exports.ConnectivityMonitor = ConnectivityMonitor;
|
|
480
|
+
exports.DocumentPersistence = DocumentPersistence;
|
|
481
|
+
exports.IndexedDBStore = IndexedDBStore;
|
|
482
|
+
exports.MemoryStore = MemoryStore;
|
|
483
|
+
exports.SyncEngine = SyncEngine;
|
|
484
|
+
exports.requestPersistentStorage = requestPersistentStorage;
|
|
485
|
+
//# sourceMappingURL=chunk-3QWXTDLY.cjs.map
|
|
486
|
+
//# sourceMappingURL=chunk-3QWXTDLY.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/persistence/types.ts","../src/persistence/memory.ts","../src/persistence/indexeddb.ts","../src/persistence/autosave.ts","../src/sync/connectivity.ts","../src/sync/engine.ts"],"names":["openDB"],"mappings":"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","file":"chunk-3QWXTDLY.cjs","sourcesContent":["import type { DocumentJSON, SaveStatus } from '../config/types';\n\n/**\n * Persistence and sync adapter interfaces (F-10.12). The editor core knows\n * nothing about REST or IndexedDB; consumers inject implementations of these\n * interfaces so the same editor works against any backend/storage. Default\n * IndexedDB and in-memory implementations ship with the package.\n */\n\n/** A stored document record: canonical JSON plus sync metadata. */\nexport interface StoredDocument {\n id: string;\n /** Canonical ProseMirror document JSON (F-8.1, C-5). */\n doc: DocumentJSON;\n /** Monotonic local revision, bumped on every local save. */\n rev: number;\n /** Server-acknowledged version, for optimistic-concurrency conflict checks. */\n baseVersion?: string | number | null;\n /** Whether the record has unsynced local changes. */\n dirty: boolean;\n updatedAt: number;\n /** Arbitrary per-document metadata supplied by the host. */\n metadata?: Record<string, unknown>;\n}\n\n/** An entry in the durable outbox of documents awaiting upload (F-9.5). */\nexport interface OutboxEntry {\n id: string;\n rev: number;\n queuedAt: number;\n attempts: number;\n lastError?: string | null;\n nextAttemptAt?: number;\n}\n\n/**\n * Local, durable document store (F-9.2, NF-9). The default implementation uses\n * IndexedDB; an in-memory implementation is provided for SSR/tests.\n */\nexport interface LocalStoreAdapter {\n getDocument(id: string): Promise<StoredDocument | null>;\n putDocument(record: StoredDocument): Promise<void>;\n deleteDocument(id: string): Promise<void>;\n listDocuments(): Promise<StoredDocument[]>;\n\n /** Outbox operations for the sync engine. */\n enqueue(entry: OutboxEntry): Promise<void>;\n dequeue(id: string): Promise<void>;\n listOutbox(): Promise<OutboxEntry[]>;\n\n /** Binary asset (offline image) operations (F-9.10). */\n putAsset?(key: string, blob: Blob): Promise<void>;\n getAsset?(key: string): Promise<Blob | null>;\n deleteAsset?(key: string): Promise<void>;\n\n /** Purge all locally persisted data (F-12.7). */\n clear(): Promise<void>;\n}\n\n/** Result of a remote save, carrying the new server version (NF-10). */\nexport interface RemoteSaveResult {\n version: string | number;\n /** Optional canonical document returned by the server (e.g. rewritten asset URLs). */\n doc?: DocumentJSON;\n}\n\n/** Raised by a {@link RemoteSyncAdapter} when a stale write is rejected (F-9.9). */\nexport class ConflictError extends Error {\n constructor(\n message: string,\n public readonly remote?: { version: string | number; doc?: DocumentJSON },\n ) {\n super(message);\n this.name = 'ConflictError';\n }\n}\n\n/**\n * Remote document API (F-9.6, F-9.7). Injected by the host using its own auth\n * (F-12.4). Implementations MUST use HTTPS (F-12.3) and SHOULD be idempotent\n * (NF-10). On a version conflict, throw {@link ConflictError}.\n */\nexport interface RemoteSyncAdapter {\n /** Create or update the document on the server; returns the new version. */\n save(record: StoredDocument, signal?: AbortSignal): Promise<RemoteSaveResult>;\n /** Fetch the latest server copy, or null if it does not exist. */\n fetch?(id: string, signal?: AbortSignal): Promise<StoredDocument | null>;\n /** Best-effort reachability check used for connectivity confirmation (§8.7). */\n ping?(signal?: AbortSignal): Promise<boolean>;\n}\n\n/** Uploads offline-inserted assets and returns canonical URLs (F-9.10, F-12.5). */\nexport interface AssetUploadAdapter {\n upload(blob: Blob, meta: { filename?: string; mime: string }): Promise<{ url: string }>;\n}\n\n/** Listener for save/sync status transitions (F-9.4, F-10.15). */\nexport type SaveStatusListener = (status: SaveStatus, detail?: { error?: string }) => void;\n","import type { LocalStoreAdapter, OutboxEntry, StoredDocument } from './types';\n\n/**\n * In-memory {@link LocalStoreAdapter}. Used as an SSR-safe fallback when\n * IndexedDB is unavailable and as a test double. Not durable across reloads.\n */\nexport class MemoryStore implements LocalStoreAdapter {\n private docs = new Map<string, StoredDocument>();\n private outbox = new Map<string, OutboxEntry>();\n private assets = new Map<string, Blob>();\n\n async getDocument(id: string): Promise<StoredDocument | null> {\n return this.docs.get(id) ?? null;\n }\n\n async putDocument(record: StoredDocument): Promise<void> {\n this.docs.set(record.id, structuredCloneSafe(record));\n }\n\n async deleteDocument(id: string): Promise<void> {\n this.docs.delete(id);\n }\n\n async listDocuments(): Promise<StoredDocument[]> {\n return [...this.docs.values()];\n }\n\n async enqueue(entry: OutboxEntry): Promise<void> {\n this.outbox.set(entry.id, { ...entry });\n }\n\n async dequeue(id: string): Promise<void> {\n this.outbox.delete(id);\n }\n\n async listOutbox(): Promise<OutboxEntry[]> {\n return [...this.outbox.values()];\n }\n\n async putAsset(key: string, blob: Blob): Promise<void> {\n this.assets.set(key, blob);\n }\n\n async getAsset(key: string): Promise<Blob | null> {\n return this.assets.get(key) ?? null;\n }\n\n async deleteAsset(key: string): Promise<void> {\n this.assets.delete(key);\n }\n\n async clear(): Promise<void> {\n this.docs.clear();\n this.outbox.clear();\n this.assets.clear();\n }\n}\n\nfunction structuredCloneSafe<T>(value: T): T {\n if (typeof structuredClone === 'function') {\n try {\n return structuredClone(value);\n } catch {\n /* fall through */\n }\n }\n return JSON.parse(JSON.stringify(value)) as T;\n}\n","import { type IDBPDatabase, openDB } from 'idb';\nimport type { LocalStoreAdapter, OutboxEntry, StoredDocument } from './types';\nimport { MemoryStore } from './memory';\n\nconst DB_VERSION = 1;\nconst STORE_DOCS = 'documents';\nconst STORE_OUTBOX = 'outbox';\nconst STORE_ASSETS = 'assets';\n\n/**\n * Durable {@link LocalStoreAdapter} backed by IndexedDB (§8.7). `localStorage`\n * is explicitly rejected (too small, synchronous, string-only). Falls back to an\n * in-memory store when IndexedDB is unavailable (SSR, private mode) so the\n * editor never crashes (F-11.1).\n */\nexport class IndexedDBStore implements LocalStoreAdapter {\n private dbName: string;\n private dbPromise: Promise<IDBPDatabase> | null = null;\n private fallback: MemoryStore | null = null;\n\n constructor(dbName = 'react-next-editor') {\n this.dbName = dbName;\n }\n\n /** Whether IndexedDB is usable in the current environment. */\n static isSupported(): boolean {\n return typeof indexedDB !== 'undefined';\n }\n\n private async db(): Promise<IDBPDatabase | null> {\n if (!IndexedDBStore.isSupported()) {\n if (!this.fallback) this.fallback = new MemoryStore();\n return null;\n }\n if (!this.dbPromise) {\n this.dbPromise = openDB(this.dbName, DB_VERSION, {\n upgrade(db) {\n if (!db.objectStoreNames.contains(STORE_DOCS)) {\n db.createObjectStore(STORE_DOCS, { keyPath: 'id' });\n }\n if (!db.objectStoreNames.contains(STORE_OUTBOX)) {\n db.createObjectStore(STORE_OUTBOX, { keyPath: 'id' });\n }\n if (!db.objectStoreNames.contains(STORE_ASSETS)) {\n db.createObjectStore(STORE_ASSETS);\n }\n },\n }).catch((err) => {\n // eslint-disable-next-line no-console\n console.error('[react-next-editor] IndexedDB unavailable, using memory store.', err);\n this.fallback = new MemoryStore();\n throw err;\n });\n }\n try {\n return await this.dbPromise;\n } catch {\n return null;\n }\n }\n\n async getDocument(id: string): Promise<StoredDocument | null> {\n const db = await this.db();\n if (!db) return this.fallback!.getDocument(id);\n return (await db.get(STORE_DOCS, id)) ?? null;\n }\n\n async putDocument(record: StoredDocument): Promise<void> {\n const db = await this.db();\n if (!db) return this.fallback!.putDocument(record);\n await db.put(STORE_DOCS, record);\n }\n\n async deleteDocument(id: string): Promise<void> {\n const db = await this.db();\n if (!db) return this.fallback!.deleteDocument(id);\n await db.delete(STORE_DOCS, id);\n }\n\n async listDocuments(): Promise<StoredDocument[]> {\n const db = await this.db();\n if (!db) return this.fallback!.listDocuments();\n return db.getAll(STORE_DOCS);\n }\n\n async enqueue(entry: OutboxEntry): Promise<void> {\n const db = await this.db();\n if (!db) return this.fallback!.enqueue(entry);\n await db.put(STORE_OUTBOX, entry);\n }\n\n async dequeue(id: string): Promise<void> {\n const db = await this.db();\n if (!db) return this.fallback!.dequeue(id);\n await db.delete(STORE_OUTBOX, id);\n }\n\n async listOutbox(): Promise<OutboxEntry[]> {\n const db = await this.db();\n if (!db) return this.fallback!.listOutbox();\n return db.getAll(STORE_OUTBOX);\n }\n\n async putAsset(key: string, blob: Blob): Promise<void> {\n const db = await this.db();\n if (!db) return this.fallback!.putAsset(key, blob);\n await db.put(STORE_ASSETS, blob, key);\n }\n\n async getAsset(key: string): Promise<Blob | null> {\n const db = await this.db();\n if (!db) return this.fallback!.getAsset(key);\n return (await db.get(STORE_ASSETS, key)) ?? null;\n }\n\n async deleteAsset(key: string): Promise<void> {\n const db = await this.db();\n if (!db) return this.fallback!.deleteAsset(key);\n await db.delete(STORE_ASSETS, key);\n }\n\n async clear(): Promise<void> {\n const db = await this.db();\n if (!db) return this.fallback!.clear();\n await Promise.all([\n db.clear(STORE_DOCS),\n db.clear(STORE_OUTBOX),\n db.clear(STORE_ASSETS),\n ]);\n }\n}\n\n/**\n * Request persistent storage to reduce eviction risk for unsynced data (F-9.11).\n * Resolves to whether persistence was granted; never throws.\n */\nexport async function requestPersistentStorage(): Promise<boolean> {\n try {\n if (typeof navigator !== 'undefined' && navigator.storage?.persist) {\n return await navigator.storage.persist();\n }\n } catch {\n /* ignore */\n }\n return false;\n}\n","import type { DocumentJSON, SaveStatus } from '../config/types';\nimport type { LocalStoreAdapter, SaveStatusListener, StoredDocument } from './types';\n\nexport interface DocumentPersistenceOptions {\n documentId: string;\n store: LocalStoreAdapter;\n /** Debounce window for autosave writes (default 800ms). */\n debounceMs?: number;\n /** Initial metadata to attach to the stored record. */\n metadata?: Record<string, unknown>;\n onStatus?: SaveStatusListener;\n}\n\n/**\n * Manages local-first persistence for a single document (F-8.x, F-9.2, NF-9):\n * debounced autosave of `doc.toJSON()` to the durable store, dirty-flag and\n * outbox maintenance for later sync, and crash/reload recovery via {@link load}.\n * The local store is the source of truth during editing; the network is never in\n * the critical path (C-7, NF-8).\n */\nexport class DocumentPersistence {\n private readonly id: string;\n private readonly store: LocalStoreAdapter;\n private readonly debounceMs: number;\n private readonly onStatus?: SaveStatusListener;\n private metadata?: Record<string, unknown>;\n\n private current: StoredDocument | null = null;\n private timer: ReturnType<typeof setTimeout> | null = null;\n private pending: DocumentJSON | null = null;\n private destroyed = false;\n private writing = false;\n\n constructor(options: DocumentPersistenceOptions) {\n this.id = options.documentId;\n this.store = options.store;\n this.debounceMs = options.debounceMs ?? 800;\n this.onStatus = options.onStatus;\n this.metadata = options.metadata;\n }\n\n private emit(status: SaveStatus, detail?: { error?: string }): void {\n this.onStatus?.(status, detail);\n }\n\n /** Load the latest locally-persisted document (crash/reload recovery, F-11.9). */\n async load(): Promise<StoredDocument | null> {\n const record = await this.store.getDocument(this.id);\n this.current = record;\n return record;\n }\n\n /** The current stored record, if loaded/saved. */\n getRecord(): StoredDocument | null {\n return this.current;\n }\n\n /** Whether there are unsynced local changes. */\n isDirty(): boolean {\n return this.current?.dirty ?? false;\n }\n\n /** Schedule a debounced save of the latest document JSON (F-4.8). */\n scheduleSave(doc: DocumentJSON): void {\n if (this.destroyed) return;\n this.pending = doc;\n this.emit('savingLocal');\n if (this.timer) clearTimeout(this.timer);\n this.timer = setTimeout(() => {\n void this.flush();\n }, this.debounceMs);\n }\n\n /** Immediately persist any pending document (e.g. on blur/unmount). */\n async flush(): Promise<void> {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n if (this.pending == null) return;\n const doc = this.pending;\n this.pending = null;\n await this.saveNow(doc);\n }\n\n /**\n * Persist a document to the local store, atomically bump the revision, mark it\n * dirty and enqueue it in the outbox for later upload. Writes are serialized to\n * avoid partial saves (NF-9).\n */\n async saveNow(doc: DocumentJSON): Promise<StoredDocument> {\n if (this.writing) {\n // Coalesce concurrent writes: keep the latest as pending and return current.\n this.pending = doc;\n return this.current ?? this.makeRecord(doc);\n }\n this.writing = true;\n try {\n const record = this.makeRecord(doc);\n await this.store.putDocument(record);\n await this.store.enqueue({\n id: this.id,\n rev: record.rev,\n queuedAt: record.updatedAt,\n attempts: 0,\n });\n this.current = record;\n this.emit('savedLocal');\n return record;\n } catch (err) {\n this.emit('syncFailed', { error: (err as Error)?.message });\n throw err;\n } finally {\n this.writing = false;\n if (this.pending != null) {\n const next = this.pending;\n this.pending = null;\n await this.saveNow(next);\n }\n }\n }\n\n private makeRecord(doc: DocumentJSON): StoredDocument {\n const prev = this.current;\n return {\n id: this.id,\n doc,\n rev: (prev?.rev ?? 0) + 1,\n baseVersion: prev?.baseVersion ?? null,\n dirty: true,\n updatedAt: Date.now(),\n metadata: this.metadata ?? prev?.metadata,\n };\n }\n\n /** Mark the document as synced after a successful remote save (NF-10). */\n async markSynced(version: string | number, serverDoc?: DocumentJSON): Promise<void> {\n if (!this.current) return;\n const record: StoredDocument = {\n ...this.current,\n doc: serverDoc ?? this.current.doc,\n baseVersion: version,\n dirty: false,\n };\n await this.store.putDocument(record);\n await this.store.dequeue(this.id);\n this.current = record;\n this.emit('synced');\n }\n\n /** Purge this document's locally-persisted data and outbox entry (F-12.7). */\n async clearLocal(): Promise<void> {\n await this.store.deleteDocument(this.id);\n await this.store.dequeue(this.id);\n this.current = null;\n }\n\n /** Stop the autosave timer and flush pending work. */\n async destroy(): Promise<void> {\n this.destroyed = true;\n await this.flush();\n }\n}\n","/**\n * Connectivity detection (§8.7). Listens to `online`/`offline` events but does\n * NOT trust `navigator.onLine` alone (it reports interface presence, not API\n * reachability); when a `ping` is provided, real reachability is confirmed\n * before reporting \"online\". Safe to construct in any environment.\n */\nexport interface ConnectivityOptions {\n /** Confirms real API reachability (e.g. a HEAD to the data API). */\n ping?: (signal?: AbortSignal) => Promise<boolean>;\n /** Polling interval in ms while running (default 30s). 0 disables polling. */\n intervalMs?: number;\n onChange?: (online: boolean) => void;\n}\n\nexport class ConnectivityMonitor {\n private readonly ping?: (signal?: AbortSignal) => Promise<boolean>;\n private readonly intervalMs: number;\n private readonly onChange?: (online: boolean) => void;\n private online: boolean;\n private timer: ReturnType<typeof setInterval> | null = null;\n private started = false;\n\n constructor(options: ConnectivityOptions = {}) {\n this.ping = options.ping;\n this.intervalMs = options.intervalMs ?? 30_000;\n this.onChange = options.onChange;\n this.online = typeof navigator !== 'undefined' ? navigator.onLine : true;\n }\n\n isOnline(): boolean {\n return this.online;\n }\n\n private readonly handleOnline = () => void this.check();\n private readonly handleOffline = () => this.set(false);\n\n start(): void {\n if (this.started || typeof window === 'undefined') return;\n this.started = true;\n window.addEventListener('online', this.handleOnline);\n window.addEventListener('offline', this.handleOffline);\n if (this.intervalMs > 0) {\n this.timer = setInterval(() => void this.check(), this.intervalMs);\n }\n void this.check();\n }\n\n stop(): void {\n if (!this.started || typeof window === 'undefined') return;\n this.started = false;\n window.removeEventListener('online', this.handleOnline);\n window.removeEventListener('offline', this.handleOffline);\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n /** Re-evaluate connectivity now, confirming reachability via ping when set. */\n async check(): Promise<boolean> {\n const navOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;\n if (!navOnline) {\n this.set(false);\n return false;\n }\n if (!this.ping) {\n this.set(true);\n return true;\n }\n try {\n const reachable = await this.ping();\n this.set(reachable);\n return reachable;\n } catch {\n this.set(false);\n return false;\n }\n }\n\n private set(online: boolean): void {\n if (online !== this.online) {\n this.online = online;\n this.onChange?.(online);\n }\n }\n}\n","import type { SaveStatus } from '../config/types';\nimport {\n ConflictError,\n type LocalStoreAdapter,\n type RemoteSyncAdapter,\n type SaveStatusListener,\n type StoredDocument,\n} from '../persistence/types';\n\nexport interface SyncEngineOptions {\n store: LocalStoreAdapter;\n remote: RemoteSyncAdapter;\n /** Max upload attempts before a document is parked for manual retry (default 6). */\n maxAttempts?: number;\n /** Base backoff delay in ms (default 1000). Doubles per attempt, capped at 5min. */\n baseDelayMs?: number;\n onStatus?: SaveStatusListener;\n /** Invoked when a version conflict is detected (F-9.9). */\n onConflict?: (local: StoredDocument, remote?: { version: string | number }) => void;\n}\n\nconst MAX_BACKOFF_MS = 5 * 60_000;\n\n/**\n * Flushes the durable outbox to the REST API on demand/reconnect (F-9.6–F-9.8).\n * Idempotent uploads, exponential backoff on transient failure, and a\n * version-guard conflict path (G-2 default). Edits are never lost: a document\n * stays dirty and queued until the server confirms it.\n */\nexport class SyncEngine {\n private readonly store: LocalStoreAdapter;\n private readonly remote: RemoteSyncAdapter;\n private readonly maxAttempts: number;\n private readonly baseDelayMs: number;\n private readonly onStatus?: SaveStatusListener;\n private readonly onConflict?: SyncEngineOptions['onConflict'];\n\n private flushing = false;\n private abortController: AbortController | null = null;\n\n constructor(options: SyncEngineOptions) {\n this.store = options.store;\n this.remote = options.remote;\n this.maxAttempts = options.maxAttempts ?? 6;\n this.baseDelayMs = options.baseDelayMs ?? 1000;\n this.onStatus = options.onStatus;\n this.onConflict = options.onConflict;\n }\n\n private emit(status: SaveStatus, detail?: { error?: string }): void {\n this.onStatus?.(status, detail);\n }\n\n /**\n * Process every queued document once. Re-entrancy-safe: concurrent calls are\n * coalesced. Returns the number of documents successfully synced.\n */\n async flush(): Promise<number> {\n if (this.flushing) return 0;\n this.flushing = true;\n this.abortController = new AbortController();\n const signal = this.abortController.signal;\n let synced = 0;\n\n try {\n const entries = await this.store.listOutbox();\n if (entries.length === 0) return 0;\n this.emit('syncing');\n const now = Date.now();\n\n for (const entry of entries) {\n if (signal.aborted) break;\n if (entry.nextAttemptAt && entry.nextAttemptAt > now) continue;\n\n const record = await this.store.getDocument(entry.id);\n if (!record || !record.dirty || record.rev !== entry.rev) {\n // Stale or already-synced entry; clear it.\n await this.store.dequeue(entry.id);\n continue;\n }\n\n try {\n const result = await this.remote.save(record, signal);\n // Only clear the dirty flag if no newer local revision arrived meanwhile.\n const latest = await this.store.getDocument(entry.id);\n if (latest && latest.rev === record.rev) {\n await this.store.putDocument({\n ...latest,\n doc: result.doc ?? latest.doc,\n baseVersion: result.version,\n dirty: false,\n });\n await this.store.dequeue(entry.id);\n } else {\n // A newer edit exists; leave it queued under its own rev.\n await this.store.dequeue(entry.id);\n }\n synced++;\n } catch (err) {\n if (err instanceof ConflictError) {\n await this.store.enqueue({\n ...entry,\n attempts: entry.attempts + 1,\n lastError: 'conflict',\n nextAttemptAt: Number.MAX_SAFE_INTEGER, // park until resolved\n });\n this.onConflict?.(record, err.remote);\n this.emit('syncFailed', { error: 'conflict' });\n continue;\n }\n const attempts = entry.attempts + 1;\n const backoff = Math.min(MAX_BACKOFF_MS, this.baseDelayMs * 2 ** entry.attempts);\n await this.store.enqueue({\n ...entry,\n attempts,\n lastError: (err as Error)?.message ?? 'upload failed',\n nextAttemptAt:\n attempts >= this.maxAttempts ? Number.MAX_SAFE_INTEGER : Date.now() + backoff,\n });\n this.emit('syncFailed', { error: (err as Error)?.message });\n }\n }\n\n const remaining = await this.store.listOutbox();\n this.emit(remaining.length === 0 ? 'synced' : 'savedLocal');\n return synced;\n } finally {\n this.flushing = false;\n this.abortController = null;\n }\n }\n\n /** Abort an in-flight flush (e.g. on going offline or unmount). */\n cancel(): void {\n this.abortController?.abort();\n }\n\n /**\n * Re-queue a parked/conflicted document for another attempt (used by a\n * host-defined conflict resolution flow after the user chooses to overwrite).\n */\n async retry(id: string, baseVersion?: string | number | null): Promise<void> {\n const record = await this.store.getDocument(id);\n if (!record) return;\n if (baseVersion !== undefined) {\n await this.store.putDocument({ ...record, baseVersion });\n }\n await this.store.enqueue({\n id,\n rev: record.rev,\n queuedAt: Date.now(),\n attempts: 0,\n });\n }\n}\n"]}
|