@dxos/client-services 0.1.39 → 0.1.40

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 (29) hide show
  1. package/dist/lib/browser/{chunk-2DGNNQWS.mjs → chunk-LGMYP2WL.mjs} +679 -569
  2. package/dist/lib/browser/chunk-LGMYP2WL.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1 -1
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/packlets/testing/index.mjs +14 -14
  6. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  7. package/dist/lib/node/index.cjs +721 -611
  8. package/dist/lib/node/index.cjs.map +4 -4
  9. package/dist/lib/node/meta.json +1 -1
  10. package/dist/lib/node/packlets/testing/index.cjs +725 -621
  11. package/dist/lib/node/packlets/testing/index.cjs.map +4 -4
  12. package/dist/types/src/packlets/invitations/invitation-extension.d.ts +54 -0
  13. package/dist/types/src/packlets/invitations/invitation-extension.d.ts.map +1 -0
  14. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +1 -1
  15. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  16. package/dist/types/src/packlets/invitations/invitations-service.d.ts +8 -3
  17. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  18. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  19. package/dist/types/src/packlets/vault/iframe-proxy-runtime.d.ts.map +1 -1
  20. package/package.json +30 -30
  21. package/src/packlets/invitations/invitation-extension.ts +211 -0
  22. package/src/packlets/invitations/invitations-handler.ts +54 -123
  23. package/src/packlets/invitations/invitations-service.ts +106 -137
  24. package/src/packlets/services/utils.ts +3 -3
  25. package/src/packlets/testing/test-builder.ts +9 -7
  26. package/src/packlets/vault/iframe-host-runtime.ts +3 -3
  27. package/src/packlets/vault/iframe-proxy-runtime.ts +11 -3
  28. package/src/packlets/vault/worker-runtime.ts +3 -3
  29. package/dist/lib/browser/chunk-2DGNNQWS.mjs.map +0 -7
@@ -6,31 +6,24 @@ import assert from 'node:assert';
6
6
 
7
7
  import { PushStream, scheduleTask, sleep, TimeoutError, Trigger } from '@dxos/async';
8
8
  import {
9
+ AuthenticatingInvitationObservable,
9
10
  AUTHENTICATION_CODE_LENGTH,
10
11
  CancellableInvitationObservable,
11
12
  INVITATION_TIMEOUT,
12
- ON_CLOSE_DELAY,
13
- AuthenticatingInvitationObservable
13
+ ON_CLOSE_DELAY
14
14
  } from '@dxos/client';
15
15
  import { Context } from '@dxos/context';
16
16
  import { generatePasscode } from '@dxos/credentials';
17
+ import { InvalidInvitationExtensionRoleError } from '@dxos/errors';
17
18
  import { PublicKey } from '@dxos/keys';
18
19
  import { log } from '@dxos/log';
19
20
  import { createTeleportProtocolFactory, NetworkManager, StarTopology } from '@dxos/network-manager';
20
- import { schema, trace } from '@dxos/protocols';
21
+ import { trace } from '@dxos/protocols';
21
22
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
22
23
  import { ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
23
- import {
24
- AuthenticationRequest,
25
- AuthenticationResponse,
26
- IntroductionRequest,
27
- IntroductionResponse,
28
- AdmissionRequest,
29
- AdmissionResponse,
30
- InvitationHostService
31
- } from '@dxos/protocols/proto/dxos/halo/invitations';
32
- import { ExtensionContext, RpcExtension } from '@dxos/teleport';
24
+ import { AuthenticationResponse } from '@dxos/protocols/proto/dxos/halo/invitations';
33
25
 
26
+ import { InvitationGuestExtension, InvitationHostExtension } from './invitation-extension';
34
27
  import { InvitationProtocol } from './invitation-protocol';
35
28
 
36
29
  const MAX_OTP_ATTEMPTS = 3;
@@ -219,6 +212,18 @@ export class InvitationsHandler {
219
212
  }
220
213
  }
