@dxos/echo-pipeline 0.6.13 → 0.6.14-main.2b6a0f3

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 (129) hide show
  1. package/dist/lib/browser/chunk-COV5H3SU.mjs +2060 -0
  2. package/dist/lib/browser/chunk-COV5H3SU.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +3477 -17
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +17 -7
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-7HHYCGUR.cjs → chunk-XHGWCBX6.cjs} +116 -64
  9. package/dist/lib/node/chunk-XHGWCBX6.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +3454 -35
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +25 -15
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/lib/{browser/chunk-UKXIJW43.mjs → node-esm/chunk-KKYLPT56.mjs} +103 -53
  16. package/dist/lib/node-esm/chunk-KKYLPT56.mjs.map +7 -0
  17. package/dist/lib/{browser/chunk-MPWFDDQK.mjs → node-esm/index.mjs} +1723 -342
  18. package/dist/lib/node-esm/index.mjs.map +7 -0
  19. package/dist/lib/node-esm/meta.json +1 -0
  20. package/dist/lib/node-esm/testing/index.mjs +562 -0
  21. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  22. package/dist/types/src/automerge/automerge-host.d.ts +24 -1
  23. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  24. package/dist/types/src/automerge/collection-synchronizer.d.ts +2 -0
  25. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  26. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  27. package/dist/types/src/automerge/echo-replicator.d.ts +3 -3
  28. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  29. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +3 -3
  30. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  31. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  32. package/dist/types/src/automerge/space-collection.d.ts +3 -2
  33. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  34. package/dist/types/src/db-host/automerge-metrics.d.ts +11 -0
  35. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -0
  36. package/dist/types/src/db-host/data-service.d.ts +3 -2
  37. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  38. package/dist/types/src/db-host/database-root.d.ts +20 -0
  39. package/dist/types/src/db-host/database-root.d.ts.map +1 -0
  40. package/dist/types/src/db-host/documents-iterator.d.ts +7 -0
  41. package/dist/types/src/db-host/documents-iterator.d.ts.map +1 -0
  42. package/dist/types/src/db-host/echo-host.d.ts +73 -0
  43. package/dist/types/src/db-host/echo-host.d.ts.map +1 -0
  44. package/dist/types/src/db-host/index.d.ts +5 -0
  45. package/dist/types/src/db-host/index.d.ts.map +1 -1
  46. package/dist/types/src/db-host/migration.d.ts +8 -0
  47. package/dist/types/src/db-host/migration.d.ts.map +1 -0
  48. package/dist/types/src/db-host/query-service.d.ts +25 -0
  49. package/dist/types/src/db-host/query-service.d.ts.map +1 -0
  50. package/dist/types/src/db-host/query-state.d.ts +41 -0
  51. package/dist/types/src/db-host/query-state.d.ts.map +1 -0
  52. package/dist/types/src/db-host/space-state-manager.d.ts +23 -0
  53. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -0
  54. package/dist/types/src/edge/echo-edge-replicator.d.ts +23 -0
  55. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -0
  56. package/dist/types/src/edge/echo-edge-replicator.test.d.ts +2 -0
  57. package/dist/types/src/edge/echo-edge-replicator.test.d.ts.map +1 -0
  58. package/dist/types/src/edge/index.d.ts +2 -0
  59. package/dist/types/src/edge/index.d.ts.map +1 -0
  60. package/dist/types/src/index.d.ts +1 -0
  61. package/dist/types/src/index.d.ts.map +1 -1
  62. package/dist/types/src/metadata/metadata-store.d.ts +4 -1
  63. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  64. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  65. package/dist/types/src/space/space.d.ts +1 -0
  66. package/dist/types/src/space/space.d.ts.map +1 -1
  67. package/dist/types/src/testing/test-agent-builder.d.ts +2 -0
  68. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  69. package/dist/types/src/testing/test-replicator.d.ts +4 -4
  70. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  71. package/package.json +41 -50
  72. package/src/automerge/automerge-host.test.ts +8 -9
  73. package/src/automerge/automerge-host.ts +46 -7
  74. package/src/automerge/automerge-repo.test.ts +18 -16
  75. package/src/automerge/collection-synchronizer.test.ts +10 -5
  76. package/src/automerge/collection-synchronizer.ts +17 -6
  77. package/src/automerge/echo-data-monitor.test.ts +1 -3
  78. package/src/automerge/echo-network-adapter.test.ts +4 -3
  79. package/src/automerge/echo-network-adapter.ts +5 -4
  80. package/src/automerge/echo-replicator.ts +3 -3
  81. package/src/automerge/mesh-echo-replicator-connection.ts +10 -9
  82. package/src/automerge/mesh-echo-replicator.ts +2 -1
  83. package/src/automerge/space-collection.ts +3 -2
  84. package/src/automerge/storage-adapter.test.ts +2 -3
  85. package/src/db-host/automerge-metrics.ts +38 -0
  86. package/src/db-host/data-service.ts +29 -14
  87. package/src/db-host/database-root.ts +87 -0
  88. package/src/db-host/documents-iterator.ts +73 -0
  89. package/src/db-host/documents-synchronizer.test.ts +2 -2
  90. package/src/db-host/echo-host.ts +257 -0
  91. package/src/db-host/index.ts +6 -1
  92. package/src/db-host/migration.ts +57 -0
  93. package/src/db-host/query-service.ts +209 -0
  94. package/src/db-host/query-state.ts +214 -0
  95. package/src/db-host/space-state-manager.ts +90 -0
  96. package/src/edge/echo-edge-replicator.test.ts +96 -0
  97. package/src/edge/echo-edge-replicator.ts +341 -0
  98. package/src/edge/index.ts +5 -0
  99. package/src/index.ts +1 -0
  100. package/src/metadata/metadata-store.ts +22 -2
  101. package/src/pipeline/pipeline-stress.test.ts +44 -47
  102. package/src/pipeline/pipeline.test.ts +3 -4
  103. package/src/space/control-pipeline.test.ts +2 -3
  104. package/src/space/control-pipeline.ts +10 -1
  105. package/src/space/replication.browser.test.ts +2 -8
  106. package/src/space/space-manager.browser.test.ts +6 -5
  107. package/src/space/space-protocol.browser.test.ts +29 -34
  108. package/src/space/space-protocol.test.ts +37 -27
  109. package/src/space/space-protocol.ts +0 -4
  110. package/src/space/space.test.ts +30 -11
  111. package/src/space/space.ts +7 -2
  112. package/src/testing/test-agent-builder.ts +16 -4
  113. package/src/testing/test-replicator.ts +3 -3
  114. package/dist/lib/browser/chunk-MPWFDDQK.mjs.map +0 -7
  115. package/dist/lib/browser/chunk-UKXIJW43.mjs.map +0 -7
  116. package/dist/lib/browser/chunk-XPCF2V5U.mjs +0 -31
  117. package/dist/lib/browser/chunk-XPCF2V5U.mjs.map +0 -7
  118. package/dist/lib/browser/light.mjs +0 -32
  119. package/dist/lib/browser/light.mjs.map +0 -7
  120. package/dist/lib/node/chunk-5DH4KR2S.cjs +0 -2148
  121. package/dist/lib/node/chunk-5DH4KR2S.cjs.map +0 -7
  122. package/dist/lib/node/chunk-7HHYCGUR.cjs.map +0 -7
  123. package/dist/lib/node/chunk-DZVH7HDD.cjs +0 -43
  124. package/dist/lib/node/chunk-DZVH7HDD.cjs.map +0 -7
  125. package/dist/lib/node/light.cjs +0 -52
  126. package/dist/lib/node/light.cjs.map +0 -7
  127. package/dist/types/src/light.d.ts +0 -4
  128. package/dist/types/src/light.d.ts.map +0 -1
  129. package/src/light.ts +0 -7
