@dxos/client-services 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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 (113) hide show
  1. package/dist/lib/browser/{chunk-J33W6T4Q.mjs → chunk-5A3KX2RY.mjs} +1745 -1208
  2. package/dist/lib/browser/chunk-5A3KX2RY.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +33 -16
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +14 -7
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node-esm/{chunk-34HKLADW.mjs → chunk-FNPO5UMU.mjs} +1745 -1208
  9. package/dist/lib/node-esm/chunk-FNPO5UMU.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +33 -16
  11. package/dist/lib/node-esm/index.mjs.map +3 -3
  12. package/dist/lib/node-esm/meta.json +1 -1
  13. package/dist/lib/node-esm/testing/index.mjs +14 -7
  14. package/dist/lib/node-esm/testing/index.mjs.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/agents/edge-agent-manager.d.ts +3 -2
  18. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -1
  19. package/dist/types/src/packlets/identity/identity-manager.d.ts +3 -3
  20. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  21. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +5 -4
  22. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -1
  23. package/dist/types/src/packlets/identity/identity-service.d.ts +1 -6
  24. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  25. package/dist/types/src/packlets/identity/identity.d.ts +6 -9
  26. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  27. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -1
  28. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +3 -3
  29. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
  30. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  31. package/dist/types/src/packlets/logging/logging-service.d.ts +4 -0
  32. package/dist/types/src/packlets/logging/logging-service.d.ts.map +1 -1
  33. package/dist/types/src/packlets/network/network-service.d.ts.map +1 -1
  34. package/dist/types/src/packlets/services/feed-syncer.d.ts +59 -0
  35. package/dist/types/src/packlets/services/feed-syncer.d.ts.map +1 -0
  36. package/dist/types/src/packlets/services/feed-syncer.test.d.ts +2 -0
  37. package/dist/types/src/packlets/services/feed-syncer.test.d.ts.map +1 -0
  38. package/dist/types/src/packlets/services/platform.d.ts.map +1 -1
  39. package/dist/types/src/packlets/services/service-context.d.ts +3 -4
  40. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  41. package/dist/types/src/packlets/services/service-host.d.ts +1 -1
  42. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  43. package/dist/types/src/packlets/space-export/space-archive-reader.d.ts +9 -1
  44. package/dist/types/src/packlets/space-export/space-archive-reader.d.ts.map +1 -1
  45. package/dist/types/src/packlets/space-export/space-archive-writer.d.ts +6 -0
  46. package/dist/types/src/packlets/space-export/space-archive-writer.d.ts.map +1 -1
  47. package/dist/types/src/packlets/space-export/space-archive.test.d.ts +2 -0
  48. package/dist/types/src/packlets/space-export/space-archive.test.d.ts.map +1 -0
  49. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +17 -10
  50. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  51. package/dist/types/src/packlets/spaces/data-space.d.ts +22 -6
  52. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  53. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  54. package/dist/types/src/packlets/spaces/genesis.d.ts +2 -1
  55. package/dist/types/src/packlets/spaces/genesis.d.ts.map +1 -1
  56. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  57. package/dist/types/src/packlets/spaces/spaces-service.d.ts +2 -2
  58. package/dist/types/src/packlets/spaces/spaces-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/worker/worker-runtime.d.ts +11 -3
  61. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  62. package/dist/types/src/version.d.ts +1 -1
  63. package/dist/types/src/version.d.ts.map +1 -1
  64. package/dist/types/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +43 -43
  66. package/src/index.ts +1 -0
  67. package/src/packlets/agents/edge-agent-manager.ts +8 -5
  68. package/src/packlets/agents/edge-agent-service.ts +2 -2
  69. package/src/packlets/identity/identity-manager.test.ts +5 -5
  70. package/src/packlets/identity/identity-manager.ts +16 -13
  71. package/src/packlets/identity/identity-recovery-manager.ts +20 -16
  72. package/src/packlets/identity/identity-service.test.ts +6 -26
  73. package/src/packlets/identity/identity-service.ts +5 -76
  74. package/src/packlets/identity/identity.test.ts +2 -2
  75. package/src/packlets/identity/identity.ts +7 -29
  76. package/src/packlets/invitations/edge-invitation-handler.ts +4 -3
  77. package/src/packlets/invitations/invitations-handler.test.ts +4 -4
  78. package/src/packlets/invitations/invitations-handler.ts +3 -3
  79. package/src/packlets/invitations/invitations-manager.ts +37 -14
  80. package/src/packlets/invitations/invitations-service.ts +4 -4
  81. package/src/packlets/invitations/space-invitation-protocol.test.ts +17 -16
  82. package/src/packlets/invitations/space-invitation-protocol.ts +3 -1
  83. package/src/packlets/logging/logging-service.ts +4 -0
  84. package/src/packlets/network/network-service.ts +5 -4
  85. package/src/packlets/services/feed-syncer.test.ts +340 -0
  86. package/src/packlets/services/feed-syncer.ts +337 -0
  87. package/src/packlets/services/platform.ts +7 -1
  88. package/src/packlets/services/service-context.test.ts +3 -2
  89. package/src/packlets/services/service-context.ts +106 -31
  90. package/src/packlets/services/service-host.test.ts +8 -7
  91. package/src/packlets/services/service-host.ts +9 -7
  92. package/src/packlets/space-export/space-archive-reader.ts +64 -3
  93. package/src/packlets/space-export/space-archive-writer.ts +36 -1
  94. package/src/packlets/space-export/space-archive.test.ts +287 -0
  95. package/src/packlets/spaces/data-space-manager.test.ts +79 -13
  96. package/src/packlets/spaces/data-space-manager.ts +71 -103
  97. package/src/packlets/spaces/data-space.ts +46 -23
  98. package/src/packlets/spaces/edge-feed-replicator.test.ts +1 -1
  99. package/src/packlets/spaces/edge-feed-replicator.ts +8 -7
  100. package/src/packlets/spaces/epoch-migrations.ts +3 -3
  101. package/src/packlets/spaces/genesis.ts +6 -1
  102. package/src/packlets/spaces/notarization-plugin.ts +2 -1
  103. package/src/packlets/spaces/spaces-service.test.ts +9 -6
  104. package/src/packlets/spaces/spaces-service.ts +30 -8
  105. package/src/packlets/testing/invitation-utils.ts +3 -2
  106. package/src/packlets/worker/worker-runtime.ts +14 -6
  107. package/src/packlets/worker/worker-session.ts +4 -4
  108. package/src/version.ts +1 -1
  109. package/dist/lib/browser/chunk-J33W6T4Q.mjs.map +0 -7
  110. package/dist/lib/node-esm/chunk-34HKLADW.mjs.map +0 -7
  111. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts +0 -19
  112. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts.map +0 -1
  113. package/src/packlets/identity/default-space-state-machine.ts +0 -44
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
+ import { Context } from '@dxos/context';
7
8
  import { MemorySignalManager, MemorySignalManagerContext } from '@dxos/messaging';
