@dxos/client-services 0.3.11-main.baa8c4d → 0.3.11-main.bc6b68b

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 (56) hide show
  1. package/dist/lib/browser/{chunk-IALPJ5BW.mjs → chunk-ZVDSCJMX.mjs} +116 -75
  2. package/dist/lib/browser/chunk-ZVDSCJMX.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +42 -33
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +4 -11
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-IOC4Z4I4.cjs → chunk-P2T7SCYC.cjs} +173 -132
  9. package/dist/lib/node/chunk-P2T7SCYC.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +77 -68
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/packlets/testing/index.cjs +11 -18
  14. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  15. package/dist/types/src/packlets/identity/identity-service.d.ts +3 -3
  16. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  17. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +2 -0
  18. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  19. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +28 -3
  20. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  21. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  22. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +2 -0
  23. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  24. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  25. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  26. package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts +2 -0
  27. package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts.map +1 -1
  28. package/dist/types/src/packlets/vault/index.d.ts +1 -1
  29. package/dist/types/src/packlets/vault/index.d.ts.map +1 -1
  30. package/dist/types/src/packlets/vault/{iframe-proxy-runtime.d.ts → shared-worker-connection.d.ts} +12 -5
  31. package/dist/types/src/packlets/vault/shared-worker-connection.d.ts.map +1 -0
  32. package/dist/types/src/packlets/vault/worker-runtime.d.ts +10 -3
  33. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  34. package/dist/types/src/packlets/vault/worker-session.d.ts +2 -2
  35. package/dist/types/src/packlets/vault/worker-session.d.ts.map +1 -1
  36. package/dist/types/src/version.d.ts +1 -1
  37. package/package.json +35 -35
  38. package/src/packlets/identity/identity-service.test.ts +1 -1
  39. package/src/packlets/identity/identity-service.ts +6 -3
  40. package/src/packlets/invitations/device-invitation-protocol.test.ts +14 -0
  41. package/src/packlets/invitations/device-invitation-protocol.ts +14 -0
  42. package/src/packlets/invitations/invitation-protocol.ts +44 -6
  43. package/src/packlets/invitations/invitations-handler.ts +20 -18
  44. package/src/packlets/invitations/space-invitation-protocol.test.ts +28 -0
  45. package/src/packlets/invitations/space-invitation-protocol.ts +11 -0
  46. package/src/packlets/services/service-host.ts +27 -12
  47. package/src/packlets/testing/invitation-utils.ts +2 -10
  48. package/src/packlets/vault/iframe-host-runtime.ts +2 -0
  49. package/src/packlets/vault/index.ts +1 -1
  50. package/src/packlets/vault/{iframe-proxy-runtime.ts → shared-worker-connection.ts} +18 -6
  51. package/src/packlets/vault/worker-runtime.ts +27 -9
  52. package/src/packlets/vault/worker-session.ts +11 -8
  53. package/src/version.ts +1 -1
  54. package/dist/lib/browser/chunk-IALPJ5BW.mjs.map +0 -7
  55. package/dist/lib/node/chunk-IOC4Z4I4.cjs.map +0 -7
  56. package/dist/types/src/packlets/vault/iframe-proxy-runtime.d.ts.map +0 -1
@@ -6,6 +6,7 @@ import { expect } from 'chai';
6
6
 
7
7
  import { asyncChain } from '@dxos/async';
8
8
  import { Context } from '@dxos/context';
9
+ import { AlreadyJoinedError } from '@dxos/protocols';
9
10
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
10
11
  import { describe, test, afterTest } from '@dxos/test';
11
12
 
@@ -36,4 +37,17 @@ describe('services/device', () => {
36
37
  await Promise.all(performInvitation({ host, guest, options: { kind: Invitation.Kind.DEVICE } }));
37
38
  expect(guest.identityManager.identity?.identityKey).to.deep.eq(identity1.identityKey);
38
39
  });
