@dabble/patches 0.8.6 → 0.8.8
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 +29 -70
- package/dist/client/LWWInMemoryStore.d.ts +8 -2
- package/dist/client/LWWInMemoryStore.js +11 -8
- package/dist/client/LWWIndexedDBStore.d.ts +8 -2
- package/dist/client/LWWIndexedDBStore.js +5 -14
- package/dist/client/OTInMemoryStore.js +2 -1
- package/dist/client/OTIndexedDBStore.js +1 -2
- package/dist/client/PatchesBranchClient.d.ts +31 -0
- package/dist/client/PatchesBranchClient.js +41 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/net/PatchesClient.d.ts +2 -2
- package/dist/net/PatchesClient.js +2 -2
- package/dist/net/PatchesSync.js +1 -1
- package/dist/net/index.d.ts +1 -1
- package/dist/net/protocol/types.d.ts +8 -2
- package/dist/net/rest/PatchesREST.d.ts +2 -2
- package/dist/net/rest/PatchesREST.js +2 -2
- package/dist/net/rest/SSEServer.js +1 -1
- package/dist/server/BranchManager.d.ts +5 -2
- package/dist/server/LWWBranchManager.d.ts +2 -2
- package/dist/server/LWWBranchManager.js +2 -2
- package/dist/server/OTBranchManager.d.ts +5 -3
- package/dist/server/OTBranchManager.js +21 -11
- package/dist/server/branchUtils.d.ts +2 -1
- package/dist/server/branchUtils.js +3 -2
- package/dist/server/types.d.ts +2 -2
- package/dist/types.d.ts +9 -1
- package/package.json +1 -1
|
@@ -198,78 +198,37 @@ function splitLargeInsertText(text, maxChunkLength, attributes) {
|
|
|
198
198
|
}
|
|
199
199
|
return results;
|
|
200
200
|
}
|
|
201
|
+
function stripTextDeltas(value, basePath, textOps) {
|
|
202
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
203
|
+
if (Array.isArray(value.ops) && value.ops.some((op) => op.insert !== void 0)) {
|
|
204
|
+
textOps.push({ op: "@txt", path: basePath, value: value.ops });
|
|
205
|
+
const { ops: _ops, ...stub } = value;
|
|
206
|
+
return stub;
|
|
207
|
+
}
|
|
208
|
+
const result = {};
|
|
209
|
+
for (const [key, val] of Object.entries(value)) {
|
|
210
|
+
result[key] = stripTextDeltas(val, `${basePath}/${key}`, textOps);
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
201
214
|
function breakLargeValueOp(origChange, op, maxBytes, startRev, sizeCalculator) {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
newOp.path = op.path;
|
|
218
|
-
newOp.value = chunk;
|
|
219
|
-
} else {
|
|
220
|
-
newOp.op = "patch";
|
|
221
|
-
newOp.path = op.path;
|
|
222
|
-
newOp.appendString = chunk;
|
|
223
|
-
}
|
|
224
|
-
results.push(deriveNewChange(origChange, rev++, [newOp]));
|
|
225
|
-
}
|
|
226
|
-
return results;
|
|
227
|
-
} else if (Array.isArray(op.value) && op.value.length > 1) {
|
|
228
|
-
const originalArray = op.value;
|
|
229
|
-
let currentChunk = [];
|
|
230
|
-
let chunkStartIndex = 0;
|
|
231
|
-
for (let i = 0; i < originalArray.length; i++) {
|
|
232
|
-
const item = originalArray[i];
|
|
233
|
-
const tentativeChunk = [...currentChunk, item];
|
|
234
|
-
const tentativeOp = { ...op, value: tentativeChunk };
|
|
235
|
-
const tentativeChangeSize = getSizeForStorage({ ...origChange, ops: [tentativeOp] }, sizeCalculator);
|
|
236
|
-
if (currentChunk.length > 0 && tentativeChangeSize > maxBytes) {
|
|
237
|
-
const chunkOp = {};
|
|
238
|
-
if (chunkStartIndex === 0) {
|
|
239
|
-
chunkOp.op = op.op;
|
|
240
|
-
chunkOp.path = op.path;
|
|
241
|
-
chunkOp.value = currentChunk;
|
|
242
|
-
} else {
|
|
243
|
-
chunkOp.op = "patch";
|
|
244
|
-
chunkOp.path = op.path;
|
|
245
|
-
chunkOp.appendArray = currentChunk;
|
|
246
|
-
}
|
|
247
|
-
results.push(deriveNewChange(origChange, rev++, [chunkOp]));
|
|
248
|
-
currentChunk = [item];
|
|
249
|
-
chunkStartIndex = i;
|
|
250
|
-
} else {
|
|
251
|
-
currentChunk.push(item);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (currentChunk.length > 0) {
|
|
255
|
-
const chunkOp = {};
|
|
256
|
-
if (chunkStartIndex === 0) {
|
|
257
|
-
chunkOp.op = op.op;
|
|
258
|
-
chunkOp.path = op.path;
|
|
259
|
-
chunkOp.value = currentChunk;
|
|
260
|
-
} else {
|
|
261
|
-
chunkOp.op = "patch";
|
|
262
|
-
chunkOp.path = op.path;
|
|
263
|
-
chunkOp.appendArray = currentChunk;
|
|
264
|
-
}
|
|
265
|
-
results.push(deriveNewChange(origChange, rev, [chunkOp]));
|
|
266
|
-
}
|
|
267
|
-
return results;
|
|
215
|
+
const value = op.value;
|
|
216
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
217
|
+
console.warn(`Oversized op ${op.op} at "${op.path}" is not an object; including as-is`);
|
|
218
|
+
return [deriveNewChange(origChange, startRev, [op])];
|
|
219
|
+
}
|
|
220
|
+
const textOps = [];
|
|
221
|
+
const strippedValue = stripTextDeltas(value, op.path, textOps);
|
|
222
|
+
if (textOps.length === 0) {
|
|
223
|
+
console.warn(`Oversized op ${op.op} at "${op.path}" has no text deltas; including as-is`);
|
|
224
|
+
return [deriveNewChange(origChange, startRev, [op])];
|
|
225
|
+
}
|
|
226
|
+
const allOps = [{ ...op, value: strippedValue }, ...textOps];
|
|
227
|
+
const combinedChange = deriveNewChange(origChange, startRev, allOps);
|
|
228
|
+
if (getSizeForStorage(combinedChange, sizeCalculator) <= maxBytes) {
|
|
229
|
+
return [combinedChange];
|
|
268
230
|
}
|
|
269
|
-
|
|
270
|
-
`Warning: Single operation of type ${op.op} (path: ${op.path}) could not be split further by breakLargeValueOp despite exceeding maxBytes. Including as is.`
|
|
271
|
-
);
|
|
272
|
-
return [deriveNewChange(origChange, rev, [op])];
|
|
231
|
+
return breakSingleChange(combinedChange, maxBytes, sizeCalculator);
|
|
273
232
|
}
|
|
274
233
|
function deriveNewChange(origChange, rev, ops) {
|
|
275
234
|
const { id: _id, ops: _o, rev: _r, baseRev: _br, created: _c, batchId: _bi, ...metadata } = origChange;
|
|
@@ -31,7 +31,7 @@ declare class LWWInMemoryStore implements LWWClientStore {
|
|
|
31
31
|
listDocs(includeDeleted?: boolean): Promise<TrackedDoc[]>;
|
|
32
32
|
/**
|
|
33
33
|
* Saves the current document state to storage.
|
|
34
|
-
* Clears
|
|
34
|
+
* Clears committed fields (subsumed by the snapshot) but preserves pending ops.
|
|
35
35
|
*/
|
|
36
36
|
saveDoc(docId: string, docState: PatchesState): Promise<void>;
|
|
37
37
|
/**
|
|
@@ -71,7 +71,13 @@ declare class LWWInMemoryStore implements LWWClientStore {
|
|
|
71
71
|
*/
|
|
72
72
|
saveSendingChange(docId: string, change: Change): Promise<void>;
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
74
|
+
* Move sending ops to committed, then clear the sending slot.
|
|
75
|
+
* committedRev is NOT updated here — applyServerChanges owns that using the
|
|
76
|
+
* server's actual rev. Updating it here would bump the rev above the server's
|
|
77
|
+
* real value for noop changes (where the server doesn't create a new rev).
|
|
78
|
+
*
|
|
79
|
+
* Call this BEFORE applyServerChanges so that server corrections (which run
|
|
80
|
+
* after) overwrite any stale ops for fields the server won via LWW.
|
|
75
81
|
*/
|
|
76
82
|
confirmSendingChange(docId: string): Promise<void>;
|
|
77
83
|
/**
|
|
@@ -51,14 +51,15 @@ class LWWInMemoryStore {
|
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* Saves the current document state to storage.
|
|
54
|
-
* Clears
|
|
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);
|
|
57
58
|
this.docs.set(docId, {
|
|
58
59
|
snapshot: { state: docState.state, rev: docState.rev },
|
|
59
60
|
committedFields: /* @__PURE__ */ new Map(),
|
|
60
|
-
pendingOps: /* @__PURE__ */ new Map(),
|
|
61
|
-
sendingChange: null,
|
|
61
|
+
pendingOps: existing?.pendingOps ?? /* @__PURE__ */ new Map(),
|
|
62
|
+
sendingChange: existing?.sendingChange ?? null,
|
|
62
63
|
committedRev: docState.rev
|
|
63
64
|
});
|
|
64
65
|
}
|
|
@@ -150,7 +151,13 @@ class LWWInMemoryStore {
|
|
|
150
151
|
buf.pendingOps.clear();
|
|
151
152
|
}
|
|
152
153
|
/**
|
|
153
|
-
*
|
|
154
|
+
* Move sending ops to committed, then clear the sending slot.
|
|
155
|
+
* committedRev is NOT updated here — applyServerChanges owns that using the
|
|
156
|
+
* server's actual rev. Updating it here would bump the rev above the server's
|
|
157
|
+
* real value for noop changes (where the server doesn't create a new rev).
|
|
158
|
+
*
|
|
159
|
+
* Call this BEFORE applyServerChanges so that server corrections (which run
|
|
160
|
+
* after) overwrite any stale ops for fields the server won via LWW.
|
|
154
161
|
*/
|
|
155
162
|
async confirmSendingChange(docId) {
|
|
156
163
|
const buf = this.docs.get(docId);
|
|
@@ -158,10 +165,6 @@ class LWWInMemoryStore {
|
|
|
158
165
|
for (const op of buf.sendingChange.ops) {
|
|
159
166
|
buf.committedFields.set(op.path, op.value);
|
|
160
167
|
}
|
|
161
|
-
const changeRev = buf.sendingChange.rev;
|
|
162
|
-
if (changeRev !== void 0 && changeRev > buf.committedRev) {
|
|
163
|
-
buf.committedRev = changeRev;
|
|
164
|
-
}
|
|
165
168
|
buf.sendingChange = null;
|
|
166
169
|
}
|
|
167
170
|
/**
|
|
@@ -73,7 +73,7 @@ declare class LWWIndexedDBStore implements LWWClientStore {
|
|
|
73
73
|
getDoc(docId: string): Promise<PatchesSnapshot | undefined>;
|
|
74
74
|
/**
|
|
75
75
|
* Saves the current document state to storage.
|
|
76
|
-
* Clears
|
|
76
|
+
* Clears committed fields (subsumed by the snapshot) but preserves pending ops.
|
|
77
77
|
*/
|
|
78
78
|
saveDoc(docId: string, docState: PatchesState): Promise<void>;
|
|
79
79
|
/**
|
|
@@ -101,7 +101,13 @@ declare class LWWIndexedDBStore implements LWWClientStore {
|
|
|
101
101
|
*/
|
|
102
102
|
saveSendingChange(docId: string, change: Change): Promise<void>;
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
104
|
+
* Move sending ops to committed, then clear the sending slot.
|
|
105
|
+
* committedRev is NOT updated here — applyServerChanges owns that using the
|
|
106
|
+
* server's actual rev. Updating it here would bump the rev above the server's
|
|
107
|
+
* real value for noop changes (where the server doesn't create a new rev).
|
|
108
|
+
*
|
|
109
|
+
* Call this BEFORE applyServerChanges so that server corrections (which run
|
|
110
|
+
* after) overwrite any stale ops for fields the server won via LWW.
|
|
105
111
|
*/
|
|
106
112
|
confirmSendingChange(docId: string): Promise<void>;
|
|
107
113
|
/**
|
|
@@ -122,17 +122,15 @@ class LWWIndexedDBStore {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
async saveDoc(docId, docState) {
|
|
125
|
-
const [tx, snapshots, committedOps,
|
|
126
|
-
["snapshots", "committedOps", "
|
|
125
|
+
const [tx, snapshots, committedOps, docsStore] = await this.db.transaction(
|
|
126
|
+
["snapshots", "committedOps", "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)
|
|
134
|
-
this.deleteFieldsForDoc(pendingOps, docId),
|
|
135
|
-
sendingChanges.delete(docId)
|
|
133
|
+
this.deleteFieldsForDoc(committedOps, docId)
|
|
136
134
|
]);
|
|
137
135
|
await tx.complete();
|
|
138
136
|
}
|
|
@@ -233,8 +231,8 @@ class LWWIndexedDBStore {
|
|
|
233
231
|
await tx.complete();
|
|
234
232
|
}
|
|
235
233
|
async confirmSendingChange(docId) {
|
|
236
|
-
const [tx, sendingChanges, committedOps
|
|
237
|
-
["sendingChanges", "committedOps"
|
|
234
|
+
const [tx, sendingChanges, committedOps] = await this.db.transaction(
|
|
235
|
+
["sendingChanges", "committedOps"],
|
|
238
236
|
"readwrite"
|
|
239
237
|
);
|
|
240
238
|
const sending = await sendingChanges.get(docId);
|
|
@@ -243,13 +241,6 @@ class LWWIndexedDBStore {
|
|
|
243
241
|
return;
|
|
244
242
|
}
|
|
245
243
|
await Promise.all(sending.change.ops.map((op) => committedOps.put({ ...op, docId })));
|
|
246
|
-
const changeRev = sending.change.rev;
|
|
247
|
-
if (changeRev !== void 0) {
|
|
248
|
-
const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0, algorithm: "lww" };
|
|
249
|
-
if (changeRev > docMeta.committedRev) {
|
|
250
|
-
await docsStore.put({ ...docMeta, committedRev: changeRev });
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
244
|
await sendingChanges.delete(docId);
|
|
254
245
|
await tx.complete();
|
|
255
246
|
}
|
|
@@ -31,10 +31,11 @@ class OTInMemoryStore {
|
|
|
31
31
|
}
|
|
32
32
|
// ─── Writes ────────────────────────────────────────────────────────────
|
|
33
33
|
async saveDoc(docId, snapshot) {
|
|
34
|
+
const existing = this.docs.get(docId);
|
|
34
35
|
this.docs.set(docId, {
|
|
35
36
|
snapshot,
|
|
36
37
|
committed: [],
|
|
37
|
-
pending: []
|
|
38
|
+
pending: existing?.pending ?? []
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
async savePendingChanges(docId, changes) {
|
|
@@ -119,8 +119,7 @@ class OTIndexedDBStore {
|
|
|
119
119
|
await Promise.all([
|
|
120
120
|
docsStore.put({ docId, committedRev: rev, algorithm: "ot" }),
|
|
121
121
|
snapshots.put({ docId, state, rev }),
|
|
122
|
-
committedChanges.delete([docId, 0], [docId, Infinity])
|
|
123
|
-
pendingChanges.delete([docId, 0], [docId, Infinity])
|
|
122
|
+
committedChanges.delete([docId, 0], [docId, Infinity])
|
|
124
123
|
]);
|
|
125
124
|
await tx.complete();
|
|
126
125
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Store } from 'easy-signal';
|
|
2
|
+
import { BranchAPI } from '../net/protocol/types.js';
|
|
3
|
+
import { Branch, EditableBranchMetadata } from '../types.js';
|
|
4
|
+
import '../json-patch/JSONPatch.js';
|
|
5
|
+
import '@dabble/delta';
|
|
6
|
+
import '../json-patch/types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Client-side branch management interface for a document.
|
|
10
|
+
* Allows listing, creating, closing, and merging branches.
|
|
11
|
+
*/
|
|
12
|
+
declare class PatchesBranchClient {
|
|
13
|
+
private readonly api;
|
|
14
|
+
/** Document ID */
|
|
15
|
+
readonly id: string;
|
|
16
|
+
/** Store for the branches list */
|
|
17
|
+
readonly branches: Store<Branch[]>;
|
|
18
|
+
constructor(id: string, api: BranchAPI);
|
|
19
|
+
/** List all branches for this document */
|
|
20
|
+
listBranches(): Promise<Branch[]>;
|
|
21
|
+
/** Create a new branch from a specific revision */
|
|
22
|
+
createBranch(rev: number, metadata?: EditableBranchMetadata): Promise<string>;
|
|
23
|
+
/** Close a branch without merging its changes */
|
|
24
|
+
closeBranch(branchId: string): Promise<void>;
|
|
25
|
+
/** Merge a branch's changes back into this document */
|
|
26
|
+
mergeBranch(branchId: string): Promise<void>;
|
|
27
|
+
/** Clear state */
|
|
28
|
+
clear(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { PatchesBranchClient };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import "../chunk-IZ2YBCUP.js";
|
|
2
|
+
import { store } from "easy-signal";
|
|
3
|
+
class PatchesBranchClient {
|
|
4
|
+
constructor(id, api) {
|
|
5
|
+
this.api = api;
|
|
6
|
+
this.id = id;
|
|
7
|
+
this.branches = store([]);
|
|
8
|
+
}
|
|
9
|
+
/** Document ID */
|
|
10
|
+
id;
|
|
11
|
+
/** Store for the branches list */
|
|
12
|
+
branches;
|
|
13
|
+
/** List all branches for this document */
|
|
14
|
+
async listBranches() {
|
|
15
|
+
this.branches.state = await this.api.listBranches(this.id);
|
|
16
|
+
return this.branches.state;
|
|
17
|
+
}
|
|
18
|
+
/** Create a new branch from a specific revision */
|
|
19
|
+
async createBranch(rev, metadata) {
|
|
20
|
+
const branchId = await this.api.createBranch(this.id, rev, metadata);
|
|
21
|
+
await this.listBranches();
|
|
22
|
+
return branchId;
|
|
23
|
+
}
|
|
24
|
+
/** Close a branch without merging its changes */
|
|
25
|
+
async closeBranch(branchId) {
|
|
26
|
+
await this.api.closeBranch(branchId);
|
|
27
|
+
await this.listBranches();
|
|
28
|
+
}
|
|
29
|
+
/** Merge a branch's changes back into this document */
|
|
30
|
+
async mergeBranch(branchId) {
|
|
31
|
+
await this.api.mergeBranch(branchId);
|
|
32
|
+
await this.listBranches();
|
|
33
|
+
}
|
|
34
|
+
/** Clear state */
|
|
35
|
+
clear() {
|
|
36
|
+
this.branches.state = [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
PatchesBranchClient
|
|
41
|
+
};
|
package/dist/client/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { LWWAlgorithm } from './LWWAlgorithm.js';
|
|
|
10
10
|
export { LWWBatcher } from './LWWBatcher.js';
|
|
11
11
|
export { OTAlgorithm } from './OTAlgorithm.js';
|
|
12
12
|
export { OpenDocOptions, Patches, PatchesOptions } from './Patches.js';
|
|
13
|
+
export { PatchesBranchClient } from './PatchesBranchClient.js';
|
|
13
14
|
export { PatchesHistoryClient } from './PatchesHistoryClient.js';
|
|
14
15
|
export { AlgorithmName, PatchesStore, TrackedDoc } from './PatchesStore.js';
|
|
15
16
|
export { OTClientStore } from './OTClientStore.js';
|
package/dist/client/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { LWWAlgorithm } from './client/LWWAlgorithm.js';
|
|
|
11
11
|
export { LWWBatcher } from './client/LWWBatcher.js';
|
|
12
12
|
export { OTAlgorithm } from './client/OTAlgorithm.js';
|
|
13
13
|
export { OpenDocOptions, Patches, PatchesOptions } from './client/Patches.js';
|
|
14
|
+
export { PatchesBranchClient } from './client/PatchesBranchClient.js';
|
|
14
15
|
export { PatchesHistoryClient } from './client/PatchesHistoryClient.js';
|
|
15
16
|
export { AlgorithmName, PatchesStore, TrackedDoc } from './client/PatchesStore.js';
|
|
16
17
|
export { OTClientStore } from './client/OTClientStore.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as easy_signal from 'easy-signal';
|
|
2
|
-
import { Change, CommitChangesOptions, PatchesState, ChangeInput, DeleteDocOptions, EditableVersionMetadata, ListVersionsOptions, VersionMetadata, PatchesSnapshot, Branch } from '../types.js';
|
|
2
|
+
import { Change, CommitChangesOptions, PatchesState, ChangeInput, DeleteDocOptions, EditableVersionMetadata, ListVersionsOptions, VersionMetadata, PatchesSnapshot, Branch, EditableBranchMetadata } from '../types.js';
|
|
3
3
|
import { JSONRPCClient } from './protocol/JSONRPCClient.js';
|
|
4
4
|
import { PatchesAPI, ClientTransport } from './protocol/types.js';
|
|
5
5
|
import '../json-patch/JSONPatch.js';
|
|
@@ -117,7 +117,7 @@ declare class PatchesClient implements PatchesAPI {
|
|
|
117
117
|
* @param metadata - Optional metadata for the new branch.
|
|
118
118
|
* @returns A promise resolving with the unique ID of the newly created branch.
|
|
119
119
|
*/
|
|
120
|
-
createBranch(docId: string, rev: number, metadata?:
|
|
120
|
+
createBranch(docId: string, rev: number, metadata?: EditableBranchMetadata, initialChanges?: Change[]): Promise<string>;
|
|
121
121
|
/**
|
|
122
122
|
* Closes a branch on the server.
|
|
123
123
|
* @param branchId - The ID of the branch to close.
|
|
@@ -143,8 +143,8 @@ class PatchesClient {
|
|
|
143
143
|
* @param metadata - Optional metadata for the new branch.
|
|
144
144
|
* @returns A promise resolving with the unique ID of the newly created branch.
|
|
145
145
|
*/
|
|
146
|
-
async createBranch(docId, rev, metadata) {
|
|
147
|
-
return this.rpc.call("createBranch", docId, rev, metadata);
|
|
146
|
+
async createBranch(docId, rev, metadata, initialChanges) {
|
|
147
|
+
return this.rpc.call("createBranch", docId, rev, metadata, initialChanges);
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
150
|
* Closes a branch on the server.
|
package/dist/net/PatchesSync.js
CHANGED
|
@@ -318,8 +318,8 @@ class PatchesSync extends (_a = ReadonlyStoreClass, _syncDoc_dec = [serialGate],
|
|
|
318
318
|
openDoc.import({ ...snapshot, changes: [] });
|
|
319
319
|
}
|
|
320
320
|
} else {
|
|
321
|
-
await this._applyServerChangesToDoc(docId, committed);
|
|
322
321
|
await algorithm.confirmSent(docId, changeBatch);
|
|
322
|
+
await this._applyServerChangesToDoc(docId, committed);
|
|
323
323
|
}
|
|
324
324
|
pending = await algorithm.getPendingToSend(docId) ?? [];
|
|
325
325
|
}
|
package/dist/net/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { encodeDocId, normalizeIds } from './rest/utils.js';
|
|
|
8
8
|
export { JSONRPCClient } from './protocol/JSONRPCClient.js';
|
|
9
9
|
export { ApiDefinition, ConnectionSignalSubscriber, JSONRPCServer, JSONRPCServerOptions, MessageHandler } from './protocol/JSONRPCServer.js';
|
|
10
10
|
export { getAuthContext, getClientId } from './serverContext.js';
|
|
11
|
-
export { AwarenessUpdateNotificationParams, ClientTransport, ConnectionState, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, Message, PatchesAPI, PatchesNotificationParams, ServerTransport, SignalNotificationParams } from './protocol/types.js';
|
|
11
|
+
export { AwarenessUpdateNotificationParams, BranchAPI, ClientTransport, ConnectionState, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, Message, PatchesAPI, PatchesNotificationParams, ServerTransport, SignalNotificationParams } from './protocol/types.js';
|
|
12
12
|
export { rpcError, rpcNotification, rpcResponse } from './protocol/utils.js';
|
|
13
13
|
export { Access, AuthContext, AuthorizationProvider, allowAll, assertNotDeleted, denyAll } from './websocket/AuthorizationProvider.js';
|
|
14
14
|
export { onlineState } from './websocket/onlineState.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Unsubscriber } from 'easy-signal';
|
|
2
|
-
import {
|
|
2
|
+
import { Branch, EditableBranchMetadata, Change, PatchesState, ChangeInput, CommitChangesOptions, EditableVersionMetadata, ListVersionsOptions, VersionMetadata } from '../../types.js';
|
|
3
3
|
import '../../json-patch/JSONPatch.js';
|
|
4
4
|
import '@dabble/delta';
|
|
5
5
|
import '../../json-patch/types.js';
|
|
@@ -131,6 +131,12 @@ interface PatchesAPI {
|
|
|
131
131
|
/** Update the name and other metadata of a specific version. */
|
|
132
132
|
updateVersion(docId: string, versionId: string, metadata: EditableVersionMetadata): Promise<void>;
|
|
133
133
|
}
|
|
134
|
+
interface BranchAPI {
|
|
135
|
+
listBranches(docId: string): Promise<Branch[]>;
|
|
136
|
+
createBranch(docId: string, rev: number, metadata?: EditableBranchMetadata, initialChanges?: Change[]): Promise<string>;
|
|
137
|
+
closeBranch(branchId: string): Promise<void>;
|
|
138
|
+
mergeBranch(branchId: string): Promise<void>;
|
|
139
|
+
}
|
|
134
140
|
interface PatchesNotificationParams {
|
|
135
141
|
docId: string;
|
|
136
142
|
changes: Change[];
|
|
@@ -148,4 +154,4 @@ interface SignalNotificationParams {
|
|
|
148
154
|
data: any;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
export { type AwarenessUpdateNotificationParams, type ClientTransport, CommitChangesOptions, type ConnectionState, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, type Message, type PatchesAPI, type PatchesNotificationParams, type ServerTransport, type SignalNotificationParams };
|
|
157
|
+
export { type AwarenessUpdateNotificationParams, type BranchAPI, type ClientTransport, CommitChangesOptions, type ConnectionState, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, type Message, type PatchesAPI, type PatchesNotificationParams, type ServerTransport, type SignalNotificationParams };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as easy_signal from 'easy-signal';
|
|
2
|
-
import { Change, CommitChangesOptions, PatchesState, ChangeInput, DeleteDocOptions, EditableVersionMetadata, ListVersionsOptions, VersionMetadata, PatchesSnapshot, Branch } from '../../types.js';
|
|
2
|
+
import { Change, CommitChangesOptions, PatchesState, ChangeInput, DeleteDocOptions, EditableVersionMetadata, ListVersionsOptions, VersionMetadata, PatchesSnapshot, Branch, EditableBranchMetadata } from '../../types.js';
|
|
3
3
|
import { PatchesConnection } from '../PatchesConnection.js';
|
|
4
4
|
import { ConnectionState } from '../protocol/types.js';
|
|
5
5
|
import '../../json-patch/JSONPatch.js';
|
|
@@ -62,7 +62,7 @@ declare class PatchesREST implements PatchesConnection {
|
|
|
62
62
|
getVersionChanges(docId: string, versionId: string): Promise<Change[]>;
|
|
63
63
|
updateVersion(docId: string, versionId: string, metadata: EditableVersionMetadata): Promise<void>;
|
|
64
64
|
listBranches(docId: string): Promise<Branch[]>;
|
|
65
|
-
createBranch(docId: string, rev: number, metadata?:
|
|
65
|
+
createBranch(docId: string, rev: number, metadata?: EditableBranchMetadata, initialChanges?: Change[]): Promise<string>;
|
|
66
66
|
closeBranch(branchId: string): Promise<void>;
|
|
67
67
|
mergeBranch(branchId: string): Promise<void>;
|
|
68
68
|
private _setState;
|
|
@@ -155,10 +155,10 @@ class PatchesREST {
|
|
|
155
155
|
async listBranches(docId) {
|
|
156
156
|
return this._fetch(`/docs/${encodeDocId(docId)}/_branches`);
|
|
157
157
|
}
|
|
158
|
-
async createBranch(docId, rev, metadata) {
|
|
158
|
+
async createBranch(docId, rev, metadata, initialChanges) {
|
|
159
159
|
return this._fetch(`/docs/${encodeDocId(docId)}/_branches`, {
|
|
160
160
|
method: "POST",
|
|
161
|
-
body: { rev, ...metadata }
|
|
161
|
+
body: { rev, ...metadata, ...initialChanges ? { initialChanges } : {} }
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
async closeBranch(branchId) {
|
|
@@ -52,7 +52,7 @@ class SSEServer {
|
|
|
52
52
|
}
|
|
53
53
|
const { readable, writable } = new TransformStream();
|
|
54
54
|
client.writer = writable.getWriter();
|
|
55
|
-
client.writer.write(client.encoder.encode(":
|
|
55
|
+
client.writer.write(client.encoder.encode("retry: 5000\n\n"));
|
|
56
56
|
if (lastEventId) {
|
|
57
57
|
const lastId = parseInt(lastEventId, 10);
|
|
58
58
|
if (isNaN(lastId)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Branch, EditableBranchMetadata,
|
|
1
|
+
import { Branch, EditableBranchMetadata, Change, BranchStatus } from '../types.js';
|
|
2
2
|
import '../json-patch/JSONPatch.js';
|
|
3
3
|
import '@dabble/delta';
|
|
4
4
|
import '../json-patch/types.js';
|
|
@@ -23,9 +23,12 @@ interface BranchManager {
|
|
|
23
23
|
* @param docId - The source document ID.
|
|
24
24
|
* @param atPoint - Algorithm-specific branching point (revision for OT, typically current rev for LWW).
|
|
25
25
|
* @param metadata - Optional branch metadata (name, custom fields).
|
|
26
|
+
* @param initialChanges - Optional pre-built initialization changes. If provided, stored directly
|
|
27
|
+
* and contentStartRev is set to lastChange.rev + 1. If omitted, the implementation generates
|
|
28
|
+
* initialization changes from the source state.
|
|
26
29
|
* @returns The new branch document ID.
|
|
27
30
|
*/
|
|
28
|
-
createBranch(docId: string, atPoint: number, metadata?: EditableBranchMetadata): Promise<string>;
|
|
31
|
+
createBranch(docId: string, atPoint: number, metadata?: EditableBranchMetadata, initialChanges?: Change[]): Promise<string>;
|
|
29
32
|
/**
|
|
30
33
|
* Updates branch metadata.
|
|
31
34
|
* @param branchId - The branch document ID.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApiDefinition } from '../net/protocol/JSONRPCServer.js';
|
|
2
|
-
import { Branch, EditableBranchMetadata,
|
|
2
|
+
import { Branch, EditableBranchMetadata, Change, BranchStatus } from '../types.js';
|
|
3
3
|
import { BranchManager } from './BranchManager.js';
|
|
4
4
|
import { LWWServer } from './LWWServer.js';
|
|
5
5
|
import { LWWStoreBackend, BranchingStoreBackend } from './types.js';
|
|
@@ -52,7 +52,7 @@ declare class LWWBranchManager implements BranchManager {
|
|
|
52
52
|
* @param metadata - Optional branch metadata.
|
|
53
53
|
* @returns The new branch document ID.
|
|
54
54
|
*/
|
|
55
|
-
createBranch(docId: string, atPoint: number, metadata?: EditableBranchMetadata): Promise<string>;
|
|
55
|
+
createBranch(docId: string, atPoint: number, metadata?: EditableBranchMetadata, _initialChanges?: Change[]): Promise<string>;
|
|
56
56
|
/**
|
|
57
57
|
* Updates branch metadata.
|
|
58
58
|
* @param branchId - The branch document ID.
|
|
@@ -36,7 +36,7 @@ class LWWBranchManager {
|
|
|
36
36
|
* @param metadata - Optional branch metadata.
|
|
37
37
|
* @returns The new branch document ID.
|
|
38
38
|
*/
|
|
39
|
-
async createBranch(docId, atPoint, metadata) {
|
|
39
|
+
async createBranch(docId, atPoint, metadata, _initialChanges) {
|
|
40
40
|
await assertNotABranch(this.store, docId);
|
|
41
41
|
const snapshot = await this.store.getSnapshot(docId);
|
|
42
42
|
const baseRev = snapshot?.rev ?? 0;
|
|
@@ -52,7 +52,7 @@ class LWWBranchManager {
|
|
|
52
52
|
if (ops.length > 0) {
|
|
53
53
|
await this.store.saveOps(branchDocId, ops);
|
|
54
54
|
}
|
|
55
|
-
const branch = createBranchRecord(branchDocId, docId, atPoint, metadata);
|
|
55
|
+
const branch = createBranchRecord(branchDocId, docId, atPoint, rev + 1, metadata);
|
|
56
56
|
await this.store.createBranch(branch);
|
|
57
57
|
return branchDocId;
|
|
58
58
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApiDefinition } from '../net/protocol/JSONRPCServer.js';
|
|
2
|
-
import { Branch, EditableBranchMetadata,
|
|
2
|
+
import { Branch, EditableBranchMetadata, Change, BranchStatus } from '../types.js';
|
|
3
3
|
import { BranchManager } from './BranchManager.js';
|
|
4
4
|
import { PatchesServer } from './PatchesServer.js';
|
|
5
5
|
import { OTStoreBackend, BranchingStoreBackend } from './types.js';
|
|
@@ -44,11 +44,13 @@ declare class OTBranchManager implements BranchManager {
|
|
|
44
44
|
* Creates a new branch for a document.
|
|
45
45
|
* @param docId - The ID of the document to branch from.
|
|
46
46
|
* @param rev - The revision of the document to branch from.
|
|
47
|
-
* @param branchName - Optional name for the branch.
|
|
48
47
|
* @param metadata - Additional optional metadata to store with the branch.
|
|
48
|
+
* @param initialChanges - Optional pre-built initialization changes. If provided, stored
|
|
49
|
+
* directly. If omitted, a root-replace change is generated from the source state at `rev`
|
|
50
|
+
* and split via breakChanges when maxPayloadBytes is set.
|
|
49
51
|
* @returns The ID of the new branch document.
|
|
50
52
|
*/
|
|
51
|
-
createBranch(docId: string, rev: number, metadata?: EditableBranchMetadata): Promise<string>;
|
|
53
|
+
createBranch(docId: string, rev: number, metadata?: EditableBranchMetadata, initialChanges?: Change[]): Promise<string>;
|
|
52
54
|
/**
|
|
53
55
|
* Updates a branch's metadata.
|
|
54
56
|
* @param branchId - The ID of the branch to update.
|
|
@@ -31,20 +31,29 @@ class OTBranchManager {
|
|
|
31
31
|
* Creates a new branch for a document.
|
|
32
32
|
* @param docId - The ID of the document to branch from.
|
|
33
33
|
* @param rev - The revision of the document to branch from.
|
|
34
|
-
* @param branchName - Optional name for the branch.
|
|
35
34
|
* @param metadata - Additional optional metadata to store with the branch.
|
|
35
|
+
* @param initialChanges - Optional pre-built initialization changes. If provided, stored
|
|
36
|
+
* directly. If omitted, a root-replace change is generated from the source state at `rev`
|
|
37
|
+
* and split via breakChanges when maxPayloadBytes is set.
|
|
36
38
|
* @returns The ID of the new branch document.
|
|
37
39
|
*/
|
|
38
|
-
async createBranch(docId, rev, metadata) {
|
|
40
|
+
async createBranch(docId, rev, metadata, initialChanges) {
|
|
39
41
|
await assertNotABranch(this.store, docId);
|
|
40
|
-
const { state: stateAtRev } = await getStateAtRevision(this.store, docId, rev);
|
|
41
42
|
const branchDocId = await generateBranchId(this.store, docId);
|
|
42
43
|
const now = Date.now();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
44
|
+
let initChanges;
|
|
45
|
+
if (initialChanges?.length) {
|
|
46
|
+
initChanges = initialChanges;
|
|
47
|
+
} else {
|
|
48
|
+
const { state: stateAtRev } = await getStateAtRevision(this.store, docId, rev);
|
|
49
|
+
const rootReplace = createChange(0, 1, [{ op: "replace", path: "", value: stateAtRev }], {
|
|
50
|
+
createdAt: now,
|
|
51
|
+
committedAt: now
|
|
52
|
+
});
|
|
53
|
+
initChanges = this.maxPayloadBytes ? breakChanges([rootReplace], this.maxPayloadBytes) : [rootReplace];
|
|
54
|
+
}
|
|
55
|
+
const contentStartRev = initChanges[initChanges.length - 1].rev + 1;
|
|
56
|
+
await this.store.saveChanges(branchDocId, initChanges);
|
|
48
57
|
const initialVersionMetadata = createVersionMetadata({
|
|
49
58
|
origin: "main",
|
|
50
59
|
startedAt: now,
|
|
@@ -55,8 +64,8 @@ class OTBranchManager {
|
|
|
55
64
|
groupId: branchDocId,
|
|
56
65
|
branchName: metadata?.name
|
|
57
66
|
});
|
|
58
|
-
await this.store.createVersion(branchDocId, initialVersionMetadata,
|
|
59
|
-
const branch = createBranchRecord(branchDocId, docId, rev, metadata);
|
|
67
|
+
await this.store.createVersion(branchDocId, initialVersionMetadata, initChanges);
|
|
68
|
+
const branch = createBranchRecord(branchDocId, docId, rev, contentStartRev, metadata);
|
|
60
69
|
await this.store.createBranch(branch);
|
|
61
70
|
return branchDocId;
|
|
62
71
|
}
|
|
@@ -88,7 +97,8 @@ class OTBranchManager {
|
|
|
88
97
|
assertBranchOpenForMerge(branch, branchId);
|
|
89
98
|
const sourceDocId = branch.docId;
|
|
90
99
|
const branchStartRevOnSource = branch.branchedAtRev;
|
|
91
|
-
const
|
|
100
|
+
const contentStartRev = branch.contentStartRev ?? 2;
|
|
101
|
+
const branchChanges = await this.store.listChanges(branchId, { startAfter: contentStartRev - 1 });
|
|
92
102
|
if (branchChanges.length === 0) {
|
|
93
103
|
console.log(`Branch ${branchId} has no changes to merge.`);
|
|
94
104
|
await this.closeBranch(branchId, "merged");
|
|
@@ -36,10 +36,11 @@ declare function generateBranchId(store: BranchIdGenerator, docId: string): Prom
|
|
|
36
36
|
* @param branchDocId - The branch document ID.
|
|
37
37
|
* @param sourceDocId - The source document being branched from.
|
|
38
38
|
* @param branchedAtRev - The revision at which the branch was created.
|
|
39
|
+
* @param contentStartRev - The first revision of user content on the branch (after init changes).
|
|
39
40
|
* @param metadata - Optional branch metadata (name, etc.).
|
|
40
41
|
* @returns A new Branch object.
|
|
41
42
|
*/
|
|
42
|
-
declare function createBranchRecord(branchDocId: string, sourceDocId: string, branchedAtRev: number, metadata?: EditableBranchMetadata): Branch;
|
|
43
|
+
declare function createBranchRecord(branchDocId: string, sourceDocId: string, branchedAtRev: number, contentStartRev: number, metadata?: EditableBranchMetadata): Branch;
|
|
43
44
|
/**
|
|
44
45
|
* Store interface for branch loading.
|
|
45
46
|
*/
|
|
@@ -7,7 +7,7 @@ const branchManagerApi = {
|
|
|
7
7
|
closeBranch: "write",
|
|
8
8
|
mergeBranch: "write"
|
|
9
9
|
};
|
|
10
|
-
const nonModifiableBranchFields = /* @__PURE__ */ new Set(["id", "docId", "branchedAtRev", "createdAt", "status"]);
|
|
10
|
+
const nonModifiableBranchFields = /* @__PURE__ */ new Set(["id", "docId", "branchedAtRev", "createdAt", "status", "contentStartRev"]);
|
|
11
11
|
function assertBranchMetadata(metadata) {
|
|
12
12
|
if (!metadata) return;
|
|
13
13
|
for (const key in metadata) {
|
|
@@ -19,12 +19,13 @@ function assertBranchMetadata(metadata) {
|
|
|
19
19
|
async function generateBranchId(store, docId) {
|
|
20
20
|
return store.createBranchId ? await Promise.resolve(store.createBranchId(docId)) : createId(22);
|
|
21
21
|
}
|
|
22
|
-
function createBranchRecord(branchDocId, sourceDocId, branchedAtRev, metadata) {
|
|
22
|
+
function createBranchRecord(branchDocId, sourceDocId, branchedAtRev, contentStartRev, metadata) {
|
|
23
23
|
return {
|
|
24
24
|
...metadata,
|
|
25
25
|
id: branchDocId,
|
|
26
26
|
docId: sourceDocId,
|
|
27
27
|
branchedAtRev,
|
|
28
|
+
contentStartRev,
|
|
28
29
|
createdAt: Date.now(),
|
|
29
30
|
status: "open"
|
|
30
31
|
};
|
package/dist/server/types.d.ts
CHANGED
|
@@ -155,8 +155,8 @@ interface BranchingStoreBackend {
|
|
|
155
155
|
loadBranch(branchId: string): Promise<Branch | null>;
|
|
156
156
|
/** Creates or updates the metadata record for a branch. */
|
|
157
157
|
createBranch(branch: Branch): Promise<void>;
|
|
158
|
-
/** Updates
|
|
159
|
-
updateBranch(branchId: string, updates: Partial<
|
|
158
|
+
/** Updates mutable fields of an existing branch record (excludes immutable identity fields). */
|
|
159
|
+
updateBranch(branchId: string, updates: Partial<Omit<Branch, 'id' | 'docId' | 'branchedAtRev' | 'createdAt' | 'contentStartRev'>>): Promise<void>;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
export type { BranchingStoreBackend, LWWStoreBackend, ListFieldsOptions, OTStoreBackend, ServerStoreBackend, SnapshotResult, TombstoneStoreBackend, VersioningStoreBackend };
|
package/dist/types.d.ts
CHANGED
|
@@ -96,10 +96,18 @@ interface Branch {
|
|
|
96
96
|
name?: string;
|
|
97
97
|
/** Current status of the branch. */
|
|
98
98
|
status: BranchStatus;
|
|
99
|
+
/**
|
|
100
|
+
* The first revision on the branch that contains user content (after initialization changes).
|
|
101
|
+
* Initialization changes (e.g. the root-replace that seeds the branch with source state)
|
|
102
|
+
* are at revisions < contentStartRev and are skipped during merge.
|
|
103
|
+
* Typically 2 for a single-change initialization; higher when the initial state is split
|
|
104
|
+
* across multiple changes due to size limits.
|
|
105
|
+
*/
|
|
106
|
+
contentStartRev: number;
|
|
99
107
|
/** Optional arbitrary metadata associated with the branch record. */
|
|
100
108
|
[metadata: string]: any;
|
|
101
109
|
}
|
|
102
|
-
type EditableBranchMetadata = Disallowed<Branch, 'id' | 'docId' | 'branchedAtRev' | 'createdAt' | 'status'>;
|
|
110
|
+
type EditableBranchMetadata = Disallowed<Branch, 'id' | 'docId' | 'branchedAtRev' | 'createdAt' | 'status' | 'contentStartRev'>;
|
|
103
111
|
/**
|
|
104
112
|
* Represents a tombstone for a deleted document.
|
|
105
113
|
* Tombstones persist after deletion to inform late-connecting clients
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dabble/patches",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"description": "Immutable JSON Patch implementation based on RFC 6902 supporting operational transformation and last-writer-wins",
|
|
5
5
|
"author": "Jacob Wright <jacwright@gmail.com>",
|
|
6
6
|
"bugs": {
|