@dxos/client-services 0.4.10-main.3e0c8a5 → 0.4.10-main.3e35a2f

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 (81) hide show
  1. package/dist/lib/browser/{chunk-6GHZYBUW.mjs → chunk-S3G2RM7S.mjs} +744 -559
  2. package/dist/lib/browser/chunk-S3G2RM7S.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +11 -3
  4. package/dist/lib/browser/index.mjs.map +1 -1
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +11 -5
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-D5VK6MZQ.cjs → chunk-3T6D6GIB.cjs} +688 -591
  9. package/dist/lib/node/chunk-3T6D6GIB.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +46 -38
  11. package/dist/lib/node/index.cjs.map +1 -1
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/packlets/testing/index.cjs +15 -9
  14. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  15. package/dist/types/src/index.d.ts +1 -0
  16. package/dist/types/src/index.d.ts.map +1 -1
  17. package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts +5 -0
  18. package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts.map +1 -0
  19. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts +5 -0
  20. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -0
  21. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts +15 -0
  22. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -0
  23. package/dist/types/src/packlets/{services → diagnostics}/diagnostics.d.ts +1 -1
  24. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -0
  25. package/dist/types/src/packlets/diagnostics/index.d.ts +4 -0
  26. package/dist/types/src/packlets/diagnostics/index.d.ts.map +1 -0
  27. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +0 -1
  28. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  29. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  30. package/dist/types/src/packlets/services/index.d.ts +1 -1
  31. package/dist/types/src/packlets/services/index.d.ts.map +1 -1
  32. package/dist/types/src/packlets/services/service-context.d.ts +7 -5
  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 +5 -1
  35. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  36. package/dist/types/src/packlets/services/util.d.ts +1 -0
  37. package/dist/types/src/packlets/services/util.d.ts.map +1 -1
  38. package/dist/types/src/packlets/storage/index.d.ts +1 -0
  39. package/dist/types/src/packlets/storage/index.d.ts.map +1 -1
  40. package/dist/types/src/packlets/storage/level.d.ts +4 -0
  41. package/dist/types/src/packlets/storage/level.d.ts.map +1 -0
  42. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  43. package/dist/types/src/packlets/storage/util.d.ts +4 -0
  44. package/dist/types/src/packlets/storage/util.d.ts.map +1 -0
  45. package/dist/types/src/packlets/system/system-service.d.ts +1 -1
  46. package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
  47. package/dist/types/src/packlets/testing/test-builder.d.ts +5 -2
  48. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  49. package/dist/types/src/version.d.ts +1 -1
  50. package/package.json +36 -34
  51. package/src/index.ts +1 -0
  52. package/src/packlets/devices/devices-service.test.ts +1 -1
  53. package/src/packlets/diagnostics/browser-diagnostics-broadcast.ts +94 -0
  54. package/src/packlets/diagnostics/diagnostics-broadcast.ts +20 -0
  55. package/src/packlets/diagnostics/diagnostics-collector.ts +65 -0
  56. package/src/packlets/{services → diagnostics}/diagnostics.ts +2 -2
  57. package/src/packlets/diagnostics/index.ts +7 -0
  58. package/src/packlets/identity/identity-service.test.ts +1 -1
  59. package/src/packlets/invitations/device-invitation-protocol.test.ts +1 -1
  60. package/src/packlets/invitations/invitations-handler.ts +5 -12
  61. package/src/packlets/invitations/invitations-service.ts +5 -5
  62. package/src/packlets/network/network-service.test.ts +1 -1
  63. package/src/packlets/services/automerge-host.test.ts +9 -3
  64. package/src/packlets/services/index.ts +1 -1
  65. package/src/packlets/services/service-context.test.ts +9 -6
  66. package/src/packlets/services/service-context.ts +12 -5
  67. package/src/packlets/services/service-host.ts +49 -13
  68. package/src/packlets/services/service-registry.test.ts +1 -1
  69. package/src/packlets/services/util.ts +2 -0
  70. package/src/packlets/spaces/data-space-manager.test.ts +4 -4
  71. package/src/packlets/spaces/spaces-service.test.ts +1 -1
  72. package/src/packlets/storage/index.ts +1 -0
  73. package/src/packlets/storage/level.ts +19 -0
  74. package/src/packlets/storage/storage.ts +3 -9
  75. package/src/packlets/storage/util.ts +19 -0
  76. package/src/packlets/system/system-service.ts +1 -1
  77. package/src/packlets/testing/test-builder.ts +23 -5
  78. package/src/version.ts +1 -1
  79. package/dist/lib/browser/chunk-6GHZYBUW.mjs.map +0 -7
  80. package/dist/lib/node/chunk-D5VK6MZQ.cjs.map +0 -7
  81. package/dist/types/src/packlets/services/diagnostics.d.ts.map +0 -1