40
+
41
+ test('invitation when already joined', async () => {
42
+ const [host, guest] = await asyncChain<ServiceContext>([closeAfterTest])(createPeers(2));
43
+
44
+ const identity1 = await host.createIdentity();
45
+ expect(host.identityManager.identity).to.eq(identity1);
46
+
47
+ await Promise.all(performInvitation({ host, guest, options: { kind: Invitation.Kind.DEVICE } }));
48
+ expect(guest.identityManager.identity?.identityKey).to.deep.eq(identity1.identityKey);
49
+
50
+ const [_, result] = performInvitation({ host, guest, options: { kind: Invitation.Kind.DEVICE } });
51
+ expect((await result).error).to.be.instanceOf(AlreadyJoinedError);
52
+ });
39
53
  });
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { invariant } from '@dxos/invariant';
6
6
  import { type Keyring } from '@dxos/keyring';
7
+ import { AlreadyJoinedError } from '@dxos/protocols';
7
8
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
8
9
  import {
9
10
  type AdmissionRequest,
@@ -46,6 +47,17 @@ export class DeviceInvitationProtocol implements InvitationProtocol {
46
47
  };
47
48
  }
48
49
 
50
+ checkInvitation(invitation: Partial<Invitation>) {
51
+ try {
52
+ const identity = this._getIdentity();
53
+ if (identity) {
54
+ return new AlreadyJoinedError('Currently only one identity per client is supported.');
55
+ }
56
+ } catch {
57
+ // No identity.
58
+ }
59
+ }
60
+
49
61
  createIntroduction(): IntroductionRequest {
50
62
  return {};
51
63
  }
