@dxos/client-services 0.4.10-main.fd4f2a3 → 0.4.10-main.fd8ea31

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 (97) hide show
  1. package/dist/lib/browser/{chunk-V2ALN47T.mjs → chunk-WLE7E36I.mjs} +1402 -1074
  2. package/dist/lib/browser/chunk-WLE7E36I.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +13 -3
  4. package/dist/lib/browser/index.mjs.map +1 -1
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +133 -115
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-7WXQVUAE.cjs → chunk-YXZQQAQN.cjs} +1308 -1063
  9. package/dist/lib/node/chunk-YXZQQAQN.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +49 -39
  11. package/dist/lib/node/index.cjs.map +1 -1
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/packlets/testing/index.cjs +133 -118
  14. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  15. package/dist/types/src/index.d.ts +1 -0
  16. package/dist/types/src/index.d.ts.map +1 -1
  17. package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts +5 -0
  18. package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts.map +1 -0
  19. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts +5 -0
  20. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -0
  21. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts +15 -0
  22. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -0
  23. package/dist/types/src/packlets/{services → diagnostics}/diagnostics.d.ts +1 -1
  24. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -0
  25. package/dist/types/src/packlets/diagnostics/index.d.ts +4 -0
  26. package/dist/types/src/packlets/diagnostics/index.d.ts.map +1 -0
  27. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  28. package/dist/types/src/packlets/indexing/util.d.ts +2 -6
  29. package/dist/types/src/packlets/indexing/util.d.ts.map +1 -1
  30. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +3 -1
  31. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  32. package/dist/types/src/packlets/invitations/index.d.ts +1 -0
  33. package/dist/types/src/packlets/invitations/index.d.ts.map +1 -1
  34. package/dist/types/src/packlets/invitations/invitation-extension.d.ts +1 -0
  35. package/dist/types/src/packlets/invitations/invitation-extension.d.ts.map +1 -1
  36. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +6 -1
  37. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  38. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -4
  39. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  40. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +44 -0
  41. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -0
  42. package/dist/types/src/packlets/invitations/invitations-service.d.ts +7 -23
  43. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  44. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +2 -1
  45. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  46. package/dist/types/src/packlets/services/index.d.ts +1 -1
  47. package/dist/types/src/packlets/services/index.d.ts.map +1 -1
  48. package/dist/types/src/packlets/services/service-context.d.ts +2 -0
  49. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  50. package/dist/types/src/packlets/services/service-host.d.ts +5 -1
  51. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  52. package/dist/types/src/packlets/services/util.d.ts +1 -0
  53. package/dist/types/src/packlets/services/util.d.ts.map +1 -1
  54. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +5 -1
  55. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  56. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  57. package/dist/types/src/packlets/system/system-service.d.ts +1 -1
  58. package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
  59. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  60. package/dist/types/src/packlets/testing/test-builder.d.ts +6 -1
  61. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  62. package/dist/types/src/version.d.ts +1 -1
  63. package/package.json +35 -34
  64. package/src/index.ts +1 -0
  65. package/src/packlets/diagnostics/browser-diagnostics-broadcast.ts +94 -0
  66. package/src/packlets/diagnostics/diagnostics-broadcast.ts +20 -0
  67. package/src/packlets/diagnostics/diagnostics-collector.ts +65 -0
  68. package/src/packlets/{services → diagnostics}/diagnostics.ts +2 -2
  69. package/src/packlets/diagnostics/index.ts +7 -0
  70. package/src/packlets/identity/identity-manager.ts +1 -0
  71. package/src/packlets/identity/identity.test.ts +3 -0
  72. package/src/packlets/indexing/util.ts +9 -66
  73. package/src/packlets/invitations/device-invitation-protocol.ts +6 -1
  74. package/src/packlets/invitations/index.ts +1 -0
  75. package/src/packlets/invitations/invitation-extension.ts +28 -1
  76. package/src/packlets/invitations/invitation-protocol.ts +7 -1
  77. package/src/packlets/invitations/invitations-handler.ts +75 -96
  78. package/src/packlets/invitations/invitations-manager.ts +271 -0
  79. package/src/packlets/invitations/invitations-service.ts +23 -168
  80. package/src/packlets/invitations/space-invitation-protocol.ts +45 -3
  81. package/src/packlets/services/automerge-host.test.ts +10 -4
  82. package/src/packlets/services/index.ts +1 -1
  83. package/src/packlets/services/service-context.test.ts +4 -1
  84. package/src/packlets/services/service-context.ts +19 -5
  85. package/src/packlets/services/service-host.ts +34 -19
  86. package/src/packlets/services/util.ts +2 -0
  87. package/src/packlets/spaces/data-space-manager.test.ts +4 -4
  88. package/src/packlets/spaces/data-space-manager.ts +48 -2
  89. package/src/packlets/spaces/data-space.ts +1 -1
  90. package/src/packlets/storage/level.ts +1 -1
  91. package/src/packlets/system/system-service.ts +1 -1
  92. package/src/packlets/testing/invitation-utils.ts +100 -97
  93. package/src/packlets/testing/test-builder.ts +39 -5
  94. package/src/version.ts +1 -1
  95. package/dist/lib/browser/chunk-V2ALN47T.mjs.map +0 -7
  96. package/dist/lib/node/chunk-7WXQVUAE.cjs.map +0 -7
  97. package/dist/types/src/packlets/services/diagnostics.d.ts.map +0 -1
