@dxos/client-services 0.4.10-main.c42bfdb → 0.4.10-main.c61b011

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 (77) hide show
  1. package/dist/lib/browser/{chunk-W7UANCHR.mjs → chunk-ZWMG6QKN.mjs} +831 -706
  2. package/dist/lib/browser/chunk-ZWMG6QKN.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +10 -6
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +138 -118
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-JSVLZGJM.cjs → chunk-PP42CXTU.cjs} +862 -734
  9. package/dist/lib/node/chunk-PP42CXTU.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +50 -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/packlets/testing/index.cjs +137 -120
  14. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  15. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  16. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +3 -1
  17. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  18. package/dist/types/src/packlets/invitations/index.d.ts +1 -0
  19. package/dist/types/src/packlets/invitations/index.d.ts.map +1 -1
  20. package/dist/types/src/packlets/invitations/invitation-extension.d.ts +1 -0
  21. package/dist/types/src/packlets/invitations/invitation-extension.d.ts.map +1 -1
  22. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +6 -1
  23. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  24. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -4
  25. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  26. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +44 -0
  27. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -0
  28. package/dist/types/src/packlets/invitations/invitations-service.d.ts +7 -23
  29. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  30. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +2 -1
  31. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  32. package/dist/types/src/packlets/services/service-context.d.ts +6 -6
  33. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  34. package/dist/types/src/packlets/services/service-host.d.ts +3 -1
  35. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  36. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +8 -3
  37. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  38. package/dist/types/src/packlets/spaces/data-space.d.ts +4 -3
  39. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  40. package/dist/types/src/packlets/storage/level.d.ts.map +1 -1
  41. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  42. package/dist/types/src/packlets/testing/test-builder.d.ts +9 -3
  43. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  44. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  45. package/dist/types/src/version.d.ts +1 -1
  46. package/package.json +34 -34
  47. package/src/packlets/identity/identity-manager.ts +1 -0
  48. package/src/packlets/identity/identity.test.ts +3 -0
  49. package/src/packlets/invitations/device-invitation-protocol.ts +6 -1
  50. package/src/packlets/invitations/index.ts +1 -0
  51. package/src/packlets/invitations/invitation-extension.ts +28 -1
  52. package/src/packlets/invitations/invitation-protocol.ts +7 -1
  53. package/src/packlets/invitations/invitations-handler.ts +75 -96
  54. package/src/packlets/invitations/invitations-manager.ts +271 -0
  55. package/src/packlets/invitations/invitations-service.ts +23 -168
  56. package/src/packlets/invitations/space-invitation-protocol.ts +45 -3
  57. package/src/packlets/services/automerge-host.test.ts +13 -7
  58. package/src/packlets/services/service-context.test.ts +4 -1
  59. package/src/packlets/services/service-context.ts +23 -23
  60. package/src/packlets/services/service-host.test.ts +6 -0
  61. package/src/packlets/services/service-host.ts +13 -22
  62. package/src/packlets/spaces/data-space-manager.test.ts +4 -4
  63. package/src/packlets/spaces/data-space-manager.ts +56 -13
  64. package/src/packlets/spaces/data-space.ts +14 -19
  65. package/src/packlets/storage/level.ts +2 -1
  66. package/src/packlets/testing/invitation-utils.ts +100 -97
  67. package/src/packlets/testing/test-builder.ts +37 -8
  68. package/src/packlets/vault/worker-runtime.ts +3 -1
  69. package/src/version.ts +1 -1
  70. package/dist/lib/browser/chunk-W7UANCHR.mjs.map +0 -7
  71. package/dist/lib/node/chunk-JSVLZGJM.cjs.map +0 -7
  72. package/dist/types/src/packlets/indexing/index.d.ts +0 -2
  73. package/dist/types/src/packlets/indexing/index.d.ts.map +0 -1
  74. package/dist/types/src/packlets/indexing/util.d.ts +0 -15
  75. package/dist/types/src/packlets/indexing/util.d.ts.map +0 -1
  76. package/src/packlets/indexing/index.ts +0 -5
  77. package/src/packlets/indexing/util.ts +0 -89
@@ -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.');
@@ -4,10 +4,11 @@
4
4
 
5
5
  import { expect } from 'chai';
6
6
 
7
- import { asyncTimeout, sleep } from '@dxos/async';
7
+ import { asyncTimeout } from '@dxos/async';
8
+ import { getHeads } from '@dxos/automerge/automerge';
9
+ import { AutomergeContext } from '@dxos/echo-db';
8
10
  import { AutomergeHost, DataServiceImpl } from '@dxos/echo-pipeline';
