@dxos/client-services 0.4.10-main.c75170d → 0.4.10-main.c8e5c39

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 (69) hide show
  1. package/dist/lib/browser/{chunk-JP7F2IH3.mjs → chunk-7OKNHCYB.mjs} +425 -410
  2. package/dist/lib/browser/chunk-7OKNHCYB.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +6 -4
  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 +131 -116
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-34EZSH65.cjs → chunk-5JA576YH.cjs} +527 -508
  9. package/dist/lib/node/chunk-5JA576YH.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +48 -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 +130 -118
  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/invitation-protocol.d.ts +6 -1
  19. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  20. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +4 -2
  21. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  22. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +9 -7
  23. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
  24. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  25. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +2 -1
  26. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  27. package/dist/types/src/packlets/services/service-context.d.ts +4 -6
  28. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  29. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  30. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +8 -3
  31. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  32. package/dist/types/src/packlets/spaces/data-space.d.ts +4 -3
  33. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  34. package/dist/types/src/packlets/storage/level.d.ts.map +1 -1
  35. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  36. package/dist/types/src/packlets/testing/test-builder.d.ts +7 -3
  37. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  38. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  39. package/dist/types/src/version.d.ts +1 -1
  40. package/package.json +34 -34
  41. package/src/packlets/identity/identity-manager.ts +1 -0
  42. package/src/packlets/identity/identity.test.ts +3 -0
  43. package/src/packlets/invitations/device-invitation-protocol.ts +6 -1
  44. package/src/packlets/invitations/invitation-protocol.ts +7 -1
  45. package/src/packlets/invitations/invitations-handler.ts +10 -71
  46. package/src/packlets/invitations/invitations-manager.ts +114 -40
  47. package/src/packlets/invitations/invitations-service.ts +4 -2
  48. package/src/packlets/invitations/space-invitation-protocol.ts +45 -3
  49. package/src/packlets/services/automerge-host.test.ts +4 -4
  50. package/src/packlets/services/service-context.test.ts +3 -3
  51. package/src/packlets/services/service-context.ts +12 -25
  52. package/src/packlets/services/service-host.test.ts +6 -0
  53. package/src/packlets/services/service-host.ts +5 -16
  54. package/src/packlets/spaces/data-space-manager.test.ts +4 -4
  55. package/src/packlets/spaces/data-space-manager.ts +56 -13
  56. package/src/packlets/spaces/data-space.ts +14 -19
  57. package/src/packlets/storage/level.ts +1 -0
  58. package/src/packlets/testing/invitation-utils.ts +100 -97
  59. package/src/packlets/testing/test-builder.ts +27 -14
  60. package/src/packlets/vault/worker-runtime.ts +3 -1
  61. package/src/version.ts +1 -1
  62. package/dist/lib/browser/chunk-JP7F2IH3.mjs.map +0 -7
  63. package/dist/lib/node/chunk-34EZSH65.cjs.map +0 -7
  64. package/dist/types/src/packlets/indexing/index.d.ts +0 -2
  65. package/dist/types/src/packlets/indexing/index.d.ts.map +0 -1
  66. package/dist/types/src/packlets/indexing/util.d.ts +0 -16
  67. package/dist/types/src/packlets/indexing/util.d.ts.map +0 -1
  68. package/src/packlets/indexing/index.ts +0 -5
  69. package/src/packlets/indexing/util.ts +0 -89
@@ -2,20 +2,27 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Event } from '@dxos/async';
6
- import type { AuthenticatingInvitation, CancellableInvitation } from '@dxos/client-protocol';
7
- import { type Context } from '@dxos/context';
5
+ import { Event, PushStream } from '@dxos/async';
6
+ import {
7
+ type AuthenticatingInvitation,
8
+ AUTHENTICATION_CODE_LENGTH,
9
+ CancellableInvitation,
10
+ INVITATION_TIMEOUT,
11
+ } from '@dxos/client-protocol';
12
+ import { Context } from '@dxos/context';
13
+ import { generatePasscode } from '@dxos/credentials';
8
14
  import { hasInvitationExpired, type MetadataStore } from '@dxos/echo-pipeline';
9
15
  import { invariant } from '@dxos/invariant';
16
+ import { PublicKey } from '@dxos/keys';
10
17
  import { log } from '@dxos/log';
11
18
  import {
12
19
  type AcceptInvitationRequest,
13
- type Invitation,
14
20
  type AuthenticationRequest,
21
+ Invitation,
15
22
  } from '@dxos/protocols/proto/dxos/client/services';
