cojson 0.18.6 → 0.18.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.
Files changed (86) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/coValueContentMessage.d.ts +2 -0
  4. package/dist/coValueContentMessage.d.ts.map +1 -1
  5. package/dist/coValueContentMessage.js +7 -0
  6. package/dist/coValueContentMessage.js.map +1 -1
  7. package/dist/coValueCore/SessionMap.d.ts +2 -2
  8. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  9. package/dist/coValueCore/SessionMap.js +2 -4
  10. package/dist/coValueCore/SessionMap.js.map +1 -1
  11. package/dist/coValueCore/branching.d.ts +31 -9
  12. package/dist/coValueCore/branching.d.ts.map +1 -1
  13. package/dist/coValueCore/branching.js +50 -100
  14. package/dist/coValueCore/branching.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +12 -8
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +93 -23
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +4 -2
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +6 -4
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/coList.d.ts.map +1 -1
  24. package/dist/coValues/coList.js +10 -1
  25. package/dist/coValues/coList.js.map +1 -1
  26. package/dist/coValues/coMap.d.ts +2 -2
  27. package/dist/coValues/coMap.d.ts.map +1 -1
  28. package/dist/coValues/coMap.js +8 -8
  29. package/dist/coValues/coMap.js.map +1 -1
  30. package/dist/coValues/group.d.ts.map +1 -1
  31. package/dist/coValues/group.js +14 -1
  32. package/dist/coValues/group.js.map +1 -1
  33. package/dist/config.d.ts +6 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +8 -0
  36. package/dist/config.js.map +1 -1
  37. package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
  38. package/dist/crypto/PureJSCrypto.js +14 -6
  39. package/dist/crypto/PureJSCrypto.js.map +1 -1
  40. package/dist/exports.d.ts +3 -2
  41. package/dist/exports.d.ts.map +1 -1
  42. package/dist/exports.js +2 -2
  43. package/dist/exports.js.map +1 -1
  44. package/dist/localNode.d.ts +1 -0
  45. package/dist/localNode.d.ts.map +1 -1
  46. package/dist/localNode.js +10 -2
  47. package/dist/localNode.js.map +1 -1
  48. package/dist/storage/storageAsync.d.ts.map +1 -1
  49. package/dist/storage/storageAsync.js.map +1 -1
  50. package/dist/sync.d.ts +3 -3
  51. package/dist/sync.d.ts.map +1 -1
  52. package/dist/sync.js +29 -19
  53. package/dist/sync.js.map +1 -1
  54. package/dist/tests/branching.test.js +107 -9
  55. package/dist/tests/branching.test.js.map +1 -1
  56. package/dist/tests/coValueCore.test.js +45 -1
  57. package/dist/tests/coValueCore.test.js.map +1 -1
  58. package/dist/tests/sync.content.test.d.ts +2 -0
  59. package/dist/tests/sync.content.test.d.ts.map +1 -0
  60. package/dist/tests/sync.content.test.js +120 -0
  61. package/dist/tests/sync.content.test.js.map +1 -0
  62. package/dist/tests/sync.load.test.js +15 -2
  63. package/dist/tests/sync.load.test.js.map +1 -1
  64. package/dist/tests/sync.storage.test.js +1 -1
  65. package/dist/tests/sync.upload.test.js +2 -2
  66. package/package.json +2 -2
  67. package/src/coValueContentMessage.ts +13 -0
  68. package/src/coValueCore/SessionMap.ts +2 -2
  69. package/src/coValueCore/branching.ts +94 -149
  70. package/src/coValueCore/coValueCore.ts +121 -27
  71. package/src/coValueCore/verifiedState.ts +8 -0
  72. package/src/coValues/coList.ts +12 -1
  73. package/src/coValues/coMap.ts +10 -12
  74. package/src/coValues/group.ts +14 -1
  75. package/src/config.ts +9 -0
  76. package/src/crypto/PureJSCrypto.ts +25 -13
  77. package/src/exports.ts +7 -1
  78. package/src/localNode.ts +12 -2
  79. package/src/storage/storageAsync.ts +0 -1
  80. package/src/sync.ts +37 -33
  81. package/src/tests/branching.test.ts +158 -9
  82. package/src/tests/coValueCore.test.ts +62 -2
  83. package/src/tests/sync.content.test.ts +153 -0
  84. package/src/tests/sync.load.test.ts +19 -2
  85. package/src/tests/sync.storage.test.ts +1 -1
  86. package/src/tests/sync.upload.test.ts +2 -2
