@dxos/client-services 0.6.12-main.5cc132e → 0.6.12-main.78ddbdf
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-7FTR2DGP.mjs → chunk-XSFLJVDP.mjs} +5154 -5061
- package/dist/lib/browser/chunk-XSFLJVDP.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 +4 -5
- package/dist/lib/browser/testing/index.mjs.map +2 -2
- package/dist/lib/node/{chunk-KUHUPLSZ.cjs → chunk-F3WGFGEN.cjs} +4980 -4887
- package/dist/lib/node/chunk-F3WGFGEN.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-QGQGMTAW.mjs → chunk-3HDLTAT2.mjs} +5142 -5047
- package/dist/lib/node-esm/chunk-3HDLTAT2.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +4 -3
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +5 -5
- package/dist/lib/node-esm/testing/index.mjs.map +2 -2
- 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 +7 -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 +3 -4
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +1 -2
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +1 -2
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +1 -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/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/version.d.ts +1 -1
- package/package.json +39 -38
- package/src/packlets/identity/identity-manager.test.ts +1 -1
- package/src/packlets/identity/identity-manager.ts +35 -19
- package/src/packlets/identity/identity.test.ts +126 -236
- package/src/packlets/identity/identity.ts +38 -8
- package/src/packlets/invitations/invitation-host-extension.ts +0 -3
- package/src/packlets/invitations/invitations-handler.ts +1 -1
- package/src/packlets/services/service-context.ts +24 -12
- package/src/packlets/services/service-host.ts +2 -2
- package/src/packlets/spaces/data-space-manager.ts +4 -1
- package/src/packlets/spaces/data-space.ts +7 -2
- package/src/packlets/spaces/edge-feed-replicator.test.ts +244 -0
- package/src/packlets/spaces/edge-feed-replicator.ts +59 -21
- package/src/packlets/spaces/epoch-migrations.ts +2 -2
- package/src/packlets/testing/test-builder.ts +1 -2
- package/src/packlets/worker/worker-runtime.ts +2 -2
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-7FTR2DGP.mjs.map +0 -7
- package/dist/lib/node/chunk-KUHUPLSZ.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-QGQGMTAW.mjs.map +0 -7
- 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 -62
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
} from '@dxos/credentials';
|
|
15
15
|
import { type Signer } from '@dxos/crypto';
|
|
16
16
|
import { type Space } from '@dxos/echo-pipeline';
|
|
17
|
-
import {
|
|
17
|
+
import { type EdgeConnection } from '@dxos/edge-client';
|
|
18
|
+
import { writeMessages, type FeedWrapper } from '@dxos/feed-store';
|
|
18
19
|
import { invariant } from '@dxos/invariant';
|
|
19
20
|
import { PublicKey, type SpaceId } from '@dxos/keys';
|
|
20
21
|
import { log } from '@dxos/log';
|
|
22
|
+
import { type Runtime } from '@dxos/protocols/proto/dxos/config';
|
|
21
23
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
22
24
|
import {
|
|
23
25
|
AdmittedFeed,
|
|
@@ -32,6 +34,7 @@ import { type ComplexMap, ComplexSet } from '@dxos/util';
|
|
|
32
34
|
|
|
33
35
|
import { TrustedKeySetAuthVerifier } from './authenticator';
|
|
34
36
|
import { DefaultSpaceStateMachine } from './default-space-state-machine';
|
|
37
|
+
import { EdgeFeedReplicator } from '../spaces';
|
|
35
38
|
|
|
36
39
|
export type IdentityParams = {
|
|
37
40
|
identityKey: PublicKey;
|
|
@@ -39,6 +42,9 @@ export type IdentityParams = {
|
|
|
39
42
|
signer: Signer;
|
|
40
43
|
space: Space;
|
|
41
44
|
presence?: Presence;
|
|
45
|
+
|
|
46
|
+
edgeConnection?: EdgeConnection;
|
|
47
|
+
edgeFeatures?: Runtime.Client.EdgeFeatures;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
/**
|
|
@@ -52,6 +58,8 @@ export class Identity {
|
|
|
52
58
|
private readonly _deviceStateMachine: DeviceStateMachine;
|
|
53
59
|
private readonly _profileStateMachine: ProfileStateMachine;
|
|
54
60
|
private readonly _defaultSpaceStateMachine: DefaultSpaceStateMachine;
|
|
61
|
+
private readonly _edgeFeedReplicator?: EdgeFeedReplicator = undefined;
|
|
62
|
+
|
|
55
63
|
public readonly authVerifier: TrustedKeySetAuthVerifier;
|
|
56
64
|
|
|
57
65
|
public readonly identityKey: PublicKey;
|
|
@@ -59,15 +67,15 @@ export class Identity {
|
|
|
59
67
|
|
|
60
68
|
public readonly stateUpdate = new Event();
|
|
61
69
|
|
|
62
|
-
constructor(
|
|
63
|
-
this.space = space;
|
|
64
|
-
this._signer = signer;
|
|
65
|
-
this._presence = presence;
|
|
70
|
+
constructor(params: IdentityParams) {
|
|
71
|
+
this.space = params.space;
|
|
72
|
+
this._signer = params.signer;
|
|
73
|
+
this._presence = params.presence;
|
|
66
74
|
|
|
67
|
-
this.identityKey = identityKey;
|
|
68
|
-
this.deviceKey = deviceKey;
|
|
75
|
+
this.identityKey = params.identityKey;
|
|
76
|
+
this.deviceKey = params.deviceKey;
|
|
69
77
|
|
|
70
|
-
log.trace('dxos.halo.device', { deviceKey });
|
|
78
|
+
log.trace('dxos.halo.device', { deviceKey: params.deviceKey });
|
|
71
79
|
|
|
72
80
|
this._deviceStateMachine = new DeviceStateMachine({
|
|
73
81
|
identityKey: this.identityKey,
|
|
@@ -88,6 +96,10 @@ export class Identity {
|
|
|
88
96
|
update: this.stateUpdate,
|
|
89
97
|
authTimeout: AUTH_TIMEOUT,
|
|
90
98
|
});
|
|
99
|
+
|
|
100
|
+
if (params.edgeConnection && params.edgeFeatures?.feedReplicator) {
|
|
101
|
+
this._edgeFeedReplicator = new EdgeFeedReplicator({ messenger: params.edgeConnection, spaceId: this.space.id });
|
|
102
|
+
}
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
// TODO(burdon): Expose state object?
|
|
@@ -105,7 +117,14 @@ export class Identity {
|
|
|
105
117
|
await this.space.spaceState.addCredentialProcessor(this._deviceStateMachine);
|
|
106
118
|
await this.space.spaceState.addCredentialProcessor(this._profileStateMachine);
|
|
107
119
|
await this.space.spaceState.addCredentialProcessor(this._defaultSpaceStateMachine);
|
|
120
|
+
|
|
121
|
+
if (this._edgeFeedReplicator) {
|
|
122
|
+
this.space.protocol.feedAdded.append(this._onFeedAdded);
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
await this.space.open(ctx);
|
|
126
|
+
|
|
127
|
+
await this._edgeFeedReplicator?.open();
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
@trace.span()
|
|
@@ -115,6 +134,13 @@ export class Identity {
|
|
|
115
134
|
await this.space.spaceState.removeCredentialProcessor(this._defaultSpaceStateMachine);
|
|
116
135
|
await this.space.spaceState.removeCredentialProcessor(this._profileStateMachine);
|
|
117
136
|
await this.space.spaceState.removeCredentialProcessor(this._deviceStateMachine);
|
|
137
|
+
|
|
138
|
+
if (this._edgeFeedReplicator) {
|
|
139
|
+
this.space.protocol.feedAdded.remove(this._onFeedAdded);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await this._edgeFeedReplicator?.close();
|
|
143
|
+
|
|
118
144
|
await this.space.close();
|
|
119
145
|
}
|
|
120
146
|
|
|
@@ -223,4 +249,8 @@ export class Identity {
|
|
|
223
249
|
].map((credential): FeedMessage.Payload => ({ credential: { credential } })),
|
|
224
250
|
);
|
|
225
251
|
}
|
|
252
|
+
|
|
253
|
+
private _onFeedAdded = async (feed: FeedWrapper<any>) => {
|
|
254
|
+
await this._edgeFeedReplicator!.addFeed(feed);
|
|
255
|
+
};
|
|
226
256
|
}
|
|
@@ -106,13 +106,11 @@ export class InvitationHostExtension extends RpcExtension<
|
|
|
106
106
|
|
|
107
107
|
introduce: async (request) => {
|
|
108
108
|
const { profile, invitationId } = request;
|
|
109
|
-
|
|
110
109
|
const traceId = PublicKey.random().toHex();
|
|
111
110
|
log.trace('dxos.sdk.invitation-handler.host.introduce', trace.begin({ id: traceId }));
|
|
112
111
|
|
|
113
112
|
const invitation = this._requireActiveInvitation();
|
|
114
113
|
this._assertInvitationState(Invitation.State.CONNECTED);
|
|
115
|
-
|
|
116
114
|
if (invitationId !== invitation?.invitationId) {
|
|
117
115
|
log.warn('incorrect invitationId', { expected: invitation.invitationId, actual: invitationId });
|
|
118
116
|
this._callbacks.onError(new Error('Incorrect invitationId.'));
|
|
@@ -126,7 +124,6 @@ export class InvitationHostExtension extends RpcExtension<
|
|
|
126
124
|
log('guest introduced themselves', { guestProfile: profile });
|
|
127
125
|
this.guestProfile = profile;
|
|
128
126
|
this._callbacks.onStateUpdate(Invitation.State.READY_FOR_AUTHENTICATION);
|
|
129
|
-
|
|
130
127
|
this._challenge =
|
|
131
128
|
invitation.authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? randomBytes(32) : undefined;
|
|
132
129
|
|
|
@@ -463,7 +463,7 @@ export class InvitationsHandler {
|
|
|
463
463
|
oldState: stateToString(invitation.state),
|
|
464
464
|
});
|
|
465
465
|
} else {
|
|
466
|
-
log
|
|
466
|
+
log('invitation state update', {
|
|
467
467
|
actor: actor?.constructor.name,
|
|
468
468
|
newState: stateToString(newState),
|
|
469
469
|
oldState: stateToString(invitation.state),
|
|
@@ -6,8 +6,14 @@ import { Trigger } from '@dxos/async';
|
|
|
6
6
|
import { Context, Resource } from '@dxos/context';
|
|
7
7
|
import { getCredentialAssertion, type CredentialProcessor } from '@dxos/credentials';
|
|
8
8
|
import { failUndefined } from '@dxos/debug';
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
EchoEdgeReplicator,
|
|
11
|
+
EchoHost,
|
|
12
|
+
MeshEchoReplicator,
|
|
13
|
+
MetadataStore,
|
|
14
|
+
SpaceManager,
|
|
15
|
+
valueEncoding,
|
|
16
|
+
} from '@dxos/echo-pipeline';
|
|
11
17
|
import type { EdgeConnection } from '@dxos/edge-client';
|
|
12
18
|
import { FeedFactory, FeedStore } from '@dxos/feed-store';
|
|
13
19
|
import { invariant } from '@dxos/invariant';
|
|
@@ -31,8 +37,8 @@ import { safeInstanceof } from '@dxos/util';
|
|
|
31
37
|
import {
|
|
32
38
|
IdentityManager,
|
|
33
39
|
type CreateIdentityOptions,
|
|
34
|
-
type IdentityManagerRuntimeParams,
|
|
35
40
|
type JoinIdentityParams,
|
|
41
|
+
type IdentityManagerParams,
|
|
36
42
|
} from '../identity';
|
|
37
43
|
import {
|
|
38
44
|
DeviceInvitationProtocol,
|
|
@@ -43,7 +49,10 @@ import {
|
|
|
43
49
|
} from '../invitations';
|
|
44
50
|
import { DataSpaceManager, type DataSpaceManagerRuntimeParams, type SigningContext } from '../spaces';
|
|
45
51
|
|
|
46
|
-
export type ServiceContextRuntimeParams =
|
|
52
|
+
export type ServiceContextRuntimeParams = Pick<
|
|
53
|
+
IdentityManagerParams,
|
|
54
|
+
'devicePresenceOfflineTimeout' | 'devicePresenceAnnounceInterval'
|
|
55
|
+
> &
|
|
47
56
|
DataSpaceManagerRuntimeParams & {
|
|
48
57
|
invitationConnectionDefaultParams?: Partial<TeleportParams>;
|
|
49
58
|
disableP2pReplication?: boolean;
|
|
@@ -116,13 +125,16 @@ export class ServiceContext extends Resource {
|
|
|
116
125
|
disableP2pReplication: this._runtimeParams?.disableP2pReplication,
|
|
117
126
|
});
|
|
118
127
|
|
|
119
|
-
this.identityManager = new IdentityManager(
|
|
120
|
-
this.metadataStore,
|
|
121
|
-
this.keyring,
|
|
122
|
-
this.feedStore,
|
|
123
|
-
this.spaceManager,
|
|
124
|
-
this._runtimeParams
|
|
125
|
-
|
|
128
|
+
this.identityManager = new IdentityManager({
|
|
129
|
+
metadataStore: this.metadataStore,
|
|
130
|
+
keyring: this.keyring,
|
|
131
|
+
feedStore: this.feedStore,
|
|
132
|
+
spaceManager: this.spaceManager,
|
|
133
|
+
devicePresenceOfflineTimeout: this._runtimeParams?.devicePresenceOfflineTimeout,
|
|
134
|
+
devicePresenceAnnounceInterval: this._runtimeParams?.devicePresenceAnnounceInterval,
|
|
135
|
+
edgeConnection: this._edgeConnection,
|
|
136
|
+
edgeFeatures: this._edgeFeatures,
|
|
137
|
+
callbacks: {
|
|
126
138
|
onIdentityConstruction: (identity) => {
|
|
127
139
|
if (this._edgeConnection) {
|
|
128
140
|
log.info('Setting identity on edge connection', {
|
|
@@ -141,7 +153,7 @@ export class ServiceContext extends Resource {
|
|
|
141
153
|
}
|
|
142
154
|
},
|
|
143
155
|
},
|
|
144
|
-
);
|
|
156
|
+
});
|
|
145
157
|
|
|
146
158
|
this.echoHost = new EchoHost({ kv: this.level });
|
|
147
159
|
|
|
@@ -15,8 +15,8 @@ import { EdgeSignalManager, WebsocketSignalManager, type SignalManager } from '@
|
|
|
15
15
|
import {
|
|
16
16
|
SwarmNetworkManager,
|
|
17
17
|
createIceProvider,
|
|
18
|
-
createSimplePeerTransportFactory,
|
|
19
18
|
type TransportFactory,
|
|
19
|
+
createRtcTransportFactory,
|
|
20
20
|
} from '@dxos/network-manager';
|
|
21
21
|
import { trace } from '@dxos/protocols';
|
|
22
22
|
import { SystemStatus } from '@dxos/protocols/proto/dxos/client/services';
|
|
@@ -218,7 +218,7 @@ export class ClientServicesHost {
|
|
|
218
218
|
|
|
219
219
|
const {
|
|
220
220
|
connectionLog = true,
|
|
221
|
-
transportFactory =
|
|
221
|
+
transportFactory = createRtcTransportFactory(
|
|
222
222
|
{ iceServers: this._config?.get('runtime.services.ice') },
|
|
223
223
|
this._config?.get('runtime.services.iceProviders') &&
|
|
224
224
|
createIceProvider(this._config!.get('runtime.services.iceProviders')!),
|
|
@@ -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,
|
|
@@ -7,8 +7,13 @@ 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
18
|
import type { EdgeConnection } from '@dxos/edge-client';
|
|
14
19
|
import { type FeedStore, type FeedWrapper } from '@dxos/feed-store';
|
|
@@ -0,0 +1,244 @@
|
|
|
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 { 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 { PublicKey, 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('sends a block', async () => {
|
|
45
|
+
const { endpoint, admitConnection, messageSink } = await createEdge();
|
|
46
|
+
const { messenger } = await createClient(endpoint);
|
|
47
|
+
|
|
48
|
+
const { feed } = await attachReplicator(messenger);
|
|
49
|
+
|
|
50
|
+
admitConnection.wake();
|
|
51
|
+
await appendMessage(feed);
|
|
52
|
+
|
|
53
|
+
await expect.poll(() => messageSink.length).toEqual(2);
|
|
54
|
+
expect(messageSink[1].type).toEqual('data');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('re-requests metadata on reconnect', async () => {
|
|
58
|
+
const { endpoint, admitConnection, messageSink } = await createEdge();
|
|
59
|
+
const { messenger } = await createClient(endpoint);
|
|
60
|
+
|
|
61
|
+
await attachReplicator(messenger);
|
|
62
|
+
|
|
63
|
+
admitConnection.wake();
|
|
64
|
+
await expect.poll(() => messageSink.length).toEqual(1);
|
|
65
|
+
|
|
66
|
+
updateIdentity(messenger);
|
|
67
|
+
await messenger.reconnect.waitForCount(1);
|
|
68
|
+
|
|
69
|
+
await expect.poll(() => messageSink.length).toEqual(2);
|
|
70
|
+
expect(messageSink[1].type).toEqual('get-metadata');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('recovers after query sending failure during identity change', async () => {
|
|
74
|
+
const { endpoint, admitConnection, messageSink } = await createEdge();
|
|
75
|
+
const { messenger, sendSpy } = await createClient(endpoint);
|
|
76
|
+
|
|
77
|
+
await attachReplicator(messenger);
|
|
78
|
+
|
|
79
|
+
sendSpy.mockImplementationOnce(() => {
|
|
80
|
+
throw new EdgeIdentityChangedError(); // Hard to mock the exact race condition for when this error is thrown
|
|
81
|
+
});
|
|
82
|
+
admitConnection.wake();
|
|
83
|
+
|
|
84
|
+
await expect.poll(() => sendSpy.mock.calls.length).toEqual(1);
|
|
85
|
+
expect(messageSink.length).toEqual(0);
|
|
86
|
+
updateIdentity(messenger);
|
|
87
|
+
|
|
88
|
+
await expect.poll(() => messageSink.length).toEqual(1);
|
|
89
|
+
expect(messageSink[0].type).toEqual('get-metadata');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('recovers after response sending failure during identity change', async () => {
|
|
93
|
+
const { endpoint, admitConnection, messageSink, sendResponseMessage } = await createEdge();
|
|
94
|
+
const { messenger, sendSpy } = await createClient(endpoint);
|
|
95
|
+
|
|
96
|
+
const { feed } = await attachReplicator(messenger);
|
|
97
|
+
await appendMessage(feed);
|
|
98
|
+
|
|
99
|
+
sendSpy.mockImplementationOnce(async (request: any) => {
|
|
100
|
+
sendResponseMessage(request, encodeCbor({ type: 'metadata', feedKey: feed.key.toHex(), length: 0 }));
|
|
101
|
+
return Promise.resolve();
|
|
102
|
+
});
|
|
103
|
+
sendSpy.mockImplementationOnce(async () => {
|
|
104
|
+
throw new EdgeIdentityChangedError();
|
|
105
|
+
});
|
|
106
|
+
admitConnection.wake();
|
|
107
|
+
|
|
108
|
+
await expect.poll(() => sendSpy.mock.calls.length).toEqual(2);
|
|
109
|
+
expect(messageSink.length).toEqual(0);
|
|
110
|
+
updateIdentity(messenger);
|
|
111
|
+
|
|
112
|
+
await expect.poll(() => messageSink.length).toEqual(2);
|
|
113
|
+
expect(messageSink[1].type).toEqual('data');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('propagates errors unrelated to reconnect', async () => {
|
|
117
|
+
const { endpoint, admitConnection } = await createEdge();
|
|
118
|
+
const { messenger, sendSpy } = await createClient(endpoint);
|
|
119
|
+
|
|
120
|
+
const { replicator } = await attachReplicator(messenger, { skipOpen: true });
|
|
121
|
+
const raised = new Trigger();
|
|
122
|
+
await replicator.open(new Context({ onError: () => raised.wake() }));
|
|
123
|
+
onTestFinished(async () => {
|
|
124
|
+
await replicator.close();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
sendSpy.mockImplementationOnce(() => {
|
|
128
|
+
throw new Error();
|
|
129
|
+
});
|
|
130
|
+
admitConnection.wake();
|
|
131
|
+
|
|
132
|
+
await raised.wait();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('identity update before connected', async () => {
|
|
136
|
+
const { endpoint, admitConnection, messageSink } = await createEdge();
|
|
137
|
+
const { messenger } = await createClient(endpoint);
|
|
138
|
+
|
|
139
|
+
await attachReplicator(messenger);
|
|
140
|
+
updateIdentity(messenger);
|
|
141
|
+
await sleep(100);
|
|
142
|
+
admitConnection.wake();
|
|
143
|
+
|
|
144
|
+
await expect.poll(() => messageSink.length).toEqual(2);
|
|
145
|
+
expect(messageSink.map((m) => m.type)).toStrictEqual(range(2, () => 'get-metadata'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('block appended during reconnect', async () => {
|
|
149
|
+
const { endpoint, admitConnection, feedLength } = await createEdge();
|
|
150
|
+
const { messenger } = await createClient(endpoint);
|
|
151
|
+
|
|
152
|
+
const { feed } = await attachReplicator(messenger);
|
|
153
|
+
admitConnection.wake();
|
|
154
|
+
await sleep(10);
|
|
155
|
+
|
|
156
|
+
admitConnection.reset();
|
|
157
|
+
updateIdentity(messenger);
|
|
158
|
+
await appendMessage(feed);
|
|
159
|
+
await sleep(20);
|
|
160
|
+
admitConnection.wake();
|
|
161
|
+
|
|
162
|
+
await expect.poll(() => feedLength()).toEqual(1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('reconnect during block append', async () => {
|
|
166
|
+
const { endpoint, admitConnection, feedLength } = await createEdge();
|
|
167
|
+
const { messenger } = await createClient(endpoint);
|
|
168
|
+
|
|
169
|
+
const { feed } = await attachReplicator(messenger);
|
|
170
|
+
admitConnection.wake();
|
|
171
|
+
await sleep(10);
|
|
172
|
+
|
|
173
|
+
void appendMessage(feed);
|
|
174
|
+
updateIdentity(messenger);
|
|
175
|
+
|
|
176
|
+
await expect.poll(() => feedLength()).toEqual(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const createEdge = async () => {
|
|
180
|
+
const port = await getRandomPort('127.0.0.1');
|
|
181
|
+
let lastBlockIndex = -1;
|
|
182
|
+
const admitConnection = new Trigger();
|
|
183
|
+
const { cleanup, endpoint, messageSink, sendResponseMessage } = await createTestEdgeWsServer(port, {
|
|
184
|
+
admitConnection,
|
|
185
|
+
payloadDecoder: decodeCbor,
|
|
186
|
+
messageHandler: async (message: any) => {
|
|
187
|
+
if (message.type === 'get-metadata') {
|
|
188
|
+
return encodeCbor({ type: 'metadata', feedKey: message.feedKey, length: lastBlockIndex + 1 });
|
|
189
|
+
} else {
|
|
190
|
+
lastBlockIndex = Math.max(lastBlockIndex, message.blocks[message.blocks.length - 1].index);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
onTestFinished(cleanup);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
endpoint,
|
|
198
|
+
messageSink,
|
|
199
|
+
admitConnection,
|
|
200
|
+
sendResponseMessage,
|
|
201
|
+
feedLength: () => lastBlockIndex + 1,
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const createClient = async (endpoint: string) => {
|
|
206
|
+
const peerKey = PublicKey.random().toHex();
|
|
207
|
+
const messenger = new EdgeClient(peerKey, peerKey, { socketEndpoint: endpoint });
|
|
208
|
+
const sendSpy = vi.spyOn(messenger, 'send');
|
|
209
|
+
await openAndClose(messenger);
|
|
210
|
+
return { messenger, sendSpy };
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const attachReplicator = async (messenger: EdgeClient, options?: { skipOpen?: boolean }) => {
|
|
214
|
+
const spaceId = SpaceId.random();
|
|
215
|
+
const feed = await createNewFeed();
|
|
216
|
+
const replicator = new EdgeFeedReplicator({ messenger, spaceId });
|
|
217
|
+
await replicator.addFeed(feed);
|
|
218
|
+
if (!options?.skipOpen) {
|
|
219
|
+
await openAndClose(replicator);
|
|
220
|
+
}
|
|
221
|
+
return { feed, replicator };
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const createNewFeed = async () => {
|
|
225
|
+
const storage = createStorage();
|
|
226
|
+
const keyring = new Keyring();
|
|
227
|
+
const feedStore = new FeedStore<FeedMessage>({
|
|
228
|
+
factory: new FeedFactory<FeedMessage>({
|
|
229
|
+
root: storage.createDirectory(),
|
|
230
|
+
signer: keyring,
|
|
231
|
+
hypercore: { valueEncoding },
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
onTestFinished(() => feedStore.close());
|
|
235
|
+
return feedStore.openFeed(await keyring.createKey(), { writable: true });
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const updateIdentity = (messenger: EdgeClient) => {
|
|
239
|
+
const identityKey = PublicKey.random().toHex();
|
|
240
|
+
messenger.setIdentity({ peerKey: messenger.peerKey, identityKey });
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const appendMessage = (feed: FeedWrapper<FeedMessage>) => feed.append({ timeframe: new Timeframe() });
|
|
244
|
+
});
|
|
@@ -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
14
|
import { log } 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
|
|
|
@@ -48,7 +52,7 @@ export class EdgeFeedReplicator extends Resource {
|
|
|
48
52
|
protected override async _open(): Promise<void> {
|
|
49
53
|
// TODO: handle reconnects
|
|
50
54
|
this._ctx.onDispose(
|
|
51
|
-
this._messenger.addListener(
|
|
55
|
+
this._messenger.addListener((message: RouterMessage) => {
|
|
52
56
|
if (!message.serviceId) {
|
|
53
57
|
return;
|
|
54
58
|
}
|
|
@@ -64,21 +68,43 @@ export class EdgeFeedReplicator extends Resource {
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
const payload = decodeCbor(message.payload!.value) as ProtocolMessage;
|
|
67
|
-
log.info('
|
|
71
|
+
log.info('receive', { from: message.source, feedKey: payload.feedKey, type: payload.type });
|
|
68
72
|
this._onMessage(payload);
|
|
69
73
|
}),
|
|
70
74
|
);
|
|
71
75
|
|
|
72
|
-
this.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
this._messenger.connected.on(this._ctx, async () => {
|
|
77
|
+
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
|
+
});
|
|
100
|
+
});
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
protected override async _close(): Promise<void> {
|
|
80
|
-
this.
|
|
104
|
+
await this._resetConnection();
|
|
105
|
+
}
|
|
81
106
|
|
|
107
|
+
private async _resetConnection() {
|
|
82
108
|
this._connected = false;
|
|
83
109
|
await this._connectionCtx?.dispose();
|
|
84
110
|
this._connectionCtx = undefined;
|
|
@@ -89,8 +115,8 @@ export class EdgeFeedReplicator extends Resource {
|
|
|
89
115
|
log.info('addFeed', { key: feed.key });
|
|
90
116
|
this._feeds.set(feed.key, feed);
|
|
91
117
|
|
|
92
|
-
if (this._connected) {
|
|
93
|
-
await this._replicateFeed(feed);
|
|
118
|
+
if (this._connected && this._connectionCtx) {
|
|
119
|
+
await this._replicateFeed(this._connectionCtx, feed);
|
|
94
120
|
}
|
|
95
121
|
}
|
|
96
122
|
|
|
@@ -98,21 +124,26 @@ export class EdgeFeedReplicator extends Resource {
|
|
|
98
124
|
return defaultMap(this._pushMutex, key, () => new Mutex());
|
|
99
125
|
}
|
|
100
126
|
|
|
101
|
-
private async _replicateFeed(feed: FeedWrapper<any>) {
|
|
102
|
-
invariant(this._connectionCtx);
|
|
103
|
-
|
|
127
|
+
private async _replicateFeed(ctx: Context, feed: FeedWrapper<any>) {
|
|
104
128
|
await this._sendMessage({
|
|
105
129
|
type: 'get-metadata',
|
|
106
130
|
feedKey: feed.key.toHex(),
|
|
107
131
|
});
|
|
108
132
|
|
|
109
|
-
Event.wrap(feed.core as any, 'append').on(
|
|
133
|
+
Event.wrap(feed.core as any, 'append').on(ctx, async () => {
|
|
110
134
|
await this._pushBlocksIfNeeded(feed);
|
|
111
135
|
});
|
|
112
136
|
}
|
|
113
137
|
|
|
114
138
|
private async _sendMessage(message: ProtocolMessage) {
|
|
115
|
-
|
|
139
|
+
if (!this._connectionCtx) {
|
|
140
|
+
log.info('message dropped because connection was disposed');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const logPayload =
|
|
145
|
+
message.type === 'data' ? { feedKey: message.feedKey, blocks: message.blocks.map((b) => b.index) } : { message };
|
|
146
|
+
log.info('sending message', logPayload);
|
|
116
147
|
|
|
117
148
|
invariant(message.feedKey);
|
|
118
149
|
const payloadValue = bufferToArray(encodeCbor(message));
|
|
@@ -130,11 +161,15 @@ export class EdgeFeedReplicator extends Resource {
|
|
|
130
161
|
}
|
|
131
162
|
|
|
132
163
|
private _onMessage(message: ProtocolMessage) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
164
|
+
if (!this._connectionCtx) {
|
|
165
|
+
log.warn('received message after connection context was disposed');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
scheduleMicroTask(this._connectionCtx, async () => {
|
|
136
169
|
switch (message.type) {
|
|
137
170
|
case 'metadata': {
|
|
171
|
+
log.info('received metadata', { message });
|
|
172
|
+
|
|
138
173
|
const feedKey = PublicKey.fromHex(message.feedKey);
|
|
139
174
|
const feed = this._feeds.get(feedKey);
|
|
140
175
|
if (!feed) {
|
|
@@ -160,6 +195,8 @@ export class EdgeFeedReplicator extends Resource {
|
|
|
160
195
|
}
|
|
161
196
|
|
|
162
197
|
case 'data': {
|
|
198
|
+
log.info('received data', { feed: message.feedKey, blocks: message.blocks.map((b) => b.index) });
|
|
199
|
+
|
|
163
200
|
const feedKey = PublicKey.fromHex(message.feedKey);
|
|
164
201
|
const feed = this._feeds.get(feedKey);
|
|
165
202
|
if (!feed) {
|
|
@@ -223,9 +260,10 @@ export class EdgeFeedReplicator extends Resource {
|
|
|
223
260
|
}
|
|
224
261
|
|
|
225
262
|
private async _pushBlocksIfNeeded(feed: FeedWrapper<any>) {
|
|
226
|
-
using
|
|
263
|
+
using _ = await this._getPushMutex(feed.key).acquire();
|
|
227
264
|
|
|
228
265
|
if (!this._remoteLength.has(feed.key)) {
|
|
266
|
+
log('blocks not pushed because remote length is unknown');
|
|
229
267
|
return;
|
|
230
268
|
}
|
|
231
269
|
|