221
214
  });
215
+ },
216
+ onError: (err) => {
217
+ if (err instanceof InvalidInvitationExtensionRoleError) {
218
+ return;
219
+ }
220
+ if (err instanceof TimeoutError) {
221
+ log('timeout', { ...protocol.toJSON() });
222
+ stream.next({ ...invitation, state: Invitation.State.TIMEOUT });
223
+ } else {
224
+ log.error('failed', err);
225
+ stream.error(err);
226
+ }
222
227
  }
223
228
  });
224
229
  extension._traceParent = this._traceParent;
@@ -260,12 +265,19 @@ export class InvitationsHandler {
260
265
 
261
266
  const authenticated = new Trigger<string>();
262
267
 
268
+ let currentState: Invitation.State;
263
269
  const stream = new PushStream<Invitation>();
270
+ const setState = (newData: Partial<Invitation>) => {
271
+ assert(newData.state !== undefined);
272
+ currentState = newData.state;
273
+ stream.next({ ...invitation, ...newData });
274
+ };
275
+
264
276
  const ctx = new Context({
265
277
  onError: (err) => {
266
278
  if (err instanceof TimeoutError) {
267
279
  log('timeout', { ...protocol.toJSON() });
268
- stream.next({ ...invitation, state: Invitation.State.TIMEOUT });
280
+ setState({ state: Invitation.State.TIMEOUT });
269
281
  } else {
270
282
  log.warn('auth failed', err);
271
283
  stream.error(err);
@@ -283,7 +295,14 @@ export class InvitationsHandler {
283
295
  let connectionCount = 0;
284
296
 
285
297
  const extension = new InvitationGuestExtension({
286
- onOpen: () => {
298
+ onOpen: (extensionCtx) => {
299
+ extensionCtx.onDispose(async () => {
300
+ log('extension disposed', { currentState });
301
+ if (currentState !== Invitation.State.SUCCESS) {
302
+ stream.error(new Error('Remote peer disconnected.'));
303
+ }
304
+ });
305
+
287
306
  scheduleTask(ctx, async () => {
288
307
  const traceId = PublicKey.random().toHex();
289
308
  try {
@@ -299,7 +318,7 @@ export class InvitationsHandler {
299
318
  scheduleTask(ctx, () => ctx.raise(new TimeoutError(timeout)), timeout);
300
319
 
301
320
  log('connected', { ...protocol.toJSON() });
302
- stream.next({ ...invitation, state: Invitation.State.CONNECTED });
321
+ setState({ state: Invitation.State.CONNECTED });
303
322
 
304
323
  // 1. Introduce guest to host.
305
324
  log('introduce', { ...protocol.toJSON() });
@@ -316,11 +335,11 @@ export class InvitationsHandler {
316
335
  if (isAuthenticationRequired(invitation)) {
317
336
  for (let attempt = 1; attempt <= MAX_OTP_ATTEMPTS; attempt++) {
318
337
  log('guest waiting for authentication code...');
319
- stream.next({ ...invitation, state: Invitation.State.READY_FOR_AUTHENTICATION });
338
+ setState({ state: Invitation.State.READY_FOR_AUTHENTICATION });
320
339
  const authCode = await authenticated.wait({ timeout });
321
340
 
322
341
  log('sending authentication request');
323
- stream.next({ ...invitation, state: Invitation.State.AUTHENTICATING });
342
+ setState({ state: Invitation.State.AUTHENTICATING });
324
343
  const response = await extension.rpc.InvitationHostService.authenticate({ authCode });
325
344
  if (response.status === undefined || response.status === AuthenticationResponse.Status.OK) {
326
345
  break;
@@ -337,7 +356,7 @@ export class InvitationsHandler {
337
356
  }
338
357
  } else {
339
358
  // Notify that introduction is complete even if auth is not required.
340
- stream.next({ ...invitation, state: Invitation.State.READY_FOR_AUTHENTICATION });
359
+ setState({ state: Invitation.State.READY_FOR_AUTHENTICATION });
341
360
  }
342
361
 
343
362
  // 3. Send admission credentials to host (with local space keys).
@@ -350,12 +369,12 @@ export class InvitationsHandler {
350
369
 
351
370
  // 5. Success.
352
371
  log('admitted by host', { ...protocol.toJSON() });
353
- stream.next({ ...invitation, ...result, state: Invitation.State.SUCCESS });
372
+ setState({ ...result, state: Invitation.State.SUCCESS });
354
373
  log.trace('dxos.sdk.invitations-handler.guest.onOpen', trace.end({ id: traceId }));
355
374
  } catch (err: any) {
356
375
  if (err instanceof TimeoutError) {
357
376
  log('timeout', { ...protocol.toJSON() });
358
- stream.next({ ...invitation, state: Invitation.State.TIMEOUT });
377
+ setState({ state: Invitation.State.TIMEOUT });
359
378
  } else {
360
379
  log('auth failed', err);
361
380
  stream.error(err);
@@ -365,6 +384,18 @@ export class InvitationsHandler {
365
384
  await ctx.dispose();
366
385
  }
367
386
  });
387
+ },
388
+ onError: (err) => {
389
+ if (err instanceof InvalidInvitationExtensionRoleError) {
390
+ return;
391
+ }
392
+ if (err instanceof TimeoutError) {
393
+ log('timeout', { ...protocol.toJSON() });
394
+ setState({ state: Invitation.State.TIMEOUT });
395
+ } else {
396
+ log('auth failed', err);
397
+ stream.error(err);
398
+ }
368
399
  }
369
400
  });
370
401
 
@@ -384,14 +415,14 @@ export class InvitationsHandler {
384
415
  });
385
416
  ctx.onDispose(() => swarmConnection.close());
386
417
 
387
- stream.next({ ...invitation, state: Invitation.State.CONNECTING });
418
+ setState({ state: Invitation.State.CONNECTING });
388
419
  });
389
420
 
390
421
  const observable = new AuthenticatingInvitationObservable({
391
422
  initialInvitation: invitation,
392
423
  subscriber: stream.observable,
393
424
  onCancel: async () => {
394
- stream.next({ ...invitation, state: Invitation.State.CANCELLED });
425
+ setState({ state: Invitation.State.CANCELLED });
395
426
  await ctx.dispose();
396
427
  },
397
428
  onAuthenticate: async (code: string) => {
@@ -405,103 +436,3 @@ export class InvitationsHandler {
405
436
  }
406
437
 
407
438
  const isAuthenticationRequired = (invitation: Invitation) => invitation.authMethod !== Invitation.AuthMethod.NONE;
408
-
409
- type InvitationHostExtensionCallbacks = {
410
- // Deliberately not async to not block the extensions opening.
411
- onOpen: () => void;
412
-
413
- introduce: (request: IntroductionRequest) => Promise<IntroductionResponse>;
414
- authenticate: (request: AuthenticationRequest) => Promise<AuthenticationResponse>;
415
- admit: (request: AdmissionRequest) => Promise<AdmissionResponse>;
416
- };
417
-
418
- /**
419
- * Host's side for a connection to a concrete peer in p2p network during invitation.
420
- */
421
- class InvitationHostExtension extends RpcExtension<{}, { InvitationHostService: InvitationHostService }> {
422
- /**
423
- * @internal
424
- */
425
- public _traceParent?: string;
426
-
427
- constructor(private readonly _callbacks: InvitationHostExtensionCallbacks) {
428
- super({
429
- exposed: {
430
- InvitationHostService: schema.getService('dxos.halo.invitations.InvitationHostService')
431
- }
432
- });
433
- }
434
-
435
- protected override async getHandlers(): Promise<{ InvitationHostService: InvitationHostService }> {
436
- return {
437
- // TODO(dmaretskyi): For now this is just forwarding the data to callbacks since we don't have session-specific logic.
438
- // Perhaps in the future we will have more complex logic here.
439
- InvitationHostService: {
440
- introduce: async (request) => {
441
- const traceId = PublicKey.random().toHex();
442
- log.trace(
443
- 'dxos.sdk.invitation-handler.host.introduce',
444
- trace.begin({ id: traceId, parentId: this._traceParent })
445
- );
446
- const response = await this._callbacks.introduce(request);
447
- log.trace('dxos.sdk.invitation-handler.host.introduce', trace.end({ id: traceId }));
448
- return response;
449
- },
450
-
451
- authenticate: async (request) => {
452
- const traceId = PublicKey.random().toHex();
453
- log.trace(
454
- 'dxos.sdk.invitation-handler.host.authenticate',
455
- trace.begin({ id: traceId, parentId: this._traceParent })
456
- );
457
- const response = await this._callbacks.authenticate(request);
458
- log.trace('dxos.sdk.invitation-handler.host.authenticate', trace.end({ id: traceId, data: { ...response } }));
459
- return response;
460
- },
461
-
462
- admit: async (request) => {
463
- const traceId = PublicKey.random().toHex();
464
- log.trace(
465
- 'dxos.sdk.invitation-handler.host.admit',
466
- trace.begin({ id: traceId, parentId: this._traceParent })
467
- );
468
- const response = await this._callbacks.admit(request);
469
- log.trace('dxos.sdk.invitation-handler.host.admit', trace.end({ id: traceId }));
470
- return response;
471
- }
472
- }
473
- };
474
- }
475
-
476
- override async onOpen(context: ExtensionContext) {
477
- await super.onOpen(context);
478
- this._callbacks.onOpen();
479
- }
480
- }
481
-
482
- type InvitationGuestExtensionCallbacks = {
483
- // Deliberately not async to not block the extensions opening.
484
- onOpen: () => void;
485
- };
486
-
487
- /**
488
- * Guest's side for a connection to a concrete peer in p2p network during invitation.
489
- */
490
- class InvitationGuestExtension extends RpcExtension<{ InvitationHostService: InvitationHostService }, {}> {
491
- constructor(private readonly _callbacks: InvitationGuestExtensionCallbacks) {
492
- super({
493
- requested: {
494
- InvitationHostService: schema.getService('dxos.halo.invitations.InvitationHostService')
495
- }
496
- });
497
- }
498
-
499
- protected override async getHandlers() {
500
- return {};
501
- }
502
-
503
- override async onOpen(context: ExtensionContext) {
504
- await super.onOpen(context);
505
- this._callbacks.onOpen();
506
- }
507
- }
@@ -4,10 +4,16 @@
4
4
 
5
5
  import assert from 'node:assert';
6
6
 
7
+ import { Event } from '@dxos/async';
7
8
  import { AuthenticatingInvitationObservable, CancellableInvitationObservable } from '@dxos/client';
8
9
  import { Stream } from '@dxos/codec-protobuf';
9
10
  import { log } from '@dxos/log';
10
- import { AuthenticationRequest, Invitation, InvitationsService } from '@dxos/protocols/proto/dxos/client/services';
11
+ import {
12
+ AuthenticationRequest,
13
+ Invitation,
14
+ InvitationsService,
15
+ QueryInvitationsResponse
16
+ } from '@dxos/protocols/proto/dxos/client/services';
11
17
 
12
18
  import { InvitationProtocol } from './invitation-protocol';
13
19
  import { InvitationsHandler } from './invitations-handler';
@@ -18,6 +24,10 @@ import { InvitationsHandler } from './invitations-handler';
18
24
  export class InvitationsServiceImpl implements InvitationsService {
19
25
  private readonly _createInvitations = new Map<string, CancellableInvitationObservable>();
20
26
  private readonly _acceptInvitations = new Map<string, AuthenticatingInvitationObservable>();
27
+ private readonly _invitationCreated = new Event<Invitation>();
28
+ private readonly _invitationAccepted = new Event<Invitation>();
29
+ private readonly _removedCreated = new Event<Invitation>();
30
+ private readonly _removedAccepted = new Event<Invitation>();
21
31
 
22
32
  constructor(
23
33
  private readonly _invitationsHandler: InvitationsHandler,
@@ -31,160 +41,61 @@ export class InvitationsServiceImpl implements InvitationsService {
31
41
  };
32
42
  }
33
43
 
34
- createInvitation(invitation: Invitation): Stream<Invitation> {
35
- return new Stream<Invitation>(({ next, close }) => {
36
- const handler = this._getHandler(invitation);
37
- log('stream opened', this.getLoggingContext());
44
+ createInvitation(options: Invitation): Stream<Invitation> {
45
+ let invitation: CancellableInvitationObservable;
46
+
47
+ const existingInvitation = this._createInvitations.get(options.invitationId);
48
+ if (existingInvitation) {
49
+ invitation = existingInvitation;
50
+ } else {
51
+ const handler = this._getHandler(options);
52
+ invitation = this._invitationsHandler.createInvitation(handler, options);
53
+ this._createInvitations.set(invitation.get().invitationId, invitation);
54
+ this._invitationCreated.emit(invitation.get());
55
+ }
38
56
 
39
- let invitationId: string;
40
- const observable = this._invitationsHandler.createInvitation(handler, invitation);
41
- observable.subscribe(
57
+ return new Stream<Invitation>(({ next, close }) => {
58
+ invitation.subscribe(
42
59
  (invitation) => {
43
- switch (invitation.state) {
44
- case Invitation.State.CONNECTING: {
45
- assert(invitation.invitationId);
46
- invitationId = invitation.invitationId;
47
- this._createInvitations.set(invitation.invitationId, observable);
48
- invitation.state = Invitation.State.CONNECTING;
49
- next(invitation);
50
- break;
51
- }
52
- case Invitation.State.CONNECTED: {
53
- assert(invitation.invitationId);
54
- invitation.state = Invitation.State.CONNECTED;
55
- next(invitation);
56
- break;
57
- }
58
- case Invitation.State.READY_FOR_AUTHENTICATION: {
59
- assert(invitation.invitationId);
60
- invitation.state = Invitation.State.READY_FOR_AUTHENTICATION;
61
- next(invitation);
62
- break;
63
- }
64
- case Invitation.State.AUTHENTICATING: {
65
- assert(invitation.invitationId);
66
- invitation.state = Invitation.State.AUTHENTICATING;
67
- next(invitation);
68
- break;
69
- }
70
- case Invitation.State.SUCCESS: {
71
- assert(invitation.invitationId);
72
- invitation.state = Invitation.State.SUCCESS;
73
- next(invitation);
74
- break;
75
- }
76
- case Invitation.State.CANCELLED: {
77
- assert(invitationId);
78
- invitation.invitationId = invitationId;
79
- invitation.state = Invitation.State.CANCELLED;
80
- next(invitation);
81
- break;
82
- }
83
- case Invitation.State.TIMEOUT: {
84
- assert(invitationId);
85
- invitation.invitationId = invitationId;
86
- invitation.state = Invitation.State.TIMEOUT;
87
- next(invitation);
88
- break;
89
- }
90
- }
60
+ next(invitation);
91
61
  },
92
62
  (err: Error) => {
93
63
  close(err);
94
64
  },
95
65
  () => {
96
66
  close();
67
+ this._createInvitations.delete(invitation.get().invitationId);
97
68
  }
98
69
  );
99
-
100
- return (err?: Error) => {
101
- const context = this.getLoggingContext();
102
- if (err) {
103
- log.warn('stream closed', { ...context, err });
104
- } else {
105
- log('stream closed', context);
106
- }
107
-
108
- this._createInvitations.delete(invitation.invitationId!);
109
- };
110
70
  });
111
71
  }
112
72
 
113
- acceptInvitation(invitation: Invitation): Stream<Invitation> {
114
- return new Stream<Invitation>(({ next, close }) => {
115
- log('stream opened', this.getLoggingContext());
116
- const handler = this._getHandler(invitation);
73
+ acceptInvitation(options: Invitation): Stream<Invitation> {
74
+ let invitation: AuthenticatingInvitationObservable;
75
+
76
+ const existingInvitation = this._acceptInvitations.get(options.invitationId);
77
+ if (existingInvitation) {
78
+ invitation = existingInvitation;
79
+ } else {
80
+ const handler = this._getHandler(options);
81
+ invitation = this._invitationsHandler.acceptInvitation(handler, options);
82
+ this._acceptInvitations.set(invitation.get().invitationId, invitation);
83
+ this._invitationAccepted.emit(invitation.get());
84
+ }
117
85
 
118
- let invitationId: string;
119
- const observable = this._invitationsHandler.acceptInvitation(handler, invitation);
120
- observable.subscribe(
86
+ return new Stream<Invitation>(({ next, close }) => {
87
+ invitation.subscribe(
121
88
  (invitation) => {
122
- switch (invitation.state) {
123
- case Invitation.State.CONNECTING: {
124
- assert(invitation.invitationId);
125
- invitationId = invitation.invitationId;
126
- this._acceptInvitations.set(invitation.invitationId, observable);
127
- invitation.state = Invitation.State.CONNECTING;
128
- next(invitation);
129
- break;
130
- }
131
- case Invitation.State.CONNECTED: {
132
- assert(invitation.invitationId);
133
- invitation.state = Invitation.State.CONNECTED;
134
- next(invitation);
135
- break;
136
- }
137
- case Invitation.State.READY_FOR_AUTHENTICATION: {
138
- assert(invitation.invitationId);
139
- invitation.state = Invitation.State.READY_FOR_AUTHENTICATION;
140
- next(invitation);
141
- break;
142
- }
143
- case Invitation.State.AUTHENTICATING: {
144
- assert(invitation.invitationId);
145
- invitation.state = Invitation.State.AUTHENTICATING;
146
- next(invitation);
147
- break;
148
- }
149
- case Invitation.State.SUCCESS: {
150
- invitation.state = Invitation.State.SUCCESS;
151
- next(invitation);
152
- break;
153
- }
154
- case Invitation.State.CANCELLED: {
155
- assert(invitationId);
156
- invitation.invitationId = invitationId;
157
- invitation.state = Invitation.State.CANCELLED;
158
- next(invitation);
159
- break;
160
- }
161
- case Invitation.State.TIMEOUT: {
162
- assert(invitationId);
163
- invitation.invitationId = invitationId;
164
- invitation.state = Invitation.State.TIMEOUT;
165
- next(invitation);
166
- break;
167
- }
168
- }
89
+ next(invitation);
169
90
  },
170
91
  (err: Error) => {
171
92
  close(err);
172
93
  },
173
94
  () => {
174
95
  close();
96
+ this._acceptInvitations.delete(invitation.get().invitationId);
175
97
  }
176
98
  );
177
-
178
- return (err?: Error) => {
179
- const context = this.getLoggingContext();
180
- if (err) {
181
- log.warn('stream closed', { ...context, err });
182
- } else {
183
- log('stream closed', context);
184
- }
185
-
186
- this._acceptInvitations.delete(invitation.invitationId!);
187
- };
188
99
  });
189
100
  }
190
101
 
@@ -200,13 +111,71 @@ export class InvitationsServiceImpl implements InvitationsService {
200
111
  }
201
112
 
202
113
  async cancelInvitation({ invitationId }: { invitationId: string }): Promise<void> {
203
- log('cancelling...');
114
+ log('deleting...');
204
115
  assert(invitationId);
205
- const observable = this._createInvitations.get(invitationId) ?? this._acceptInvitations.get(invitationId);
206
- if (!observable) {
207
- log.warn('invalid invitation', { invitationId });
116
+ const created = this._createInvitations.get(invitationId);
117
+ const accepted = this._acceptInvitations.get(invitationId);
118
+ if (created) {
119
+ await created.cancel();
120
+ this._createInvitations.delete(invitationId);
121
+ this._removedCreated.emit(created.get());
122
+ } else if (accepted) {
123
+ await accepted.cancel();
124
+ this._acceptInvitations.delete(invitationId);
125
+ this._removedAccepted.emit(accepted.get());
208
126
  } else {
209
- await observable?.cancel();
127
+ log.warn('invalid invitation', { invitationId });
210
128
  }
211
129
  }
130
+
131
+ queryInvitations(): Stream<QueryInvitationsResponse> {
132
+ return new Stream<QueryInvitationsResponse>(({ next, ctx }) => {
133
+ // Push added invitations to the stream.
134
+ this._invitationCreated.on(ctx, (invitation) => {
135
+ next({
136
+ action: QueryInvitationsResponse.Action.ADDED,
137
+ type: QueryInvitationsResponse.Type.CREATED,
138
+ invitations: [invitation]
139
+ });
140
+ });
141
+
142
+ this._invitationAccepted.on(ctx, (invitation) => {
143
+ next({
144
+ action: QueryInvitationsResponse.Action.ADDED,
145
+ type: QueryInvitationsResponse.Type.ACCEPTED,
146
+ invitations: [invitation]
147
+ });
148
+ });
149
+
150
+ // Push removed invitations to the stream.
151
+ this._removedCreated.on(ctx, (invitation) => {
152
+ next({
153
+ action: QueryInvitationsResponse.Action.REMOVED,
154
+ type: QueryInvitationsResponse.Type.CREATED,
155
+ invitations: [invitation]
156
+ });
157
+ });
158
+
159
+ this._removedAccepted.on(ctx, (invitation) => {
160
+ next({
161
+ action: QueryInvitationsResponse.Action.REMOVED,
162
+ type: QueryInvitationsResponse.Type.ACCEPTED,
163
+ invitations: [invitation]
164
+ });
165
+ });
166
+
167
+ // Push existing invitations to the stream.
168
+ next({
169
+ action: QueryInvitationsResponse.Action.ADDED,
170
+ type: QueryInvitationsResponse.Type.CREATED,
171
+ invitations: Array.from(this._createInvitations.values()).map((invitation) => invitation.get())
172
+ });
173
+
174
+ next({
175
+ action: QueryInvitationsResponse.Action.ADDED,
176
+ type: QueryInvitationsResponse.Type.ACCEPTED,
177
+ invitations: Array.from(this._acceptInvitations.values()).map((invitation) => invitation.get())
178
+ });
179
+ });
180
+ }
212
181
  }
@@ -30,11 +30,11 @@ export const fromHost = (config: Config = new Config()): ClientServicesProvider
30
30
  */
31
31
  // TODO(burdon): Move to client-services and remove dependencies from here.
32
32
  const createNetworkManager = (config: Config, options: Partial<NetworkManagerOptions> = {}): NetworkManager => {
33
- const signalServer = config.get('runtime.services.signal.server');
34
- if (signalServer) {
33
+ const signals = config.get('runtime.services.signaling');
34
+ if (signals) {
35
35
  const {
36
36
  log = true,
37
- signalManager = new WebsocketSignalManager([signalServer]),
37
+ signalManager = new WebsocketSignalManager(signals),
38
38
  transportFactory = createWebRTCTransportFactory({
39
39
  iceServers: config.get('runtime.services.ice')
40
40
  })
@@ -23,10 +23,12 @@ export const testConfigWithLocalSignal = new Config({
23
23
  version: 1,
24
24
  runtime: {
25
25
  services: {
26
- signal: {
27
- // TODO(burdon): Port numbers and global consts?
28
- server: 'ws://localhost:4000/.well-known/dx/signal'
29
- }
26
+ signaling: [
27
+ {
28
+ // TODO(burdon): Port numbers and global consts?
29
+ server: 'ws://localhost:4000/.well-known/dx/signal'
30
+ }
31
+ ]
30
32
  }
31
33
  }
32
34
  });
@@ -56,11 +58,11 @@ export class TestBuilder {
56
58
  * Get network manager using local shared memory or remote signal manager.
57
59
  */
58
60
  get networkManager() {
59
- const signalServer = this._config.get('runtime.services.signal.server');
60
- if (signalServer) {
61
+ const signals = this._config.get('runtime.services.signaling');
62
+ if (signals) {
61
63
  return new NetworkManager({
62
64
  log: true,
63
- signalManager: new WebsocketSignalManager([signalServer]),
65
+ signalManager: new WebsocketSignalManager(signals),
64
66
  transportFactory: createWebRTCTransportFactory({
65
67
  iceServers: this._config.get('runtime.services.ice')
66
68
  })
@@ -75,14 +75,14 @@ export class IFrameHostRuntime {
75
75
  log('starting...');
76
76
  try {
77
77
  this._config = await getAsyncValue(this._configProvider);
78
- const signalServer = this._config.get('runtime.services.signal.server');
78
+ const signals = this._config.get('runtime.services.signaling');
79
79
  this._clientServices = new LocalClientServices({
80
80
  lockKey: LOCK_KEY,
81
81
  config: this._config,
82
82
  networkManager: new NetworkManager({
83
83
  log: true,
84
- signalManager: signalServer
85
- ? new WebsocketSignalManager([signalServer])
84
+ signalManager: signals
85
+ ? new WebsocketSignalManager(signals)
86
86
  : new MemorySignalManager(new MemorySignalManagerContext()), // TODO(dmaretskyi): Inject this context.
87
87
  transportFactory: this._transportFactory
88
88
  })
@@ -3,6 +3,8 @@
3
3
  //
4
4
 
5
5
  import { iframeServiceBundle, workerServiceBundle, WorkerServiceBundle } from '@dxos/client';
6
+ import { RemoteServiceConnectionError } from '@dxos/errors';
7
+ import { log } from '@dxos/log';
6
8
  import { WebRTCTransportService } from '@dxos/network-manager';
7
9
  import { BridgeService } from '@dxos/protocols/proto/dxos/mesh/bridge';
8
10
  import { createProtoRpcPeer, ProtoRpcPeer, RpcPort } from '@dxos/rpc';
@@ -54,7 +56,7 @@ export class IFrameProxyRuntime {
54
56
  }
55
57
  },
56
58
  port: this._systemPort,
57
- timeout: 200
59
+ timeout: 1000
58
60
  });
59
61
 
60
62
  this._workerAppPort.subscribe((msg) => this._windowAppPort.send(msg));
@@ -70,8 +72,14 @@ export class IFrameProxyRuntime {
70
72
  }
71
73
 
72
74
  async open(origin: string) {
73
- await this._systemRpc.open();
74
- await this._systemRpc.rpc.WorkerService.start({ origin });
75
+ try {
76
+ await this._systemRpc.open();
77
+ await this._systemRpc.rpc.WorkerService.start({ origin });
78
+ } catch (err) {
79
+ log.catch(err);
80
+ throw new RemoteServiceConnectionError('Failed to connect to worker');
81
+ }
82
+
75
83
  await this._shellRuntime?.open();
76
84
  }
77
85