16
23
 
17
24
  import type { InvitationProtocol } from './invitation-protocol';
18
- import type { InvitationsHandler } from './invitations-handler';
25
+ import { createAdmissionKeypair, type InvitationsHandler } from './invitations-handler';
19
26
 
20
27
  /**
21
28
  * Entry point for creating and accepting invitations, keeps track of existing invitation set and
@@ -36,36 +43,44 @@ export class InvitationsManager {
36
43
 
37
44
  constructor(
38
45
  private readonly _invitationsHandler: InvitationsHandler,
39
- private readonly _getHandler: (invitation: Invitation) => InvitationProtocol,
46
+ private readonly _getHandler: (invitation: Partial<Invitation> & Pick<Invitation, 'kind'>) => InvitationProtocol,
40
47
  private readonly _metadataStore: MetadataStore,
41
48
  ) {}
42
49
 
43
- createInvitation(options: Invitation): CancellableInvitation {
44
- const existingInvitation = this._createInvitations.get(options.invitationId);
45
- if (existingInvitation) {
46
- return existingInvitation;
50
+ async createInvitation(options: Partial<Invitation> & Pick<Invitation, 'kind'>): Promise<CancellableInvitation> {
51
+ if (options.invitationId) {
52
+ const existingInvitation = this._createInvitations.get(options.invitationId);
53
+ if (existingInvitation) {
54
+ return existingInvitation;
55
+ }
47
56
  }
48
57
 
49
58
  const handler = this._getHandler(options);
50
- const invitation = this._invitationsHandler.createInvitation(handler, options);
51
- this._createInvitations.set(invitation.get().invitationId, invitation);
52
- this.invitationCreated.emit(invitation.get());
53
-
54
- const saveInvitationTask = invitation.get().persistent
55
- ? this._safePersistInBackground(invitation)
56
- : Promise.resolve();
59
+ const invitation = this._createInvitation(handler, options);
60
+ const { ctx, stream, observableInvitation } = this._createObservableInvitation(handler, invitation);
57
61
 
62
+ this._createInvitations.set(invitation.invitationId, observableInvitation);
63
+ this.invitationCreated.emit(invitation);
58
64
  // onComplete is called on cancel, expiration, or redemption of a single-use invitation
59
- this._onInvitationComplete(invitation, async () => {
60
- this._createInvitations.delete(invitation.get().invitationId);
61
- this.removedCreated.emit(invitation.get());
62
- if (invitation.get().persistent) {
63
- await saveInvitationTask;
64
- await this._safeDeleteInvitation(invitation.get());
65
+ this._onInvitationComplete(observableInvitation, async () => {
66
+ this._createInvitations.delete(observableInvitation.get().invitationId);
67
+ this.removedCreated.emit(observableInvitation.get());
68
+ if (observableInvitation.get().persistent) {
69
+ await this._safeDeleteInvitation(observableInvitation.get());
65
70
  }
66
71
  });
67
72
 
68
- return invitation;
73
+ try {
74
+ await this._persistIfRequired(handler, stream, invitation);
75
+ } catch (err) {
76
+ log.catch(err);
77
+ await observableInvitation.cancel();
78
+ return observableInvitation;
79
+ }
80
+
81
+ this._invitationsHandler.handleInvitationFlow(ctx, stream, handler, observableInvitation.get());
82
+
83
+ return observableInvitation;
69
84
  }
70
85
 
71
86
  async loadPersistentInvitations(): Promise<{ invitations: Invitation[] }> {
@@ -78,12 +93,13 @@ export class InvitationsManager {
78
93
  // get saved persistent invitations, filter and remove from storage those that have expired.
79
94
  const freshInvitations = persistentInvitations.filter((invitation) => !hasInvitationExpired(invitation));
80
95
 
81
- const cInvitations = freshInvitations.map((persistentInvitation) => {
96
+ const loadTasks = freshInvitations.map((persistentInvitation) => {
82
97
  invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
83
- return this.createInvitation({ ...persistentInvitation, persistent: false }).get();
98
+ return this.createInvitation({ ...persistentInvitation, persistent: false });
84
99
  });
100
+ const cInvitations = await Promise.all(loadTasks);
85
101
 
86
- return { invitations: cInvitations };
102
+ return { invitations: cInvitations.map((invitation) => invitation.get()) };
87
103
  } catch (err) {
88
104
  log.catch(err);
89
105
  return { invitations: [] };
@@ -163,20 +179,78 @@ export class InvitationsManager {
163
179
  }
164
180
  }
165
181
 
166
- private _safePersistInBackground(invitation: CancellableInvitation): Promise<void> {
167
- return new Promise((resolve) => {
168
- setTimeout(async () => {
169
- try {
170
- await this._metadataStore.addInvitation(invitation.get());
171
- this.saved.emit(invitation.get());
172
- } catch (err: any) {
173
- log.catch(err);
174
- await invitation.cancel();
175
- } finally {
176
- resolve();
177
- }
178
- });
182
+ private _createInvitation(protocol: InvitationProtocol, options?: Partial<Invitation>): Invitation {
183
+ const {
184
+ invitationId = PublicKey.random().toHex(),
185
+ type = Invitation.Type.INTERACTIVE,
186
+ authMethod = Invitation.AuthMethod.SHARED_SECRET,
187
+ state = Invitation.State.INIT,
188
+ timeout = INVITATION_TIMEOUT,
189
+ swarmKey = PublicKey.random(),
190
+ persistent = options?.authMethod !== Invitation.AuthMethod.KNOWN_PUBLIC_KEY, // default no not storing keypairs
191
+ created = new Date(),
192
+ guestKeypair = undefined,
193
+ lifetime = 86400, // 1 day,
194
+ multiUse = false,
195
+ } = options ?? {};
196
+ const authCode =
197
+ options?.authCode ??
198
+ (authMethod === Invitation.AuthMethod.SHARED_SECRET ? generatePasscode(AUTHENTICATION_CODE_LENGTH) : undefined);
199
+
200
+ return {
201
+ invitationId,
202
+ type,
203
+ authMethod,
204
+ state,
205
+ swarmKey,
206
+ authCode,
207
+ timeout,
208
+ persistent: persistent && type !== Invitation.Type.DELEGATED, // delegated invitations are persisted in control feed
209
+ guestKeypair:
210
+ guestKeypair ?? (authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? createAdmissionKeypair() : undefined),
211
+ created,
212
+ lifetime,
213
+ multiUse,
214
+ delegationCredentialId: options?.delegationCredentialId,
215
+ ...protocol.getInvitationContext(),
216
+ } satisfies Invitation;
217
+ }
218
+
219
+ private _createObservableInvitation(handler: InvitationProtocol, invitation: Invitation) {
220
+ const stream = new PushStream<Invitation>();
221
+ const ctx = new Context({
222
+ onError: (err) => {
223
+ stream.error(err);
224
+ void ctx.dispose();
225
+ },
179
226
  });
227
+ ctx.onDispose(() => {
228
+ log('complete', { ...handler.toJSON() });
229
+ stream.complete();
230
+ });
231
+ const observableInvitation = new CancellableInvitation({
232
+ initialInvitation: invitation,
233
+ subscriber: stream.observable,
234
+ onCancel: async () => {
235
+ stream.next({ ...invitation, state: Invitation.State.CANCELLED });
236
+ await ctx.dispose();
237
+ },
238
+ });
239
+ return { ctx, stream, observableInvitation };
240
+ }
241
+
242
+ private async _persistIfRequired(
243
+ handler: InvitationProtocol,
244
+ changeStream: PushStream<Invitation>,
245
+ invitation: Invitation,
246
+ ): Promise<void> {
247
+ if (invitation.type === Invitation.Type.DELEGATED && invitation.delegationCredentialId == null) {
248
+ const delegationCredentialId = await handler.delegate(invitation);
249
+ changeStream.next({ ...invitation, delegationCredentialId });
250
+ } else if (invitation.persistent) {
251
+ await this._metadataStore.addInvitation(invitation);
252
+ this.saved.emit(invitation);
253
+ }
180
254
  }
181
255
 
182
256
  private async _safeDeleteInvitation(invitation: Invitation): Promise<void> {
@@ -27,9 +27,11 @@ export class InvitationsServiceImpl implements InvitationsService {
27
27
  }
28
28
 
29
29
  createInvitation(options: Invitation): Stream<Invitation> {
30
- const invitation = this._invitationsManager.createInvitation(options);
31
30
  return new Stream<Invitation>(({ next, close }) => {
32
- invitation.subscribe(next, close, close);
31
+ void this._invitationsManager
32
+ .createInvitation(options)
33
+ .then((invitation) => invitation.subscribe(next, close, close))
34
+ .catch(close);
33
35
  });
34
36
  }
35
37
 
@@ -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
11
  import { createTestLevel } from '@dxos/echo-pipeline/testing';
10
- import { AutomergeContext } from '@dxos/echo-schema';
11
12
  import { afterTest, describe, test } from '@dxos/test';
12
13
 
13
14
  describe('AutomergeHost', () => {
@@ -50,9 +51,8 @@ describe('AutomergeHost', () => {
50
51
  doc.change((doc: any) => {
51
52
  doc.text = newText;
52
53
  });
54
+ await client.flush({ states: [{ documentId: doc.documentId, heads: getHeads(doc.docSync()) }] });
53
55
 
54
- // TODO(mykola): Is there a way to know when automerge has started replication?
55
- await sleep(100);
56
56
  await asyncTimeout(handle.whenReady(), 1_000);
57
57
  expect(handle.docSync().text).to.equal(newText);
58
58
  });
@@ -27,15 +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
+ await openAndClose(device1.echoHost);
31
31
  await device1.createIdentity();
32
32
 
33
33
  const device2 = await createServiceContext({ signalContext: networkContext });
34
- await openAndClose(device2.automergeHost);
34
+ await openAndClose(device2.echoHost);
35
35
  await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
36
36
 
37
37
  const identity2 = await createServiceContext({ signalContext: networkContext });
38
- await openAndClose(identity2.automergeHost);
38
+ await openAndClose(identity2.echoHost);
39
39
  await identity2.createIdentity();
40
40
  const space1 = await identity2.dataSpaceManager!.createSpace();
41
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, 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,7 +32,6 @@ 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,
@@ -64,9 +63,7 @@ export class ServiceContext extends Resource {
64
63
  public readonly identityManager: IdentityManager;
65
64
  public readonly invitations: InvitationsHandler;
66
65
  public readonly invitationsManager: InvitationsManager;
67
- public readonly automergeHost: AutomergeHost;
68
- public readonly indexMetadata: IndexMetadataStore;
69
- public readonly indexer: Indexer;
66
+ public readonly echoHost: EchoHost;
70
67
 
71
68
  // Initialized after identity is initialized.
72
69
  public dataSpaceManager?: DataSpaceManager;
@@ -122,19 +119,9 @@ export class ServiceContext extends Resource {
122
119
  this._runtimeParams as IdentityManagerRuntimeParams,
123
120
  );
124
121
 
125
- this.indexMetadata = new IndexMetadataStore({ db: level.sublevel('index-metadata') });
126
-
127
- this.automergeHost = new AutomergeHost({
128
- directory: storage.createDirectory('automerge'),
129
- db: level.sublevel('automerge'),
130
- storageCallbacks: createStorageCallbacks({ host: () => this.automergeHost, metadata: this.indexMetadata }),
131
- });
132
-
133
- this.indexer = new Indexer({
134
- indexStore: new IndexStore({ db: level.sublevel('index-storage') }),
135
- metadataStore: this.indexMetadata,
136
- loadDocuments: createSelectedDocumentsIterator(this.automergeHost),
137
- getAllDocuments: createDocumentsIterator(this.automergeHost),
122
+ this.echoHost = new EchoHost({
123
+ kv: this.level,
124
+ storage: this.storage,
138
125
  });
139
126
 
140
127
  this.invitations = new InvitationsHandler(this.networkManager);
@@ -166,7 +153,7 @@ export class ServiceContext extends Resource {
166
153
  await this.signalManager.open();
167
154
  await this.networkManager.open();
168
155
 
169
- await this.automergeHost.open();
156
+ await this.echoHost.open(ctx);
170
157
  await this.metadataStore.load();
171
158
  await this.spaceManager.open();
172
159
  await this.identityManager.open(ctx);
@@ -181,20 +168,19 @@ export class ServiceContext extends Resource {
181
168
  log('opened');
182
169
  }
183
170
 
184
- protected override async _close() {
171
+ protected override async _close(ctx: Context) {
185
172
  log('closing...');
186
173
  if (this._deviceSpaceSync && this.identityManager.identity) {
187
174
  await this.identityManager.identity.space.spaceState.removeCredentialProcessor(this._deviceSpaceSync);
188
175
  }
189
- await this.automergeHost.close();
190
176
  await this.dataSpaceManager?.close();
191
177
  await this.identityManager.close();
192
178
  await this.spaceManager.close();
193
179
  await this.feedStore.close();
180
+ await this.metadataStore.close();
181
+ await this.echoHost.close(ctx);
194
182
  await this.networkManager.close();
195
183
  await this.signalManager.close();
196
- await this.metadataStore.close();
197
- await this.indexer.destroy();
198
184
  log('closed');
199
185
  }
200
186
 
@@ -255,7 +241,8 @@ export class ServiceContext extends Resource {
255
241
  this.keyring,
256
242
  signingContext,
257
243
  this.feedStore,
258
- this.automergeHost,
244
+ this.echoHost,
245
+ this.invitationsManager,
259
246
  this._runtimeParams as DataSpaceManagerRuntimeParams,
260
247
  );
261
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,15 +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 {
12
- DataServiceImpl,
13
- type ObjectStructure,
14
- encodeReference,
15
- type SpaceDoc,
16
- type LevelDB,
17
- } from '@dxos/echo-pipeline';
11
+ import { type ObjectStructure, encodeReference, type SpaceDoc, type LevelDB } from '@dxos/echo-pipeline';
18
12
  import { getTypeReference } from '@dxos/echo-schema';
19
- import { IndexServiceImpl } from '@dxos/indexing';
20
13
  import { invariant } from '@dxos/invariant';
21
14
  import { PublicKey } from '@dxos/keys';
22
15
  import { log } from '@dxos/log';
@@ -285,12 +278,8 @@ export class ClientServicesHost {
285
278
  },
286
279
  ),
287
280
 
288
- DataService: new DataServiceImpl(this._serviceContext.automergeHost),
289
-
290
- IndexService: new IndexServiceImpl({
291
- indexer: this._serviceContext.indexer,
292
- automergeHost: this._serviceContext.automergeHost,
293
- }),
281
+ DataService: this._serviceContext.echoHost.dataService,
282
+ QueryService: this._serviceContext.echoHost.queryService,
294
283
 
295
284
  NetworkService: new NetworkServiceImpl(this._serviceContext.networkManager, this._serviceContext.signalManager),
296
285
 
@@ -368,7 +357,7 @@ export class ClientServicesHost {
368
357
 
369
358
  const automergeIndex = space.automergeSpaceState.rootUrl;
370
359
  invariant(automergeIndex);
371
- 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);
372
361
  await document.whenReady();
373
362
 
374
363
  // TODO(dmaretskyi): Better API for low-level data access.
@@ -388,7 +377,7 @@ export class ClientServicesHost {
388
377
  assignDeep(doc, ['objects', propertiesId], properties);
389
378
  });
390
379
 
391
- await this._serviceContext.automergeHost.repo.flush();
380
+ await this._serviceContext.echoHost.flush();
392
381
 
393
382
  return identity;
394
383
  }
@@ -20,7 +20,7 @@ describe('DataSpaceManager', () => {
20
20
 
21
21
  const peer = builder.createPeer();
22
22
  await peer.createIdentity();
23
- await openAndClose(peer.automergeHost, peer.dataSpaceManager);
23
+ await openAndClose(peer.echoHost, peer.dataSpaceManager);
24
24
 
25
25
  const space = await peer.dataSpaceManager.createSpace();
26
26
 
@@ -42,7 +42,7 @@ describe('DataSpaceManager', () => {
42
42
  const peer2 = builder.createPeer();
43
43
  await peer2.createIdentity();
44
44
 
45
- await openAndClose(peer1.automergeHost, peer1.dataSpaceManager, peer2.automergeHost, peer2.dataSpaceManager);
45
+ await openAndClose(peer1.echoHost, peer1.dataSpaceManager, peer2.echoHost, peer2.dataSpaceManager);
46
46
 
47
47
  const space1 = await peer1.dataSpaceManager.createSpace();
48
48
  await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
@@ -111,7 +111,7 @@ describe('DataSpaceManager', () => {
111
111
  await peer2.createIdentity();
112
112
  await peer2.dataSpaceManager.open();
113
113
 
114
- await openAndClose(peer1.automergeHost, peer1.dataSpaceManager, peer2.automergeHost, peer2.dataSpaceManager);
114
+ await openAndClose(peer1.echoHost, peer1.dataSpaceManager, peer2.echoHost, peer2.dataSpaceManager);
115
115
 
116
116
  const space1 = await peer1.dataSpaceManager.createSpace();
117
117
  await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
@@ -153,7 +153,7 @@ describe('DataSpaceManager', () => {
153
153
 
154
154
  const peer = builder.createPeer();
155
155
  await peer.createIdentity();
156
- await openAndClose(peer.automergeHost, peer.dataSpaceManager);
156
+ await openAndClose(peer.echoHost, peer.dataSpaceManager);
157
157
 
158
158
  const space = await peer.dataSpaceManager.createSpace();
159
159
  await space.inner.controlPipeline.state.waitUntilTimeframe(space.inner.controlPipeline.state.endTimeframe);