@dxos/client-services 0.6.13-main.548ca8d → 0.6.13-staging.1e988a3
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-UEQIHAL2.mjs → chunk-4XKSDWEO.mjs} +5218 -5905
- package/dist/lib/browser/chunk-4XKSDWEO.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 +7 -8
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-MA5EWTRH.cjs → chunk-HNOOWB6F.cjs} +5060 -5743
- package/dist/lib/node/chunk-HNOOWB6F.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 +13 -14
- package/dist/lib/node/testing/index.cjs.map +3 -3
- 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.test.d.ts +2 -0
- package/dist/types/src/packlets/identity/authenticator.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 +7 -19
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +1 -8
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +1 -2
- package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +1 -2
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/services/automerge-host.test.d.ts +2 -0
- package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +1 -0
- package/dist/types/src/packlets/services/service-context.d.ts +9 -9
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +0 -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 +3 -6
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +3 -4
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +0 -3
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
- 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 +6 -31
- 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 +2 -1
- 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/dist/types/src/version.d.ts.map +1 -1
- package/package.json +39 -43
- package/src/packlets/devices/devices-service.test.ts +5 -4
- package/src/packlets/diagnostics/diagnostics-broadcast.ts +0 -1
- package/src/packlets/identity/{authenticator.node.test.ts → authenticator.test.ts} +3 -2
- package/src/packlets/identity/authenticator.ts +2 -5
- package/src/packlets/identity/contacts-service.ts +1 -1
- package/src/packlets/identity/identity-manager.test.ts +6 -5
- package/src/packlets/identity/identity-manager.ts +19 -35
- package/src/packlets/identity/identity-service.test.ts +8 -4
- package/src/packlets/identity/identity.test.ts +239 -128
- package/src/packlets/identity/identity.ts +8 -42
- package/src/packlets/invitations/device-invitation-protocol.test.ts +4 -7
- package/src/packlets/invitations/invitation-guest-extenstion.ts +4 -8
- package/src/packlets/invitations/invitation-host-extension.ts +7 -8
- package/src/packlets/invitations/invitations-handler.test.ts +9 -16
- package/src/packlets/invitations/invitations-handler.ts +92 -23
- package/src/packlets/invitations/space-invitation-protocol.test.ts +3 -4
- package/src/packlets/invitations/space-invitation-protocol.ts +0 -4
- package/src/packlets/logging/logging.test.ts +2 -1
- package/src/packlets/network/network-service.test.ts +3 -2
- package/src/packlets/services/automerge-host.test.ts +60 -0
- package/src/packlets/services/service-context.test.ts +1 -3
- package/src/packlets/services/service-context.ts +31 -68
- package/src/packlets/services/service-host.test.ts +12 -8
- package/src/packlets/services/service-host.ts +6 -8
- package/src/packlets/services/service-registry.test.ts +2 -1
- package/src/packlets/spaces/data-space-manager.test.ts +2 -2
- package/src/packlets/spaces/data-space-manager.ts +5 -40
- package/src/packlets/spaces/data-space.ts +6 -34
- package/src/packlets/spaces/edge-feed-replicator.ts +22 -80
- package/src/packlets/spaces/epoch-migrations.ts +2 -2
- package/src/packlets/spaces/notarization-plugin.test.ts +7 -10
- package/src/packlets/spaces/notarization-plugin.ts +29 -169
- package/src/packlets/spaces/spaces-service.test.ts +9 -5
- package/src/packlets/spaces/spaces-service.ts +1 -6
- package/src/packlets/storage/storage.ts +1 -0
- package/src/packlets/system/system-service.test.ts +2 -1
- package/src/packlets/testing/test-builder.ts +4 -3
- package/src/packlets/worker/worker-runtime.ts +2 -2
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-UEQIHAL2.mjs.map +0 -7
- package/dist/lib/node/chunk-MA5EWTRH.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-AIBLDI4U.mjs +0 -8403
- package/dist/lib/node-esm/chunk-AIBLDI4U.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -416
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -420
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +0 -2
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +0 -30
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/invitation-state.d.ts +0 -19
- package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +0 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +0 -2
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +0 -1
- package/dist/types/src/testing/setup.d.ts +0 -3
- package/dist/types/src/testing/setup.d.ts.map +0 -1
- package/src/packlets/invitations/edge-invitation-handler.ts +0 -184
- package/src/packlets/invitations/invitation-state.ts +0 -111
- package/src/packlets/spaces/edge-feed-replicator.test.ts +0 -253
- package/src/testing/setup.ts +0 -11
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { expect } from 'chai';
|
|
6
6
|
|
|
7
7
|
import { asyncChain } from '@dxos/async';
|
|
8
8
|
import { Context } from '@dxos/context';
|
|
9
9
|
import { AlreadyJoinedError } from '@dxos/protocols';
|
|
10
10
|
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
|
+
import { describe, test, afterTest } from '@dxos/test';
|
|
11
12
|
|
|
12
13
|
import { type ServiceContext } from '../services';
|
|
13
14
|
import { createPeers, createServiceContext, performInvitation } from '../testing';
|
|
14
15
|
|
|
15
16
|
const closeAfterTest = async (peer: ServiceContext) => {
|
|
16
|
-
|
|
17
|
-
await peer.close();
|
|
18
|
-
});
|
|
17
|
+
afterTest(() => peer.close());
|
|
19
18
|
return peer;
|
|
20
19
|
};
|
|
21
20
|
|
|
@@ -23,9 +22,7 @@ describe('services/device', () => {
|
|
|
23
22
|
test('creates identity', async () => {
|
|
24
23
|
const peer = await createServiceContext();
|
|
25
24
|
await peer.open(new Context());
|
|
26
|
-
|
|
27
|
-
await peer.close();
|
|
28
|
-
});
|
|
25
|
+
afterTest(() => peer.close());
|
|
29
26
|
|
|
30
27
|
const identity = await peer.createIdentity();
|
|
31
28
|
expect(identity).not.to.be.undefined;
|
|
@@ -12,7 +12,6 @@ import { type Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
|
12
12
|
import { type InvitationHostService, InvitationOptions } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
13
13
|
import { type ExtensionContext, RpcExtension } from '@dxos/teleport';
|
|
14
14
|
|
|
15
|
-
import { type FlowLockHolder } from './invitation-state';
|
|
16
15
|
import { tryAcquireBeforeContextDisposed } from './utils';
|
|
17
16
|
|
|
18
17
|
const OPTIONS_TIMEOUT = 10_000;
|
|
@@ -28,13 +27,10 @@ type InvitationGuestExtensionCallbacks = {
|
|
|
28
27
|
/**
|
|
29
28
|
* Guest's side for a connection to a concrete peer in p2p network during invitation.
|
|
30
29
|
*/
|
|
31
|
-
export class InvitationGuestExtension
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
>
|
|
36
|
-
implements FlowLockHolder
|
|
37
|
-
{
|
|
30
|
+
export class InvitationGuestExtension extends RpcExtension<
|
|
31
|
+
{ InvitationHostService: InvitationHostService },
|
|
32
|
+
{ InvitationHostService: InvitationHostService }
|
|
33
|
+
> {
|
|
38
34
|
private _ctx = new Context();
|
|
39
35
|
private _remoteOptions?: InvitationOptions;
|
|
40
36
|
private _remoteOptionsTrigger = new Trigger();
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
} from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
22
22
|
import { type ExtensionContext, RpcExtension } from '@dxos/teleport';
|
|
23
23
|
|
|
24
|
-
import type { FlowLockHolder } from './invitation-state';
|
|
25
24
|
import { stateToString, tryAcquireBeforeContextDisposed } from './utils';
|
|
26
25
|
|
|
27
26
|
/// Timeout for the options exchange.
|
|
@@ -44,13 +43,10 @@ type InvitationHostExtensionCallbacks = {
|
|
|
44
43
|
/**
|
|
45
44
|
* Host's side for a connection to a concrete peer in p2p network during invitation.
|
|
46
45
|
*/
|
|
47
|
-
export class InvitationHostExtension
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
>
|
|
52
|
-
implements FlowLockHolder
|
|
53
|
-
{
|
|
46
|
+
export class InvitationHostExtension extends RpcExtension<
|
|
47
|
+
{ InvitationHostService: InvitationHostService },
|
|
48
|
+
{ InvitationHostService: InvitationHostService }
|
|
49
|
+
> {
|
|
54
50
|
/**
|
|
55
51
|
* @internal
|
|
56
52
|
*/
|
|
@@ -110,11 +106,13 @@ export class InvitationHostExtension
|
|
|
110
106
|
|
|
111
107
|
introduce: async (request) => {
|
|
112
108
|
const { profile, invitationId } = request;
|
|
109
|
+
|
|
113
110
|
const traceId = PublicKey.random().toHex();
|
|
114
111
|
log.trace('dxos.sdk.invitation-handler.host.introduce', trace.begin({ id: traceId }));
|
|
115
112
|
|
|
116
113
|
const invitation = this._requireActiveInvitation();
|
|
117
114
|
this._assertInvitationState(Invitation.State.CONNECTED);
|
|
115
|
+
|
|
118
116
|
if (invitationId !== invitation?.invitationId) {
|
|
119
117
|
log.warn('incorrect invitationId', { expected: invitation.invitationId, actual: invitationId });
|
|
120
118
|
this._callbacks.onError(new Error('Incorrect invitationId.'));
|
|
@@ -128,6 +126,7 @@ export class InvitationHostExtension
|
|
|
128
126
|
log('guest introduced themselves', { guestProfile: profile });
|
|
129
127
|
this.guestProfile = profile;
|
|
130
128
|
this._callbacks.onStateUpdate(Invitation.State.READY_FOR_AUTHENTICATION);
|
|
129
|
+
|
|
131
130
|
this._challenge =
|
|
132
131
|
invitation.authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? randomBytes(32) : undefined;
|
|
133
132
|
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { expect } from 'chai';
|
|
6
6
|
|
|
7
7
|
import { type PushStream, sleep, Trigger, waitForCondition } from '@dxos/async';
|
|
8
8
|
import { Context } from '@dxos/context';
|
|
9
9
|
import { PublicKey } from '@dxos/keys';
|
|
10
10
|
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
|
-
import { openAndClose } from '@dxos/test
|
|
11
|
+
import { afterTest, describe, openAndClose, test } from '@dxos/test';
|
|
12
12
|
import { range } from '@dxos/util';
|
|
13
13
|
|
|
14
14
|
import { type InvitationProtocol } from './invitation-protocol';
|
|
@@ -34,7 +34,6 @@ type StateUpdateSink = PushStream<Invitation> & {
|
|
|
34
34
|
|
|
35
35
|
describe('InvitationHandler', () => {
|
|
36
36
|
let testBuilder: TestBuilder;
|
|
37
|
-
|
|
38
37
|
beforeEach(() => {
|
|
39
38
|
testBuilder = new TestBuilder();
|
|
40
39
|
});
|
|
@@ -164,7 +163,7 @@ describe('InvitationHandler', () => {
|
|
|
164
163
|
expect(guest.ctx.disposed).to.be.true;
|
|
165
164
|
});
|
|
166
165
|
|
|
167
|
-
test('guest gives up after trying with three hosts',
|
|
166
|
+
test('guest gives up after trying with three hosts', async () => {
|
|
168
167
|
const hosts: PeerSetup[] = [await createPeer()];
|
|
169
168
|
const [host] = hosts;
|
|
170
169
|
const invitation = await createInvitation(host, { multiUse: true });
|
|
@@ -182,7 +181,7 @@ describe('InvitationHandler', () => {
|
|
|
182
181
|
|
|
183
182
|
await sleep(10);
|
|
184
183
|
expect(guest.sink.lastState).to.eq(Invitation.State.ERROR);
|
|
185
|
-
});
|
|
184
|
+
}).timeout(20_000);
|
|
186
185
|
|
|
187
186
|
test('single host - many guests', async () => {
|
|
188
187
|
const hosts: PeerSetup[] = [await createPeer()];
|
|
@@ -257,14 +256,12 @@ describe('InvitationHandler', () => {
|
|
|
257
256
|
const space = await peer.dataSpaceManager.createSpace();
|
|
258
257
|
spaceKey = space.key;
|
|
259
258
|
}
|
|
260
|
-
const invitationHandler = new InvitationsHandler(peer.networkManager,
|
|
261
|
-
|
|
259
|
+
const invitationHandler = new InvitationsHandler(peer.networkManager, {
|
|
260
|
+
controlHeartbeatInterval: 250, // faster peer failure detection
|
|
262
261
|
});
|
|
263
262
|
const protocol = new SpaceInvitationProtocol(peer.dataSpaceManager, peer.identity, peer.keyring, spaceKey);
|
|
264
263
|
const ctx = new Context();
|
|
265
|
-
|
|
266
|
-
await ctx.dispose();
|
|
267
|
-
});
|
|
264
|
+
afterTest(() => ctx.dispose());
|
|
268
265
|
const sink = newStateUpdateSink();
|
|
269
266
|
return { ctx, sink, peer, protocol, handler: invitationHandler, spaceKey };
|
|
270
267
|
};
|
|
@@ -272,18 +269,14 @@ describe('InvitationHandler', () => {
|
|
|
272
269
|
const hostInvitation = async (setup: PeerSetup, invitation: Invitation) => {
|
|
273
270
|
await setup.ctx.dispose();
|
|
274
271
|
setup.ctx = new Context();
|
|
275
|
-
|
|
276
|
-
await setup.ctx.dispose();
|
|
277
|
-
});
|
|
272
|
+
afterTest(() => setup.ctx.dispose());
|
|
278
273
|
setup.handler.handleInvitationFlow(setup.ctx, setup.sink, setup.protocol, invitation);
|
|
279
274
|
};
|
|
280
275
|
|
|
281
276
|
const acceptInvitation = async (setup: PeerSetup, invitation: Invitation): Promise<Trigger<string>> => {
|
|
282
277
|
await setup.ctx.dispose();
|
|
283
278
|
setup.ctx = new Context();
|
|
284
|
-
|
|
285
|
-
await setup.ctx.dispose();
|
|
286
|
-
});
|
|
279
|
+
afterTest(() => setup.ctx.dispose());
|
|
287
280
|
const authCodeInput = new Trigger<string>();
|
|
288
281
|
setup.handler.acceptInvitation(setup.ctx, setup.sink, setup.protocol, invitation, authCodeInput);
|
|
289
282
|
return authCodeInput;
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type PushStream, scheduleTask, TimeoutError, type Trigger } from '@dxos/async';
|
|
5
|
+
import { Mutex, type PushStream, scheduleTask, TimeoutError, type Trigger } from '@dxos/async';
|
|
6
6
|
import { INVITATION_TIMEOUT } from '@dxos/client-protocol';
|
|
7
7
|
import { type Context, ContextDisposedError } from '@dxos/context';
|
|
8
8
|
import { createKeyPair, sign } from '@dxos/crypto';
|
|
9
|
-
import { type EdgeHttpClient } from '@dxos/edge-client';
|
|
10
9
|
import { invariant } from '@dxos/invariant';
|
|
11
10
|
import { PublicKey } from '@dxos/keys';
|
|
12
11
|
import { log } from '@dxos/log';
|
|
@@ -20,21 +19,17 @@ import { type ExtensionContext, type TeleportExtension, type TeleportParams } fr
|
|
|
20
19
|
import { trace as _trace } from '@dxos/tracing';
|
|
21
20
|
import { ComplexSet } from '@dxos/util';
|
|
22
21
|
|
|
23
|
-
import { type EdgeInvitationConfig, EdgeInvitationHandler } from './edge-invitation-handler';
|
|
24
22
|
import { InvitationGuestExtension } from './invitation-guest-extenstion';
|
|
25
23
|
import { InvitationHostExtension, isAuthenticationRequired, MAX_OTP_ATTEMPTS } from './invitation-host-extension';
|
|
26
24
|
import { type InvitationProtocol } from './invitation-protocol';
|
|
27
|
-
import { createGuardedInvitationState } from './invitation-state';
|
|
28
25
|
import { InvitationTopology } from './invitation-topology';
|
|
26
|
+
import { stateToString } from './utils';
|
|
29
27
|
|
|
30
28
|
const metrics = _trace.metrics;
|
|
31
29
|
|
|
32
30
|
const MAX_DELEGATED_INVITATION_HOST_TRIES = 3;
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
teleport: Partial<TeleportParams>;
|
|
36
|
-
edgeInvitations?: EdgeInvitationConfig;
|
|
37
|
-
};
|
|
32
|
+
type InvitationExtension = InvitationHostExtension | InvitationGuestExtension;
|
|
38
33
|
|
|
39
34
|
/**
|
|
40
35
|
* Generic handler for Halo and Space invitations.
|
|
@@ -70,8 +65,7 @@ export class InvitationsHandler {
|
|
|
70
65
|
*/
|
|
71
66
|
constructor(
|
|
72
67
|
private readonly _networkManager: SwarmNetworkManager,
|
|
73
|
-
private readonly
|
|
74
|
-
private readonly _connectionParams?: InvitationConnectionParams,
|
|
68
|
+
private readonly _defaultTeleportParams?: Partial<TeleportParams>,
|
|
75
69
|
) {}
|
|
76
70
|
|
|
77
71
|
handleInvitationFlow(
|
|
@@ -81,7 +75,7 @@ export class InvitationsHandler {
|
|
|
81
75
|
invitation: Invitation,
|
|
82
76
|
): void {
|
|
83
77
|
metrics.increment('dxos.invitation.created');
|
|
84
|
-
const guardedState =
|
|
78
|
+
const guardedState = this._createGuardedState(ctx, invitation, stream);
|
|
85
79
|
// Called for every connecting peer.
|
|
86
80
|
const createExtension = (): InvitationHostExtension => {
|
|
87
81
|
const extension = new InvitationHostExtension(guardedState.mutex, {
|
|
@@ -214,7 +208,7 @@ export class InvitationsHandler {
|
|
|
214
208
|
}
|
|
215
209
|
|
|
216
210
|
const triedPeersIds = new ComplexSet(PublicKey.hash);
|
|
217
|
-
const guardedState =
|
|
211
|
+
const guardedState = this._createGuardedState(ctx, invitation, stream);
|
|
218
212
|
|
|
219
213
|
const shouldCancelInvitationFlow = (extension: InvitationGuestExtension) => {
|
|
220
214
|
const isLockedByAnotherConnection = guardedState.mutex.isLocked() && !extension.hasFlowLock();
|
|
@@ -316,7 +310,7 @@ export class InvitationsHandler {
|
|
|
316
310
|
|
|
317
311
|
// 5. Success.
|
|
318
312
|
log('admitted by host', { ...protocol.toJSON() });
|
|
319
|
-
guardedState.complete({
|
|
313
|
+
await guardedState.complete({
|
|
320
314
|
...guardedState.current,
|
|
321
315
|
...result,
|
|
322
316
|
state: Invitation.State.SUCCESS,
|
|
@@ -352,15 +346,6 @@ export class InvitationsHandler {
|
|
|
352
346
|
return extension;
|
|
353
347
|
};
|
|
354
348
|
|
|
355
|
-
const edgeInvitationHandler = new EdgeInvitationHandler(this._connectionParams?.edgeInvitations, this._edgeClient, {
|
|
356
|
-
onInvitationSuccess: async (admissionResponse, admissionRequest) => {
|
|
357
|
-
const result = await protocol.accept(admissionResponse, admissionRequest);
|
|
358
|
-
log('admitted by edge', { ...protocol.toJSON() });
|
|
359
|
-
guardedState.complete({ ...guardedState.current, ...result, state: Invitation.State.SUCCESS });
|
|
360
|
-
},
|
|
361
|
-
});
|
|
362
|
-
edgeInvitationHandler.handle(ctx, guardedState, protocol, deviceProfile);
|
|
363
|
-
|
|
364
349
|
scheduleTask(ctx, async () => {
|
|
365
350
|
const error = protocol.checkInvitation(invitation);
|
|
366
351
|
if (error) {
|
|
@@ -404,7 +389,7 @@ export class InvitationsHandler {
|
|
|
404
389
|
topic: invitation.swarmKey,
|
|
405
390
|
protocolProvider: createTeleportProtocolFactory(async (teleport) => {
|
|
406
391
|
teleport.addExtension('dxos.halo.invitations', extensionFactory());
|
|
407
|
-
}, this.
|
|
392
|
+
}, this._defaultTeleportParams),
|
|
408
393
|
topology: new InvitationTopology(role),
|
|
409
394
|
label,
|
|
410
395
|
});
|
|
@@ -412,6 +397,90 @@ export class InvitationsHandler {
|
|
|
412
397
|
return swarmConnection;
|
|
413
398
|
}
|
|
414
399
|
|
|
400
|
+
/**
|
|
401
|
+
* A utility object for serializing invitation state changes by multiple concurrent
|
|
402
|
+
* invitation flow connections.
|
|
403
|
+
*/
|
|
404
|
+
private _createGuardedState(ctx: Context, invitation: Invitation, stream: PushStream<Invitation>) {
|
|
405
|
+
// the mutex guards invitation flow on host and guest side, making sure only one flow is currently active
|
|
406
|
+
// deadlocks seem very unlikely because hosts don't initiate multiple connections
|
|
407
|
+
// even if this somehow happens that there are 2 guests (A, B) and 2 hosts (1, 2) and:
|
|
408
|
+
// A has lock for flow with 1, B has lock for flow with 2
|
|
409
|
+
// 1 has lock for flow with B, 2 has lock for flow with A
|
|
410
|
+
// there'll be a 10-second introduction timeout after which connection will be closed and deadlock broken
|
|
411
|
+
const mutex = new Mutex();
|
|
412
|
+
let lastActiveExtension: any = null;
|
|
413
|
+
let currentInvitation = { ...invitation };
|
|
414
|
+
const isStateChangeAllowed = (extension: InvitationExtension | null) => {
|
|
415
|
+
if (ctx.disposed || (extension !== null && mutex.isLocked() && !extension.hasFlowLock())) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
// don't allow transitions from a terminal state unless a new extension acquired mutex
|
|
419
|
+
// handles a case when error occurs (e.g. connection is closed) after we completed the flow
|
|
420
|
+
// successfully or already reported another error
|
|
421
|
+
return extension == null || lastActiveExtension !== extension || this._isNotTerminal(currentInvitation.state);
|
|
422
|
+
};
|
|
423
|
+
return {
|
|
424
|
+
mutex,
|
|
425
|
+
get current() {
|
|
426
|
+
return currentInvitation;
|
|
427
|
+
},
|
|
428
|
+
// disposing context prevents any further state updates
|
|
429
|
+
complete: (newState: Partial<Invitation>) => {
|
|
430
|
+
currentInvitation = { ...currentInvitation, ...newState };
|
|
431
|
+
stream.next(currentInvitation);
|
|
432
|
+
return ctx.dispose();
|
|
433
|
+
},
|
|
434
|
+
set: (extension: InvitationExtension | null, newState: Invitation.State): boolean => {
|
|
435
|
+
if (isStateChangeAllowed(extension)) {
|
|
436
|
+
this._logStateUpdate(currentInvitation, extension, newState);
|
|
437
|
+
currentInvitation = { ...currentInvitation, state: newState };
|
|
438
|
+
stream.next(currentInvitation);
|
|
439
|
+
lastActiveExtension = extension;
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
},
|
|
444
|
+
error: (extension: InvitationExtension | null, error: any): boolean => {
|
|
445
|
+
if (isStateChangeAllowed(extension)) {
|
|
446
|
+
this._logStateUpdate(currentInvitation, extension, Invitation.State.ERROR);
|
|
447
|
+
currentInvitation = { ...currentInvitation, state: Invitation.State.ERROR };
|
|
448
|
+
stream.next(currentInvitation);
|
|
449
|
+
stream.error(error);
|
|
450
|
+
lastActiveExtension = extension;
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private _logStateUpdate(invitation: Invitation, actor: any, newState: Invitation.State) {
|
|
459
|
+
if (this._isNotTerminal(newState)) {
|
|
460
|
+
log('invitation state update', {
|
|
461
|
+
actor: actor?.constructor.name,
|
|
462
|
+
newState: stateToString(newState),
|
|
463
|
+
oldState: stateToString(invitation.state),
|
|
464
|
+
});
|
|
465
|
+
} else {
|
|
466
|
+
log.info('invitation state update', {
|
|
467
|
+
actor: actor?.constructor.name,
|
|
468
|
+
newState: stateToString(newState),
|
|
469
|
+
oldState: stateToString(invitation.state),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private _isNotTerminal(currentState: Invitation.State): boolean {
|
|
475
|
+
return ![
|
|
476
|
+
Invitation.State.SUCCESS,
|
|
477
|
+
Invitation.State.ERROR,
|
|
478
|
+
Invitation.State.CANCELLED,
|
|
479
|
+
Invitation.State.TIMEOUT,
|
|
480
|
+
Invitation.State.EXPIRED,
|
|
481
|
+
].includes(currentState);
|
|
482
|
+
}
|
|
483
|
+
|
|
415
484
|
private async _handleGuestOtpAuth(
|
|
416
485
|
extension: InvitationGuestExtension,
|
|
417
486
|
setState: (newState: Invitation.State) => void,
|
|
@@ -2,21 +2,20 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { expect } from 'chai';
|
|
6
6
|
|
|
7
7
|
import { asyncChain, Trigger } from '@dxos/async';
|
|
8
8
|
import { raise } from '@dxos/debug';
|
|
9
9
|
import { AlreadyJoinedError } from '@dxos/protocols';
|
|
10
10
|
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
|
+
import { afterTest, describe, test } from '@dxos/test';
|
|
11
12
|
|
|
12
13
|
import { type ServiceContext } from '../services';
|
|
13
14
|
import { createIdentity, createPeers } from '../testing';
|
|
14
15
|
import { acceptInvitation, createInvitation, performInvitation } from '../testing/invitation-utils';
|
|
15
16
|
|
|
16
17
|
const closeAfterTest = async (peer: ServiceContext) => {
|
|
17
|
-
|
|
18
|
-
await peer.close();
|
|
19
|
-
});
|
|
18
|
+
afterTest(() => peer.close());
|
|
20
19
|
return peer;
|
|
21
20
|
};
|
|
22
21
|
|
|
@@ -60,13 +60,9 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
getInvitationContext(): Partial<Invitation> & Pick<Invitation, 'kind'> {
|
|
63
|
-
invariant(this._spaceKey);
|
|
64
|
-
const space = this._spaceManager.spaces.get(this._spaceKey);
|
|
65
|
-
invariant(space);
|
|
66
63
|
return {
|
|
67
64
|
kind: Invitation.Kind.SPACE,
|
|
68
65
|
spaceKey: this._spaceKey,
|
|
69
|
-
spaceId: space.id,
|
|
70
66
|
};
|
|
71
67
|
}
|
|
72
68
|
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { expect } from 'chai';
|
|
6
6
|
|
|
7
7
|
import { Trigger } from '@dxos/async';
|
|
8
8
|
import { log, LogLevel } from '@dxos/log';
|
|
9
9
|
import { type LogEntry } from '@dxos/protocols/proto/dxos/client/services';
|
|
10
|
+
import { beforeEach, describe, test } from '@dxos/test';
|
|
10
11
|
|
|
11
12
|
import { LoggingServiceImpl } from './logging-service';
|
|
12
13
|
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { expect } from 'chai';
|
|
6
6
|
|
|
7
7
|
import { Trigger } from '@dxos/async';
|
|
8
8
|
import { Context } from '@dxos/context';
|
|
9
9
|
import { type NetworkService, ConnectionState } from '@dxos/protocols/proto/dxos/client/services';
|
|
10
|
+
import { afterEach, afterTest, beforeEach, describe, test } from '@dxos/test';
|
|
10
11
|
|
|
11
12
|
import { NetworkServiceImpl } from './network-service';
|
|
12
13
|
import { type ServiceContext } from '../services';
|
|
@@ -40,7 +41,7 @@ describe('NetworkService', () => {
|
|
|
40
41
|
query.subscribe(({ swarm }) => {
|
|
41
42
|
result.wake(swarm);
|
|
42
43
|
});
|
|
43
|
-
|
|
44
|
+
afterTest(() => query.close());
|
|
44
45
|
expect(await result.wait()).to.equal(ConnectionState.ONLINE);
|
|
45
46
|
|
|
46
47
|
result = new Trigger<ConnectionState | undefined>();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import { asyncTimeout } from '@dxos/async';
|
|
8
|
+
import { AutomergeContext } from '@dxos/echo-db';
|
|
9
|
+
import { AutomergeHost, DataServiceImpl } from '@dxos/echo-pipeline';
|
|
10
|
+
import { IndexMetadataStore } from '@dxos/indexing';
|
|
11
|
+
import { createTestLevel } from '@dxos/kv-store/testing';
|
|
12
|
+
import { afterTest, describe, openAndClose, test } from '@dxos/test';
|
|
13
|
+
|
|
14
|
+
describe('AutomergeHost', () => {
|
|
15
|
+
test('automerge context is being synced with host', async () => {
|
|
16
|
+
//
|
|
17
|
+
// Setup:
|
|
18
|
+
// client-services | <-------------> | client
|
|
19
|
+
// AutomergeHost | DataServiceImpl | AutomergeContext
|
|
20
|
+
// creates repo and document | replicates repo | finds document in repo
|
|
21
|
+
//
|
|
22
|
+
|
|
23
|
+
const level = createTestLevel();
|
|
24
|
+
await level.open();
|
|
25
|
+
afterTest(() => level.close());
|
|
26
|
+
|
|
27
|
+
const host = new AutomergeHost({
|
|
28
|
+
db: level,
|
|
29
|
+
indexMetadataStore: new IndexMetadataStore({ db: level.sublevel('index-metadata') }),
|
|
30
|
+
});
|
|
31
|
+
await host.open();
|
|
32
|
+
afterTest(() => host.close());
|
|
33
|
+
|
|
34
|
+
const dataService = new DataServiceImpl({ automergeHost: host, updateIndexes: async () => {} });
|
|
35
|
+
const client = new AutomergeContext(dataService);
|
|
36
|
+
await openAndClose(client);
|
|
37
|
+
|
|
38
|
+
// Create document in repo.
|
|
39
|
+
const handle = host.repo.create();
|
|
40
|
+
const text = 'Hello World!';
|
|
41
|
+
handle.change((doc: any) => {
|
|
42
|
+
doc.text = text;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Find document in repo.
|
|
46
|
+
const doc = client.repo.find<{ text: string }>(handle.url);
|
|
47
|
+
await asyncTimeout(doc.whenReady(), 1_000);
|
|
48
|
+
expect(doc.docSync().text).to.equal(text);
|
|
49
|
+
|
|
50
|
+
// Changes from client are replicated to host.
|
|
51
|
+
const newText = 'Goodbye World!!!';
|
|
52
|
+
doc.change((doc: any) => {
|
|
53
|
+
doc.text = newText;
|
|
54
|
+
});
|
|
55
|
+
await client.flush({ documentIds: [doc.documentId] });
|
|
56
|
+
|
|
57
|
+
await asyncTimeout(handle.whenReady(), 1_000);
|
|
58
|
+
expect(handle.docSync().text).to.equal(newText);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { describe, test } from 'vitest';
|
|
6
|
-
|
|
7
5
|
import { MemorySignalManagerContext, MemorySignalManager } from '@dxos/messaging';
|
|
8
6
|
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
9
|
-
import { openAndClose } from '@dxos/test
|
|
7
|
+
import { describe, openAndClose, test } from '@dxos/test';
|
|
10
8
|
|
|
11
9
|
import { createServiceContext, performInvitation } from '../testing';
|
|
12
10
|
|