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
@@ -188,4 +188,53 @@ describe("PeerState", () => {
188
188
  expect(knownStatesSpy).toHaveBeenCalledWith(action);
189
189
  expect(optimisticKnownStatesSpy).toHaveBeenCalledWith(action);
190
190
  });
191
+
192
+ test("should use same reference for knownStates and optimisticKnownStates for storage peers", () => {
193
+ const mockStoragePeer: Peer = {
194
+ id: "test-storage-peer",
195
+ role: "storage",
196
+ priority: 1,
197
+ crashOnClose: false,
198
+ incoming: (async function* () {})(),
199
+ outgoing: {
200
+ push: vi.fn().mockResolvedValue(undefined),
201
+ close: vi.fn(),
202
+ },
203
+ };
204
+ const peerState = new PeerState(mockStoragePeer, undefined);
205
+
206
+ // Verify they are the same reference
207
+ expect(peerState.knownStates).toBe(peerState.optimisticKnownStates);
208
+
209
+ // Verify that dispatching only updates one state
210
+ const knownStatesSpy = vi.spyOn(peerState.knownStates, "dispatch");
211
+ const optimisticKnownStatesSpy = vi.spyOn(
212
+ peerState.optimisticKnownStates,
213
+ "dispatch",
214
+ );
215
+
216
+ const action: PeerKnownStateActions = {
217
+ type: "SET",
218
+ id: "co_z1",
219
+ value: {
220
+ id: "co_z1",
221
+ header: false,
222
+ sessions: {},
223
+ },
224
+ };
225
+ peerState.dispatchToKnownStates(action);
226
+
227
+ // Only one dispatch should happen since they're the same reference
228
+ expect(knownStatesSpy).toHaveBeenCalledTimes(1);
229
+ expect(knownStatesSpy).toHaveBeenCalledWith(action);
230
+ expect(optimisticKnownStatesSpy).toHaveBeenCalledTimes(1);
231
+ expect(optimisticKnownStatesSpy).toHaveBeenCalledWith(action);
232
+ });
233
+
234
+ test("should use separate references for knownStates and optimisticKnownStates for non-storage peers", () => {
235
+ const { peerState } = setup(); // Uses a regular peer
236
+
237
+ // Verify they are different references
238
+ expect(peerState.knownStates).not.toBe(peerState.optimisticKnownStates);
239
+ });
191
240
  });
@@ -1,88 +1,21 @@
1
- import { metrics } from "@opentelemetry/api";
2
- import {
3
- AggregationTemporality,
4
- InMemoryMetricExporter,
5
- MeterProvider,
6
- MetricReader,
7
- type MetricReaderOptions,
8
- type PushMetricExporter,
9
- } from "@opentelemetry/sdk-metrics";
10
1
  import { afterEach, describe, expect, test } from "vitest";
11
2
  import { PriorityBasedMessageQueue } from "../PriorityBasedMessageQueue.js";
12
3
  import { CO_VALUE_PRIORITY } from "../priority.js";
13
4
  import type { SyncMessage } from "../sync.js";