@@ -0,0 +1,341 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Mutex, scheduleTask } from '@dxos/async';
6
+ import * as A from '@dxos/automerge/automerge';
7
+ import { cbor } from '@dxos/automerge/automerge-repo';
8
+ import { Context, Resource } from '@dxos/context';
9
+ import { randomUUID } from '@dxos/crypto';
10
+ import type { CollectionId } from '@dxos/echo-protocol';
11
+ import { type EdgeConnection } from '@dxos/edge-client';
12
+ import { invariant } from '@dxos/invariant';
13
+ import type { SpaceId } from '@dxos/keys';
14
+ import { log } from '@dxos/log';
15
+ import { EdgeService, type AutomergeProtocolMessage, type PeerId } from '@dxos/protocols';
16
+ import { buf } from '@dxos/protocols/buf';
17
+ import {
18
+ type Message as RouterMessage,
19
+ MessageSchema as RouterMessageSchema,
20
+ } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
21
+ import { bufferToArray } from '@dxos/util';
22
+
23
+ import {
24
+ getSpaceIdFromCollectionId,
25
+ type EchoReplicator,
26
+ type EchoReplicatorContext,
27
+ type ReplicatorConnection,
28
+ type ShouldAdvertiseParams,
29
+ type ShouldSyncCollectionParams,
30
+ } from '../automerge';
31
+
32
+ /**
33
+ * Delay before restarting the connection after receiving a forbidden error.
34
+ */
35
+ const INITIAL_RESTART_DELAY = 500;
36
+ const RESTART_DELAY_JITTER = 250;
37
+ const MAX_RESTART_DELAY = 5000;
38
+
39
+ export type EchoEdgeReplicatorParams = {
40
+ edgeConnection: EdgeConnection;
41
+ disableSharePolicy?: boolean;
42
+ };
43
+
44
+ export class EchoEdgeReplicator implements EchoReplicator {
45
+ private readonly _edgeConnection: EdgeConnection;
46
+ private readonly _mutex = new Mutex();
47
+
48
+ private _ctx?: Context = undefined;
49
+ private _context: EchoReplicatorContext | null = null;
50
+ private _connectedSpaces = new Set<SpaceId>();
51
+ private _connections = new Map<SpaceId, EdgeReplicatorConnection>();
52
+ private _sharePolicyEnabled = true;
53
+
54
+ constructor({ edgeConnection, disableSharePolicy }: EchoEdgeReplicatorParams) {
55
+ this._edgeConnection = edgeConnection;
56
+ this._sharePolicyEnabled = !disableSharePolicy;
57
+ }
58
+
59
+ async connect(context: EchoReplicatorContext): Promise<void> {
60
+ log.info('connect', { peerId: context.peerId, connectedSpaces: this._connectedSpaces.size });
61
+ this._context = context;
62
+
63
+ this._ctx = Context.default();
64
+ this._edgeConnection.reconnect.on(this._ctx, async () => {
65
+ using _guard = await this._mutex.acquire();
66
+
67
+ const spaces = [...this._connectedSpaces];
68
+ for (const connection of this._connections.values()) {
69
+ await connection.close();
70
+ }
71
+ this._connections.clear();
72
+
73
+ if (this._context !== null) {
74
+ for (const spaceId of spaces) {
75
+ await this._openConnection(spaceId);
76
+ }
77
+ }
78
+ });
79
+
80
+ for (const spaceId of this._connectedSpaces) {
81
+ await this._openConnection(spaceId);
82
+ }
83
+ }
84
+
85
+ async disconnect(): Promise<void> {
86
+ using _guard = await this._mutex.acquire();
87
+ await this._ctx?.dispose();
88
+
89
+ for (const connection of this._connections.values()) {
90
+ await connection.close();
91
+ }
92
+ this._connections.clear();
93
+ }
94
+
95
+ async connectToSpace(spaceId: SpaceId) {
96
+ using _guard = await this._mutex.acquire();
97
+
98
+ this._connectedSpaces.add(spaceId);
99
+
100
+ // Check if AM-repo requested that we connect to remote peers.
101
+ if (this._context !== null) {
102
+ await this._openConnection(spaceId);
103
+ }
104
+ }
105
+
106
+ async disconnectFromSpace(spaceId: SpaceId) {
107
+ using _guard = await this._mutex.acquire();
108
+
109
+ this._connectedSpaces.delete(spaceId);
110
+
111
+ const connection = this._connections.get(spaceId);
112
+ if (connection) {
113
+ await connection.close();
114
+ this._connections.delete(spaceId);
115
+ }
116
+ }
117
+
118
+ private async _openConnection(spaceId: SpaceId, reconnects: number = 0) {
119
+ invariant(this._context);
120
+ invariant(!this._connections.has(spaceId));
121
+
122
+ let restartScheduled = false;
123
+
124
+ const connection = new EdgeReplicatorConnection({
125
+ edgeConnection: this._edgeConnection,
126
+ spaceId,
127
+ context: this._context,
128
+ sharedPolicyEnabled: this._sharePolicyEnabled,
129
+ onRemoteConnected: async () => {
130
+ this._context?.onConnectionOpen(connection);
131
+ },
132
+ onRemoteDisconnected: async () => {
133
+ this._context?.onConnectionClosed(connection);
134
+ },
135
+ onRestartRequested: async () => {
136
+ if (!this._ctx || restartScheduled) {
137
+ return;
138
+ }
139
+
140
+ const restartDelay =
141
+ Math.min(MAX_RESTART_DELAY, INITIAL_RESTART_DELAY * reconnects) + Math.random() * RESTART_DELAY_JITTER;
142
+
143
+ log.info('connection restart scheduled', { spaceId, reconnects, restartDelay });
144
+
145
+ restartScheduled = true;
146
+ scheduleTask(
147
+ this._ctx,
148
+ async () => {
149
+ using _guard = await this._mutex.acquire();
150
+ if (this._connections.get(spaceId) !== connection) {
151
+ return;
152
+ }
153
+
154
+ const ctx = this._ctx;
155
+ await connection.close(); // Will call onRemoteDisconnected
156
+ this._connections.delete(spaceId);
157
+ if (ctx?.disposed) {
158
+ return;
159
+ }
160
+ await this._openConnection(spaceId, reconnects + 1);
161
+ },
162
+ restartDelay,
163
+ );
164
+ },
165
+ });
166
+ this._connections.set(spaceId, connection);
167
+
168
+ await connection.open();
169
+ }
170
+ }
171
+
172
+ type EdgeReplicatorConnectionsParams = {
173
+ edgeConnection: EdgeConnection;
174
+ spaceId: SpaceId;
175
+ context: EchoReplicatorContext;
176
+ sharedPolicyEnabled: boolean;
177
+ onRemoteConnected: () => Promise<void>;
178
+ onRemoteDisconnected: () => Promise<void>;
179
+ onRestartRequested: () => Promise<void>;
180
+ };
181
+
182
+ class EdgeReplicatorConnection extends Resource implements ReplicatorConnection {
183
+ private readonly _edgeConnection: EdgeConnection;
184
+ private _remotePeerId: string | null = null;
185
+ private readonly _targetServiceId: string;
186
+ private readonly _spaceId: SpaceId;
187
+ private readonly _context: EchoReplicatorContext;
188
+ private readonly _sharedPolicyEnabled: boolean;
189
+ private readonly _onRemoteConnected: () => Promise<void>;
190
+ private readonly _onRemoteDisconnected: () => Promise<void>;
191
+ private readonly _onRestartRequested: () => void;
192
+
193
+ private _readableStreamController!: ReadableStreamDefaultController<AutomergeProtocolMessage>;
194
+
195
+ public readable: ReadableStream<AutomergeProtocolMessage>;
196
+ public writable: WritableStream<AutomergeProtocolMessage>;
197
+
198
+ constructor({
199
+ edgeConnection,
200
+ spaceId,
201
+ context,
202
+ sharedPolicyEnabled,
203
+ onRemoteConnected,
204
+ onRemoteDisconnected,
205
+ onRestartRequested,
206
+ }: EdgeReplicatorConnectionsParams) {
207
+ super();
208
+ this._edgeConnection = edgeConnection;
209
+ this._spaceId = spaceId;
210
+ this._context = context;
211
+
212
+ // Generate a unique peer id for every connection.
213
+ // This way automerge-repo will have separate sync states for every connection.
214
+ // This is important because the previous connection might have had some messages that failed to deliver
215
+ // abd if we don't clear the sync-state, automerge will not attempt to deliver them again.
216
+ this._remotePeerId = `${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}-${randomUUID()}`;
217
+ this._targetServiceId = `${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}`;
218
+ this._sharedPolicyEnabled = sharedPolicyEnabled;
219
+ this._onRemoteConnected = onRemoteConnected;
220
+ this._onRemoteDisconnected = onRemoteDisconnected;
221
+ this._onRestartRequested = onRestartRequested;
222
+
223
+ this.readable = new ReadableStream<AutomergeProtocolMessage>({
224
+ start: (controller) => {
225
+ this._readableStreamController = controller;
226
+ },
227
+ });
228
+
229
+ this.writable = new WritableStream<AutomergeProtocolMessage>({
230
+ write: async (message: AutomergeProtocolMessage, controller) => {
231
+ await this._sendMessage(message);
232
+ },
233
+ });
234
+ }
235
+
236
+ protected override async _open(ctx: Context): Promise<void> {
237
+ log('open');
238
+ // TODO: handle reconnects
239
+ this._ctx.onDispose(
240
+ this._edgeConnection.addListener((msg: RouterMessage) => {
241
+ this._onMessage(msg);
242
+ }),
243
+ );
244
+
245
+ await this._onRemoteConnected();
246
+ }
247
+
248
+ protected override async _close(): Promise<void> {
249
+ log('close');
250
+ this._readableStreamController.close();
251
+ await this._onRemoteDisconnected();
252
+ }
253
+
254
+ get peerId(): string {
255
+ invariant(this._remotePeerId, 'Not connected');
256
+ return this._remotePeerId;
257
+ }
258
+
259
+ async shouldAdvertise(params: ShouldAdvertiseParams): Promise<boolean> {
260
+ if (!this._sharedPolicyEnabled) {
261
+ return true;
262
+ }
263
+ const spaceId = await this._context.getContainingSpaceIdForDocument(params.documentId);
264
+ if (!spaceId) {
265
+ // There's no spaceId if the document is not present locally. This means the sharePolicy check is being
266
+ // performed on message reception, so spaceId check was already performed in _onMessage.
267
+ return true;
268
+ }
269
+ return spaceId === this._spaceId;
270
+ }
271
+
272
+ shouldSyncCollection(params: ShouldSyncCollectionParams): boolean {
273
+ if (!this._sharedPolicyEnabled) {
274
+ return true;
275
+ }
276
+ const spaceId = getSpaceIdFromCollectionId(params.collectionId as CollectionId);
277
+ return spaceId === this._spaceId;
278
+ }
279
+
280
+ private _onMessage(message: RouterMessage) {
281
+ if (message.serviceId !== this._targetServiceId) {
282
+ return;
283
+ }
284
+
285
+ const payload = cbor.decode(message.payload!.value) as AutomergeProtocolMessage;
286
+ log('recv', () => {
287
+ const decodedData =
288
+ payload.type === 'sync' && payload.data
289
+ ? A.decodeSyncMessage(payload.data)
290
+ : payload.type === 'collection-state'
291
+ ? (payload as any).state
292
+ : payload;
293
+ return { from: message.serviceId, type: payload.type, decodedData };
294
+ });
295
+ // Fix the peer id.
296
+ payload.senderId = this._remotePeerId! as PeerId;
297
+ this._processMessage(payload);
298
+ }
299
+
300
+ private _processMessage(message: AutomergeProtocolMessage) {
301
+ // There's a race between the credentials being replicated that are needed for access control and the data replication.
302
+ // AutomergeReplicator might return a Forbidden error if the credentials are not yet replicated.
303
+ // We restart the connection with some delay to account for that.
304
+ if (isForbiddenErrorMessage(message)) {
305
+ this._onRestartRequested();
306
+ return;
307
+ }
308
+
309
+ this._readableStreamController.enqueue(message);
310
+ }
311
+
312
+ private async _sendMessage(message: AutomergeProtocolMessage) {
313
+ // Fix the peer id.
314
+ (message as any).targetId = this._targetServiceId as PeerId;
315
+
316
+ log('send', {
317
+ type: message.type,
318
+ senderId: message.senderId,
319
+ targetId: (message as any).targetId,
320
+ documentId: (message as any).documentId,
321
+ });
322
+ const encoded = cbor.encode(message);
323
+
324
+ await this._edgeConnection.send(
325
+ buf.create(RouterMessageSchema, {
326
+ serviceId: this._targetServiceId,
327
+ source: {
328
+ identityKey: this._edgeConnection.identityKey,
329
+ peerKey: this._edgeConnection.peerKey,
330
+ },
331
+ payload: { value: bufferToArray(encoded) },
332
+ }),
333
+ );
334
+ }
335
+ }
336
+
337
+ /**
338
+ * This message is sent by EDGE AutomergeReplicator when the authorization is denied.
339
+ */
340
+ const isForbiddenErrorMessage = (message: AutomergeProtocolMessage) =>
341
+ message.type === 'error' && message.message === 'Forbidden';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './echo-edge-replicator';
package/src/index.ts CHANGED
@@ -8,3 +8,4 @@ export * from './metadata';
8
8
  export * from './pipeline';
