@dxos/client-services 0.1.56-main.09ca29d → 0.1.56-main.0e9f69e

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 (73) hide show
  1. package/dist/lib/browser/{chunk-FD4L7N4K.mjs → chunk-II6TDINB.mjs} +782 -511
  2. package/dist/lib/browser/chunk-II6TDINB.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +30 -31
  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 +31 -46
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/index.cjs +918 -649
  9. package/dist/lib/node/index.cjs.map +4 -4
  10. package/dist/lib/node/meta.json +1 -1
  11. package/dist/lib/node/packlets/testing/index.cjs +1198 -939
  12. package/dist/lib/node/packlets/testing/index.cjs.map +4 -4
  13. package/dist/types/src/packlets/identity/identity-manager.d.ts +7 -1
  14. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  15. package/dist/types/src/packlets/identity/identity-service.d.ts +7 -3
  16. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  17. package/dist/types/src/packlets/identity/identity.d.ts +3 -2
  18. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  19. package/dist/types/src/packlets/logging/logging-service.d.ts +6 -2
  20. package/dist/types/src/packlets/logging/logging-service.d.ts.map +1 -1
  21. package/dist/types/src/packlets/services/client-rpc-server.d.ts.map +1 -1
  22. package/dist/types/src/packlets/services/diagnostics.d.ts +46 -0
  23. package/dist/types/src/packlets/services/diagnostics.d.ts.map +1 -0
  24. package/dist/types/src/packlets/services/index.d.ts +1 -0
  25. package/dist/types/src/packlets/services/index.d.ts.map +1 -1
  26. package/dist/types/src/packlets/services/platform.d.ts +16 -0
  27. package/dist/types/src/packlets/services/platform.d.ts.map +1 -0
  28. package/dist/types/src/packlets/services/service-context.d.ts +2 -1
  29. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  30. package/dist/types/src/packlets/services/service-host.d.ts +7 -3
  31. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  32. package/dist/types/src/packlets/services/util.d.ts +6 -0
  33. package/dist/types/src/packlets/services/util.d.ts.map +1 -0
  34. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  35. package/dist/types/src/packlets/system/system-service.d.ts +12 -2
  36. package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
  37. package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts.map +1 -1
  38. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  39. package/dist/types/src/packlets/vault/worker-session.d.ts.map +1 -1
  40. package/dist/types/src/version.d.ts +2 -0
  41. package/dist/types/src/version.d.ts.map +1 -0
  42. package/package.json +34 -33
  43. package/src/packlets/devices/devices-service.test.ts +2 -1
  44. package/src/packlets/identity/identity-manager.test.ts +15 -3
  45. package/src/packlets/identity/identity-manager.ts +27 -6
  46. package/src/packlets/identity/identity-service.test.ts +17 -2
  47. package/src/packlets/identity/identity-service.ts +23 -14
  48. package/src/packlets/identity/identity.test.ts +7 -6
  49. package/src/packlets/identity/identity.ts +8 -3
  50. package/src/packlets/invitations/device-invitation-protocol.test.ts +2 -1
  51. package/src/packlets/logging/logging-service.ts +17 -9
  52. package/src/packlets/network/network-service.test.ts +2 -1
  53. package/src/packlets/services/client-rpc-server.ts +4 -4
  54. package/src/packlets/services/diagnostics.ts +211 -0
  55. package/src/packlets/services/index.ts +1 -0
  56. package/src/packlets/services/platform.ts +48 -0
  57. package/src/packlets/services/service-context.test.ts +59 -0
  58. package/src/packlets/services/service-context.ts +16 -9
  59. package/src/packlets/services/service-host.test.ts +11 -10
  60. package/src/packlets/services/service-host.ts +38 -18
  61. package/src/packlets/services/service-registry.test.ts +3 -1
  62. package/src/packlets/services/util.ts +33 -0
  63. package/src/packlets/spaces/data-space-manager.test.ts +80 -22
  64. package/src/packlets/spaces/data-space.ts +17 -5
  65. package/src/packlets/spaces/spaces-service.test.ts +2 -1
  66. package/src/packlets/system/system-service.test.ts +1 -0
  67. package/src/packlets/system/system-service.ts +34 -2
  68. package/src/packlets/testing/test-builder.ts +1 -1
  69. package/src/packlets/vault/iframe-host-runtime.ts +2 -1
  70. package/src/packlets/vault/worker-runtime.ts +3 -2
  71. package/src/packlets/vault/worker-session.ts +6 -1
  72. package/src/version.ts +1 -0
  73. package/dist/lib/browser/chunk-FD4L7N4K.mjs.map +0 -7
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Event } from '@dxos/async';
6
+ import { Context } from '@dxos/context';
6
7
  import { createCredentialSignerWithKey, CredentialGenerator } from '@dxos/credentials';
