@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.
Files changed (118) hide show
  1. package/dist/lib/browser/{chunk-UEQIHAL2.mjs → chunk-4XKSDWEO.mjs} +5218 -5905
  2. package/dist/lib/browser/chunk-4XKSDWEO.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +3 -3
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +7 -8
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-MA5EWTRH.cjs → chunk-HNOOWB6F.cjs} +5060 -5743
  9. package/dist/lib/node/chunk-HNOOWB6F.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +46 -46
  11. package/dist/lib/node/index.cjs.map +3 -3
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +13 -14
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  16. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  17. package/dist/types/src/packlets/identity/authenticator.test.d.ts +2 -0
  18. package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +1 -0
  19. package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
  20. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  21. package/dist/types/src/packlets/identity/identity-manager.d.ts +7 -19
  22. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  23. package/dist/types/src/packlets/identity/identity.d.ts +1 -8
  24. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  25. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +1 -2
  26. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
  27. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +1 -2
  28. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  29. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
  30. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  31. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  32. package/dist/types/src/packlets/services/automerge-host.test.d.ts +2 -0
  33. package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +1 -0
  34. package/dist/types/src/packlets/services/service-context.d.ts +9 -9
  35. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  36. package/dist/types/src/packlets/services/service-host.d.ts +0 -1
  37. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  38. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +3 -6
  39. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  40. package/dist/types/src/packlets/spaces/data-space.d.ts +3 -4
  41. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  42. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +0 -3
  43. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  44. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
  45. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  46. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +6 -31
  47. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  48. package/dist/types/src/packlets/spaces/spaces-service.d.ts +1 -1
  49. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  50. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  51. package/dist/types/src/packlets/testing/test-builder.d.ts +2 -1
  52. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  53. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  54. package/dist/types/src/version.d.ts +1 -1
  55. package/dist/types/src/version.d.ts.map +1 -1
  56. package/package.json +39 -43
  57. package/src/packlets/devices/devices-service.test.ts +5 -4
  58. package/src/packlets/diagnostics/diagnostics-broadcast.ts +0 -1
  59. package/src/packlets/identity/{authenticator.node.test.ts → authenticator.test.ts} +3 -2
  60. package/src/packlets/identity/authenticator.ts +2 -5
  61. package/src/packlets/identity/contacts-service.ts +1 -1
  62. package/src/packlets/identity/identity-manager.test.ts +6 -5
  63. package/src/packlets/identity/identity-manager.ts +19 -35
  64. package/src/packlets/identity/identity-service.test.ts +8 -4
  65. package/src/packlets/identity/identity.test.ts +239 -128
  66. package/src/packlets/identity/identity.ts +8 -42
  67. package/src/packlets/invitations/device-invitation-protocol.test.ts +4 -7
  68. package/src/packlets/invitations/invitation-guest-extenstion.ts +4 -8
  69. package/src/packlets/invitations/invitation-host-extension.ts +7 -8
  70. package/src/packlets/invitations/invitations-handler.test.ts +9 -16
  71. package/src/packlets/invitations/invitations-handler.ts +92 -23
  72. package/src/packlets/invitations/space-invitation-protocol.test.ts +3 -4
  73. package/src/packlets/invitations/space-invitation-protocol.ts +0 -4
  74. package/src/packlets/logging/logging.test.ts +2 -1
  75. package/src/packlets/network/network-service.test.ts +3 -2
  76. package/src/packlets/services/automerge-host.test.ts +60 -0
  77. package/src/packlets/services/service-context.test.ts +1 -3
  78. package/src/packlets/services/service-context.ts +31 -68
  79. package/src/packlets/services/service-host.test.ts +12 -8
  80. package/src/packlets/services/service-host.ts +6 -8
  81. package/src/packlets/services/service-registry.test.ts +2 -1
  82. package/src/packlets/spaces/data-space-manager.test.ts +2 -2
  83. package/src/packlets/spaces/data-space-manager.ts +5 -40
  84. package/src/packlets/spaces/data-space.ts +6 -34
  85. package/src/packlets/spaces/edge-feed-replicator.ts +22 -80
  86. package/src/packlets/spaces/epoch-migrations.ts +2 -2
  87. package/src/packlets/spaces/notarization-plugin.test.ts +7 -10
  88. package/src/packlets/spaces/notarization-plugin.ts +29 -169
  89. package/src/packlets/spaces/spaces-service.test.ts +9 -5
  90. package/src/packlets/spaces/spaces-service.ts +1 -6
  91. package/src/packlets/storage/storage.ts +1 -0
  92. package/src/packlets/system/system-service.test.ts +2 -1
  93. package/src/packlets/testing/test-builder.ts +4 -3
  94. package/src/packlets/worker/worker-runtime.ts +2 -2
  95. package/src/version.ts +1 -1
  96. package/dist/lib/browser/chunk-UEQIHAL2.mjs.map +0 -7
  97. package/dist/lib/node/chunk-MA5EWTRH.cjs.map +0 -7
  98. package/dist/lib/node-esm/chunk-AIBLDI4U.mjs +0 -8403
  99. package/dist/lib/node-esm/chunk-AIBLDI4U.mjs.map +0 -7
  100. package/dist/lib/node-esm/index.mjs +0 -416
  101. package/dist/lib/node-esm/index.mjs.map +0 -7
  102. package/dist/lib/node-esm/meta.json +0 -1
  103. package/dist/lib/node-esm/testing/index.mjs +0 -420
  104. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  105. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +0 -2
  106. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +0 -1
  107. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +0 -30
  108. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +0 -1
  109. package/dist/types/src/packlets/invitations/invitation-state.d.ts +0 -19
  110. package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +0 -1
  111. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +0 -2
  112. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +0 -1
  113. package/dist/types/src/testing/setup.d.ts +0 -3
  114. package/dist/types/src/testing/setup.d.ts.map +0 -1
  115. package/src/packlets/invitations/edge-invitation-handler.ts +0 -184
  116. package/src/packlets/invitations/invitation-state.ts +0 -111
  117. package/src/packlets/spaces/edge-feed-replicator.test.ts +0 -253
  118. package/src/testing/setup.ts +0 -11