8
9
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
9
10
  import { openAndClose } from '@dxos/test-utils';
@@ -19,7 +20,7 @@ describe('services/ServiceContext', () => {
19
20
  const device2 = await createOpenServiceContext(networkContext);
20
21
  await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
21
22
 
22
- const space1 = await device1.dataSpaceManager!.createSpace();
23
+ const space1 = await device1.dataSpaceManager!.createSpace(new Context());
23
24
  await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
24
25
  const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
25
26
  await space2!.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.timeframe);
@@ -35,7 +36,7 @@ describe('services/ServiceContext', () => {
35
36
 
36
37
  const identity2 = await createOpenServiceContext(networkContext);
37
38
  await identity2.createIdentity();
38
- const space1 = await identity2.dataSpaceManager!.createSpace();
39
+ const space1 = await identity2.dataSpaceManager!.createSpace(new Context());
39
40
  await Promise.all(
40
41
  performInvitation({
41
42
  host: identity2,
@@ -22,12 +22,13 @@ import { type RuntimeProvider } from '@dxos/effect';
22
22
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
23
23
  import { invariant } from '@dxos/invariant';
24
24
  import { Keyring } from '@dxos/keyring';
25
- import { PublicKey } from '@dxos/keys';
25
+ import { PublicKey, type SpaceId } from '@dxos/keys';
26
26
  import { type LevelDB } from '@dxos/kv-store';
27
27
  import { log } from '@dxos/log';
28
28
  import { type SignalManager } from '@dxos/messaging';
29
29
  import { type SwarmNetworkManager } from '@dxos/network-manager';
30
30
  import { InvalidStorageVersionError, STORAGE_VERSION, trace } from '@dxos/protocols';
31
+ import { FeedProtocol } from '@dxos/protocols';
31
32
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
32
33
  import { type Runtime } from '@dxos/protocols/proto/dxos/config';
33
34
  import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
@@ -41,6 +42,7 @@ import { safeInstanceof } from '@dxos/util';
41
42
  import { EdgeAgentManager } from '../agents';
42
43
  import {
43
44
  type CreateIdentityOptions,
45
+ type Identity,
44
46
  IdentityManager,
45
47
  type IdentityManagerProps,
46
48
  type JoinIdentityProps,
@@ -56,6 +58,8 @@ import {
56
58
  } from '../invitations';
57
59
  import { DataSpaceManager, type DataSpaceManagerRuntimeProps, type SigningContext } from '../spaces';
58
60
 
61
+ import { FeedSyncer } from './feed-syncer';
62
+
59
63
  export type ServiceContextRuntimeProps = Pick<
60
64
  IdentityManagerProps,
61
65
  'devicePresenceOfflineTimeout' | 'devicePresenceAnnounceInterval'
@@ -64,8 +68,6 @@ export type ServiceContextRuntimeProps = Pick<
64
68
  invitationConnectionDefaultProps?: InvitationConnectionProps;
65
69
  disableP2pReplication?: boolean;
66
70
  enableVectorIndexing?: boolean;
67
- enableSqlite?: boolean;
68
- enableLocalQueues?: boolean;
69
71
  };
70
72
  /**
71
73
  * Shared backend for all client services.
@@ -90,6 +92,7 @@ export class ServiceContext extends Resource {
90
92
  public readonly echoHost: EchoHost;
91
93
  private readonly _meshReplicator?: MeshEchoReplicator = undefined;
92
94
  private readonly _echoEdgeReplicator?: EchoEdgeReplicator = undefined;
95
+ private readonly _feedSyncer?: FeedSyncer = undefined;
93
96
 
94
97
  // Initialized after identity is initialized.
95
98
  public dataSpaceManager?: DataSpaceManager;
@@ -166,12 +169,15 @@ export class ServiceContext extends Resource {
166
169
  kv: this.level,
167
170
  peerIdProvider: () => this.identityManager.identity?.deviceKey?.toHex(),
168
171
  getSpaceKeyByRootDocumentId: (documentId) => this.spaceManager.findSpaceByRootDocumentId(documentId)?.key,
169
- indexing: {
170
- vector: this._runtimeProps?.enableVectorIndexing,
171
- sqlIndex: this._runtimeProps?.enableSqlite,
172
- },
173
172
  runtime: this._runtime,
174
- localQueues: this._runtimeProps?.enableLocalQueues,
173
+ syncQueue: async (ctx, request) => {
174
+ return this._feedSyncer?.syncBlocking(ctx, {
175
+ spaceId: request.spaceId as SpaceId,
176
+ subspaceTag: request.subspaceTag,
177
+ shouldPush: request.shouldPush,
178
+ shouldPull: request.shouldPull,
179
+ });
180
+ },
175
181
  });
176
182
 
177
183
  this.invitations = new InvitationsHandler(
@@ -206,6 +212,17 @@ export class ServiceContext extends Resource {
206
212
  edgeHttpClient: this._edgeHttpClient,
207
213
  });
208
214
  }
215
+
216
+ if (this.echoHost.feedStore && this._edgeConnection) {
217
+ this._feedSyncer = new FeedSyncer({
218
+ runtime: this._runtime,
219
+ feedStore: this.echoHost.feedStore,
220
+ edgeClient: this._edgeConnection,
221
+ peerId: this.identityManager.identity?.deviceKey?.toHex() ?? '',
222
+ getSpaceIds: () => this.echoHost!.spaceIds,
223
+ syncNamespaces: [FeedProtocol.WellKnownNamespaces.data, FeedProtocol.WellKnownNamespaces.trace],
224
+ });
225
+ }
209
226
  }
210
227
 
211
228
  @Trace.span()
@@ -215,32 +232,67 @@ export class ServiceContext extends Resource {
215
232
  log('opening...');
216
233
  log.trace('dxos.sdk.service-context.open', trace.begin({ id: this._instanceId }));
217
234
 
235
+ log('opening identityManager...');
218
236
  await this.identityManager.open(ctx);
237
+ log('identityManager opened', { hasIdentity: !!this.identityManager.identity });
219
238
 
220
- await this._setNetworkIdentity();
239
+ log('setting network identity...');
240
+ await this._setNetworkIdentity({ identity: this.identityManager.identity });
241
+ log('network identity set');
221
242
 
243
+ log('opening edge connection...');
222
244
  await this._edgeConnection?.open();
245
+ log('edge connection opened');
246
+
247
+ log('opening signal manager...');
223
248
  await this.signalManager.open();
249
+ log('signal manager opened');
250
+
251
+ log('opening network manager...');
224
252
  await this.networkManager.open();
253
+ log('network manager opened');
225
254
 
255
+ log('opening echo host...');
226
256
  await this.echoHost.open(ctx);
257
+ log('echo host opened');
227
258
 
228
259
  if (this._meshReplicator) {
229
- await this.echoHost.addReplicator(this._meshReplicator);
260
+ log('adding mesh replicator...');
261
+ await this.echoHost.addReplicator(ctx, this._meshReplicator);
262
+ log('mesh replicator added');
230
263
  }
231
264
  if (this._echoEdgeReplicator) {
232
- await this.echoHost.addReplicator(this._echoEdgeReplicator);
265
+ log('adding edge replicator...');
266
+ await this.echoHost.addReplicator(ctx, this._echoEdgeReplicator);
267
+ log('edge replicator added');
233
268
  }
234
269
 
270
+ log('loading metadata store...');
235
271
  await this.metadataStore.load();
272
+ log('metadata store loaded');
273
+
274
+ log('opening space manager...');
236
275
  await this.spaceManager.open();
276
+ log('space manager opened');
237
277
 
238
278
  if (this.identityManager.identity) {
239
- await this.identityManager.identity.joinNetwork();
279
+ log('joining network...');
280
+ await this.identityManager.identity.joinNetwork(ctx);
281
+ log('network joined');
282
+
283
+ log('initializing spaces...(calling _initialize)');
240
284
  await this._initialize(ctx);
285
+ log('spaces initialized');
286
+ } else {
287
+ log('no identity, skipping network join and space initialization');
241
288
  }
242
289
 
243
- const loadedInvitations = await this.invitationsManager.loadPersistentInvitations();
290
+ log('opening feed syncer...');
291
+ await this._feedSyncer?.open();
292
+ log('feed syncer opened');
293
+
294
+ log('loading persistent invitations...');
295
+ const loadedInvitations = await this.invitationsManager.loadPersistentInvitations(ctx);
244
296
  log('loaded persistent invitations', { count: loadedInvitations.invitations?.length });
245
297
 
246
298
  log.trace('dxos.sdk.service-context.open', trace.end({ id: this._instanceId }));
@@ -249,16 +301,19 @@ export class ServiceContext extends Resource {
249
301
 
250
302
  protected override async _close(ctx: Context): Promise<void> {
251
303
  log('closing...');
304
+
305
+ await this._feedSyncer?.close();
306
+
252
307
  if (this._deviceSpaceSync && this.identityManager.identity) {
253
308
  await this.identityManager.identity.space.spaceState.removeCredentialProcessor(this._deviceSpaceSync);
254
309
  }
255
- await this.dataSpaceManager?.close();
310
+ await this.dataSpaceManager?.close(ctx);
256
311
  await this.edgeAgentManager?.close();
257
- await this.identityManager.close();
312
+ await this.identityManager.close(ctx);
258
313
  await this.spaceManager.close();
259
314
  await this.echoHost.close(ctx);
260
315
 
261
- await this.networkManager.close();
316
+ await this.networkManager.close(ctx);
262
317
  await this.signalManager.close();
263
318
  await this._edgeConnection?.close();
264
319
  await this.feedStore.close();
@@ -268,10 +323,11 @@ export class ServiceContext extends Resource {
268
323
  }
269
324
 
270
325
  async createIdentity(params: CreateIdentityOptions = {}) {
271
- const identity = await this.identityManager.createIdentity(params);
272
- await this._setNetworkIdentity();
273
- await identity.joinNetwork();
274
- await this._initialize(new Context());
326
+ const ctx = Context.default();
327
+ const identity = await this.identityManager.createIdentity(params, ctx);
328
+ await this._setNetworkIdentity({ identity });
329
+ await identity.joinNetwork(ctx);
330
+ await this._initialize(ctx);
275
331
  return identity;
276
332
  }
277
333
 
@@ -295,11 +351,11 @@ export class ServiceContext extends Resource {
295
351
  }
296
352
 
297
353
  private async _acceptIdentity(params: JoinIdentityProps) {
298
- const { identity, identityRecord } = await this.identityManager.prepareIdentity(params);
299
- await this._setNetworkIdentity({ deviceCredential: params.authorizedDeviceCredential! });
300
- await identity.joinNetwork();
354
+ const { identity, identityRecord } = await this.identityManager.prepareIdentity(params, this._ctx);
355
+ await this._setNetworkIdentity({ deviceCredential: params.authorizedDeviceCredential!, identity });
356
+ await identity.joinNetwork(this._ctx);
301
357
  await this.identityManager.acceptIdentity(identity, identityRecord, params.deviceProfile);
302
- await this._initialize(new Context());
358
+ await this._initialize(this._ctx);
303
359
  return identity;
304
360
  }
305
361
 
@@ -314,7 +370,7 @@ export class ServiceContext extends Resource {
314
370
  // Called when identity is created.
315
371
  @Trace.span()
316
372
  private async _initialize(ctx: Context): Promise<void> {
317
- log('initializing spaces...');
373
+ log('_initialize: start');
318
374
  const identity = this.identityManager.identity ?? failUndefined();
319
375
  const signingContext: SigningContext = {
320
376
  credentialSigner: identity.getIdentityCredentialSigner(),
@@ -326,6 +382,7 @@ export class ServiceContext extends Resource {
326
382
  },
327
383
  };
328
384
 
385
+ log('_initialize: creating DataSpaceManager');
329
386
  this.dataSpaceManager = new DataSpaceManager({
330
387
  spaceManager: this.spaceManager,
331
388
  metadataStore: this.metadataStore,
@@ -341,7 +398,9 @@ export class ServiceContext extends Resource {
341
398
  runtimeProps: this._runtimeProps as DataSpaceManagerRuntimeProps,
342
399
  edgeFeatures: this._edgeFeatures,
343
400
  });
401
+ log('_initialize: opening DataSpaceManager...');
344
402
  await this.dataSpaceManager.open();
403
+ log('_initialize: DataSpaceManager opened');
345
404
 
346
405
  this.edgeAgentManager = new EdgeAgentManager(
347
406
  this._edgeFeatures,
@@ -349,13 +408,16 @@ export class ServiceContext extends Resource {
349
408
  this.dataSpaceManager,
350
409
  identity,
351
410
  );
411
+ log('_initialize: opening EdgeAgentManager...');
352
412
  await this.edgeAgentManager.open();
413
+ log('_initialize: EdgeAgentManager opened');
353
414
 
354
415
  this._handlerFactories.set(Invitation.Kind.SPACE, (invitation) => {
355
416
  invariant(this.dataSpaceManager, 'dataSpaceManager not initialized yet');
356
417
  return new SpaceInvitationProtocol(this.dataSpaceManager, signingContext, this.keyring, invitation.spaceKey);
357
418
  });
358
419
  this.initialized.wake();
420
+ log('_initialize: initialized.wake() called');
359
421
 
360
422
  this._deviceSpaceSync = {
361
423
  processCredential: async (credential: Credential) => {
@@ -378,7 +440,7 @@ export class ServiceContext extends Resource {
378
440
 
379
441
  try {
380
442
  log('accepting space recorded in halo', { details: assertion });
381
- await this.dataSpaceManager.acceptSpace({
443
+ await this.dataSpaceManager.acceptSpace(this._ctx, {
382
444
  spaceKey: assertion.spaceKey,
383
445
  genesisFeedKey: assertion.genesisFeedKey,
384
446
  });
@@ -391,33 +453,42 @@ export class ServiceContext extends Resource {
391
453
  await identity.space.spaceState.addCredentialProcessor(this._deviceSpaceSync);
392
454
  }
393
455
 
394
- private async _setNetworkIdentity(params?: { deviceCredential: Credential }): Promise<void> {
456
+ private async _setNetworkIdentity(params?: { deviceCredential?: Credential; identity?: Identity }): Promise<void> {
457
+ log('_setNetworkIdentity: acquiring mutex...');
395
458
  using _ = await this._edgeIdentityUpdateMutex.acquire();
459
+ log('_setNetworkIdentity: mutex acquired');
396
460
 
397
461
  let edgeIdentity: EdgeIdentity;
398
- const identity = this.identityManager.identity;
462
+ const identity = params?.identity;
399
463
  if (identity) {
400
- log('setting identity on edge connection', {
464
+ log('_setNetworkIdentity: has identity', {
401
465
  identity: identity.identityKey.toHex(),
402
- swarms: this.networkManager.topics,
466
+ hasDeviceCredential: !!params?.deviceCredential,
403
467
  });
404
468
 
405
469
  if (params?.deviceCredential) {
470
+ log('_setNetworkIdentity: creating chain edge identity with device credential...');
406
471
  edgeIdentity = await createChainEdgeIdentity(
407
472
  identity.signer,
408
473
  identity.identityKey,
409
474
  identity.deviceKey,
410
- params?.deviceCredential && { credential: params.deviceCredential },
475
+ { credential: params.deviceCredential },
411
476
  [], // TODO(dmaretskyi): Service access credentials.
412
477
  );
478
+ log('_setNetworkIdentity: chain edge identity created');
413
479
  } else {
480
+ log('_setNetworkIdentity: waiting for identity.ready()...');
414
481
  // TODO: throw here or from identity if device chain can't be loaded, to avoid indefinite hangup
415
482
  await warnAfterTimeout(10_000, 'Waiting for identity to be ready for edge connection', async () => {
416
483
  await identity.ready();
417
484
  });
485
+ log('_setNetworkIdentity: identity.ready() resolved', {
486
+ hasDeviceCredentialChain: !!identity.deviceCredentialChain,
487
+ });
418
488
 
419
489
  invariant(identity.deviceCredentialChain);
420
490
 
491
+ log('_setNetworkIdentity: creating chain edge identity...');
421
492
  edgeIdentity = await createChainEdgeIdentity(
422
493
  identity.signer,
423
494
  identity.identityKey,
@@ -425,9 +496,12 @@ export class ServiceContext extends Resource {
425
496
  identity.deviceCredentialChain,
426
497
  [], // TODO(dmaretskyi): Service access credentials.
427
498
  );
499
+ log('_setNetworkIdentity: chain edge identity created');
428
500
  }
429
501
  } else {
502
+ log('_setNetworkIdentity: no identity, creating ephemeral edge identity...');
430
503
  edgeIdentity = await createEphemeralEdgeIdentity();
504
+ log('_setNetworkIdentity: ephemeral edge identity created');
431
505
  }
432
506
 
433
507
  this._edgeConnection?.setIdentity(edgeIdentity);
@@ -436,5 +510,6 @@ export class ServiceContext extends Resource {
436
510
  identityKey: edgeIdentity.identityKey,
437
511
  peerKey: edgeIdentity.peerKey,
438
512
  });
513
+ log('_setNetworkIdentity: done');
439
514
  }
440
515
  }
@@ -13,6 +13,7 @@ import { verifyPresentation } from '@dxos/credentials';
13
13
  import { type PublicKey } from '@dxos/keys';
14
14
  import { MemorySignalManagerContext } from '@dxos/messaging';
15
15
  import { type Identity } from '@dxos/protocols/proto/dxos/client/services';
16
+ import { MembershipPolicy } from '@dxos/protocols/proto/dxos/halo/credentials';
16
17
  import { type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
17
18
  import { isNode } from '@dxos/util';
18
19
 
@@ -29,16 +30,16 @@ describe('ClientServicesHost', () => {
29
30
  test('open and close', async () => {
30
31
  const host = createServiceHost(new Config(), new MemorySignalManagerContext());
31
32
  await host.open(new Context());
32
- await host.close();
33
+ await host.close(Context.default());
33
34
  });
34
35
 
35
36
  test('queryCredentials', async () => {
36
37
  const host = createServiceHost(new Config(), new MemorySignalManagerContext());
37
38
  await host.open(new Context());
38
- onTestFinished(() => host.close());
39
+ onTestFinished(() => host.close(Context.default()));
39
40
 
40
41
  await host.services.IdentityService!.createIdentity({});
41
- const { spaceKey } = await host.services.SpacesService!.createSpace();
42
+ const { spaceKey } = await host.services.SpacesService!.createSpace({ membershipPolicy: MembershipPolicy.INVITE });
42
43
 
43
44
  const stream = host.services.SpacesService!.queryCredentials({ spaceKey });
44
45
  const [done, tick] = latch({ count: 3 });
@@ -54,7 +55,7 @@ describe('ClientServicesHost', () => {
54
55
  test('write and query credentials', async () => {
55
56
  const host = createServiceHost(new Config(), new MemorySignalManagerContext());
56
57
  await host.open(new Context());
57
- onTestFinished(() => host.close());
58
+ onTestFinished(() => host.close(Context.default()));
58
59
 
59
60
  await host.services.IdentityService!.createIdentity({});
60
61
 
@@ -91,7 +92,7 @@ describe('ClientServicesHost', () => {
91
92
  test('sign presentation', async () => {
92
93
  const host = createServiceHost(new Config(), new MemorySignalManagerContext());
93
94
  await host.open(new Context());
94
- onTestFinished(() => host.close());
95
+ onTestFinished(() => host.close(Context.default()));
95
96
 
96
97
  await host.services.IdentityService!.createIdentity({});
97
98
 
@@ -128,7 +129,7 @@ describe('ClientServicesHost', () => {
128
129
  expect(host.context.storage.size).to.exist;
129
130
 
130
131
  await asyncTimeout(host.reset(), 1000);
131
- await host.close();
132
+ await host.close(Context.default());
132
133
  }
133
134
 
134
135
  {
@@ -146,7 +147,7 @@ describe('ClientServicesHost', () => {
146
147
  });
147
148
  await expect(asyncTimeout(trigger.wait(), 200)).rejects.toBeInstanceOf(Error);
148
149
  await stream?.close();
149
- await host.close();
150
+ await host.close(Context.default());
150
151
  }
151
152
  });
152
153
  });
@@ -147,7 +147,7 @@ export class ClientServicesHost {
147
147
  void this.open(new Context());
148
148
  }
149
149
  },
150
- onRelease: () => this.close(),
150
+ onRelease: () => this.close(Context.default()),
151
151
  });
152
152
  }
153
153
 
@@ -244,9 +244,6 @@ export class ClientServicesHost {
244
244
  if (this._runtimeProps.enableVectorIndexing === undefined) {
245
245
  this._runtimeProps.enableVectorIndexing = config?.get('runtime.client.enableVectorIndexing', false);
246
246
  }
247
- if (this._runtimeProps.enableLocalQueues === undefined) {
248
- this._runtimeProps.enableLocalQueues = config?.get('runtime.client.enableLocalQueues', false);
249
- }
250
247
 
251
248
  invariant(!this._config, 'config already set');
252
249
  this._config = config;
@@ -262,8 +259,9 @@ export class ClientServicesHost {
262
259
 
263
260
  const endpoint = config?.get('runtime.services.edge.url');
264
261
  if (endpoint) {
262
+ const clientTag = config?.get('runtime.app.env.DX_EDGE_CLIENT_TAG');
265
263
  this._edgeConnection = new EdgeClient(createStubEdgeIdentity(), { socketEndpoint: endpoint });
266
- this._edgeHttpClient = new EdgeHttpClient(endpoint);
264
+ this._edgeHttpClient = new EdgeHttpClient(endpoint, { clientTag });
267
265
  }
268
266
 
269
267
  const {
@@ -348,7 +346,6 @@ export class ClientServicesHost {
348
346
  this._serviceContext.identityManager,
349
347
  this._serviceContext.recoveryManager,
350
348
  this._serviceContext.keyring,
351
- () => this._serviceContext.dataSpaceManager!,
352
349
  (params) => this._createIdentity(params),
353
350
  (profile) => this._serviceContext.broadcastProfileUpdate(profile),
354
351
  );
@@ -395,8 +392,13 @@ export class ClientServicesHost {
395
392
  EdgeAgentService: new EdgeAgentServiceImpl(agentManagerProvider, this._edgeConnection),
396
393
  });
397
394
 
395
+ log('service-host: opening service context...');
398
396
  await this._serviceContext.open(ctx);
397
+ log('service-host: service context opened');
398
+
399
+ log('service-host: opening identity service...');
399
400
  await identityService.open();
401
+ log('service-host: identity service opened');
400
402
 
401
403
  const devtoolsProxy = this._config?.get('runtime.client.devtoolsProxy');
402
404
  if (devtoolsProxy) {
@@ -420,7 +422,7 @@ export class ClientServicesHost {
420
422
 
421
423
  @synchronized
422
424
  @Trace.span()
423
- async close(): Promise<void> {
425
+ async close(ctx: Context): Promise<void> {
424
426
  if (!this._open) {
425
427
  return;
426
428
  }
@@ -6,12 +6,26 @@ import type { DocumentId } from '@automerge/automerge-repo';
6
6
 
7
7
  import { assertArgument, failedInvariant, invariant } from '@dxos/invariant';
8
8
  import { log } from '@dxos/log';
9
- import { SpaceArchiveFileStructure, type SpaceArchiveMetadata } from '@dxos/protocols';
9
+ import {
10
+ type FeedArchiveBlock,
11
+ type FeedArchiveMetadata,
12
+ SpaceArchiveFileStructure,
13
+ type SpaceArchiveMetadata,
14
+ } from '@dxos/protocols';
10
15
  import type { SpaceArchive } from '@dxos/protocols/proto/dxos/client/services';
11
16
 
17
+ /**
18
+ * Extracted feed data from the archive.
19
+ */
20
+ export type ExtractedFeed = {
21
+ metadata: FeedArchiveMetadata;
22
+ blocks: FeedArchiveBlock[];
23
+ };
24
+
12
25
  export type ExtractedSpaceArchive = {
13
26
  metadata: SpaceArchiveMetadata;
14
27
  documents: Record<DocumentId, Uint8Array>;
28
+ feeds: Record<string, ExtractedFeed>;
15
29
  };
16
30
 
17
31
  export const extractSpaceArchive = async (archive: SpaceArchive): Promise<ExtractedSpaceArchive> => {
@@ -29,6 +43,53 @@ export const extractSpaceArchive = async (archive: SpaceArchive): Promise<Extrac
29
43
  documents[documentId] = entry.content ?? failedInvariant();
30
44
  }
31
45
 
32
- log('extracted space archive', { metadata, documents });
33
- return { metadata, documents };
46
+ const feeds: Record<string, ExtractedFeed> = {};
47
+ const feedsPrefix = `${SpaceArchiveFileStructure.feeds}/`;
48
+ const feedEntries = entries.filter((entry) => entry.fileName.startsWith(feedsPrefix));
49
+
50
+ const feedMetadataByFeedId = new Map<string, FeedArchiveMetadata>();
51
+ const feedBlocksByFeedId = new Map<string, Map<number, FeedArchiveBlock[]>>();
52
+
53
+ for (const entry of feedEntries) {
54
+ const relativePath = entry.fileName.slice(feedsPrefix.length);
55
+ const pathParts = relativePath.split('/');
56
+ if (pathParts.length !== 2) {
57
+ continue;
58
+ }
59
+
60
+ const [feedId, fileName] = pathParts;
61
+ invariant(feedId, 'Feed ID is required');
62
+ invariant(fileName, 'File name is required');
63
+
64
+ if (fileName === SpaceArchiveFileStructure.feedMetadata) {
65
+ const feedMetadata = JSON.parse(entry.getContentAsText()) as FeedArchiveMetadata;
66
+ feedMetadataByFeedId.set(feedId, feedMetadata);
67
+ } else if (fileName.startsWith(SpaceArchiveFileStructure.feedBlocksPrefix) && fileName.endsWith('.json')) {
68
+ const chunkIndexStr = fileName.slice(SpaceArchiveFileStructure.feedBlocksPrefix.length).replace(/\.json$/, '');
69
+ const chunkIndex = parseInt(chunkIndexStr, 10);
70
+ invariant(!isNaN(chunkIndex), `Invalid chunk index: ${chunkIndexStr}`);
71
+
72
+ const blocks = JSON.parse(entry.getContentAsText()) as FeedArchiveBlock[];
73
+ if (!feedBlocksByFeedId.has(feedId)) {
74
+ feedBlocksByFeedId.set(feedId, new Map());
75
+ }
76
+ feedBlocksByFeedId.get(feedId)!.set(chunkIndex, blocks);
77
+ }
78
+ }
79
+
80
+ for (const [feedId, feedMetadata] of feedMetadataByFeedId) {
81
+ const blockChunks = feedBlocksByFeedId.get(feedId) ?? new Map<number, FeedArchiveBlock[]>();
82
+ const sortedChunkIndices = Array.from(blockChunks.keys()).sort((a, b) => a - b);
83
+ const allBlocks: FeedArchiveBlock[] = [];
84
+ for (const chunkIndex of sortedChunkIndices) {
85
+ allBlocks.push(...blockChunks.get(chunkIndex)!);
86
+ }
87
+ feeds[feedId] = {
88
+ metadata: feedMetadata,
89
+ blocks: allBlocks,
90
+ };
91
+ }
92
+
93
+ log('extracted space archive', { metadata, documents, feedCount: Object.keys(feeds).length });
94
+ return { metadata, documents, feeds };
34
95
  };
@@ -7,7 +7,14 @@ import type * as tar from '@obsidize/tar-browserify';
7
7
  import { type Context, Resource } from '@dxos/context';
8
8
  import { assertArgument, assertState } from '@dxos/invariant';
9
9
  import type { IdentityDid, SpaceId } from '@dxos/keys';
10
- import { SpaceArchiveFileStructure, type SpaceArchiveMetadata, SpaceArchiveVersion } from '@dxos/protocols';
10
+ import {
11
+ FEED_ARCHIVE_BLOCKS_PER_CHUNK,
12
+ type FeedArchiveBlock,
13
+ type FeedArchiveMetadata,
14
+ SpaceArchiveFileStructure,
15
+ type SpaceArchiveMetadata,
16
+ SpaceArchiveVersion,
17
+ } from '@dxos/protocols';
11
18
  import type { SpaceArchive } from '@dxos/protocols/proto/dxos/client/services';
12
19
  import { createFilename } from '@dxos/util';
13
20
 
@@ -57,6 +64,34 @@ export class SpaceArchiveWriter extends Resource {
57
64
  this._archive.addBinaryFile(`${SpaceArchiveFileStructure.documents}/${documentId}.bin`, data);
58
65
  }
59
66
 
67
+ /**
68
+ * Writes a feed with its metadata and blocks to the archive.
69
+ * Blocks are written in chunks of {@link FEED_ARCHIVE_BLOCKS_PER_CHUNK}.
70
+ */
71
+ async writeFeed(feedId: string, namespace: string, blocks: FeedArchiveBlock[]): Promise<void> {
72
+ assertArgument(feedId, 'feedId', 'Feed ID is required');
73
+ assertArgument(namespace, 'namespace', 'Namespace is required');
74
+ assertState(this._archive, 'Not open');
75
+
76
+ const feedPath = `${SpaceArchiveFileStructure.feeds}/${feedId}`;
77
+
78
+ const metadata: FeedArchiveMetadata = {
79
+ id: feedId,
80
+ namespace,
81
+ };
82
+ this._archive.addTextFile(`${feedPath}/${SpaceArchiveFileStructure.feedMetadata}`, JSON.stringify(metadata));
83
+
84
+ for (let chunkIndex = 0; chunkIndex * FEED_ARCHIVE_BLOCKS_PER_CHUNK < blocks.length; chunkIndex++) {
85
+ const start = chunkIndex * FEED_ARCHIVE_BLOCKS_PER_CHUNK;
86
+ const end = Math.min(start + FEED_ARCHIVE_BLOCKS_PER_CHUNK, blocks.length);
87
+ const chunk = blocks.slice(start, end);
88
+ this._archive.addTextFile(
89
+ `${feedPath}/${SpaceArchiveFileStructure.feedBlocksPrefix}${chunkIndex}.json`,
90
+ JSON.stringify(chunk),
91
+ );
92
+ }
93
+ }
94
+
60
95
  async finish(): Promise<SpaceArchive> {
61
96
  assertState(this._archive, 'Not open');
62
97
  assertState(this._meta, 'Not started');