@@ -0,0 +1,94 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Trigger } from '@dxos/async';
6
+ import { log } from '@dxos/log';
7
+ import { type SystemService } from '@dxos/protocols/proto/dxos/client/services';
8
+
9
+ import {
10
+ type CollectDiagnosticsBroadcastSender,
11
+ type CollectDiagnosticsBroadcastHandler,
12
+ } from './diagnostics-collector';
13
+
14
+ const CHANNEL_NAME = 'dxos.diagnostics.broadcast';
15
+
16
+ enum MessageType {
17
+ PROBE = 'probe',
18
+ PROBE_ACK = 'probe-ack',
19
+ REQUEST_DIAGNOSTICS = 'request-diagnostics',
20
+ RECEIVE_DIAGNOSTICS = 'receive-diagnostics',
21
+ }
22
+
23
+ interface Message {
24
+ type: MessageType;
25
+ payload?: any;
26
+ }
27
+
28
+ export const createCollectDiagnosticsBroadcastSender = (): CollectDiagnosticsBroadcastSender => {
29
+ return {
30
+ broadcastDiagnosticsRequest: async () => {
31
+ let expectedResponse = MessageType.PROBE_ACK;
32
+ let channel: BroadcastChannel | undefined;
33
+ try {
34
+ const trigger = new Trigger<Message>();
35
+ channel = new BroadcastChannel(CHANNEL_NAME);
36
+ channel.onmessage = (msg) => {
37
+ if (expectedResponse === msg.data.type) {
38
+ trigger.wake(msg.data);
39
+ }
40
+ };
41
+ channel.postMessage({ type: MessageType.PROBE });
42
+ await trigger.wait({ timeout: 200 });
43
+ expectedResponse = MessageType.RECEIVE_DIAGNOSTICS;
44
+ trigger.reset();
45
+ channel.postMessage({ type: MessageType.REQUEST_DIAGNOSTICS });
46
+ const diagnostics = await trigger.wait({ timeout: 5000 });
47
+ return diagnostics.payload;
48
+ } catch (e) {
49
+ const errorDescription = e instanceof Error ? e.message : JSON.stringify(e);
50
+ return { expectedResponse, errorDescription };
51
+ } finally {
52
+ safeClose(channel);
53
+ }
54
+ },
55
+ };
56
+ };
57
+
58
+ export const createCollectDiagnosticsBroadcastHandler = (
59
+ systemService: SystemService,
60
+ ): CollectDiagnosticsBroadcastHandler => {
61
+ let channel: BroadcastChannel | undefined;
62
+ return {
63
+ start: () => {
64
+ channel = new BroadcastChannel(CHANNEL_NAME);
65
+ channel.onmessage = async (message) => {
66
+ try {
67
+ if (message.data.type === MessageType.PROBE) {
68
+ channel?.postMessage({ type: MessageType.PROBE_ACK });
69
+ } else if (message.data.type === MessageType.REQUEST_DIAGNOSTICS) {
70
+ const diagnostics = await systemService.getDiagnostics({});
71
+ channel?.postMessage({
72
+ type: MessageType.RECEIVE_DIAGNOSTICS,
73
+ payload: diagnostics,
74
+ });
75
+ }
76
+ } catch (error) {
77
+ log.catch(error);
78
+ }
79
+ };
80
+ },
81
+ stop: () => {
82
+ safeClose(channel);
83
+ channel = undefined;
84
+ },
85
+ };
86
+ };
87
+
88
+ const safeClose = (channel?: BroadcastChannel) => {
89
+ try {
90
+ channel?.close();
91
+ } catch (e) {
92
+ // ignored
93
+ }
94
+ };
@@ -0,0 +1,20 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+ import { type SystemService } from '@dxos/protocols/proto/dxos/client/services';
5
+
6
+ import {
7
+ type CollectDiagnosticsBroadcastSender,
8
+ type CollectDiagnosticsBroadcastHandler,
9
+ } from './diagnostics-collector';
10
+
11
+ export const createCollectDiagnosticsBroadcastSender = (): CollectDiagnosticsBroadcastSender => {
12
+ return { broadcastDiagnosticsRequest: async () => undefined };
13
+ };
14
+
15
+ export const createCollectDiagnosticsBroadcastHandler = (_: SystemService): CollectDiagnosticsBroadcastHandler => {
16
+ return {
17
+ start: () => {},
18
+ stop: () => {},
19
+ };
20
+ };
@@ -0,0 +1,65 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type ClientServicesProvider } from '@dxos/client-protocol';
6
+ import { type Config, ConfigResource } from '@dxos/config';
7
+ import { GetDiagnosticsRequest } from '@dxos/protocols/proto/dxos/client/services';
8
+ import { TRACE_PROCESSOR } from '@dxos/tracing';
9
+ import { type JsonKeyOptions, jsonKeyReplacer, nonNullable } from '@dxos/util';
10
+
11
+ import { createCollectDiagnosticsBroadcastSender } from './diagnostics-broadcast';
12
+ import { ClientServicesProviderResource } from '../services';
13
+
14
+ export interface CollectDiagnosticsBroadcastSender {
15
+ broadcastDiagnosticsRequest(): any;
16
+ }
17
+
18
+ export interface CollectDiagnosticsBroadcastHandler {
19
+ start(): void;
20
+ stop(): void;
21
+ }
22
+
23
+ export class DiagnosticsCollector {
24
+ private static broadcastSender = createCollectDiagnosticsBroadcastSender();
25
+
26
+ public static async collect(
27
+ config: Config | Config[] = findConfigs(),
28
+ services: ClientServicesProvider | null = findSystemServiceProvider(),
29
+ options: JsonKeyOptions = {},
30
+ ): Promise<any> {
31
+ const serviceDiagnostics = await services?.services?.SystemService?.getDiagnostics({
32
+ keys: options.humanize
33
+ ? GetDiagnosticsRequest.KEY_OPTION.HUMANIZE
34
+ : options.truncate
35
+ ? GetDiagnosticsRequest.KEY_OPTION.TRUNCATE
36
+ : undefined,
37
+ });
38
+
39
+ const clientDiagnostics = {
40
+ config,
41
+ trace: TRACE_PROCESSOR.getDiagnostics(),
42
+ };
43
+
44
+ const diagnostics =
45
+ serviceDiagnostics != null
46
+ ? { client: clientDiagnostics, services: serviceDiagnostics }
47
+ : {
48
+ client: clientDiagnostics,
49
+ broadcast: await this.broadcastSender.broadcastDiagnosticsRequest(),
50
+ };
51
+
52
+ return JSON.parse(JSON.stringify(diagnostics, jsonKeyReplacer(options)));
53
+ }
54
+ }
55
+
56
+ const findSystemServiceProvider = (): ClientServicesProvider | null => {
57
+ const serviceProviders = TRACE_PROCESSOR.findByAnnotation(ClientServicesProviderResource);
58
+ const providerResource = serviceProviders.find((r) => r.instance.deref()?.services?.SystemService != null);
59
+ return providerResource?.instance?.deref() ?? null;
60
+ };
61
+
62
+ const findConfigs = (): Config[] => {
63
+ const configs = TRACE_PROCESSOR.findByAnnotation(ConfigResource);
64
+ return configs.map((r) => r.instance.deref()).filter(nonNullable);
65
+ };
@@ -25,9 +25,9 @@ import { type Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
25
25
  import { type Resource, type Span } from '@dxos/protocols/proto/dxos/tracing';
26
26
  import { TRACE_PROCESSOR } from '@dxos/tracing';
27
27
 
28
- import { getPlatform } from './platform';
29
- import { type ServiceContext } from './service-context';
30
28
  import { DXOS_VERSION } from '../../version';
29
+ import { type ServiceContext } from '../services';
30
+ import { getPlatform } from '../services/platform';
31
31
  import { type DataSpace } from '../spaces';
32
32
 
33
33
  const DEFAULT_TIMEOUT = 1_000;
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './diagnostics';
6
+ export * from './diagnostics-collector';
7
+ export * from './diagnostics-broadcast';
@@ -22,7 +22,7 @@ describe('IdentityService', () => {
22
22
  let identityService: IdentityService;
23
23
 
24
24
  beforeEach(async () => {
25
- serviceContext = createServiceContext();
25
+ serviceContext = await createServiceContext();
26
26
  await serviceContext.open(new Context());
27
27
  identityService = new IdentityServiceImpl(
28
28
  (options) => serviceContext.createIdentity(options),
@@ -20,7 +20,7 @@ const closeAfterTest = async (peer: ServiceContext) => {
20
20
 
21
21
  describe('services/device', () => {
22
22
  test('creates identity', async () => {
23
- const peer = createServiceContext();
23
+ const peer = await createServiceContext();
24
24
  await peer.open(new Context());
25
25
  afterTest(() => peer.close());
26
26
 
@@ -76,7 +76,8 @@ export class InvitationsHandler {
76
76
  swarmKey = PublicKey.random(),
77
77
  persistent = true,
78
78
  created = new Date(),
79
- lifetime = 86400, // 1 day
79
+ lifetime = 86400, // 1 day,
80
+ multiUse = false,
80
81
  } = options ?? {};
81
82
  const authCode =
82
83
  options?.authCode ??
@@ -91,9 +92,10 @@ export class InvitationsHandler {
91
92
  swarmKey,
92
93
  authCode,
93
94
  timeout,
94
- persistent,
95
+ persistent: persistent && type !== Invitation.Type.OFFLINE, // offline invitations are persisted in control feed
95
96
  created,
96
97
  lifetime,
98
+ multiUse,
97
99
  ...protocol.getInvitationContext(),
98
100
  };
99
101
 
@@ -162,7 +164,7 @@ export class InvitationsHandler {
162
164
  }
163
165
  log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.error({ id: traceId, error: err }));
164
166
  } finally {
165
- if (type !== Invitation.Type.MULTIUSE) {
167
+ if (!multiUse) {
166
168
  // Wait for graceful close before disposing.
167
169
  await swarmConnection.close();
168
170
  await ctx.dispose();
@@ -424,12 +426,3 @@ export class InvitationsHandler {
424
426
  return observable;
425
427
  }
426
428
  }
427
-
428
- export const invitationExpired = (invitation: Invitation) => {
429
- return (
430
- invitation.created &&
431
- invitation.lifetime &&
432
- invitation.lifetime !== 0 &&
433
- invitation.created.getTime() + invitation.lifetime * 1000 < Date.now()
434
- );
435
- };
@@ -6,7 +6,7 @@ import { Event, scheduleTask } from '@dxos/async';
6
6
  import { type AuthenticatingInvitation, type CancellableInvitation } from '@dxos/client-protocol';
7
7
  import { Stream } from '@dxos/codec-protobuf';
8
8
  import { Context } from '@dxos/context';
9
- import { type MetadataStore } from '@dxos/echo-pipeline';
9
+ import { hasInvitationExpired, type MetadataStore } from '@dxos/echo-pipeline';
10
10
  import { invariant } from '@dxos/invariant';
11
11
  import { log } from '@dxos/log';
12
12
  import {
@@ -18,7 +18,7 @@ import {
18
18
  } from '@dxos/protocols/proto/dxos/client/services';
19
19
 
20
20
  import { type InvitationProtocol } from './invitation-protocol';
21
- import { invitationExpired, type InvitationsHandler } from './invitations-handler';
21
+ import { type InvitationsHandler } from './invitations-handler';
22
22
 
23
23
  /**
24
24
  * Adapts invitation service observable to client/service stream.
@@ -91,7 +91,7 @@ export class InvitationsServiceImpl implements InvitationsService {
91
91
  }
92
92
 
93
93
  this._createInvitations.delete(invitation.get().invitationId);
94
- if (invitation.get().type !== Invitation.Type.MULTIUSE) {
94
+ if (!invitation.get().multiUse) {
95
95
  this._removedCreated.emit(invitation.get());
96
96
  }
97
97
  },
@@ -103,7 +103,7 @@ export class InvitationsServiceImpl implements InvitationsService {
103
103
  const persistentInvitations = this._metadataStore.getInvitations();
104
104
 
105
105
  // get saved persistent invitations, filter and remove from storage those that have expired.
106
- const freshInvitations = persistentInvitations.filter(async (invitation) => !invitationExpired(invitation));
106
+ const freshInvitations = persistentInvitations.filter(async (invitation) => !hasInvitationExpired(invitation));
107
107
 
108
108
  const cInvitations = freshInvitations.map((persistentInvitation) => {
109
109
  invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
@@ -148,7 +148,7 @@ export class InvitationsServiceImpl implements InvitationsService {
148
148
  () => {
149
149
  close();
150
150
  this._acceptInvitations.delete(invitation.get().invitationId);
151
- if (invitation.get().type !== Invitation.Type.MULTIUSE) {
151
+ if (!invitation.get().multiUse) {
152
152
  this._removedAccepted.emit(invitation.get());
153
153
  }
154
154
  },
@@ -18,7 +18,7 @@ describe('NetworkService', () => {
18
18
  let networkService: NetworkService;
19
19
 
20
20
  beforeEach(async () => {
21
- serviceContext = createServiceContext();
21
+ serviceContext = await createServiceContext();
22
22
  await serviceContext.open(new Context());
23
23
  networkService = new NetworkServiceImpl(serviceContext.networkManager, serviceContext.signalManager);
24
24
  });
@@ -6,8 +6,8 @@ import { expect } from 'chai';
6
6
 
7
7
  import { asyncTimeout, sleep } from '@dxos/async';
8
8
  import { AutomergeHost, DataServiceImpl } from '@dxos/echo-pipeline';
9
+ import { createTestLevel } from '@dxos/echo-pipeline/testing';
9
10
  import { AutomergeContext } from '@dxos/echo-schema';
10
- import { StorageType, createStorage } from '@dxos/random-access-storage';
11
11
  import { afterTest, describe, test } from '@dxos/test';
12
12
 
13
13
  describe('AutomergeHost', () => {
@@ -19,10 +19,16 @@ describe('AutomergeHost', () => {
19
19
  // creates repo and document | replicates repo | finds document in repo
20
20
  //
21
21
 
22
- const storageDirectory = createStorage({ type: StorageType.RAM }).createDirectory();
22
+ const level = createTestLevel();
23
+ await level.open();
24
+ afterTest(() => level.close());
23
25
 
24
- const host = new AutomergeHost({ directory: storageDirectory });
26
+ const host = new AutomergeHost({
27
+ db: level.sublevel('automerge'),
28
+ });
29
+ await host.open();
25
30
  afterTest(() => host.close());
31
+
26
32
  const dataService = new DataServiceImpl(host);
27
33
  const client = new AutomergeContext(dataService);
28
34
  afterTest(() => client.close());
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './client-rpc-server';
6
- export * from './diagnostics';
7
6
  export * from './service-context';
8
7
  export * from './service-host';
9
8
  export * from './service-registry';
9
+ export { ClientServicesProviderResource } from './util';
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { MemorySignalManagerContext } from '@dxos/messaging';
6
6
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
7
- import { describe, test } from '@dxos/test';
7
+ import { describe, openAndClose, test } from '@dxos/test';
8
8
 
9
9
  import { createServiceContext } from '../testing';
10
10
  import { performInvitation } from '../testing/invitation-utils';
@@ -12,10 +12,10 @@ import { performInvitation } from '../testing/invitation-utils';
12
12
  describe('services/ServiceContext', () => {
13
13
  test('new space is synchronized on device invitations', async () => {
14
14
  const networkContext = new MemorySignalManagerContext();
15
- const device1 = createServiceContext({ signalContext: networkContext });
15
+ const device1 = await createServiceContext({ signalContext: networkContext });
16
16
  await device1.createIdentity();
17
17
 
18
- const device2 = createServiceContext({ signalContext: networkContext });
18
+ const device2 = await createServiceContext({ signalContext: networkContext });
19
19
  await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
20
20
 
21
21
  const space1 = await device1.dataSpaceManager!.createSpace();
@@ -26,13 +26,16 @@ describe('services/ServiceContext', () => {
26
26
 
27
27
  test('joined space is synchronized on device invitations', async () => {
28
28
  const networkContext = new MemorySignalManagerContext();
29
- const device1 = createServiceContext({ signalContext: networkContext });
29
+ const device1 = await createServiceContext({ signalContext: networkContext });
30
+ await openAndClose(device1.automergeHost);
30
31
  await device1.createIdentity();
31
32
 
32
- const device2 = createServiceContext({ signalContext: networkContext });
33
+ const device2 = await createServiceContext({ signalContext: networkContext });
34
+ await openAndClose(device2.automergeHost);
33
35
  await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
34
36
 
35
- const identity2 = createServiceContext({ signalContext: networkContext });
37
+ const identity2 = await createServiceContext({ signalContext: networkContext });
38
+ await openAndClose(identity2.automergeHost);
36
39
  await identity2.createIdentity();
37
40
  const space1 = await identity2.dataSpaceManager!.createSpace();
38
41
  await Promise.all(
@@ -2,8 +2,10 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ import { type Level } from 'level';
6
+
5
7
  import { Trigger } from '@dxos/async';
6
- import { Context } from '@dxos/context';
8
+ import { Context, Resource } from '@dxos/context';
7
9
  import { getCredentialAssertion, type CredentialProcessor } from '@dxos/credentials';
8
10
  import { failUndefined } from '@dxos/debug';
9
11
  import { AutomergeHost, MetadataStore, SnapshotStore, SpaceManager, valueEncoding } from '@dxos/echo-pipeline';
@@ -47,7 +49,7 @@ export type ServiceContextRuntimeParams = IdentityManagerRuntimeParams & DataSpa
47
49
  // TODO(dmaretskyi): Gets duplicated in CJS build between normal and testing bundles.
48
50
  @safeInstanceof('dxos.client-services.ServiceContext')
49
51
  @Trace.resource()
50
- export class ServiceContext {
52
+ export class ServiceContext extends Resource {
51
53
  public readonly initialized = new Trigger();
52
54
  public readonly metadataStore: MetadataStore;
53
55
  /**
@@ -78,10 +80,13 @@ export class ServiceContext {
78
80
 
79
81
  constructor(
80
82
  public readonly storage: Storage,
83
+ public readonly level: Level<string, string>,
81
84
  public readonly networkManager: NetworkManager,
82
85
  public readonly signalManager: SignalManager,
83
86
  public readonly _runtimeParams?: IdentityManagerRuntimeParams & DataSpaceManagerRuntimeParams,
84
87
  ) {
88
+ super();
89
+
85
90
  // TODO(burdon): Move strings to constants.
86
91
  this.metadataStore = new MetadataStore(storage.createDirectory('metadata'));
87
92
  this.snapshotStore = new SnapshotStore(storage.createDirectory('snapshots'));
@@ -115,10 +120,11 @@ export class ServiceContext {
115
120
  this._runtimeParams as IdentityManagerRuntimeParams,
116
121
  );
117
122
 
118
- this.indexMetadata = new IndexMetadataStore({ directory: storage.createDirectory('index-metadata') });
123
+ this.indexMetadata = new IndexMetadataStore({ db: level.sublevel('index-metadata') });
119
124
 
120
125
  this.automergeHost = new AutomergeHost({
121
126
  directory: storage.createDirectory('automerge'),
127
+ db: level.sublevel('automerge'),
122
128
  metadata: this.indexMetadata,
123
129
  });
124
130
 
@@ -145,7 +151,7 @@ export class ServiceContext {
145
151
  }
146
152
 
147
153
  @Trace.span()
148
- async open(ctx: Context) {
154
+ protected override async _open(ctx: Context) {
149
155
  await this._checkStorageVersion();
150
156
 
151
157
  log('opening...');
@@ -153,6 +159,7 @@ export class ServiceContext {
153
159
  await this.signalManager.open();
154
160
  await this.networkManager.open();
155
161
 
162
+ await this.automergeHost.open();
156
163
  await this.metadataStore.load();
157
164
  await this.spaceManager.open();
158
165
  await this.identityManager.open(ctx);
@@ -163,7 +170,7 @@ export class ServiceContext {
163
170
  log('opened');
164
171
  }
165
172
 
166
- async close() {
173
+ protected override async _close() {
167
174
  log('closing...');
168
175
  if (this._deviceSpaceSync && this.identityManager.identity) {
169
176
  await this.identityManager.identity.space.spaceState.removeCredentialProcessor(this._deviceSpaceSync);
@@ -2,13 +2,20 @@
2
2
  // Copyright 2021 DXOS.org
3
3
  //
4
4
 
5
- import { Event, sleep, synchronized } from '@dxos/async';
6
- import { clientServiceBundle, defaultKey, type ClientServices, PropertiesSchema } from '@dxos/client-protocol';
5
+ import { type Level } from 'level';
6
+
7
+ import { Event, synchronized } from '@dxos/async';
8
+ import { clientServiceBundle, defaultKey, type ClientServices, Properties } from '@dxos/client-protocol';
7
9
  import { type Config } from '@dxos/config';
8
10
  import { Context } from '@dxos/context';
9
- import { DataServiceImpl, type SpaceDoc } from '@dxos/echo-pipeline';
11
+ import {
12
+ DataServiceImpl,
13
+ type ObjectStructure,
14
+ encodeReference,
15
+ type SpaceDoc,
16
+ type MyLevel,
17
+ } from '@dxos/echo-pipeline';
10
18
  import * as E from '@dxos/echo-schema';
11
- import { createRawObjectDoc } from '@dxos/echo-schema';
12
19
  import { IndexServiceImpl } from '@dxos/indexing';
13
20
  import { invariant } from '@dxos/invariant';
14
21
  import { PublicKey } from '@dxos/keys';
@@ -22,18 +29,22 @@ import { TRACE_PROCESSOR, trace as Trace } from '@dxos/tracing';
22
29
  import { assignDeep } from '@dxos/util';
23
30
  import { WebsocketRpcClient } from '@dxos/websocket-rpc';
24
31
 
25
- import { createDiagnostics } from './diagnostics';
26
32
  import { ServiceContext, type ServiceContextRuntimeParams } from './service-context';
27
33
  import { ServiceRegistry } from './service-registry';
28
34
  import { DevicesServiceImpl } from '../devices';
29
35
  import { DevtoolsHostEvents, DevtoolsServiceImpl } from '../devtools';
36
+ import {
37
+ type CollectDiagnosticsBroadcastHandler,
38
+ createCollectDiagnosticsBroadcastHandler,
39
+ createDiagnostics,
40
+ } from '../diagnostics';
30
41
  import { IdentityServiceImpl, type CreateIdentityOptions } from '../identity';
31
42
  import { InvitationsServiceImpl } from '../invitations';
32
43
  import { Lock, type ResourceLock } from '../locks';
33
44
  import { LoggingServiceImpl } from '../logging';
34
45
  import { NetworkServiceImpl } from '../network';
35
46
  import { SpacesServiceImpl } from '../spaces';
36
- import { createStorageObjects } from '../storage';
47
+ import { createLevel, createStorageObjects } from '../storage';
37
48
  import { SystemServiceImpl } from '../system';
38
49
 
39
50
  export type ClientServicesHostParams = {
@@ -45,6 +56,7 @@ export type ClientServicesHostParams = {
45
56
  signalManager?: SignalManager;
46
57
  connectionLog?: boolean;
47
58
  storage?: Storage;
59
+ level?: MyLevel;
48
60
  lockKey?: string;
49
61
  callbacks?: ClientServicesHostCallbacks;
50
62
  runtimeParams?: ServiceContextRuntimeParams;
@@ -77,11 +89,13 @@ export class ClientServicesHost {
77
89
  private _signalManager?: SignalManager;
78
90
  private _networkManager?: NetworkManager;
79
91
  private _storage?: Storage;
92
+ private _level?: Level<string, string>;
80
93
  private _callbacks?: ClientServicesHostCallbacks;
81
94
  private _devtoolsProxy?: WebsocketRpcClient<{}, ClientServices>;
82
95
 
83
96
  private _serviceContext!: ServiceContext;
84
97
  private readonly _runtimeParams?: ServiceContextRuntimeParams;
98
+ private diagnosticsBroadcastHandler: CollectDiagnosticsBroadcastHandler;
85
99
 
86
100
  @Trace.info()
87
101
  private _opening = false;
@@ -94,12 +108,14 @@ export class ClientServicesHost {
94
108
  transportFactory,
95
109
  signalManager,
96
110
  storage,
111
+ level,
97
112
  // TODO(wittjosiah): Turn this on by default.
98
113
  lockKey,
99
114
  callbacks,
100
115
  runtimeParams,
101
116
  }: ClientServicesHostParams = {}) {
102
117
  this._storage = storage;
118
+ this._level = level;
103
119
  this._callbacks = callbacks;
104
120
  this._runtimeParams = runtimeParams;
105
121
 
@@ -139,6 +155,7 @@ export class ClientServicesHost {
139
155
  },
140
156
  });
141
157
 
158
+ this.diagnosticsBroadcastHandler = createCollectDiagnosticsBroadcastHandler(this._systemService);
142
159
  this._loggingService = new LoggingServiceImpl();
143
160
 
144
161
  this._serviceRegistry = new ServiceRegistry<ClientServices>(clientServiceBundle, {
@@ -227,12 +244,19 @@ export class ClientServicesHost {
227
244
 
228
245
  this._opening = true;
229
246
  log('opening...', { lockKey: this._resourceLock?.lockKey });
247
+
248
+ if (!this._level) {
249
+ this._level = await createLevel(this._config.get('runtime.client.storage', {})!);
250
+ }
251
+ await this._level.open();
252
+
230
253
  await this._resourceLock?.acquire();
231
254
 
232
255
  await this._loggingService.open();
233
256
 
234
257
  this._serviceContext = new ServiceContext(
235
258
  this._storage,
259
+ this._level,
236
260
  this._networkManager,
237
261
  this._signalManager,
238
262
  this._runtimeParams,
@@ -302,6 +326,7 @@ export class ClientServicesHost {
302
326
  });
303
327
  void this._devtoolsProxy.open();
304
328
  }
329
+ this.diagnosticsBroadcastHandler.start();
305
330
 
306
331
  this._opening = false;
307
332
  this._open = true;
@@ -320,10 +345,12 @@ export class ClientServicesHost {
320
345
 
321
346
  const deviceKey = this._serviceContext.identityManager.identity?.deviceKey;
322
347
  log('closing...', { deviceKey });
348
+ this.diagnosticsBroadcastHandler.stop();
323
349
  await this._devtoolsProxy?.close();
324
350
  this._serviceRegistry.setServices({ SystemService: this._systemService });
325
351
  await this._loggingService.close();
326
352
  await this._serviceContext.close();
353
+ await this._level?.close();
327
354
  this._open = false;
328
355
  this._statusUpdate.emit();
329
356
  log('closed', { deviceKey });
@@ -353,15 +380,24 @@ export class ClientServicesHost {
353
380
  const document = await this._serviceContext.automergeHost.repo.find<SpaceDoc>(automergeIndex as any);
354
381
  await document.whenReady();
355
382
 
356
- const objectDocument = createRawObjectDoc(
357
- { [defaultKey]: identity.identityKey.toHex() },
358
- { type: E.getTypeReference(PropertiesSchema) },
359
- );
383
+ // TODO(dmaretskyi): Better API for low-level data access.
384
+ const properties: ObjectStructure = {
385
+ system: {
386
+ type: encodeReference(E.getTypeReference(Properties)!),
387
+ },
388
+ data: {
389
+ [defaultKey]: identity.identityKey.toHex(),
390
+ },
391
+ meta: {
392
+ keys: [],
393
+ },
394
+ };
395
+ const propertiesId = PublicKey.random().toHex();
360
396
  document.change((doc: SpaceDoc) => {
361
- assignDeep(doc, ['objects', objectDocument.id], objectDocument.handle.docSync());
397
+ assignDeep(doc, ['objects', propertiesId], properties);
362
398
  });
363
- // TODO: replace with flush when supported by automerge-repo
364
- await sleep(200);
399
+
400
+ await this._serviceContext.automergeHost.repo.flush();
365
401
 
366
402
  return identity;
367
403
  }
@@ -31,7 +31,7 @@ const serviceBundle = createServiceBundle<TestServices>({
31
31
  describe('service registry', () => {
32
32
  test('builds a service registry', async () => {
33
33
  const remoteSource = 'https://remote.source';
34
- const serviceContext = createServiceContext();
34
+ const serviceContext = await createServiceContext();
35
35
  await serviceContext.open(new Context());
36
36
 
37
37
  const serviceRegistry = new ServiceRegistry(serviceBundle, {
@@ -5,6 +5,8 @@
5
5
  import { PublicKey } from '@dxos/keys';
6
6
  import { humanize } from '@dxos/util';
7
7
 
8
+ export const ClientServicesProviderResource = Symbol.for('dxos.resource.ClientServices');
9
+
8
10
  // TODO(burdon): Move to util.
9
11
 
10
12
  export type JsonStringifyOptions = {