@@ -1,9 +1,41 @@
1
- import type { CoValueCore, JsonValue } from "../exports.js";
2
- import type { RawCoID, SessionID, TransactionID } from "../ids.js";
1
+ import type { CoValueCore } from "../exports.js";
2
+ import type { RawCoID, SessionID } from "../ids.js";
3
3
  import { type AvailableCoValueCore, idforHeader } from "./coValueCore.js";
4
4
  import type { CoValueHeader } from "./verifiedState.js";
5
5
  import type { CoValueKnownState } from "../sync.js";
6
- import type { ListOpPayload, OpID } from "../coValues/coList.js";
6
+
7
+ /**
8
+ * Commit to identify the starting point of the branch
9
+ *
10
+ * In case of clonflicts, the first commit of this kind is considered the source of truth
11
+ */
12
+ export type BranchStartCommit = {
13
+ from: CoValueKnownState["sessions"];
14
+ };
15
+
16
+ /**
17
+ * Commit that tracks a branch creation
18
+ */
19
+ export type BranchPointerCommit = {
20
+ branch: string;
21
+ ownerId?: RawCoID;
22
+ };
23
+
24
+ /**
25
+ * Meta information attached to each merged transaction to retrieve the original transaction ID
26
+ */
27
+ export type MergedTransactionMetadata = {
28
+ mi: number; // Transaction index and marker of a merge commit
29
+ s?: SessionID;
30
+ b?: RawCoID;
31
+ };
32
+
33
+ /**
34
+ * Merge commit located in a branch to track how many transactions have already been merged
35
+ */
36
+ export type MergeCommit = {
37
+ merged: CoValueKnownState["sessions"];
38
+ };
7
39
 
