@dxos/client-services 0.6.11 → 0.6.12-main.568932b
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-QYVLLBAA.mjs → chunk-AHZ7ASHC.mjs} +5619 -5143
- package/dist/lib/browser/chunk-AHZ7ASHC.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +3 -3
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +5 -6
- package/dist/lib/browser/testing/index.mjs.map +2 -2
- package/dist/lib/node/{chunk-F7G2TXVG.cjs → chunk-YJWFC43Y.cjs} +5391 -4915
- package/dist/lib/node/chunk-YJWFC43Y.cjs.map +7 -0
- package/dist/lib/node/index.cjs +46 -46
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +11 -12
- package/dist/lib/node/testing/index.cjs.map +2 -2
- package/dist/lib/node-esm/chunk-7ADUGZEP.mjs +8192 -0
- package/dist/lib/node-esm/chunk-7ADUGZEP.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +416 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/testing/index.mjs +418 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +2 -0
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +1 -0
- 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-manager.d.ts +19 -7
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +8 -1
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +7 -6
- 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 +6 -3
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +4 -3
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +3 -0
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +2 -0
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
- package/dist/types/src/packlets/spaces/epoch-migrations.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/packlets/spaces/spaces-service.d.ts +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +1 -2
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
- package/dist/types/src/testing/setup.d.ts +3 -0
- package/dist/types/src/testing/setup.d.ts.map +1 -0
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/package.json +43 -39
- package/src/packlets/devices/devices-service.test.ts +4 -5
- package/src/packlets/diagnostics/diagnostics-broadcast.ts +1 -0
- package/src/packlets/identity/{authenticator.test.ts → authenticator.node.test.ts} +2 -3
- package/src/packlets/identity/authenticator.ts +5 -2
- package/src/packlets/identity/contacts-service.ts +1 -1
- package/src/packlets/identity/identity-manager.test.ts +5 -6
- package/src/packlets/identity/identity-manager.ts +35 -19
- package/src/packlets/identity/identity-service.test.ts +4 -8
- package/src/packlets/identity/identity.test.ts +128 -239
- package/src/packlets/identity/identity.ts +42 -8
- package/src/packlets/invitations/device-invitation-protocol.test.ts +7 -4
- package/src/packlets/invitations/invitation-host-extension.ts +0 -3
- package/src/packlets/invitations/invitations-handler.test.ts +14 -7
- package/src/packlets/invitations/invitations-handler.ts +1 -1
- package/src/packlets/invitations/space-invitation-protocol.test.ts +4 -3
- package/src/packlets/logging/logging.test.ts +1 -2
- package/src/packlets/network/network-service.test.ts +2 -3
- package/src/packlets/services/service-context.test.ts +3 -1
- package/src/packlets/services/service-context.ts +64 -28
- package/src/packlets/services/service-host.test.ts +8 -12
- package/src/packlets/services/service-host.ts +8 -6
- package/src/packlets/services/service-registry.test.ts +1 -2
- package/src/packlets/spaces/data-space-manager.test.ts +2 -2
- package/src/packlets/spaces/data-space-manager.ts +38 -5
- package/src/packlets/spaces/data-space.ts +34 -6
- package/src/packlets/spaces/edge-feed-replicator.test.ts +251 -0
- package/src/packlets/spaces/edge-feed-replicator.ts +80 -22
- package/src/packlets/spaces/epoch-migrations.ts +2 -2
- package/src/packlets/spaces/notarization-plugin.test.ts +10 -7
- package/src/packlets/spaces/notarization-plugin.ts +169 -29
- package/src/packlets/spaces/spaces-service.test.ts +5 -9
- package/src/packlets/spaces/spaces-service.ts +6 -1
- package/src/packlets/storage/storage.ts +0 -1
- package/src/packlets/system/system-service.test.ts +1 -2
- package/src/packlets/testing/test-builder.ts +2 -3
- package/src/packlets/worker/worker-runtime.ts +2 -2
- package/src/testing/setup.ts +11 -0
- package/src/version.ts +1 -5
- package/dist/lib/browser/chunk-QYVLLBAA.mjs.map +0 -7
- package/dist/lib/node/chunk-F7G2TXVG.cjs.map +0 -7
- package/dist/types/src/packlets/identity/authenticator.test.d.ts +0 -2
- package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +0 -1
- package/dist/types/src/packlets/services/automerge-host.test.d.ts +0 -2
- package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +0 -1
- package/src/packlets/services/automerge-host.test.ts +0 -60
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { asyncTimeout, latch } from '@dxos/async';
|
|
8
8
|
import { createAdmissionCredentials } from '@dxos/credentials';
|
|
@@ -10,7 +10,7 @@ import { AuthStatus } from '@dxos/echo-pipeline';
|
|
|
10
10
|
import { writeMessages } from '@dxos/feed-store';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
12
|
import { SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
13
|
-
import {
|
|
13
|
+
import { openAndClose } from '@dxos/test-utils';
|
|
14
14
|
|
|
15
15
|
import { TestBuilder, type TestPeer } from '../testing';
|
|
16
16
|
|
|
@@ -14,8 +14,11 @@ import {
|
|
|
14
14
|
type DelegateInvitationCredential,
|
|
15
15
|
type MemberInfo,
|
|
16
16
|
} from '@dxos/credentials';
|
|
17
|
-
import { convertLegacyReferences, findInlineObjectOfType, type EchoEdgeReplicator, type EchoHost } from '@dxos/echo-db';
|
|
18
17
|
import {
|
|
18
|
+
convertLegacyReferences,
|
|
19
|
+
findInlineObjectOfType,
|
|
20
|
+
type EchoEdgeReplicator,
|
|
21
|
+
type EchoHost,
|
|
19
22
|
AuthStatus,
|
|
20
23
|
CredentialServerExtension,
|
|
21
24
|
type MeshEchoReplicator,
|
|
@@ -33,7 +36,7 @@ import {
|
|
|
33
36
|
type SpaceDoc,
|
|
34
37
|
} from '@dxos/echo-protocol';
|
|
35
38
|
import { TYPE_PROPERTIES, generateEchoId, getTypeReference } from '@dxos/echo-schema';
|
|
36
|
-
import type { EdgeConnection } from '@dxos/edge-client';
|
|
39
|
+
import type { EdgeConnection, EdgeHttpClient } from '@dxos/edge-client';
|
|
37
40
|
import { writeMessages, type FeedStore } from '@dxos/feed-store';
|
|
38
41
|
import { invariant } from '@dxos/invariant';
|
|
39
42
|
import { type Keyring } from '@dxos/keyring';
|
|
@@ -43,7 +46,7 @@ import { AlreadyJoinedError, trace as Trace } from '@dxos/protocols';
|
|
|
43
46
|
import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
44
47
|
import { type Runtime } from '@dxos/protocols/proto/dxos/config';
|
|
45
48
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
46
|
-
import { type SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
49
|
+
import { type SpaceMetadata, EdgeReplicationSetting } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
47
50
|
import { SpaceMember, type Credential, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
48
51
|
import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
49
52
|
import { type PeerState } from '@dxos/protocols/proto/dxos/mesh/presence';
|
|
@@ -107,6 +110,7 @@ export type DataSpaceManagerParams = {
|
|
|
107
110
|
echoHost: EchoHost;
|
|
108
111
|
invitationsManager: InvitationsManager;
|
|
109
112
|
edgeConnection?: EdgeConnection;
|
|
113
|
+
edgeHttpClient?: EdgeHttpClient;
|
|
110
114
|
meshReplicator?: MeshEchoReplicator;
|
|
111
115
|
echoEdgeReplicator?: EchoEdgeReplicator;
|
|
112
116
|
runtimeParams?: DataSpaceManagerRuntimeParams;
|
|
@@ -135,6 +139,7 @@ export class DataSpaceManager extends Resource {
|
|
|
135
139
|
private readonly _echoHost: EchoHost;
|
|
136
140
|
private readonly _invitationsManager: InvitationsManager;
|
|
137
141
|
private readonly _edgeConnection?: EdgeConnection = undefined;
|
|
142
|
+
private readonly _edgeHttpClient?: EdgeHttpClient = undefined;
|
|
138
143
|
private readonly _edgeFeatures?: Runtime.Client.EdgeFeatures = undefined;
|
|
139
144
|
private readonly _meshReplicator?: MeshEchoReplicator = undefined;
|
|
140
145
|
private readonly _echoEdgeReplicator?: EchoEdgeReplicator = undefined;
|
|
@@ -154,6 +159,7 @@ export class DataSpaceManager extends Resource {
|
|
|
154
159
|
this._edgeConnection = params.edgeConnection;
|
|
155
160
|
this._edgeFeatures = params.edgeFeatures;
|
|
156
161
|
this._echoEdgeReplicator = params.echoEdgeReplicator;
|
|
162
|
+
this._edgeHttpClient = params.edgeHttpClient;
|
|
157
163
|
this._runtimeParams = params.runtimeParams;
|
|
158
164
|
|
|
159
165
|
trace.diagnostic({
|
|
@@ -388,6 +394,26 @@ export class DataSpaceManager extends Resource {
|
|
|
388
394
|
});
|
|
389
395
|
}
|
|
390
396
|
|
|
397
|
+
async setSpaceEdgeReplicationSetting(spaceKey: PublicKey, setting: EdgeReplicationSetting) {
|
|
398
|
+
const space = this._spaces.get(spaceKey);
|
|
399
|
+
invariant(space, 'Space not found.');
|
|
400
|
+
|
|
401
|
+
await this._metadataStore.setSpaceEdgeReplicationSetting(spaceKey, setting);
|
|
402
|
+
|
|
403
|
+
if (space.isOpen) {
|
|
404
|
+
switch (setting) {
|
|
405
|
+
case EdgeReplicationSetting.DISABLED:
|
|
406
|
+
await this._echoEdgeReplicator?.disconnectFromSpace(space.id);
|
|
407
|
+
break;
|
|
408
|
+
case EdgeReplicationSetting.ENABLED:
|
|
409
|
+
await this._echoEdgeReplicator?.connectToSpace(space.id);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
space.stateUpdate.emit();
|
|
415
|
+
}
|
|
416
|
+
|
|
391
417
|
private async _constructSpace(metadata: SpaceMetadata) {
|
|
392
418
|
log('construct space', { metadata });
|
|
393
419
|
const gossip = new Gossip({
|
|
@@ -479,13 +505,20 @@ export class DataSpaceManager extends Resource {
|
|
|
479
505
|
},
|
|
480
506
|
cache: metadata.cache,
|
|
481
507
|
edgeConnection: this._edgeConnection,
|
|
508
|
+
edgeHttpClient: this._edgeHttpClient,
|
|
482
509
|
edgeFeatures: this._edgeFeatures,
|
|
483
510
|
});
|
|
484
511
|
dataSpace.postOpen.append(async () => {
|
|
485
|
-
|
|
512
|
+
const setting = dataSpace.getEdgeReplicationSetting();
|
|
513
|
+
if (setting === EdgeReplicationSetting.ENABLED) {
|
|
514
|
+
await this._echoEdgeReplicator?.connectToSpace(dataSpace.id);
|
|
515
|
+
}
|
|
486
516
|
});
|
|
487
517
|
dataSpace.preClose.append(async () => {
|
|
488
|
-
|
|
518
|
+
const setting = dataSpace.getEdgeReplicationSetting();
|
|
519
|
+
if (setting === EdgeReplicationSetting.ENABLED) {
|
|
520
|
+
await this._echoEdgeReplicator?.disconnectFromSpace(dataSpace.id);
|
|
521
|
+
}
|
|
489
522
|
});
|
|
490
523
|
|
|
491
524
|
presence.newPeer.on((peerState) => {
|
|
@@ -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 {
|
|
11
|
-
|
|
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
|
|
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: () =>
|
|
@@ -318,6 +329,7 @@ export class DataSpace {
|
|
|
318
329
|
this._state = SpaceState.SPACE_INITIALIZING;
|
|
319
330
|
log('new state', { state: SpaceState[this._state] });
|
|
320
331
|
|
|
332
|
+
log('initializing control pipeline');
|
|
321
333
|
await this._initializeAndReadControlPipeline();
|
|
322
334
|
|
|
323
335
|
// Allow other tasks to run before loading the data pipeline.
|
|
@@ -325,10 +337,13 @@ export class DataSpace {
|
|
|
325
337
|
|
|
326
338
|
const ready = this.stateUpdate.waitForCondition(() => this._state === SpaceState.SPACE_READY);
|
|
327
339
|
|
|
340
|
+
log('initializing automerge root');
|
|
328
341
|
this._automergeSpaceState.startProcessingRootDocs();
|
|
329
342
|
|
|
330
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');
|
|
331
345
|
await ready;
|
|
346
|
+
log('space is ready');
|
|
332
347
|
}
|
|
333
348
|
|
|
334
349
|
private async _enterReadyState() {
|
|
@@ -345,6 +360,7 @@ export class DataSpace {
|
|
|
345
360
|
private async _initializeAndReadControlPipeline() {
|
|
346
361
|
await this._inner.controlPipeline.state.waitUntilReachedTargetTimeframe({
|
|
347
362
|
ctx: this._ctx,
|
|
363
|
+
timeout: 10_000,
|
|
348
364
|
breakOnStall: false,
|
|
349
365
|
});
|
|
350
366
|
|
|
@@ -408,8 +424,16 @@ export class DataSpace {
|
|
|
408
424
|
}
|
|
409
425
|
|
|
410
426
|
if (credentials.length > 0) {
|
|
411
|
-
|
|
412
|
-
|
|
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
|
+
}
|
|
413
437
|
|
|
414
438
|
// Set this after credentials are notarized so that on failure we will retry.
|
|
415
439
|
await this._metadataStore.setWritableFeedKeys(this.key, this.inner.controlFeedKey!, this.inner.dataFeedKey!);
|
|
@@ -546,6 +570,10 @@ export class DataSpace {
|
|
|
546
570
|
this.stateUpdate.emit();
|
|
547
571
|
}
|
|
548
572
|
|
|
573
|
+
getEdgeReplicationSetting() {
|
|
574
|
+
return this._metadataStore.getSpaceEdgeReplicationSetting(this.key);
|
|
575
|
+
}
|
|
576
|
+
|
|
549
577
|
private _onFeedAdded = async (feed: FeedWrapper<any>) => {
|
|
550
578
|
await this._edgeFeedReplicator!.addFeed(feed);
|
|
551
579
|
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { decode as decodeCbor, encode as encodeCbor } from 'cbor-x';
|
|
6
|
+
import { getRandomPort } from 'get-port-please';
|
|
7
|
+
import { describe, test, onTestFinished, vi, expect } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { Trigger, sleep } 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 } from '@dxos/feed-store';
|
|
15
|
+
import { type FeedWrapper } from '@dxos/feed-store';
|
|
16
|
+
import { Keyring } from '@dxos/keyring';
|
|
17
|
+
import { SpaceId } from '@dxos/keys';
|
|
18
|
+
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
19
|
+
import { createStorage } from '@dxos/random-access-storage';
|
|
20
|
+
import { openAndClose } from '@dxos/test-utils';
|
|
21
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
22
|
+
import { range } from '@dxos/util';
|
|
23
|
+
|
|
24
|
+
import { EdgeFeedReplicator } from './edge-feed-replicator';
|
|
25
|
+
|
|
26
|
+
describe('EdgeFeedReplicator', () => {
|
|
27
|
+
test('requests metadata after connection is open', async () => {
|
|
28
|
+
const { endpoint, admitConnection, messageSink } = await createEdge();
|
|
29
|
+
const { messenger, sendSpy } = await createClient(endpoint);
|
|
30
|
+
|
|
31
|
+
await attachReplicator(messenger);
|
|
32
|
+
|
|
33
|
+
await sleep(50);
|
|
34
|
+
|
|
35
|
+
expect(sendSpy).not.toHaveBeenCalled();
|
|
36
|
+
expect(messageSink.length).toEqual(0);
|
|
37
|
+
|
|
38
|
+
admitConnection.wake();
|
|
39
|
+
await expect.poll(() => sendSpy.mock.calls.length).toEqual(1);
|
|
40
|
+
expect(messageSink.length).toEqual(1);
|
|
41
|
+
expect(messageSink[0].type).toEqual('get-metadata');
|
|
42
|
+
});
|
|
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
|
+
|
|
54
|
+
test('sends a block', async () => {
|
|
55
|
+
const { endpoint, admitConnection, messageSink } = await createEdge();
|
|
56
|
+
const { messenger } = await createClient(endpoint);
|
|
57
|
+
|
|
58
|
+
const { feed } = await attachReplicator(messenger);
|
|
59
|
+
|
|
60
|
+
admitConnection.wake();
|
|
61
|
+
await appendMessage(feed);
|
|
62
|
+
|
|
63
|
+
await expect.poll(() => messageSink.length).toEqual(2);
|
|
64
|
+
expect(messageSink[1].type).toEqual('data');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('re-requests metadata on reconnect', async () => {
|
|
68
|
+
const { endpoint, admitConnection, messageSink } = await createEdge();
|
|
69
|
+
const { messenger } = await createClient(endpoint);
|
|
70
|
+
|
|
71
|
+
await attachReplicator(messenger);
|
|
72
|
+
|
|
73
|
+
admitConnection.wake();
|
|
74
|
+
await expect.poll(() => messageSink.length).toEqual(1);
|
|
75
|
+
|
|
76
|
+
await updateIdentity(messenger);
|
|
77
|
+
await messenger.reconnect.waitForCount(1);
|
|
78
|
+
|
|
79
|
+
await expect.poll(() => messageSink.length).toEqual(2);
|
|
80
|
+
expect(messageSink[1].type).toEqual('get-metadata');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('recovers after query sending failure during identity change', async () => {
|
|
84
|
+
const { endpoint, admitConnection, messageSink } = await createEdge();
|
|
85
|
+
const { messenger, sendSpy } = await createClient(endpoint);
|
|
86
|
+
|
|
87
|
+
await attachReplicator(messenger);
|
|
88
|
+
|
|
89
|
+
sendSpy.mockImplementationOnce(() => {
|
|
90
|
+
throw new EdgeIdentityChangedError(); // Hard to mock the exact race condition for when this error is thrown
|
|
91
|
+
});
|
|
92
|
+
admitConnection.wake();
|
|
93
|
+
|
|
94
|
+
await expect.poll(() => sendSpy.mock.calls.length).toEqual(1);
|
|
95
|
+
expect(messageSink.length).toEqual(0);
|
|
96
|
+
await updateIdentity(messenger);
|
|
97
|
+
|
|
98
|
+
await expect.poll(() => messageSink.length).toEqual(1);
|
|
99
|
+
expect(messageSink[0].type).toEqual('get-metadata');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('recovers after response sending failure during identity change', async () => {
|
|
103
|
+
const { endpoint, admitConnection, messageSink, sendResponseMessage } = await createEdge();
|
|
104
|
+
const { messenger, sendSpy } = await createClient(endpoint);
|
|
105
|
+
|
|
106
|
+
const { feed } = await attachReplicator(messenger);
|
|
107
|
+
await appendMessage(feed);
|
|
108
|
+
|
|
109
|
+
sendSpy.mockImplementationOnce(async (request: any) => {
|
|
110
|
+
sendResponseMessage(request, encodeCbor({ type: 'metadata', feedKey: feed.key.toHex(), length: 0 }));
|
|
111
|
+
return Promise.resolve();
|
|
112
|
+
});
|
|
113
|
+
sendSpy.mockImplementationOnce(async () => {
|
|
114
|
+
throw new EdgeIdentityChangedError();
|
|
115
|
+
});
|
|
116
|
+
admitConnection.wake();
|
|
117
|
+
|
|
118
|
+
await expect.poll(() => sendSpy.mock.calls.length).toEqual(2);
|
|
119
|
+
expect(messageSink.length).toEqual(0);
|
|
120
|
+
await updateIdentity(messenger);
|
|
121
|
+
|
|
122
|
+
await expect.poll(() => messageSink.find((msg) => msg.type === 'data')).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('propagates errors unrelated to reconnect', async () => {
|
|
126
|
+
const { endpoint, admitConnection } = await createEdge();
|
|
127
|
+
const { messenger, sendSpy } = await createClient(endpoint);
|
|
128
|
+
|
|
129
|
+
const { replicator } = await attachReplicator(messenger, { skipOpen: true });
|
|
130
|
+
const raised = new Trigger();
|
|
131
|
+
await replicator.open(new Context({ onError: () => raised.wake() }));
|
|
132
|
+
onTestFinished(async () => {
|
|
133
|
+
await replicator.close();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
sendSpy.mockImplementationOnce(() => {
|
|
137
|
+
throw new Error();
|
|
138
|
+
});
|
|
139
|
+
admitConnection.wake();
|
|
140
|
+
|
|
141
|
+
await raised.wait();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('identity update before connected', async () => {
|
|
145
|
+
const { endpoint, admitConnection, messageSink } = await createEdge();
|
|
146
|
+
const { messenger } = await createClient(endpoint);
|
|
147
|
+
|
|
148
|
+
await attachReplicator(messenger);
|
|
149
|
+
await updateIdentity(messenger);
|
|
150
|
+
await sleep(100);
|
|
151
|
+
admitConnection.wake();
|
|
152
|
+
|
|
153
|
+
await expect.poll(() => messageSink.length).toEqual(2);
|
|
154
|
+
expect(messageSink.map((m) => m.type)).toStrictEqual(range(2, () => 'get-metadata'));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('block appended during reconnect', async () => {
|
|
158
|
+
const { endpoint, admitConnection, feedLength } = await createEdge();
|
|
159
|
+
const { messenger } = await createClient(endpoint);
|
|
160
|
+
|
|
161
|
+
const { feed } = await attachReplicator(messenger);
|
|
162
|
+
admitConnection.wake();
|
|
163
|
+
await sleep(10);
|
|
164
|
+
|
|
165
|
+
admitConnection.reset();
|
|
166
|
+
await updateIdentity(messenger);
|
|
167
|
+
await appendMessage(feed);
|
|
168
|
+
await sleep(20);
|
|
169
|
+
admitConnection.wake();
|
|
170
|
+
|
|
171
|
+
await expect.poll(() => feedLength()).toEqual(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('reconnect during block append', async () => {
|
|
175
|
+
const { endpoint, admitConnection, feedLength } = await createEdge();
|
|
176
|
+
const { messenger } = await createClient(endpoint);
|
|
177
|
+
|
|
178
|
+
const { feed } = await attachReplicator(messenger);
|
|
179
|
+
admitConnection.wake();
|
|
180
|
+
await sleep(10);
|
|
181
|
+
|
|
182
|
+
void appendMessage(feed);
|
|
183
|
+
await updateIdentity(messenger);
|
|
184
|
+
|
|
185
|
+
await expect.poll(() => feedLength()).toEqual(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const createEdge = async () => {
|
|
189
|
+
const port = await getRandomPort('127.0.0.1');
|
|
190
|
+
let lastBlockIndex = -1;
|
|
191
|
+
const admitConnection = new Trigger();
|
|
192
|
+
const { cleanup, endpoint, messageSink, sendResponseMessage } = await createTestEdgeWsServer(port, {
|
|
193
|
+
admitConnection,
|
|
194
|
+
payloadDecoder: decodeCbor,
|
|
195
|
+
messageHandler: async (message: any) => {
|
|
196
|
+
if (message.type === 'get-metadata') {
|
|
197
|
+
return encodeCbor({ type: 'metadata', feedKey: message.feedKey, length: lastBlockIndex + 1 });
|
|
198
|
+
} else {
|
|
199
|
+
lastBlockIndex = Math.max(lastBlockIndex, message.blocks[message.blocks.length - 1].index);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
onTestFinished(cleanup);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
endpoint,
|
|
207
|
+
messageSink,
|
|
208
|
+
admitConnection,
|
|
209
|
+
sendResponseMessage,
|
|
210
|
+
feedLength: () => lastBlockIndex + 1,
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const createClient = async (endpoint: string) => {
|
|
215
|
+
const messenger = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: endpoint });
|
|
216
|
+
const sendSpy = vi.spyOn(messenger, 'send');
|
|
217
|
+
await openAndClose(messenger);
|
|
218
|
+
return { messenger, sendSpy };
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const attachReplicator = async (messenger: EdgeClient, options?: { skipOpen?: boolean }) => {
|
|
222
|
+
const spaceId = SpaceId.random();
|
|
223
|
+
const feed = await createNewFeed();
|
|
224
|
+
const replicator = new EdgeFeedReplicator({ messenger, spaceId });
|
|
225
|
+
await replicator.addFeed(feed);
|
|
226
|
+
if (!options?.skipOpen) {
|
|
227
|
+
await openAndClose(replicator);
|
|
228
|
+
}
|
|
229
|
+
return { feed, replicator };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const createNewFeed = async () => {
|
|
233
|
+
const storage = createStorage();
|
|
234
|
+
const keyring = new Keyring();
|
|
235
|
+
const feedStore = new FeedStore<FeedMessage>({
|
|
236
|
+
factory: new FeedFactory<FeedMessage>({
|
|
237
|
+
root: storage.createDirectory(),
|
|
238
|
+
signer: keyring,
|
|
239
|
+
hypercore: { valueEncoding },
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
onTestFinished(() => feedStore.close());
|
|
243
|
+
return feedStore.openFeed(await keyring.createKey(), { writable: true });
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const updateIdentity = async (messenger: EdgeClient) => {
|
|
247
|
+
messenger.setIdentity(await createEphemeralEdgeIdentity());
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const appendMessage = (feed: FeedWrapper<FeedMessage>) => feed.append({ timeframe: new Timeframe() });
|
|
251
|
+
});
|
|
@@ -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 {
|
|
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 {
|
|
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(
|
|
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
|
|
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.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|