@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
@@ -0,0 +1,48 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export type Platform = {
6
+ type: 'browser' | 'shared-worker' | 'node';
7
+ userAgent?: string;
8
+ platform?: string;
9
+ runtime?: string;
10
+ uptime?: number;
11
+ memory?: {
12
+ rss: number;
13
+ heapTotal: number;
14
+ heapUsed: number;
15
+ external: number;
16
+ arrayBuffers: number;
17
+ };
18
+ };
19
+
20
+ export const getPlatform = (): Platform => {
21
+ if ((process as any).browser) {
22
+ if (typeof window !== 'undefined') {
23
+ // Browser.
24
+ const { userAgent } = window.navigator;
25
+ return {
26
+ type: 'browser',
27
+ userAgent,
28
+ uptime: Math.floor((Date.now() - window.performance.timeOrigin) / 1_000),
29
+ };
30
+ } else {
31
+ // Shared worker.
32
+ return {
33
+ type: 'shared-worker',
34
+ uptime: Math.floor((Date.now() - performance.timeOrigin) / 1_000),
35
+ };
36
+ }
37
+ } else {
38
+ // Node.
39
+ const { platform, version, arch } = process;
40
+ return {
41
+ type: 'node',
42
+ platform: `${platform} ${version} ${arch}`,
43
+ runtime: process.version,
44
+ uptime: Math.floor(process.uptime()),
45
+ memory: process.memoryUsage(),
46
+ };
47
+ }
48
+ };
@@ -2,9 +2,18 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { expect } from 'chai';
6
+
7
+ import { DocumentModel, DocumentModelState, MutationBuilder } from '@dxos/document-model';
8
+ import { createModelMutation, encodeModelMutation, genesisMutation } from '@dxos/echo-db';
9
+ import { WriteReceipt } from '@dxos/feed-store';
10
+ import { PublicKey } from '@dxos/keys';
11
+ import { log } from '@dxos/log';
5
12
  import { MemorySignalManagerContext } from '@dxos/messaging';
6
13
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
7
14
  import { describe, test } from '@dxos/test';
15
+ import { Timeframe } from '@dxos/timeframe';
16
+ import { range } from '@dxos/util';
8
17
 
9
18
  import { createServiceContext, syncItemsLocal } from '../testing';
10
19
  import { performInvitation } from '../testing/invitation-utils';
@@ -16,11 +25,61 @@ describe('services/ServiceContext', () => {
16
25
  await device1.createIdentity();
17
26
  const space1 = await device1.dataSpaceManager!.createSpace();
18
27
 
28
+ const itemId = PublicKey.random().toHex();
29
+ await space1.dataPipeline.databaseHost!.getWriteStream()?.write({
30
+ batch: genesisMutation(itemId, DocumentModel.meta.type),
31
+ });
32
+
33
+ let counter = 0;
34
+
35
+ for (const _ in range(5)) {
36
+ const receipts: WriteReceipt[] = [];
37
+ for (const _ in range(50)) {
38
+ receipts.push(
39
+ await space1.dataPipeline.databaseHost!.getWriteStream()!.write({
40
+ batch: createModelMutation(
41
+ itemId,
42
+ encodeModelMutation(DocumentModel.meta, new MutationBuilder().set('counter', ++counter).build()),
43
+ ),
44
+ }),
45
+ );
46
+ }
47
+
48
+ await space1.dataPipeline.pipelineState!.waitUntilTimeframe(
49
+ Timeframe.merge(...receipts.map((receipt) => new Timeframe([[receipt.feedKey, receipt.seq]]))),
50
+ );
51
+
52
+ await space1.createEpoch();
53
+ log.info('epoch', { number: space1.dataPipeline.currentEpoch?.subject.assertion.number });
54
+ }
55
+
19
56
  const device2 = createServiceContext({ signalContext: networkContext });
20
57
  await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
21
58
 
22
59
  await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
23
60
  const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
61
+
62
+ log.info('peer 2', {
63
+ currentEpoch: space2!.dataPipeline.currentEpoch?.subject.assertion.number,
64
+ appliedEpoch: space2!.dataPipeline.appliedEpoch?.subject.assertion.number,
65
+ });
66
+
67
+ await space2?.dataPipeline.onNewEpoch.waitForCondition(
68
+ () =>
69
+ space2!.dataPipeline.appliedEpoch?.subject.assertion.number ===
70
+ space1.dataPipeline.currentEpoch?.subject.assertion.number,
71
+ );
72
+
73
+ expect(space2!.dataPipeline.currentEpoch!.subject.assertion.number).to.equal(
74
+ space1.dataPipeline.currentEpoch!.subject.assertion.number,
75
+ );
76
+ expect(space2!.dataPipeline.appliedEpoch!.subject.assertion.number).to.equal(
77
+ space1.dataPipeline.appliedEpoch!.subject.assertion.number,
78
+ );
79
+
80
+ const item2 = space2!.dataPipeline.itemManager.entities.get(itemId);
81
+ expect((item2!.state as DocumentModelState).data.counter).to.equal(counter);
82
+
24
83
  await syncItemsLocal(space1.dataPipeline, space2!.dataPipeline);
25
84
  });
