@dabble/patches 0.8.7 → 0.8.9

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.
Files changed (54) hide show
  1. package/dist/algorithms/ot/shared/changeBatching.js +30 -71
  2. package/dist/client/BranchClientStore.d.ts +71 -0
  3. package/dist/client/BranchClientStore.js +0 -0
  4. package/dist/client/ClientAlgorithm.d.ts +12 -0
  5. package/dist/client/IndexedDBStore.d.ts +13 -2
  6. package/dist/client/IndexedDBStore.js +125 -1
  7. package/dist/client/LWWInMemoryStore.js +5 -3
  8. package/dist/client/LWWIndexedDBStore.d.ts +1 -0
  9. package/dist/client/LWWIndexedDBStore.js +14 -5
  10. package/dist/client/OTAlgorithm.d.ts +3 -0
  11. package/dist/client/OTAlgorithm.js +4 -0
  12. package/dist/client/OTClientStore.d.ts +11 -0
  13. package/dist/client/OTIndexedDBStore.d.ts +9 -0
  14. package/dist/client/OTIndexedDBStore.js +15 -3
  15. package/dist/client/Patches.d.ts +6 -0
  16. package/dist/client/Patches.js +11 -0
  17. package/dist/client/PatchesBranchClient.d.ts +92 -0
  18. package/dist/client/PatchesBranchClient.js +170 -0
  19. package/dist/client/index.d.ts +3 -0
  20. package/dist/client/index.js +1 -0
  21. package/dist/compression/index.d.ts +15 -7
  22. package/dist/compression/index.js +13 -2
  23. package/dist/index.d.ts +4 -1
  24. package/dist/net/PatchesClient.d.ts +13 -8
  25. package/dist/net/PatchesClient.js +15 -8
  26. package/dist/net/PatchesSync.d.ts +36 -3
  27. package/dist/net/PatchesSync.js +72 -0
  28. package/dist/net/index.d.ts +3 -2
  29. package/dist/net/protocol/types.d.ts +9 -2
  30. package/dist/net/rest/PatchesREST.d.ts +8 -5
  31. package/dist/net/rest/PatchesREST.js +29 -19
  32. package/dist/net/rest/SSEServer.js +1 -1
  33. package/dist/net/rest/index.d.ts +1 -1
  34. package/dist/net/rest/index.js +1 -2
  35. package/dist/net/rest/utils.d.ts +1 -9
  36. package/dist/net/rest/utils.js +0 -4
  37. package/dist/server/BranchManager.d.ts +11 -7
  38. package/dist/server/LWWBranchManager.d.ts +10 -9
  39. package/dist/server/LWWBranchManager.js +47 -30
  40. package/dist/server/LWWMemoryStoreBackend.d.ts +5 -2
  41. package/dist/server/LWWMemoryStoreBackend.js +21 -3
  42. package/dist/server/OTBranchManager.d.ts +14 -10
  43. package/dist/server/OTBranchManager.js +65 -63
  44. package/dist/server/branchUtils.d.ts +7 -15
  45. package/dist/server/branchUtils.js +18 -14
  46. package/dist/server/index.d.ts +1 -1
  47. package/dist/server/index.js +2 -2
  48. package/dist/server/types.d.ts +10 -4
  49. package/dist/solid/context.d.ts +1 -0
  50. package/dist/solid/index.d.ts +1 -0
  51. package/dist/types.d.ts +43 -6
  52. package/dist/vue/index.d.ts +1 -0
  53. package/dist/vue/provider.d.ts +1 -0
  54. package/package.json +1 -1
@@ -5,7 +5,7 @@ import { createChange } from "../data/change.js";
5
5
  import { createVersionMetadata } from "../data/version.js";
