cojson 0.17.14 → 0.18.1

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 (83) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +34 -0
  3. package/dist/coValueCore/branching.d.ts +36 -0
  4. package/dist/coValueCore/branching.d.ts.map +1 -0
  5. package/dist/coValueCore/branching.js +122 -0
  6. package/dist/coValueCore/branching.js.map +1 -0
  7. package/dist/coValueCore/coValueCore.d.ts +71 -5
  8. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore/coValueCore.js +162 -53
  10. package/dist/coValueCore/coValueCore.js.map +1 -1
  11. package/dist/coValueCore/decodeTransactionChangesAndMeta.d.ts +3 -0
  12. package/dist/coValueCore/decodeTransactionChangesAndMeta.d.ts.map +1 -0
  13. package/dist/coValueCore/decodeTransactionChangesAndMeta.js +59 -0
  14. package/dist/coValueCore/decodeTransactionChangesAndMeta.js.map +1 -0
  15. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  16. package/dist/coValues/account.d.ts +2 -1
  17. package/dist/coValues/account.d.ts.map +1 -1
  18. package/dist/coValues/account.js +6 -0
  19. package/dist/coValues/account.js.map +1 -1
  20. package/dist/coValues/coList.d.ts +3 -3
  21. package/dist/coValues/coList.d.ts.map +1 -1
  22. package/dist/coValues/coList.js +4 -7
  23. package/dist/coValues/coList.js.map +1 -1
  24. package/dist/coValues/coMap.d.ts +3 -3
  25. package/dist/coValues/coMap.d.ts.map +1 -1
  26. package/dist/coValues/coMap.js +6 -6
  27. package/dist/coValues/coMap.js.map +1 -1
  28. package/dist/coValues/coStream.d.ts +3 -3
  29. package/dist/coValues/coStream.d.ts.map +1 -1
  30. package/dist/coValues/coStream.js +4 -4
  31. package/dist/coValues/coStream.js.map +1 -1
  32. package/dist/ids.d.ts.map +1 -1
  33. package/dist/ids.js.map +1 -1
  34. package/dist/jsonStringify.d.ts +1 -0
  35. package/dist/jsonStringify.d.ts.map +1 -1
  36. package/dist/jsonStringify.js +8 -0
  37. package/dist/jsonStringify.js.map +1 -1
  38. package/dist/permissions.d.ts +2 -7
  39. package/dist/permissions.d.ts.map +1 -1
  40. package/dist/permissions.js +72 -70
  41. package/dist/permissions.js.map +1 -1
  42. package/dist/sync.d.ts +2 -1
  43. package/dist/sync.d.ts.map +1 -1
  44. package/dist/sync.js +16 -5
  45. package/dist/sync.js.map +1 -1
  46. package/dist/tests/account.test.js +20 -0
  47. package/dist/tests/account.test.js.map +1 -1
  48. package/dist/tests/branching.test.d.ts +2 -0
  49. package/dist/tests/branching.test.d.ts.map +1 -0
  50. package/dist/tests/branching.test.js +99 -0
  51. package/dist/tests/branching.test.js.map +1 -0
  52. package/dist/tests/coValueCore.test.js +2 -3
  53. package/dist/tests/coValueCore.test.js.map +1 -1
  54. package/dist/tests/sync.sharding.test.js +63 -0
  55. package/dist/tests/sync.sharding.test.js.map +1 -1
  56. package/dist/tests/sync.storage.test.js +1 -3
  57. package/dist/tests/sync.storage.test.js.map +1 -1
  58. package/dist/tests/sync.upload.test.js +1 -3
  59. package/dist/tests/sync.upload.test.js.map +1 -1
  60. package/dist/tests/testUtils.d.ts +1 -0
  61. package/dist/tests/testUtils.d.ts.map +1 -1
  62. package/dist/tests/testUtils.js +1 -1
  63. package/dist/tests/testUtils.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/coValueCore/branching.ts +198 -0
  66. package/src/coValueCore/coValueCore.ts +255 -72
  67. package/src/coValueCore/decodeTransactionChangesAndMeta.ts +81 -0
  68. package/src/coValueCore/verifiedState.ts +1 -1
  69. package/src/coValues/account.ts +11 -9
  70. package/src/coValues/coList.ts +8 -10
  71. package/src/coValues/coMap.ts +8 -11
  72. package/src/coValues/coStream.ts +7 -8
  73. package/src/ids.ts +4 -1
  74. package/src/jsonStringify.ts +8 -0
  75. package/src/permissions.ts +80 -89
  76. package/src/sync.ts +21 -6
  77. package/src/tests/account.test.ts +24 -0
  78. package/src/tests/branching.test.ts +141 -0
  79. package/src/tests/coValueCore.test.ts +2 -3
  80. package/src/tests/sync.sharding.test.ts +72 -0
  81. package/src/tests/sync.storage.test.ts +3 -3
  82. package/src/tests/sync.upload.test.ts +3 -3
  83. package/src/tests/testUtils.ts +2 -1
