@dxos/client-services 0.6.12 → 0.6.13-main.041e8aa

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 (120) hide show
  1. package/dist/lib/browser/{chunk-TOAILL4T.mjs → chunk-LEYLUMAF.mjs} +5875 -5150
  2. package/dist/lib/browser/chunk-LEYLUMAF.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +3 -3
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +12 -7
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-H6C4XY6B.cjs → chunk-T2ZIGH7Z.cjs} +5923 -5202
  9. package/dist/lib/node/chunk-T2ZIGH7Z.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +46 -46
  11. package/dist/lib/node/index.cjs.map +3 -3
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +18 -13
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/lib/node-esm/chunk-EQQ2C4PE.mjs +8441 -0
  16. package/dist/lib/node-esm/chunk-EQQ2C4PE.mjs.map +7 -0
  17. package/dist/lib/node-esm/index.mjs +416 -0
  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 +424 -0
  21. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  22. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  23. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  24. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +2 -0
  25. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +1 -0
  26. package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
  27. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  28. package/dist/types/src/packlets/identity/identity-manager.d.ts +25 -9
  29. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  30. package/dist/types/src/packlets/identity/identity.d.ts +11 -3
  31. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  32. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  33. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +30 -0
  34. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -0
  35. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +2 -1
  36. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
  37. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +2 -1
  38. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  39. package/dist/types/src/packlets/invitations/invitation-state.d.ts +19 -0
  40. package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +1 -0
  41. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
  42. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  43. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  44. package/dist/types/src/packlets/services/service-context.d.ts +10 -9
  45. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  46. package/dist/types/src/packlets/services/service-host.d.ts +1 -0
  47. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  48. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +6 -3
  49. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  50. package/dist/types/src/packlets/spaces/data-space.d.ts +4 -3
  51. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  52. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +3 -0
  53. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  54. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +2 -0
  55. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +1 -0
  56. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
  57. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  58. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +31 -6
  59. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  60. package/dist/types/src/packlets/spaces/spaces-service.d.ts +1 -1
  61. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  62. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  63. package/dist/types/src/packlets/testing/test-builder.d.ts +1 -2
  64. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  65. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  66. package/dist/types/src/testing/setup.d.ts +3 -0
  67. package/dist/types/src/testing/setup.d.ts.map +1 -0
  68. package/dist/types/src/version.d.ts +1 -1
  69. package/dist/types/src/version.d.ts.map +1 -1
  70. package/package.json +43 -39
  71. package/src/packlets/devices/devices-service.test.ts +4 -5
  72. package/src/packlets/diagnostics/diagnostics-broadcast.ts +1 -0
  73. package/src/packlets/identity/{authenticator.test.ts → authenticator.node.test.ts} +2 -3
  74. package/src/packlets/identity/authenticator.ts +5 -2
  75. package/src/packlets/identity/contacts-service.ts +1 -1
  76. package/src/packlets/identity/identity-manager.test.ts +31 -16
  77. package/src/packlets/identity/identity-manager.ts +47 -31
  78. package/src/packlets/identity/identity-service.test.ts +4 -8
  79. package/src/packlets/identity/identity.test.ts +130 -239
  80. package/src/packlets/identity/identity.ts +56 -17
  81. package/src/packlets/invitations/device-invitation-protocol.test.ts +7 -4
  82. package/src/packlets/invitations/device-invitation-protocol.ts +5 -1
  83. package/src/packlets/invitations/edge-invitation-handler.ts +184 -0
  84. package/src/packlets/invitations/invitation-guest-extenstion.ts +8 -4
  85. package/src/packlets/invitations/invitation-host-extension.ts +8 -7
  86. package/src/packlets/invitations/invitation-state.ts +111 -0
  87. package/src/packlets/invitations/invitations-handler.test.ts +16 -9
  88. package/src/packlets/invitations/invitations-handler.ts +23 -92
  89. package/src/packlets/invitations/space-invitation-protocol.test.ts +4 -3
  90. package/src/packlets/invitations/space-invitation-protocol.ts +4 -0
  91. package/src/packlets/logging/logging.test.ts +1 -2
  92. package/src/packlets/network/network-service.test.ts +2 -3
  93. package/src/packlets/services/service-context.test.ts +3 -1
  94. package/src/packlets/services/service-context.ts +93 -37
  95. package/src/packlets/services/service-host.test.ts +8 -12
  96. package/src/packlets/services/service-host.ts +8 -6
  97. package/src/packlets/services/service-registry.test.ts +1 -2
  98. package/src/packlets/spaces/data-space-manager.test.ts +2 -2
  99. package/src/packlets/spaces/data-space-manager.ts +42 -7
  100. package/src/packlets/spaces/data-space.ts +35 -6
  101. package/src/packlets/spaces/edge-feed-replicator.test.ts +252 -0
  102. package/src/packlets/spaces/edge-feed-replicator.ts +80 -22
  103. package/src/packlets/spaces/epoch-migrations.ts +2 -2
  104. package/src/packlets/spaces/notarization-plugin.test.ts +10 -7
  105. package/src/packlets/spaces/notarization-plugin.ts +169 -29
  106. package/src/packlets/spaces/spaces-service.test.ts +5 -9
  107. package/src/packlets/spaces/spaces-service.ts +6 -1
  108. package/src/packlets/storage/storage.ts +0 -1
  109. package/src/packlets/system/system-service.test.ts +1 -2
  110. package/src/packlets/testing/test-builder.ts +7 -4
  111. package/src/packlets/worker/worker-runtime.ts +2 -2
  112. package/src/testing/setup.ts +11 -0
  113. package/src/version.ts +1 -5
  114. package/dist/lib/browser/chunk-TOAILL4T.mjs.map +0 -7
  115. package/dist/lib/node/chunk-H6C4XY6B.cjs.map +0 -7
  116. package/dist/types/src/packlets/identity/authenticator.test.d.ts +0 -2
  117. package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +0 -1
  118. package/dist/types/src/packlets/services/automerge-host.test.d.ts +0 -2
  119. package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +0 -1
  120. package/src/packlets/services/automerge-host.test.ts +0 -60
