@dabble/patches 0.6.0 → 0.7.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 +221 -208
- package/dist/BaseDoc-DkP3tUhT.d.ts +206 -0
- package/dist/algorithms/client/applyCommittedChanges.d.ts +7 -0
- package/dist/algorithms/client/applyCommittedChanges.js +6 -3
- package/dist/algorithms/lww/consolidateOps.d.ts +40 -0
- package/dist/algorithms/lww/consolidateOps.js +103 -0
- package/dist/algorithms/lww/index.d.ts +2 -0
- package/dist/algorithms/lww/index.js +1 -0
- package/dist/algorithms/lww/mergeServerWithLocal.d.ts +22 -0
- package/dist/algorithms/lww/mergeServerWithLocal.js +32 -0
- package/dist/algorithms/server/commitChanges.d.ts +32 -8
- package/dist/algorithms/server/commitChanges.js +20 -5
- package/dist/algorithms/server/createVersion.d.ts +1 -1
- package/dist/algorithms/server/getSnapshotAtRevision.d.ts +1 -1
- package/dist/algorithms/server/getStateAtRevision.d.ts +1 -1
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +1 -1
- package/dist/client/BaseDoc.d.ts +6 -0
- package/dist/client/BaseDoc.js +70 -0
- package/dist/client/ClientAlgorithm.d.ts +101 -0
- package/dist/client/ClientAlgorithm.js +0 -0
- package/dist/client/InMemoryStore.d.ts +5 -7
- package/dist/client/InMemoryStore.js +6 -35
- package/dist/client/IndexedDBStore.d.ts +39 -73
- package/dist/client/IndexedDBStore.js +17 -220
- package/dist/client/LWWAlgorithm.d.ts +43 -0
- package/dist/client/LWWAlgorithm.js +87 -0
- package/dist/client/LWWClientStore.d.ts +73 -0
- package/dist/client/LWWClientStore.js +0 -0
- package/dist/client/LWWDoc.d.ts +56 -0
- package/dist/client/LWWDoc.js +84 -0
- package/dist/client/LWWInMemoryStore.d.ts +88 -0
- package/dist/client/LWWInMemoryStore.js +208 -0
- package/dist/client/LWWIndexedDBStore.d.ts +91 -0
- package/dist/client/LWWIndexedDBStore.js +275 -0
- package/dist/client/OTAlgorithm.d.ts +42 -0
- package/dist/client/OTAlgorithm.js +113 -0
- package/dist/client/OTClientStore.d.ts +50 -0
- package/dist/client/OTClientStore.js +0 -0
- package/dist/client/OTDoc.d.ts +6 -0
- package/dist/client/OTDoc.js +97 -0
- package/dist/client/OTIndexedDBStore.d.ts +84 -0
- package/dist/client/OTIndexedDBStore.js +163 -0
- package/dist/client/Patches.d.ts +36 -16
- package/dist/client/Patches.js +60 -27
- package/dist/client/PatchesDoc.d.ts +4 -113
- package/dist/client/PatchesDoc.js +3 -153
- package/dist/client/PatchesStore.d.ts +8 -105
- package/dist/client/factories.d.ts +72 -0
- package/dist/client/factories.js +80 -0
- package/dist/client/index.d.ts +14 -5
- package/dist/client/index.js +9 -0
- package/dist/compression/index.d.ts +1 -1
- package/dist/data/change.js +2 -0
- package/dist/fractionalIndex.d.ts +67 -0
- package/dist/fractionalIndex.js +241 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +1 -0
- package/dist/json-patch/types.d.ts +2 -0
- package/dist/net/PatchesClient.js +15 -15
- package/dist/net/PatchesSync.d.ts +24 -12
- package/dist/net/PatchesSync.js +56 -64
- package/dist/net/index.d.ts +6 -10
- package/dist/net/index.js +6 -1
- package/dist/net/protocol/JSONRPCClient.d.ts +4 -4
- package/dist/net/protocol/JSONRPCClient.js +6 -4
- package/dist/net/protocol/JSONRPCServer.d.ts +45 -9
- package/dist/net/protocol/JSONRPCServer.js +63 -8
- package/dist/net/serverContext.d.ts +38 -0
- package/dist/net/serverContext.js +20 -0
- package/dist/net/webrtc/WebRTCTransport.js +1 -1
- package/dist/net/websocket/AuthorizationProvider.d.ts +3 -3
- package/dist/net/websocket/WebSocketServer.d.ts +29 -20
- package/dist/net/websocket/WebSocketServer.js +23 -12
- package/dist/server/BranchManager.d.ts +50 -0
- package/dist/server/BranchManager.js +0 -0
- package/dist/server/CompressedStoreBackend.d.ts +7 -5
- package/dist/server/CompressedStoreBackend.js +3 -9
- package/dist/server/LWWBranchManager.d.ts +82 -0
- package/dist/server/LWWBranchManager.js +99 -0
- package/dist/server/LWWMemoryStoreBackend.d.ts +78 -0
- package/dist/server/LWWMemoryStoreBackend.js +191 -0
- package/dist/server/LWWServer.d.ts +130 -0
- package/dist/server/LWWServer.js +207 -0
- package/dist/server/{PatchesBranchManager.d.ts → OTBranchManager.d.ts} +32 -12
- package/dist/server/{PatchesBranchManager.js → OTBranchManager.js} +25 -40
- package/dist/server/OTServer.d.ts +108 -0
- package/dist/server/OTServer.js +141 -0
- package/dist/server/PatchesHistoryManager.d.ts +20 -7
- package/dist/server/PatchesHistoryManager.js +26 -3
- package/dist/server/PatchesServer.d.ts +70 -81
- package/dist/server/PatchesServer.js +0 -175
- package/dist/server/branchUtils.d.ts +82 -0
- package/dist/server/branchUtils.js +66 -0
- package/dist/server/index.d.ts +17 -6
- package/dist/server/index.js +33 -4
- package/dist/server/tombstone.d.ts +29 -0
- package/dist/server/tombstone.js +32 -0
- package/dist/server/types.d.ts +128 -26
- package/dist/server/utils.d.ts +12 -0
- package/dist/server/utils.js +23 -0
- package/dist/solid/context.d.ts +5 -4
- package/dist/solid/doc-manager.d.ts +3 -3
- package/dist/solid/index.d.ts +5 -4
- package/dist/solid/primitives.d.ts +2 -3
- package/dist/types.d.ts +4 -2
- package/dist/vue/composables.d.ts +2 -3
- package/dist/vue/doc-manager.d.ts +3 -3
- package/dist/vue/index.d.ts +5 -4
- package/dist/vue/provider.d.ts +5 -4
- package/package.json +1 -1
- package/dist/algorithms/client/collapsePendingChanges.d.ts +0 -30
- package/dist/algorithms/client/collapsePendingChanges.js +0 -78
- package/dist/net/websocket/RPCServer.d.ts +0 -141
- package/dist/net/websocket/RPCServer.js +0 -204
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { JSONPatchOp } from '../json-patch/types.js';
|
|
2
|
+
import { PatchesSnapshot, PatchesState, Change } from '../types.js';
|
|
3
|
+
import { LWWClientStore } from './LWWClientStore.js';
|
|
4
|
+
import { TrackedDoc } from './PatchesStore.js';
|
|
5
|
+
import '../json-patch/JSONPatch.js';
|
|
6
|
+
import '@dabble/delta';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* In-memory implementation of LWWClientStore for LWW (Last-Write-Wins) sync algorithm.
|
|
10
|
+
*
|
|
11
|
+
* Uses field-level storage for LWW conflict resolution:
|
|
12
|
+
* - committedFields: Server-confirmed field values
|
|
13
|
+
* - pendingOps: Local changes waiting to be sent (keyed by path)
|
|
14
|
+
* - sendingChange: In-flight change being sent to server
|
|
15
|
+
*
|
|
16
|
+
* Useful for unit tests or when you want stateless realtime behavior with LWW semantics.
|
|
17
|
+
*/
|
|
18
|
+
declare class LWWInMemoryStore implements LWWClientStore {
|
|
19
|
+
private docs;
|
|
20
|
+
/**
|
|
21
|
+
* Rebuilds a document state from snapshot + committed fields + sending + pending.
|
|
22
|
+
*/
|
|
23
|
+
getDoc(docId: string): Promise<PatchesSnapshot | undefined>;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the last committed revision for a document.
|
|
26
|
+
*/
|
|
27
|
+
getCommittedRev(docId: string): Promise<number>;
|
|
28
|
+
/**
|
|
29
|
+
* List all documents in the store.
|
|
30
|
+
*/
|
|
31
|
+
listDocs(includeDeleted?: boolean): Promise<TrackedDoc[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Saves the current document state to storage.
|
|
34
|
+
* Clears all committed fields, pending ops, and sending change.
|
|
35
|
+
*/
|
|
36
|
+
saveDoc(docId: string, docState: PatchesState): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Track documents.
|
|
39
|
+
*/
|
|
40
|
+
trackDocs(docIds: string[]): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Untrack documents by removing all their data.
|
|
43
|
+
*/
|
|
44
|
+
untrackDocs(docIds: string[]): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Marks a document as deleted and clears all associated data.
|
|
47
|
+
*/
|
|
48
|
+
deleteDoc(docId: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Confirm the deletion of a document.
|
|
51
|
+
*/
|
|
52
|
+
confirmDeleteDoc(docId: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Closes the store and releases resources.
|
|
55
|
+
*/
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Get pending ops, optionally filtered by path prefixes.
|
|
59
|
+
*/
|
|
60
|
+
getPendingOps(docId: string, pathPrefixes?: string[]): Promise<JSONPatchOp[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Save pending ops, optionally deleting paths.
|
|
63
|
+
*/
|
|
64
|
+
savePendingOps(docId: string, ops: JSONPatchOp[], pathsToDelete?: string[]): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Get the in-flight change for retry/reconnect scenarios.
|
|
67
|
+
*/
|
|
68
|
+
getSendingChange(docId: string): Promise<Change | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Atomically save sending change AND clear all pending ops.
|
|
71
|
+
*/
|
|
72
|
+
saveSendingChange(docId: string, change: Change): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Clear sendingChange after server ack, move ops to committed.
|
|
75
|
+
*/
|
|
76
|
+
confirmSendingChange(docId: string): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Apply server changes using LWW timestamp resolution.
|
|
79
|
+
*/
|
|
80
|
+
applyServerChanges(docId: string, serverChanges: Change[]): Promise<void>;
|
|
81
|
+
private getOrCreateBuffer;
|
|
82
|
+
/**
|
|
83
|
+
* Converts pending ops to an array of Change objects.
|
|
84
|
+
*/
|
|
85
|
+
private pendingOpsToChanges;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { LWWInMemoryStore };
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import "../chunk-IZ2YBCUP.js";
|
|
2
|
+
import { createChange } from "../data/change.js";
|
|
3
|
+
import { applyPatch } from "../json-patch/applyPatch.js";
|
|
4
|
+
class LWWInMemoryStore {
|
|
5
|
+
docs = /* @__PURE__ */ new Map();
|
|
6
|
+
// ─── Document Operations ─────────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Rebuilds a document state from snapshot + committed fields + sending + pending.
|
|
9
|
+
*/
|
|
10
|
+
async getDoc(docId) {
|
|
11
|
+
const buf = this.docs.get(docId);
|
|
12
|
+
if (!buf || buf.deleted) return void 0;
|
|
13
|
+
let state = buf.snapshot?.state ? { ...buf.snapshot.state } : {};
|
|
14
|
+
const committedOps = Array.from(buf.committedFields.entries()).map(([path, value]) => ({
|
|
15
|
+
op: "replace",
|
|
16
|
+
path,
|
|
17
|
+
value
|
|
18
|
+
}));
|
|
19
|
+
if (committedOps.length > 0) {
|
|
20
|
+
state = applyPatch(state, committedOps, { partial: true });
|
|
21
|
+
}
|
|
22
|
+
if (buf.sendingChange?.ops?.length) {
|
|
23
|
+
state = applyPatch(state, buf.sendingChange.ops, { partial: true });
|
|
24
|
+
}
|
|
25
|
+
const pendingOps = Array.from(buf.pendingOps.values());
|
|
26
|
+
if (pendingOps.length > 0) {
|
|
27
|
+
state = applyPatch(state, pendingOps, { partial: true });
|
|
28
|
+
}
|
|
29
|
+
const pendingChanges = this.pendingOpsToChanges(buf.pendingOps, buf.committedRev);
|
|
30
|
+
return {
|
|
31
|
+
state,
|
|
32
|
+
rev: buf.committedRev,
|
|
33
|
+
changes: buf.sendingChange ? [buf.sendingChange, ...pendingChanges] : pendingChanges
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the last committed revision for a document.
|
|
38
|
+
*/
|
|
39
|
+
async getCommittedRev(docId) {
|
|
40
|
+
return this.docs.get(docId)?.committedRev ?? 0;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* List all documents in the store.
|
|
44
|
+
*/
|
|
45
|
+
async listDocs(includeDeleted = false) {
|
|
46
|
+
return Array.from(this.docs.entries()).filter(([, buf]) => includeDeleted || !buf.deleted).map(([docId, buf]) => ({
|
|
47
|
+
docId,
|
|
48
|
+
committedRev: buf.committedRev,
|
|
49
|
+
deleted: buf.deleted
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Saves the current document state to storage.
|
|
54
|
+
* Clears all committed fields, pending ops, and sending change.
|
|
55
|
+
*/
|
|
56
|
+
async saveDoc(docId, docState) {
|
|
57
|
+
this.docs.set(docId, {
|
|
58
|
+
snapshot: { state: docState.state, rev: docState.rev },
|
|
59
|
+
committedFields: /* @__PURE__ */ new Map(),
|
|
60
|
+
pendingOps: /* @__PURE__ */ new Map(),
|
|
61
|
+
sendingChange: null,
|
|
62
|
+
committedRev: docState.rev
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// ─── Tracking ────────────────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Track documents.
|
|
68
|
+
*/
|
|
69
|
+
async trackDocs(docIds) {
|
|
70
|
+
for (const docId of docIds) {
|
|
71
|
+
const buf = this.getOrCreateBuffer(docId);
|
|
72
|
+
delete buf.deleted;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Untrack documents by removing all their data.
|
|
77
|
+
*/
|
|
78
|
+
async untrackDocs(docIds) {
|
|
79
|
+
for (const docId of docIds) {
|
|
80
|
+
this.docs.delete(docId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ─── Deletion ────────────────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Marks a document as deleted and clears all associated data.
|
|
86
|
+
*/
|
|
87
|
+
async deleteDoc(docId) {
|
|
88
|
+
const buf = this.getOrCreateBuffer(docId);
|
|
89
|
+
buf.deleted = true;
|
|
90
|
+
buf.snapshot = void 0;
|
|
91
|
+
buf.committedFields.clear();
|
|
92
|
+
buf.pendingOps.clear();
|
|
93
|
+
buf.sendingChange = null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Confirm the deletion of a document.
|
|
97
|
+
*/
|
|
98
|
+
async confirmDeleteDoc(docId) {
|
|
99
|
+
this.docs.delete(docId);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Closes the store and releases resources.
|
|
103
|
+
*/
|
|
104
|
+
async close() {
|
|
105
|
+
this.docs.clear();
|
|
106
|
+
}
|
|
107
|
+
// ─── LWWClientStore Methods ─────────────────────────────────────────────
|
|
108
|
+
/**
|
|
109
|
+
* Get pending ops, optionally filtered by path prefixes.
|
|
110
|
+
*/
|
|
111
|
+
async getPendingOps(docId, pathPrefixes) {
|
|
112
|
+
const buf = this.docs.get(docId);
|
|
113
|
+
if (!buf) return [];
|
|
114
|
+
const ops = Array.from(buf.pendingOps.values());
|
|
115
|
+
if (!pathPrefixes || pathPrefixes.length === 0) {
|
|
116
|
+
return ops;
|
|
117
|
+
}
|
|
118
|
+
return ops.filter((op) => pathPrefixes.some((prefix) => op.path === prefix || op.path.startsWith(prefix + "/")));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Save pending ops, optionally deleting paths.
|
|
122
|
+
*/
|
|
123
|
+
async savePendingOps(docId, ops, pathsToDelete) {
|
|
124
|
+
const buf = this.getOrCreateBuffer(docId);
|
|
125
|
+
if (buf.deleted) {
|
|
126
|
+
delete buf.deleted;
|
|
127
|
+
}
|
|
128
|
+
if (pathsToDelete) {
|
|
129
|
+
for (const path of pathsToDelete) {
|
|
130
|
+
buf.pendingOps.delete(path);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const op of ops) {
|
|
134
|
+
buf.pendingOps.set(op.path, op);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the in-flight change for retry/reconnect scenarios.
|
|
139
|
+
*/
|
|
140
|
+
async getSendingChange(docId) {
|
|
141
|
+
return this.docs.get(docId)?.sendingChange ?? null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Atomically save sending change AND clear all pending ops.
|
|
145
|
+
*/
|
|
146
|
+
async saveSendingChange(docId, change) {
|
|
147
|
+
const buf = this.docs.get(docId);
|
|
148
|
+
if (!buf) return;
|
|
149
|
+
buf.sendingChange = change;
|
|
150
|
+
buf.pendingOps.clear();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Clear sendingChange after server ack, move ops to committed.
|
|
154
|
+
*/
|
|
155
|
+
async confirmSendingChange(docId) {
|
|
156
|
+
const buf = this.docs.get(docId);
|
|
157
|
+
if (!buf?.sendingChange) return;
|
|
158
|
+
for (const op of buf.sendingChange.ops) {
|
|
159
|
+
buf.committedFields.set(op.path, op.value);
|
|
160
|
+
}
|
|
161
|
+
if (buf.sendingChange.rev > buf.committedRev) {
|
|
162
|
+
buf.committedRev = buf.sendingChange.rev;
|
|
163
|
+
}
|
|
164
|
+
buf.sendingChange = null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Apply server changes using LWW timestamp resolution.
|
|
168
|
+
*/
|
|
169
|
+
async applyServerChanges(docId, serverChanges) {
|
|
170
|
+
const buf = this.getOrCreateBuffer(docId);
|
|
171
|
+
for (const change of serverChanges) {
|
|
172
|
+
for (const op of change.ops) {
|
|
173
|
+
buf.committedFields.set(op.path, op.value);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const lastRev = serverChanges.at(-1)?.rev;
|
|
177
|
+
if (lastRev !== void 0 && lastRev > buf.committedRev) {
|
|
178
|
+
buf.committedRev = lastRev;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ─── Helper Methods ──────────────────────────────────────────────────────
|
|
182
|
+
getOrCreateBuffer(docId) {
|
|
183
|
+
let buf = this.docs.get(docId);
|
|
184
|
+
if (!buf) {
|
|
185
|
+
buf = {
|
|
186
|
+
committedFields: /* @__PURE__ */ new Map(),
|
|
187
|
+
pendingOps: /* @__PURE__ */ new Map(),
|
|
188
|
+
sendingChange: null,
|
|
189
|
+
committedRev: 0
|
|
190
|
+
};
|
|
191
|
+
this.docs.set(docId, buf);
|
|
192
|
+
}
|
|
193
|
+
return buf;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Converts pending ops to an array of Change objects.
|
|
197
|
+
*/
|
|
198
|
+
pendingOpsToChanges(ops, baseRev) {
|
|
199
|
+
if (ops.size === 0) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
const opsArray = Array.from(ops.values());
|
|
203
|
+
return [createChange(baseRev, baseRev + 1, opsArray)];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export {
|
|
207
|
+
LWWInMemoryStore
|
|
208
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { JSONPatchOp } from '../json-patch/types.js';
|
|
2
|
+
import { PatchesSnapshot, PatchesState, Change } from '../types.js';
|
|
3
|
+
import { IndexedDBStore } from './IndexedDBStore.js';
|
|
4
|
+
import { LWWClientStore } from './LWWClientStore.js';
|
|
5
|
+
import '../json-patch/JSONPatch.js';
|
|
6
|
+
import '@dabble/delta';
|
|
7
|
+
import '../utils/deferred.js';
|
|
8
|
+
import './PatchesStore.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* IndexedDB store implementation for Last-Writer-Wins (LWW) sync strategy.
|
|
12
|
+
*
|
|
13
|
+
* Creates stores:
|
|
14
|
+
* - docs<{ docId: string; committedRev: number; deleted?: boolean }> (primary key: docId) [shared with OT]
|
|
15
|
+
* - snapshots<{ docId: string; rev: number; state: any }> (primary key: docId) [shared with OT]
|
|
16
|
+
* - committedOps<{ docId: string; op: string; path: string; from?: string; value?: any }> (primary key: [docId, path])
|
|
17
|
+
* - pendingOps<{ docId: string; path: string; op: string; ts: number; value: any }> (primary key: [docId, path])
|
|
18
|
+
* - sendingChanges<{ docId: string; change: Change }> (primary key: docId)
|
|
19
|
+
*
|
|
20
|
+
* This store manages field-level operations for LWW conflict resolution:
|
|
21
|
+
* - Committed ops represent confirmed server state
|
|
22
|
+
* - Pending ops are local changes waiting to be sent
|
|
23
|
+
* - Sending changes are in-flight operations
|
|
24
|
+
*
|
|
25
|
+
* Every 200 ops, committed ops are compacted into the snapshot.
|
|
26
|
+
*/
|
|
27
|
+
declare class LWWIndexedDBStore extends IndexedDBStore implements LWWClientStore {
|
|
28
|
+
protected getDBVersion(): number;
|
|
29
|
+
protected onUpgrade(db: IDBDatabase, _oldVersion: number): void;
|
|
30
|
+
/**
|
|
31
|
+
* Rebuilds a document state from snapshot + committed ops + sending + pending.
|
|
32
|
+
*
|
|
33
|
+
* 1. Load the snapshot (base state + rev)
|
|
34
|
+
* 2. Apply all committedOps for docId
|
|
35
|
+
* 3. Check sendingChanges - if exists, apply its ops
|
|
36
|
+
* 4. Apply all pendingOps
|
|
37
|
+
* 5. Return reconstructed state
|
|
38
|
+
*/
|
|
39
|
+
getDoc(docId: string): Promise<PatchesSnapshot | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Saves the current document state to storage.
|
|
42
|
+
* Clears all committed fields and pending ops.
|
|
43
|
+
*/
|
|
44
|
+
saveDoc(docId: string, docState: PatchesState): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Marks a document as deleted and clears all associated data.
|
|
47
|
+
*/
|
|
48
|
+
deleteDoc(docId: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Untracks documents by removing all their data.
|
|
51
|
+
*/
|
|
52
|
+
untrackDocs(docIds: string[]): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Get pending ops, optionally filtered by path prefixes.
|
|
55
|
+
*/
|
|
56
|
+
getPendingOps(docId: string, pathPrefixes?: string[]): Promise<JSONPatchOp[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Save pending ops, optionally deleting paths.
|
|
59
|
+
*/
|
|
60
|
+
savePendingOps(docId: string, ops: JSONPatchOp[], pathsToDelete?: string[]): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Get the in-flight change for retry/reconnect scenarios.
|
|
63
|
+
*/
|
|
64
|
+
getSendingChange(docId: string): Promise<Change | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Atomically save sending change AND clear all pending ops.
|
|
67
|
+
*/
|
|
68
|
+
saveSendingChange(docId: string, change: Change): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Clear sendingChange after server ack, move ops to committed.
|
|
71
|
+
*/
|
|
72
|
+
confirmSendingChange(docId: string): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Apply server changes using LWW timestamp resolution.
|
|
75
|
+
*/
|
|
76
|
+
applyServerChanges(docId: string, serverChanges: Change[]): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Converts pending ops to an array of Change objects.
|
|
79
|
+
*/
|
|
80
|
+
private pendingOpsToChanges;
|
|
81
|
+
/**
|
|
82
|
+
* Deletes all entries for a document from a store.
|
|
83
|
+
*/
|
|
84
|
+
private deleteFieldsForDoc;
|
|
85
|
+
/**
|
|
86
|
+
* Compacts committed ops into the snapshot.
|
|
87
|
+
*/
|
|
88
|
+
private compactSnapshot;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { LWWIndexedDBStore };
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__decorateElement,
|
|
3
|
+
__decoratorMetadata,
|
|
4
|
+
__decoratorStart,
|
|
5
|
+
__runInitializers
|
|
6
|
+
} from "../chunk-IZ2YBCUP.js";
|
|
7
|
+
var _applyServerChanges_dec, _confirmSendingChange_dec, _saveSendingChange_dec, _getSendingChange_dec, _savePendingOps_dec, _getPendingOps_dec, _deleteDoc_dec, _saveDoc_dec, _getDoc_dec, _a, _init;
|
|
8
|
+
import { createChange } from "../data/change.js";
|
|
9
|
+
import { applyPatch } from "../json-patch/applyPatch.js";
|
|
10
|
+
import { blockable } from "../utils/concurrency.js";
|
|
11
|
+
import { IDBStoreWrapper, IndexedDBStore } from "./IndexedDBStore.js";
|
|
12
|
+
const DB_VERSION = 1;
|
|
13
|
+
const SNAPSHOT_INTERVAL = 200;
|
|
14
|
+
class LWWIndexedDBStore extends (_a = IndexedDBStore, _getDoc_dec = [blockable], _saveDoc_dec = [blockable], _deleteDoc_dec = [blockable], _getPendingOps_dec = [blockable], _savePendingOps_dec = [blockable], _getSendingChange_dec = [blockable], _saveSendingChange_dec = [blockable], _confirmSendingChange_dec = [blockable], _applyServerChanges_dec = [blockable], _a) {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(...arguments);
|
|
17
|
+
__runInitializers(_init, 5, this);
|
|
18
|
+
}
|
|
19
|
+
getDBVersion() {
|
|
20
|
+
return DB_VERSION;
|
|
21
|
+
}
|
|
22
|
+
onUpgrade(db, _oldVersion) {
|
|
23
|
+
if (!db.objectStoreNames.contains("committedOps")) {
|
|
24
|
+
db.createObjectStore("committedOps", { keyPath: ["docId", "path"] });
|
|
25
|
+
}
|
|
26
|
+
if (!db.objectStoreNames.contains("pendingOps")) {
|
|
27
|
+
db.createObjectStore("pendingOps", { keyPath: ["docId", "path"] });
|
|
28
|
+
}
|
|
29
|
+
if (!db.objectStoreNames.contains("sendingChanges")) {
|
|
30
|
+
db.createObjectStore("sendingChanges", { keyPath: "docId" });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async getDoc(docId) {
|
|
34
|
+
const [tx, docsStore, snapshots, committedOps, pendingOps, sendingChanges] = await this.transaction(
|
|
35
|
+
["docs", "snapshots", "committedOps", "pendingOps", "sendingChanges"],
|
|
36
|
+
"readonly"
|
|
37
|
+
);
|
|
38
|
+
const docMeta = await docsStore.get(docId);
|
|
39
|
+
if (docMeta?.deleted) {
|
|
40
|
+
await tx.complete();
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
const snapshot = await snapshots.get(docId);
|
|
44
|
+
const committed = await committedOps.getAll([docId, ""], [docId, "\uFFFF"]);
|
|
45
|
+
const sending = await sendingChanges.get(docId);
|
|
46
|
+
const pending = await pendingOps.getAll([docId, ""], [docId, "\uFFFF"]);
|
|
47
|
+
if (!snapshot && !committed.length && !pending.length && !sending) {
|
|
48
|
+
await tx.complete();
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
let state = snapshot?.state ? { ...snapshot.state } : {};
|
|
52
|
+
if (committed.length > 0) {
|
|
53
|
+
const ops = committed.map(({ docId: _docId, ...op }) => op);
|
|
54
|
+
state = applyPatch(state, ops, { partial: true });
|
|
55
|
+
}
|
|
56
|
+
if (sending?.change?.ops?.length) {
|
|
57
|
+
state = applyPatch(state, sending.change.ops, { partial: true });
|
|
58
|
+
}
|
|
59
|
+
if (pending.length > 0) {
|
|
60
|
+
const pendingOps2 = pending.map((op) => ({
|
|
61
|
+
op: op.op,
|
|
62
|
+
path: op.path,
|
|
63
|
+
value: op.value,
|
|
64
|
+
ts: op.ts
|
|
65
|
+
}));
|
|
66
|
+
state = applyPatch(state, pendingOps2, { partial: true });
|
|
67
|
+
}
|
|
68
|
+
const pendingChanges = this.pendingOpsToChanges(docId, pending, snapshot?.rev ?? 0);
|
|
69
|
+
await tx.complete();
|
|
70
|
+
return {
|
|
71
|
+
state,
|
|
72
|
+
rev: docMeta?.committedRev ?? snapshot?.rev ?? 0,
|
|
73
|
+
changes: sending?.change ? [sending.change, ...pendingChanges] : pendingChanges
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async saveDoc(docId, docState) {
|
|
77
|
+
const [tx, snapshots, committedOps, pendingOps, sendingChanges, docsStore] = await this.transaction(
|
|
78
|
+
["snapshots", "committedOps", "pendingOps", "sendingChanges", "docs"],
|
|
79
|
+
"readwrite"
|
|
80
|
+
);
|
|
81
|
+
const { rev, state } = docState;
|
|
82
|
+
await Promise.all([
|
|
83
|
+
docsStore.put({ docId, committedRev: rev }),
|
|
84
|
+
snapshots.put({ docId, state, rev }),
|
|
85
|
+
this.deleteFieldsForDoc(committedOps, docId),
|
|
86
|
+
this.deleteFieldsForDoc(pendingOps, docId),
|
|
87
|
+
sendingChanges.delete(docId)
|
|
88
|
+
]);
|
|
89
|
+
await tx.complete();
|
|
90
|
+
}
|
|
91
|
+
async deleteDoc(docId) {
|
|
92
|
+
const [tx, snapshots, committedOps, pendingOps, sendingChanges, docsStore] = await this.transaction(
|
|
93
|
+
["snapshots", "committedOps", "pendingOps", "sendingChanges", "docs"],
|
|
94
|
+
"readwrite"
|
|
95
|
+
);
|
|
96
|
+
const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0 };
|
|
97
|
+
await docsStore.put({ ...docMeta, deleted: true });
|
|
98
|
+
await Promise.all([
|
|
99
|
+
snapshots.delete(docId),
|
|
100
|
+
this.deleteFieldsForDoc(committedOps, docId),
|
|
101
|
+
this.deleteFieldsForDoc(pendingOps, docId),
|
|
102
|
+
sendingChanges.delete(docId)
|
|
103
|
+
]);
|
|
104
|
+
await tx.complete();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Untracks documents by removing all their data.
|
|
108
|
+
*/
|
|
109
|
+
async untrackDocs(docIds) {
|
|
110
|
+
const [tx, docsStore, snapshots, committedOps, pendingOps, sendingChanges] = await this.transaction(
|
|
111
|
+
["docs", "snapshots", "committedOps", "pendingOps", "sendingChanges"],
|
|
112
|
+
"readwrite"
|
|
113
|
+
);
|
|
114
|
+
await Promise.all(
|
|
115
|
+
docIds.map(
|
|
116
|
+
(docId) => Promise.all([
|
|
117
|
+
docsStore.delete(docId),
|
|
118
|
+
snapshots.delete(docId),
|
|
119
|
+
this.deleteFieldsForDoc(committedOps, docId),
|
|
120
|
+
this.deleteFieldsForDoc(pendingOps, docId),
|
|
121
|
+
sendingChanges.delete(docId)
|
|
122
|
+
])
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
await tx.complete();
|
|
126
|
+
}
|
|
127
|
+
async getPendingOps(docId, pathPrefixes) {
|
|
128
|
+
const [tx, pendingOpsStore] = await this.transaction(["pendingOps"], "readonly");
|
|
129
|
+
let pending;
|
|
130
|
+
if (!pathPrefixes || pathPrefixes.length === 0) {
|
|
131
|
+
pending = await pendingOpsStore.getAll([docId, ""], [docId, "\uFFFF"]);
|
|
132
|
+
} else {
|
|
133
|
+
const allPending = await pendingOpsStore.getAll([docId, ""], [docId, "\uFFFF"]);
|
|
134
|
+
pending = allPending.filter(
|
|
135
|
+
(op) => pathPrefixes.some((prefix) => op.path === prefix || op.path.startsWith(prefix + "/"))
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
await tx.complete();
|
|
139
|
+
return pending.map((op) => ({
|
|
140
|
+
op: op.op,
|
|
141
|
+
path: op.path,
|
|
142
|
+
value: op.value,
|
|
143
|
+
ts: op.ts
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
async savePendingOps(docId, ops, pathsToDelete) {
|
|
147
|
+
const [tx, pendingOpsStore, docsStore] = await this.transaction(["pendingOps", "docs"], "readwrite");
|
|
148
|
+
let docMeta = await docsStore.get(docId);
|
|
149
|
+
if (!docMeta) {
|
|
150
|
+
docMeta = { docId, committedRev: 0 };
|
|
151
|
+
await docsStore.put(docMeta);
|
|
152
|
+
} else if (docMeta.deleted) {
|
|
153
|
+
delete docMeta.deleted;
|
|
154
|
+
await docsStore.put(docMeta);
|
|
155
|
+
}
|
|
156
|
+
if (pathsToDelete) {
|
|
157
|
+
await Promise.all(pathsToDelete.map((path) => pendingOpsStore.delete([docId, path])));
|
|
158
|
+
}
|
|
159
|
+
await Promise.all(
|
|
160
|
+
ops.map(
|
|
161
|
+
(op) => pendingOpsStore.put({
|
|
162
|
+
docId,
|
|
163
|
+
path: op.path,
|
|
164
|
+
op: op.op,
|
|
165
|
+
ts: op.ts ?? Date.now(),
|
|
166
|
+
value: op.value
|
|
167
|
+
})
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
await tx.complete();
|
|
171
|
+
}
|
|
172
|
+
async getSendingChange(docId) {
|
|
173
|
+
const [tx, sendingChanges] = await this.transaction(["sendingChanges"], "readonly");
|
|
174
|
+
const sending = await sendingChanges.get(docId);
|
|
175
|
+
await tx.complete();
|
|
176
|
+
return sending?.change ?? null;
|
|
177
|
+
}
|
|
178
|
+
async saveSendingChange(docId, change) {
|
|
179
|
+
const [tx, pendingOpsStore, sendingChanges] = await this.transaction(["pendingOps", "sendingChanges"], "readwrite");
|
|
180
|
+
await sendingChanges.put({ docId, change });
|
|
181
|
+
await this.deleteFieldsForDoc(pendingOpsStore, docId);
|
|
182
|
+
await tx.complete();
|
|
183
|
+
}
|
|
184
|
+
async confirmSendingChange(docId) {
|
|
185
|
+
const [tx, sendingChanges, committedOps, docsStore] = await this.transaction(
|
|
186
|
+
["sendingChanges", "committedOps", "docs"],
|
|
187
|
+
"readwrite"
|
|
188
|
+
);
|
|
189
|
+
const sending = await sendingChanges.get(docId);
|
|
190
|
+
if (!sending) {
|
|
191
|
+
await tx.complete();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await Promise.all(sending.change.ops.map((op) => committedOps.put({ ...op, docId })));
|
|
195
|
+
const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0 };
|
|
196
|
+
if (sending.change.rev > docMeta.committedRev) {
|
|
197
|
+
await docsStore.put({ ...docMeta, committedRev: sending.change.rev });
|
|
198
|
+
}
|
|
199
|
+
await sendingChanges.delete(docId);
|
|
200
|
+
await tx.complete();
|
|
201
|
+
}
|
|
202
|
+
async applyServerChanges(docId, serverChanges) {
|
|
203
|
+
const [tx, committedOps, snapshots, docsStore] = await this.transaction(
|
|
204
|
+
["committedOps", "snapshots", "docs"],
|
|
205
|
+
"readwrite"
|
|
206
|
+
);
|
|
207
|
+
const allOps = serverChanges.flatMap((change) => change.ops);
|
|
208
|
+
await Promise.all(allOps.map((op) => committedOps.put({ ...op, docId })));
|
|
209
|
+
const lastCommittedRev = serverChanges.at(-1)?.rev;
|
|
210
|
+
if (lastCommittedRev !== void 0) {
|
|
211
|
+
const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0 };
|
|
212
|
+
if (lastCommittedRev > docMeta.committedRev) {
|
|
213
|
+
await docsStore.put({ ...docMeta, committedRev: lastCommittedRev });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const fieldCount = await committedOps.count([docId, ""], [docId, "\uFFFF"]);
|
|
217
|
+
if (fieldCount >= SNAPSHOT_INTERVAL) {
|
|
218
|
+
await this.compactSnapshot(docId, snapshots, committedOps, docsStore);
|
|
219
|
+
}
|
|
220
|
+
await tx.complete();
|
|
221
|
+
}
|
|
222
|
+
// ─── Helper Methods ──────────────────────────────────────────────────────
|
|
223
|
+
/**
|
|
224
|
+
* Converts pending ops to an array of Change objects.
|
|
225
|
+
*/
|
|
226
|
+
pendingOpsToChanges(_docId, ops, baseRev) {
|
|
227
|
+
if (ops.length === 0) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const opsArray = ops.map((op) => ({
|
|
231
|
+
op: op.op,
|
|
232
|
+
path: op.path,
|
|
233
|
+
value: op.value,
|
|
234
|
+
ts: op.ts
|
|
235
|
+
}));
|
|
236
|
+
return [createChange(baseRev, baseRev + 1, opsArray)];
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Deletes all entries for a document from a store.
|
|
240
|
+
*/
|
|
241
|
+
async deleteFieldsForDoc(store, docId) {
|
|
242
|
+
const entries = await store.getAll([docId, ""], [docId, "\uFFFF"]);
|
|
243
|
+
await Promise.all(entries.map((e) => store.delete([e.docId, e.path])));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Compacts committed ops into the snapshot.
|
|
247
|
+
*/
|
|
248
|
+
async compactSnapshot(docId, snapshots, committedOps, docsStore) {
|
|
249
|
+
const snapshot = await snapshots.get(docId);
|
|
250
|
+
const committed = await committedOps.getAll([docId, ""], [docId, "\uFFFF"]);
|
|
251
|
+
if (committed.length === 0) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
let state = snapshot?.state ? { ...snapshot.state } : {};
|
|
255
|
+
state = applyPatch(state, committed, { partial: true });
|
|
256
|
+
const docMeta = await docsStore.get(docId);
|
|
257
|
+
const rev = docMeta?.committedRev ?? snapshot?.rev ?? 0;
|
|
258
|
+
await snapshots.put({ docId, state, rev });
|
|
259
|
+
await this.deleteFieldsForDoc(committedOps, docId);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
_init = __decoratorStart(_a);
|
|
263
|
+
__decorateElement(_init, 1, "getDoc", _getDoc_dec, LWWIndexedDBStore);
|
|
264
|
+
__decorateElement(_init, 1, "saveDoc", _saveDoc_dec, LWWIndexedDBStore);
|
|
265
|
+
__decorateElement(_init, 1, "deleteDoc", _deleteDoc_dec, LWWIndexedDBStore);
|
|
266
|
+
__decorateElement(_init, 1, "getPendingOps", _getPendingOps_dec, LWWIndexedDBStore);
|
|
267
|
+
__decorateElement(_init, 1, "savePendingOps", _savePendingOps_dec, LWWIndexedDBStore);
|
|
268
|
+
__decorateElement(_init, 1, "getSendingChange", _getSendingChange_dec, LWWIndexedDBStore);
|
|
269
|
+
__decorateElement(_init, 1, "saveSendingChange", _saveSendingChange_dec, LWWIndexedDBStore);
|
|
270
|
+
__decorateElement(_init, 1, "confirmSendingChange", _confirmSendingChange_dec, LWWIndexedDBStore);
|
|
271
|
+
__decorateElement(_init, 1, "applyServerChanges", _applyServerChanges_dec, LWWIndexedDBStore);
|
|
272
|
+
__decoratorMetadata(_init, LWWIndexedDBStore);
|
|
273
|
+
export {
|
|
274
|
+
LWWIndexedDBStore
|
|
275
|
+
};
|