@@ -13,6 +13,7 @@ import { isAccountID } from "../typeUtils/isAccountID.js";
13
13
  import { isCoValue } from "../typeUtils/isCoValue.js";
14
14
  import { RawAccountID } from "./account.js";
15
15
  import { RawGroup } from "./group.js";
16
+ import { Transaction } from "../coValueCore/verifiedState.js";
16
17
 
17
18
  export type BinaryStreamInfo = {
18
19
  mimeType: string;
@@ -58,15 +59,18 @@ export class RawCoStreamView<
58
59
  [key: SessionID]: CoStreamItem<Item>[];
59
60
  };
60
61
  /** @internal */
61
- knownTransactions: CoValueKnownState["sessions"];
62
- totalValidTransactions = 0;
62
+ knownTransactions: Set<Transaction>;
63
63
  readonly _item!: Item;
64
64
 
65
+ get totalValidTransactions() {
66
+ return this.knownTransactions.size;
67
+ }
68
+
65
69
  constructor(core: AvailableCoValueCore) {
66
70
  this.id = core.id as CoID<this>;
67
71
  this.core = core;
68
72
  this.items = {};
69
- this.knownTransactions = {};
73
+ this.knownTransactions = new Set<Transaction>();
70
74
  this.processNewTransactions();
71
75
  }
72
76
 
@@ -113,7 +117,6 @@ export class RawCoStreamView<
113
117
  }
114
118
 
115
119
  for (const { txID, madeAt, changes } of newValidTransactions) {
116
- this.totalValidTransactions++;
117
120
  for (const changeUntyped of changes) {
118
121
  const change = changeUntyped as Item;
119
122
  let entries = this.items[txID.sessionID];
@@ -124,10 +127,6 @@ export class RawCoStreamView<
124
127
  entries.push({ value: change, madeAt, tx: txID });
125
128
  changeEntries.add(entries);
126
129
  }
127
- this.knownTransactions[txID.sessionID] = Math.max(
128
- this.knownTransactions[txID.sessionID] ?? 0,
129
- txID.txIndex,
130
- );
131
130
  }
132
131
 
