@dxos/client-services 0.6.12-main.78ddbdf → 0.6.12-main.89e9959

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 (53) hide show
  1. package/dist/lib/browser/{chunk-XSFLJVDP.mjs → chunk-XVI3VSJT.mjs} +637 -292
  2. package/dist/lib/browser/chunk-XVI3VSJT.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1 -1
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +2 -2
  6. package/dist/lib/browser/testing/index.mjs.map +2 -2
  7. package/dist/lib/node/{chunk-F3WGFGEN.cjs → chunk-NZL66D6K.cjs} +727 -382
  8. package/dist/lib/node/chunk-NZL66D6K.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +45 -45
  10. package/dist/lib/node/meta.json +1 -1
  11. package/dist/lib/node/testing/index.cjs +8 -8
  12. package/dist/lib/node/testing/index.cjs.map +2 -2
  13. package/dist/lib/node-esm/{chunk-3HDLTAT2.mjs → chunk-6747X7GN.mjs} +637 -292
  14. package/dist/lib/node-esm/chunk-6747X7GN.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1 -1
  16. package/dist/lib/node-esm/meta.json +1 -1
  17. package/dist/lib/node-esm/testing/index.mjs +2 -2
  18. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  19. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  20. package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
  21. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  22. package/dist/types/src/packlets/identity/identity.d.ts +1 -0
  23. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  24. package/dist/types/src/packlets/services/service-context.d.ts +4 -2
  25. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  26. package/dist/types/src/packlets/services/service-host.d.ts +1 -0
  27. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  28. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +3 -1
  29. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  30. package/dist/types/src/packlets/spaces/data-space.d.ts +2 -1
  31. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  32. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +2 -0
  33. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  34. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +31 -6
  35. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  36. package/dist/types/src/version.d.ts +1 -1
  37. package/package.json +38 -38
  38. package/src/packlets/identity/authenticator.ts +5 -2
  39. package/src/packlets/identity/contacts-service.ts +1 -1
  40. package/src/packlets/identity/identity.ts +4 -0
  41. package/src/packlets/services/service-context.ts +41 -17
  42. package/src/packlets/services/service-host.ts +7 -5
  43. package/src/packlets/spaces/data-space-manager.ts +5 -1
  44. package/src/packlets/spaces/data-space.ts +23 -4
  45. package/src/packlets/spaces/edge-feed-replicator.test.ts +22 -15
  46. package/src/packlets/spaces/edge-feed-replicator.ts +45 -25
  47. package/src/packlets/spaces/notarization-plugin.test.ts +8 -4
  48. package/src/packlets/spaces/notarization-plugin.ts +169 -29
  49. package/src/packlets/testing/test-builder.ts +1 -1
  50. package/src/version.ts +1 -1
  51. package/dist/lib/browser/chunk-XSFLJVDP.mjs.map +0 -7
  52. package/dist/lib/node/chunk-F3WGFGEN.cjs.map +0 -7
  53. package/dist/lib/node-esm/chunk-3HDLTAT2.mjs.map +0 -7
@@ -177,6 +177,10 @@ export class Identity {
177
177
  return this._presence;
178
178
  }
179
179
 