@@ -2,20 +2,19 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { describe, expect, test, onTestFinished } from 'vitest';
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
- onTestFinished(async () => {
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
- onTestFinished(async () => {
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
- extends RpcExtension<
33
- { InvitationHostService: InvitationHostService },
34
- { InvitationHostService: InvitationHostService }
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
- extends RpcExtension<
49
- { InvitationHostService: InvitationHostService },
50
- { InvitationHostService: InvitationHostService }
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 { beforeEach, onTestFinished, describe, expect, test } from 'vitest';
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-utils';
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', { timeout: 20_000 }, async () => {
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, undefined, {
261
- teleport: { controlHeartbeatInterval: 250 }, // faster peer failure detection
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
- onTestFinished(async () => {
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
- onTestFinished(async () => {
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
- onTestFinished(async () => {
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
- export type InvitationConnectionParams = {
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 _edgeClient?: EdgeHttpClient,
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 = createGuardedInvitationState(ctx, invitation, stream);
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 = createGuardedInvitationState(ctx, invitation, stream);
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._connectionParams?.teleport),
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 { onTestFinished, describe, expect, test } from 'vitest';
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
- onTestFinished(async () => {
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 { afterEach, beforeEach, describe, expect, test } from 'vitest';
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 { afterEach, onTestFinished, beforeEach, describe, expect, test } from 'vitest';
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
- onTestFinished(() => query.close());
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-utils';
7
+ import { describe, openAndClose, test } from '@dxos/test';
10
8
 
11
9
  import { createServiceContext, performInvitation } from '../testing';
12
10