14
-
15
- interface A extends MetricReaderOptions {
16
- exporter: PushMetricExporter;
17
- }
18
-
19
- /**
20
- * This is a test metric reader that uses an in-memory metric exporter and exposes a method to get the value of a metric given its name and attributes.
21
- *
22
- * This is useful for testing the values of metrics that are collected by the SDK.
23
- *
24
- * TODO: We could move this to a separate file and make it a utility class that can be used in other tests.
25
- * TODO: We may want to rethink how we access metrics (see `getMetricValue` method) to make it more flexible.
26
- */
27
- class TestMetricReader extends MetricReader {
28
- private _exporter = new InMemoryMetricExporter(
29
- AggregationTemporality.CUMULATIVE,
30
- );
31
-
32
- protected onShutdown(): Promise<void> {
33
- throw new Error("Method not implemented.");
34
- }
35
- protected onForceFlush(): Promise<void> {
36
- throw new Error("Method not implemented.");
37
- }
38
-
39
- async getMetricValue(
40
- name: string,
41
- attributes: { [key: string]: string | number } = {},
42
- ) {
43
- await this.collectAndExport();
44
- const metric = this._exporter
45
- .getMetrics()[0]
46
- ?.scopeMetrics[0]?.metrics.find((m) => m.descriptor.name === name);
47
-
48
- const dp = metric?.dataPoints.find(
49
- (dp) => JSON.stringify(dp.attributes) === JSON.stringify(attributes),
50
- );
51
-
52
- this._exporter.reset();
53
-
54
- return dp?.value;
55
- }
56
-
57
- async collectAndExport(): Promise<void> {
58
- const result = await this.collect();
59
- await new Promise<void>((resolve, reject) => {
60
- this._exporter.export(result.resourceMetrics, (result) => {
61
- if (result.error != null) {
62
- reject(result.error);
63
- } else {
64
- resolve();
65
- }
66
- });
67
- });
68
- }
69
- }
5
+ import {
6
+ createTestMetricReader,
7
+ tearDownTestMetricReader,
8
+ } from "./testUtils.js";
70
9
 
71
10
  function setup() {
72
- const metricReader = new TestMetricReader();
73
- metrics.setGlobalMeterProvider(
74
- new MeterProvider({
75
- readers: [metricReader],
76
- }),
77
- );
78
-
11
+ const metricReader = createTestMetricReader();
79
12
  const queue = new PriorityBasedMessageQueue(CO_VALUE_PRIORITY.MEDIUM);
80
13
  return { queue, metricReader };
81
14
  }
82
15
 