180
+ get signer() {
181
+ return this._signer;
182
+ }
183
+
180
184
  /**
181
185
  * Issues credentials as identity.
182
186
  * Requires identity to be ready.
@@ -2,10 +2,10 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Trigger } from '@dxos/async';
5
+ import { Mutex, scheduleMicroTask, Trigger } from '@dxos/async';
6
6
  import { Context, Resource } from '@dxos/context';
7
7
  import { getCredentialAssertion, type CredentialProcessor } from '@dxos/credentials';
8
- import { failUndefined } from '@dxos/debug';
8
+ import { failUndefined, warnAfterTimeout } from '@dxos/debug';
9
9
  import {
10
10
  EchoEdgeReplicator,
11
11
  EchoHost,
@@ -14,7 +14,8 @@ import {
14
14
  SpaceManager,
15
15
  valueEncoding,
16
16
  } from '@dxos/echo-pipeline';
17
- import type { EdgeConnection } from '@dxos/edge-client';
17
+ import { createChainEdgeIdentity, createEphemeralEdgeIdentity } from '@dxos/edge-client';
18
+ import type { EdgeHttpClient, EdgeConnection } from '@dxos/edge-client';
18
19
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
19
20
  import { invariant } from '@dxos/invariant';
20
21
  import { Keyring } from '@dxos/keyring';
@@ -37,8 +38,8 @@ import { safeInstanceof } from '@dxos/util';
37
38
  import {
38
39
  IdentityManager,
39
40
  type CreateIdentityOptions,
40
- type JoinIdentityParams,
41
41
  type IdentityManagerParams,
42
+ type JoinIdentityParams,
42
43
  } from '../identity';
43
44
  import {
44
45
  DeviceInvitationProtocol,
@@ -65,6 +66,8 @@ export type ServiceContextRuntimeParams = Pick<
65
66
  @safeInstanceof('dxos.client-services.ServiceContext')
66
67
  @Trace.resource()
67
68
  export class ServiceContext extends Resource {
69
+ private readonly _edgeIdentityUpdateMutex = new Mutex();
70
+
68
71
  public readonly initialized = new Trigger();
69
72
  public readonly metadataStore: MetadataStore;
70
73
  public readonly blobStore: BlobStore;
@@ -96,6 +99,7 @@ export class ServiceContext extends Resource {
96
99
  public readonly networkManager: SwarmNetworkManager,
97
100
  public readonly signalManager: SignalManager,
98
101
  private readonly _edgeConnection: EdgeConnection | undefined,
102
+ private readonly _edgeHttpClient: EdgeHttpClient | undefined,
99
103
  public readonly _runtimeParams?: ServiceContextRuntimeParams,
100
104
  private readonly _edgeFeatures?: Runtime.Client.EdgeFeatures,
101
105
  ) {
@@ -137,18 +141,33 @@ export class ServiceContext extends Resource {
137
141
  callbacks: {
138
142
  onIdentityConstruction: (identity) => {
139
143
  if (this._edgeConnection) {
140
- log.info('Setting identity on edge connection', {
141
- identity: identity.identityKey.toHex(),
142
- oldIdentity: this._edgeConnection.identityKey,
143
- swarms: this.networkManager.topics,
144
- });
145
- this._edgeConnection.setIdentity({
146
- peerKey: identity.deviceKey.toHex(),
147
- identityKey: identity.identityKey.toHex(),
148
- });
149
- this.networkManager.setPeerInfo({
150
- identityKey: identity.identityKey.toHex(),
151
- peerKey: identity.deviceKey.toHex(),
144
+ scheduleMicroTask(this._ctx, async () => {
145
+ using _ = await this._edgeIdentityUpdateMutex.acquire();
146
+
147
+ log.info('Setting identity on edge connection', {
148
+ identity: identity.identityKey.toHex(),
149
+ oldIdentity: this._edgeConnection!.identityKey,
150
+ swarms: this.networkManager.topics,
151
+ });
152
+
153
+ await warnAfterTimeout(10_000, 'Waiting for identity to be ready for edge connection', async () => {
154
+ await identity.ready();
155
+ });
156
+
157
+ invariant(identity.deviceCredentialChain);
158
+ this._edgeConnection!.setIdentity(
159
+ await createChainEdgeIdentity(
160
+ identity.signer,
161
+ identity.identityKey,
162
+ identity.deviceKey,
163
+ identity.deviceCredentialChain,
164
+ [], // TODO(dmaretskyi): Service access credentials.
165
+ ),
166
+ );
167
+ this.networkManager.setPeerInfo({
168
+ identityKey: identity.identityKey.toHex(),
169
+ peerKey: identity.deviceKey.toHex(),
170
+ });
152
171
  });
153
172
  }
154
173
  },
@@ -197,7 +216,11 @@ export class ServiceContext extends Resource {
197
216
 
198
217
  log('opening...');
199
218
  log.trace('dxos.sdk.service-context.open', trace.begin({ id: this._instanceId }));
200
- await this._edgeConnection?.open();
219
+ if (this._edgeConnection) {
220
+ // TODO(dmaretskyi): Use device key.
221
+ this._edgeConnection.setIdentity(await createEphemeralEdgeIdentity());
222
+ await this._edgeConnection.open();
223
+ }
201
224
  await this.signalManager.open();
202
225
  await this.networkManager.open();
203
226
 
@@ -304,6 +327,7 @@ export class ServiceContext extends Resource {
304
327
  echoHost: this.echoHost,
305
328
  invitationsManager: this.invitationsManager,
306
329
  edgeConnection: this._edgeConnection,
330
+ edgeHttpClient: this._edgeHttpClient,
307
331
  echoEdgeReplicator: this._echoEdgeReplicator,
308
332
  meshReplicator: this._meshReplicator,
309
333
  runtimeParams: this._runtimeParams as DataSpaceManagerRuntimeParams,
@@ -6,7 +6,7 @@ import { Event, synchronized } from '@dxos/async';
6
6
  import { clientServiceBundle, type ClientServices } from '@dxos/client-protocol';
7
7
  import { type Config } from '@dxos/config';
8
8
  import { Context } from '@dxos/context';
9
- import { EdgeClient, type EdgeConnection } from '@dxos/edge-client';
9
+ import { EdgeClient, EdgeHttpClient, createStubEdgeIdentity, type EdgeConnection } from '@dxos/edge-client';
10
10
  import { invariant } from '@dxos/invariant';
11
11
  import { PublicKey } from '@dxos/keys';
12
12
  import { type LevelDB } from '@dxos/kv-store';
@@ -15,8 +15,8 @@ import { EdgeSignalManager, WebsocketSignalManager, type SignalManager } from '@
15
15
  import {
16
16
  SwarmNetworkManager,
17
17
  createIceProvider,
18
- type TransportFactory,
19
18
  createRtcTransportFactory,
19
+ type TransportFactory,
20
20
  } from '@dxos/network-manager';
21
21
  import { trace } from '@dxos/protocols';
22
22
  import { SystemStatus } from '@dxos/protocols/proto/dxos/client/services';
@@ -29,9 +29,9 @@ import { ServiceRegistry } from './service-registry';
29
29
  import { DevicesServiceImpl } from '../devices';
30
30
  import { DevtoolsHostEvents, DevtoolsServiceImpl } from '../devtools';
31
31
  import {
32
- type CollectDiagnosticsBroadcastHandler,
33
32
  createCollectDiagnosticsBroadcastHandler,
34
33
  createDiagnostics,
34
+ type CollectDiagnosticsBroadcastHandler,
35
35
  } from '../diagnostics';
36
36
  import { IdentityServiceImpl, type CreateIdentityOptions } from '../identity';
37
37
  import { ContactsServiceImpl } from '../identity/contacts-service';
@@ -89,6 +89,7 @@ export class ClientServicesHost {
89
89
  private _callbacks?: ClientServicesHostCallbacks;
90
90
  private _devtoolsProxy?: WebsocketRpcClient<{}, ClientServices>;
91
91
  private _edgeConnection?: EdgeConnection = undefined;
92
+ private _edgeHttpClient?: EdgeHttpClient = undefined;
92
93
 
93
94
  private _serviceContext!: ServiceContext;
94
95
  private readonly _runtimeParams: ServiceContextRuntimeParams;
@@ -212,8 +213,8 @@ export class ClientServicesHost {
212
213
 
213
214
  const edgeEndpoint = config?.get('runtime.services.edge.url');
214
215
  if (edgeEndpoint) {
215
- const randomKey = PublicKey.random().toHex();
216
- this._edgeConnection = new EdgeClient(randomKey, randomKey, { socketEndpoint: edgeEndpoint });
216
+ this._edgeConnection = new EdgeClient(createStubEdgeIdentity(), { socketEndpoint: edgeEndpoint });
217
+ this._edgeHttpClient = new EdgeHttpClient(edgeEndpoint);
217
218
  }
218
219
 
219
220
  const {
@@ -278,6 +279,7 @@ export class ClientServicesHost {
278
279
  this._networkManager,
279
280
  this._signalManager,
280
281
  this._edgeConnection,
282
+ this._edgeHttpClient,
281
283
  this._runtimeParams,
282
284
  this._config.get('runtime.client.edgeFeatures'),
283
285
  );
@@ -36,7 +36,7 @@ import {
36
36
  type SpaceDoc,
37
37
  } from '@dxos/echo-protocol';
38
38
  import { TYPE_PROPERTIES, generateEchoId, getTypeReference } from '@dxos/echo-schema';
39
- import type { EdgeConnection } from '@dxos/edge-client';
39
+ import type { EdgeConnection, EdgeHttpClient } from '@dxos/edge-client';
40
40
  import { writeMessages, type FeedStore } from '@dxos/feed-store';
41
41
  import { invariant } from '@dxos/invariant';
42
42
  import { type Keyring } from '@dxos/keyring';
@@ -110,6 +110,7 @@ export type DataSpaceManagerParams = {
110
110
  echoHost: EchoHost;
111
111
  invitationsManager: InvitationsManager;
112
112
  edgeConnection?: EdgeConnection;
113
+ edgeHttpClient?: EdgeHttpClient;
113
114
  meshReplicator?: MeshEchoReplicator;
114
115
  echoEdgeReplicator?: EchoEdgeReplicator;
115
116
  runtimeParams?: DataSpaceManagerRuntimeParams;
@@ -138,6 +139,7 @@ export class DataSpaceManager extends Resource {
138
139
  private readonly _echoHost: EchoHost;
139
140
  private readonly _invitationsManager: InvitationsManager;
140
141
  private readonly _edgeConnection?: EdgeConnection = undefined;
142
+ private readonly _edgeHttpClient?: EdgeHttpClient = undefined;
141
143
  private readonly _edgeFeatures?: Runtime.Client.EdgeFeatures = undefined;
142
144
  private readonly _meshReplicator?: MeshEchoReplicator = undefined;
143
145
  private readonly _echoEdgeReplicator?: EchoEdgeReplicator = undefined;
@@ -157,6 +159,7 @@ export class DataSpaceManager extends Resource {
157
159
  this._edgeConnection = params.edgeConnection;
158
160
  this._edgeFeatures = params.edgeFeatures;
159
161
  this._echoEdgeReplicator = params.echoEdgeReplicator;
162
+ this._edgeHttpClient = params.edgeHttpClient;
160
163
  this._runtimeParams = params.runtimeParams;
161
164
 
162
165
  trace.diagnostic({
@@ -482,6 +485,7 @@ export class DataSpaceManager extends Resource {
482
485
  },
483
486
  cache: metadata.cache,
484
487
  edgeConnection: this._edgeConnection,
488
+ edgeHttpClient: this._edgeHttpClient,
485
489
  edgeFeatures: this._edgeFeatures,
486
490
  });
487
491
  dataSpace.postOpen.append(async () => {
@@ -15,7 +15,7 @@ import {
15
15
  type Space,
16
16
  } from '@dxos/echo-pipeline';
17
17
  import { SpaceDocVersion } from '@dxos/echo-protocol';
18
- import type { EdgeConnection } from '@dxos/edge-client';
18
+ import type { EdgeConnection, EdgeHttpClient } from '@dxos/edge-client';
19
19
  import { type FeedStore, type FeedWrapper } from '@dxos/feed-store';
20
20
  import { failedInvariant } from '@dxos/invariant';
21
21
  import { type Keyring } from '@dxos/keyring';
@@ -80,6 +80,7 @@ export type DataSpaceParams = {
80
80
  callbacks?: DataSpaceCallbacks;
81
81
  cache?: SpaceCache;
82
82
  edgeConnection?: EdgeConnection;
83
+ edgeHttpClient?: EdgeHttpClient;
83
84
  edgeFeatures?: Runtime.Client.EdgeFeatures;
84
85
  };
85
86
 
@@ -101,7 +102,7 @@ export class DataSpace {
101
102
  private readonly _feedStore: FeedStore<FeedMessage>;
102
103
  private readonly _metadataStore: MetadataStore;
103
104
  private readonly _signingContext: SigningContext;
104
- private readonly _notarizationPlugin = new NotarizationPlugin();
105
+ private readonly _notarizationPlugin: NotarizationPlugin;
105
106
  private readonly _callbacks: DataSpaceCallbacks;
106
107
  private readonly _cache?: SpaceCache = undefined;
107
108
  private readonly _echoHost: EchoHost;
@@ -141,6 +142,11 @@ export class DataSpace {
141
142
  this._signingContext = params.signingContext;
142
143
  this._callbacks = params.callbacks ?? {};
143
144
  this._echoHost = params.echoHost;
145
+ this._notarizationPlugin = new NotarizationPlugin({
146
+ spaceId: this._inner.id,
147
+ edgeClient: params.edgeHttpClient,
148
+ edgeFeatures: params.edgeFeatures,
149
+ });
144
150
 
145
151
  this.authVerifier = new TrustedKeySetAuthVerifier({
146
152
  trustedKeysProvider: () =>
@@ -323,6 +329,7 @@ export class DataSpace {
323
329
  this._state = SpaceState.SPACE_INITIALIZING;
324
330
  log('new state', { state: SpaceState[this._state] });
325
331
 
332
+ log('initializing control pipeline');
326
333
  await this._initializeAndReadControlPipeline();
327
334
 
328
335
  // Allow other tasks to run before loading the data pipeline.
@@ -330,10 +337,13 @@ export class DataSpace {
330
337
 
331
338
  const ready = this.stateUpdate.waitForCondition(() => this._state === SpaceState.SPACE_READY);
332
339
 
340
+ log('initializing automerge root');
333
341
  this._automergeSpaceState.startProcessingRootDocs();
334
342
 
335
343
  // TODO(dmaretskyi): Change so `initializeDataPipeline` doesn't wait for the space to be READY, but rather any state with a valid root.
344
+ log('waiting for space to be ready');
336
345
  await ready;
346
+ log('space is ready');
337
347
  }
338
348
 
339
349
  private async _enterReadyState() {
@@ -350,6 +360,7 @@ export class DataSpace {
350
360
  private async _initializeAndReadControlPipeline() {
351
361
  await this._inner.controlPipeline.state.waitUntilReachedTargetTimeframe({
352
362
  ctx: this._ctx,
363
+ timeout: 10_000,
353
364
  breakOnStall: false,
354
365
  });
355
366
 
@@ -413,8 +424,16 @@ export class DataSpace {
413
424
  }
414
425
 
415
426
  if (credentials.length > 0) {
416
- // Never times out
417
- await this.notarizationPlugin.notarize({ ctx: this._ctx, credentials, timeout: 0 });
427
+ try {
428
+ log('will notarize credentials for feed admission', { count: credentials.length });
429
+ // Never times out
430
+ await this.notarizationPlugin.notarize({ ctx: this._ctx, credentials, timeout: 0 });
431
+
432
+ log('credentials notarized');
433
+ } catch (err) {
434
+ log.error('error notarizing credentials for feed admission', err);
435
+ throw err;
436
+ }
418
437
 
419
438
  // Set this after credentials are notarized so that on failure we will retry.
420
439
  await this._metadataStore.setWritableFeedKeys(this.key, this.inner.controlFeedKey!, this.inner.dataFeedKey!);
@@ -9,12 +9,12 @@ import { describe, test, onTestFinished, vi, expect } from 'vitest';
9
9
  import { Trigger, sleep } from '@dxos/async';
10
10
  import { Context } from '@dxos/context';
11
11
  import { valueEncoding } from '@dxos/echo-pipeline';
12
- import { EdgeClient, EdgeIdentityChangedError } from '@dxos/edge-client';
12
+ import { createEphemeralEdgeIdentity, EdgeClient, EdgeIdentityChangedError } from '@dxos/edge-client';
13
13
  import { createTestEdgeWsServer } from '@dxos/edge-client/testing';
14
14
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
15
15
  import { type FeedWrapper } from '@dxos/feed-store';
16
16
  import { Keyring } from '@dxos/keyring';
17
- import { PublicKey, SpaceId } from '@dxos/keys';
17
+ import { SpaceId } from '@dxos/keys';
18
18
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
19
19
  import { createStorage } from '@dxos/random-access-storage';
20
20
  import { openAndClose } from '@dxos/test-utils';
@@ -41,6 +41,16 @@ describe('EdgeFeedReplicator', () => {
41
41
  expect(messageSink[0].type).toEqual('get-metadata');
42
42
  });
43
43
 
44
+ test('replicates if added to a connected client', async () => {
45
+ const { endpoint, admitConnection, messageSink } = await createEdge();
46
+ const { messenger } = await createClient(endpoint);
47
+ admitConnection.wake();
48
+ await expect.poll(() => messenger.isConnected).toBeTruthy();
49
+
50
+ await attachReplicator(messenger);
51
+ await expect.poll(() => messageSink.length).toEqual(1);
52
+ });
53
+
44
54
  test('sends a block', async () => {
45
55
  const { endpoint, admitConnection, messageSink } = await createEdge();
46
56
  const { messenger } = await createClient(endpoint);
@@ -63,7 +73,7 @@ describe('EdgeFeedReplicator', () => {
63
73
  admitConnection.wake();
64
74
  await expect.poll(() => messageSink.length).toEqual(1);
65
75
 
66
- updateIdentity(messenger);
76
+ await updateIdentity(messenger);
67
77
  await messenger.reconnect.waitForCount(1);
68
78
 
69
79
  await expect.poll(() => messageSink.length).toEqual(2);
@@ -83,7 +93,7 @@ describe('EdgeFeedReplicator', () => {
83
93
 
84
94
  await expect.poll(() => sendSpy.mock.calls.length).toEqual(1);
85
95
  expect(messageSink.length).toEqual(0);
86
- updateIdentity(messenger);
96
+ await updateIdentity(messenger);
87
97
 
88
98
  await expect.poll(() => messageSink.length).toEqual(1);
89
99
  expect(messageSink[0].type).toEqual('get-metadata');
@@ -107,10 +117,9 @@ describe('EdgeFeedReplicator', () => {
107
117
 
108
118
  await expect.poll(() => sendSpy.mock.calls.length).toEqual(2);
109
119
  expect(messageSink.length).toEqual(0);
110
- updateIdentity(messenger);
120
+ await updateIdentity(messenger);
111
121
 
112
- await expect.poll(() => messageSink.length).toEqual(2);
113
- expect(messageSink[1].type).toEqual('data');
122
+ await expect.poll(() => messageSink.find((msg) => msg.type === 'data')).toBeDefined();
114
123
  });
115
124
 
116
125
  test('propagates errors unrelated to reconnect', async () => {
@@ -137,7 +146,7 @@ describe('EdgeFeedReplicator', () => {
137
146
  const { messenger } = await createClient(endpoint);
138
147
 
139
148
  await attachReplicator(messenger);
140
- updateIdentity(messenger);
149
+ await updateIdentity(messenger);
141
150
  await sleep(100);
142
151
  admitConnection.wake();
143
152
 
@@ -154,7 +163,7 @@ describe('EdgeFeedReplicator', () => {
154
163
  await sleep(10);
155
164
 
156
165
  admitConnection.reset();
157
- updateIdentity(messenger);
166
+ await updateIdentity(messenger);
158
167
  await appendMessage(feed);
159
168
  await sleep(20);
160
169
  admitConnection.wake();
@@ -171,7 +180,7 @@ describe('EdgeFeedReplicator', () => {
171
180
  await sleep(10);
172
181
 
173
182
  void appendMessage(feed);
174
- updateIdentity(messenger);
183
+ await updateIdentity(messenger);
175
184
 
176
185
  await expect.poll(() => feedLength()).toEqual(1);
177
186
  });
@@ -203,8 +212,7 @@ describe('EdgeFeedReplicator', () => {
203
212
  };
204
213
 
205
214
  const createClient = async (endpoint: string) => {
206
- const peerKey = PublicKey.random().toHex();
207
- const messenger = new EdgeClient(peerKey, peerKey, { socketEndpoint: endpoint });
215
+ const messenger = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: endpoint });
208
216
  const sendSpy = vi.spyOn(messenger, 'send');
209
217
  await openAndClose(messenger);
210
218
  return { messenger, sendSpy };
@@ -235,9 +243,8 @@ describe('EdgeFeedReplicator', () => {
235
243
  return feedStore.openFeed(await keyring.createKey(), { writable: true });
236
244
  };
237
245
 
238
- const updateIdentity = (messenger: EdgeClient) => {
239
- const identityKey = PublicKey.random().toHex();
240
- messenger.setIdentity({ peerKey: messenger.peerKey, identityKey });
246
+ const updateIdentity = async (messenger: EdgeClient) => {
247
+ messenger.setIdentity(await createEphemeralEdgeIdentity());
241
248
  };
242
249
 
243
250
  const appendMessage = (feed: FeedWrapper<FeedMessage>) => feed.append({ timeframe: new Timeframe() });
@@ -11,7 +11,7 @@ import { EdgeConnectionClosedError, EdgeIdentityChangedError } from '@dxos/edge-
11
11
  import { type FeedWrapper } from '@dxos/feed-store';
12
12
  import { invariant } from '@dxos/invariant';
13
13
  import { PublicKey, type SpaceId } from '@dxos/keys';
14
- import { log } from '@dxos/log';
14
+ import { log, logInfo } from '@dxos/log';
15
15
  import { EdgeService } from '@dxos/protocols';
16
16
  import { buf } from '@dxos/protocols/buf';
17
17
  import {
@@ -28,7 +28,10 @@ export type EdgeFeedReplicatorParams = {
28
28
 
29
29
  export class EdgeFeedReplicator extends Resource {
30
30
  private readonly _messenger: EdgeConnection;
31
+
32
+ @logInfo
31
33
  private readonly _spaceId: SpaceId;
34
+
32
35
  private readonly _feeds = new ComplexMap<PublicKey, FeedWrapper<any>>(PublicKey.hash);
33
36
 
34
37
  private _connectionCtx?: Context = undefined;
@@ -50,6 +53,7 @@ export class EdgeFeedReplicator extends Resource {
50
53
  }
51
54
 
52
55
  protected override async _open(): Promise<void> {
56
+ log('open');
53
57
  // TODO: handle reconnects
54
58
  this._ctx.onDispose(
55
59
  this._messenger.addListener((message: RouterMessage) => {
@@ -68,43 +72,40 @@ export class EdgeFeedReplicator extends Resource {
68
72
  }
69
73
 
70
74
  const payload = decodeCbor(message.payload!.value) as ProtocolMessage;
71
- log.info('receive', { from: message.source, feedKey: payload.feedKey, type: payload.type });
75
+ log('receive', { from: message.source, feedKey: payload.feedKey, type: payload.type });
72
76
  this._onMessage(payload);
73
77
  }),
74
78
  );
75
79
 
76
80
  this._messenger.connected.on(this._ctx, async () => {
77
81
  await this._resetConnection();
78
-
79
- this._connected = true;
80
- const connectionCtx = new Context({
81
- onError: async (err: any) => {
82
- if (connectionCtx !== this._connectionCtx) {
83
- return;
84
- }
85
- if (err instanceof EdgeIdentityChangedError || err instanceof EdgeConnectionClosedError) {
86
- log('resetting on reconnect');
87
- await this._resetConnection();
88
- } else {
89
- this._ctx.raise(err);
90
- }
91
- },
92
- });
93
- this._connectionCtx = connectionCtx;
94
- log('connection context created');
95
- scheduleMicroTask(connectionCtx, async () => {
96
- for (const feed of this._feeds.values()) {
97
- await this._replicateFeed(connectionCtx, feed);
98
- }
99
- });
82
+ this._startReplication();
100
83
  });
84
+
85
+ if (this._messenger.isConnected) {
86
+ this._startReplication();
87
+ }
101
88
  }
102
89
 
103
90
  protected override async _close(): Promise<void> {
91
+ log('close');
104
92
  await this._resetConnection();
105
93
  }
106
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
107
  private async _resetConnection() {
108
+ log('resetConnection');
108
109
  this._connected = false;
109
110
  await this._connectionCtx?.dispose();
110
111
  this._connectionCtx = undefined;
@@ -112,7 +113,7 @@ export class EdgeFeedReplicator extends Resource {
112
113
  }
113
114
 
114
115
  async addFeed(feed: FeedWrapper<any>) {
115
- log.info('addFeed', { key: feed.key });
116
+ log.info('addFeed', { key: feed.key, connected: this._connected, hasConnectionCtx: !!this._connectionCtx });
116
117
  this._feeds.set(feed.key, feed);
117
118
 
118
119
  if (this._connected && this._connectionCtx) {
@@ -125,6 +126,7 @@ export class EdgeFeedReplicator extends Resource {
125
126
  }
126
127
 
127
128
  private async _replicateFeed(ctx: Context, feed: FeedWrapper<any>) {
129
+ log('replicateFeed', { key: feed.key });
128
130
  await this._sendMessage({
129
131
  type: 'get-metadata',
130
132
  feedKey: feed.key.toHex(),
@@ -148,6 +150,7 @@ export class EdgeFeedReplicator extends Resource {
148
150
  invariant(message.feedKey);
149
151
  const payloadValue = bufferToArray(encodeCbor(message));
150
152
 
153
+ log('send', { type: message.type });
151
154
  await this._messenger.send(
152
155
  buf.create(RouterMessageSchema, {
153
156
  source: {
@@ -272,6 +275,23 @@ export class EdgeFeedReplicator extends Resource {
272
275
  await this._pushBlocks(feed, remoteLength, feed.length);
273
276
  }
274
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
+ }
275
295
  }
276
296
 
277
297
  // hypercore requires buffers
@@ -8,20 +8,22 @@ 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
15
 
15
- import { NotarizationPlugin } from './notarization-plugin';
16
+ import { NotarizationPlugin, type NotarizationPluginParams } from './notarization-plugin';
16
17
 
17
18
  class TestAgent extends TestPeer {
18
19
  private readonly _ctx = new Context();
19
20
 
20
21
  feed = new MockFeedWriter<Credential>();
21
- notarizationPlugin = new NotarizationPlugin();
22
+ notarizationPlugin: NotarizationPlugin;
22
23
 
23
- constructor() {
24
+ constructor(params: NotarizationPluginParams) {
24
25
  super();
26
+ this.notarizationPlugin = new NotarizationPlugin(params);
25
27
  this.feed.written.on(this._ctx, async ([credential]) => {
26
28
  log('written to feed', { credential });
27
29
  await this.notarizationPlugin.processCredential(credential);
@@ -50,8 +52,10 @@ describe('NotarizationPlugin', () => {
50
52
  const testBuilder = new TestBuilder();
51
53
  onTestFinished(() => testBuilder.destroy());
52
54
 
55
+ const params = { spaceId: SpaceId.random() };
56
+
53
57
  // peer0 is there to test retries.
54
- const [_peer0, peer1, peer2] = await testBuilder.createPeers({ factory: () => new TestAgent() });
58
+ const [_peer0, peer1, peer2] = await testBuilder.createPeers({ factory: () => new TestAgent(params) });
55
59
  peer1.enableWriting();
56
60
 
57
61
  peer1.feed.written.on(async ([credential]) => {