@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.
- package/dist/lib/browser/{chunk-XSFLJVDP.mjs → chunk-XVI3VSJT.mjs} +637 -292
- package/dist/lib/browser/chunk-XVI3VSJT.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +2 -2
- package/dist/lib/browser/testing/index.mjs.map +2 -2
- package/dist/lib/node/{chunk-F3WGFGEN.cjs → chunk-NZL66D6K.cjs} +727 -382
- package/dist/lib/node/chunk-NZL66D6K.cjs.map +7 -0
- package/dist/lib/node/index.cjs +45 -45
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +8 -8
- package/dist/lib/node/testing/index.cjs.map +2 -2
- package/dist/lib/node-esm/{chunk-3HDLTAT2.mjs → chunk-6747X7GN.mjs} +637 -292
- package/dist/lib/node-esm/chunk-6747X7GN.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1 -1
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +2 -2
- package/dist/lib/node-esm/testing/index.mjs.map +2 -2
- package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
- package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +1 -0
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +4 -2
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +1 -0
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +3 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +2 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +2 -0
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +31 -6
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/package.json +38 -38
- package/src/packlets/identity/authenticator.ts +5 -2
- package/src/packlets/identity/contacts-service.ts +1 -1
- package/src/packlets/identity/identity.ts +4 -0
- package/src/packlets/services/service-context.ts +41 -17
- package/src/packlets/services/service-host.ts +7 -5
- package/src/packlets/spaces/data-space-manager.ts +5 -1
- package/src/packlets/spaces/data-space.ts +23 -4
- package/src/packlets/spaces/edge-feed-replicator.test.ts +22 -15
- package/src/packlets/spaces/edge-feed-replicator.ts +45 -25
- package/src/packlets/spaces/notarization-plugin.test.ts +8 -4
- package/src/packlets/spaces/notarization-plugin.ts +169 -29
- package/src/packlets/testing/test-builder.ts +1 -1
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-XSFLJVDP.mjs.map +0 -7
- package/dist/lib/node/chunk-F3WGFGEN.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-3HDLTAT2.mjs.map +0 -7
|
@@ -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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
this.
|
|
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
|
|
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
|
-
|
|
417
|
-
|
|
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 {
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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]) => {
|