26
85
 
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Trigger } from '@dxos/async';
6
+ import { Context } from '@dxos/context';
6
7
  import { CredentialProcessor, getCredentialAssertion } from '@dxos/credentials';
7
8
  import { failUndefined } from '@dxos/debug';
8
9
  import {
@@ -26,6 +27,8 @@ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
26
27
  import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
27
28
  import { Storage } from '@dxos/random-access-storage';
28
29
  import { BlobStore } from '@dxos/teleport-extension-object-sync';
30
+ import { trace as Trace } from '@dxos/tracing';
31
+ import { safeInstanceof } from '@dxos/util';
29
32
 
30
33
  import { CreateIdentityOptions, IdentityManager, JoinIdentityParams } from '../identity';
31
34
  import {
@@ -40,6 +43,9 @@ import { DataSpaceManager, SigningContext } from '../spaces';
40
43
  * Shared backend for all client services.
41
44
  */
42
45
  // TODO(burdon): Rename/break-up into smaller components. And/or make members private.
46
+ // TODO(dmaretskyi): Gets duplicated in CJS build between normal and testing bundles.
47
+ @safeInstanceof('dxos.client-services.ServiceContext')
48
+ @Trace.resource()
43
49
  export class ServiceContext {
44
50
  public readonly initialized = new Trigger();
45
51
  public readonly dataServiceSubscriptions = new DataServiceSubscriptions();
@@ -121,21 +127,21 @@ export class ServiceContext {
121
127
  );
122
128
  }
123
129
 
124
- async open() {
125
- log.trace('dxos.sdk.service-context.open', trace.begin({ id: this._instanceId }));
126
-
130
+ @Trace.span()
131
+ async open(ctx: Context) {
127
132
  await this._checkStorageVersion();
128
133
 
129
134
  log('opening...');
135
+ log.trace('dxos.sdk.service-context.open', trace.begin({ id: this._instanceId }));
130
136
  await this.signalManager.open();
131
137
  await this.networkManager.open();
132
138
  await this.spaceManager.open();
133
- await this.identityManager.open();
139
+ await this.identityManager.open(ctx);
134
140
  if (this.identityManager.identity) {
135
- await this._initialize();
141
+ await this._initialize(ctx);
136
142
  }
137
- log('opened');
138
143
  log.trace('dxos.sdk.service-context.open', trace.end({ id: this._instanceId }));
144
+ log('opened');
139
145
  }
140
146
 
141
147
  async close() {
@@ -156,7 +162,7 @@ export class ServiceContext {
156
162
  async createIdentity(params: CreateIdentityOptions = {}) {
157
163
  const identity = await this.identityManager.createIdentity(params);
158
164
 
159
- await this._initialize();
165
+ await this._initialize(new Context());
160
166
  return identity;
161
167
  }
162
168
 
@@ -169,7 +175,7 @@ export class ServiceContext {
169
175
  private async _acceptIdentity(params: JoinIdentityParams) {
170
176
  const identity = await this.identityManager.acceptIdentity(params);
171
177
 
172
- await this._initialize();
178
+ await this._initialize(new Context());
173
179
  return identity;
174
180
  }
175
181
 
@@ -182,7 +188,8 @@ export class ServiceContext {
182
188
  }
183
189
 
184
190
  // Called when identity is created.
185
- private async _initialize() {
191
+ @Trace.span()
192
+ private async _initialize(ctx: Context) {
186
193
  log('initializing spaces...');
187
194
  const identity = this.identityManager.identity ?? failUndefined();
188
195
  const signingContext: SigningContext = {
@@ -8,6 +8,7 @@ import { rmSync } from 'node:fs';
8
8
 
9
9
  import { asyncTimeout, latch, Trigger } from '@dxos/async';
10
10
  import { Config } from '@dxos/config';
11
+ import { Context } from '@dxos/context';
11
12
  import { verifyPresentation } from '@dxos/credentials';
12
13
  import { PublicKey } from '@dxos/keys';
13
14
  import { MemorySignalManagerContext } from '@dxos/messaging';
@@ -30,7 +31,7 @@ describe('ClientServicesHost', () => {
30
31
 
31
32
  test('queryCredentials', async () => {
32
33
  const host = createServiceHost(new Config(), new MemorySignalManagerContext());
33
- await host.open();
34
+ await host.open(new Context());
34
35
  afterTest(() => host.close());
35
36
 
36
37
  await host.services.IdentityService!.createIdentity({});
@@ -49,14 +50,14 @@ describe('ClientServicesHost', () => {
49
50
 
50
51
  test('write and query credentials', async () => {
51
52
  const host = createServiceHost(new Config(), new MemorySignalManagerContext());
52
- await host.open();
53
+ await host.open(new Context());
53
54
  afterTest(() => host.close());
54
55
 
55
56
  await host.services.IdentityService!.createIdentity({});
56
57
 
57
58
  const testCredential = await createMockCredential({
58
- signer: host._serviceContext.keyring,
59
- issuer: host._serviceContext.identityManager.identity!.deviceKey,
59
+ signer: host.context.keyring,
60
+ issuer: host.context.identityManager.identity!.deviceKey,
60
61
  });
61
62
 
62
63
  // Test if Identity exposes haloSpace key.
@@ -86,14 +87,14 @@ describe('ClientServicesHost', () => {
86
87
 
87
88
  test('sign presentation', async () => {
88
89
  const host = createServiceHost(new Config(), new MemorySignalManagerContext());
89
- await host.open();
90
+ await host.open(new Context());
90
91
  afterTest(() => host.close());
91
92
 
92
93
  await host.services.IdentityService!.createIdentity({});
93
94
 
94
95
  const testCredential = await createMockCredential({
95
- signer: host._serviceContext.keyring,
96
- issuer: host._serviceContext.identityManager.identity!.deviceKey,
96
+ signer: host.context.keyring,
97
+ issuer: host.context.identityManager.identity!.deviceKey,
97
98
  });
98
99
 
99
100
  const nonce = new Uint8Array([0, 0, 0, 0]);
@@ -117,11 +118,11 @@ describe('ClientServicesHost', () => {
117
118
  });
118
119
  {
119
120
  const host = createServiceHost(config, new MemorySignalManagerContext());
120
- await host.open();
121
+ await host.open(new Context());
121
122
 
122
123
  await host.services.IdentityService?.createIdentity({});
123
124
 
124
- expect(host._serviceContext.storage.size).to.exist;
125
+ expect(host.context.storage.size).to.exist;
125
126
 
126
127
  await asyncTimeout(host.reset(), 1000);
127
128
  await host.close();
@@ -129,7 +130,7 @@ describe('ClientServicesHost', () => {
129
130
 
130
131
  {
131
132
  const host = createServiceHost(config, new MemorySignalManagerContext());
132
- await host.open();
133
+ await host.open(new Context());
133
134
  const trigger = new Trigger<Identity>();
134
135
 
135
136
  const stream = host.services.IdentityService?.queryIdentity();
@@ -5,6 +5,7 @@
5
5
  import { Event, synchronized } from '@dxos/async';
6
6
  import { clientServiceBundle, ClientServices } from '@dxos/client-protocol';
7
7
  import { Config } from '@dxos/config';
8
+ import { Context } from '@dxos/context';
8
9
  import { DocumentModel } from '@dxos/document-model';
9
10
  import { DataServiceImpl } from '@dxos/echo-pipeline';
10
11
  import { invariant } from '@dxos/invariant';
@@ -17,6 +18,7 @@ import { trace } from '@dxos/protocols';
17
18
  import { SystemStatus } from '@dxos/protocols/proto/dxos/client/services';
18
19
  import { Storage } from '@dxos/random-access-storage';
19
20
  import { TextModel } from '@dxos/text-model';
21
+ import { TRACE_PROCESSOR, trace as Trace } from '@dxos/tracing';
20
22
 
21
23
  import { DevicesServiceImpl } from '../devices';
22
24
  import { DevtoolsServiceImpl, DevtoolsHostEvents } from '../devtools';
@@ -28,6 +30,7 @@ import { NetworkServiceImpl } from '../network';
28
30
  import { SpacesServiceImpl } from '../spaces';
29
31
  import { createStorageObjects } from '../storage';
30
32
  import { SystemServiceImpl } from '../system';
33
+ import { createDiagnostics } from './diagnostics';
31
34
  import { ServiceContext } from './service-context';
32
35
  import { ServiceRegistry } from './service-registry';
33
36
 
@@ -64,11 +67,13 @@ export type InitializeOptions = {
64
67
  /**
65
68
  * Remote service implementation.
66
69
  */
70
+ @Trace.resource()
67
71
  export class ClientServicesHost {
68
72
  private readonly _resourceLock?: ResourceLock;
69
73
  private readonly _serviceRegistry: ServiceRegistry<ClientServices>;
70
74
  private readonly _systemService: SystemServiceImpl;
71
75
  private readonly _loggingService: LoggingServiceImpl;
76
+ private readonly _tracingService = TRACE_PROCESSOR.createTraceSender();
72
77
 
73
78
  private _config?: Config;
74
79
  private readonly _statusUpdate = new Event<void>();
@@ -78,17 +83,19 @@ export class ClientServicesHost {
78
83
  private _storage?: Storage;
79
84
  private _callbacks?: ClientServicesHostCallbacks;
80
85
 
81
- _serviceContext!: ServiceContext;
86
+ private _serviceContext!: ServiceContext;
87
+
88
+ @Trace.info()
82
89
  private _opening = false;
90
+
91
+ @Trace.info()
83
92
  private _open = false;
84
93
 
85
94
  constructor({
86
95
  config,
87
96
  modelFactory = createDefaultModelFactory(),
88
- // TODO(burdon): Create ApolloLink abstraction (see Client).
89
97
  transportFactory,
90
98
  signalManager,
91
- connectionLog,
92
99
  storage,
93
100
  // TODO(wittjosiah): Turn this on by default.
94
101
  lockKey,
@@ -107,7 +114,7 @@ export class ClientServicesHost {
107
114
  lockKey,
108
115
  onAcquire: () => {
109
116
  if (!this._opening) {
110
- void this.open();
117
+ void this.open(new Context());
111
118
  }
112
119
  },
113
120
  onRelease: () => this.close(),
@@ -116,11 +123,11 @@ export class ClientServicesHost {
116
123
 
117
124
  this._systemService = new SystemServiceImpl({
118
125
  config: this._config,
119
-
120
126
  statusUpdate: this._statusUpdate,
121
-
122
127
  getCurrentStatus: () => (this.isOpen ? SystemStatus.ACTIVE : SystemStatus.INACTIVE),
123
-
128
+ getDiagnostics: () => {
129
+ return createDiagnostics(this._serviceRegistry.services, this._serviceContext, this._config!);
130
+ },
124
131
  onUpdateStatus: async (status: SystemStatus) => {
125
132
  if (!this.isOpen && status === SystemStatus.ACTIVE) {
126
133
  await this._resourceLock?.acquire();
@@ -128,7 +135,6 @@ export class ClientServicesHost {
128
135
  await this._resourceLock?.release();
129
136
  }
130
137
  },
131
-
132
138
  onReset: async () => {
133
139
  await this.reset();
134
140
  },
@@ -136,9 +142,9 @@ export class ClientServicesHost {
136
142
 
137
143
  this._loggingService = new LoggingServiceImpl();
138
144
 
139
- // TODO(burdon): Start to think of DMG (dynamic services).
140
145
  this._serviceRegistry = new ServiceRegistry<ClientServices>(clientServiceBundle, {
141
146
  SystemService: this._systemService,
147
+ TracingService: this._tracingService,
142
148
  });
143
149
  }
144
150
 
@@ -146,6 +152,14 @@ export class ClientServicesHost {
146
152
  return this._open;
147
153
  }
148
154
 
155
+ get config() {
156
+ return this._config;
157
+ }
158
+
159
+ get context() {
160
+ return this._serviceContext;
161
+ }
162
+
149
163
  get serviceRegistry() {
150
164
  return this._serviceRegistry;
151
165
  }
@@ -165,10 +179,10 @@ export class ClientServicesHost {
165
179
  */
166
180
  initialize({ config, ...options }: InitializeOptions) {
167
181
  invariant(!this._open, 'service host is open');
182
+ log('initializing...');
168
183
 
169
184
  if (config) {
170
185
  invariant(!this._config, 'config already set');
171
-
172
186
  this._config = config;
173
187
  if (!this._storage) {
174
188
  this._storage = createStorageObjects(config.get('runtime.client.storage', {})!).storage;
@@ -190,16 +204,19 @@ export class ClientServicesHost {
190
204
  transportFactory,
191
205
  signalManager,
192
206
  });
207
+
208
+ log('initialized');
193
209
  }
194
210
 
195
211
  @synchronized
196
- async open() {
212
+ @Trace.span()
213
+ async open(ctx: Context) {
197
214
  if (this._open) {
198
215
  return;
199
216
  }
200
217
 
201
218
  const traceId = PublicKey.random().toHex();
202
- log.trace('dxos.sdk.client-services-host.open', trace.begin({ id: traceId }));
219
+ log.trace('dxos.client-services.host.open', trace.begin({ id: traceId }));
203
220
 
204
221
  invariant(this._config, 'config not set');
205
222
  invariant(this._storage, 'storage not set');
@@ -212,8 +229,6 @@ export class ClientServicesHost {
212
229
 
213
230
  await this._loggingService.open();
214
231
 
215
- // TODO(wittjosiah): Make re-entrant.
216
- // TODO(burdon): Break into components.
217
232
  this._serviceContext = new ServiceContext(
218
233
  this._storage,
219
234
  this._networkManager,
@@ -221,11 +236,14 @@ export class ClientServicesHost {
221
236
  this._modelFactory,
222
237
  );
223
238
 
224
- // TODO(burdon): Start to think of DMG (dynamic services).
225
239
  this._serviceRegistry.setServices({
226
240
  SystemService: this._systemService,
227
241
 
228
- IdentityService: new IdentityServiceImpl(this._serviceContext),
242
+ IdentityService: new IdentityServiceImpl(
243
+ (params) => this._serviceContext.createIdentity(params),
244
+ this._serviceContext.identityManager,
245
+ this._serviceContext.keyring,
246
+ ),
229
247
 
230
248
  InvitationsService: new InvitationsServiceImpl(this._serviceContext.invitations, (invitation) =>
231
249
  this._serviceContext.getInvitationHandler(invitation),
@@ -248,6 +266,7 @@ export class ClientServicesHost {
248
266
  NetworkService: new NetworkServiceImpl(this._serviceContext.networkManager, this._serviceContext.signalManager),
249
267
 
250
268
  LoggingService: this._loggingService,
269
+ TracingService: this._tracingService,
251
270
 
252
271
  // TODO(burdon): Move to new protobuf definitions.
253
272
  DevtoolsHost: new DevtoolsServiceImpl({
@@ -257,16 +276,17 @@ export class ClientServicesHost {
257
276
  }),
258
277
  });
259
278
 
260
- await this._serviceContext.open();
279
+ await this._serviceContext.open(ctx);
261
280
  this._opening = false;
262
281
  this._open = true;
263
282
  this._statusUpdate.emit();
264
283
  const deviceKey = this._serviceContext.identityManager.identity?.deviceKey;
265
284
  log('opened', { deviceKey });
266
- log.trace('dxos.sdk.client-services-host.open', trace.end({ id: traceId }));
285
+ log.trace('dxos.client-services.host.open', trace.end({ id: traceId }));
267
286
  }
268
287
 
269
288
  @synchronized
289
+ @Trace.span()
270
290
  async close() {
271
291
  if (!this._open) {
272
292
  return;
@@ -7,6 +7,7 @@ import { expect } from 'chai';
7
7
  import { Event } from '@dxos/async';
8
8
  import { ClientServices } from '@dxos/client-protocol';
9
9
  import { Config } from '@dxos/config';
10
+ import { Context } from '@dxos/context';
10
11
  import { log } from '@dxos/log';
11
12
  import { schema } from '@dxos/protocols';
12
13
  import { SystemService, SystemStatus } from '@dxos/protocols/proto/dxos/client/services';
@@ -31,12 +32,13 @@ describe('service registry', () => {
31
32
  test('builds a service registry', async () => {
32
33
  const remoteSource = 'https://remote.source';
33
34
  const serviceContext = createServiceContext();
34
- await serviceContext.open();
35
+ await serviceContext.open(new Context());
35
36
 
36
37
  const serviceRegistry = new ServiceRegistry(serviceBundle, {
37
38
  SystemService: new SystemServiceImpl({
38
39
  config: new Config({ runtime: { client: { remoteSource } } }),
39
40
  getCurrentStatus: () => SystemStatus.ACTIVE,
41
+ getDiagnostics: async () => ({}),
40
42
  onReset: () => {},
41
43
  onUpdateStatus: () => {},
42
44
  statusUpdate: new Event(),
@@ -0,0 +1,33 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { PublicKey } from '@dxos/keys';
6
+ import { humanize } from '@dxos/util';
7
+
8
+ // TODO(burdon): Move to util.
9
+
10
+ export type JsonStringifyOptions = {
11
+ truncate?: boolean;
12
+ humanize?: boolean;
13
+ };
14
+
15
+ export const jsonStringify = <T>(data: T, options: JsonStringifyOptions) => {
16
+ if (options.humanize || options.truncate) {
17
+ return JSON.parse(JSON.stringify(data, jsonKeyReplacer(options)));
18
+ }
19
+
20
+ return data;
21
+ };
22
+
23
+ const jsonKeyReplacer = (options: JsonStringifyOptions) => (key: string, value: any) => {
24
+ if (typeof value === 'string') {
25
+ // Infer if key.
26
+ const key = PublicKey.fromHex(value);
27
+ if (key.toHex() === value) {
28
+ return options.humanize ? humanize(key) : key.truncate();
29
+ }
30
+ }
31
+
32
+ return value;
33
+ };
@@ -13,9 +13,11 @@ import { writeMessages } from '@dxos/feed-store';
13
13
  import { PublicKey } from '@dxos/keys';
14
14
  import { log } from '@dxos/log';
15
15
  import { SpaceState } from '@dxos/protocols/proto/dxos/client/services';
16
+ import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
16
17
  import { Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
17
18
  import { StorageType } from '@dxos/random-access-storage';
18
19
  import { afterTest, describe, openAndClose, test } from '@dxos/test';
20
+ import { Timeframe } from '@dxos/timeframe';
19
21
  import { range } from '@dxos/util';
20
22
 
21
23
  import { TestBuilder, syncItemsLocal } from '../testing';
@@ -156,33 +158,34 @@ describe('DataSpaceManager', () => {
156
158
  });
157
159
 
158
160
  describe('Epochs', () => {
159
- test('Epoch truncates feeds', async () => {
160
- const builder = new TestBuilder();
161
- afterTest(async () => builder.destroy());
161
+ test
162
+ .skip('Epoch truncates feeds', async () => {
163
+ const builder = new TestBuilder();
164
+ afterTest(async () => builder.destroy());
162
165
 
163
- const peer = builder.createPeer({
164
- storageType: typeof window === 'undefined' ? StorageType.NODE : StorageType.WEBFS,
165
- });
166
- await peer.createIdentity();
167
- await openAndClose(peer.dataSpaceManager);
168
- const space = await peer.dataSpaceManager.createSpace();
169
- await space.inner.controlPipeline.state.waitUntilTimeframe(space.inner.controlPipeline.state.endTimeframe);
166
+ const peer = builder.createPeer({
167
+ storageType: typeof window === 'undefined' ? StorageType.NODE : StorageType.WEBFS,
168
+ });
169
+ await peer.createIdentity();
170
+ await openAndClose(peer.dataSpaceManager);
171
+ const space = await peer.dataSpaceManager.createSpace();
172
+ await space.inner.controlPipeline.state.waitUntilTimeframe(space.inner.controlPipeline.state.endTimeframe);
170
173
 
171
- const feedDataPath = path.join(space.inner.dataPipeline.pipelineState!.feeds[0].key.toHex(), 'data');
172
- const directory = peer.storage.createDirectory('feeds');
173
- const file = directory.getOrCreateFile(feedDataPath);
174
- afterTest(() => file.close());
174
+ const feedDataPath = path.join(space.inner.dataPipeline.pipelineState!.feeds[0].key.toHex(), 'data');
175
+ const directory = peer.storage.createDirectory('feeds');
176
+ const file = directory.getOrCreateFile(feedDataPath);
177
+ afterTest(() => file.close());
175
178
 
176
- expect((await file.stat()).size === 0).to.be.true;
179
+ expect((await file.stat()).size === 0).to.be.true;
177
180
 
178
- for (const _ in range(10)) {
179
- await testLocalDatabase(space.dataPipeline);
180
- }
181
+ for (const _ in range(10)) {
182
+ await testLocalDatabase(space.dataPipeline);
183
+ }
181
184
 
182
- expect((await file.stat()).size !== 0).to.be.true;
183
- await space.createEpoch();
184
- expect((await file.stat()).size === 0).to.be.true;
185
- })
185
+ expect((await file.stat()).size !== 0).to.be.true;
186
+ await space.createEpoch();
187
+ expect((await file.stat()).size === 0).to.be.true;
188
+ })
186
189
  .onlyEnvironments('nodejs', 'chromium', 'firefox')
187
190
  .tag('flaky');
188
191
 
@@ -228,6 +231,61 @@ describe('DataSpaceManager', () => {
228
231
  expect(epochs).to.deep.equal([epochsNumber]);
229
232
  }
230
233
  });
234
+
235
+ test('Items are cleared before epoch applied', async () => {
236
+ const builder = new TestBuilder();
237
+ afterTest(async () => builder.destroy());
238
+ const peer = builder.createPeer();
239
+ await peer.createIdentity();
240
+ await openAndClose(peer.dataSpaceManager);
241
+
242
+ // Create space and fill it with Items.
243
+ const space = await peer.dataSpaceManager.createSpace();
244
+ await space.inner.controlPipeline.state.waitUntilTimeframe(space.inner.controlPipeline.state.endTimeframe);
245
+ const itemsNumber = 2;
246
+ for (const _ of range(itemsNumber)) {
247
+ await testLocalDatabase(space.dataPipeline);
248
+ }
249
+
250
+ // Create empty Epoch and check if it clears items.
251
+ {
252
+ const processedFirstEpoch = space.dataPipeline.onNewEpoch.waitFor(
253
+ (epoch) => epoch.subject.assertion.number === 1,
254
+ );
255
+
256
+ // Empty snapshot.
257
+ const snapshot: SpaceSnapshot = {
258
+ spaceKey: space.key.asUint8Array(),
259
+ timeframe: space.inner.dataPipeline.pipelineState!.timeframe,
260
+ database: {},
261
+ };
262
+
263
+ const snapshotCid = await peer.snapshotStore.saveSnapshot(snapshot);
264
+
265
+ const epoch: Epoch = {
266
+ previousId: space.dataPipeline.currentEpoch?.id,
267
+ timeframe: space.inner.dataPipeline.pipelineState!.timeframe,
268
+ number: (space.dataPipeline.currentEpoch?.subject.assertion as Epoch).number + 1,
269
+ snapshotCid,
270
+ };
271
+
272
+ const receipt = await space.inner.controlPipeline.writer.write({
273
+ credential: {
274
+ credential: await peer.props.signingContext!.credentialSigner.createCredential({
275
+ subject: space.key,
276
+ assertion: {
277
+ '@type': 'dxos.halo.credentials.Epoch',
278
+ ...epoch,
279
+ },
280
+ }),
281
+ },
282
+ });
283
+ await space.inner.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
284
+ await processedFirstEpoch;
285
+ }
286
+
287
+ expect(Array.from(space.dataPipeline.itemManager.entities.keys()).length).to.equal(0);
288
+ });
231
289
  });
232
290
 
233
291
  describe('activation', () => {