6
6
  import {
7
7
  assertBranchMetadata,
8
- assertBranchOpenForMerge,
8
+ assertBranchExists,
9
9
  assertNotABranch,
10
10
  branchManagerApi,
11
11
  createBranchRecord,
@@ -22,41 +22,56 @@ class OTBranchManager {
22
22
  /**
23
23
  * Lists all open branches for a document.
24
24
  * @param docId - The ID of the document.
25
+ * @param options - Optional filtering options (e.g. `since` for incremental sync).
25
26
  * @returns The branches.
26
27
  */
27
- async listBranches(docId) {
28
- return await this.store.listBranches(docId);
28
+ async listBranches(docId, options) {
29
+ return await this.store.listBranches(docId, options);
29
30
  }
30
31
  /**
31
32
  * Creates a new branch for a document.
32
33
  * @param docId - The ID of the document to branch from.
33
34
  * @param rev - The revision of the document to branch from.
34
- * @param branchName - Optional name for the branch.
35
- * @param metadata - Additional optional metadata to store with the branch.
36
35
  * @returns The ID of the new branch document.
37
36
  */
38
37
  async createBranch(docId, rev, metadata) {
38
+ const branchDocId = metadata?.id ?? await generateBranchId(this.store, docId);
39
+ if (metadata?.id) {
40
+ const existing = await this.store.loadBranch(branchDocId);
41
+ if (existing) {
42
+ if (existing.docId !== docId) {
43
+ throw new Error(`Branch ${branchDocId} already exists for a different document`);
44
+ }
45
+ return branchDocId;
46
+ }
47
+ }
39
48
  await assertNotABranch(this.store, docId);
40
- const { state: stateAtRev } = await getStateAtRevision(this.store, docId, rev);
41
- const branchDocId = await generateBranchId(this.store, docId);
42
49
  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]);
48
- const initialVersionMetadata = createVersionMetadata({
49
- origin: "main",
50
- startedAt: now,
51
- endedAt: now,
52
- endRev: rev,
53
- startRev: rev,
54
- name: metadata?.name,
55
- groupId: branchDocId,
56
- branchName: metadata?.name
57
- });
58
- await this.store.createVersion(branchDocId, initialVersionMetadata, [initialChange]);
59
- const branch = createBranchRecord(branchDocId, docId, rev, metadata);
50
+ let contentStartRev;
51
+ if (metadata?.contentStartRev) {
52
+ contentStartRev = metadata.contentStartRev;
53
+ } else {
54
+ const { state: stateAtRev } = await getStateAtRevision(this.store, docId, rev);
55
+ const rootReplace = createChange(0, 1, [{ op: "replace", path: "", value: stateAtRev }], {
56
+ createdAt: now,
57
+ committedAt: now
58
+ });
59
+ const initChanges = this.maxPayloadBytes ? breakChanges([rootReplace], this.maxPayloadBytes) : [rootReplace];
60
+ contentStartRev = initChanges[initChanges.length - 1].rev + 1;
61
+ await this.store.saveChanges(branchDocId, initChanges);
62
+ const initialVersionMetadata = createVersionMetadata({
63
+ origin: "main",
64
+ startedAt: now,
65
+ endedAt: now,
66
+ endRev: rev,
67
+ startRev: rev,
68
+ name: metadata?.name,
69
+ groupId: branchDocId,
70
+ branchName: metadata?.name
71
+ });
72
+ await this.store.createVersion(branchDocId, initialVersionMetadata, initChanges);
73
+ }
74
+ const branch = createBranchRecord(branchDocId, docId, rev, contentStartRev, metadata);
60
75
  await this.store.createBranch(branch);
61
76
  return branchDocId;
62
77
  }
@@ -67,78 +82,65 @@ class OTBranchManager {
67
82
  */
68
83
  async updateBranch(branchId, metadata) {
69
84
  assertBranchMetadata(metadata);
70
- await this.store.updateBranch(branchId, metadata);
85
+ await this.store.updateBranch(branchId, { ...metadata, modifiedAt: Date.now() });
71
86
  }
72
87
  /**
73
- * Closes a branch, marking it as merged or deleted.
74
- * @param branchId - The ID of the branch to close.
75
- * @param status - The status to set for the branch.
88
+ * Deletes a branch, replacing the record with a tombstone.
76
89
  */
77
- async closeBranch(branchId, status) {
78
- await this.store.updateBranch(branchId, { status: status ?? "closed" });
90
+ async deleteBranch(branchId) {
91
+ await this.store.deleteBranch(branchId);
79
92
  }
80
93
  /**
81
94
  * Merges changes from a branch back into its source document.
95
+ *
96
+ * Supports multiple merges — the branch stays open and `lastMergedRev` tracks
97
+ * which branch revision was last merged. Subsequent merges only pick up new changes.
98
+ *
99
+ * All merge changes use `batchId: branchId` so that `commitChanges` never transforms
100
+ * branch changes against each other (they share the same causal context).
101
+ *
82
102
  * @param branchId - The ID of the branch document to merge.
83
103
  * @returns The server commit change(s) applied to the source document.
84
104
  * @throws Error if branch not found, already closed/merged, or merge fails.
85
105
  */
86
106
  async mergeBranch(branchId) {
87
107
  const branch = await this.store.loadBranch(branchId);
88
- assertBranchOpenForMerge(branch, branchId);
108
+ assertBranchExists(branch, branchId);
89
109
  const sourceDocId = branch.docId;
90
110
  const branchStartRevOnSource = branch.branchedAtRev;
91
- const branchChanges = await this.store.listChanges(branchId, {});
111
+ const startAfter = branch.lastMergedRev ?? (branch.contentStartRev ?? 2) - 1;
112
+ const branchChanges = await this.store.listChanges(branchId, { startAfter });
92
113
  if (branchChanges.length === 0) {
93
- console.log(`Branch ${branchId} has no changes to merge.`);
94
- await this.closeBranch(branchId, "merged");
95
114
  return [];
96
115
  }
97
- const sourceChanges = await this.store.listChanges(sourceDocId, {
98
- startAfter: branchStartRevOnSource
99
- });
100
- const canFastForward = sourceChanges.length === 0;
116
+ const lastBranchRev = branchChanges[branchChanges.length - 1].rev;
101
117
  const branchVersions = await this.store.listVersions(branchId, { origin: "main" });
102
- const versionOrigin = canFastForward ? "main" : "branch";
103
118
  let lastVersionId;
104
119
  for (const v of branchVersions) {
105
120
  const newVersionMetadata = createVersionMetadata({
106
121
  ...v,
107
- origin: versionOrigin,
122
+ origin: "branch",
108
123
  startRev: branchStartRevOnSource,
109
124
  groupId: branchId,
110
125
  branchName: branch.name,
111
- // Keep branchName for traceability
112
126
  parentId: lastVersionId
113
127
  });
114
128
  const changes = await this.store.loadVersionChanges?.(branchId, v.id);
115
129
  await this.store.createVersion(sourceDocId, newVersionMetadata, changes);
116
130
  lastVersionId = newVersionMetadata.id;
117
131
  }
132
+ const changesToCommit = branchChanges.map((c, i) => ({
133
+ ...c,
134
+ baseRev: branchStartRevOnSource,
135
+ rev: branchStartRevOnSource + i + 1,
136
+ batchId: branchId
137
+ }));
118
138
  const committedMergeChanges = await wrapMergeCommit(branchId, sourceDocId, async () => {
119
- if (canFastForward) {
120
- const adjustedChanges = branchChanges.map((c) => ({
121
- ...c,
122
- baseRev: branchStartRevOnSource,
123
- rev: void 0
124
- // Let commitChanges assign sequential revs
125
- }));
126
- return (await this.patchesServer.commitChanges(sourceDocId, adjustedChanges)).changes;
127
- } else {
128
- const rev = branchStartRevOnSource + branchChanges.length;
129
- const flattenedChange = createChange(
130
- branchStartRevOnSource,
131
- rev,
132
- branchChanges.flatMap((c) => c.ops)
133
- );
134
- let changesToCommit = [flattenedChange];
135
- if (this.maxPayloadBytes) {
136
- changesToCommit = breakChanges(changesToCommit, this.maxPayloadBytes);
137
- }
138
- return (await this.patchesServer.commitChanges(sourceDocId, changesToCommit)).changes;
139
- }
139
+ return (await this.patchesServer.commitChanges(sourceDocId, changesToCommit)).changes;
140
140
  });
141
- await this.closeBranch(branchId, "merged");
141
+ const currentBranch = await this.store.loadBranch(branchId);
142
+ const effectiveLastMergedRev = Math.max(lastBranchRev, currentBranch?.lastMergedRev ?? 0);
143
+ await this.store.updateBranch(branchId, { lastMergedRev: effectiveLastMergedRev, modifiedAt: Date.now() });
142
144
  return committedMergeChanges;
143
145
  }
144
146
  }
@@ -1,5 +1,5 @@
1
1
  import { ApiDefinition } from '../net/protocol/JSONRPCServer.js';
2
- import { EditableBranchMetadata, Branch, BranchStatus } from '../types.js';
2
+ import { EditableBranchMetadata, Branch, CreateBranchMetadata } from '../types.js';
3
3
  import 'easy-signal';
4
4
  import '../net/websocket/AuthorizationProvider.js';
5
5
  import './types.js';
@@ -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?: CreateBranchMetadata | EditableBranchMetadata): Branch;
43
44
  /**
44
45
  * Store interface for branch loading.
45
46
  */
@@ -54,12 +55,12 @@ interface BranchLoader {
54
55
  */
55
56
  declare function assertNotABranch(store: BranchLoader, docId: string): Promise<void>;
56
57
  /**
57
- * Validates that a branch exists and is open for merging.
58
+ * Validates that a branch exists.
58
59
  * @param branch - The branch to validate (may be null).
59
60
  * @param branchId - The branch ID (for error messages).
60
- * @throws Error if branch not found or not open.
61
+ * @throws Error if branch not found.
61
62
  */
62
- declare function assertBranchOpenForMerge(branch: Branch | null, branchId: string): asserts branch is Branch;
63
+ declare function assertBranchExists(branch: Branch | null, branchId: string): asserts branch is Branch;
63
64
  /**
64
65
  * Wraps a merge commit operation with standard error handling.
65
66
  * @param branchId - The branch being merged.
@@ -69,14 +70,5 @@ declare function assertBranchOpenForMerge(branch: Branch | null, branchId: strin
69
70
  * @throws Error with standardized message if commit fails.
70
71
  */
71
72
  declare function wrapMergeCommit<T>(branchId: string, sourceDocId: string, commitFn: () => Promise<T>): Promise<T>;
72
- /**
73
- * Standard close branch operation.
74
- * @param store - Store with updateBranch capability.
75
- * @param branchId - The branch to close.
76
- * @param status - The status to set (defaults to 'closed').
77
- */
78
- declare function closeBranch(store: {
79
- updateBranch(branchId: string, updates: Partial<Pick<Branch, 'status' | 'name'>>): Promise<void>;
80
- }, branchId: string, status?: Exclude<BranchStatus, 'open'> | null): Promise<void>;
81
73
 
82
- export { type BranchIdGenerator, type BranchLoader, assertBranchMetadata, assertBranchOpenForMerge, assertNotABranch, branchManagerApi, closeBranch, createBranchRecord, generateBranchId, wrapMergeCommit };
74
+ export { type BranchIdGenerator, type BranchLoader, assertBranchExists, assertBranchMetadata, assertNotABranch, branchManagerApi, createBranchRecord, generateBranchId, wrapMergeCommit };
@@ -4,10 +4,19 @@ const branchManagerApi = {
4
4
  listBranches: "read",
5
5
  createBranch: "write",
6
6
  updateBranch: "write",
7
- closeBranch: "write",
7
+ deleteBranch: "write",
8
8
  mergeBranch: "write"
9
9
  };
10
- const nonModifiableBranchFields = /* @__PURE__ */ new Set(["id", "docId", "branchedAtRev", "createdAt", "status"]);
10
+ const nonModifiableBranchFields = /* @__PURE__ */ new Set([
11
+ "id",
12
+ "docId",
13
+ "branchedAtRev",
14
+ "createdAt",
15
+ "modifiedAt",
16
+ "contentStartRev",
17
+ "pendingOp",
18
+ "deleted"
19
+ ]);
11
20
  function assertBranchMetadata(metadata) {
12
21
  if (!metadata) return;
13
22
  for (const key in metadata) {
@@ -19,14 +28,16 @@ function assertBranchMetadata(metadata) {
19
28
  async function generateBranchId(store, docId) {
20
29
  return store.createBranchId ? await Promise.resolve(store.createBranchId(docId)) : createId(22);
21
30
  }
22
- function createBranchRecord(branchDocId, sourceDocId, branchedAtRev, metadata) {
31
+ function createBranchRecord(branchDocId, sourceDocId, branchedAtRev, contentStartRev, metadata) {
32
+ const now = Date.now();
23
33
  return {
24
34
  ...metadata,
25
35
  id: branchDocId,
26
36
  docId: sourceDocId,
27
37
  branchedAtRev,
28
- createdAt: Date.now(),
29
- status: "open"
38
+ contentStartRev,
39
+ createdAt: now,
40
+ modifiedAt: now
30
41
  };
31
42
  }
32
43
  async function assertNotABranch(store, docId) {
@@ -35,13 +46,10 @@ async function assertNotABranch(store, docId) {
35
46
  throw new Error("Cannot create a branch from another branch.");
36
47
  }
37
48
  }
38
- function assertBranchOpenForMerge(branch, branchId) {
49
+ function assertBranchExists(branch, branchId) {
39
50
  if (!branch) {
40
51
  throw new Error(`Branch with ID ${branchId} not found.`);
41
52
  }
42
- if (branch.status !== "open") {
43
- throw new Error(`Branch ${branchId} is not open (status: ${branch.status}). Cannot merge.`);
44
- }
45
53
  }
46
54
  async function wrapMergeCommit(branchId, sourceDocId, commitFn) {
47
55
  try {
@@ -51,15 +59,11 @@ async function wrapMergeCommit(branchId, sourceDocId, commitFn) {
51
59
  throw new Error(`Merge failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
52
60
  }
53
61
  }
54
- async function closeBranch(store, branchId, status) {
55
- await store.updateBranch(branchId, { status: status ?? "closed" });
56
- }
57
62
  export {
63
+ assertBranchExists,
58
64
  assertBranchMetadata,
59
- assertBranchOpenForMerge,
60
65
  assertNotABranch,
61
66
  branchManagerApi,
62
- closeBranch,
63
67
  createBranchRecord,
64
68
  generateBranchId,
65
69
  wrapMergeCommit
@@ -9,7 +9,7 @@ export { buildVersionState, getBaseStateBeforeVersion } from '../algorithms/ot/s
9
9
  export { concatStreams, jsonReadable, parseVersionState, readStreamAsString } from './jsonReadable.js';
10
10
  export { blockable, blockableResponse, blocking, releaseConcurrency, singleInvocation } from '../utils/concurrency.js';
11
11
  export { RevConflictError } from './RevConflictError.js';
12
- export { BranchIdGenerator, BranchLoader, assertBranchMetadata, assertBranchOpenForMerge, assertNotABranch, branchManagerApi, createBranchRecord, generateBranchId, wrapMergeCommit } from './branchUtils.js';
12
+ export { BranchIdGenerator, BranchLoader, assertBranchExists, assertBranchMetadata, assertNotABranch, branchManagerApi, createBranchRecord, generateBranchId, wrapMergeCommit } from './branchUtils.js';
13
13
  export { CompressedStoreBackend } from './CompressedStoreBackend.js';
14
14
  export { createTombstoneIfSupported, isTombstoneStore, removeTombstoneIfExists } from './tombstone.js';
15
15
  export { assertVersionMetadata } from './utils.js';
@@ -11,7 +11,7 @@ import { blockable, blockableResponse, blocking, releaseConcurrency, singleInvoc
11
11
  import { RevConflictError } from "./RevConflictError.js";
12
12
  import {
13
13
  assertBranchMetadata,
14
- assertBranchOpenForMerge,
14
+ assertBranchExists,
15
15
  assertNotABranch,
16
16
  branchManagerApi,
17
17
  createBranchRecord,
@@ -30,8 +30,8 @@ export {
30
30
  OTServer,
31
31
  PatchesHistoryManager,
32
32
  RevConflictError,
33
+ assertBranchExists,
33
34
  assertBranchMetadata,
34
- assertBranchOpenForMerge,
35
35
  assertNotABranch,
36
36
  assertVersionMetadata,
37
37
  blockable,
@@ -1,5 +1,5 @@
1
1
  import { JSONPatchOp } from '../json-patch/types.js';
2
- import { DocumentTombstone, VersionMetadata, Change, ListVersionsOptions, EditableVersionMetadata, ListChangesOptions, Branch } from '../types.js';
2
+ import { DocumentTombstone, VersionMetadata, Change, ListVersionsOptions, EditableVersionMetadata, ListChangesOptions, ListBranchesOptions, Branch } from '../types.js';
3
3
  import '../json-patch/JSONPatch.js';
4
4
  import '@dabble/delta';
5
5
 
@@ -150,13 +150,19 @@ interface BranchingStoreBackend {
150
150
  */
151
151
  createBranchId?(docId: string): Promise<string> | string;
152
152
  /** Lists metadata records for branches originating from a document. */
153
- listBranches(docId: string): Promise<Branch[]>;
153
+ listBranches(docId: string, options?: ListBranchesOptions): Promise<Branch[]>;
154
154
  /** Loads the metadata record for a specific branch ID. */
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
+ /**
161
+ * Replaces a branch record with a tombstone containing only `id`, `docId`, `modifiedAt`,
162
+ * and `deleted: true`. Tombstones are returned by `listBranches` when `since` is provided
163
+ * so that clients can clean up their local cache.
164
+ */
165
+ deleteBranch(branchId: string): Promise<void>;
160
166
  }
161
167
 
162
168
  export type { BranchingStoreBackend, LWWStoreBackend, ListFieldsOptions, OTStoreBackend, ServerStoreBackend, SnapshotResult, TombstoneStoreBackend, VersioningStoreBackend };
@@ -10,6 +10,7 @@ import '../client/ClientAlgorithm.js';
10
10
  import '../BaseDoc-BT18xPxU.js';
11
11
  import '../client/PatchesStore.js';
12
12
  import '../algorithms/ot/shared/changeBatching.js';
13
+ import '../client/BranchClientStore.js';
13
14
  import '../net/PatchesConnection.js';
14
15
  import '../net/protocol/types.js';
15
16
  import '../net/protocol/JSONRPCClient.js';
@@ -15,6 +15,7 @@ import '../BaseDoc-BT18xPxU.js';
15
15
  import '../client/PatchesStore.js';
16
16
  import '../net/PatchesSync.js';
17
17
  import '../algorithms/ot/shared/changeBatching.js';
18
+ import '../client/BranchClientStore.js';
18
19
  import '../net/PatchesConnection.js';
19
20
  import '../net/protocol/types.js';
20
21
  import '../net/protocol/JSONRPCClient.js';
package/dist/types.d.ts CHANGED
@@ -81,8 +81,6 @@ interface DocSyncState {
81
81
  syncError?: Error;
82
82
  isLoaded: boolean;
83
83
  }
84
- /** Status options for a branch */
85
- type BranchStatus = 'open' | 'closed' | 'merged' | 'archived' | 'abandoned';
86
84
  interface Branch {
87
85
  /** The ID of the branch document. */
88
86
  id: string;
@@ -92,14 +90,53 @@ interface Branch {
92
90
  branchedAtRev: number;
93
91
  /** Unix timestamp in milliseconds when the branch was created. */
94
92
  createdAt: number;
93
+ /** Unix timestamp in milliseconds when the branch was last modified. Updated on metadata changes. */
94
+ modifiedAt: number;
95
95
  /** Optional user-friendly name for the branch. */
96
96
  name?: string;
97
- /** Current status of the branch. */
98
- status: BranchStatus;
97
+ /**
98
+ * The first revision on the branch that contains user content (after initialization changes).
99
+ * Initialization changes (e.g. the root-replace that seeds the branch with source state)
100
+ * are at revisions < contentStartRev and are skipped during merge.
101
+ * Typically 2 for a single-change initialization; higher when the initial state is split
102
+ * across multiple changes due to size limits.
103
+ */
104
+ contentStartRev: number;
105
+ /**
106
+ * The branch-side revision through which changes have already been merged.
107
+ * Set after each merge so subsequent merges only pick up new changes.
108
+ * Undefined means the branch has never been merged.
109
+ */
110
+ lastMergedRev?: number;
111
+ /** The pending operation to sync to the server. Set by BranchClientStore methods. */
112
+ pendingOp?: 'create' | 'update' | 'delete';
113
+ /** True when this branch has been deleted. Stored as a tombstone for incremental sync. */
114
+ deleted?: true;
99
115
  /** Optional arbitrary metadata associated with the branch record. */
100
116
  [metadata: string]: any;
101
117
  }
102
- type EditableBranchMetadata = Disallowed<Branch, 'id' | 'docId' | 'branchedAtRev' | 'createdAt' | 'status'>;
118
+ type EditableBranchMetadata = Disallowed<Branch, 'id' | 'docId' | 'branchedAtRev' | 'createdAt' | 'modifiedAt' | 'contentStartRev' | 'pendingOp' | 'deleted'>;
119
+ /**
120
+ * Metadata for creating a new branch.
121
+ * Allows `id` and `contentStartRev` in addition to the fields allowed by `EditableBranchMetadata`.
122
+ * - `id`: Client-provided branch document ID. Required for offline branch creation.
123
+ * - `contentStartRev`: The first revision of user content. Set by the client when creating
124
+ * initial changes offline (the server uses this to know where user content begins during merge).
125
+ */
126
+ type CreateBranchMetadata = Omit<Disallowed<Branch, 'docId' | 'branchedAtRev' | 'createdAt' | 'modifiedAt' | 'pendingOp' | 'deleted'>, 'contentStartRev'> & {
127
+ contentStartRev?: number;
128
+ };
129
+ /**
130
+ * Options for listing branches.
131
+ */
132
+ interface ListBranchesOptions {
133
+ /**
134
+ * Only return branches modified after this timestamp (Unix ms).
135
+ * Enables incremental sync: after the initial full list, subsequent calls can pass the
136
+ * most recent `modifiedAt` value to fetch only updates.
137
+ */
138
+ since?: number;
139
+ }
103
140
  /**
104
141
  * Represents a tombstone for a deleted document.
105
142
  * Tombstones persist after deletion to inform late-connecting clients
@@ -239,4 +276,4 @@ type PathProxy<T = any> = IsAny<T> extends true ? DeepPathProxy : {
239
276
  */
240
277
  type ChangeMutator<T> = (patch: JSONPatch, root: PathProxy<T>) => void;
241
278
 
242
- export type { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, CommitChangesOptions, DeleteDocOptions, DocSyncState, DocSyncStatus, DocumentTombstone, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, VersionMetadata };
279
+ export type { Branch, Change, ChangeInput, ChangeMutator, CommitChangesOptions, CreateBranchMetadata, DeleteDocOptions, DocSyncState, DocSyncStatus, DocumentTombstone, EditableBranchMetadata, EditableVersionMetadata, ListBranchesOptions, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, VersionMetadata };
@@ -15,6 +15,7 @@ import '../BaseDoc-BT18xPxU.js';
15
15
  import '../client/PatchesStore.js';
16
16
  import '../net/PatchesSync.js';
17
17
  import '../algorithms/ot/shared/changeBatching.js';
18
+ import '../client/BranchClientStore.js';
18
19
  import '../net/PatchesConnection.js';
19
20
  import '../net/protocol/types.js';
20
21
  import '../net/protocol/JSONRPCClient.js';
@@ -10,6 +10,7 @@ import '../client/ClientAlgorithm.js';
10
10
  import '../BaseDoc-BT18xPxU.js';
11
11
  import '../client/PatchesStore.js';
12
12
  import '../algorithms/ot/shared/changeBatching.js';
13
+ import '../client/BranchClientStore.js';
13
14
  import '../net/PatchesConnection.js';
14
15
  import '../net/protocol/types.js';
15
16
  import '../net/protocol/JSONRPCClient.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/patches",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
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": {