@@ -7,10 +7,15 @@ import { AUTH_TIMEOUT } from '@dxos/client-protocol';
7
7
  import { Context, ContextDisposedError, cancelWithContext } from '@dxos/context';
8
8
  import type { SpecificCredential } from '@dxos/credentials';
9
9
  import { timed, warnAfterTimeout } from '@dxos/debug';
10
- import { type EchoHost, type DatabaseRoot } from '@dxos/echo-db';
11
- import { createMappedFeedWriter, type MetadataStore, type Space } from '@dxos/echo-pipeline';
10
+ import {
11
+ type EchoHost,
12
+ type DatabaseRoot,
13
+ createMappedFeedWriter,
14
+ type MetadataStore,
15
+ type Space,
16
+ } from '@dxos/echo-pipeline';
12
17
  import { SpaceDocVersion } from '@dxos/echo-protocol';
13
- import type { EdgeConnection } from '@dxos/edge-client';
18
+ import type { EdgeConnection, EdgeHttpClient } from '@dxos/edge-client';
14
19
  import { type FeedStore, type FeedWrapper } from '@dxos/feed-store';
15
20
  import { failedInvariant } from '@dxos/invariant';
16
21
  import { type Keyring } from '@dxos/keyring';
@@ -75,6 +80,7 @@ export type DataSpaceParams = {
75
80
  callbacks?: DataSpaceCallbacks;
76
81
  cache?: SpaceCache;
77
82
  edgeConnection?: EdgeConnection;
83
+ edgeHttpClient?: EdgeHttpClient;
78
84
  edgeFeatures?: Runtime.Client.EdgeFeatures;
79
85
  };
80
86
 