9
- import { AutomergeContext } from '@dxos/echo-schema';
10
- import { StorageType, createStorage } from '@dxos/random-access-storage';
11
+ import { createTestLevel } from '@dxos/echo-pipeline/testing';
11
12
  import { afterTest, describe, test } from '@dxos/test';
12
13
 
13
14
  describe('AutomergeHost', () => {
@@ -19,10 +20,16 @@ describe('AutomergeHost', () => {
19
20
  // creates repo and document | replicates repo | finds document in repo
20
21
  //
21
22
 
22
- const storageDirectory = createStorage({ type: StorageType.RAM }).createDirectory();
23
+ const level = createTestLevel();
24
+ await level.open();
25
+ afterTest(() => level.close());
23
26
 
24
- const host = new AutomergeHost({ directory: storageDirectory });
27
+ const host = new AutomergeHost({
28
+ db: level.sublevel('automerge'),
29
+ });
30
+ await host.open();
25
31
  afterTest(() => host.close());
32
+
26
33
  const dataService = new DataServiceImpl(host);
27
34
  const client = new AutomergeContext(dataService);
28
35
  afterTest(() => client.close());
@@ -44,9 +51,8 @@ describe('AutomergeHost', () => {
44
51
  doc.change((doc: any) => {
45
52
  doc.text = newText;
46
53
  });
54
+ await client.flush({ states: [{ documentId: doc.documentId, heads: getHeads(doc.docSync()) }] });
47
55
 
48
- // TODO(mykola): Is there a way to know when automerge has started replication?
49
- await sleep(100);
50
56
  await asyncTimeout(handle.whenReady(), 1_000);
51
57
  expect(handle.docSync().text).to.equal(newText);
52
58
  });
@@ -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.echoHost);
30
31
  await device1.createIdentity();
31
32
 
32
33
  const device2 = await createServiceContext({ signalContext: networkContext });
34
+ await openAndClose(device2.echoHost);
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.echoHost);
36
39
  await identity2.createIdentity();
37
40
  const space1 = await identity2.dataSpaceManager!.createSpace();
38
41
  await Promise.all(
@@ -8,9 +8,9 @@ import { Trigger } from '@dxos/async';
8
8
  import { Context, Resource } from '@dxos/context';
9
9
  import { getCredentialAssertion, type CredentialProcessor } from '@dxos/credentials';
10
10
  import { failUndefined } from '@dxos/debug';
11
- import { AutomergeHost, MetadataStore, SnapshotStore, SpaceManager, valueEncoding } from '@dxos/echo-pipeline';
11
+ import { EchoHost } from '@dxos/echo-db';
12
+ import { MetadataStore, SnapshotStore, SpaceManager, valueEncoding } from '@dxos/echo-pipeline';
12
13
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
13
- import { IndexMetadataStore, IndexStore, Indexer } 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,13 @@ import {
32
32
  type IdentityManagerRuntimeParams,
33
33
  type JoinIdentityParams,
34
34
  } from '../identity';
35
- import { createDocumentsIterator, createSelectedDocumentsIterator } from '../indexing';
36
35
  import {
37
36
  DeviceInvitationProtocol,
38
37
  InvitationsHandler,
39
38
  SpaceInvitationProtocol,
40
39
  type InvitationProtocol,
41
40
  } from '../invitations';
41
+ import { InvitationsManager } from '../invitations/invitations-manager';
42
42
  import { DataSpaceManager, type DataSpaceManagerRuntimeParams, type SigningContext } from '../spaces';
43
43
 
44
44
  export type ServiceContextRuntimeParams = IdentityManagerRuntimeParams & DataSpaceManagerRuntimeParams;
@@ -62,9 +62,8 @@ export class ServiceContext extends Resource {
62
62
  public readonly spaceManager: SpaceManager;
63
63
  public readonly identityManager: IdentityManager;
64
64
  public readonly invitations: InvitationsHandler;
65
- public readonly automergeHost: AutomergeHost;
66
- public readonly indexMetadata: IndexMetadataStore;
67
- public readonly indexer: Indexer;
65
+ public readonly invitationsManager: InvitationsManager;
66
+ public readonly echoHost: EchoHost;
68
67
 
69
68
  // Initialized after identity is initialized.
70
69
  public dataSpaceManager?: DataSpaceManager;
@@ -120,21 +119,17 @@ export class ServiceContext extends Resource {
120
119
  this._runtimeParams as IdentityManagerRuntimeParams,
121
120
  );
122
121
 
123
- this.indexMetadata = new IndexMetadataStore({ db: level.sublevel('index-metadata') });
124
-
125
- this.automergeHost = new AutomergeHost({
126
- directory: storage.createDirectory('automerge'),
127
- metadata: this.indexMetadata,
128
- });
129
-
130
- this.indexer = new Indexer({
131
- indexStore: new IndexStore({ directory: storage.createDirectory('index-store') }),
132
- metadataStore: this.indexMetadata,
133
- loadDocuments: createSelectedDocumentsIterator(this.automergeHost),
134
- getAllDocuments: createDocumentsIterator(this.automergeHost),
122
+ this.echoHost = new EchoHost({
123
+ kv: this.level,
124
+ storage: this.storage,
135
125
  });
136
126
 
137
127
  this.invitations = new InvitationsHandler(this.networkManager);
128
+ this.invitationsManager = new InvitationsManager(
129
+ this.invitations,
130
+ (invitation) => this.getInvitationHandler(invitation),
131
+ this.metadataStore,
132
+ );
138
133
 
139
134
  // TODO(burdon): _initialize called in multiple places.
140
135
  // TODO(burdon): Call _initialize on success.
@@ -158,30 +153,34 @@ export class ServiceContext extends Resource {
158
153
  await this.signalManager.open();
159
154
  await this.networkManager.open();
160
155
 
156
+ await this.echoHost.open(ctx);
161
157
  await this.metadataStore.load();
162
158
  await this.spaceManager.open();
163
159
  await this.identityManager.open(ctx);
164
160
  if (this.identityManager.identity) {
165
161
  await this._initialize(ctx);
166
162
  }
163
+
164
+ const loadedInvitations = await this.invitationsManager.loadPersistentInvitations();
165
+ log('loaded persistent invitations', { count: loadedInvitations.invitations?.length });
166
+
167
167
  log.trace('dxos.sdk.service-context.open', trace.end({ id: this._instanceId }));
168
168
  log('opened');
169
169
  }
170
170
 
171
- protected override async _close() {
171
+ protected override async _close(ctx: Context) {
172
172
  log('closing...');
173
173
  if (this._deviceSpaceSync && this.identityManager.identity) {
174
174
  await this.identityManager.identity.space.spaceState.removeCredentialProcessor(this._deviceSpaceSync);
175
175
  }
176
- await this.automergeHost.close();
177
176
  await this.dataSpaceManager?.close();
178
177
  await this.identityManager.close();
179
178
  await this.spaceManager.close();
180
179
  await this.feedStore.close();
180
+ await this.metadataStore.close();
181
+ await this.echoHost.close(ctx);
181
182
  await this.networkManager.close();
182
183
  await this.signalManager.close();
183
- await this.metadataStore.close();
184
- await this.indexer.destroy();
185
184
  log('closed');
186
185
  }
187
186
 
@@ -242,7 +241,8 @@ export class ServiceContext extends Resource {
242
241
  this.keyring,
243
242
  signingContext,
244
243
  this.feedStore,
245
- this.automergeHost,
244
+ this.echoHost,
245
+ this.invitationsManager,
246
246
  this._runtimeParams as DataSpaceManagerRuntimeParams,
247
247
  );
248
248
  await this.dataSpaceManager.open();
@@ -29,6 +29,12 @@ describe('ClientServicesHost', () => {
29
29
  isNode() && rmSync(dataRoot, { recursive: true, force: true });
30
30
  });
31
31
 
32
+ test('open and close', async () => {
33
+ const host = createServiceHost(new Config(), new MemorySignalManagerContext());
34
+ await host.open(new Context());
35
+ await host.close();
36
+ });
37
+
32
38
  test('queryCredentials', async () => {
33
39
  const host = createServiceHost(new Config(), new MemorySignalManagerContext());
34
40
  await host.open(new Context());
@@ -8,9 +8,8 @@ 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 { type ObjectStructure, encodeReference, type SpaceDoc, type LevelDB } from '@dxos/echo-pipeline';
12
+ import { getTypeReference } from '@dxos/echo-schema';
14
13
  import { invariant } from '@dxos/invariant';
15
14
  import { PublicKey } from '@dxos/keys';
16
15
  import { log } from '@dxos/log';
@@ -50,6 +49,7 @@ export type ClientServicesHostParams = {
50
49
  signalManager?: SignalManager;
51
50
  connectionLog?: boolean;
52
51
  storage?: Storage;
52
+ level?: LevelDB;
53
53
  lockKey?: string;
54
54
  callbacks?: ClientServicesHostCallbacks;
55
55
  runtimeParams?: ServiceContextRuntimeParams;
@@ -101,12 +101,14 @@ export class ClientServicesHost {
101
101
  transportFactory,
102
102
  signalManager,
103
103
  storage,
104
+ level,
104
105
  // TODO(wittjosiah): Turn this on by default.
105
106
  lockKey,
106
107
  callbacks,
107
108
  runtimeParams,
108
109
  }: ClientServicesHostParams = {}) {
109
110
  this._storage = storage;
111
+ this._level = level;
110
112
  this._callbacks = callbacks;
111
113
  this._runtimeParams = runtimeParams;
112
114
 
@@ -239,6 +241,8 @@ export class ClientServicesHost {
239
241
  if (!this._level) {
240
242
  this._level = await createLevel(this._config.get('runtime.client.storage', {})!);
241
243
  }
244
+ await this._level.open();
245
+
242
246
  await this._resourceLock?.acquire();
243
247
 
244
248
  await this._loggingService.open();
@@ -261,11 +265,7 @@ export class ClientServicesHost {
261
265
  (profile) => this._serviceContext.broadcastProfileUpdate(profile),
262
266
  ),
263
267
 
264
- InvitationsService: new InvitationsServiceImpl(
265
- this._serviceContext.invitations,
266
- (invitation) => this._serviceContext.getInvitationHandler(invitation),
267
- this._serviceContext.metadataStore,
268
- ),
268
+ InvitationsService: new InvitationsServiceImpl(this._serviceContext.invitationsManager),
269
269
 
270
270
  DevicesService: new DevicesServiceImpl(this._serviceContext.identityManager),
271
271
 
@@ -278,12 +278,8 @@ export class ClientServicesHost {
278
278
  },
279
279
  ),
280
280
 
281
- DataService: new DataServiceImpl(this._serviceContext.automergeHost),
282
-
283
- IndexService: new IndexServiceImpl({
284
- indexer: this._serviceContext.indexer,
285
- automergeHost: this._serviceContext.automergeHost,
286
- }),
281
+ DataService: this._serviceContext.echoHost.dataService,
282
+ QueryService: this._serviceContext.echoHost.queryService,
287
283
 
288
284
  NetworkService: new NetworkServiceImpl(this._serviceContext.networkManager, this._serviceContext.signalManager),
289
285
 
@@ -299,11 +295,6 @@ export class ClientServicesHost {
299
295
  });
300
296
 
301
297
  await this._serviceContext.open(ctx);
302
- // TODO(nf): move to InvitationManager in ServiceContext?
303
- invariant(this.serviceRegistry.services.InvitationsService);
304
- const loadedInvitations = await this.serviceRegistry.services.InvitationsService.loadPersistentInvitations();
305
-
306
- log('loaded persistent invitations', { count: loadedInvitations.invitations?.length });
307
298
 
308
299
  const devtoolsProxy = this._config?.get('runtime.client.devtoolsProxy');
309
300
  if (devtoolsProxy) {
@@ -366,13 +357,13 @@ export class ClientServicesHost {
366
357
 
367
358
  const automergeIndex = space.automergeSpaceState.rootUrl;
368
359
  invariant(automergeIndex);
369
- const document = await this._serviceContext.automergeHost.repo.find<SpaceDoc>(automergeIndex as any);
360
+ const document = await this._serviceContext.echoHost.automergeRepo.find<SpaceDoc>(automergeIndex as any);
370
361
  await document.whenReady();
371
362
 
372
363
  // TODO(dmaretskyi): Better API for low-level data access.
373
364
  const properties: ObjectStructure = {
374
365
  system: {
375
- type: encodeReference(E.getTypeReference(Properties)!),
366
+ type: encodeReference(getTypeReference(Properties)!),
376
367
  },
377
368
  data: {
378
369
  [defaultKey]: identity.identityKey.toHex(),
@@ -386,7 +377,7 @@ export class ClientServicesHost {
386
377
  assignDeep(doc, ['objects', propertiesId], properties);
387
378
  });
388
379
 
389
- await this._serviceContext.automergeHost.repo.flush();
380
+ await this._serviceContext.echoHost.flush();
390
381
 
391
382
  return identity;
392
383
  }