cojson 0.8.38 → 0.8.41

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 (60) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/native/PeerState.js +11 -2
  3. package/dist/native/PeerState.js.map +1 -1
  4. package/dist/native/{SyncStateSubscriptionManager.js → SyncStateManager.js} +35 -24
  5. package/dist/native/SyncStateManager.js.map +1 -0
  6. package/dist/native/coValueCore.js +25 -5
  7. package/dist/native/coValueCore.js.map +1 -1
  8. package/dist/native/coValues/coMap.js +98 -103
  9. package/dist/native/coValues/coMap.js.map +1 -1
  10. package/dist/native/coValues/coStream.js +17 -6
  11. package/dist/native/coValues/coStream.js.map +1 -1
  12. package/dist/native/coValues/group.js +127 -39
  13. package/dist/native/coValues/group.js.map +1 -1
  14. package/dist/native/exports.js.map +1 -1
  15. package/dist/native/localNode.js +5 -2
  16. package/dist/native/localNode.js.map +1 -1
  17. package/dist/native/permissions.js +51 -3
  18. package/dist/native/permissions.js.map +1 -1
  19. package/dist/native/sync.js +34 -10
  20. package/dist/native/sync.js.map +1 -1
  21. package/dist/web/PeerState.js +11 -2
  22. package/dist/web/PeerState.js.map +1 -1
  23. package/dist/web/{SyncStateSubscriptionManager.js → SyncStateManager.js} +35 -24
  24. package/dist/web/SyncStateManager.js.map +1 -0
  25. package/dist/web/coValueCore.js +25 -5
  26. package/dist/web/coValueCore.js.map +1 -1
  27. package/dist/web/coValues/coMap.js +98 -103
  28. package/dist/web/coValues/coMap.js.map +1 -1
  29. package/dist/web/coValues/coStream.js +17 -6
  30. package/dist/web/coValues/coStream.js.map +1 -1
  31. package/dist/web/coValues/group.js +127 -39
  32. package/dist/web/coValues/group.js.map +1 -1
  33. package/dist/web/exports.js.map +1 -1
  34. package/dist/web/localNode.js +5 -2
  35. package/dist/web/localNode.js.map +1 -1
  36. package/dist/web/permissions.js +51 -3
  37. package/dist/web/permissions.js.map +1 -1
  38. package/dist/web/sync.js +34 -10
  39. package/dist/web/sync.js.map +1 -1
  40. package/package.json +3 -5
  41. package/src/PeerState.ts +12 -2
  42. package/src/{SyncStateSubscriptionManager.ts → SyncStateManager.ts} +48 -35
  43. package/src/coValueCore.ts +43 -9
  44. package/src/coValues/coMap.ts +126 -127
  45. package/src/coValues/coStream.ts +27 -10
  46. package/src/coValues/group.ts +218 -50
  47. package/src/exports.ts +2 -1
  48. package/src/localNode.ts +5 -2
  49. package/src/permissions.ts +71 -8
  50. package/src/sync.ts +57 -23
  51. package/src/tests/PeerState.test.ts +49 -0
  52. package/src/tests/PriorityBasedMessageQueue.test.ts +6 -73
  53. package/src/tests/{SyncStateSubscriptionManager.test.ts → SyncStateManager.test.ts} +109 -25
  54. package/src/tests/coMap.test.ts +2 -2
  55. package/src/tests/group.test.ts +338 -47
  56. package/src/tests/permissions.test.ts +324 -0
  57. package/src/tests/sync.test.ts +112 -71
  58. package/src/tests/testUtils.ts +126 -17
  59. package/dist/native/SyncStateSubscriptionManager.js.map +0 -1
  60. package/dist/web/SyncStateSubscriptionManager.js.map +0 -1
@@ -1,7 +1,8 @@
1
1
  import { CoID, RawCoValue } from "../coValue.js";
2
- import { CoValueCore, DecryptedTransaction } from "../coValueCore.js";
2
+ import { CoValueCore } from "../coValueCore.js";
3
3
  import { AgentID, TransactionID } from "../ids.js";
4
4
  import { JsonObject, JsonValue } from "../jsonValue.js";
5
+ import { CoValueKnownState } from "../sync.js";
5
6
  import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
6
7
  import { isCoValue } from "../typeUtils/isCoValue.js";
7
8
  import { RawAccountID } from "./account.js";