@@ -96,7 +102,7 @@ export class DataSpace {
96
102
  private readonly _feedStore: FeedStore<FeedMessage>;
97
103
  private readonly _metadataStore: MetadataStore;
98
104
  private readonly _signingContext: SigningContext;
99
- private readonly _notarizationPlugin = new NotarizationPlugin();
105
+ private readonly _notarizationPlugin: NotarizationPlugin;
100
106
  private readonly _callbacks: DataSpaceCallbacks;
101
107
  private readonly _cache?: SpaceCache = undefined;
102
108
  private readonly _echoHost: EchoHost;
@@ -136,6 +142,11 @@ export class DataSpace {
136
142
  this._signingContext = params.signingContext;
137
143
  this._callbacks = params.callbacks ?? {};
138
144
  this._echoHost = params.echoHost;
145
+ this._notarizationPlugin = new NotarizationPlugin({
146
+ spaceId: this._inner.id,
147
+ edgeClient: params.edgeHttpClient,
148
+ edgeFeatures: params.edgeFeatures,
149
+ });
139
150
 
140
151
  this.authVerifier = new TrustedKeySetAuthVerifier({
141
152
  trustedKeysProvider: () =>
@@ -231,6 +242,7 @@ export class DataSpace {
231
242
  }
232
243
 
233
244
  await this._inner.open(new Context());
245
+ await this._inner.startProtocol();
234
246
 
235
247
  await this._edgeFeedReplicator?.open();
236
248
 
@@ -318,6 +330,7 @@ export class DataSpace {
318
330
  this._state = SpaceState.SPACE_INITIALIZING;
319
331
  log('new state', { state: SpaceState[this._state] });
320
332
 
333
+ log('initializing control pipeline');
321
334
  await this._initializeAndReadControlPipeline();
322
335
 
323
336
  // Allow other tasks to run before loading the data pipeline.
@@ -325,10 +338,13 @@ export class DataSpace {
325
338
 
326
339
  const ready = this.stateUpdate.waitForCondition(() => this._state === SpaceState.SPACE_READY);
327
340
 
341
+ log('initializing automerge root');
328
342
  this._automergeSpaceState.startProcessingRootDocs();
329
343
 
330
344
  // TODO(dmaretskyi): Change so `initializeDataPipeline` doesn't wait for the space to be READY, but rather any state with a valid root.
345
+ log('waiting for space to be ready');
331
346
  await ready;
347
+ log('space is ready');
332
348
  }
333
349
 
334
350
  private async _enterReadyState() {
@@ -345,6 +361,7 @@ export class DataSpace {
345
361
  private async _initializeAndReadControlPipeline() {
346
362
  await this._inner.controlPipeline.state.waitUntilReachedTargetTimeframe({
347
363
  ctx: this._ctx,
364
+ timeout: 10_000,
348
365
  breakOnStall: false,
349
366
  });
350
367
 
@@ -408,8 +425,16 @@ export class DataSpace {
408
425
  }
409
426
 
410
427
  if (credentials.length > 0) {
411
- // Never times out
412
- await this.notarizationPlugin.notarize({ ctx: this._ctx, credentials, timeout: 0 });
428
+ try {
429
+ log('will notarize credentials for feed admission', { count: credentials.length });
430
+ // Never times out
431
+ await this.notarizationPlugin.notarize({ ctx: this._ctx, credentials, timeout: 0 });
432
+
433
+ log('credentials notarized');
434
+ } catch (err) {
435
+ log.error('error notarizing credentials for feed admission', err);
436
+ throw err;
437
+ }
413
438
 
414
439
  // Set this after credentials are notarized so that on failure we will retry.
415
440
  await this._metadataStore.setWritableFeedKeys(this.key, this.inner.controlFeedKey!, this.inner.dataFeedKey!);
@@ -546,6 +571,10 @@ export class DataSpace {
546
571
  this.stateUpdate.emit();
547
572
  }
548
573
 
574
+ getEdgeReplicationSetting() {
575
+ return this._metadataStore.getSpaceEdgeReplicationSetting(this.key);
576
+ }
577
+
549
578
  private _onFeedAdded = async (feed: FeedWrapper<any>) => {
550
579
  await this._edgeFeedReplicator!.addFeed(feed);
551
580
  };
@@ -0,0 +1,252 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { decode as decodeCbor, encode as encodeCbor } from 'cbor-x';
6
+ import { getPort } from 'get-port-please';
7
+ import { describe, expect, onTestFinished, test, vi } from 'vitest';
8
+
9
+ import { sleep, Trigger } from '@dxos/async';
10
+ import { Context } from '@dxos/context';
11
+ import { valueEncoding } from '@dxos/echo-pipeline';
12
+ import { createEphemeralEdgeIdentity, EdgeClient, EdgeIdentityChangedError } from '@dxos/edge-client';
13
+ import { createTestEdgeWsServer } from '@dxos/edge-client/testing';
14
+ import { FeedFactory, FeedStore, type FeedWrapper } from '@dxos/feed-store';
15
+ import { Keyring } from '@dxos/keyring';
16
+ import { SpaceId } from '@dxos/keys';
17
+ import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
18
+ import { createStorage } from '@dxos/random-access-storage';
19
+ import { openAndClose } from '@dxos/test-utils';
20
+ import { Timeframe } from '@dxos/timeframe';
21
+ import { range } from '@dxos/util';
22
+
23
+ import { EdgeFeedReplicator } from './edge-feed-replicator';
24
+
25
+ describe('EdgeFeedReplicator', () => {
26
+ test('requests metadata after connection is open', async () => {
27
+ const { endpoint, admitConnection, messageSink } = await createEdge();
28
+ const { messenger, sendSpy } = await createClient(endpoint);
29
+
30
+ await attachReplicator(messenger);
31
+
32
+ await sleep(50);
33
+
34
+ expect(sendSpy).not.toHaveBeenCalled();
35
+ expect(messageSink.length).toEqual(0);
36
+
37
+ admitConnection.wake();
38
+ await expect.poll(() => sendSpy.mock.calls.length).toEqual(1);
39
+ expect(messageSink.length).toEqual(1);
40
+ expect(messageSink[0].type).toEqual('get-metadata');
41
+ });
42
+
43
+ test('replicates if added to a connected client', async () => {
44
+ const { endpoint, admitConnection, messageSink } = await createEdge();
45
+ const { messenger } = await createClient(endpoint);
46
+ admitConnection.wake();
47
+ await expect.poll(() => messenger.isConnected).toBeTruthy();
48
+
49
+ await attachReplicator(messenger);
50
+ await expect.poll(() => messageSink.length).toEqual(1);
51
+ });
52
+
53
+ test('sends a block', async () => {
54
+ const { endpoint, admitConnection, messageSink } = await createEdge();
55
+ const { messenger } = await createClient(endpoint);
56
+
57
+ const { feed } = await attachReplicator(messenger);
58
+
59
+ admitConnection.wake();
60
+ await appendMessage(feed);
61
+
62
+ await expect.poll(() => messageSink.length).toEqual(2);
63
+ expect(messageSink[1].type).toEqual('data');
64
+ });
65
+
66
+ test('re-requests metadata on reconnect', async () => {
67
+ const { endpoint, admitConnection, messageSink } = await createEdge();
68
+ const { messenger } = await createClient(endpoint);
69
+
70
+ await attachReplicator(messenger);
71
+
72
+ admitConnection.wake();
73
+ await expect.poll(() => messageSink.length).toEqual(1);
74
+
75
+ await updateIdentity(messenger);
76
+ await messenger.reconnect.waitForCount(1);
77
+
78
+ await expect.poll(() => messageSink.length).toEqual(2);
79
+ expect(messageSink[1].type).toEqual('get-metadata');
80
+ });
81
+
82
+ test('recovers after query sending failure during identity change', async () => {
83
+ const { endpoint, admitConnection, messageSink } = await createEdge();
84
+ const { messenger, sendSpy } = await createClient(endpoint);
85
+
86
+ await attachReplicator(messenger);
87
+
88
+ sendSpy.mockImplementationOnce(() => {
89
+ throw new EdgeIdentityChangedError(); // Hard to mock the exact race condition for when this error is thrown
90
+ });
91
+ admitConnection.wake();
92
+
93
+ await expect.poll(() => sendSpy.mock.calls.length).toEqual(1);
94
+ expect(messageSink.length).toEqual(0);
95
+ await updateIdentity(messenger);
96
+
97
+ await expect.poll(() => messageSink.length).toEqual(1);
98
+ expect(messageSink[0].type).toEqual('get-metadata');
99
+ });
100
+
101
+ test('recovers after response sending failure during identity change', async () => {
102
+ const { endpoint, admitConnection, messageSink, sendResponseMessage } = await createEdge();
103
+ const { messenger, sendSpy } = await createClient(endpoint);
104
+
105
+ const { feed } = await attachReplicator(messenger);
106
+ await appendMessage(feed);
107
+
108
+ sendSpy.mockImplementationOnce(async (request: any) => {
109
+ sendResponseMessage(request, encodeCbor({ type: 'metadata', feedKey: feed.key.toHex(), length: 0 }));
110
+ return Promise.resolve();
111
+ });
112
+ sendSpy.mockImplementationOnce(async () => {
113
+ throw new EdgeIdentityChangedError();
114
+ });
115
+ admitConnection.wake();
116
+
117
+ await expect.poll(() => sendSpy.mock.calls.length).toEqual(2);
118
+ sendSpy.mockRestore();
119
+ expect(messageSink.length).toEqual(0);
120
+ await updateIdentity(messenger);
121
+
122
+ await messenger.reconnect.waitForCount(1);
123
+ await expect.poll(() => messageSink.find((msg) => msg.type === 'data')).toBeDefined();
124
+ });
125
+
126
+ test('propagates errors unrelated to reconnect', async () => {
127
+ const { endpoint, admitConnection } = await createEdge();
128
+ const { messenger, sendSpy } = await createClient(endpoint);
129
+
130
+ const { replicator } = await attachReplicator(messenger, { skipOpen: true });
131
+ const raised = new Trigger();
132
+ await replicator.open(new Context({ onError: () => raised.wake() }));
133
+ onTestFinished(async () => {
134
+ await replicator.close();
135
+ });
136
+
137
+ sendSpy.mockImplementationOnce(() => {
138
+ throw new Error();
139
+ });
140
+ admitConnection.wake();
141
+
142
+ await raised.wait();
143
+ });
144
+
145
+ test('identity update before connected', async () => {
146
+ const { endpoint, admitConnection, messageSink } = await createEdge();
147
+ const { messenger } = await createClient(endpoint);
148
+
149
+ await attachReplicator(messenger);
150
+ await updateIdentity(messenger);
151
+ await sleep(100);
152
+ admitConnection.wake();
153
+
154
+ await expect.poll(() => messageSink.length).toEqual(2);
155
+ expect(messageSink.map((m) => m.type)).toStrictEqual(range(2, () => 'get-metadata'));
156
+ });
157
+
158
+ test('block appended during reconnect', async () => {
159
+ const { endpoint, admitConnection, feedLength } = await createEdge();
160
+ const { messenger } = await createClient(endpoint);
161
+
162
+ const { feed } = await attachReplicator(messenger);
163
+ admitConnection.wake();
164
+ await sleep(10);
165
+
166
+ admitConnection.reset();
167
+ await updateIdentity(messenger);
168
+ await appendMessage(feed);
169
+ await sleep(20);
170
+ admitConnection.wake();
171
+
172
+ await expect.poll(() => feedLength()).toEqual(1);
173
+ });
174
+
175
+ test('reconnect during block append', async () => {
176
+ const { endpoint, admitConnection, feedLength } = await createEdge();
177
+ const { messenger } = await createClient(endpoint);
178
+
179
+ const { feed } = await attachReplicator(messenger);
180
+ admitConnection.wake();
181
+ await sleep(10);
182
+
183
+ void appendMessage(feed);
184
+ await updateIdentity(messenger);
185
+
186
+ await expect.poll(() => feedLength()).toEqual(1);
187
+ });
188
+
189
+ const createEdge = async () => {
190
+ const port = await getPort({ host: 'localhost', port: 7200, portRange: [7200, 7299] });
191
+ let lastBlockIndex = -1;
192
+ const admitConnection = new Trigger();
193
+ const { cleanup, endpoint, messageSink, sendResponseMessage } = await createTestEdgeWsServer(port, {
194
+ admitConnection,
195
+ payloadDecoder: decodeCbor,
196
+ messageHandler: async (message: any) => {
197
+ if (message.type === 'get-metadata') {
198
+ return encodeCbor({ type: 'metadata', feedKey: message.feedKey, length: lastBlockIndex + 1 });
199
+ } else {
200
+ lastBlockIndex = Math.max(lastBlockIndex, message.blocks[message.blocks.length - 1].index);
201
+ }
202
+ },
203
+ });
204
+ onTestFinished(cleanup);
205
+
206
+ return {
207
+ endpoint,
208
+ messageSink,
209
+ admitConnection,
210
+ sendResponseMessage,
211
+ feedLength: () => lastBlockIndex + 1,
212
+ };
213
+ };
214
+
215
+ const createClient = async (endpoint: string) => {
216
+ const messenger = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: endpoint });
217
+ const sendSpy = vi.spyOn(messenger, 'send');
218
+ await openAndClose(messenger);
219
+ return { messenger, sendSpy };
220
+ };
221
+
222
+ const attachReplicator = async (messenger: EdgeClient, options?: { skipOpen?: boolean }) => {
223
+ const spaceId = SpaceId.random();
224
+ const feed = await createNewFeed();
225
+ const replicator = new EdgeFeedReplicator({ messenger, spaceId });
226
+ await replicator.addFeed(feed);
227
+ if (!options?.skipOpen) {
228
+ await openAndClose(replicator);
229
+ }
230
+ return { feed, replicator };
231
+ };
232
+
233
+ const createNewFeed = async () => {
234
+ const storage = createStorage();
235
+ const keyring = new Keyring();
236
+ const feedStore = new FeedStore<FeedMessage>({
237
+ factory: new FeedFactory<FeedMessage>({
238
+ root: storage.createDirectory(),
239
+ signer: keyring,
240
+ hypercore: { valueEncoding },
241
+ }),
242
+ });
243
+ onTestFinished(() => feedStore.close());
244
+ return feedStore.openFeed(await keyring.createKey(), { writable: true });
245
+ };
246
+
247
+ const updateIdentity = async (messenger: EdgeClient) => {
248
+ messenger.setIdentity(await createEphemeralEdgeIdentity());
249
+ };
250
+
251
+ const appendMessage = (feed: FeedWrapper<FeedMessage>) => feed.append({ timeframe: new Timeframe() });
252
+ });
@@ -5,15 +5,19 @@
5
5
  import { decode as decodeCbor, encode as encodeCbor } from 'cbor-x';
6
6
 
7
7
  import { Event, Mutex, scheduleMicroTask } from '@dxos/async';
8
- import { Resource, type Context } from '@dxos/context';
8
+ import { Context, Resource } from '@dxos/context';
9
9
  import { type EdgeConnection } from '@dxos/edge-client';
10
+ import { EdgeConnectionClosedError, EdgeIdentityChangedError } from '@dxos/edge-client';
10
11
  import { type FeedWrapper } from '@dxos/feed-store';
11
12
  import { invariant } from '@dxos/invariant';
12
13
  import { PublicKey, type SpaceId } from '@dxos/keys';
13
- import { log } from '@dxos/log';
14
+ import { log, logInfo } from '@dxos/log';
14
15
  import { EdgeService } from '@dxos/protocols';
15
16
  import { buf } from '@dxos/protocols/buf';
16
- import { MessageSchema as RouterMessageSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
17
+ import {
18
+ MessageSchema as RouterMessageSchema,
19
+ type Message as RouterMessage,
20
+ } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
17
21
  import type { FeedBlock, ProtocolMessage } from '@dxos/protocols/feed-replication';
18
22
  import { ComplexMap, arrayToBuffer, bufferToArray, defaultMap, rangeFromTo } from '@dxos/util';
19
23
 
@@ -24,7 +28,10 @@ export type EdgeFeedReplicatorParams = {
24
28
 
25
29
  export class EdgeFeedReplicator extends Resource {
26
30
  private readonly _messenger: EdgeConnection;
31
+
32
+ @logInfo
27
33
  private readonly _spaceId: SpaceId;
34
+
28
35
  private readonly _feeds = new ComplexMap<PublicKey, FeedWrapper<any>>(PublicKey.hash);
29
36
 
30
37
  private _connectionCtx?: Context = undefined;
@@ -46,9 +53,10 @@ export class EdgeFeedReplicator extends Resource {
46
53
  }
47
54
 
48
55
  protected override async _open(): Promise<void> {
56
+ log('open');
49
57
  // TODO: handle reconnects
50
58
  this._ctx.onDispose(
51
- this._messenger.addListener(async (message) => {
59
+ this._messenger.addListener((message: RouterMessage) => {
52
60
  if (!message.serviceId) {
53
61
  return;
54
62
  }
@@ -64,21 +72,40 @@ export class EdgeFeedReplicator extends Resource {
64
72
  }
65
73
 
66
74
  const payload = decodeCbor(message.payload!.value) as ProtocolMessage;
67
- log.info('recv', { from: message.source, payload });
75
+ log('receive', { from: message.source, feedKey: payload.feedKey, type: payload.type });
68
76
  this._onMessage(payload);
69
77
  }),
70
78
  );
71
79
 
72
- this._connected = true;
73
- this._connectionCtx = this._ctx.derive();
74
- for (const feed of this._feeds.values()) {
75
- await this._replicateFeed(feed);
80
+ this._messenger.connected.on(this._ctx, async () => {
81
+ await this._resetConnection();
82
+ this._startReplication();
83
+ });
84
+
85
+ if (this._messenger.isConnected) {
86
+ this._startReplication();
76
87
  }
77
88
  }
78
89
 
79
90
  protected override async _close(): Promise<void> {
80
- this._connected = false;
91
+ log('close');
92
+ await this._resetConnection();
93
+ }
81
94
 
95
+ private _startReplication() {
96
+ this._connected = true;
97
+ const connectionCtx = this._createConnectionContext();
98
+ this._connectionCtx = connectionCtx;
99
+ log('connection context created');
100
+ scheduleMicroTask(connectionCtx, async () => {
101
+ for (const feed of this._feeds.values()) {
102
+ await this._replicateFeed(connectionCtx, feed);
103
+ }
104
+ });
105
+ }
106
+
107
+ private async _resetConnection() {
108
+ log('resetConnection');
82
109
  this._connected = false;
83
110
  await this._connectionCtx?.dispose();
84
111
  this._connectionCtx = undefined;
@@ -86,11 +113,11 @@ export class EdgeFeedReplicator extends Resource {
86
113
  }
87
114
 
88
115
  async addFeed(feed: FeedWrapper<any>) {
89
- log.info('addFeed', { key: feed.key });
116
+ log.info('addFeed', { key: feed.key, connected: this._connected, hasConnectionCtx: !!this._connectionCtx });
90
117
  this._feeds.set(feed.key, feed);
91
118
 
92
- if (this._connected) {
93
- await this._replicateFeed(feed);
119
+ if (this._connected && this._connectionCtx) {
120
+ await this._replicateFeed(this._connectionCtx, feed);
94
121
  }
95
122
  }
96
123
 
@@ -98,25 +125,32 @@ export class EdgeFeedReplicator extends Resource {
98
125
  return defaultMap(this._pushMutex, key, () => new Mutex());
99
126
  }
100
127
 
101
- private async _replicateFeed(feed: FeedWrapper<any>) {
102
- invariant(this._connectionCtx);
103
-
128
+ private async _replicateFeed(ctx: Context, feed: FeedWrapper<any>) {
129
+ log('replicateFeed', { key: feed.key });
104
130
  await this._sendMessage({
105
131
  type: 'get-metadata',
106
132
  feedKey: feed.key.toHex(),
107
133
  });
108
134
 
109
- Event.wrap(feed.core as any, 'append').on(this._connectionCtx, async () => {
135
+ Event.wrap(feed.core as any, 'append').on(ctx, async () => {
110
136
  await this._pushBlocksIfNeeded(feed);
111
137
  });
112
138
  }
113
139
 
114
140
  private async _sendMessage(message: ProtocolMessage) {
115
- log.info('sending message', { message });
141
+ if (!this._connectionCtx) {
142
+ log.info('message dropped because connection was disposed');
143
+ return;
144
+ }
145
+
146
+ const logPayload =
147
+ message.type === 'data' ? { feedKey: message.feedKey, blocks: message.blocks.map((b) => b.index) } : { message };
148
+ log.info('sending message', logPayload);
116
149
 
117
150
  invariant(message.feedKey);
118
151
  const payloadValue = bufferToArray(encodeCbor(message));
119
152
 
153
+ log('send', { type: message.type });
120
154
  await this._messenger.send(
121
155
  buf.create(RouterMessageSchema, {
122
156
  source: {
@@ -130,11 +164,15 @@ export class EdgeFeedReplicator extends Resource {
130
164
  }
131
165
 
132
166
  private _onMessage(message: ProtocolMessage) {
133
- log.info('received message', { message });
134
-
135
- scheduleMicroTask(this._ctx, async () => {
167
+ if (!this._connectionCtx) {
168
+ log.warn('received message after connection context was disposed');
169
+ return;
170
+ }
171
+ scheduleMicroTask(this._connectionCtx, async () => {
136
172
  switch (message.type) {
137
173
  case 'metadata': {
174
+ log.info('received metadata', { message });
175
+
138
176
  const feedKey = PublicKey.fromHex(message.feedKey);
139
177
  const feed = this._feeds.get(feedKey);
140
178
  if (!feed) {
@@ -160,6 +198,8 @@ export class EdgeFeedReplicator extends Resource {
160
198
  }
161
199
 
162
200
  case 'data': {
201
+ log.info('received data', { feed: message.feedKey, blocks: message.blocks.map((b) => b.index) });
202
+
163
203
  const feedKey = PublicKey.fromHex(message.feedKey);
164
204
  const feed = this._feeds.get(feedKey);
165
205
  if (!feed) {
@@ -223,9 +263,10 @@ export class EdgeFeedReplicator extends Resource {
223
263
  }
224
264
 
225
265
  private async _pushBlocksIfNeeded(feed: FeedWrapper<any>) {
226
- using _guard = await this._getPushMutex(feed.key).acquire();
266
+ using _ = await this._getPushMutex(feed.key).acquire();
227
267
 
228
268
  if (!this._remoteLength.has(feed.key)) {
269
+ log('blocks not pushed because remote length is unknown');
229
270
  return;
230
271
  }
231
272
 
@@ -234,6 +275,23 @@ export class EdgeFeedReplicator extends Resource {
234
275
  await this._pushBlocks(feed, remoteLength, feed.length);
235
276
  }
236
277
  }
278
+
279
+ private _createConnectionContext() {
280
+ const connectionCtx = new Context({
281
+ onError: async (err: any) => {
282
+ if (connectionCtx !== this._connectionCtx) {
283
+ return;
284
+ }
285
+ if (err instanceof EdgeIdentityChangedError || err instanceof EdgeConnectionClosedError) {
286
+ log('resetting on reconnect');
287
+ await this._resetConnection();
288
+ } else {
289
+ this._ctx.raise(err);
290
+ }
291
+ },
292
+ });
293
+ return connectionCtx;
294
+ }
237
295
  }
238
296
 
239
297
  // hypercore requires buffers
@@ -4,13 +4,13 @@
4
4
 
5
5
  import type { AutomergeUrl } from '@dxos/automerge/automerge-repo';
6
6
  import { type Context } from '@dxos/context';
7
+ import { migrateDocument } from '@dxos/echo-db';
7
8
  import {
8
9
  convertLegacyReferences,
9
10
  convertLegacySpaceRootDoc,
10
11
  findInlineObjectOfType,
11
- migrateDocument,
12
12
  type EchoHost,
13
- } from '@dxos/echo-db';
13
+ } from '@dxos/echo-pipeline';
14
14
  import { SpaceDocVersion, type SpaceDoc } from '@dxos/echo-protocol';
15
15
  import { TYPE_PROPERTIES } from '@dxos/echo-schema';
16
16
  import { invariant } from '@dxos/invariant';
@@ -2,27 +2,28 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
5
+ import { onTestFinished, describe, expect, test } from 'vitest';
6
6
 
7
7
  import { Context } from '@dxos/context';
8
8
  import { CredentialGenerator } from '@dxos/credentials';
9
9
  import { MockFeedWriter } from '@dxos/feed-store/testing';
10
10
  import { Keyring } from '@dxos/keyring';
11
+ import { SpaceId } from '@dxos/keys';
11
12
  import { log } from '@dxos/log';
12
13
  import { AdmittedFeed, type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
13
14
  import { TestBuilder, type TestConnection, TestPeer } from '@dxos/teleport/testing';
14
- import { afterTest, describe, test } from '@dxos/test';
15
15
 
16
- import { NotarizationPlugin } from './notarization-plugin';
16
+ import { NotarizationPlugin, type NotarizationPluginParams } from './notarization-plugin';
17
17
 
18
18
  class TestAgent extends TestPeer {
19
19
  private readonly _ctx = new Context();
20
20
 
21
21
  feed = new MockFeedWriter<Credential>();
22
- notarizationPlugin = new NotarizationPlugin();
22
+ notarizationPlugin: NotarizationPlugin;
23
23
 
24
- constructor() {
24
+ constructor(params: NotarizationPluginParams) {
25
25
  super();
26
+ this.notarizationPlugin = new NotarizationPlugin(params);
26
27
  this.feed.written.on(this._ctx, async ([credential]) => {
27
28
  log('written to feed', { credential });
28
29
  await this.notarizationPlugin.processCredential(credential);
@@ -49,10 +50,12 @@ class TestAgent extends TestPeer {
49
50
  describe('NotarizationPlugin', () => {
50
51
  test('notarize single credential', async () => {
51
52
  const testBuilder = new TestBuilder();
52
- afterTest(() => testBuilder.destroy());
53
+ onTestFinished(() => testBuilder.destroy());
54
+
55
+ const params = { spaceId: SpaceId.random() };
53
56
 
54
57
  // peer0 is there to test retries.
55
- const [_peer0, peer1, peer2] = await testBuilder.createPeers({ factory: () => new TestAgent() });
58
+ const [_peer0, peer1, peer2] = await testBuilder.createPeers({ factory: () => new TestAgent(params) });
56
59
  peer1.enableWriting();
57
60
 
58
61
  peer1.feed.written.on(async ([credential]) => {