@dabble/patches 0.8.8 → 0.8.10
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/dist/algorithms/ot/shared/changeBatching.js +1 -1
- package/dist/client/BranchClientStore.d.ts +71 -0
- package/dist/client/BranchClientStore.js +0 -0
- package/dist/client/ClientAlgorithm.d.ts +12 -0
- package/dist/client/IndexedDBStore.d.ts +13 -2
- package/dist/client/IndexedDBStore.js +125 -1
- package/dist/client/LWWInMemoryStore.js +5 -3
- package/dist/client/LWWIndexedDBStore.d.ts +1 -0
- package/dist/client/LWWIndexedDBStore.js +14 -5
- package/dist/client/OTAlgorithm.d.ts +3 -0
- package/dist/client/OTAlgorithm.js +4 -0
- package/dist/client/OTClientStore.d.ts +11 -0
- package/dist/client/OTIndexedDBStore.d.ts +9 -0
- package/dist/client/OTIndexedDBStore.js +15 -3
- package/dist/client/Patches.d.ts +6 -0
- package/dist/client/Patches.js +11 -0
- package/dist/client/PatchesBranchClient.d.ts +73 -12
- package/dist/client/PatchesBranchClient.js +143 -14
- package/dist/client/index.d.ts +3 -1
- package/dist/compression/index.d.ts +15 -7
- package/dist/compression/index.js +13 -2
- package/dist/index.d.ts +4 -2
- package/dist/net/PatchesClient.d.ts +13 -8
- package/dist/net/PatchesClient.js +17 -10
- package/dist/net/PatchesSync.d.ts +36 -3
- package/dist/net/PatchesSync.js +72 -0
- package/dist/net/index.d.ts +2 -1
- package/dist/net/protocol/types.d.ts +5 -4
- package/dist/net/rest/PatchesREST.d.ts +8 -5
- package/dist/net/rest/PatchesREST.js +30 -20
- package/dist/net/rest/index.d.ts +1 -1
- package/dist/net/rest/index.js +1 -2
- package/dist/net/rest/utils.d.ts +1 -9
- package/dist/net/rest/utils.js +0 -4
- package/dist/server/BranchManager.d.ts +11 -10
- package/dist/server/LWWBranchManager.d.ts +10 -9
- package/dist/server/LWWBranchManager.js +48 -31
- package/dist/server/LWWMemoryStoreBackend.d.ts +5 -2
- package/dist/server/LWWMemoryStoreBackend.js +21 -3
- package/dist/server/OTBranchManager.d.ts +14 -12
- package/dist/server/OTBranchManager.js +58 -66
- package/dist/server/branchUtils.d.ts +6 -15
- package/dist/server/branchUtils.js +16 -13
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/types.d.ts +8 -2
- package/dist/solid/context.d.ts +1 -0
- package/dist/solid/index.d.ts +2 -1
- package/dist/solid/primitives.d.ts +30 -155
- package/dist/solid/primitives.js +53 -219
- package/dist/types.d.ts +35 -6
- package/dist/vue/composables.d.ts +29 -170
- package/dist/vue/composables.js +59 -200
- package/dist/vue/index.d.ts +2 -1
- package/dist/vue/provider.d.ts +1 -0
- package/package.json +1 -1
|
@@ -231,7 +231,7 @@ function breakLargeValueOp(origChange, op, maxBytes, startRev, sizeCalculator) {
|
|
|
231
231
|
return breakSingleChange(combinedChange, maxBytes, sizeCalculator);
|
|
232
232
|
}
|
|
233
233
|
function deriveNewChange(origChange, rev, ops) {
|
|
234
|
-
const { id: _id, ops: _o, rev: _r, baseRev: _br, created: _c,
|
|
234
|
+
const { id: _id, ops: _o, rev: _r, baseRev: _br, created: _c, ...metadata } = origChange;
|
|
235
235
|
return createChange(origChange.baseRev, rev, ops, metadata);
|
|
236
236
|
}
|
|
237
237
|
export {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ListBranchesOptions, Branch, CreateBranchMetadata, EditableBranchMetadata } from '../types.js';
|
|
2
|
+
import '../json-patch/JSONPatch.js';
|
|
3
|
+
import '@dabble/delta';
|
|
4
|
+
import '../json-patch/types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Client-side branch storage interface that doubles as a BranchAPI-compatible layer.
|
|
8
|
+
*
|
|
9
|
+
* Implements the same method signatures as BranchAPI (listBranches, createBranch,
|
|
10
|
+
* deleteBranch, updateBranch) so PatchesBranchClient can call a single
|
|
11
|
+
* interface regardless of online/offline mode.
|
|
12
|
+
*
|
|
13
|
+
* All mutating methods set `pendingOp` on the branch record so PatchesSync knows
|
|
14
|
+
* which operations to push to the server.
|
|
15
|
+
*
|
|
16
|
+
* Also exposes sync-facing methods used by PatchesSync to reconcile local state
|
|
17
|
+
* with the server.
|
|
18
|
+
*/
|
|
19
|
+
interface BranchClientStore {
|
|
20
|
+
/**
|
|
21
|
+
* Returns locally cached branch metas for a document.
|
|
22
|
+
* Excludes deleted branches.
|
|
23
|
+
*/
|
|
24
|
+
listBranches(docId: string, options?: ListBranchesOptions): Promise<Branch[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a branch record locally with `pendingOp: 'create'`.
|
|
27
|
+
* Returns the branch document ID.
|
|
28
|
+
*/
|
|
29
|
+
createBranch(docId: string, rev: number, metadata?: CreateBranchMetadata): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Deletes a branch locally.
|
|
32
|
+
* If the branch has `pendingOp: 'create'` (never synced), physically removes it.
|
|
33
|
+
* Otherwise saves a tombstone with `pendingOp: 'delete'` and `deleted: true`.
|
|
34
|
+
*/
|
|
35
|
+
deleteBranch(branchId: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Updates branch metadata locally (e.g. name, lastMergedRev).
|
|
38
|
+
* If the branch has `pendingOp: 'create'` (never synced), keeps it as 'create'.
|
|
39
|
+
* Otherwise sets `pendingOp: 'update'`.
|
|
40
|
+
*/
|
|
41
|
+
updateBranch(branchId: string, metadata: EditableBranchMetadata): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Loads a single branch by its branch document ID.
|
|
44
|
+
* Returns undefined if not found. Used to check if a document is a branch.
|
|
45
|
+
*/
|
|
46
|
+
loadBranch(branchId: string): Promise<Branch | undefined>;
|
|
47
|
+
/**
|
|
48
|
+
* Saves branch metas from the server into the local store.
|
|
49
|
+
* Merges with existing data: updates existing branches and adds new ones.
|
|
50
|
+
* Branches not in the provided array are left untouched (incremental update friendly).
|
|
51
|
+
*/
|
|
52
|
+
saveBranches(docId: string, branches: Branch[]): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Physically removes branches from the local store.
|
|
55
|
+
* Called by PatchesSync after the server confirms deletion.
|
|
56
|
+
*/
|
|
57
|
+
removeBranches(branchIds: string[]): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Returns all branches with a `pendingOp` set, across all documents.
|
|
60
|
+
* Used by PatchesSync to efficiently find branches that need server sync.
|
|
61
|
+
*/
|
|
62
|
+
listPendingBranches(): Promise<Branch[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Returns the most recent `modifiedAt` timestamp across all committed branches
|
|
65
|
+
* for a given document. Used as the `since` parameter for incremental list fetches.
|
|
66
|
+
* Returns undefined if no committed branches exist locally.
|
|
67
|
+
*/
|
|
68
|
+
getLastModifiedAt(docId: string): Promise<number | undefined>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type { BranchClientStore };
|
|
File without changes
|
|
@@ -56,6 +56,18 @@ interface ClientAlgorithm {
|
|
|
56
56
|
* @returns The changes created (for broadcast to other tabs)
|
|
57
57
|
*/
|
|
58
58
|
handleDocChange<T extends object>(docId: string, ops: JSONPatchOp[], doc: PatchesDoc<T> | undefined, metadata: Record<string, any>): Promise<Change[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Lists all changes (committed + pending) for a document.
|
|
61
|
+
* Used by PatchesBranchClient for client-side offline merge to read branch changes.
|
|
62
|
+
* Optional — only OT algorithms with IndexedDB stores support this.
|
|
63
|
+
*
|
|
64
|
+
* @param docId Document identifier
|
|
65
|
+
* @param options.startAfter Only return changes with rev > startAfter
|
|
66
|
+
* @returns Changes sorted by rev
|
|
67
|
+
*/
|
|
68
|
+
listChanges?(docId: string, options?: {
|
|
69
|
+
startAfter?: number;
|
|
70
|
+
}): Promise<Change[]>;
|
|
59
71
|
/**
|
|
60
72
|
* Read-only check for whether a document has any pending local data.
|
|
61
73
|
* - OT: Checks pendingChanges
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as easy_signal from 'easy-signal';
|
|
2
|
-
import { PatchesSnapshot, PatchesState } from '../types.js';
|
|
2
|
+
import { PatchesSnapshot, PatchesState, ListBranchesOptions, Branch, CreateBranchMetadata, EditableBranchMetadata } from '../types.js';
|
|
3
3
|
import { Deferred } from '../utils/deferred.js';
|
|
4
|
+
import { BranchClientStore } from './BranchClientStore.js';
|
|
4
5
|
import { PatchesStore, TrackedDoc } from './PatchesStore.js';
|
|
5
6
|
import '../json-patch/JSONPatch.js';
|
|
6
7
|
import '@dabble/delta';
|
|
@@ -20,7 +21,7 @@ import '../json-patch/types.js';
|
|
|
20
21
|
* - Revision tracking
|
|
21
22
|
* - Extensibility via onUpgrade signal for algorithm-specific stores
|
|
22
23
|
*/
|
|
23
|
-
declare class IndexedDBStore implements PatchesStore {
|
|
24
|
+
declare class IndexedDBStore implements PatchesStore, BranchClientStore {
|
|
24
25
|
private static readonly DB_VERSION;
|
|
25
26
|
protected db: IDBDatabase | null;
|
|
26
27
|
protected dbName?: string;
|
|
@@ -97,6 +98,16 @@ declare class IndexedDBStore implements PatchesStore {
|
|
|
97
98
|
* @returns The last committed revision, or 0 if not found.
|
|
98
99
|
*/
|
|
99
100
|
getCommittedRev(docId: string): Promise<number>;
|
|
101
|
+
listBranches(docId: string, _options?: ListBranchesOptions): Promise<Branch[]>;
|
|
102
|
+
createBranch(docId: string, rev: number, metadata?: CreateBranchMetadata): Promise<string>;
|
|
103
|
+
deleteBranch(branchId: string): Promise<void>;
|
|
104
|
+
updateBranch(branchId: string, metadata: EditableBranchMetadata): Promise<void>;
|
|
105
|
+
loadBranch(branchId: string): Promise<Branch | undefined>;
|
|
106
|
+
saveBranches(docId: string, branches: Branch[]): Promise<void>;
|
|
107
|
+
removeBranches(branchIds: string[]): Promise<void>;
|
|
108
|
+
listPendingBranches(): Promise<Branch[]>;
|
|
109
|
+
getLastModifiedAt(docId: string): Promise<number | undefined>;
|
|
110
|
+
private _saveBranch;
|
|
100
111
|
}
|
|
101
112
|
declare class IDBTransactionWrapper {
|
|
102
113
|
protected tx: IDBTransaction;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import "../chunk-IZ2YBCUP.js";
|
|
2
|
+
import { createId } from "crypto-id";
|
|
2
3
|
import { deferred } from "../utils/deferred.js";
|
|
3
4
|
import { signal } from "easy-signal";
|
|
4
5
|
class IndexedDBStore {
|
|
5
|
-
static DB_VERSION =
|
|
6
|
+
static DB_VERSION = 2;
|
|
6
7
|
db = null;
|
|
7
8
|
dbName;
|
|
8
9
|
dbPromise;
|
|
@@ -32,6 +33,16 @@ class IndexedDBStore {
|
|
|
32
33
|
if (!db.objectStoreNames.contains("snapshots")) {
|
|
33
34
|
db.createObjectStore("snapshots", { keyPath: "docId" });
|
|
34
35
|
}
|
|
36
|
+
if (!db.objectStoreNames.contains("branches")) {
|
|
37
|
+
const branchStore = db.createObjectStore("branches", { keyPath: "id" });
|
|
38
|
+
branchStore.createIndex("_docId", "_docId", { unique: false });
|
|
39
|
+
branchStore.createIndex("_pending", "_pending", { unique: false });
|
|
40
|
+
} else {
|
|
41
|
+
const branchStore = _transaction.objectStore("branches");
|
|
42
|
+
if (!branchStore.indexNames.contains("_pending")) {
|
|
43
|
+
branchStore.createIndex("_pending", "_pending", { unique: false });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
}
|
|
36
47
|
async initDB() {
|
|
37
48
|
if (!this.dbName) return;
|
|
@@ -191,6 +202,119 @@ class IndexedDBStore {
|
|
|
191
202
|
await tx.complete();
|
|
192
203
|
return docMeta?.committedRev ?? 0;
|
|
193
204
|
}
|
|
205
|
+
// ─── Branch Methods (BranchClientStore) ─────────────────────────────────
|
|
206
|
+
// --- BranchAPI-compatible methods ---
|
|
207
|
+
async listBranches(docId, _options) {
|
|
208
|
+
const [tx, branchStore] = await this.transaction(["branches"], "readonly");
|
|
209
|
+
const results = await branchStore.getAllByIndex("_docId", docId);
|
|
210
|
+
await tx.complete();
|
|
211
|
+
return results.filter((b) => !b.deleted).map(stripInternal);
|
|
212
|
+
}
|
|
213
|
+
async createBranch(docId, rev, metadata) {
|
|
214
|
+
const branchDocId = metadata?.id ?? createId(22);
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
const branch = {
|
|
217
|
+
...metadata,
|
|
218
|
+
id: branchDocId,
|
|
219
|
+
docId,
|
|
220
|
+
branchedAtRev: rev,
|
|
221
|
+
contentStartRev: metadata?.contentStartRev ?? 0,
|
|
222
|
+
createdAt: now,
|
|
223
|
+
modifiedAt: now,
|
|
224
|
+
pendingOp: "create"
|
|
225
|
+
};
|
|
226
|
+
await this._saveBranch(docId, branch);
|
|
227
|
+
return branchDocId;
|
|
228
|
+
}
|
|
229
|
+
async deleteBranch(branchId) {
|
|
230
|
+
const [tx, branchStore] = await this.transaction(["branches"], "readwrite");
|
|
231
|
+
const existing = await branchStore.get(branchId);
|
|
232
|
+
if (!existing) throw new Error(`Branch ${branchId} not found`);
|
|
233
|
+
if (existing.pendingOp === "create") {
|
|
234
|
+
await branchStore.delete(branchId);
|
|
235
|
+
} else {
|
|
236
|
+
const tombstone = {
|
|
237
|
+
...existing,
|
|
238
|
+
modifiedAt: Date.now(),
|
|
239
|
+
pendingOp: "delete",
|
|
240
|
+
deleted: true,
|
|
241
|
+
_pending: 1
|
|
242
|
+
};
|
|
243
|
+
await branchStore.put(tombstone);
|
|
244
|
+
}
|
|
245
|
+
await tx.complete();
|
|
246
|
+
}
|
|
247
|
+
async updateBranch(branchId, metadata) {
|
|
248
|
+
const [tx, branchStore] = await this.transaction(["branches"], "readwrite");
|
|
249
|
+
const existing = await branchStore.get(branchId);
|
|
250
|
+
if (!existing) throw new Error(`Branch ${branchId} not found`);
|
|
251
|
+
Object.assign(existing, metadata);
|
|
252
|
+
existing.modifiedAt = Date.now();
|
|
253
|
+
if (existing.pendingOp !== "create") existing.pendingOp = "update";
|
|
254
|
+
existing._pending = 1;
|
|
255
|
+
await branchStore.put(existing);
|
|
256
|
+
await tx.complete();
|
|
257
|
+
}
|
|
258
|
+
// --- Internal methods ---
|
|
259
|
+
async loadBranch(branchId) {
|
|
260
|
+
const [tx, branchStore] = await this.transaction(["branches"], "readonly");
|
|
261
|
+
const result = await branchStore.get(branchId);
|
|
262
|
+
await tx.complete();
|
|
263
|
+
return result ? stripInternal(result) : void 0;
|
|
264
|
+
}
|
|
265
|
+
// --- Sync-facing methods ---
|
|
266
|
+
async saveBranches(docId, branches) {
|
|
267
|
+
if (branches.length === 0) return;
|
|
268
|
+
const [tx, branchStore] = await this.transaction(["branches"], "readwrite");
|
|
269
|
+
await Promise.all(
|
|
270
|
+
branches.map(async (branch) => {
|
|
271
|
+
const existing = await branchStore.get(branch.id);
|
|
272
|
+
if (existing?.pendingOp && !branch.pendingOp) return;
|
|
273
|
+
const stored = { ...branch, _docId: docId };
|
|
274
|
+
if (existing?.lastMergedRev != null && (stored.lastMergedRev == null || existing.lastMergedRev > stored.lastMergedRev)) {
|
|
275
|
+
stored.lastMergedRev = existing.lastMergedRev;
|
|
276
|
+
}
|
|
277
|
+
if (branch.pendingOp) stored._pending = 1;
|
|
278
|
+
return branchStore.put(stored);
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
await tx.complete();
|
|
282
|
+
}
|
|
283
|
+
async removeBranches(branchIds) {
|
|
284
|
+
if (branchIds.length === 0) return;
|
|
285
|
+
const [tx, branchStore] = await this.transaction(["branches"], "readwrite");
|
|
286
|
+
await Promise.all(branchIds.map((id) => branchStore.delete(id)));
|
|
287
|
+
await tx.complete();
|
|
288
|
+
}
|
|
289
|
+
async listPendingBranches() {
|
|
290
|
+
const [tx, branchStore] = await this.transaction(["branches"], "readonly");
|
|
291
|
+
const results = await branchStore.getAllByIndex("_pending", 1);
|
|
292
|
+
await tx.complete();
|
|
293
|
+
return results.map(stripInternal);
|
|
294
|
+
}
|
|
295
|
+
async getLastModifiedAt(docId) {
|
|
296
|
+
const [tx, branchStore] = await this.transaction(["branches"], "readonly");
|
|
297
|
+
const branches = await branchStore.getAllByIndex("_docId", docId);
|
|
298
|
+
await tx.complete();
|
|
299
|
+
if (branches.length === 0) return void 0;
|
|
300
|
+
let max = 0;
|
|
301
|
+
for (const b of branches) {
|
|
302
|
+
if (!b.pendingOp && !b.deleted && b.modifiedAt > max) max = b.modifiedAt;
|
|
303
|
+
}
|
|
304
|
+
return max || void 0;
|
|
305
|
+
}
|
|
306
|
+
// --- Private helpers ---
|
|
307
|
+
async _saveBranch(docId, branch) {
|
|
308
|
+
const [tx, branchStore] = await this.transaction(["branches"], "readwrite");
|
|
309
|
+
const stored = { ...branch, _docId: docId };
|
|
310
|
+
if (branch.pendingOp) stored._pending = 1;
|
|
311
|
+
await branchStore.put(stored);
|
|
312
|
+
await tx.complete();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function stripInternal(stored) {
|
|
316
|
+
const { _docId, _pending, ...branch } = stored;
|
|
317
|
+
return branch;
|
|
194
318
|
}
|
|
195
319
|
class IDBTransactionWrapper {
|
|
196
320
|
tx;
|
|
@@ -54,12 +54,11 @@ class LWWInMemoryStore {
|
|
|
54
54
|
* Clears committed fields (subsumed by the snapshot) but preserves pending ops.
|
|
55
55
|
*/
|
|
56
56
|
async saveDoc(docId, docState) {
|
|
57
|
-
const existing = this.docs.get(docId);
|
|
58
57
|
this.docs.set(docId, {
|
|
59
58
|
snapshot: { state: docState.state, rev: docState.rev },
|
|
60
59
|
committedFields: /* @__PURE__ */ new Map(),
|
|
61
|
-
pendingOps:
|
|
62
|
-
sendingChange:
|
|
60
|
+
pendingOps: /* @__PURE__ */ new Map(),
|
|
61
|
+
sendingChange: null,
|
|
63
62
|
committedRev: docState.rev
|
|
64
63
|
});
|
|
65
64
|
}
|
|
@@ -165,6 +164,9 @@ class LWWInMemoryStore {
|
|
|
165
164
|
for (const op of buf.sendingChange.ops) {
|
|
166
165
|
buf.committedFields.set(op.path, op.value);
|
|
167
166
|
}
|
|
167
|
+
if (buf.sendingChange.rev > buf.committedRev) {
|
|
168
|
+
buf.committedRev = buf.sendingChange.rev;
|
|
169
|
+
}
|
|
168
170
|
buf.sendingChange = null;
|
|
169
171
|
}
|
|
170
172
|
/**
|
|
@@ -122,15 +122,17 @@ class LWWIndexedDBStore {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
async saveDoc(docId, docState) {
|
|
125
|
-
const [tx, snapshots, committedOps, docsStore] = await this.db.transaction(
|
|
126
|
-
["snapshots", "committedOps", "docs"],
|
|
125
|
+
const [tx, snapshots, committedOps, pendingOps, sendingChanges, docsStore] = await this.db.transaction(
|
|
126
|
+
["snapshots", "committedOps", "pendingOps", "sendingChanges", "docs"],
|
|
127
127
|
"readwrite"
|
|
128
128
|
);
|
|
129
129
|
const { rev, state } = docState;
|
|
130
130
|
await Promise.all([
|
|
131
131
|
docsStore.put({ docId, committedRev: rev, algorithm: "lww" }),
|
|
132
132
|
snapshots.put({ docId, state, rev }),
|
|
133
|
-
this.deleteFieldsForDoc(committedOps, docId)
|
|
133
|
+
this.deleteFieldsForDoc(committedOps, docId),
|
|
134
|
+
this.deleteFieldsForDoc(pendingOps, docId),
|
|
135
|
+
sendingChanges.delete(docId)
|
|
134
136
|
]);
|
|
135
137
|
await tx.complete();
|
|
136
138
|
}
|
|
@@ -231,8 +233,8 @@ class LWWIndexedDBStore {
|
|
|
231
233
|
await tx.complete();
|
|
232
234
|
}
|
|
233
235
|
async confirmSendingChange(docId) {
|
|
234
|
-
const [tx, sendingChanges, committedOps] = await this.db.transaction(
|
|
235
|
-
["sendingChanges", "committedOps"],
|
|
236
|
+
const [tx, sendingChanges, committedOps, docsStore] = await this.db.transaction(
|
|
237
|
+
["sendingChanges", "committedOps", "docs"],
|
|
236
238
|
"readwrite"
|
|
237
239
|
);
|
|
238
240
|
const sending = await sendingChanges.get(docId);
|
|
@@ -241,6 +243,13 @@ class LWWIndexedDBStore {
|
|
|
241
243
|
return;
|
|
242
244
|
}
|
|
243
245
|
await Promise.all(sending.change.ops.map((op) => committedOps.put({ ...op, docId })));
|
|
246
|
+
const rev = sending.change.rev;
|
|
247
|
+
if (rev !== void 0) {
|
|
248
|
+
const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0, algorithm: "lww" };
|
|
249
|
+
if (rev > docMeta.committedRev) {
|
|
250
|
+
await docsStore.put({ ...docMeta, committedRev: rev });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
244
253
|
await sendingChanges.delete(docId);
|
|
245
254
|
await tx.complete();
|
|
246
255
|
}
|
|
@@ -22,6 +22,9 @@ declare class OTAlgorithm implements ClientAlgorithm {
|
|
|
22
22
|
constructor(store: OTClientStore, options?: PatchesDocOptions);
|
|
23
23
|
createDoc<T extends object>(docId: string, snapshot?: PatchesSnapshot<T>): PatchesDoc<T>;
|
|
24
24
|
loadDoc(docId: string): Promise<PatchesSnapshot | undefined>;
|
|
25
|
+
listChanges(docId: string, options?: {
|
|
26
|
+
startAfter?: number;
|
|
27
|
+
}): Promise<Change[]>;
|
|
25
28
|
handleDocChange<T extends object>(docId: string, ops: JSONPatchOp[], doc: PatchesDoc<T> | undefined, metadata: Record<string, any>): Promise<Change[]>;
|
|
26
29
|
hasPending(docId: string): Promise<boolean>;
|
|
27
30
|
getPendingToSend(docId: string): Promise<Change[] | null>;
|
|
@@ -17,6 +17,10 @@ class OTAlgorithm {
|
|
|
17
17
|
async loadDoc(docId) {
|
|
18
18
|
return this.store.getDoc(docId);
|
|
19
19
|
}
|
|
20
|
+
async listChanges(docId, options) {
|
|
21
|
+
if (!this.store.listChanges) throw new Error("Store does not support listChanges");
|
|
22
|
+
return this.store.listChanges(docId, options);
|
|
23
|
+
}
|
|
20
24
|
async handleDocChange(docId, ops, doc, metadata) {
|
|
21
25
|
if (ops.length === 0) return [];
|
|
22
26
|
let committedRev;
|
|
@@ -32,6 +32,17 @@ interface OTClientStore extends PatchesStore {
|
|
|
32
32
|
* @param changes Array of new changes to append
|
|
33
33
|
*/
|
|
34
34
|
savePendingChanges(docId: string, changes: Change[]): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Lists all changes (committed + pending) for a document, sorted by rev.
|
|
37
|
+
* Used by PatchesBranchClient for client-side offline merge to read branch changes.
|
|
38
|
+
*
|
|
39
|
+
* @param docId Document identifier
|
|
40
|
+
* @param options.startAfter Only return changes with rev > startAfter
|
|
41
|
+
* @returns Changes sorted by rev
|
|
42
|
+
*/
|
|
43
|
+
listChanges?(docId: string, options?: {
|
|
44
|
+
startAfter?: number;
|
|
45
|
+
}): Promise<Change[]>;
|
|
35
46
|
/**
|
|
36
47
|
* Atomically applies server-confirmed changes and updates pending changes.
|
|
37
48
|
*
|
|
@@ -7,6 +7,7 @@ import '@dabble/delta';
|
|
|
7
7
|
import '../json-patch/types.js';
|
|
8
8
|
import 'easy-signal';
|
|
9
9
|
import '../utils/deferred.js';
|
|
10
|
+
import './BranchClientStore.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* IndexedDB store implementation for Operational Transformation (OT) sync algorithm.
|
|
@@ -94,6 +95,14 @@ declare class OTIndexedDBStore implements OTClientStore {
|
|
|
94
95
|
* @returns The pending changes.
|
|
95
96
|
*/
|
|
96
97
|
getPendingChanges(docId: string): Promise<Change[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Lists all changes (committed + pending) for a document, sorted by rev.
|
|
100
|
+
* @param docId - The document ID.
|
|
101
|
+
* @param options.startAfter - Only return changes with rev > startAfter.
|
|
102
|
+
*/
|
|
103
|
+
listChanges(docId: string, options?: {
|
|
104
|
+
startAfter?: number;
|
|
105
|
+
}): Promise<Change[]>;
|
|
97
106
|
/**
|
|
98
107
|
* Atomically applies server-confirmed changes and updates pending changes.
|
|
99
108
|
* This is the core sync operation that must be atomic.
|
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
__publicField,
|
|
6
6
|
__runInitializers
|
|
7
7
|
} from "../chunk-IZ2YBCUP.js";
|
|
8
|
-
var _applyServerChanges_dec, _getPendingChanges_dec, _savePendingChanges_dec, _saveDoc_dec, _deleteDoc_dec, _getDoc_dec, _init;
|
|
8
|
+
var _applyServerChanges_dec, _listChanges_dec, _getPendingChanges_dec, _savePendingChanges_dec, _saveDoc_dec, _deleteDoc_dec, _getDoc_dec, _init;
|
|
9
9
|
import { applyChanges } from "../algorithms/ot/shared/applyChanges.js";
|
|
10
10
|
import { blockable } from "../utils/concurrency.js";
|
|
11
11
|
import { IndexedDBStore } from "./IndexedDBStore.js";
|
|
12
12
|
const SNAPSHOT_INTERVAL = 200;
|
|
13
|
-
_getDoc_dec = [blockable], _deleteDoc_dec = [blockable], _saveDoc_dec = [blockable], _savePendingChanges_dec = [blockable], _getPendingChanges_dec = [blockable], _applyServerChanges_dec = [blockable];
|
|
13
|
+
_getDoc_dec = [blockable], _deleteDoc_dec = [blockable], _saveDoc_dec = [blockable], _savePendingChanges_dec = [blockable], _getPendingChanges_dec = [blockable], _listChanges_dec = [blockable], _applyServerChanges_dec = [blockable];
|
|
14
14
|
class OTIndexedDBStore {
|
|
15
15
|
constructor(db) {
|
|
16
16
|
__runInitializers(_init, 5, this);
|
|
@@ -111,7 +111,7 @@ class OTIndexedDBStore {
|
|
|
111
111
|
await tx.complete();
|
|
112
112
|
}
|
|
113
113
|
async saveDoc(docId, docState) {
|
|
114
|
-
const [tx, snapshots, committedChanges,
|
|
114
|
+
const [tx, snapshots, committedChanges, , docsStore] = await this.db.transaction(
|
|
115
115
|
["snapshots", "committedChanges", "pendingChanges", "docs"],
|
|
116
116
|
"readwrite"
|
|
117
117
|
);
|
|
@@ -143,6 +143,17 @@ class OTIndexedDBStore {
|
|
|
143
143
|
await tx.complete();
|
|
144
144
|
return result;
|
|
145
145
|
}
|
|
146
|
+
async listChanges(docId, options) {
|
|
147
|
+
const startRev = (options?.startAfter ?? -1) + 1;
|
|
148
|
+
const [tx, committedChanges, pendingChanges] = await this.db.transaction(
|
|
149
|
+
["committedChanges", "pendingChanges"],
|
|
150
|
+
"readonly"
|
|
151
|
+
);
|
|
152
|
+
const committed = await committedChanges.getAll([docId, startRev], [docId, Infinity]);
|
|
153
|
+
const pending = await pendingChanges.getAll([docId, startRev], [docId, Infinity]);
|
|
154
|
+
await tx.complete();
|
|
155
|
+
return [...committed, ...pending].sort((a, b) => a.rev - b.rev);
|
|
156
|
+
}
|
|
146
157
|
async applyServerChanges(docId, serverChanges, rebasedPendingChanges) {
|
|
147
158
|
const [tx, committedChangesStore, pendingChangesStore, snapshots, docsStore] = await this.db.transaction(
|
|
148
159
|
["committedChanges", "pendingChanges", "snapshots", "docs"],
|
|
@@ -203,6 +214,7 @@ __decorateElement(_init, 1, "deleteDoc", _deleteDoc_dec, OTIndexedDBStore);
|
|
|
203
214
|
__decorateElement(_init, 1, "saveDoc", _saveDoc_dec, OTIndexedDBStore);
|
|
204
215
|
__decorateElement(_init, 1, "savePendingChanges", _savePendingChanges_dec, OTIndexedDBStore);
|
|
205
216
|
__decorateElement(_init, 1, "getPendingChanges", _getPendingChanges_dec, OTIndexedDBStore);
|
|
217
|
+
__decorateElement(_init, 1, "listChanges", _listChanges_dec, OTIndexedDBStore);
|
|
206
218
|
__decorateElement(_init, 1, "applyServerChanges", _applyServerChanges_dec, OTIndexedDBStore);
|
|
207
219
|
__decoratorMetadata(_init, OTIndexedDBStore);
|
|
208
220
|
export {
|
package/dist/client/Patches.d.ts
CHANGED
|
@@ -123,6 +123,12 @@ declare class Patches {
|
|
|
123
123
|
* Should be called when shutting down the client.
|
|
124
124
|
*/
|
|
125
125
|
close(): Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Submits ops for a document through the serialized change queue.
|
|
128
|
+
* Used by PatchesBranchClient to merge branch changes without racing
|
|
129
|
+
* against concurrent user edits on the same document.
|
|
130
|
+
*/
|
|
131
|
+
submitDocChange(docId: string, ops: JSONPatchOp[], metadata?: Record<string, any>): Promise<void>;
|
|
126
132
|
/**
|
|
127
133
|
* Internal handler for doc changes. Called when doc.onChange emits ops.
|
|
128
134
|
* Serializes calls per docId to prevent concurrent handleDocChange from
|
package/dist/client/Patches.js
CHANGED
|
@@ -197,6 +197,17 @@ class Patches {
|
|
|
197
197
|
this.onServerCommit.clear();
|
|
198
198
|
this.onError.clear();
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Submits ops for a document through the serialized change queue.
|
|
202
|
+
* Used by PatchesBranchClient to merge branch changes without racing
|
|
203
|
+
* against concurrent user edits on the same document.
|
|
204
|
+
*/
|
|
205
|
+
submitDocChange(docId, ops, metadata = {}) {
|
|
206
|
+
const managed = this.docs.get(docId);
|
|
207
|
+
const algorithm = this.getDocAlgorithm(docId) ?? this.algorithms[this.defaultAlgorithm];
|
|
208
|
+
if (!algorithm) throw new Error(`No algorithm found for document ${docId}`);
|
|
209
|
+
return this._handleDocChange(docId, ops, managed?.doc, algorithm, metadata);
|
|
210
|
+
}
|
|
200
211
|
/**
|
|
201
212
|
* Internal handler for doc changes. Called when doc.onChange emits ops.
|
|
202
213
|
* Serializes calls per docId to prevent concurrent handleDocChange from
|
|
@@ -1,31 +1,92 @@
|
|
|
1
1
|
import { Store } from 'easy-signal';
|
|
2
|
+
import { SizeCalculator } from '../algorithms/ot/shared/changeBatching.js';
|
|
2
3
|
import { BranchAPI } from '../net/protocol/types.js';
|
|
3
|
-
import { Branch,
|
|
4
|
+
import { Branch, ListBranchesOptions, CreateBranchMetadata } from '../types.js';
|
|
5
|
+
import { BranchClientStore } from './BranchClientStore.js';
|
|
6
|
+
import { Patches } from './Patches.js';
|
|
7
|
+
import { AlgorithmName } from './PatchesStore.js';
|
|
4
8
|
import '../json-patch/JSONPatch.js';
|
|
5
9
|
import '@dabble/delta';
|
|
6
10
|
import '../json-patch/types.js';
|
|
11
|
+
import './ClientAlgorithm.js';
|
|
12
|
+
import '../BaseDoc-BT18xPxU.js';
|
|
7
13
|
|
|
14
|
+
interface PatchesBranchClientOptions {
|
|
15
|
+
/** Maximum size in bytes for a single change in storage. Used to break large initial changes. */
|
|
16
|
+
maxStorageBytes?: number;
|
|
17
|
+
/** Custom size calculator for change size measurement. */
|
|
18
|
+
sizeCalculator?: SizeCalculator;
|
|
19
|
+
/** Algorithm to use for the branch document (defaults to the Patches instance default). */
|
|
20
|
+
algorithm?: AlgorithmName;
|
|
21
|
+
}
|
|
8
22
|
/**
|
|
9
|
-
* Client-side branch management
|
|
10
|
-
*
|
|
23
|
+
* Client-side branch management for a document.
|
|
24
|
+
*
|
|
25
|
+
* Accepts either a `BranchAPI` (online, server does the work) or a `BranchClientStore`
|
|
26
|
+
* (offline-first, local store handles caching/pending/tombstones). The API shape
|
|
27
|
+
* determines merge behavior:
|
|
28
|
+
*
|
|
29
|
+
* - `BranchAPI` has `mergeBranch` — server performs the merge
|
|
30
|
+
* - `BranchClientStore` has `updateBranch` — client merges locally, updates `lastMergedRev`
|
|
11
31
|
*/
|
|
12
32
|
declare class PatchesBranchClient {
|
|
13
33
|
private readonly api;
|
|
34
|
+
private readonly patches;
|
|
35
|
+
private readonly options?;
|
|
14
36
|
/** Document ID */
|
|
15
37
|
readonly id: string;
|
|
16
38
|
/** Store for the branches list */
|
|
17
39
|
readonly branches: Store<Branch[]>;
|
|
18
|
-
constructor(id: string, api: BranchAPI);
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
constructor(id: string, api: BranchAPI | BranchClientStore, patches: Patches, options?: PatchesBranchClientOptions | undefined);
|
|
41
|
+
/** Whether this client uses a local store (offline-first mode). */
|
|
42
|
+
private get isOffline();
|
|
43
|
+
/**
|
|
44
|
+
* Loads cached branches from the local store.
|
|
45
|
+
* Returns empty array when using online-only BranchAPI.
|
|
46
|
+
*/
|
|
47
|
+
loadCached(): Promise<Branch[]>;
|
|
48
|
+
/**
|
|
49
|
+
* List all branches for this document.
|
|
50
|
+
* With a local store, returns cached data (server sync is handled by PatchesSync).
|
|
51
|
+
* With a BranchAPI, fetches directly from the server.
|
|
52
|
+
*/
|
|
53
|
+
listBranches(options?: ListBranchesOptions): Promise<Branch[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Create a new branch from a specific revision.
|
|
56
|
+
*
|
|
57
|
+
* When `initialState` is provided, the branch is created for offline-first sync:
|
|
58
|
+
* - Requires `metadata.id` to be set (used as the branch document ID)
|
|
59
|
+
* - Creates the initial root-replace change locally (broken into multiple if needed)
|
|
60
|
+
* - Saves the branch meta via the API (store marks it pending for later server sync)
|
|
61
|
+
* - Tracks the branch document and saves initial changes as pending through the algorithm
|
|
62
|
+
* - PatchesSync will create the branch on the server and flush the document changes
|
|
63
|
+
*
|
|
64
|
+
* When `initialState` is omitted, the branch is created directly via the API.
|
|
65
|
+
*/
|
|
66
|
+
createBranch(rev: number, metadata?: CreateBranchMetadata, initialState?: any): Promise<string>;
|
|
67
|
+
/**
|
|
68
|
+
* Delete a branch.
|
|
69
|
+
* The API implementation handles tombstones (offline store) or direct deletion (online).
|
|
70
|
+
*/
|
|
71
|
+
deleteBranch(branchId: string): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Delete a branch and its document.
|
|
74
|
+
* Convenience method that deletes both the branch record and the branch document.
|
|
75
|
+
*/
|
|
76
|
+
deleteBranchWithDoc(branchId: string): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Merge a branch's changes back into this document.
|
|
79
|
+
*
|
|
80
|
+
* Online (BranchAPI with `mergeBranch`): server performs the merge.
|
|
81
|
+
* Offline (BranchClientStore with `updateBranch`): client reads branch changes,
|
|
82
|
+
* re-stamps them with `batchId: branchId`, submits via algorithm.handleDocChange
|
|
83
|
+
* on the source doc, then updates `lastMergedRev` locally.
|
|
84
|
+
*/
|
|
26
85
|
mergeBranch(branchId: string): Promise<void>;
|
|
27
86
|
/** Clear state */
|
|
28
87
|
clear(): void;
|
|
88
|
+
private _createBranchOffline;
|
|
89
|
+
private _mergeBranchLocally;
|
|
29
90
|
}
|
|
30
91
|
|
|
31
|
-
export { PatchesBranchClient };
|
|
92
|
+
export { PatchesBranchClient, type PatchesBranchClientOptions };
|