@@ -71,6 +83,8 @@ export class DeviceInvitationProtocol implements InvitationProtocol {
71
83
  invariant(request.device);
72
84
  const { deviceKey, controlFeedKey, dataFeedKey } = request.device;
73
85
 
86
+ // TODO(wittjosiah): When multiple identities are supported, verify identity doesn't already exist before accepting.
87
+
74
88
  await this._acceptIdentity({
75
89
  identityKey,
76
90
  deviceKey,
@@ -2,24 +2,62 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Invitation } from '@dxos/protocols/proto/dxos/client/services';
6
- import { type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
7
- import {
8
- type AdmissionRequest,
9
- type AdmissionResponse,
10
- type IntroductionRequest,
5
+ import type { ApiError } from '@dxos/protocols';
6
+ import type { Invitation } from '@dxos/protocols/proto/dxos/client/services';
7
+ import type { ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
8
+ import type {
9
+ AdmissionRequest,
10
+ AdmissionResponse,
11
+ IntroductionRequest,
11
12
  } from '@dxos/protocols/proto/dxos/halo/invitations';
12
13
 
13
14
  export interface InvitationProtocol {
15
+ //
14
16
  // Debugging
17
+ //
18
+
19
+ /**
20
+ * Protocol-specific debug info to include in logs.
21
+ */
15
22
  toJSON(): object;
16
23
 
24
+ //
17
25
  // Host
26
+ //
27
+
28
+ /**
29
+ * Protocol-specific information to include in the invitation.
30
+ */
18
31
  getInvitationContext(): Partial<Invitation> & Pick<Invitation, 'kind'>;
32
+
33
+ /**
34
+ * Once authentication is successful, the host can admit the guest to the requested resource.
35
+ */
19
36
  admit(request: AdmissionRequest, guestProfile?: ProfileDocument): Promise<AdmissionResponse>;
20
37
 
38
+ //
21
39
  // Guest
40
+ //
41
+
42
+ /**
43
+ * Check if the invitation is valid.
44
+ *
45
+ * For example, the guest may already be a member of the space.
46
+ */
47
+ checkInvitation(invitation: Partial<Invitation>): ApiError | undefined;
48
+
49
+ /**
50
+ * Get profile information to send to the host to identify the guest.
51
+ */
22
52
  createIntroduction(): IntroductionRequest;
53
+
54
+ /**
55
+ * Get key information to send to the host in order to create an admission credential for the guest.
56
+ */
23
57
  createAdmissionRequest(): Promise<AdmissionRequest>;
58
+
59
+ /**
60
+ * Redeem the admission credential.
61
+ */
24
62
  accept(response: AdmissionResponse, request: AdmissionRequest): Promise<Partial<Invitation>>;
25
63
  }
@@ -303,9 +303,6 @@ export class InvitationsHandler {
303
303
  }
304
304
  }
305
305
  }
306
- } else {
307
- // Notify that introduction is complete even if auth is not required.
308
- setState({ state: Invitation.State.READY_FOR_AUTHENTICATION });
309
306
  }
310
307
 
311
308
  // 3. Send admission credentials to host (with local space keys).
@@ -321,7 +318,7 @@ export class InvitationsHandler {
321
318
 
322
319
  // 5. Success.
323
320
  log('admitted by host', { ...protocol.toJSON() });
324
- setState({ ...result, state: Invitation.State.SUCCESS });
321
+ setState({ ...result, target: invitation.target, state: Invitation.State.SUCCESS });
325
322
  log.trace('dxos.sdk.invitations-handler.guest.onOpen', trace.end({ id: traceId }));
326
323
  } catch (err: any) {
327
324
  if (err instanceof TimeoutError) {
@@ -355,20 +352,25 @@ export class InvitationsHandler {
355
352
  };
356
353
 
357
354
  scheduleTask(ctx, async () => {
358
- invariant(invitation.swarmKey);
359
- const topic = invitation.swarmKey;
360
- const swarmConnection = await this._networkManager.joinSwarm({
361
- topic,
362
- peerId: PublicKey.random(),
363
- protocolProvider: createTeleportProtocolFactory(async (teleport) => {
364
- teleport.addExtension('dxos.halo.invitations', createExtension());
365
- }),
366
- topology: new StarTopology(topic),
367
- label: 'invitation guest',
368
- });
369
- ctx.onDispose(() => swarmConnection.close());
370
-
371
- setState({ state: Invitation.State.CONNECTING });
355
+ const error = protocol.checkInvitation(invitation);
356
+ if (error) {
357
+ stream.error(error);
358
+ } else {
359
+ invariant(invitation.swarmKey);
360
+ const topic = invitation.swarmKey;
361
+ const swarmConnection = await this._networkManager.joinSwarm({
362
+ topic,
363
+ peerId: PublicKey.random(),
364
+ protocolProvider: createTeleportProtocolFactory(async (teleport) => {
365
+ teleport.addExtension('dxos.halo.invitations', createExtension());
366
+ }),
367
+ topology: new StarTopology(topic),
368
+ label: 'invitation guest',
369
+ });
370
+ ctx.onDispose(() => swarmConnection.close());
371
+
372
+ setState({ state: Invitation.State.CONNECTING });
373
+ }
372
374
  });
373
375
 
374
376
  const observable = new AuthenticatingInvitation({
@@ -7,6 +7,7 @@ import { expect } from 'chai';
7
7
  import { asyncChain, Trigger } from '@dxos/async';
8
8
  import { raise } from '@dxos/debug';
9
9
  import { testLocalDatabase } from '@dxos/echo-pipeline/testing';
10
+ import { AlreadyJoinedError } from '@dxos/protocols';
10
11
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
11
12
  import { afterTest, describe, test } from '@dxos/test';
12
13
 
@@ -71,6 +72,33 @@ describe('services/space-invitations-protocol', () => {
71
72
  }
72
73
  });
73
74
 
75
+ test('invitation when already joined', async () => {
76
+ const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
77
+
78
+ const space1 = await host.dataSpaceManager!.createSpace();
79
+ const spaceKey = space1.key;
80
+
81
+ await Promise.all(performInvitation({ host, guest, options: { kind: Invitation.Kind.SPACE, spaceKey } }));
82
+
83
+ {
84
+ const space1 = host.dataSpaceManager!.spaces.get(spaceKey)!;
85
+ const space2 = guest.dataSpaceManager!.spaces.get(spaceKey)!;
86
+ expect(space1).not.to.be.undefined;
87
+ expect(space2).not.to.be.undefined;
88
+
89
+ await host.dataSpaceManager?.waitUntilSpaceReady(space1.key);
90
+ await guest.dataSpaceManager?.waitUntilSpaceReady(space2.key);
91
+ }
92
+
93
+ const [_, guestResult] = performInvitation({
94
+ host,
95
+ guest,
96
+ options: { kind: Invitation.Kind.SPACE, spaceKey },
97
+ });
98
+
99
+ expect((await guestResult).error).to.be.instanceOf(AlreadyJoinedError);
100
+ });
101
+
74
102
  test('creates and accepts invitation with retry', async () => {
75
103
  const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
76
104
 
@@ -8,6 +8,7 @@ import { invariant } from '@dxos/invariant';
8
8
  import { type Keyring } from '@dxos/keyring';
9
9
  import { type PublicKey } from '@dxos/keys';
10
10
  import { log } from '@dxos/log';
11
+ import { AlreadyJoinedError } from '@dxos/protocols';
11
12
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
12
13
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
13
14
  import { type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
@@ -76,6 +77,12 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
76
77
  };
77
78
  }
78
79
 
80
+ checkInvitation(invitation: Partial<Invitation>) {
81
+ if (invitation.spaceKey && this._spaceManager.spaces.has(invitation.spaceKey)) {
82
+ return new AlreadyJoinedError('Already joined space.');
83
+ }
84
+ }
85
+
79
86
  createIntroduction(): IntroductionRequest {
80
87
  return {
81
88
  profile: this._signingContext.getProfile(),
@@ -104,6 +111,10 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
104
111
  invariant(assertion['@type'] === 'dxos.halo.credentials.SpaceMember', 'Invalid credential');
105
112
  invariant(credential.subject.id.equals(this._signingContext.identityKey));
106
113
 
114
+ if (this._spaceManager.spaces.has(assertion.spaceKey)) {
115
+ throw new AlreadyJoinedError('Already joined space.');
116
+ }
117
+
107
118
  // Create local space.
108
119
  await this._spaceManager.acceptSpace({
109
120
  spaceKey: assertion.spaceKey,
@@ -8,7 +8,7 @@ import { type Config } from '@dxos/config';
8
8
  import { Context } from '@dxos/context';
9
9
  import { DocumentModel } from '@dxos/document-model';
10
10
  import { DataServiceImpl } from '@dxos/echo-pipeline';
11
- import { type TypedObject, base } from '@dxos/echo-schema';
11
+ import { type TypedObject, base, getRawDoc } from '@dxos/echo-schema';
12
12
  import { invariant } from '@dxos/invariant';
13
13
  import { PublicKey } from '@dxos/keys';
14
14
  import { log } from '@dxos/log';
@@ -28,7 +28,7 @@ import { ServiceContext } from './service-context';
28
28
  import { ServiceRegistry } from './service-registry';
29
29
  import { DevicesServiceImpl } from '../devices';
30
30
  import { DevtoolsServiceImpl, DevtoolsHostEvents } from '../devtools';
31
- import { type CreateIdentityOptions, IdentityServiceImpl } from '../identity';
31
+ import { IdentityServiceImpl, type CreateIdentityOptions } from '../identity';
32
32
  import { InvitationsServiceImpl } from '../invitations';
33
33
  import { Lock, type ResourceLock } from '../locks';
34
34
  import { LoggingServiceImpl } from '../logging';
@@ -260,7 +260,7 @@ export class ClientServicesHost {
260
260
  SystemService: this._systemService,
261
261
 
262
262
  IdentityService: new IdentityServiceImpl(
263
- (params) => this._createIdentity(params),
263
+ (params, useAutomerge) => this._createIdentity(params, useAutomerge),
264
264
  this._serviceContext.identityManager,
265
265
  this._serviceContext.keyring,
266
266
  (profile) => this._serviceContext.broadcastProfileUpdate(profile),
@@ -351,21 +351,36 @@ export class ClientServicesHost {
351
351
  await this._callbacks?.onReset?.();
352
352
  }
353
353
 
354
- private async _createIdentity(params?: CreateIdentityOptions) {
354
+ private async _createIdentity(params: CreateIdentityOptions, useAutomerge: boolean) {
355
355
  const identity = await this._serviceContext.createIdentity(params);
356
356
 
357
357
  // Setup default space.
358
358
  await this._serviceContext.initialized.wait();
359
359
  const space = await this._serviceContext.dataSpaceManager!.createSpace();
360
- const obj: TypedObject = new Properties(undefined, { automerge: false });
360
+
361
+ const obj: TypedObject = new Properties(undefined, { automerge: useAutomerge });
361
362
  obj[defaultKey] = identity.identityKey.toHex();
362
- await this._serviceRegistry.services.DataService!.write({
363
- spaceKey: space.key,
364
- batch: {
365
- objects: [createGenesisMutationFromTypedObject(obj)],
366
- },
367
- });
368
- await this._serviceRegistry.services.DataService!.flush({ spaceKey: space.key });
363
+
364
+ if (!useAutomerge) {
365
+ await this._serviceRegistry.services.DataService!.write({
366
+ spaceKey: space.key,
367
+ batch: {
368
+ objects: [createGenesisMutationFromTypedObject(obj)],
369
+ },
370
+ });
371
+ await this._serviceRegistry.services.DataService!.flush({ spaceKey: space.key });
372
+ } else {
373
+ // TODO(dmaretskyi): Refactor this.
374
+ const automergeIndex = space.automergeSpaceState.rootUrl;
375
+ invariant(automergeIndex);
376
+ const document = await this._serviceContext.automergeHost.repo.find(automergeIndex as any);
377
+ await document.whenReady();
378
+
379
+ document.change((doc: any) => {
380
+ doc.objects ??= {};
381
+ doc.objects[obj[base]._id] = getRawDoc(obj).handle.docSync();
382
+ });
383
+ }
369
384
 
370
385
  return identity;
371
386
  }
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Trigger } from '@dxos/async';
6
- import { type AuthenticatingInvitation, type CancellableInvitation } from '@dxos/client-protocol';
6
+ import { InvitationEncoder, type AuthenticatingInvitation, type CancellableInvitation } from '@dxos/client-protocol';
7
7
  import { invariant } from '@dxos/invariant';
8
8
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
9
9
 
@@ -13,15 +13,7 @@ import { ServiceContext } from '../services';
13
13
  * Strip secrets from invitation before giving it to the peer.
14
14
  */
15
15
  export const sanitizeInvitation = (invitation: Invitation): Invitation => {
16
- return {
17
- invitationId: invitation.invitationId,
18
- type: invitation.type,
19
- kind: invitation.kind,
20
- authMethod: invitation.authMethod,
21
- swarmKey: invitation.swarmKey,
22
- state: invitation.state,
23
- timeout: invitation.timeout,
24
- };
16
+ return InvitationEncoder.decode(InvitationEncoder.encode(invitation));
25
17
  };
26
18
 
27
19
  export type InvitationHost = {
@@ -30,6 +30,8 @@ export type IFrameHostRuntimeParams = {
30
30
  *
31
31
  * Holds a lock over the client services such that only one instance can run at a time.
32
32
  * This should only be used when SharedWorker is not available.
33
+ *
34
+ * @deprecated
33
35
  */
34
36
  export class IFrameHostRuntime {
35
37
  private readonly _configProvider: IFrameHostRuntimeParams['config'];
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './iframe-host-runtime';
6
- export * from './iframe-proxy-runtime';
6
+ export * from './shared-worker-connection';
7
7
  export * from './shell-runtime';
8
8
  export * from './worker-runtime';
9
9
  export * from './worker-session';
@@ -20,18 +20,21 @@ import { getAsyncValue, type MaybePromise, type Provider } from '@dxos/util';
20
20
  import { ShellRuntimeImpl } from './shell-runtime';
21
21
 
22
22
  // NOTE: Keep as RpcPorts to avoid dependency on @dxos/rpc-tunnel so we don't depend on browser-specific apis.
23
- export type IFrameProxyRuntimeParams = {
23
+ export type SharedWorkerConnectionOptions = {
24
24
  config: Config | Provider<MaybePromise<Config>>;
25
25
  systemPort: RpcPort;
26
+ /**
27
+ * @deprecated Only used with iframes.
28
+ */
26
29
  shellPort?: RpcPort;
27
30
  };
28
31
 
29
32
  /**
30
33
  * Manages the client connection to the shared worker.
31
34
  */
32
- export class IFrameProxyRuntime {
35
+ export class SharedWorkerConnection {
33
36
  private readonly _id = String(Math.floor(Math.random() * 1000000));
34
- private readonly _configProvider: IFrameProxyRuntimeParams['config'];
37
+ private readonly _configProvider: SharedWorkerConnectionOptions['config'];
35
38
  private readonly _systemPort: RpcPort;
36
39
  private readonly _shellPort?: RpcPort;
37
40
  private _release = new Trigger();
@@ -40,7 +43,7 @@ export class IFrameProxyRuntime {
40
43
  private _systemRpc!: ProtoRpcPeer<WorkerServiceBundle>;
41
44
  private _shellRuntime?: ShellRuntimeImpl;
42
45
 
43
- constructor({ config, systemPort, shellPort }: IFrameProxyRuntimeParams) {
46
+ constructor({ config, systemPort, shellPort }: SharedWorkerConnectionOptions) {
44
47
  this._configProvider = config;
45
48
  this._systemPort = systemPort;
46
49
  this._shellPort = shellPort;
@@ -54,7 +57,12 @@ export class IFrameProxyRuntime {
54
57
  return this._shellRuntime;
55
58
  }
56
59
 
57
- async open(origin: string) {
60
+ async open(
61
+ /**
62
+ * @deprecated Only used with iframes.
63
+ */
64
+ origin: string,
65
+ ) {
58
66
  this._config = await getAsyncValue(this._configProvider);
59
67
 
60
68
  this._transportService = new SimplePeerTransportService({
@@ -96,7 +104,11 @@ export class IFrameProxyRuntime {
96
104
  async close() {
97
105
  this._release.wake();
98
106
  await this._shellRuntime?.close();
99
- await this._systemRpc.rpc.WorkerService.stop();
107
+ try {
108
+ await this._systemRpc.rpc.WorkerService.stop();
109
+ } catch {
110
+ // If this fails, the worker is probably already gone.
111
+ }
100
112
  await this._systemRpc.close();
101
113
  }
102
114
 
@@ -18,7 +18,13 @@ import { ClientServicesHost } from '../services';
18
18
  export type CreateSessionParams = {
19
19
  appPort: RpcPort;
20
20
  systemPort: RpcPort;
21
- shellPort: RpcPort;
21
+ shellPort?: RpcPort;
22
+ };
23
+
24
+ export type WorkerRuntimeCallbacks = {
25
+ acquireLock: () => Promise<void>;
26
+ releaseLock: () => void;
27
+ onReset: () => Promise<void>;
22
28
  };
23
29
 
24
30
  /**
@@ -27,6 +33,8 @@ export type CreateSessionParams = {
27
33
  * Tabs make requests to the `ClientServicesHost`, and provide a WebRTC gateway.
28
34
  */
29
35
  export class WorkerRuntime {
36
+ private readonly _acquireLock: () => Promise<void>;
37
+ private readonly _releaseLock: () => void;
30
38
  private readonly _transportFactory = new SimplePeerTransportProxyFactory();
31
39
  private readonly _ready = new Trigger<Error | undefined>();
32
40
  private readonly _sessions = new Set<WorkerSession>();
@@ -34,12 +42,15 @@ export class WorkerRuntime {
34
42
  private _sessionForNetworking?: WorkerSession; // TODO(burdon): Expose to client QueryStatusResponse.
35
43
  private _config!: Config;
36
44
 
37
- constructor(private readonly _configProvider: () => MaybePromise<Config>) {
45
+ constructor(
46
+ private readonly _configProvider: () => MaybePromise<Config>,
47
+ { acquireLock, releaseLock, onReset }: WorkerRuntimeCallbacks,
48
+ ) {
49
+ this._acquireLock = acquireLock;
50
+ this._releaseLock = releaseLock;
38
51
  this._clientServices = new ClientServicesHost({
39
52
  callbacks: {
40
- onReset: async () => {
41
- self.close();
42
- },
53
+ onReset: async () => onReset(),
43
54
  },
44
55
  });
45
56
  }
@@ -51,6 +62,7 @@ export class WorkerRuntime {
51
62
  async start() {
52
63
  log('starting...');
53
64
  try {
65
+ await this._acquireLock();
54
66
  this._config = await this._configProvider();
55
67
  const signals = this._config.get('runtime.services.signaling');
56
68
  this._clientServices.initialize({
@@ -71,7 +83,8 @@ export class WorkerRuntime {
71
83
  }
72
84
 
73
85
  async stop() {
74
- // TODO(dmaretskyi): Terminate active sessions.
86
+ // Release the lock to notify remote clients that the worker is terminating.
87
+ this._releaseLock();
75
88
  await this._clientServices.close();
76
89
  }
77
90
 
@@ -87,10 +100,15 @@ export class WorkerRuntime {
87
100
  readySignal: this._ready,
88
101
  });
89
102
 
90
- // When tab is closed.
103
+ // When tab is closed or client is destroyed.
91
104
  session.onClose.set(async () => {
92
105
  this._sessions.delete(session);
93
- this._reconnectWebrtc();
106
+ if (this._sessions.size === 0) {
107
+ // Terminate the worker when all sessions are closed.
108
+ self.close();
109
+ } else {
110
+ this._reconnectWebrtc();
111
+ }
94
112
  });
95
113
 
96
114
  await session.open();
@@ -100,7 +118,7 @@ export class WorkerRuntime {
100
118
  }
101
119
 
102
120
  /**
103
- * Selects one of the existing session fro WebRTC networking.
121
+ * Selects one of the existing session for WebRTC networking.
104
122
  */
105
123
  private _reconnectWebrtc() {
106
124
  log('reconnecting webrtc...');
@@ -21,7 +21,8 @@ export type WorkerSessionParams = {
21
21
  serviceHost: ClientServicesHost;
22
22
  systemPort: RpcPort;
23
23
  appPort: RpcPort;
24
- shellPort: RpcPort;
24
+ // TODO(wittjosiah): Remove shellPort.
25
+ shellPort?: RpcPort;
25
26
  readySignal: Trigger<Error | undefined>;
26
27
  };
27
28
 
@@ -30,7 +31,7 @@ export type WorkerSessionParams = {
30
31
  */
31
32
  export class WorkerSession {
32
33
  private readonly _clientRpc: ClientRpcServer;
33
- private readonly _shellClientRpc: ClientRpcServer;
34
+ private readonly _shellClientRpc?: ClientRpcServer;
34
35
  private readonly _iframeRpc: ProtoRpcPeer<IframeServiceBundle>;
35
36
  private readonly _startTrigger = new Trigger();
36
37
  private readonly _serviceHost: ClientServicesHost;
@@ -74,11 +75,13 @@ export class WorkerSession {
74
75
  ...middleware,
75
76
  });
76
77
 
77
- this._shellClientRpc = new ClientRpcServer({
78
- serviceRegistry: this._serviceHost.serviceRegistry,
79
- port: shellPort,
80
- ...middleware,
81
- });
78
+ this._shellClientRpc = shellPort
79
+ ? new ClientRpcServer({
80
+ serviceRegistry: this._serviceHost.serviceRegistry,
81
+ port: shellPort,
82
+ ...middleware,
83
+ })
84
+ : undefined;
82
85
 
83
86
  this._iframeRpc = createProtoRpcPeer({
84
87
  requested: iframeServiceBundle,
@@ -138,7 +141,7 @@ export class WorkerSession {
138
141
 
139
142
  private async _maybeOpenShell() {
140
143
  try {
141
- await asyncTimeout(this._shellClientRpc.open(), 1_000);
144
+ this._shellClientRpc && (await asyncTimeout(this._shellClientRpc.open(), 1_000));
142
145
  } catch {
143
146
  log.info('No shell connected.');
144
147
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const DXOS_VERSION = "0.3.11-main.baa8c4d";
1
+ export const DXOS_VERSION = "0.3.11-main.bc6b68b";