9
9
  export * from './space';
10
10
  export * from './automerge';
11
+ export * from './edge';
@@ -20,6 +20,7 @@ import {
20
20
  type IdentityRecord,
21
21
  type SpaceCache,
22
22
  type LargeSpaceMetadata,
23
+ type EdgeReplicationSetting,
23
24
  } from '@dxos/protocols/proto/dxos/echo/metadata';
24
25
  import { type Directory, type File } from '@dxos/random-access-storage';
25
26
  import { type Timeframe } from '@dxos/timeframe';
@@ -229,11 +230,20 @@ export class MetadataStore {
229
230
  return this._metadata.identity.haloSpace;
230
231
  }
231
232
 
232
- const space = this.spaces.find((space) => space.key === spaceKey);
233
+ const space = this.spaces.find((space) => space.key.equals(spaceKey));
233
234
  invariant(space, 'Space not found');
234
235
  return space;
235
236
  }
236
237
 
238
+ hasSpace(spaceKey: PublicKey): boolean {
239
+ if (this._metadata.identity?.haloSpace.key.equals(spaceKey)) {
240
+ // Check if the space is the identity space.
241
+ return true;
242
+ }
243
+
244
+ return !!this.spaces.find((space) => space.key.equals(spaceKey));
245
+ }
246
+
237
247
  private _getLargeSpaceMetadata(key: PublicKey): LargeSpaceMetadata {
238
248
  let entry = this._spaceLargeMetadata.get(key);
239
249
  if (entry) {
@@ -288,7 +298,7 @@ export class MetadataStore {
288
298
 
289
299
  async addSpace(record: SpaceMetadata) {
290
300
  invariant(
291
- !(this._metadata.spaces ?? []).find((space) => space.key === record.key),
301
+ !(this._metadata.spaces ?? []).find((space) => space.key.equals(record.key)),
292
302
  'Cannot overwrite existing space in metadata',
293
303
  );
294
304
 
@@ -336,6 +346,16 @@ export class MetadataStore {
336
346
  await this._saveSpaceLargeMetadata(spaceKey);
337
347
  await this.flush();
338
348
  }
349
+
350
+ getSpaceEdgeReplicationSetting(spaceKey: PublicKey): EdgeReplicationSetting | undefined {
351
+ return this.hasSpace(spaceKey) ? this._getSpace(spaceKey).edgeReplication : undefined;
352
+ }
353
+
354
+ async setSpaceEdgeReplicationSetting(spaceKey: PublicKey, setting: EdgeReplicationSetting) {
355
+ this._getSpace(spaceKey).edgeReplication = setting;
356
+ await this._save();
357
+ await this.flush();
358
+ }
339
359
  }
340
360
 
341
361
  const fromBytesInt32 = (buf: Buffer) => buf.readInt32LE(0);
@@ -2,9 +2,9 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import expect from 'expect';
6
5
  import * as fc from 'fast-check';
7
- import { inspect } from 'util';
6
+ import { inspect } from 'node:util';
7
+ import { describe, expect, test } from 'vitest';
8
8
 
9
9
  import { asyncTimeout } from '@dxos/async';
10
10
  import { type FeedStore, type FeedWrapper } from '@dxos/feed-store';
@@ -12,7 +12,6 @@ import { PublicKey } from '@dxos/keys';
12
12
  import { log } from '@dxos/log';
13
13
  import { type FeedMessageBlock } from '@dxos/protocols';
14
14
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
15
- import { describe, test } from '@dxos/test';
16
15
  import { Timeframe } from '@dxos/timeframe';
17
16
  import { range } from '@dxos/util';
18
17
 
@@ -24,53 +23,51 @@ const NUM_MESSAGES = 10;
24
23
 
25
24
  // TODO(burdon): Describe test.
26
25
  describe('pipeline/stress test', () => {
27
- test
28
- .skip('stress', async () => {
29
- const builder = new TestFeedBuilder();
30
-
31
- const agentIds = range(NUM_AGENTS).map(() => PublicKey.random().toHex().slice(0, 8));
32
- const anAgentId = fc.constantFrom(...agentIds);
33
-
34
- const commands = fc.commands(
35
- [
36
- fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
37
- fc.constant(new SyncCommand()),
38
- anAgentId.map((agent) => new RestartCommand(agent)),
39
- ],
40
- { size: 'large' },
41
- );
42
-
43
- const model = fc.asyncProperty(commands, async (commands) => {
44
- const feedStore = builder.createFeedStore();
45
-
46
- const agents = new Map(agentIds.map((id) => [id, new Agent(builder, feedStore, id)]));
47
- await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
48
- await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
49
-
50
- const setup: fc.ModelRunSetup<Model, Real> = () => ({
51
- model: {},
52
- real: {
53
- feedStore,
54
- agents,
55
- },
56
- });
57
-
58
- try {
59
- await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
60
- } finally {
61
- await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
62
- await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
63
- }
26
+ test.skip('stress', { timeout: 60_000 }, async () => {
27
+ const builder = new TestFeedBuilder();
28
+
29
+ const agentIds = range(NUM_AGENTS).map(() => PublicKey.random().toHex().slice(0, 8));
30
+ const anAgentId = fc.constantFrom(...agentIds);
31
+
32
+ const commands = fc.commands(
33
+ [
34
+ fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
35
+ fc.constant(new SyncCommand()),
36
+ anAgentId.map((agent) => new RestartCommand(agent)),
37
+ ],
38
+ { size: 'large' },
39
+ );
40
+
41
+ const model = fc.asyncProperty(commands, async (commands) => {
42
+ const feedStore = builder.createFeedStore();
43
+
44
+ const agents = new Map(agentIds.map((id) => [id, new Agent(builder, feedStore, id)]));
45
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
46
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
47
+
48
+ const setup: fc.ModelRunSetup<Model, Real> = () => ({
49
+ model: {},
50
+ real: {
51
+ feedStore,
52
+ agents,
53
+ },
64
54
  });
65
55
 
66
- const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
67
- [[new WriteCommand(agentIds[0], 10), new WriteCommand(agentIds[1], 10), new SyncCommand()]],
68
- [[new WriteCommand(agentIds[0], 4), new RestartCommand(agentIds[0]), new SyncCommand()]],
69
- ];
56
+ try {
57
+ await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
58
+ } finally {
59
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
60
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
61
+ }
62
+ });
63
+
64
+ const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
65
+ [[new WriteCommand(agentIds[0], 10), new WriteCommand(agentIds[1], 10), new SyncCommand()]],
66
+ [[new WriteCommand(agentIds[0], 4), new RestartCommand(agentIds[0]), new SyncCommand()]],
67
+ ];
70
68
 
71
- await fc.assert(model, { examples });
72
- })
73
- .timeout(60_000);
69
+ await fc.assert(model, { examples });
70
+ });
74
71
  });
75
72
 
76
73
  class Agent {
@@ -2,11 +2,10 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import expect from 'expect';
5
+ import { describe, expect, test, onTestFinished } from 'vitest';
6
6
 
7
7
  import { Event, sleep } from '@dxos/async';
8
8
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
9
- import { describe, test, afterTest } from '@dxos/test';
10
9
  import { Timeframe } from '@dxos/timeframe';
11
10
  import { range } from '@dxos/util';
12
11
 
@@ -62,7 +61,7 @@ describe('pipeline/Pipeline', () => {
62
61
 
63
62
  test('reading and writing with cursor changes', async () => {
64
63
  const pipeline = new Pipeline();
65
- afterTest(() => pipeline.stop());
64
+ onTestFinished(() => pipeline.stop());
66
65
 
67
66
  const builder = new TestFeedBuilder();
68
67
  const feedStore = builder.createFeedStore();
@@ -105,7 +104,7 @@ describe('pipeline/Pipeline', () => {
105
104
 
106
105
  test('cursor change while polling', async () => {
107
106
  const pipeline = new Pipeline();
108
- afterTest(() => pipeline.stop());
107
+ onTestFinished(() => pipeline.stop());
109
108
 
110
109
  const builder = new TestFeedBuilder();
111
110
  const feedStore = builder.createFeedStore();
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import expect from 'expect';
5
+ import { describe, expect, test, onTestFinished } from 'vitest';
6
6
 
7
7
  import { CredentialGenerator, createCredential } from '@dxos/credentials';
8
8
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
@@ -12,7 +12,6 @@ import { log } from '@dxos/log';
12
12
  import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
13
13
  import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
14
14
  import { createStorage, StorageType } from '@dxos/random-access-storage';
15
- import { describe, test, afterTest } from '@dxos/test';
16
15
  import { Timeframe } from '@dxos/timeframe';
17
16
 
18
17
  import { ControlPipeline } from './control-pipeline';
@@ -62,7 +61,7 @@ describe('space/control-pipeline', () => {
62
61
  await controlPipeline.setWriteFeed(genesisFeed);
63
62
  await controlPipeline.start();
64
63
 
65
- afterTest(() => controlPipeline.stop());
64
+ onTestFinished(() => controlPipeline.stop());
66
65
 
67
66
  //
68
67
  // Genesis
@@ -160,7 +160,7 @@ export class ControlPipeline {
160
160
  };
161
161
  await this._pipeline.unpause();
162
162
 
163
- log('save snapshot', { key: this._spaceKey, snapshot });
163
+ log('save snapshot', { key: this._spaceKey, snapshot: getSnapshotLoggerContext(snapshot) });
164
164
  await this._metadata.setSpaceControlPipelineSnapshot(this._spaceKey, snapshot);
165
165
  }
166
166
 
@@ -229,3 +229,12 @@ export class ControlPipeline {
229
229
  }
230
230
  }
231
231
  }
232
+
233
+ const getSnapshotLoggerContext = (snapshot: ControlPipelineSnapshot) => {
234
+ return snapshot.messages?.map((msg) => {
235
+ const issuer = msg.credential.issuer;
236
+ const subject = msg.credential.subject.id;
237
+ const type = msg.credential.subject.assertion['@type'];
238
+ return { issuer, subject, type };
239
+ });
240
+ };
@@ -2,16 +2,12 @@
2
2
  // Copyright 2021 DXOS.org
3
3
  //
4
4
 
5
- // @dxos/test platform=browser
6
-
7
- import expect from 'expect';
8
- import waitForExpect from 'wait-for-expect';
5
+ import { describe, expect, test } from 'vitest';
9
6
 
10
7
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
11
8
  import { Keyring } from '@dxos/keyring';
12
9
  import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
13
10
  import { createStorage } from '@dxos/random-access-storage';
14
- import { describe, test } from '@dxos/test';
15
11
  import { Timeframe } from '@dxos/timeframe';
16
12
 
17
13
  import { valueEncoding } from '../common';
@@ -57,8 +53,6 @@ describe('replication', () => {
57
53
  timeframe: new Timeframe([[feed1.key, 123]]),
58
54
  });
59
55
 
60
- await waitForExpect(() => {
61
- expect(feed2.properties.length).toEqual(1);
62
- });
56
+ await expect.poll(() => feed2.properties.length).toEqual(1);
63
57
  });
64
58
  });
@@ -2,10 +2,9 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- // @dxos/test platform=browser
5
+ import { describe, test, onTestFinished } from 'vitest';
6
6
 
7
7
  import { createStorage } from '@dxos/random-access-storage';
8
- import { describe, test, afterTest } from '@dxos/test';
9
8
 
10
9
  import { TestAgentBuilder, WebsocketNetworkManagerProvider } from '../testing';
11
10
 
@@ -20,7 +19,9 @@ describe('space-manager', () => {
20
19
  storage: createStorage(),
21
20
  networkManagerProvider: WebsocketNetworkManagerProvider(SIGNAL_URL),
22
21
  });
23
- afterTest(async () => await builder.close());
22
+ onTestFinished(async () => {
23
+ await builder.close();
24
+ });
24
25
 
25
26
  const peer1 = await builder.createPeer();
26
27
  const spaceManager1 = peer1.spaceManager;
@@ -30,8 +31,8 @@ describe('space-manager', () => {
30
31
  const spaceManager2 = peer2.spaceManager;
31
32
  await spaceManager2.open();
32
33
 
33
- afterTest(() => spaceManager1.close());
34
- afterTest(() => spaceManager2.close());
34
+ onTestFinished(() => spaceManager1.close());
35
+ onTestFinished(() => spaceManager2.close());
35
36
 
36
37
  // const space1 = await spaceManager1.createSpace();
37
38
  // expect(space1.isOpen).to.be.true;