@@ -2,43 +2,22 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Event, scheduleTask } from '@dxos/async';
6
- import { type AuthenticatingInvitation, type CancellableInvitation } from '@dxos/client-protocol';
7
5
  import { Stream } from '@dxos/codec-protobuf';
8
- import { Context } from '@dxos/context';
9
- import { type MetadataStore } from '@dxos/echo-pipeline';
10
- import { invariant } from '@dxos/invariant';
11
- import { log } from '@dxos/log';
12
6
  import {
13
7
  type AuthenticationRequest,
14
8
  type AcceptInvitationRequest,
15
- Invitation,
9
+ type Invitation,
16
10
  type InvitationsService,
17
11
  QueryInvitationsResponse,
18
12
  } from '@dxos/protocols/proto/dxos/client/services';
19
13
 
20
- import { type InvitationProtocol } from './invitation-protocol';
21
- import { invitationExpired, type InvitationsHandler } from './invitations-handler';
14
+ import { type InvitationsManager } from './invitations-manager';
22
15
 
23
16
  /**
24
17
  * Adapts invitation service observable to client/service stream.
25
18
  */
26
19
  export class InvitationsServiceImpl implements InvitationsService {
27
- private readonly _createInvitations = new Map<string, CancellableInvitation>();
28
- private readonly _acceptInvitations = new Map<string, AuthenticatingInvitation>();
29
- private readonly _invitationCreated = new Event<Invitation>();
30
- private readonly _invitationAccepted = new Event<Invitation>();
31
- private readonly _removedCreated = new Event<Invitation>();
32
- private readonly _removedAccepted = new Event<Invitation>();
33
- private readonly _saved = new Event<Invitation>();
34
- private readonly _persistentInvitationsLoadedEvent = new Event();
35
- private _persistentInvitationsLoaded = false;
36
-
37
- constructor(
38
- private readonly _invitationsHandler: InvitationsHandler,
39
- private readonly _getHandler: (invitation: Invitation) => InvitationProtocol,
40
- private readonly _metadataStore: MetadataStore,
41
- ) {}
20
+ constructor(private readonly _invitationsManager: InvitationsManager) {}
42
21
 
43
22
  // TODO(burdon): Guest/host label.
44
23
  getLoggingContext() {
@@ -48,148 +27,33 @@ export class InvitationsServiceImpl implements InvitationsService {
48
27
  }
49
28
 
50
29
  createInvitation(options: Invitation): Stream<Invitation> {
51
- let invitation: CancellableInvitation;
52
-
53
- const savePersistentInvitationCtx = new Context();
54
- const existingInvitation = this._createInvitations.get(options.invitationId);
55
- if (existingInvitation) {
56
- invitation = existingInvitation;
57
- } else {
58
- const handler = this._getHandler(options);
59
- invitation = this._invitationsHandler.createInvitation(handler, options);
60
- this._createInvitations.set(invitation.get().invitationId, invitation);
61
- this._invitationCreated.emit(invitation.get());
62
- }
63
-
64
30
  return new Stream<Invitation>(({ next, close }) => {
65
- if (invitation.get().persistent) {
66
- scheduleTask(savePersistentInvitationCtx, async () => {
67
- try {
68
- await this._metadataStore.addInvitation(invitation.get());
69
- this._saved.emit(invitation.get());
70
- } catch (err: any) {
71
- close(err);
72
- }
73
- });
74
- }
75
- invitation.subscribe(
76
- (invitation) => {
77
- next(invitation);
78
- },
79
- async (err: Error) => {
80
- await savePersistentInvitationCtx.dispose();
81
-
82
- // TODO(nf): also remove from storage?
83
- close(err);
84
- },
85
- async () => {
86
- close();
87
- if (invitation.get().persistent) {
88
- await savePersistentInvitationCtx.dispose();
89
- // TODO(nf): remove on all complete conditions?
90
- await this._metadataStore.removeInvitation(invitation.get().invitationId);
91
- }
92
-
93
- this._createInvitations.delete(invitation.get().invitationId);
94
- if (invitation.get().type !== Invitation.Type.MULTIUSE) {
95
- this._removedCreated.emit(invitation.get());
96
- }
97
- },
98
- );
99
- });
100
- }
101
-
102
- async loadPersistentInvitations() {
103
- const persistentInvitations = this._metadataStore.getInvitations();
104
-
105
- // get saved persistent invitations, filter and remove from storage those that have expired.
106
- const freshInvitations = persistentInvitations.filter(async (invitation) => !invitationExpired(invitation));
107
-
108
- const cInvitations = freshInvitations.map((persistentInvitation) => {
109
- invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
110
-
111
- const handler = this._getHandler(persistentInvitation);
112
- const invitation = this._invitationsHandler.createInvitation(handler, persistentInvitation);
113
- this._createInvitations.set(invitation.get().invitationId, invitation);
114
- this._invitationCreated.emit(invitation.get());
115
- return persistentInvitation;
31
+ void this._invitationsManager
32
+ .createInvitation(options)
33
+ .then((invitation) => invitation.subscribe(next, close, close))
34
+ .catch(close);
116
35
  });
117
- this._persistentInvitationsLoadedEvent.emit();
118
- this._persistentInvitationsLoaded = true;
119
- return { invitations: cInvitations };
120
36
  }
121
37
 
122
- acceptInvitation({ invitation: options, deviceProfile }: AcceptInvitationRequest): Stream<Invitation> {
123
- let invitation: AuthenticatingInvitation;
124
-
125
- // TODO(nf): duplicate check in InvitationHandler
126
- if (deviceProfile) {
127
- invariant(options.kind === Invitation.Kind.DEVICE, 'deviceProfile provided for non-device invitation');
128
- }
129
-
130
- const existingInvitation = this._acceptInvitations.get(options.invitationId);
131
- if (existingInvitation) {
132
- invitation = existingInvitation;
133
- } else {
134
- const handler = this._getHandler(options);
135
- invitation = this._invitationsHandler.acceptInvitation(handler, options, deviceProfile);
136
- this._acceptInvitations.set(invitation.get().invitationId, invitation);
137
- this._invitationAccepted.emit(invitation.get());
138
- }
139
-
38
+ acceptInvitation(request: AcceptInvitationRequest): Stream<Invitation> {
39
+ const invitation = this._invitationsManager.acceptInvitation(request);
140
40
  return new Stream<Invitation>(({ next, close }) => {
141
- invitation.subscribe(
142
- (invitation) => {
143
- next(invitation);
144
- },
145
- (err: Error) => {
146
- close(err);
147
- },
148
- () => {
149
- close();
150
- this._acceptInvitations.delete(invitation.get().invitationId);
151
- if (invitation.get().type !== Invitation.Type.MULTIUSE) {
152
- this._removedAccepted.emit(invitation.get());
153
- }
154
- },
155
- );
41
+ invitation.subscribe(next, close, close);
156
42
  });
157
43
  }
158
44
 
159
- async authenticate({ invitationId, authCode }: AuthenticationRequest): Promise<void> {
160
- log('authenticating...');
161
- invariant(invitationId);
162
- const observable = this._acceptInvitations.get(invitationId);
163
- if (!observable) {
164
- log.warn('invalid invitation', { invitationId });
165
- } else {
166
- await observable.authenticate(authCode);
167
- }
45
+ async authenticate(request: AuthenticationRequest): Promise<void> {
46
+ return this._invitationsManager.authenticate(request);
168
47
  }
169
48
 
170
- async cancelInvitation({ invitationId }: { invitationId: string }): Promise<void> {
171
- log('cancelInvitation...', { invitationId });
172
- invariant(invitationId);
173
- const created = this._createInvitations.get(invitationId);
174
- const accepted = this._acceptInvitations.get(invitationId);
175
- if (created) {
176
- await created.cancel();
177
- this._createInvitations.delete(invitationId);
178
- this._removedCreated.emit(created.get());
179
- if (created.get().persistent) {
180
- await this._metadataStore.removeInvitation(created.get().invitationId);
181
- }
182
- } else if (accepted) {
183
- await accepted.cancel();
184
- this._acceptInvitations.delete(invitationId);
185
- this._removedAccepted.emit(accepted.get());
186
- }
49
+ async cancelInvitation(request: { invitationId: string }): Promise<void> {
50
+ return this._invitationsManager.cancelInvitation(request);
187
51
  }
188
52
 
189
53
  queryInvitations(): Stream<QueryInvitationsResponse> {
190
54
  return new Stream<QueryInvitationsResponse>(({ next, ctx }) => {
191
55
  // Push added invitations to the stream.
192
- this._invitationCreated.on(ctx, (invitation) => {
56
+ this._invitationsManager.invitationCreated.on(ctx, (invitation) => {
193
57
  next({
194
58
  action: QueryInvitationsResponse.Action.ADDED,
195
59
  type: QueryInvitationsResponse.Type.CREATED,
@@ -197,7 +61,7 @@ export class InvitationsServiceImpl implements InvitationsService {
197
61
  });
198
62
  });
199
63
 
200
- this._invitationAccepted.on(ctx, (invitation) => {
64
+ this._invitationsManager.invitationAccepted.on(ctx, (invitation) => {
201
65
  next({
202
66
  action: QueryInvitationsResponse.Action.ADDED,
203
67
  type: QueryInvitationsResponse.Type.ACCEPTED,
@@ -206,7 +70,7 @@ export class InvitationsServiceImpl implements InvitationsService {
206
70
  });
207
71
 
208
72
  // Push removed invitations to the stream.
209
- this._removedCreated.on(ctx, (invitation) => {
73
+ this._invitationsManager.removedCreated.on(ctx, (invitation) => {
210
74
  next({
211
75
  action: QueryInvitationsResponse.Action.REMOVED,
212
76
  type: QueryInvitationsResponse.Type.CREATED,
@@ -214,7 +78,7 @@ export class InvitationsServiceImpl implements InvitationsService {
214
78
  });
215
79
  });
216
80
 
217
- this._removedAccepted.on(ctx, (invitation) => {
81
+ this._invitationsManager.removedAccepted.on(ctx, (invitation) => {
218
82
  next({
219
83
  action: QueryInvitationsResponse.Action.REMOVED,
220
84
  type: QueryInvitationsResponse.Type.ACCEPTED,
@@ -223,7 +87,7 @@ export class InvitationsServiceImpl implements InvitationsService {
223
87
  });
224
88
 
225
89
  // used only for testing
226
- this._saved.on(ctx, (invitation) => {
90
+ this._invitationsManager.saved.on(ctx, (invitation) => {
227
91
  next({
228
92
  action: QueryInvitationsResponse.Action.SAVED,
229
93
  type: QueryInvitationsResponse.Type.CREATED,
@@ -235,33 +99,24 @@ export class InvitationsServiceImpl implements InvitationsService {
235
99
  next({
236
100
  action: QueryInvitationsResponse.Action.ADDED,
237
101
  type: QueryInvitationsResponse.Type.CREATED,
238
- invitations: Array.from(this._createInvitations.values()).map((invitation) => invitation.get()),
102
+ invitations: this._invitationsManager.getCreatedInvitations(),
239
103
  existing: true,
240
104
  });
241
105
 
242
106
  next({
243
107
  action: QueryInvitationsResponse.Action.ADDED,
244
108
  type: QueryInvitationsResponse.Type.ACCEPTED,
245
- invitations: Array.from(this._acceptInvitations.values()).map((invitation) => invitation.get()),
109
+ invitations: this._invitationsManager.getAcceptedInvitations(),
246
110
  existing: true,
247
111
  });
248
112
 
249
- if (this._persistentInvitationsLoaded) {
113
+ this._invitationsManager.onPersistentInvitationsLoaded(ctx, () => {
250
114
  next({
251
115
  action: QueryInvitationsResponse.Action.LOAD_COMPLETE,
252
116
  type: QueryInvitationsResponse.Type.CREATED,
253
117
  // TODO(nf): populate with invitations
254
118
  });
255
- } else {
256
- this._persistentInvitationsLoadedEvent.on(ctx, () => {
257
- next({
258
- action: QueryInvitationsResponse.Action.LOAD_COMPLETE,
259
- type: QueryInvitationsResponse.Type.CREATED,
260
- // TODO(nf): populate with invitations
261
- });
262
- });
263
- }
264
-
119
+ });
265
120
  // TODO(nf): expired invitations?
266
121
  });
267
122
  }
@@ -2,7 +2,11 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { createAdmissionCredentials, getCredentialAssertion } from '@dxos/credentials';
5
+ import {
6
+ createAdmissionCredentials,
7
+ createDelegatedSpaceInvitationCredential,
8
+ getCredentialAssertion,
9
+ } from '@dxos/credentials';
6
10
  import { writeMessages } from '@dxos/feed-store';
7
11
  import { invariant } from '@dxos/invariant';
8
12
  import { type Keyring } from '@dxos/keyring';
@@ -11,7 +15,7 @@ import { log } from '@dxos/log';
11
15
  import { AlreadyJoinedError } from '@dxos/protocols';
12
16
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
13
17
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
14
- import { type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
18
+ import { SpaceMember, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
15
19
  import {
16
20
  type AdmissionRequest,
17
21
  type AdmissionResponse,
@@ -43,7 +47,11 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
43
47
  };
44
48
  }
45
49
 
46
- async admit(request: AdmissionRequest, guestProfile?: ProfileDocument | undefined): Promise<AdmissionResponse> {
50
+ async admit(
51
+ invitation: Invitation,
52
+ request: AdmissionRequest,
53
+ guestProfile?: ProfileDocument | undefined,
54
+ ): Promise<AdmissionResponse> {
47
55
  invariant(this._spaceKey);
48
56
  const space = await this._spaceManager.spaces.get(this._spaceKey);
49
57
  invariant(space);
@@ -59,6 +67,7 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
59
67
  space.key,
60
68
  space.inner.genesisFeedKey,
61
69
  guestProfile,
70
+ invitation.delegationCredentialId,
62
71
  );
63
72
 
64
73
  // TODO(dmaretskyi): Refactor.
@@ -76,6 +85,39 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
76
85
  };
77
86
  }
78
87
 
88
+ async delegate(invitation: Invitation): Promise<PublicKey> {
89
+ invariant(this._spaceKey);
90
+ const space = await this._spaceManager.spaces.get(this._spaceKey);
91
+ invariant(space);
92
+ if (invitation.authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY) {
93
+ invariant(invitation.guestKeypair?.publicKey);
94
+ }
95
+
96
+ log('writing delegate space invitation', { host: this._signingContext.deviceKey, id: invitation.invitationId });
97
+ const credential = await createDelegatedSpaceInvitationCredential(
98
+ this._signingContext.credentialSigner,
99
+ space.key,
100
+ {
101
+ invitationId: invitation.invitationId,
102
+ authMethod: invitation.authMethod,
103
+ swarmKey: invitation.swarmKey,
104
+ role: SpaceMember.Role.ADMIN,
105
+ expiresOn: invitation.lifetime
106
+ ? new Date((invitation.created?.getTime() ?? Date.now()) + invitation.lifetime)
107
+ : undefined,
108
+ multiUse: invitation.multiUse ?? false,
109
+ guestKey:
110
+ invitation.authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY
111
+ ? invitation.guestKeypair!.publicKey
112
+ : undefined,
113
+ },
114
+ );
115
+
116
+ invariant(credential.credential);
117
+ await writeMessages(space.inner.controlPipeline.writer, [credential]);
118
+ return credential.credential.credential.id!;
119
+ }
120
+
79
121
  checkInvitation(invitation: Partial<Invitation>) {
80
122
  if (invitation.spaceKey && this._spaceManager.spaces.has(invitation.spaceKey)) {
81
123
  return new AlreadyJoinedError('Already joined space.');
@@ -5,9 +5,9 @@
5
5
  import { expect } from 'chai';
6
6
 
7
7
  import { asyncTimeout, sleep } from '@dxos/async';
8
+ import { AutomergeContext } from '@dxos/echo-db';
8
9
  import { AutomergeHost, DataServiceImpl } from '@dxos/echo-pipeline';
9
- import { AutomergeContext } from '@dxos/echo-schema';
10
- import { StorageType, createStorage } from '@dxos/random-access-storage';
10
+ import { createTestLevel } from '@dxos/echo-pipeline/testing';
11
11
  import { afterTest, describe, test } from '@dxos/test';
12
12
 
13
13
  describe('AutomergeHost', () => {
@@ -19,10 +19,16 @@ describe('AutomergeHost', () => {
19
19
  // creates repo and document | replicates repo | finds document in repo
20
20
  //
21
21
 
22
- const storageDirectory = createStorage({ type: StorageType.RAM }).createDirectory();
22
+ const level = createTestLevel();
23
+ await level.open();
24
+ afterTest(() => level.close());
23
25
 
24
- const host = new AutomergeHost({ directory: storageDirectory });
26
+ const host = new AutomergeHost({
27
+ db: level.sublevel('automerge'),
28
+ });
29
+ await host.open();
25
30
  afterTest(() => host.close());
31
+
26
32
  const dataService = new DataServiceImpl(host);
27
33
  const client = new AutomergeContext(dataService);
28
34
  afterTest(() => client.close());
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './client-rpc-server';
6
- export * from './diagnostics';
7
6
  export * from './service-context';
8
7
  export * from './service-host';
9
8
  export * from './service-registry';
9
+ export { ClientServicesProviderResource } from './util';
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { MemorySignalManagerContext } from '@dxos/messaging';
6
6
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
7
- import { describe, test } from '@dxos/test';
7
+ import { describe, openAndClose, test } from '@dxos/test';
8
8
 
9
9
  import { createServiceContext } from '../testing';
10
10
  import { performInvitation } from '../testing/invitation-utils';
@@ -27,12 +27,15 @@ describe('services/ServiceContext', () => {
27
27
  test('joined space is synchronized on device invitations', async () => {
28
28
  const networkContext = new MemorySignalManagerContext();
29
29
  const device1 = await createServiceContext({ signalContext: networkContext });
30
+ await openAndClose(device1.automergeHost);
30
31
  await device1.createIdentity();
31
32
 
32
33
  const device2 = await createServiceContext({ signalContext: networkContext });
34
+ await openAndClose(device2.automergeHost);
33
35
  await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
34
36
 
35
37
  const identity2 = await createServiceContext({ signalContext: networkContext });
38
+ await openAndClose(identity2.automergeHost);
36
39
  await identity2.createIdentity();
37
40
  const space1 = await identity2.dataSpaceManager!.createSpace();
38
41
  await Promise.all(
@@ -10,7 +10,7 @@ import { getCredentialAssertion, type CredentialProcessor } from '@dxos/credenti
10
10
  import { failUndefined } from '@dxos/debug';
11
11
  import { AutomergeHost, MetadataStore, SnapshotStore, SpaceManager, valueEncoding } from '@dxos/echo-pipeline';
12
12
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
13
- import { IndexMetadataStore, IndexStore, Indexer } from '@dxos/indexing';
13
+ import { IndexMetadataStore, IndexStore, Indexer, createStorageCallbacks } from '@dxos/indexing';
14
14
  import { invariant } from '@dxos/invariant';
15
15
  import { Keyring } from '@dxos/keyring';
16
16
  import { PublicKey } from '@dxos/keys';
@@ -32,13 +32,14 @@ import {
32
32
  type IdentityManagerRuntimeParams,
33
33
  type JoinIdentityParams,
34
34
  } from '../identity';
35
- import { createDocumentsIterator, createSelectedDocumentsIterator } from '../indexing';
35
+ import { createSelectedDocumentsIterator } from '../indexing';
36
36
  import {
37
37
  DeviceInvitationProtocol,
38
38
  InvitationsHandler,
39
39
  SpaceInvitationProtocol,
40
40
  type InvitationProtocol,
41
41
  } from '../invitations';
42
+ import { InvitationsManager } from '../invitations/invitations-manager';
42
43
  import { DataSpaceManager, type DataSpaceManagerRuntimeParams, type SigningContext } from '../spaces';
43
44
 
44
45
  export type ServiceContextRuntimeParams = IdentityManagerRuntimeParams & DataSpaceManagerRuntimeParams;
@@ -62,6 +63,7 @@ export class ServiceContext extends Resource {
62
63
  public readonly spaceManager: SpaceManager;
63
64
  public readonly identityManager: IdentityManager;
64
65
  public readonly invitations: InvitationsHandler;
66
+ public readonly invitationsManager: InvitationsManager;
65
67
  public readonly automergeHost: AutomergeHost;
66
68
  public readonly indexMetadata: IndexMetadataStore;
67
69
  public readonly indexer: Indexer;
@@ -124,17 +126,23 @@ export class ServiceContext extends Resource {
124
126
 
125
127
  this.automergeHost = new AutomergeHost({
126
128
  directory: storage.createDirectory('automerge'),
127
- metadata: this.indexMetadata,
129
+ db: level.sublevel('automerge'),
130
+ storageCallbacks: createStorageCallbacks({ host: () => this.automergeHost, metadata: this.indexMetadata }),
128
131
  });
129
132
 
130
133
  this.indexer = new Indexer({
131
- indexStore: new IndexStore({ directory: storage.createDirectory('index-store') }),
134
+ db: this.level,
135
+ indexStore: new IndexStore({ db: level.sublevel('index-storage') }),
132
136
  metadataStore: this.indexMetadata,
133
137
  loadDocuments: createSelectedDocumentsIterator(this.automergeHost),
134
- getAllDocuments: createDocumentsIterator(this.automergeHost),
135
138
  });
136
139
 
137
140
  this.invitations = new InvitationsHandler(this.networkManager);
141
+ this.invitationsManager = new InvitationsManager(
142
+ this.invitations,
143
+ (invitation) => this.getInvitationHandler(invitation),
144
+ this.metadataStore,
145
+ );
138
146
 
139
147
  // TODO(burdon): _initialize called in multiple places.
140
148
  // TODO(burdon): Call _initialize on success.
@@ -158,12 +166,17 @@ export class ServiceContext extends Resource {
158
166
  await this.signalManager.open();
159
167
  await this.networkManager.open();
160
168
 
169
+ await this.automergeHost.open();
161
170
  await this.metadataStore.load();
162
171
  await this.spaceManager.open();
163
172
  await this.identityManager.open(ctx);
164
173
  if (this.identityManager.identity) {
165
174
  await this._initialize(ctx);
166
175
  }
176
+
177
+ const loadedInvitations = await this.invitationsManager.loadPersistentInvitations();
178
+ log('loaded persistent invitations', { count: loadedInvitations.invitations?.length });
179
+
167
180
  log.trace('dxos.sdk.service-context.open', trace.end({ id: this._instanceId }));
168
181
  log('opened');
169
182
  }
@@ -243,6 +256,7 @@ export class ServiceContext extends Resource {
243
256
  signingContext,
244
257
  this.feedStore,
245
258
  this.automergeHost,
259
+ this.invitationsManager,
246
260
  this._runtimeParams as DataSpaceManagerRuntimeParams,
247
261
  );
248
262
  await this.dataSpaceManager.open();
@@ -8,9 +8,15 @@ import { Event, synchronized } from '@dxos/async';
8
8
  import { clientServiceBundle, defaultKey, type ClientServices, Properties } from '@dxos/client-protocol';
9
9
  import { type Config } from '@dxos/config';
10
10
  import { Context } from '@dxos/context';
11
- import { DataServiceImpl, type ObjectStructure, encodeReference, type SpaceDoc } from '@dxos/echo-pipeline';
12
- import * as E from '@dxos/echo-schema';
13
- import { IndexServiceImpl } from '@dxos/indexing';
11
+ import {
12
+ DataServiceImpl,
13
+ type ObjectStructure,
14
+ encodeReference,
15
+ type SpaceDoc,
16
+ type LevelDB,
17
+ } from '@dxos/echo-pipeline';
18
+ import { getTypeReference } from '@dxos/echo-schema';
19
+ import { QueryServiceImpl } from '@dxos/indexing';
14
20
  import { invariant } from '@dxos/invariant';
15
21
  import { PublicKey } from '@dxos/keys';
16
22
  import { log } from '@dxos/log';
@@ -23,11 +29,15 @@ import { TRACE_PROCESSOR, trace as Trace } from '@dxos/tracing';
23
29
  import { assignDeep } from '@dxos/util';
24
30
  import { WebsocketRpcClient } from '@dxos/websocket-rpc';
25
31
 
26
- import { createDiagnostics } from './diagnostics';
27
32
  import { ServiceContext, type ServiceContextRuntimeParams } from './service-context';
28
33
  import { ServiceRegistry } from './service-registry';
29
34
  import { DevicesServiceImpl } from '../devices';
30
35
  import { DevtoolsHostEvents, DevtoolsServiceImpl } from '../devtools';
36
+ import {
37
+ type CollectDiagnosticsBroadcastHandler,
38
+ createCollectDiagnosticsBroadcastHandler,
39
+ createDiagnostics,
40
+ } from '../diagnostics';
31
41
  import { IdentityServiceImpl, type CreateIdentityOptions } from '../identity';
32
42
  import { InvitationsServiceImpl } from '../invitations';
33
43
  import { Lock, type ResourceLock } from '../locks';
@@ -46,6 +56,7 @@ export type ClientServicesHostParams = {
46
56
  signalManager?: SignalManager;
47
57
  connectionLog?: boolean;
48
58
  storage?: Storage;
59
+ level?: LevelDB;
49
60
  lockKey?: string;
50
61
  callbacks?: ClientServicesHostCallbacks;
51
62
  runtimeParams?: ServiceContextRuntimeParams;
@@ -72,6 +83,7 @@ export class ClientServicesHost {
72
83
  private readonly _systemService: SystemServiceImpl;
73
84
  private readonly _loggingService: LoggingServiceImpl;
74
85
  private readonly _tracingService = TRACE_PROCESSOR.createTraceSender();
86
+ private _queryService!: QueryServiceImpl;
75
87
 
76
88
  private _config?: Config;
77
89
  private readonly _statusUpdate = new Event<void>();
@@ -84,6 +96,7 @@ export class ClientServicesHost {
84
96
 
85
97
  private _serviceContext!: ServiceContext;
86
98
  private readonly _runtimeParams?: ServiceContextRuntimeParams;
99
+ private diagnosticsBroadcastHandler: CollectDiagnosticsBroadcastHandler;
87
100
 
88
101
  @Trace.info()
89
102
  private _opening = false;
@@ -96,12 +109,14 @@ export class ClientServicesHost {
96
109
  transportFactory,
97
110
  signalManager,
98
111
  storage,
112
+ level,
99
113
  // TODO(wittjosiah): Turn this on by default.
100
114
  lockKey,
101
115
  callbacks,
102
116
  runtimeParams,
103
117
  }: ClientServicesHostParams = {}) {
104
118
  this._storage = storage;
119
+ this._level = level;
105
120
  this._callbacks = callbacks;
106
121
  this._runtimeParams = runtimeParams;
107
122
 
@@ -141,6 +156,7 @@ export class ClientServicesHost {
141
156
  },
142
157
  });
143
158
 
159
+ this.diagnosticsBroadcastHandler = createCollectDiagnosticsBroadcastHandler(this._systemService);
144
160
  this._loggingService = new LoggingServiceImpl();
145
161
 
146
162
  this._serviceRegistry = new ServiceRegistry<ClientServices>(clientServiceBundle, {
@@ -233,6 +249,8 @@ export class ClientServicesHost {
233
249
  if (!this._level) {
234
250
  this._level = await createLevel(this._config.get('runtime.client.storage', {})!);
235
251
  }
252
+ await this._level.open();
253
+
236
254
  await this._resourceLock?.acquire();
237
255
 
238
256
  await this._loggingService.open();
@@ -245,6 +263,12 @@ export class ClientServicesHost {
245
263
  this._runtimeParams,
246
264
  );
247
265
 
266
+ this._queryService = new QueryServiceImpl({
267
+ indexer: this._serviceContext.indexer,
268
+ automergeHost: this._serviceContext.automergeHost,
269
+ });
270
+ await this._queryService.open(ctx);
271
+
248
272
  this._serviceRegistry.setServices({
249
273
  SystemService: this._systemService,
250
274
 
@@ -255,11 +279,7 @@ export class ClientServicesHost {
255
279
  (profile) => this._serviceContext.broadcastProfileUpdate(profile),
256
280
  ),
257
281
 
258
- InvitationsService: new InvitationsServiceImpl(
259
- this._serviceContext.invitations,
260
- (invitation) => this._serviceContext.getInvitationHandler(invitation),
261
- this._serviceContext.metadataStore,
262
- ),
282
+ InvitationsService: new InvitationsServiceImpl(this._serviceContext.invitationsManager),
263
283
 
264
284
  DevicesService: new DevicesServiceImpl(this._serviceContext.identityManager),
265
285
 
@@ -274,10 +294,7 @@ export class ClientServicesHost {
274
294
 
275
295
  DataService: new DataServiceImpl(this._serviceContext.automergeHost),
276
296
 
277
- IndexService: new IndexServiceImpl({
278
- indexer: this._serviceContext.indexer,
279
- automergeHost: this._serviceContext.automergeHost,
280
- }),
297
+ QueryService: this._queryService,
281
298
 
282
299
  NetworkService: new NetworkServiceImpl(this._serviceContext.networkManager, this._serviceContext.signalManager),
283
300
 
@@ -293,11 +310,6 @@ export class ClientServicesHost {
293
310
  });
294
311
 
295
312
  await this._serviceContext.open(ctx);
296
- // TODO(nf): move to InvitationManager in ServiceContext?
297
- invariant(this.serviceRegistry.services.InvitationsService);
298
- const loadedInvitations = await this.serviceRegistry.services.InvitationsService.loadPersistentInvitations();
299
-
300
- log('loaded persistent invitations', { count: loadedInvitations.invitations?.length });
301
313
 
302
314
  const devtoolsProxy = this._config?.get('runtime.client.devtoolsProxy');
303
315
  if (devtoolsProxy) {
@@ -309,6 +321,7 @@ export class ClientServicesHost {
309
321
  });
310
322
  void this._devtoolsProxy.open();
311
323
  }
324
+ this.diagnosticsBroadcastHandler.start();
312
325
 
313
326
  this._opening = false;
314
327
  this._open = true;
@@ -327,9 +340,11 @@ export class ClientServicesHost {
327
340
 
328
341
  const deviceKey = this._serviceContext.identityManager.identity?.deviceKey;
329
342
  log('closing...', { deviceKey });
343
+ this.diagnosticsBroadcastHandler.stop();
330
344
  await this._devtoolsProxy?.close();
331
345
  this._serviceRegistry.setServices({ SystemService: this._systemService });
332
346
  await this._loggingService.close();
347
+ await this._queryService.close();
333
348
  await this._serviceContext.close();
334
349
  await this._level?.close();
335
350
  this._open = false;
@@ -364,7 +379,7 @@ export class ClientServicesHost {
364
379
  // TODO(dmaretskyi): Better API for low-level data access.
365
380
  const properties: ObjectStructure = {
366
381
  system: {
367
- type: encodeReference(E.getTypeReference(Properties)!),
382
+ type: encodeReference(getTypeReference(Properties)!),
368
383
  },
369
384
  data: {
370
385
  [defaultKey]: identity.identityKey.toHex(),