8
40
  export function getBranchHeader({
9
41
  type,
@@ -49,30 +81,37 @@ export function getBranchId(
49
81
  );
50
82
  }
51
83
 
52
- if (!ownerId) {
53
- const header = coValue.verified.header;
54
-
55
- // Group and account coValues can't have branches, so we return the source id
56
- if (header.ruleset.type !== "ownedByGroup") {
57
- return coValue.id;
58
- }
84
+ const currentOwnerId = ownerId ?? getBranchOwnerId(coValue);
59
85
 
60
- ownerId = header.ruleset.group;
86
+ if (!currentOwnerId) {
87
+ return coValue.id;
61
88
  }
62
89
 
63
90
  const header = getBranchHeader({
64
91
  type: coValue.verified.header.type,
65
92
  branchName: name,
66
- ownerId,
93
+ ownerId: currentOwnerId,
67
94
  sourceId: coValue.id,
68
95
  });
69
96
 
70
97
  return idforHeader(header, coValue.node.crypto);
71
98
  }
72
99
 
73
- export type BranchCommit = {
74
- branch: CoValueKnownState["sessions"];
75
- };
100
+ export function getBranchOwnerId(coValue: CoValueCore) {
101
+ if (!coValue.verified) {
102
+ throw new Error(
103
+ "CoValueCore: getBranchOwnerId called on coValue without verified state",
104
+ );
105
+ }
106
+
107
+ const header = coValue.verified.header;
108
+
109
+ if (header.ruleset.type !== "ownedByGroup") {
110
+ return undefined;
111
+ }
112
+
113
+ return header.ruleset.group;
114
+ }
76
115
 
77
116
  /**
78
117
  * Given a coValue, a branch name and an owner id, creates a new branch CoValue
@@ -88,32 +127,34 @@ export function createBranch(
88
127
  );
89
128
  }
90
129
 
91
- if (!ownerId) {
92
- const header = coValue.verified.header;
93
-
94
- // Group and account coValues can't have branches, so we return the source coValue
95
- if (header.ruleset.type !== "ownedByGroup") {
96
- return coValue;
97
- }
130
+ const branchOwnerId = ownerId ?? getBranchOwnerId(coValue);
98
131
 
99
- ownerId = header.ruleset.group;
132
+ if (!branchOwnerId) {
133
+ return coValue;
100
134
  }
101
135
 
102
136
  const header = getBranchHeader({
103
137
  type: coValue.verified.header.type,
104
138
  branchName: name,
105
- ownerId,
139
+ ownerId: branchOwnerId,
106
140
  sourceId: coValue.id,
107
141
  });
108
142
 
109
- const value = coValue.node.createCoValue(header);
143
+ const branch = coValue.node.createCoValue(header);
144
+ const sessions = { ...coValue.knownState().sessions };
110
145
 
111
146
  // Create a branch commit to identify the starting point of the branch
112
- value.makeTransaction([], "private", {
113
- branch: coValue.knownState().sessions,
114
- } satisfies BranchCommit);
147
+ branch.makeTransaction([], "private", {
148
+ from: sessions,
149
+ } satisfies BranchStartCommit);
150
+
151
+ // Create a branch pointer, to identify that we created a branch
152
+ coValue.makeTransaction([], "private", {
153
+ branch: name,
154
+ ownerId,
155
+ } satisfies BranchPointerCommit);
115
156
 
116
- return value;
157
+ return branch;
117
158
  }
118
159
 
119
160
  /**
@@ -141,15 +182,6 @@ export function getBranchSource(
141
182
  return source;
142
183
  }
143
184
 
144
- export type MergeCommit = {
145
- // The point where the branch was merged
146
- merge: CoValueKnownState["sessions"];
147
- // The id of the branch that was merged
148
- id: RawCoID;
149
- // The number of transactions that were merged, will be used in the future to handle the edits history properly
150
- count: number;
151
- };
152
-
153
185
  /**
154
186
  * Given a branch coValue, merges the branch into the source coValue
155
187
  */
@@ -164,12 +196,6 @@ export function mergeBranch(branch: CoValueCore): CoValueCore {
164
196
  return branch;
165
197
  }
166
198
 
167
- const sourceId = branch.getCurrentBranchSourceId();
168
-
169
- if (!sourceId) {
170
- throw new Error("CoValueCore: mergeBranch called on a non-branch coValue");
171
- }
172
-
173
199
  const target = getBranchSource(branch);
174
200
 
175
201
  if (!target) {
@@ -178,13 +204,9 @@ export function mergeBranch(branch: CoValueCore): CoValueCore {
178
204
 
179
205
  // Look for previous merge commits, to see which transactions needs to be merged
180
206
  // Done mostly for performance reasons, as we could merge all the transactions every time and nothing would change
181
- const mergedTransactions = target.mergeCommits.reduce(
182
- (acc, { commit }) => {
183
- if (commit.id !== branch.id) {
184
- return acc;
185
- }
186
-
187
- for (const [sessionID, count] of Object.entries(commit.merge) as [
207
+ const mergedTransactions = branch.getMergeCommits().reduce(
208
+ (acc, { merged }) => {
209
+ for (const [sessionID, count] of Object.entries(merged) as [
188
210
  SessionID,
189
211
  number,
190
212
  ][]) {
@@ -210,111 +232,34 @@ export function mergeBranch(branch: CoValueCore): CoValueCore {
210
232
  return target;
211
233
  }
212
234
 
213
- // Create a merge commit to identify the merge point
214
- target.makeTransaction([], "private", {
215
- merge: { ...branch.knownState().sessions },
216
- id: branch.id,
217
- count: branchValidTransactions.length,
218
- } satisfies MergeCommit);
219
-
220
- const currentSessionID = target.node.currentSessionID;
235
+ // We do track in the meta information the original txID to make sure that
236
+ // the CoList opid still point to the correct transaction
237
+ // To reduce the cost of the meta we skip the repeated information
238
+ let lastSessionId: string | undefined = undefined;
239
+ let lastBranchId: string | undefined = undefined;
221
240
 
222
- if (
223
- target.verified.header.type === "colist" ||
224
- target.verified.header.type === "coplaintext"
225
- ) {
226
- const mapping: Record<`${SessionID}:${number}`, number> = {};
241
+ for (const tx of branchValidTransactions) {
242
+ const mergeMeta: MergedTransactionMetadata = {
243
+ mi: tx.txID.txIndex,
244
+ };
227
245
 
228
- const session = target.verified.sessions.get(currentSessionID);
229
- let txIdx = session ? session.transactions.length : 0;
230
-
231
- // Create a mapping from the branch transactions to the target transactions
232
- for (const { txID } of branchValidTransactions) {
233
- mapping[`${txID.sessionID}:${txID.txIndex}`] = txIdx;
234
- txIdx++;
246
+ if (lastSessionId !== tx.txID.sessionID) {
247
+ mergeMeta.s = tx.txID.sessionID;
235
248
  }
236
249
 
237
- for (const { tx, changes } of branchValidTransactions) {
238
- target.makeTransaction(
239
- mapCoListChangesToTarget(
240
- changes as ListOpPayload<JsonValue>[],
241
- currentSessionID,
242
- mapping,
243
- ),
244
- tx.privacy,
245
- );
246
- }
247
- } else {
248
- for (const { tx, changes } of branchValidTransactions) {
249
- target.makeTransaction(changes, tx.privacy);
250
+ if (lastBranchId !== tx.txID.branch) {
251
+ mergeMeta.b = tx.txID.branch;
250
252
  }
251
- }
252
253
 
253
- return target;
254
- }
255
-
256
- /**
257
- * Given a list of changes, maps the opIDs to the target transactions
258
- */
259
- function mapCoListChangesToTarget(
260
- changes: ListOpPayload<JsonValue>[],
261
- currentSessionID: SessionID,
262
- mapping: Record<`${SessionID}:${number}`, number>,
263
- ) {
264
- return changes.map((change) => {
265
- if (change.op === "app") {
266
- if (change.after === "start") {
267
- return change;
268
- }
269
-
270
- return {
271
- ...change,
272
- after: convertOpID(change.after, currentSessionID, mapping),
273
- };
274
- }
275
-
276
- if (change.op === "del") {
277
- return {
278
- ...change,
279
- insertion: convertOpID(change.insertion, currentSessionID, mapping),
280
- };
281
- }
282
-
283
- if (change.op === "pre") {
284
- if (change.before === "end") {
285
- return change;
286
- }
287
-
288
- return {
289
- ...change,
290
- before: convertOpID(change.before, currentSessionID, mapping),
291
- };
292
- }
293
-
294
- return change;
295
- });
296
- }
297
-
298
- function convertOpID(
299
- opID: OpID,
300
- sessionID: SessionID,
301
- mapping: Record<`${SessionID}:${number}`, number>,
302
- ) {
303
- // If the opID comes from the source branch, we don't need to map it
304
- if (!opID.branch) {
305
- return opID;
254
+ target.makeTransaction(tx.changes, tx.tx.privacy, mergeMeta, tx.madeAt);
255
+ lastSessionId = tx.txID.sessionID;
256
+ lastBranchId = tx.txID.branch;
306
257
  }
307
258
 
308
- const mappedIndex = mapping[`${opID.sessionID}:${opID.txIndex}`];
309
-
310
- // If the opID doesn't exist in the mapping, we don't need to map it
311
- if (mappedIndex === undefined) {
312
- return opID;
313
- }
259
+ // Track the merged transactions for the branch, so future merges will know which transactions have already been merged
260
+ branch.makeTransaction([], "private", {
261
+ merged: branch.knownState().sessions,
262
+ } satisfies MergeCommit);
314
263
 
315
- return {
316
- sessionID: sessionID,
317
- txIndex: mappedIndex,
318
- changeIdx: opID.changeIdx,
319
- };
264
+ return target;
320
265
  }
@@ -5,10 +5,10 @@ import type { RawCoValue } from "../coValue.js";
5
5
  import type { ControlledAccountOrAgent } from "../coValues/account.js";
6
6
  import type { RawGroup } from "../coValues/group.js";
7
7
  import { CO_VALUE_LOADING_CONFIG } from "../config.js";
8
+ import { validateTxSizeLimitInBytes } from "../coValueContentMessage.js";
8
9
  import { coreToCoValue } from "../coreToCoValue.js";
9
10
  import {
10
11
  CryptoProvider,
11
- Encrypted,
12
12
  Hash,
13
13
  KeyID,
14
14
  KeySecret,
@@ -17,7 +17,6 @@ import {
17
17
  } from "../crypto/crypto.js";
18
18
  import { AgentID, RawCoID, SessionID, TransactionID } from "../ids.js";
19
19
  import { JsonObject, JsonValue } from "../jsonValue.js";
20
- import { parseJSON, safeParseJSON } from "../jsonStringify.js";
21
20
  import { LocalNode, ResolveAccountAgentError } from "../localNode.js";
22
21
  import { logger } from "../logger.js";
23
22
  import { determineValidTransactions } from "../permissions.js";
@@ -29,10 +28,14 @@ import { CoValueHeader, Transaction, VerifiedState } from "./verifiedState.js";
29
28
  import { SessionMap } from "./SessionMap.js";
30
29
  import {
31
30
  MergeCommit,
31
+ BranchPointerCommit,
32
+ MergedTransactionMetadata,
32
33
  createBranch,
33
34
  getBranchId,
35
+ getBranchOwnerId,
34
36
  getBranchSource,
35
37
  mergeBranch,
38
+ BranchStartCommit,
36
39
  } from "./branching.js";
37
40
  import { type RawAccountID } from "../coValues/account.js";
38
41
  import { decodeTransactionChangesAndMeta } from "./decodeTransactionChangesAndMeta.js";
@@ -70,6 +73,9 @@ export type VerifiedTransaction = {
70
73
 
71
74
  // True if the meta information has been parsed and loaded in the CoValueCore
72
75
  hasMetaBeenParsed: boolean;
76
+
77
+ // The previous verified transaction for the same session
78
+ previous: VerifiedTransaction | undefined;
73
79
  };
74
80
 
75
81
  export type DecryptedTransaction = {
@@ -600,6 +606,7 @@ export class CoValueCore {
600
606
  changes: JsonValue[],
601
607
  privacy: "private" | "trusting",
602
608
  meta?: JsonObject,
609
+ madeAt?: number,
603
610
  ): boolean {
604
611
  if (!this.verified) {
605
612
  throw new Error(
@@ -607,6 +614,8 @@ export class CoValueCore {
607
614
  );
608
615
  }
609
616
 
617
+ validateTxSizeLimitInBytes(changes);
618
+
610
619
  // This is an ugly hack to get a unique but stable session ID for editing the current account
611
620
  const sessionID =
612
621
  this.verified.header.meta?.type === "account"
@@ -634,6 +643,7 @@ export class CoValueCore {
634
643
  keyID,
635
644
  keySecret,
636
645
  meta,
646
+ madeAt ?? Date.now(),
637
647
  );
638
648
  } else {
639
649
  result = this.verified.makeNewTrustingTransaction(
@@ -641,6 +651,7 @@ export class CoValueCore {
641
651
  signerAgent,
642
652
  changes,
643
653
  meta,
654
+ madeAt ?? Date.now(),
644
655
  );
645
656
  }
646
657
 
@@ -689,12 +700,13 @@ export class CoValueCore {
689
700
  }
690
701
 
691
702
  // The starting point of the branch, in case this CoValue is a branch
692
- branchStart:
693
- | { branch: CoValueKnownState["sessions"]; madeAt: number }
694
- | undefined;
703
+ branchStart: { from: BranchStartCommit["from"]; madeAt: number } | undefined;
695
704
 
696
705
  // The list of merge commits that have been made
697
- mergeCommits: { commit: MergeCommit; madeAt: number }[] = [];
706
+ mergeCommits: MergeCommit[] = [];
707
+ branches: BranchPointerCommit[] = [];
708
+ earliestTxMadeAt: number = Number.MAX_SAFE_INTEGER;
709
+ latestTxMadeAt: number = 0;
698
710
 
699
711
  // Reset the parsed transactions and branches, to validate them again from scratch when the group is updated
700
712
  resetParsedTransactions() {
@@ -710,6 +722,11 @@ export class CoValueCore {
710
722
  verifiedTransactions: VerifiedTransaction[] = [];
711
723
  private verifiedTransactionsKnownSessions: CoValueKnownState["sessions"] = {};
712
724
 
725
+ private lastVerifiedTransactionBySessionID: Record<
726
+ SessionID,
727
+ VerifiedTransaction
728
+ > = {};
729
+
713
730
  /**
714
731
  * Loads the new transaction from the SessionMap into verifiedTransactions as a VerifiedTransaction.
715
732
  *
@@ -752,7 +769,7 @@ export class CoValueCore {
752
769
  txIndex,
753
770
  };
754
771
 
755
- this.verifiedTransactions.push({
772
+ const verifiedTransaction = {
756
773
  author: accountOrAgentIDfromSessionID(sessionID),
757
774
  txID,
758
775
  madeAt: tx.madeAt,
@@ -764,7 +781,20 @@ export class CoValueCore {
764
781
  hasInvalidMeta: false,
765
782
  hasMetaBeenParsed: false,
766
783
  tx,
767
- });
784
+ previous: this.lastVerifiedTransactionBySessionID[sessionID],
785
+ };
786
+
787
+ if (verifiedTransaction.madeAt > this.latestTxMadeAt) {
788
+ this.latestTxMadeAt = verifiedTransaction.madeAt;
789
+ }
790
+
791
+ if (verifiedTransaction.madeAt < this.earliestTxMadeAt) {
792
+ this.earliestTxMadeAt = verifiedTransaction.madeAt;
793
+ }
794
+
795
+ this.verifiedTransactions.push(verifiedTransaction);
796
+ this.lastVerifiedTransactionBySessionID[sessionID] =
797
+ verifiedTransaction;
768
798
  });
769
799
 
770
800
  this.verifiedTransactionsKnownSessions[sessionID] =
@@ -793,23 +823,54 @@ export class CoValueCore {
793
823
 
794
824
  transaction.hasMetaBeenParsed = true;
795
825
 
796
- if (
797
- transaction.meta?.["branch"] &&
798
- (!this.branchStart || transaction.madeAt < this.branchStart.madeAt)
799
- ) {
800
- this.branchStart = {
801
- branch: transaction.meta.branch as CoValueKnownState["sessions"],
802
- madeAt: transaction.madeAt,
803
- };
826
+ // Branch related meta information
827
+ if (this.isBranch()) {
828
+ // Check if the transaction is a branch start
829
+ if ("from" in transaction.meta) {
830
+ if (!this.branchStart || transaction.madeAt < this.branchStart.madeAt) {
831
+ const commit = transaction.meta as BranchStartCommit;
832
+
833
+ this.branchStart = {
834
+ from: commit.from,
835
+ madeAt: transaction.madeAt,
836
+ };
837
+ }
838
+ }
839
+
840
+ // Check if the transaction is a merged checkpoint for a branch
841
+ if ("merged" in transaction.meta) {
842
+ const mergeCommit = transaction.meta as MergeCommit;
843
+ this.mergeCommits.push(mergeCommit);
844
+ }
804
845
  }
805
846
 
806
- if (transaction.meta?.["merge"]) {
807
- const mergeCommit = transaction.meta as MergeCommit;
847
+ // Check if the transaction is a branch pointer
848
+ if ("branch" in transaction.meta) {
849
+ const branch = transaction.meta as BranchPointerCommit;
808
850
 
809
- this.mergeCommits.push({
810
- commit: mergeCommit,
811
- madeAt: transaction.madeAt,
812
- });
851
+ this.branches.push(branch);
852
+ }
853
+
854
+ // Check if the transaction has been merged from a branch
855
+ if ("mi" in transaction.meta) {
856
+ const meta = transaction.meta as MergedTransactionMetadata;
857
+
858
+ // Check if the transaction is a merge commit
859
+ const previousTransaction = transaction.previous?.txID;
860
+ const sessionID = meta.s ?? previousTransaction?.sessionID;
861
+
862
+ if (sessionID) {
863
+ transaction.txID = {
864
+ sessionID,
865
+ txIndex: meta.mi,
866
+ branch: meta.b ?? previousTransaction?.branch,
867
+ };
868
+ } else {
869
+ logger.error("Merge commit without session ID", {
870
+ txID: transaction.txID,
871
+ prev: previousTransaction ?? null,
872
+ });
873
+ }
813
874
  }
814
875
  }
815
876
 
@@ -876,7 +937,9 @@ export class CoValueCore {
876
937
  const { txID } = transaction;
877
938
 
878
939
  const from = options?.from?.[txID.sessionID] ?? -1;
879
- const to = options?.to?.[txID.sessionID] ?? Infinity;
940
+
941
+ // Load the to filter index. Sessions that are not in the to filter will be skipped
942
+ const to = options?.to ? (options.to[txID.sessionID] ?? -1) : Infinity;
880
943
 
881
944
  // The txIndex starts at 0 and from/to are referring to the count of transactions
882
945
  if (from > txID.txIndex || to < txID.txIndex) {
@@ -889,7 +952,7 @@ export class CoValueCore {
889
952
  // If this is a branch, we load the valid transactions from the source
890
953
  if (source && this.branchStart && !options?.skipBranchSource) {
891
954
  const sourceTransactions = source.getValidTransactions({
892
- to: this.branchStart.branch,
955
+ to: this.branchStart.from,
893
956
  ignorePrivateTransactions: options?.ignorePrivateTransactions ?? false,
894
957
  knownTransactions: options?.knownTransactions,
895
958
  });
@@ -915,15 +978,46 @@ export class CoValueCore {
915
978
  }
916
979
 
917
980
  getCurrentBranchName() {
918
- return this.verified?.header.meta?.branch as string | undefined;
981
+ return this.verified?.branchName;
919
982
  }
920
983
 
921
984
  getCurrentBranchSourceId() {
922
- return this.verified?.header.meta?.source as RawCoID | undefined;
985
+ return this.verified?.branchSourceId;
923
986
  }
924
987
 
925
988
  isBranch() {
926
- return Boolean(this.getCurrentBranchSourceId());
989
+ return Boolean(this.verified?.branchSourceId);
990
+ }
991
+
992
+ hasBranch(name: string, ownerId?: RawCoID) {
993
+ // This function requires the meta information to be parsed, which might not be the case
994
+ // if the value content hasn't been loaded yet
995
+ this.parseNewTransactions(false);
996
+
997
+ const currentOwnerId = getBranchOwnerId(this);
998
+ return this.branches.some((item) => {
999
+ if (item.branch !== name) {
1000
+ return false;
1001
+ }
1002
+
1003
+ if (item.ownerId === ownerId) {
1004
+ return true;
1005
+ }
1006
+
1007
+ if (!ownerId) {
1008
+ return item.ownerId === currentOwnerId;
1009
+ }
1010
+
1011
+ if (!item.ownerId) {
1012
+ return ownerId === currentOwnerId;
1013
+ }
1014
+ });
1015
+ }
1016
+
1017
+ getMergeCommits() {
1018
+ this.parseNewTransactions(false);
1019
+
1020
+ return this.mergeCommits;
927
1021
  }
928
1022
 
929
1023
  getValidSortedTransactions(options?: {
@@ -59,6 +59,8 @@ export class VerifiedState {
59
59
  private _cachedNewContentSinceEmpty: NewContentMessage[] | undefined;
60
60
  private streamingKnownState?: CoValueKnownState["sessions"];
61
61
  public lastAccessed: number | undefined;
62
+ public branchSourceId?: RawCoID;
63
+ public branchName?: string;
62
64
 
63
65
  constructor(
64
66
  id: RawCoID,
@@ -74,6 +76,8 @@ export class VerifiedState {
74
76
  this.streamingKnownState = streamingKnownState
75
77
  ? { ...streamingKnownState }
76
78
  : undefined;
79
+ this.branchSourceId = header.meta?.source as RawCoID | undefined;
80
+ this.branchName = header.meta?.branch as string | undefined;
77
81
  }
78
82
 
79
83
  clone(): VerifiedState {
@@ -114,12 +118,14 @@ export class VerifiedState {
114
118
  signerAgent: ControlledAccountOrAgent,
115
119
  changes: JsonValue[],
116
120
  meta: JsonObject | undefined,
121
+ madeAt: number,
117
122
  ) {
118
123
  const result = this.sessions.makeNewTrustingTransaction(
119
124
  sessionID,
120
125
  signerAgent,
121
126
  changes,
122
127
  meta,
128
+ madeAt,
123
129
  );
124
130
 
125
131
  this._cachedNewContentSinceEmpty = undefined;
@@ -135,6 +141,7 @@ export class VerifiedState {
135
141
  keyID: KeyID,
136
142
  keySecret: KeySecret,
137
143
  meta: JsonObject | undefined,
144
+ madeAt: number,
138
145
  ) {
139
146
  const result = this.sessions.makeNewPrivateTransaction(
140
147
  sessionID,
@@ -143,6 +150,7 @@ export class VerifiedState {
143
150
  keyID,
144
151
  keySecret,
145
152
  meta,
153
+ madeAt,
146
154
  );
147
155
 
148
156
  this._cachedNewContentSinceEmpty = undefined;
@@ -138,7 +138,13 @@ export class RawCoList<
138
138
  sessionEntry[opID.txIndex] = txEntry;
139
139
  }
140
140
 
141
+ // Check if the change index already exists, may be the case of double merges
142
+ if (txEntry[opID.changeIdx]) {
143
+ return false;
144
+ }
145
+
141
146
  txEntry[opID.changeIdx] = value;
147
+ return true;
142
148
  }
143
149
 
144
150
  private isDeleted(opID: OpID) {
@@ -216,13 +222,18 @@ export class RawCoList<
216
222
  };
217
223
 
218
224
  if (change.op === "pre" || change.op === "app") {
219
- this.createInsertionsEntry(opID, {
225
+ const created = this.createInsertionsEntry(opID, {
220
226
  madeAt,
221
227
  predecessors: [],
222
228
  successors: [],
223
229
  change,
224
230
  });
225
231
 
232
+ // If the change index already exists, we don't need to process it again
233
+ if (!created) {
234
+ continue;
235
+ }
236
+
226
237
  if (change.op === "pre") {
227
238
  if (change.before === "end") {
228
239
  this.beforeEnd.push(opID);