133
132
  for (const entries of changeEntries) {
package/src/ids.ts CHANGED
@@ -20,7 +20,10 @@ export function rawCoIDfromBytes(bytes: Uint8Array): RawCoID {
20
20
  return `co_z${base58.encode(bytes.slice(0, shortHashLength))}` as RawCoID;
21
21
  }
22
22
 
23
- export type TransactionID = { sessionID: SessionID; txIndex: number };
23
+ export type TransactionID = {
24
+ sessionID: SessionID;
25
+ txIndex: number;
26
+ };
24
27
 
25
28
  export type AgentID = `sealer_z${string}/signer_z${string}`;
26
29
 
@@ -66,3 +66,11 @@ export function stableStringify<T>(
66
66
  export function parseJSON<T>(json: Stringified<T>): T {
67
67
  return JSON.parse(json);
68
68
  }
69
+
70
+ export function safeParseJSON<T>(json: Stringified<T>): T | undefined {
71
+ try {
72
+ return JSON.parse(json);
73
+ } catch (e) {
74
+ return undefined;
75
+ }
76
+ }
@@ -77,10 +77,7 @@ function logPermissionError(
77
77
  logger.debug("Permission error: " + message, attributes);
78
78
  }
79
79
 
80
- export function determineValidTransactions(
81
- coValue: CoValueCore,
82
- knownTransactions?: CoValueKnownState["sessions"],
83
- ): { txID: TransactionID; tx: Transaction }[] {
80
+ export function determineValidTransactions(coValue: CoValueCore) {
84
81
  if (!coValue.isAvailable()) {
85
82
  throw new Error("determineValidTransactions CoValue is not available");
86
83
  }
@@ -91,8 +88,7 @@ export function determineValidTransactions(
91
88
  throw new Error("Group must have initialAdmin");
92
89
  }
93
90
 
94
- return determineValidTransactionsForGroup(coValue, initialAdmin)
95
- .validTransactions;
91
+ determineValidTransactionsForGroup(coValue, initialAdmin);
96
92
  } else if (coValue.verified.header.ruleset.type === "ownedByGroup") {
97
93
  const groupContent = expectGroup(
98
94
  coValue.node
@@ -107,58 +103,44 @@ export function determineValidTransactions(
107
103
  throw new Error("Group must be a map");
108
104
  }
109
105
 
110
- const validTransactions: ValidTransactionsResult[] = [];
111
-
112
- for (const [sessionID, sessionLog] of coValue.verified.sessions.entries()) {
113
- const transactor = accountOrAgentIDfromSessionID(sessionID);
114
- const knownTransactionsForSession = knownTransactions?.[sessionID] ?? -1;
106
+ for (const tx of coValue.verifiedTransactions) {
107
+ if (tx.isValidated) {
108
+ continue;
109
+ }
115
110
 
116
- sessionLog.transactions.forEach((tx, txIndex) => {
117
- if (knownTransactionsForSession >= txIndex) {
118
- return;
119
- }
111
+ tx.isValidated = true;
112
+ const wasValid = tx.isValid;
120
113
 
121
- const groupAtTime = groupContent.atTime(tx.madeAt);
122
- const effectiveTransactor = agentInAccountOrMemberInGroup(
123
- transactor,
124
- groupAtTime,
125
- );
114
+ const groupAtTime = groupContent.atTime(tx.madeAt);
115
+ const effectiveTransactor = agentInAccountOrMemberInGroup(
116
+ tx.author,
117
+ groupAtTime,
118
+ );
126
119
 
127
- if (!effectiveTransactor) {
128
- return;
129
- }
120
+ if (!effectiveTransactor) {
121
+ tx.isValid = false;
122
+ continue;
123
+ }
130
124
 
131
- const transactorRoleAtTxTime =
132
- groupAtTime.roleOfInternal(effectiveTransactor);
125
+ const transactorRoleAtTxTime =
126
+ groupAtTime.roleOfInternal(effectiveTransactor);
133
127
 
134
- if (
135
- transactorRoleAtTxTime !== "admin" &&
136
- transactorRoleAtTxTime !== "writer" &&
137
- transactorRoleAtTxTime !== "writeOnly"
138
- ) {
139
- return;
140
- }
128
+ if (
129
+ transactorRoleAtTxTime !== "admin" &&
130
+ transactorRoleAtTxTime !== "writer" &&
131
+ transactorRoleAtTxTime !== "writeOnly"
132
+ ) {
133
+ tx.isValid = false;
134
+ continue;
135
+ }
141
136
 
142
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
143
- });
137
+ tx.isValid = true;
144
138
  }
145
-
146
- return validTransactions;
147
139
  } else if (coValue.verified.header.ruleset.type === "unsafeAllowAll") {
148
- const validTransactions: ValidTransactionsResult[] = [];
149
-
150
- for (const [sessionID, sessionLog] of coValue.verified.sessions.entries()) {
151
- const knownTransactionsForSession = knownTransactions?.[sessionID] ?? -1;
152
-
153
- sessionLog.transactions.forEach((tx, txIndex) => {
154
- if (knownTransactionsForSession >= txIndex) {
155
- return;
156
- }
157
-
158
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
159
- });
140
+ for (const tx of coValue.verifiedTransactions) {
141
+ tx.isValid = true;
142
+ tx.isValidated = true;
160
143
  }
161
- return validTransactions;
162
144
  } else {
163
145
  throw new Error(
164
146
  "Unknown ruleset type " +
@@ -228,22 +210,9 @@ function determineValidTransactionsForGroup(
228
210
  coValue: CoValueCore,
229
211
  initialAdmin: RawAccountID | AgentID,
230
212
  extendChain?: Set<CoValueCore["id"]>,
231
- ): { validTransactions: ValidTransactionsResult[]; memberState: MemberState } {
232
- const allTransactionsSorted: {
233
- sessionID: SessionID;
234
- txIndex: number;
235
- tx: Transaction;
236
- }[] = [];
237
-
238
- for (const [sessionID, sessionLog] of coValue.verified?.sessions.entries() ??
239
- []) {
240
- sessionLog.transactions.forEach((tx, txIndex) => {
241
- allTransactionsSorted.push({ sessionID, txIndex, tx });
242
- });
243
- }
244
-
245
- allTransactionsSorted.sort((a, b) => {
246
- return a.tx.madeAt - b.tx.madeAt;
213
+ ): { memberState: MemberState } {
214
+ coValue.verifiedTransactions.sort((a, b) => {
215
+ return a.madeAt - b.madeAt;
247
216
  });
248
217
 
249
218
  const memberState: MemberState = {};
@@ -252,15 +221,16 @@ function determineValidTransactionsForGroup(
252
221
 
253
222
  const writeKeys = new Set<string>();
254
223
 
255
- for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
256
- const transactor = accountOrAgentIDfromSessionID(sessionID);
224
+ for (const transaction of coValue.verifiedTransactions) {
225
+ const transactor = transaction.author;
226
+
227
+ transaction.isValidated = true;
228
+
229
+ const tx = transaction.tx;
257
230
 
258
231
  if (tx.privacy === "private") {
259
232
  if (memberState[transactor] === "admin") {
260
- validTransactions.push({
261
- txID: { sessionID, txIndex },
262
- tx,
263
- });
233
+ transaction.isValid = true;
264
234
  continue;
265
235
  } else {
266
236
  logPermissionError(
@@ -270,16 +240,20 @@ function determineValidTransactionsForGroup(
270
240
  }
271
241
  }
272
242
 
273
- let changes;
243
+ let changes = transaction.changes;
274
244
 
275
- try {
276
- changes = parseJSON(tx.changes);
277
- } catch (e) {
278
- logPermissionError("Invalid JSON in transaction", {
279
- id: coValue.id,
280
- tx,
281
- });
282
- continue;
245
+ if (!changes) {
246
+ try {
247
+ changes = parseJSON(tx.changes);
248
+ transaction.changes = changes;
249
+ } catch (e) {
250
+ logPermissionError("Invalid JSON in transaction", {
251
+ id: coValue.id,
252
+ tx,
253
+ });
254
+ transaction.hasInvalidChanges = true;
255
+ continue;
256
+ }
283
257
  }
284
258
 
285
259
  const change = changes[0] as
@@ -292,29 +266,33 @@ function determineValidTransactionsForGroup(
292
266
 
293
267
  if (changes.length !== 1) {
294
268
  logPermissionError("Group transaction must have exactly one change");
269
+ transaction.isValid = false;
295
270
  continue;
296
271
  }
297
272
 
298
273
  if (change.op !== "set") {
299
274
  logPermissionError("Group transaction must set a role or readKey");
275
+ transaction.isValid = false;
300
276
  continue;
301
277
  }
302
278
 
303
279
  if (change.key === "readKey") {
304
280
  if (memberState[transactor] !== "admin") {
305
281
  logPermissionError("Only admins can set readKeys");
282
+ transaction.isValid = false;
306
283
  continue;
307
284
  }
308
285
 
309
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
286
+ transaction.isValid = true;
310
287
  continue;
311
288
  } else if (change.key === "profile") {
312
289
  if (memberState[transactor] !== "admin") {
313
290
  logPermissionError("Only admins can set profile");
291
+ transaction.isValid = false;
314
292
  continue;
315
293
  }
316
294
 
317
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
295
+ transaction.isValid = true;
318
296
  continue;
319
297
  } else if (change.key === "root") {
320
298
  if (memberState[transactor] !== "admin") {
@@ -322,7 +300,7 @@ function determineValidTransactionsForGroup(
322
300
  continue;
323
301
  }
324
302
 
325
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
303
+ transaction.isValid = true;
326
304
  continue;
327
305
  } else if (
328
306
  isKeyForKeyField(change.key) ||
@@ -337,15 +315,17 @@ function determineValidTransactionsForGroup(
337
315
  !isOwnWriteKeyRevelation(change.key, transactor, writeOnlyKeys)
338
316
  ) {
339
317
  logPermissionError("Only admins can reveal keys");
318
+ transaction.isValid = false;
340
319
  continue;
341
320
  }
342
321
 
343
322
  // TODO: check validity of agents who the key is revealed to?
344
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
323
+ transaction.isValid = true;
345
324
  continue;
346
325
  } else if (isParentExtension(change.key)) {
347
326
  if (memberState[transactor] !== "admin") {
348
327
  logPermissionError("Only admins can set parent extensions");
328
+ transaction.isValid = false;
349
329
  continue;
350
330
  }
351
331
 
@@ -364,13 +344,14 @@ function determineValidTransactionsForGroup(
364
344
  logPermissionError(
365
345
  "Circular extend detected, dropping the transaction",
366
346
  );
347
+ transaction.isValid = false;
367
348
  continue;
368
349
  }
369
350
 
370
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
351
+ transaction.isValid = true;
371
352
  continue;
372
353
  } else if (isChildExtension(change.key)) {
373
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
354
+ transaction.isValid = true;
374
355
  continue;
375
356
  } else if (isWriteKeyForMember(change.key)) {
376
357
  const memberKey = getAccountOrAgentFromWriteKeyForMember(change.key);
@@ -381,6 +362,7 @@ function determineValidTransactionsForGroup(
381
362
  memberKey !== transactor
382
363
  ) {
383
364
  logPermissionError("Only admins can set writeKeys");
365
+ transaction.isValid = false;
384
366
  continue;
385
367
  }
386
368
 
@@ -398,12 +380,13 @@ function determineValidTransactionsForGroup(
398
380
  logPermissionError(
399
381
  "Write key already exists and can't be overridden by invite",
400
382
  );
383
+ transaction.isValid = false;
401
384
  continue;
402
385
  }
403
386
 
404
387
  writeKeys.add(change.key);
405
388
 
406
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
389
+ transaction.isValid = true;
407
390
  continue;
408
391
  }
409
392
 
@@ -422,6 +405,7 @@ function determineValidTransactionsForGroup(
422
405
  change.value !== "writeOnlyInvite"
423
406
  ) {
424
407
  logPermissionError("Group transaction must set a valid role");
408
+ transaction.isValid = false;
425
409
  continue;
426
410
  }
427
411
 
@@ -437,6 +421,7 @@ function determineValidTransactionsForGroup(
437
421
  logPermissionError(
438
422
  "Everyone can only be set to reader, writer, writeOnly or revoked",
439
423
  );
424
+ transaction.isValid = false;
440
425
  continue;
441
426
  }
442
427
 
@@ -460,41 +445,47 @@ function determineValidTransactionsForGroup(
460
445
  assignedRole !== "admin"
461
446
  ) {
462
447
  logPermissionError("Admins can only demote themselves.");
448
+ transaction.isValid = false;
463
449
  continue;
464
450
  }
465
451
  } else if (memberState[transactor] === "adminInvite") {
466
452
  if (change.value !== "admin") {
467
453
  logPermissionError("AdminInvites can only create admins.");
454
+ transaction.isValid = false;
468
455
  continue;
469
456
  }
470
457
  } else if (memberState[transactor] === "writerInvite") {
471
458
  if (change.value !== "writer") {
472
459
  logPermissionError("WriterInvites can only create writers.");
460
+ transaction.isValid = false;
473
461
  continue;
474
462
  }
475
463
  } else if (memberState[transactor] === "readerInvite") {
476
464
  if (change.value !== "reader") {
477
465
  logPermissionError("ReaderInvites can only create reader.");
466
+ transaction.isValid = false;
478
467
  continue;
479
468
  }
480
469
  } else if (memberState[transactor] === "writeOnlyInvite") {
481
470
  if (change.value !== "writeOnly") {
482
471
  logPermissionError("WriteOnlyInvites can only create writeOnly.");
472
+ transaction.isValid = false;
483
473
  continue;
484
474
  }
485
475
  } else {
486
476
  logPermissionError(
487
477
  "Group transaction must be made by current admin or invite",
488
478
  );
479
+ transaction.isValid = false;
489
480
  continue;
490
481
  }
491
482
  }
492
483
 
493
484
  memberState[affectedMember] = change.value;
494
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
485
+ transaction.isValid = true;
495
486
  }
496
487
 
497
- return { validTransactions, memberState };
488
+ return { memberState };
498
489
  }
499
490
 
500
491
  function agentInAccountOrMemberInGroup(
package/src/sync.ts CHANGED
@@ -279,17 +279,32 @@ export class SyncManager {
279
279
  peer.trackToldKnownState(id);
280
280
  }
281
281
 
282
+ reconcileServerPeers() {
283
+ const serverPeers = Object.values(this.peers).filter(
284
+ (peer) => peer.role === "server",
285
+ );
286
+ for (const peer of serverPeers) {
287
+ this.startPeerReconciliation(peer);
288
+ }
289
+ }
290
+
282
291
  startPeerReconciliation(peer: PeerState) {
283
292
  const coValuesOrderedByDependency: CoValueCore[] = [];
284
293
 
285
- const gathered = new Set<string>();
286
-
294
+ const seen = new Set<string>();
287
295
  const buildOrderedCoValueList = (coValue: CoValueCore) => {
288
- if (gathered.has(coValue.id)) {
296
+ if (seen.has(coValue.id)) {
289
297
  return;
290
298
  }
299
+ seen.add(coValue.id);
291
300
 
292
- gathered.add(coValue.id);
301
+ // Ignore the covalue if this peer isn't relevant to it
302
+ if (
303
+ this.getServerPeers(coValue.id).find((p) => p.id === peer.id) ===
304
+ undefined
305
+ ) {
306
+ return;
307
+ }
293
308
 
294
309
  for (const id of coValue.getDependedOnCoValues()) {
295
310
  const coValue = this.local.getCoValue(id);
@@ -355,7 +370,7 @@ export class SyncManager {
355
370
  });
356
371
  }
357
372
 
358
- addPeer(peer: Peer) {
373
+ addPeer(peer: Peer, skipReconciliation: boolean = false) {
359
374
  const prevPeer = this.peers[peer.id];
360
375
 
361
376
  if (prevPeer && !prevPeer.closed) {
@@ -373,7 +388,7 @@ export class SyncManager {
373
388
  },
374
389
  );
375
390
 
376
- if (peerState.role === "server") {
391
+ if (!skipReconciliation && peerState.role === "server") {
377
392
  void this.startPeerReconciliation(peerState);
378
393
  }
379
394
 
@@ -137,6 +137,30 @@ test("Should migrate the root from private to trusting", async () => {
137
137
  expect(account3.ops).toEqual(account2.ops); // No new transactions were made
138
138
  });
139
139
 
140
+ test("myRole returns 'admin' for the current account", async () => {
141
+ const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
142
+ creationProps: { name: "Hermes Puggington" },
143
+ crypto: Crypto,
144
+ });
145
+
146
+ const account = await node.load(accountID);
147
+ if (account === "unavailable") throw new Error("Account unavailable");
148
+
149
+ expect(account.myRole()).toEqual("admin");
150
+ });
151
+
152
+ test("roleOf returns 'admin' when the accountID is the same as the receiver account", async () => {
153
+ const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
154
+ creationProps: { name: "Hermes Puggington" },
155
+ crypto: Crypto,
156
+ });
157
+
158
+ const account = await node.load(accountID);
159
+ if (account === "unavailable") throw new Error("Account unavailable");
160
+
161
+ expect(account.roleOf(accountID)).toEqual("admin");
162
+ });
163
+
140
164
  test("throws an error if the user tried to add a member to an account", async () => {
141
165
  const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
142
166
  creationProps: { name: "Hermes Puggington" },
@@ -0,0 +1,141 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { createTestNode } from "./testUtils.js";
3
+ import { expectMap } from "../coValue.js";
4
+
5
+ describe("Branching Logic", () => {
6
+ describe("Branch Operations with Transactions", () => {
7
+ test("should maintain separate transaction histories", async () => {
8
+ const node = createTestNode();
9
+ const group = node.createGroup();
10
+ const originalMap = group.createMap();
11
+ const branchName = "feature-branch";
12
+
13
+ // Add transactions to original map
14
+ originalMap.set("originalKey1", "value1", "trusting");
15
+ originalMap.set("originalKey2", "value2", "trusting");
16
+
17
+ const branch = expectMap(
18
+ originalMap.core.createBranch(branchName, group.id).getCurrentContent(),
19
+ );
20
+
21
+ // Add transactions to branch
22
+ branch.set("branchKey1", "branchValue1", "trusting");
23
+ branch.set("branchKey2", "branchValue2", "trusting");
24
+
25
+ expect(originalMap.get("branchKey1")).toBe(undefined);
26
+ expect(originalMap.get("branchKey2")).toBe(undefined);
27
+ expect(branch.get("branchKey2")).toBe("branchValue2");
28
+ expect(branch.get("branchKey1")).toBe("branchValue1");
29
+ expect(branch.get("originalKey1")).toBe("value1");
30
+ expect(branch.get("originalKey2")).toBe("value2");
31
+ });
32
+ });
33
+
34
+ describe("Branch Merging", () => {
35
+ test("should merge branch transactions back to source", () => {
36
+ const node = createTestNode();
37
+ const group = node.createGroup();
38
+ const originalMap = group.createMap();
39
+ const branchName = "feature-branch";
40
+
41
+ // Add transactions to original map
42
+ originalMap.set("key1", "value1", "trusting");
43
+ originalMap.set("key2", "value2", "trusting");
44
+
45
+ // Create branch
46
+ const branch = expectMap(
47
+ originalMap.core.createBranch(branchName, group.id).getCurrentContent(),
48
+ );
49
+
50
+ // Add transactions to branch
51
+ branch.set("key1", "branchValue1", "trusting");
52
+
53
+ // Merge branch
54
+ const result = expectMap(branch.core.mergeBranch().getCurrentContent());
55
+
56
+ // Check that source now has branch transactions
57
+ expect(result.get("key1")).toBe("branchValue1");
58
+ expect(result.get("key2")).toBe("value2");
59
+ });
60
+
61
+ test("should not merge branch transactions back to source if they are already merged", () => {
62
+ const node = createTestNode();
63
+ const group = node.createGroup();
64
+ const originalMap = group.createMap();
65
+ const branchName = "feature-branch";
66
+
67
+ // Add transactions to original map
68
+ originalMap.set("key1", "value1", "trusting");
69
+ originalMap.set("key2", "value2", "trusting");
70
+
71
+ // Create branch
72
+ const branch = expectMap(
73
+ originalMap.core.createBranch(branchName, group.id).getCurrentContent(),
74
+ );
75
+
76
+ // Add transactions to branch
77
+ branch.set("key1", "branchValue1", "trusting");
78
+
79
+ // Merge branch twice
80
+ expectMap(branch.core.mergeBranch().getCurrentContent());
81
+ const result = expectMap(branch.core.mergeBranch().getCurrentContent());
82
+
83
+ expect(result.core.mergeCommits.length).toBe(1);
84
+
85
+ // Check that source now has branch transactions
86
+ expect(result.get("key1")).toBe("branchValue1");
87
+ expect(result.get("key2")).toBe("value2");
88
+ });
89
+
90
+ test("should not create a merge commit if the branch is empty", () => {
91
+ const node = createTestNode();
92
+ const group = node.createGroup();
93
+ const originalMap = group.createMap();
94
+ const branchName = "feature-branch";
95
+
96
+ // Add transactions to original map
97
+ originalMap.set("key1", "value1", "trusting");
98
+ originalMap.set("key2", "value2", "trusting");
99
+
100
+ // Create branch
101
+ const branch = expectMap(
102
+ originalMap.core.createBranch(branchName, group.id).getCurrentContent(),
103
+ );
104
+
105
+ // Merge branch twice
106
+ const result = expectMap(branch.core.mergeBranch().getCurrentContent());
107
+
108
+ expect(result.core.mergeCommits.length).toBe(0);
109
+ });
110
+
111
+ test("should merge the new changes from the branch after the last merge", () => {
112
+ const node = createTestNode();
113
+ const group = node.createGroup();
114
+ const originalMap = group.createMap();
115
+ const branchName = "feature-branch";
116
+
117
+ originalMap.set("key1", "value1", "trusting");
118
+ originalMap.set("key2", "value2", "trusting");
119
+
120
+ const branch = expectMap(
121
+ originalMap.core.createBranch(branchName, group.id).getCurrentContent(),
122
+ );
123
+
124
+ branch.set("key1", "branchValue1", "trusting");
125
+
126
+ branch.core.mergeBranch();
127
+
128
+ expect(originalMap.get("key1")).toBe("branchValue1");
129
+ expect(originalMap.get("key2")).toBe("value2");
130
+
131
+ branch.set("key2", "branchValue2", "trusting");
132
+
133
+ branch.core.mergeBranch();
134
+
135
+ expect(originalMap.core.mergeCommits.length).toBe(2);
136
+
137
+ expect(originalMap.get("key1")).toBe("branchValue1");
138
+ expect(originalMap.get("key2")).toBe("branchValue2");
139
+ });
140
+ });
141
+ });
@@ -277,9 +277,8 @@ test("creates a transaction with trusting meta information", async () => {
277
277
  meta: true,
278
278
  });
279
279
 
280
- const validTransactions = determineValidTransactions(map.core);
281
-
282
- expect(validTransactions[0]?.tx.meta).toBe(`{"meta":true}`);
280
+ expect(map.core.verifiedTransactions[0]?.tx.meta).toBe(`{"meta":true}`);
281
+ expect(map.core.verifiedTransactions[0]?.meta).toEqual({ meta: true });
283
282
  });
284
283
 
285
284
  test("creates a transaction with private meta information", async () => {