@@ -46,14 +47,14 @@ export class RawCoMapView<
46
47
  /** @internal */
47
48
  latestTxMadeAt: number;
48
49
  /** @internal */
49
- cachedOps?: {
50
+ ops: {
50
51
  [Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
51
52
  };
52
53
  /** @internal */
53
- validSortedTransactions?: DecryptedTransaction[];
54
+ knownTransactions: CoValueKnownState["sessions"];
54
55
 
55
56
  /** @internal */
56
- options?: { ignorePrivateTransactions: boolean; atTime?: number };
57
+ ignorePrivateTransactions: boolean;
57
58
  /** @internal */
58
59
  atTimeFilter?: number = undefined;
59
60
  /** @category 6. Meta */
@@ -64,123 +65,81 @@ export class RawCoMapView<
64
65
  core: CoValueCore,
65
66
  options?: {
66
67
  ignorePrivateTransactions: boolean;
67
- atTime?: number;
68
- validSortedTransactions?: DecryptedTransaction[];
69
- cachedOps?: {
70
- [Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
71
- };
72
68
  },
73
69
  ) {
74
70
  this.id = core.id as CoID<this>;
75
71
  this.core = core;
76
- this.latest = {};
77
72
  this.latestTxMadeAt = 0;
78
- this.options = options;
79
- this.cachedOps = options?.cachedOps;
80
- this.validSortedTransactions = options?.validSortedTransactions;
73
+ this.ignorePrivateTransactions =
74
+ options?.ignorePrivateTransactions ?? false;
75
+ this.ops = {};
76
+ this.latest = {};
77
+ this.knownTransactions = {};
81
78
 
82
- this.processLatestTransactions();
79
+ this.processNewTransactions();
83
80
  }
84
81
 
85
- /** @internal */
86
- private getValidSortedTransactions() {
87
- if (this.validSortedTransactions) {
88
- return this.validSortedTransactions;
82
+ processNewTransactions() {
83
+ if (this.isTimeTravelEntity()) {
84
+ throw new Error("Cannot process transactions on a time travel entity");
89
85
  }
90
86
 
91
- const validSortedTransactions = this.core.getValidSortedTransactions({
92
- ignorePrivateTransactions:
93
- this.options?.ignorePrivateTransactions ?? false,
94
- });
95
-
96
- this.validSortedTransactions = validSortedTransactions;
97
-
98
- return validSortedTransactions;
99
- }
100
-
101
- private resetCachedValues() {
102
- this.validSortedTransactions = undefined;
103
- this.cachedOps = undefined;
104
- }
105
-
106
- private processLatestTransactions() {
107
- // Reset all internal state and cached values
108
- this.latest = {};
109
- this.latestTxMadeAt = 0;
87
+ const { ops } = this;
110
88
 
111
- const { latest } = this;
89
+ const changedEntries = new Map<
90
+ keyof typeof ops,
91
+ NonNullable<(typeof ops)[keyof typeof ops]>
92
+ >();
112
93
 
113
- const atTimeFilter = this.options?.atTime;
114
-
115
- for (const { txID, changes, madeAt } of this.getValidSortedTransactions()) {
116
- if (atTimeFilter && madeAt > atTimeFilter) {
117
- continue;
118
- }
119
-
120
- if (madeAt > this.latestTxMadeAt) {
121
- this.latestTxMadeAt = madeAt;
122
- }
94
+ const nextValidTransactions = this.core.getValidTransactions({
95
+ ignorePrivateTransactions: this.ignorePrivateTransactions,
96
+ knownTransactions: this.knownTransactions,
97
+ });
123
98
 
99
+ for (const { txID, changes, madeAt } of nextValidTransactions) {
124
100
  for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
125
101
  const change = changes[changeIdx] as MapOpPayload<
126
102
  keyof Shape & string,
127
103
  Shape[keyof Shape & string]
128
104
  >;
129
- const entry = latest[change.key];
130
- if (!entry) {
131
- latest[change.key] = {
132
- txID,
133
- madeAt,
134
- changeIdx,
135
- change,
136
- };
137
- } else if (madeAt >= entry.madeAt) {
138
- entry.txID = txID;
139
- entry.madeAt = madeAt;
140
- entry.changeIdx = changeIdx;
141
- entry.change = change;
142
- }
143
- }
144
- }
145
- }
146
-
147
- revalidateTransactions() {
148
- this.resetCachedValues();
149
- this.processLatestTransactions();
150
- }
151
-
152
- private getOps() {
153
- if (this.cachedOps) {
154
- return this.cachedOps;
155
- }
105
+ const entry = {
106
+ txID,
107
+ madeAt,
108
+ changeIdx,
109
+ change,
110
+ };
156
111
 
157
- const ops: {
158
- [Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
159
- } = {};
112
+ if (madeAt > this.latestTxMadeAt) {
113
+ this.latestTxMadeAt = madeAt;
114
+ }
160
115
 
161
- for (const { txID, changes, madeAt } of this.getValidSortedTransactions()) {
162
- for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
163
- const change = changes[changeIdx] as MapOpPayload<
164
- keyof Shape & string,
165
- Shape[keyof Shape & string]
166
- >;
167
- let entries = ops[change.key];
116
+ const entries = ops[change.key];
168
117
  if (!entries) {
169
- entries = [];
118
+ const entries = [entry];
170
119
  ops[change.key] = entries;
120
+ changedEntries.set(change.key, entries);
121
+ } else {
122
+ entries.push(entry);
123
+ changedEntries.set(change.key, entries);
171
124
  }
172
- entries.push({
173
- txID,
174
- madeAt,
175
- changeIdx,
176
- change,
177
- });
125
+ this.knownTransactions[txID.sessionID] = Math.max(
126
+ this.knownTransactions[txID.sessionID] ?? 0,
127
+ txID.txIndex,
128
+ );
178
129
  }
179
130
  }
180
131
 
181
- this.cachedOps = ops;
132
+ for (const entries of changedEntries.values()) {
133
+ entries.sort(this.core.compareTransactions);
134
+ }
182
135
 
183
- return ops;
136
+ for (const [key, entries] of changedEntries.entries()) {
137
+ this.latest[key] = entries[entries.length - 1];
138
+ }
139
+ }
140
+
141
+ isTimeTravelEntity() {
142
+ return Boolean(this.atTimeFilter);
184
143
  }
185
144
 
186
145
  /** @category 6. Meta */
@@ -198,14 +157,11 @@ export class RawCoMapView<
198
157
  if (time >= this.latestTxMadeAt) {
199
158
  return this;
200
159
  } else {
201
- const clone = new RawCoMapView(this.core, {
202
- ignorePrivateTransactions:
203
- this.options?.ignorePrivateTransactions ?? false,
204
- atTime: time,
205
- cachedOps: this.cachedOps,
206
- validSortedTransactions: this.validSortedTransactions,
207
- });
208
- Object.setPrototypeOf(clone, this);
160
+ const clone = Object.create(this) as RawCoMapView<Shape, Meta>;
161
+
162
+ clone.atTimeFilter = time;
163
+ clone.latest = {};
164
+
209
165
  return clone as this;
210
166
  }
211
167
  }
@@ -218,12 +174,12 @@ export class RawCoMapView<
218
174
  return undefined;
219
175
  }
220
176
 
221
- const atTimeFilter = this.options?.atTime;
177
+ const atTimeFilter = this.atTimeFilter;
222
178
 
223
179
  if (atTimeFilter) {
224
- return this.getOps()[key]?.filter((op) => op.madeAt <= atTimeFilter);
180
+ return this.ops[key]?.filter((op) => op.madeAt <= atTimeFilter);
225
181
  } else {
226
- return this.getOps()[key];
182
+ return this.ops[key];
227
183
  }
228
184
  }
229
185
 
@@ -232,36 +188,64 @@ export class RawCoMapView<
232
188
  *
233
189
  * @category 1. Reading */
234
190
  keys<K extends keyof Shape & string = keyof Shape & string>(): K[] {
235
- return (Object.keys(this.latest) as K[]).filter((key) => {
236
- const latestChange = this.latest[key];
191
+ return (Object.keys(this.ops) as K[]).filter((key) => {
192
+ const entry = this.getRaw(key);
237
193
 
238
- if (!latestChange) {
194
+ if (entry === undefined) {
239
195
  return false;
240
196
  }
241
197
 
242
- if (latestChange.change.op === "del") {
198
+ if (entry.change.op === "del") {
243
199
  return false;
244
- } else {
245
- return true;
246
200
  }
201
+
202
+ return true;
247
203
  });
248
204
  }
249
205
 
206
+ getRaw<K extends keyof Shape & string>(key: K) {
207
+ let latestChange = this.latest[key];
208
+
209
+ if (latestChange === undefined) {
210
+ const entries = this.ops[key];
211
+
212
+ // Time travel values are lazily computed
213
+ if (entries && !(key in this.latest)) {
214
+ const atTimeFilter = this.atTimeFilter;
215
+
216
+ if (!atTimeFilter) {
217
+ latestChange = entries[entries.length - 1];
218
+ } else {
219
+ latestChange = entries.findLast((op) => op.madeAt <= atTimeFilter);
220
+ }
221
+
222
+ this.latest[key] = latestChange;
223
+ }
224
+
225
+ if (latestChange === undefined) {
226
+ return undefined;
227
+ }
228
+ }
229
+
230
+ return latestChange;
231
+ }
232
+
250
233
  /**
251
234
  * Returns the current value for the given key.
252
235
  *
253
236
  * @category 1. Reading
254
237
  **/
255
238
  get<K extends keyof Shape & string>(key: K): Shape[K] | undefined {
256
- const latestChange = this.latest[key];
257
- if (!latestChange) {
239
+ const entry = this.getRaw(key);
240
+
241
+ if (entry === undefined) {
258
242
  return undefined;
259
243
  }
260
244
 
261
- if (latestChange.change.op === "del") {
245
+ if (entry.change.op === "del") {
262
246
  return undefined;
263
247
  } else {
264
- return latestChange.change.value as Shape[K];
248
+ return entry.change.value as Shape[K];
265
249
  }
266
250
  }
267
251
 
@@ -273,7 +257,7 @@ export class RawCoMapView<
273
257
  [K in keyof Shape & string]: Shape[K];
274
258
  }> = {};
275
259
 
276
- for (const key of Object.keys(this.latest) as (keyof Shape & string)[]) {
260
+ for (const key of Object.keys(this.ops) as (keyof Shape & string)[]) {
277
261
  const value = this.get(key);
278
262
  if (value !== undefined) {
279
263
  object[key] = value;
@@ -294,9 +278,9 @@ export class RawCoMapView<
294
278
 
295
279
  /** @category 5. Edit history */
296
280
  nthEditAt<K extends keyof Shape & string>(key: K, n: number) {
297
- const ops = this.getOps()[key];
281
+ const ops = this.ops[key];
298
282
 
299
- const atTimeFilter = this.options?.atTime;
283
+ const atTimeFilter = this.atTimeFilter;
300
284
  const entry = ops?.[n];
301
285
 
302
286
  if (!entry) {
@@ -321,24 +305,31 @@ export class RawCoMapView<
321
305
  value?: Shape[K];
322
306
  }
323
307
  | undefined {
324
- const ops = this.timeFilteredOps(key);
325
- const lastEntry = ops?.[ops.length - 1];
308
+ const entry = this.getRaw(key);
326
309
 
327
- if (!lastEntry) {
310
+ if (!entry) {
328
311
  return undefined;
329
312
  }
330
313
 
331
- return operationToEditEntry(lastEntry);
314
+ return operationToEditEntry(entry);
332
315
  }
333
316
 
334
317
  /** @category 5. Edit history */
335
318
  *editsAt<K extends keyof Shape & string>(key: K) {
336
- const ops = this.timeFilteredOps(key);
337
- if (!ops) {
319
+ const entries = this.ops[key];
320
+
321
+ if (!entries) {
338
322
  return;
339
323
  }
340
324
 
341
- for (const entry of ops) {
325
+ const atTimeFilter = this.atTimeFilter;
326
+
327
+ for (const entry of entries) {
328
+ // Entries are sorted by madeAt
329
+ if (atTimeFilter && entry.madeAt > atTimeFilter) {
330
+ return;
331
+ }
332
+
342
333
  yield operationToEditEntry(entry);
343
334
  }
344
335
  }
@@ -374,6 +365,10 @@ export class RawCoMap<
374
365
  value: Shape[K],
375
366
  privacy: "private" | "trusting" = "private",
376
367
  ): void {
368
+ if (this.isTimeTravelEntity()) {
369
+ throw new Error("Cannot set value on a time travel entity");
370
+ }
371
+
377
372
  this.core.makeTransaction(
378
373
  [
379
374
  {
@@ -385,7 +380,7 @@ export class RawCoMap<
385
380
  privacy,
386
381
  );
387
382
 
388
- this.revalidateTransactions();
383
+ this.processNewTransactions();
389
384
  }
390
385
 
391
386
  /** Delete the given key (setting it to undefined).
@@ -400,6 +395,10 @@ export class RawCoMap<
400
395
  key: keyof Shape & string,
401
396
  privacy: "private" | "trusting" = "private",
402
397
  ) {
398
+ if (this.isTimeTravelEntity()) {
399
+ throw new Error("Cannot delete value on a time travel entity");
400
+ }
401
+
403
402
  this.core.makeTransaction(
404
403
  [
405
404
  {
@@ -410,7 +409,7 @@ export class RawCoMap<
410
409
  privacy,
411
410
  );
412
411
 
413
- this.revalidateTransactions();
412
+ this.processNewTransactions();
414
413
  }
415
414
  }
416
415
 
@@ -3,6 +3,7 @@ import { CoID, RawCoValue } from "../coValue.js";
3
3
  import { CoValueCore } from "../coValueCore.js";
4
4
  import { AgentID, SessionID, TransactionID } from "../ids.js";
5
5
  import { JsonObject, JsonValue } from "../jsonValue.js";
6
+ import { CoValueKnownState } from "../sync.js";
6
7
  import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
7
8
  import { isAccountID } from "../typeUtils/isAccountID.js";
8
9
  import { isCoValue } from "../typeUtils/isCoValue.js";
@@ -52,13 +53,16 @@ export class RawCoStreamView<
52
53
  items: {
53
54
  [key: SessionID]: CoStreamItem<Item>[];
54
55
  };
56
+ /** @internal */
57
+ knownTransactions: CoValueKnownState["sessions"];
55
58
  readonly _item!: Item;
56
59
 
57
60
  constructor(core: CoValueCore) {
58
61
  this.id = core.id as CoID<this>;
59
62
  this.core = core;
60
63
  this.items = {};
61
- this.fillFromCoValue();
64
+ this.knownTransactions = {};
65
+ this.processNewTransactions();
62
66
  }
63
67
 
64
68
  get headerMeta(): Meta {
@@ -75,14 +79,13 @@ export class RawCoStreamView<
75
79
  }
76
80
 
77
81
  /** @internal */
78
- protected fillFromCoValue() {
79
- this.items = {};
82
+ protected processNewTransactions() {
83
+ const changeEntries = new Set<CoStreamItem<Item>[]>();
80
84
 
81
- for (const {
82
- txID,
83
- madeAt,
84
- changes,
85
- } of this.core.getValidSortedTransactions()) {
85
+ for (const { txID, madeAt, changes } of this.core.getValidTransactions({
86
+ ignorePrivateTransactions: false,
87
+ knownTransactions: this.knownTransactions,
88
+ })) {
86
89
  for (const changeUntyped of changes) {
87
90
  const change = changeUntyped as Item;
88
91
  let entries = this.items[txID.sessionID];
@@ -91,7 +94,21 @@ export class RawCoStreamView<
91
94
  this.items[txID.sessionID] = entries;
92
95
  }
93
96
  entries.push({ value: change, madeAt, tx: txID });
97
+ changeEntries.add(entries);
94
98
  }
99
+ this.knownTransactions[txID.sessionID] = Math.max(
100
+ this.knownTransactions[txID.sessionID] ?? 0,
101
+ txID.txIndex,
102
+ );
103
+ }
104
+
105
+ for (const entries of changeEntries) {
106
+ entries.sort(
107
+ (a, b) =>
108
+ a.madeAt - b.madeAt ||
109
+ (a.tx.sessionID < b.tx.sessionID ? -1 : 1) ||
110
+ a.tx.txIndex - b.tx.txIndex,
111
+ );
95
112
  }
96
113
  }
97
114
 
@@ -255,7 +272,7 @@ export class RawCoStream<
255
272
  {
256
273
  push(item: Item, privacy: "private" | "trusting" = "private"): void {
257
274
  this.core.makeTransaction([isCoValue(item) ? item.id : item], privacy);
258
- this.fillFromCoValue();
275
+ this.processNewTransactions();
259
276
  }
260
277
  }
261
278
 
@@ -343,7 +360,7 @@ export class RawBinaryCoStream<
343
360
  ): void {
344
361
  this.core.makeTransaction([item], privacy);
345
362
  if (updateView) {
346
- this.fillFromCoValue();
363
+ this.processNewTransactions();
347
364
  }
348
365
  }
349
366