@dabble/patches 0.8.7 → 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.
@@ -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 results = [];
203
- let rev = startRev;
204
- const baseOpSize = getSizeForStorage({ ...op, value: "" }, sizeCalculator);
205
- const baseChangeSize = getSizeForStorage({ ...origChange, ops: [{ ...op, value: "" }] }, sizeCalculator) - baseOpSize;
206
- const valueBudget = maxBytes - baseChangeSize - 50;
207
- if (typeof op.value === "string" && op.value.length > 100) {
208
- const text = op.value;
209
- const targetChunkSize = Math.max(1, valueBudget);
210
- const numChunks = Math.ceil(text.length / targetChunkSize);
211
- const chunkSize = Math.ceil(text.length / numChunks);
212
- for (let i = 0; i < text.length; i += chunkSize) {
213
- const chunk = text.slice(i, i + chunkSize);
214
- const newOp = { op: "add" };
215
- if (i === 0) {
216
- newOp.op = op.op;
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
- console.warn(
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;
@@ -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
+ };
@@ -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';
@@ -12,4 +12,5 @@ export * from "./OTDoc.js";
12
12
  export * from "./OTAlgorithm.js";
13
13
  export * from "./Patches.js";
14
14
  export * from "./PatchesDoc.js";
15
+ export * from "./PatchesBranchClient.js";
15
16
  export * from "./PatchesHistoryClient.js";
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?: EditableVersionMetadata): Promise<string>;
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.
@@ -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 { PatchesState, Change, ChangeInput, CommitChangesOptions, EditableVersionMetadata, ListVersionsOptions, VersionMetadata } from '../../types.js';
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?: EditableVersionMetadata): Promise<string>;
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(": connected\n\n"));
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, BranchStatus, Change } from '../types.js';
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, BranchStatus, Change } from '../types.js';
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, BranchStatus, Change } from '../types.js';
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
- const initialChange = createChange(0, 1, [{ op: "replace", path: "", value: stateAtRev }], {
44
- createdAt: now,
45
- committedAt: now
46
- });
47
- await this.store.saveChanges(branchDocId, [initialChange]);
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, [initialChange]);
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 branchChanges = await this.store.listChanges(branchId, {});
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
  };
@@ -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 specific fields (status, name, metadata) of an existing branch record. */
159
- updateBranch(branchId: string, updates: Partial<Pick<Branch, 'status' | 'name' | 'metadata'>>): Promise<void>;
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.7",
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": {