cojson 0.8.38 → 0.8.39

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 (33) hide show
  1. package/CHANGELOG.md +7 -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 +3 -0
  7. package/dist/native/coValueCore.js.map +1 -1
  8. package/dist/native/exports.js.map +1 -1
  9. package/dist/native/sync.js +34 -10
  10. package/dist/native/sync.js.map +1 -1
  11. package/dist/web/PeerState.js +11 -2
  12. package/dist/web/PeerState.js.map +1 -1
  13. package/dist/web/{SyncStateSubscriptionManager.js → SyncStateManager.js} +35 -24
  14. package/dist/web/SyncStateManager.js.map +1 -0
  15. package/dist/web/coValueCore.js +3 -0
  16. package/dist/web/coValueCore.js.map +1 -1
  17. package/dist/web/exports.js.map +1 -1
  18. package/dist/web/sync.js +34 -10
  19. package/dist/web/sync.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/PeerState.ts +12 -2
  22. package/src/{SyncStateSubscriptionManager.ts → SyncStateManager.ts} +48 -35
  23. package/src/coValueCore.ts +6 -0
  24. package/src/exports.ts +2 -1
  25. package/src/sync.ts +57 -23
  26. package/src/tests/PeerState.test.ts +49 -0
  27. package/src/tests/PriorityBasedMessageQueue.test.ts +6 -73
  28. package/src/tests/{SyncStateSubscriptionManager.test.ts → SyncStateManager.test.ts} +109 -25
  29. package/src/tests/group.test.ts +6 -9
  30. package/src/tests/sync.test.ts +112 -71
  31. package/src/tests/testUtils.ts +108 -4
  32. package/dist/native/SyncStateSubscriptionManager.js.map +0 -1
  33. 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
  });
@@ -87,12 +87,12 @@ test("Remove a member from a group where the admin role is inherited", async ()
87
87
  // The reader should be automatically kicked out of the child group
88
88
  await group.removeMember(node3.account);
89
89
 
90
- await node1.syncManager.waitForUploadIntoPeer(node1ToNode2Peer.id, group.id);
90
+ await group.core.waitForSync();
91
91
 
92
92
  // Update the map to check that node3 can't read updates anymore
93
93
  map.set("test", "Hidden to node3");
94
94
 
95
- await node2.syncManager.waitForUploadIntoPeer(node2ToNode3Peer.id, map.id);
95
+ await map.core.waitForSync();
96
96
 
97
97
  // Check that the value has not been updated on node3
98
98
  expect(mapOnNode3.get("test")).toEqual("Available to everyone");
@@ -119,16 +119,13 @@ test("An admin should be able to rotate the readKey on child groups and keep acc
119
119
  const childGroup = node2.createGroup();
120
120
  childGroup.extend(groupOnNode2);
121
121
 
122
- await node2.syncManager.waitForUploadIntoPeer(
123
- node2ToNode1Peer.id,
124
- childGroup.id,
125
- );
122
+ await childGroup.core.waitForSync();
126
123
 
127
124
  // The node1 account removes the reader from the group
128
125
  // In this case we want to ensure that node1 is still able to read new coValues
129
126
  // Even if some childs are not available when the readKey is rotated
130
127
  await group.removeMember(node3.account);
131
- await node1.syncManager.waitForUploadIntoPeer(node1ToNode2Peer.id, group.id);
128
+ await group.core.waitForSync();
132
129
 
133
130
  const map = childGroup.createMap();
134
131
  map.set("test", "Available to node1");
@@ -158,7 +155,7 @@ test("An admin should be able to rotate the readKey on child groups even if it w
158
155
  // In this case we want to ensure that node1 is still able to read new coValues
159
156
  // Even if some childs are not available when the readKey is rotated
160
157
  await group.removeMember(node3.account);
161
- await node1.syncManager.waitForUploadIntoPeer(node1ToNode2Peer.id, group.id);
158
+ await group.core.waitForSync();
162
159
 
163
160
  const map = childGroup.createMap();
164
161
  map.set("test", "Available to node1");
@@ -193,7 +190,7 @@ test("An admin should be able to rotate the readKey on child groups even if it w
193
190
  // In this case we want to ensure that node1 is still able to read new coValues
194
191
  // Even if some childs are not available when the readKey is rotated
195
192
  await group.removeMember(node3.account);
196
- await node1.syncManager.waitForUploadIntoPeer(node1ToNode2Peer.id, group.id);
193
+ await group.core.waitForSync();
197
194
 
198
195
  const map = childGroup.createMap();
199
196
  map.set("test", "Available to node1");