7
8
  import { MetadataStore, SpaceManager, SwarmIdentity } from '@dxos/echo-pipeline';
8
9
  import { FeedStore } from '@dxos/feed-store';
@@ -13,8 +14,9 @@ import { log } from '@dxos/log';
13
14
  import { trace } from '@dxos/protocols';
14
15
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
15
16
  import { IdentityRecord, SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
16
- import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
17
+ import { AdmittedFeed, ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
17
18
  import { Timeframe } from '@dxos/timeframe';
19
+ import { trace as Trace } from '@dxos/tracing';
18
20
  import { deferFunction } from '@dxos/util';
19
21
 
20
22
  import { createAuthProvider } from './authenticator';
@@ -46,6 +48,7 @@ export type CreateIdentityOptions = {
46
48
  };
47
49
 
48
50
  // TODO(dmaretskyi): Rename: represents the peer's state machine.
51
+ @Trace.resource()
49
52
  export class IdentityManager {
50
53
  readonly stateUpdate = new Event();
51
54
 
@@ -64,7 +67,8 @@ export class IdentityManager {
64
67
  return this._identity;
65
68
  }
66
69
 
67
- async open() {
70
+ @Trace.span()
71
+ async open(ctx: Context) {
68
72
  const traceId = PublicKey.random().toHex();
69
73
  log.trace('dxos.halo.identity-manager.open', trace.begin({ id: traceId }));
70
74
  await this._metadataStore.load();
@@ -73,7 +77,7 @@ export class IdentityManager {
73
77
  log('identity record', { identityRecord });
74
78
  if (identityRecord) {
75
79
  this._identity = await this._constructIdentity(identityRecord);
76
- await this._identity.open();
80
+ await this._identity.open(ctx);
77
81
  await this._identity.ready();
78
82
  log.trace('dxos.halo.identity', {
79
83
  identityKey: identityRecord.identityKey,
@@ -85,7 +89,7 @@ export class IdentityManager {
85
89
  }
86
90
 
87
91
  async close() {
88
- await this._identity?.close();
92
+ await this._identity?.close(new Context());
89
93
  }
90
94
 
91
95
  async createIdentity({ displayName }: CreateIdentityOptions = {}) {
@@ -105,7 +109,7 @@ export class IdentityManager {
105
109
  };
106
110
 
107
111
  const identity = await this._constructIdentity(identityRecord);
108
- await identity.open();
112
+ await identity.open(new Context());
109
113
 
110
114
  {
111
115
  const generator = new CredentialGenerator(this._keyring, identityRecord.identityKey, identityRecord.deviceKey);
@@ -175,7 +179,7 @@ export class IdentityManager {
175
179
  };
176
180
  const identity = await this._constructIdentity(identityRecord);
177
181
 
178
- await identity.open();
182
+ await identity.open(new Context());
179
183
  this._identity = identity;
180
184
  await this._metadataStore.setIdentityRecord(identityRecord);
181
185
  await this._identity.ready();
@@ -189,6 +193,23 @@ export class IdentityManager {
189
193
  return identity;
190
194
  }
191
195
 
196
+ /**
197
+ * Update the profile document of an existing identity.
198
+ */
199
+ async updateProfile(profile: ProfileDocument) {
200
+ invariant(this._identity, 'Identity not initialized.');
201
+ // TODO(wittjosiah): Use CredentialGenerator.
202
+ const credential = await this._identity.getIdentityCredentialSigner().createCredential({
203
+ subject: this._identity.identityKey,
204
+ assertion: {
205
+ '@type': 'dxos.halo.credentials.IdentityProfile',
206
+ profile,
207
+ },
208
+ });
209
+ const receipt = await this._identity.controlPipeline.writer.write({ credential: { credential } });
210
+ await this._identity.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
211
+ }
212
+
192
213
  private async _constructIdentity(identityRecord: IdentityRecord) {
193
214
  invariant(!this._identity);
194
215
  log('constructing identity', { identityRecord });
@@ -6,6 +6,7 @@ import chai, { expect } from 'chai';
6
6
  import chaiAsPromised from 'chai-as-promised';
7
7
 
8
8
  import { Trigger } from '@dxos/async';
9
+ import { Context } from '@dxos/context';
9
10
  import { PublicKey } from '@dxos/keys';
10
11
  import { Identity, IdentityService } from '@dxos/protocols/proto/dxos/client/services';
11
12
  import { afterEach, afterTest, beforeEach, describe, test } from '@dxos/test';
@@ -22,8 +23,12 @@ describe('IdentityService', () => {
22
23
 
23
24
  beforeEach(async () => {
24
25
  serviceContext = createServiceContext();
25
- await serviceContext.open();
26
- identityService = new IdentityServiceImpl(serviceContext);
26
+ await serviceContext.open(new Context());
27
+ identityService = new IdentityServiceImpl(
28
+ (options) => serviceContext.createIdentity(options),
29
+ serviceContext.identityManager,
30
+ serviceContext.keyring,
31
+ );
27
32
  });
28
33
 
29
34
  afterEach(async () => {
@@ -54,6 +59,16 @@ describe('IdentityService', () => {
54
59
 
55
60
  describe.skip('recoverIdentity', () => {});
56
61
 
62
+ describe('updateProfile', () => {
63
+ test('updates profile', async () => {
64
+ const identity = await identityService.createIdentity({});
65
+ expect(identity.profile?.displayName).to.be.undefined;
66
+
67
+ const updatedIdentity = await identityService.updateProfile({ displayName: 'Example' });
68
+ expect(updatedIdentity.profile?.displayName).to.equal('Example');
69
+ });
70
+ });
71
+
57
72
  describe('queryIdentity', () => {
58
73
  test('returns undefined if no identity is available', async () => {
59
74
  const query = identityService.queryIdentity();
@@ -6,6 +6,7 @@ import { Stream } from '@dxos/codec-protobuf';
6
6
  import { signPresentation } from '@dxos/credentials';
7
7
  import { todo } from '@dxos/debug';
8
8
  import { invariant } from '@dxos/invariant';
9
+ import { Keyring } from '@dxos/keyring';
9
10
  import {
10
11
  Identity,
11
12
  IdentityService,
@@ -15,15 +16,17 @@ import {
15
16
  } from '@dxos/protocols/proto/dxos/client/services';
16
17
  import { Presentation, ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
17
18
 
18
- import { ServiceContext } from '../services';
19
+ import { CreateIdentityOptions, IdentityManager } from './identity-manager';
19
20
 
20
21
  export class IdentityServiceImpl implements IdentityService {
21
- // TODO(wittjosiah): Remove dependency on service context.
22
- constructor(private readonly _serviceContext: ServiceContext) {}
22
+ constructor(
23
+ private readonly _createIdentity: (params?: CreateIdentityOptions) => Promise<Identity>,
24
+ private readonly _identityManager: IdentityManager,
25
+ private readonly _keyring: Keyring,
26
+ ) {}
23
27
 
24
28
  async createIdentity(request: ProfileDocument): Promise<Identity> {
25
- await this._serviceContext.createIdentity(request);
26
-
29
+ await this._createIdentity(request);
27
30
  return this._getIdentity()!;
28
31
  }
29
32
 
@@ -36,30 +39,36 @@ export class IdentityServiceImpl implements IdentityService {
36
39
  const emitNext = () => next({ identity: this._getIdentity() });
37
40
 
38
41
  emitNext();
39
- return this._serviceContext.identityManager.stateUpdate.on(emitNext);
42
+ return this._identityManager.stateUpdate.on(emitNext);
40
43
  });
41
44
  }
42
45
 
43
46
  private _getIdentity(): Identity | undefined {
44
- if (!this._serviceContext.identityManager.identity) {
47
+ if (!this._identityManager.identity) {
45
48
  return undefined;
46
49
  }
47
50
 
48
51
  return {
49
- identityKey: this._serviceContext.identityManager.identity.identityKey,
50
- spaceKey: this._serviceContext.identityManager.identity.space.key,
51
- profile: this._serviceContext.identityManager.identity.profileDocument,
52
+ identityKey: this._identityManager.identity.identityKey,
53
+ spaceKey: this._identityManager.identity.space.key,
54
+ profile: this._identityManager.identity.profileDocument,
52
55
  };
53
56
  }
54
57
 
58
+ async updateProfile(profile: ProfileDocument): Promise<Identity> {
59
+ invariant(this._identityManager.identity, 'Identity not initialized.');
60
+ await this._identityManager.updateProfile(profile);
61
+ return this._getIdentity()!;
62
+ }
63
+
55
64
  async signPresentation({ presentation, nonce }: SignPresentationRequest): Promise<Presentation> {
56
- invariant(this._serviceContext.identityManager.identity, 'Identity not initialized.');
65
+ invariant(this._identityManager.identity, 'Identity not initialized.');
57
66
 
58
67
  return await signPresentation({
59
68
  presentation,
60
- signer: this._serviceContext.keyring,
61
- signerKey: this._serviceContext.identityManager.identity.deviceKey,
62
- chain: this._serviceContext.identityManager.identity.deviceCredentialChain,
69
+ signer: this._keyring,
70
+ signerKey: this._identityManager.identity.deviceKey,
71
+ chain: this._identityManager.identity.deviceCredentialChain,
63
72
  nonce,
64
73
  });
65
74
  }
@@ -4,6 +4,7 @@
4
4
 
5
5
  import expect from 'expect';
6
6
 
7
+ import { Context } from '@dxos/context';
7
8
  import { CredentialGenerator, verifyCredential } from '@dxos/credentials';
8
9
  import {
9
10
  MetadataStore,
@@ -106,8 +107,8 @@ describe('identity/identity', () => {
106
107
  space,
107
108
  });
108
109
 
109
- await identity.open();
110
- afterTest(() => identity.close());
110
+ await identity.open(new Context());
111
+ afterTest(() => identity.close(new Context()));
111
112
 
112
113
  //
113
114
  // Identity genesis
@@ -219,8 +220,8 @@ describe('identity/identity', () => {
219
220
  space,
220
221
  }));
221
222
 
222
- await identity.open();
223
- afterTest(() => identity.close());
223
+ await identity.open(new Context());
224
+ afterTest(() => identity.close(new Context()));
224
225
 
225
226
  //
226
227
  // Identity genesis
@@ -310,8 +311,8 @@ describe('identity/identity', () => {
310
311
  space,
311
312
  }));
312
313
 
313
- await identity.open();
314
- afterTest(() => identity.close());
314
+ await identity.open(new Context());
315
+ afterTest(() => identity.close(new Context()));
315
316
  }
316
317
 
317
318
  //
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { Event } from '@dxos/async';
6
6
  import { AUTH_TIMEOUT, LOAD_CONTROL_FEEDS_TIMEOUT } from '@dxos/client-protocol';
7
+ import { Context } from '@dxos/context';
7
8
  import {
8
9
  DeviceStateMachine,
9
10
  CredentialSigner,
@@ -21,6 +22,7 @@ import { log } from '@dxos/log';
21
22
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
23
  import { AdmittedFeed, ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
23
24
  import { DeviceAdmissionRequest } from '@dxos/protocols/proto/dxos/halo/invitations';
25
+ import { trace } from '@dxos/tracing';
24
26
  import { ComplexSet } from '@dxos/util';
25
27
 
26
28
  import { TrustedKeySetAuthVerifier } from './authenticator';
@@ -35,6 +37,7 @@ export type IdentityParams = {
35
37
  /**
36
38
  * Agent identity manager, which includes the agent's Halo space.
37
39
  */
40
+ @trace.resource()
38
41
  export class Identity {
39
42
  public readonly space: Space;
40
43
  private readonly _signer: Signer;
@@ -76,13 +79,15 @@ export class Identity {
76
79
  return this._deviceStateMachine.authorizedDeviceKeys;
77
80
  }
78
81
 
79
- async open() {
82
+ @trace.span()
83
+ async open(ctx: Context) {
80
84
  await this.space.spaceState.addCredentialProcessor(this._deviceStateMachine);
81
85
  await this.space.spaceState.addCredentialProcessor(this._profileStateMachine);
82
- await this.space.open();
86
+ await this.space.open(ctx);
83
87
  }
84
88
 
85
- async close() {
89
+ @trace.span()
90
+ async close(ctx: Context) {
86
91
  await this.authVerifier.close();
87
92
  await this.space.spaceState.removeCredentialProcessor(this._profileStateMachine);
88
93
  await this.space.spaceState.removeCredentialProcessor(this._deviceStateMachine);
@@ -5,6 +5,7 @@
5
5
  import { expect } from 'chai';
6
6
 
7
7
  import { asyncChain } from '@dxos/async';
8
+ import { Context } from '@dxos/context';
8
9
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
9
10
  import { describe, test, afterTest } from '@dxos/test';
10
11
 
@@ -19,7 +20,7 @@ const closeAfterTest = async (peer: ServiceContext) => {
19
20
  describe('services/device', () => {
20
21
  test('creates identity', async () => {
21
22
  const peer = createServiceContext();
22
- await peer.open();
23
+ await peer.open(new Context());
23
24
  afterTest(() => peer.close());
24
25
 
25
26
  const identity = await peer.createIdentity();
@@ -13,6 +13,7 @@ import {
13
13
  ControlMetricsRequest,
14
14
  ControlMetricsResponse,
15
15
  QueryMetricsRequest,
16
+ QueryMetricsResponse,
16
17
  } from '@dxos/protocols/proto/dxos/client/services';
17
18
  import { jsonify, numericalValues, tracer } from '@dxos/util';
18
19
 
@@ -21,6 +22,7 @@ import { jsonify, numericalValues, tracer } from '@dxos/util';
21
22
  */
22
23
  export class LoggingServiceImpl implements LoggingService {
23
24
  private readonly _logs = new Event<NaturalLogEntry>();
25
+ private readonly _started = new Date();
24
26
 
25
27
  async open() {
26
28
  log.runtimeConfig.processors.push(this._logProcessor);
@@ -45,11 +47,14 @@ export class LoggingServiceImpl implements LoggingService {
45
47
  return { recording: tracer.recording };
46
48
  }
47
49
 
48
- queryMetrics({ interval }: QueryMetricsRequest): Stream<Metrics> {
50
+ /**
51
+ * @deprecated (Move to diagnostics).
52
+ */
53
+ queryMetrics({ interval = 5_000 }: QueryMetricsRequest): Stream<QueryMetricsResponse> {
49
54
  // TODO(burdon): Map all traces; how to bind to reducer/metrics shape (e.g., numericalValues)?
50
- const createNumericalValues = (key: string) => {
51
- const consume = tracer.get(key) ?? [];
52
- return { key, stats: numericalValues(consume, 'duration') };
55
+ const getNumericalValues = (key: string) => {
56
+ const events = tracer.get(key) ?? [];
57
+ return { key, stats: numericalValues(events, 'duration') };
53
58
  };
54
59
 
55
60
  return new Stream(({ next }) => {
@@ -57,16 +62,19 @@ export class LoggingServiceImpl implements LoggingService {
57
62
  const metrics: Metrics = {
58
63
  timestamp: new Date(),
59
64
  values: [
60
- createNumericalValues('dxos.echo.pipeline.control'),
61
- createNumericalValues('dxos.echo.pipeline.data'),
62
- ],
65
+ getNumericalValues('dxos.echo.pipeline.control'),
66
+ getNumericalValues('dxos.echo.pipeline.data'),
67
+ ].filter(Boolean) as Metrics.KeyPair[],
63
68
  };
64
69
 
65
- next(metrics);
70
+ next({
71
+ timestamp: new Date(),
72
+ metrics,
73
+ });
66
74
  };
67
75
 
68
76
  update();
69
- const i = setInterval(update, interval);
77
+ const i = setInterval(update, Math.max(interval, 1_000));
70
78
  return () => {
71
79
  clearInterval(i);
72
80
  };
@@ -5,6 +5,7 @@
5
5
  import { expect } from 'chai';
6
6
 
7
7
  import { Trigger } from '@dxos/async';
8
+ import { Context } from '@dxos/context';
8
9
  import { NetworkService, ConnectionState } from '@dxos/protocols/proto/dxos/client/services';
9
10
  import { afterEach, afterTest, beforeEach, describe, test } from '@dxos/test';
10
11
 
@@ -18,7 +19,7 @@ describe('NetworkService', () => {
18
19
 
19
20
  beforeEach(async () => {
20
21
  serviceContext = createServiceContext();
21
- await serviceContext.open();
22
+ await serviceContext.open(new Context());
22
23
  networkService = new NetworkServiceImpl(serviceContext.networkManager, serviceContext.signalManager);
23
24
  });
24
25
 
@@ -5,7 +5,7 @@
5
5
  import { ClientServices } from '@dxos/client-protocol';
6
6
  import { Any, ServiceHandler, Stream } from '@dxos/codec-protobuf';
7
7
  import { raise } from '@dxos/debug';
8
- import { parseMethodName, RpcPeer, RpcPeerOptions } from '@dxos/rpc';
8
+ import { parseMethodName, RpcPeer, RpcPeerOptions, ServiceBundle } from '@dxos/rpc';
9
9
  import { MaybePromise } from '@dxos/util';
10
10
 
11
11
  import { ServiceRegistry } from './service-registry';
@@ -74,16 +74,16 @@ export class ClientRpcServer {
74
74
  private _getServiceHandler(serviceName: string) {
75
75
  if (!this._handlerCache.has(serviceName)) {
76
76
  const [key, descriptor] =
77
- Object.entries(this._serviceRegistry.descriptors).find(
77
+ Object.entries(this._serviceRegistry.descriptors as ServiceBundle<Record<string, any>>).find(
78
78
  ([key, descriptor]) => descriptor.name === serviceName,
79
79
  ) ?? raise(new Error(`Service not available: ${serviceName}`));
80
80
 
81
- const service = this._serviceRegistry.services[key as keyof ClientServices];
81
+ const service = this._serviceRegistry.services[key as keyof ClientServices] as any;
82
82
  if (!service) {
83
83
  throw new Error(`Service not available: ${serviceName}`);
84
84
  }
85
85
 
86
- this._handlerCache.set(serviceName, descriptor.createServer(service as any));
86
+ this._handlerCache.set(serviceName, descriptor.createServer(service));
87
87
  }
88
88
 
89
89
  return this._handlerCache.get(serviceName)!;
@@ -0,0 +1,211 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { ClientServices } from '@dxos/client-protocol';
6
+ import { getFirstStreamValue } from '@dxos/codec-protobuf';
7
+ import { Config, ConfigProto } from '@dxos/config';
8
+ import { credentialTypeFilter } from '@dxos/credentials';
9
+ import { DocumentModel, DocumentModelState } from '@dxos/document-model';
10
+ import { invariant } from '@dxos/invariant';
11
+ import { PublicKey } from '@dxos/keys';
12
+ import { log } from '@dxos/log';
13
+ import { STORAGE_VERSION } from '@dxos/protocols';
14
+ import {
15
+ Device,
16
+ Identity,
17
+ Metrics,
18
+ Space as SpaceProto,
19
+ SpaceMember,
20
+ } from '@dxos/protocols/proto/dxos/client/services';
21
+ import { SubscribeToFeedsResponse } from '@dxos/protocols/proto/dxos/devtools/host';
22
+ import { Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
23
+
24
+ import { DXOS_VERSION } from '../../version';
25
+ import { DataSpace } from '../spaces';
26
+ import { getPlatform, Platform } from './platform';
27
+ import { ServiceContext } from './service-context';
28
+
29
+ const DEFAULT_TIMEOUT = 1_000;
30
+
31
+ export type Diagnostics = {
32
+ created: string;
33
+ platform: Platform;
34
+ config?: ConfigProto;
35
+ client: {
36
+ version: string;
37
+ storage: {
38
+ version: number;
39
+ };
40
+ };
41
+ identity?: Identity;
42
+ devices?: Device[];
43
+ spaces?: SpaceStats[];
44
+ feeds?: Partial<SubscribeToFeedsResponse.Feed>[];
45
+ metrics?: Metrics;
46
+ };
47
+
48
+ // TODO(burdon): Normalize for ECHO/HALO.
49
+ export type SpaceStats = {
50
+ key: PublicKey;
51
+ properties?: {
52
+ name: string;
53
+ };
54
+ db?: {
55
+ objects: number;
56
+ };
57
+ metrics?: SpaceProto.Metrics & {
58
+ startupTime?: number;
59
+ };
60
+ epochs?: (Epoch & { id?: PublicKey })[];
61
+ members?: SpaceMember[];
62
+ pipeline?: SpaceProto.PipelineState;
63
+ };
64
+
65
+ /**
66
+ * Create diagnostics to provide snapshot of current system state.
67
+ */
68
+ export const createDiagnostics = async (
69
+ clientServices: Partial<ClientServices>,
70
+ serviceContext: ServiceContext,
71
+ config: Config,
72
+ ): Promise<Diagnostics> => {
73
+ const diagnostics: Diagnostics = {
74
+ created: new Date().toISOString(),
75
+ platform: getPlatform(),
76
+ client: {
77
+ version: DXOS_VERSION,
78
+ storage: {
79
+ version: STORAGE_VERSION,
80
+ },
81
+ },
82
+ };
83
+
84
+ // Trace metrics.
85
+ // TODO(burdon): Move here from logging service?
86
+ {
87
+ invariant(clientServices.LoggingService, 'SystemService is not available.');
88
+ diagnostics.metrics = await getFirstStreamValue(clientServices.LoggingService.queryMetrics({}), {
89
+ timeout: DEFAULT_TIMEOUT,
90
+ }).catch(() => undefined);
91
+ }
92
+
93
+ const identity = serviceContext.identityManager.identity;
94
+ if (identity) {
95
+ // Identity.
96
+ diagnostics.identity = {
97
+ identityKey: identity.identityKey,
98
+ spaceKey: identity.space.key,
99
+ profile: identity.profileDocument,
100
+ };
101
+
102
+ // Devices.
103
+ const { devices } =
104
+ (await getFirstStreamValue(clientServices.DevicesService!.queryDevices(), {
105
+ timeout: DEFAULT_TIMEOUT,
106
+ }).catch(() => undefined)) ?? {};
107
+ diagnostics.devices = devices;
108
+
109
+ // TODO(dmaretskyi): Add metrics for halo space.
110
+
111
+ // Spaces.
112
+ if (serviceContext.dataSpaceManager) {
113
+ diagnostics.spaces = await Promise.all(
114
+ Array.from(serviceContext.dataSpaceManager.spaces.values()).map((space) => getSpaceStats(space)) ?? [],
115
+ );
116
+ }
117
+
118
+ // Feeds.
119
+ const { feeds = [] } =
120
+ (await getFirstStreamValue(clientServices.DevtoolsHost!.subscribeToFeeds({}), {
121
+ timeout: DEFAULT_TIMEOUT,
122
+ }).catch(() => undefined)) ?? {};
123
+ diagnostics.feeds = feeds.map(({ feedKey, bytes, length }) => ({ feedKey, bytes, length }));
124
+ }
125
+
126
+ diagnostics.config = config.values;
127
+
128
+ return diagnostics;
129
+ };
130
+
131
+ const getProperties = (space: DataSpace) => {
132
+ let properties: any = {};
133
+ try {
134
+ // Add properties to cache.
135
+ const propertiesItem = space.dataPipeline.itemManager.items.find(
136
+ (item) =>
137
+ item.modelMeta?.type === DocumentModel.meta.type &&
138
+ (item.state as DocumentModelState)?.type === 'dxos.sdk.client.Properties',
139
+ );
140
+
141
+ const state = propertiesItem?.state as DocumentModelState;
142
+ properties = state?.data;
143
+ } catch (err: any) {
144
+ log.warn(err.message);
145
+ }
146
+
147
+ return properties;
148
+ };
149
+
150
+ const getSpaceStats = async (space: DataSpace): Promise<SpaceStats> => {
151
+ const stats: SpaceStats = {
152
+ key: space.key,
153
+ metrics: space.metrics,
154
+
155
+ epochs: space.inner.spaceState.credentials
156
+ .filter(credentialTypeFilter('dxos.halo.credentials.Epoch'))
157
+ .map((credential) => ({
158
+ ...credential.subject.assertion,
159
+ id: credential.id,
160
+ })),
161
+
162
+ members: Array.from(space.inner.spaceState.members.values()).map((member) => ({
163
+ identity: {
164
+ identityKey: member.key,
165
+ profile: {
166
+ displayName: member.assertion.profile?.displayName,
167
+ },
168
+ },
169
+ presence:
170
+ space.presence.getPeersOnline().filter(({ identityKey }) => identityKey.equals(member.key)).length > 0
171
+ ? SpaceMember.PresenceState.ONLINE
172
+ : SpaceMember.PresenceState.OFFLINE,
173
+ })),
174
+
175
+ pipeline: {
176
+ // TODO(burdon): Pick properties from credentials if needed.
177
+ // currentEpoch: space.dataPipeline.currentEpoch,
178
+ // appliedEpoch: space.dataPipeline.appliedEpoch,
179
+
180
+ controlFeeds: space.inner.controlPipeline.state.feeds.map((feed) => feed.key),
181
+ currentControlTimeframe: space.inner.controlPipeline.state.timeframe,
182
+ targetControlTimeframe: space.inner.controlPipeline.state.targetTimeframe,
183
+ totalControlTimeframe: space.inner.controlPipeline.state.endTimeframe,
184
+
185
+ // TODO(burdon): Empty?
186
+ dataFeeds: space.dataPipeline.pipelineState?.feeds.map((feed) => feed.key) ?? [],
187
+ startDataTimeframe: space.dataPipeline.pipelineState?.startTimeframe,
188
+ currentDataTimeframe: space.dataPipeline.pipelineState?.timeframe,
189
+ targetDataTimeframe: space.dataPipeline.pipelineState?.targetTimeframe,
190
+ totalDataTimeframe: space.dataPipeline.pipelineState?.endTimeframe,
191
+ },
192
+ };
193
+
194
+ // TODO(burdon): May not be open?
195
+ if (space.dataPipeline.itemManager) {
196
+ Object.assign(stats, {
197
+ properties: getProperties(space),
198
+ db: {
199
+ objects: space.dataPipeline.itemManager.entities.size,
200
+ },
201
+ } as SpaceStats);
202
+ }
203
+
204
+ // TODO(burdon): Factor out.
205
+ if (stats.metrics) {
206
+ const { open, ready } = stats.metrics;
207
+ stats.metrics.startupTime = open && ready && ready.getTime() - open.getTime();
208
+ }
209
+
210
+ return stats;
211
+ };
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './client-rpc-server';
6
+ export * from './diagnostics';
6
7
  export * from './service-context';
7
8
  export * from './service-host';
8
9
  export * from './service-registry';