83
16
  describe("PriorityBasedMessageQueue", () => {
84
17
  afterEach(() => {
85
- metrics.disable();
18
+ tearDownTestMetricReader();
86
19
  });
87
20
 
88
21
  test("should initialize with correct properties", () => {
@@ -2,12 +2,19 @@ import { describe, expect, onTestFinished, test, vi } from "vitest";
2
2
  import {
3
3
  GlobalSyncStateListenerCallback,
4
4
  PeerSyncStateListenerCallback,
5
- } from "../SyncStateSubscriptionManager.js";
5
+ } from "../SyncStateManager.js";
6
+ import { accountHeaderForInitialAgentSecret } from "../coValues/account.js";
6
7
  import { connectedPeers } from "../streamUtils.js";
7
8
  import { emptyKnownState } from "../sync.js";
8
- import { createTestNode, waitFor } from "./testUtils.js";
9
-
10
- describe("SyncStateSubscriptionManager", () => {
9
+ import {
10
+ blockMessageTypeOnOutgoingPeer,
11
+ createTestNode,
12
+ createTwoConnectedNodes,
13
+ loadCoValueOrFail,
14
+ waitFor,
15
+ } from "./testUtils.js";
16
+
17
+ describe("SyncStateManager", () => {
11
18
  test("subscribeToUpdates receives updates when peer state changes", async () => {
12
19
  // Setup nodes
13
20
  const client = createTestNode();
@@ -31,7 +38,7 @@ describe("SyncStateSubscriptionManager", () => {
31
38
  client.syncManager.addPeer(jazzCloudAsPeer);
32
39
  jazzCloud.syncManager.addPeer(clientAsPeer);
33
40
 
34
- const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
41
+ const subscriptionManager = client.syncManager.syncState;
35
42
 
36
43
  const updateSpy: GlobalSyncStateListenerCallback = vi.fn();
37
44
  const unsubscribe = subscriptionManager.subscribeToUpdates(updateSpy);
@@ -41,14 +48,14 @@ describe("SyncStateSubscriptionManager", () => {
41
48
  expect(updateSpy).toHaveBeenCalledWith(
42
49
  "jazzCloudConnection",
43
50
  emptyKnownState(map.core.id),
44
- { isUploaded: false },
51
+ { uploaded: false },
45
52
  );
46
53
 
47
54
  await waitFor(() => {
48
- return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
55
+ return subscriptionManager.getCurrentSyncState(
49
56
  "jazzCloudConnection",
50
57
  map.core.id,
51
- );
58
+ ).uploaded;
52
59
  });
53
60
 
54
61
  expect(updateSpy).toHaveBeenCalledWith(
@@ -56,7 +63,7 @@ describe("SyncStateSubscriptionManager", () => {
56
63
  client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
57
64
  map.core.id,
58
65
  )!,
59
- { isUploaded: true },
66
+ { uploaded: true },
60
67
  );
61
68
 
62
69
  // Cleanup
@@ -92,7 +99,7 @@ describe("SyncStateSubscriptionManager", () => {
92
99
  client.syncManager.addPeer(clientStoragePeer);
93
100
  jazzCloud.syncManager.addPeer(clientAsPeer);
94
101
 
95
- const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
102
+ const subscriptionManager = client.syncManager.syncState;
96
103
 
97
104
  const updateToJazzCloudSpy: PeerSyncStateListenerCallback = vi.fn();
98
105
  const updateToStorageSpy: PeerSyncStateListenerCallback = vi.fn();
@@ -114,26 +121,26 @@ describe("SyncStateSubscriptionManager", () => {
114
121
 
115
122
  expect(updateToJazzCloudSpy).toHaveBeenCalledWith(
116
123
  emptyKnownState(map.core.id),
117
- { isUploaded: false },
124
+ { uploaded: false },
118
125
  );
119
126
 
120
127
  await waitFor(() => {
121
- return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
128
+ return subscriptionManager.getCurrentSyncState(
122
129
  "jazzCloudConnection",
123
130
  map.core.id,
124
- );
131
+ ).uploaded;
125
132
  });
126
133
 
127
134
  expect(updateToJazzCloudSpy).toHaveBeenLastCalledWith(
128
135
  client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
129
136
  map.core.id,
130
137
  )!,
131
- { isUploaded: true },
138
+ { uploaded: true },
132
139
  );
133
140
 
134
141
  expect(updateToStorageSpy).toHaveBeenLastCalledWith(
135
142
  emptyKnownState(map.core.id),
136
- { isUploaded: false },
143
+ { uploaded: false },
137
144
  );
138
145
  });
139
146
 
@@ -162,27 +169,27 @@ describe("SyncStateSubscriptionManager", () => {
162
169
 
163
170
  await client.syncManager.actuallySyncCoValue(map.core);
164
171
 
165
- const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
172
+ const subscriptionManager = client.syncManager.syncState;
166
173
 
167
174
  expect(
168
- subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
175
+ subscriptionManager.getCurrentSyncState(
169
176
  "jazzCloudConnection",
170
177
  map.core.id,
171
- ),
178
+ ).uploaded,
172
179
  ).toBe(false);
173
180
 
174
181
  await waitFor(() => {
175
- return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
182
+ return subscriptionManager.getCurrentSyncState(
176
183
  "jazzCloudConnection",
177
184
  map.core.id,
178
- );
185
+ ).uploaded;
179
186
  });
180
187
 
181
188
  expect(
182
- subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
189
+ subscriptionManager.getCurrentSyncState(
183
190
  "jazzCloudConnection",
184
191
  map.core.id,
185
- ),
192
+ ).uploaded,
186
193
  ).toBe(true);
187
194
  });
188
195
 
@@ -209,7 +216,7 @@ describe("SyncStateSubscriptionManager", () => {
209
216
  client.syncManager.addPeer(jazzCloudAsPeer);
210
217
  jazzCloud.syncManager.addPeer(clientAsPeer);
211
218
 
212
- const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
219
+ const subscriptionManager = client.syncManager.syncState;
213
220
  const anyUpdateSpy = vi.fn();
214
221
  const unsubscribe1 = subscriptionManager.subscribeToUpdates(anyUpdateSpy);
215
222
  const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
@@ -225,12 +232,89 @@ describe("SyncStateSubscriptionManager", () => {
225
232
  anyUpdateSpy.mockClear();
226
233
 
227
234
  await waitFor(() => {
228
- return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
235
+ return subscriptionManager.getCurrentSyncState(
229
236
  "jazzCloudConnection",
230
237
  map.core.id,
231
- );
238
+ ).uploaded;
232
239
  });
233
240
 
234
241
  expect(anyUpdateSpy).not.toHaveBeenCalled();
235
242
  });
243
+
244
+ test("getCurrentSyncState should return the correct state", async () => {
245
+ // Setup nodes
246
+ const {
247
+ node1: clientNode,
248
+ node2: serverNode,
249
+ node1ToNode2Peer: clientToServerPeer,
250
+ node2ToNode1Peer: serverToClientPeer,
251
+ } = await createTwoConnectedNodes("client", "server");
252
+
253
+ // Create test data
254
+ const group = clientNode.node.createGroup();
255
+ const map = group.createMap();
256
+ map.set("key1", "value1", "trusting");
257
+ group.addMember("everyone", "writer");
258
+
259
+ // Initially should not be synced
260
+ expect(
261
+ clientNode.node.syncManager.syncState.getCurrentSyncState(
262
+ clientToServerPeer.id,
263
+ map.core.id,
264
+ ),
265
+ ).toEqual({ uploaded: false });
266
+
267
+ // Wait for full sync
268
+ await map.core.waitForSync();
269
+
270
+ expect(
271
+ clientNode.node.syncManager.syncState.getCurrentSyncState(
272
+ clientToServerPeer.id,
273
+ map.core.id,
274
+ ),
275
+ ).toEqual({ uploaded: true });
276
+
277
+ const mapOnServer = await loadCoValueOrFail(serverNode.node, map.id);
278
+
279
+ // Block the content messages so the client won't fully sync immediately
280
+ const outgoing = blockMessageTypeOnOutgoingPeer(
281
+ serverToClientPeer,
282
+ "content",
283
+ );
284
+
285
+ mapOnServer.set("key2", "value2", "trusting");
286
+
287
+ expect(
288
+ clientNode.node.syncManager.syncState.getCurrentSyncState(
289
+ clientToServerPeer.id,
290
+ map.core.id,
291
+ ),
292
+ ).toEqual({ uploaded: true });
293
+
294
+ expect(
295
+ serverNode.node.syncManager.syncState.getCurrentSyncState(
296
+ serverToClientPeer.id,
297
+ map.core.id,
298
+ ),
299
+ ).toEqual({ uploaded: false });
300
+
301
+ await outgoing.sendBlockedMessages();
302
+ outgoing.unblock();
303
+
304
+ await mapOnServer.core.waitForSync();
305
+
306
+ expect(
307
+ clientNode.node.syncManager.syncState.getCurrentSyncState(
308
+ clientToServerPeer.id,
309
+ map.core.id,
310
+ ),
311
+ ).toEqual({ uploaded: true });
312
+
313
+ expect(
314
+ serverNode.node.syncManager.syncState.getCurrentSyncState(
315
+ serverToClientPeer.id,
316
+ map.core.id,
317
+ ),
318
+ ).toEqual({ uploaded: true });
319
+ });
236
320
  });
@@ -75,10 +75,10 @@ test("Can get CoMap entry values at different points in time", () => {
75
75
  expect(content.atTime(beforeB).get("hello")).toEqual("A");
76
76
  expect(content.atTime(beforeC).get("hello")).toEqual("B");
77
77
 
78
- const ops = content.timeFilteredOps("hello");
78
+ const ops = content.ops["hello"]!;
79
79
 
80
80
  expect(content.atTime(beforeC).lastEditAt("hello")).toEqual(
81
- operationToEditEntry(ops![1]!),
81
+ operationToEditEntry(ops[1]!),
82
82
  );
83
83
  expect(content.atTime(beforeC).nthEditAt("hello", 0)).toEqual(
84
84
  operationToEditEntry(ops![0]!),