@dxos/echo-pipeline 0.1.56-main.c19b7cd → 0.1.56-main.c277f68

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 (40) hide show
  1. package/dist/lib/browser/{chunk-EJ35NHZV.mjs → chunk-GDF4DQU2.mjs} +296 -272
  2. package/dist/lib/browser/chunk-GDF4DQU2.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1 -1
  4. package/dist/lib/browser/index.mjs.map +1 -1
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +12 -17
  7. package/dist/lib/browser/testing/index.mjs.map +1 -1
  8. package/dist/lib/node/index.cjs +300 -276
  9. package/dist/lib/node/index.cjs.map +3 -3
  10. package/dist/lib/node/meta.json +1 -1
  11. package/dist/lib/node/testing/index.cjs +313 -294
  12. package/dist/lib/node/testing/index.cjs.map +3 -3
  13. package/dist/types/src/db-host/data-service-host.d.ts.map +1 -1
  14. package/dist/types/src/pipeline/pipeline-stress.test.d.ts +2 -0
  15. package/dist/types/src/pipeline/pipeline-stress.test.d.ts.map +1 -0
  16. package/dist/types/src/pipeline/pipeline.d.ts +4 -3
  17. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  18. package/dist/types/src/space/control-pipeline.d.ts +4 -0
  19. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  20. package/dist/types/src/space/data-pipeline.d.ts +2 -0
  21. package/dist/types/src/space/data-pipeline.d.ts.map +1 -1
  22. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  23. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  24. package/dist/types/src/space/space.d.ts.map +1 -1
  25. package/dist/types/src/testing/database-test-rig.d.ts +4 -1
  26. package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
  27. package/package.json +32 -32
  28. package/src/db-host/data-service-host.ts +9 -2
  29. package/src/metadata/metadata-store.ts +1 -1
  30. package/src/pipeline/pipeline-stress.test.ts +226 -0
  31. package/src/pipeline/pipeline.test.ts +2 -214
  32. package/src/pipeline/pipeline.ts +47 -22
  33. package/src/space/control-pipeline.ts +47 -22
  34. package/src/space/data-pipeline.ts +20 -10
  35. package/src/space/space-manager.ts +1 -0
  36. package/src/space/space-protocol.ts +3 -1
  37. package/src/space/space.ts +1 -0
  38. package/src/testing/database-test-rig.ts +27 -2
  39. package/src/tests/database-unit.test.ts +39 -0
  40. package/dist/lib/browser/chunk-EJ35NHZV.mjs.map +0 -7
@@ -3,14 +3,8 @@
3
3
  //
4
4
 
5
5
  import expect from 'expect';
6
- import * as fc from 'fast-check';
7
- import { inspect } from 'util';
8
6
 
9
- import { Event, asyncTimeout, sleep } from '@dxos/async';
10
- import { FeedStore, FeedWrapper } from '@dxos/feed-store';
11
- import { PublicKey } from '@dxos/keys';
12
- import { log } from '@dxos/log';
13
- import { FeedMessageBlock } from '@dxos/protocols';
7
+ import { Event, sleep } from '@dxos/async';
14
8
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
15
9
  import { describe, test, afterTest } from '@dxos/test';
16
10
  import { Timeframe } from '@dxos/timeframe';
@@ -85,7 +79,7 @@ describe('pipeline/Pipeline', () => {
85
79
 
86
80
  const processedSequenceNumbers: number[] = [];
87
81
 
88
- // skip first 10, process the rest, and the repeat the last 10.
82
+ // Skip first 10, process the rest, and the repeat the last 10.
89
83
  const expectedSequenceNumbers = [...sequenceNumbers.slice(10, 30), ...sequenceNumbers.slice(20, 30)];
90
84
 
91
85
  await pipeline.setCursor(new Timeframe([[feed.key, 9]]));
@@ -144,210 +138,4 @@ describe('pipeline/Pipeline', () => {
144
138
  await processedEvent.waitForCondition(() => processedSequenceNumbers.length === 10);
145
139
  expect(processedSequenceNumbers).toEqual(expectedSequenceNumbers);
146
140
  });
147
-
148
- test
149
- .skip('stress', async () => {
150
- const builder = new TestFeedBuilder();
151
-
152
- const NUM_AGENTS = 2;
153
- const NUM_MESSAGES = 100;
154
-
155
- class Agent {
156
- public startingTimeframe = new Timeframe();
157
- public pipeline!: Pipeline;
158
- public feed!: FeedWrapper<FeedMessage>;
159
- public messages: FeedMessageBlock[] = [];
160
- public writePromise: Promise<any> = Promise.resolve();
161
-
162
- constructor(public id: string, public feedStore: FeedStore<FeedMessage>) {}
163
-
164
- async open() {
165
- const key = await builder.keyring.createKey();
166
- this.feed = await this.feedStore.openFeed(key, { writable: true });
167
- }
168
-
169
- async start() {
170
- this.pipeline = new Pipeline();
171
- await this.pipeline.setCursor(this.startingTimeframe);
172
-
173
- await this.pipeline.start();
174
-
175
- // NOTE: not awaiting here breaks the test.
176
- await Promise.all(this.feedStore.feeds.map((feed) => this.pipeline.addFeed(feed)));
177
- this.pipeline.setWriteFeed(this.feed);
178
-
179
- // consume in async task.
180
- void this.consume();
181
- }
182
-
183
- async stop() {
184
- await this.pipeline.stop();
185
- this.startingTimeframe = this.pipeline.state.timeframe;
186
- }
187
-
188
- async close() {
189
- await this.feed.close();
190
- }
191
-
192
- write(message: FeedMessage.Payload) {
193
- const prev = this.writePromise;
194
- const promise = this.pipeline.writer!.write(message);
195
- this.writePromise = Promise.all([prev, promise]);
196
- }
197
-
198
- async consume() {
199
- for await (const msg of this.pipeline.consume()) {
200
- this.messages.push(msg);
201
- }
202
- log('stopped consuming');
203
- }
204
- }
205
-
206
- type Model = {};
207
- type Real = {
208
- feedStore: FeedStore<FeedMessage>;
209
- agents: Map<string, Agent>;
210
- };
211
-
212
- class WriteCommand implements fc.AsyncCommand<Model, Real> {
213
- constructor(public agent: string, public count: number) {}
214
-
215
- check = () => true;
216
-
217
- async run(model: Model, real: Real) {
218
- console.log(`WriteCommand(${this.agent}, ${this.count})`);
219
-
220
- const agent = real.agents.get(this.agent)!;
221
-
222
- const toWrite = Math.min(this.count, NUM_MESSAGES - agent.feed.length);
223
- if (toWrite > 0) {
224
- for (const _ of range(toWrite)) {
225
- agent.write({}); // Content is not important.
226
- }
227
- }
228
- }
229
-
230
- toString = () => `WriteCommand(${this.agent}, ${this.count})`;
231
- }
232
-
233
- class SyncCommand implements fc.AsyncCommand<Model, Real> {
234
- check = () => true;
235
-
236
- async run(model: Model, real: Real) {
237
- console.log('SyncCommand()');
238
- const targets: any = {};
239
-
240
- try {
241
- for (const agent of real.agents.values()) {
242
- await agent.writePromise;
243
- }
244
-
245
- for (const agent of real.agents.values()) {
246
- targets[agent.id] = agent.pipeline.state.endTimeframe;
247
- if (agent.pipeline.state.endTimeframe.isEmpty()) {
248
- console.log('empty endtimeframe', {
249
- id: agent.id,
250
- endTimeframe: agent.pipeline.state.endTimeframe,
251
- feeds: agent.pipeline.getFeeds().map((feed) => [feed.key.toString(), feed.length]),
252
- });
253
- }
254
- await asyncTimeout(agent.pipeline.state.waitUntilTimeframe(agent.pipeline.state.endTimeframe), 1000);
255
- }
256
-
257
- const tf: Timeframe = real.agents.values().next().value!.pipeline.state.timeframe;
258
- for (const agent of real.agents.values()) {
259
- expect(agent.pipeline.state.timeframe.equals(tf)).toEqual(true);
260
- }
261
-
262
- const totalMessages = real.feedStore.feeds.reduce((acc, feed) => acc + feed.length, 0);
263
- for (const agent of real.agents.values()) {
264
- expect(agent.messages.length).toEqual(totalMessages);
265
- }
266
- } catch (err) {
267
- console.log(
268
- inspect(
269
- {
270
- agents: Array.from(real.agents.values()).map((agent) => ({
271
- id: agent.id,
272
- messages: agent.messages.length,
273
- feeds: agent.pipeline.getFeeds().map((feed) => [feed.key, feed.length]),
274
- timeframe: agent.pipeline.state.timeframe,
275
- endTimeframe: agent.pipeline.state.endTimeframe,
276
- })),
277
- feeds: real.feedStore.feeds.map((feed) => [feed.key, feed.length]),
278
- targets,
279
- },
280
- false,
281
- null,
282
- true,
283
- ),
284
- );
285
- throw err;
286
- }
287
- }
288
-
289
- toString = () => 'SyncCommand()';
290
- }
291
-
292
- class RestartCommand implements fc.AsyncCommand<Model, Real> {
293
- constructor(public agent: string) {}
294
-
295
- check = () => true;
296
-
297
- async run(model: Model, real: Real) {
298
- console.log(`RestartCommand(${this.agent})`);
299
-
300
- const agent = real.agents.get(this.agent)!;
301
-
302
- await agent.writePromise;
303
- await agent.stop();
304
- await agent.start();
305
- }
306
-
307
- toString = () => `RestartCommand(${this.agent})`;
308
- }
309
-
310
- const agentIds = range(NUM_AGENTS).map(() => PublicKey.random().toHex().slice(0, 8));
311
- const anAgentId = fc.constantFrom(...agentIds);
312
-
313
- const commands = fc.commands(
314
- [
315
- fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
316
- fc.constant(new SyncCommand()),
317
- anAgentId.map((agent) => new RestartCommand(agent)),
318
- ],
319
- { size: 'large' },
320
- );
321
-
322
- const model = fc.asyncProperty(commands, async (commands) => {
323
- const feedStore = builder.createFeedStore();
324
-
325
- const agents = new Map(agentIds.map((id) => [id, new Agent(id, feedStore)]));
326
- await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
327
- await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
328
-
329
- const setup: fc.ModelRunSetup<Model, Real> = () => ({
330
- model: {},
331
- real: {
332
- feedStore,
333
- agents,
334
- },
335
- });
336
-
337
- try {
338
- await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
339
- } finally {
340
- await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
341
- await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
342
- }
343
- });
344
-
345
- const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
346
- [[new WriteCommand(agentIds[0], 10), new WriteCommand(agentIds[1], 10), new SyncCommand()]],
347
- [[new WriteCommand(agentIds[0], 4), new RestartCommand(agentIds[0]), new SyncCommand()]],
348
- ];
349
-
350
- await fc.assert(model, { examples });
351
- })
352
- .timeout(60_000);
353
141
  });
@@ -204,9 +204,6 @@ export class Pipeline implements PipelineAccessor {
204
204
  private readonly _timeframeClock = new TimeframeClock(new Timeframe());
205
205
  private readonly _feeds = new ComplexMap<PublicKey, FeedWrapper<FeedMessage>>(PublicKey.hash);
206
206
 
207
- // Inbound feed stream.
208
- private _feedSetIterator?: FeedSetIterator<FeedMessage>;
209
-
210
207
  // External state accessor.
211
208
  private readonly _state: PipelineState = new PipelineState(this._feeds, this._timeframeClock);
212
209
 
@@ -214,14 +211,20 @@ export class Pipeline implements PipelineAccessor {
214
211
  private readonly _processingTrigger = new Trigger().wake();
215
212
  private readonly _pauseTrigger = new Trigger().wake();
216
213
 
217
- private _isStopping = false;
218
- private _isStarted = false;
219
- private _isOpen = false;
220
- private _isPaused = false;
214
+ // Pending downloads.
215
+ private readonly _downloads = new ComplexMap<FeedWrapper<FeedMessage>, any>((value) => PublicKey.hash(value.key));
216
+
217
+ // Inbound feed stream.
218
+ private _feedSetIterator?: FeedSetIterator<FeedMessage>;
221
219
 
222
220
  // Outbound feed writer.
223
221
  private _writer: FeedWriter<FeedMessage.Payload> | undefined;
224
222
 
223
+ private _isStopping = false;
224
+ private _isStarted = false;
225
+ private _isBeingConsumed = false;
226
+ private _isPaused = false;
227
+
225
228
  get state() {
226
229
  return this._state;
227
230
  }
@@ -243,10 +246,14 @@ export class Pipeline implements PipelineAccessor {
243
246
  // which might be opening feeds during the mutation processing, which w
244
247
  async addFeed(feed: FeedWrapper<FeedMessage>) {
245
248
  this._feeds.set(feed.key, feed);
249
+
246
250
  if (this._feedSetIterator) {
247
251
  await this._feedSetIterator.addFeed(feed);
248
252
  }
249
- this._setFeedDownloadState(feed);
253
+
254
+ if (this._isStarted && !this._isPaused) {
255
+ this._setFeedDownloadState(feed);
256
+ }
250
257
  }
251
258
 
252
259
  setWriteFeed(feed: FeedWrapper<FeedMessage>) {
@@ -270,12 +277,22 @@ export class Pipeline implements PipelineAccessor {
270
277
  await this._feedSetIterator!.open();
271
278
  this._isStarted = true;
272
279
  log('started');
280
+
281
+ if (!this._isPaused) {
282
+ for (const feed of this._feeds.values()) {
283
+ this._setFeedDownloadState(feed);
284
+ }
285
+ }
273
286
  }
274
287
 
275
288
  @synchronized
276
289
  async stop() {
277
290
  log('stopping...');
278
291
  this._isStopping = true;
292
+ for (const [feed, handle] of this._downloads.entries()) {
293
+ feed.undownload(handle);
294
+ }
295
+ this._downloads.clear();
279
296
  await this._feedSetIterator?.close();
280
297
  await this._processingTrigger.wait(); // Wait for the in-flight message to be processed.
281
298
  await this._state._ctx.dispose();
@@ -297,10 +314,6 @@ export class Pipeline implements PipelineAccessor {
297
314
  this._timeframeClock.setTimeframe(timeframe);
298
315
 
299
316
  // Cancel downloads of mutations before the cursor.
300
- for (const feed of this._feeds.values()) {
301
- this._setFeedDownloadState(feed);
302
- }
303
-
304
317
  if (this._feedSetIterator) {
305
318
  await this._feedSetIterator.close();
306
319
  await this._initIterator();
@@ -313,7 +326,6 @@ export class Pipeline implements PipelineAccessor {
313
326
  */
314
327
  @synchronized
315
328
  async pause() {
316
- invariant(this._isStarted, 'Pipeline is not open.');
317
329
  if (this._isPaused) {
318
330
  return;
319
331
  }
@@ -325,11 +337,14 @@ export class Pipeline implements PipelineAccessor {
325
337
 
326
338
  @synchronized
327
339
  async unpause() {
328
- invariant(this._isStarted, 'Pipeline is not open.');
329
340
  invariant(this._isPaused, 'Pipeline is not paused.');
330
341
 
331
342
  this._pauseTrigger.wake();
332
343
  this._isPaused = false;
344
+
345
+ for (const feed of this._feeds.values()) {
346
+ this._setFeedDownloadState(feed);
347
+ }
333
348
  }
334
349
 
335
350
  /**
@@ -337,8 +352,8 @@ export class Pipeline implements PipelineAccessor {
337
352
  * Updates the timeframe clock after the message has bee processed.
338
353
  */
339
354
  async *consume(): AsyncIterable<FeedMessageBlock> {
340
- invariant(!this._isOpen, 'Pipeline is already being consumed.');
341
- this._isOpen = true;
355
+ invariant(!this._isBeingConsumed, 'Pipeline is already being consumed.');
356
+ this._isBeingConsumed = true;
342
357
 
343
358
  invariant(this._feedSetIterator, 'Iterator not initialized.');
344
359
  let lastFeedSetIterator = this._feedSetIterator;
@@ -367,17 +382,27 @@ export class Pipeline implements PipelineAccessor {
367
382
  }
368
383
 
369
384
  // TODO(burdon): Test re-entrant?
370
- this._isOpen = false;
385
+ this._isBeingConsumed = false;
371
386
  }
372
387
 
373
388
  private _setFeedDownloadState(feed: FeedWrapper<FeedMessage>) {
374
- const timeframe = this._state._startTimeframe;
375
- const seq = timeframe.get(feed.key) ?? 0;
389
+ let handle = this._downloads.get(feed); // TODO(burdon): Always undefined.
390
+ if (handle) {
391
+ feed.undownload(handle);
392
+ }
376
393
 
377
- feed.undownload({ callback: () => log('undownload') });
378
- feed.download({ start: seq + 1, linear: true }).catch((err: Error) => {
379
- log('failed to download feed', { err });
394
+ const timeframe = this._state._startTimeframe;
395
+ const seq = timeframe.get(feed.key) ?? -1;
396
+ log.info('download', { feed: feed.key.truncate(), seq, length: feed.length });
397
+ handle = feed.download({ start: seq + 1, linear: true }, (err: any, data: any) => {
398
+ if (err) {
399
+ // log.error(err); // TODO(burdon): Feed is closed.
400
+ } else {
401
+ log.info('data', data); // TODO(burdon): Never called.
402
+ }
380
403
  });
404
+
405
+ this._downloads.set(feed, handle);
381
406
  }
382
407
 
383
408
  private async _initIterator() {
@@ -2,13 +2,16 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ import { Context } from '@dxos/context';
5
6
  import { SpaceStateMachine, SpaceState, MemberInfo, FeedInfo } from '@dxos/credentials';
6
7
  import { FeedWrapper } from '@dxos/feed-store';
7
8
  import { PublicKey } from '@dxos/keys';
8
9
  import { log } from '@dxos/log';
10
+ import { FeedMessageBlock } from '@dxos/protocols';
9
11
  import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
10
12
  import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
11
13
  import { Timeframe } from '@dxos/timeframe';
14
+ import { TimeSeriesCounter, TimeUsageCounter, trace } from '@dxos/tracing';
12
15
  import { AsyncCallback, Callback, tracer } from '@dxos/util';
13
16
 
14
17
  import { MetadataStore } from '../metadata';
@@ -26,6 +29,7 @@ const TIMEFRAME_SAVE_DEBOUNCE_INTERVAL = 500;
26
29
  /**
27
30
  * Processes HALO credentials, which include genesis and invitations.
28
31
  */
32
+ @trace.resource()
29
33
  export class ControlPipeline {
30
34
  private readonly _pipeline: Pipeline;
31
35
  private readonly _spaceStateMachine: SpaceStateMachine;
@@ -39,6 +43,12 @@ export class ControlPipeline {
39
43
  public readonly onMemberAdmitted: Callback<AsyncCallback<MemberInfo>>;
40
44
  public readonly onCredentialProcessed: Callback<AsyncCallback<Credential>>;
41
45
 
46
+ @trace.metricsCounter()
47
+ private _usage = new TimeUsageCounter();
48
+
49
+ @trace.metricsCounter()
50
+ private _mutations = new TimeSeriesCounter();
51
+
42
52
  constructor({ spaceKey, genesisFeed, feedProvider, metadataStore }: ControlPipelineParams) {
43
53
  this._spaceKey = spaceKey;
44
54
  this._metadata = metadataStore;
@@ -82,34 +92,49 @@ export class ControlPipeline {
82
92
  async start() {
83
93
  log('starting...');
84
94
  setTimeout(async () => {
85
- for await (const msg of this._pipeline.consume()) {
86
- try {
87
- // log('processing', { msg });
88
- log('processing', { key: msg.feedKey, seq: msg.seq });
89
- if (msg.data.payload.credential) {
90
- const timer = tracer.mark('dxos.echo.pipeline.control');
91
- const result = await this._spaceStateMachine.process(
92
- msg.data.payload.credential.credential,
93
- PublicKey.from(msg.feedKey),
94
- );
95
-
96
- timer.end();
97
- if (!result) {
98
- log.warn('processing failed', { msg });
99
- } else {
100
- await this._noteTargetStateIfNeeded(this._pipeline.state.pendingTimeframe);
101
- }
102
- }
103
- } catch (err: any) {
104
- log.catch(err);
105
- }
106
- }
95
+ void this._consumePipeline(new Context());
107
96
  });
108
97
 
109
98
  await this._pipeline.start();
110
99
  log('started');
111
100
  }
112
101
 
102
+ @trace.span()
103
+ private async _consumePipeline(ctx: Context) {
104
+ for await (const msg of this._pipeline.consume()) {
105
+ const span = this._usage.beginRecording();
106
+ this._mutations.inc();
107
+
108
+ try {
109
+ await this._processMessage(ctx, msg);
110
+ } catch (err: any) {
111
+ log.catch(err);
112
+ }
113
+
114
+ span.end();
115
+ }
116
+ }
117
+
118
+ @trace.span()
119
+ private async _processMessage(ctx: Context, msg: FeedMessageBlock) {
120
+ // log('processing', { msg });
121
+ log('processing', { key: msg.feedKey, seq: msg.seq });
122
+ if (msg.data.payload.credential) {
123
+ const timer = tracer.mark('dxos.echo.pipeline.control');
124
+ const result = await this._spaceStateMachine.process(
125
+ msg.data.payload.credential.credential,
126
+ PublicKey.from(msg.feedKey),
127
+ );
128
+
129
+ timer.end();
130
+ if (!result) {
131
+ log.warn('processing failed', { msg });
132
+ } else {
133
+ await this._noteTargetStateIfNeeded(this._pipeline.state.pendingTimeframe);
134
+ }
135
+ }
136
+ }
137
+
113
138
  private async _noteTargetStateIfNeeded(timeframe: Timeframe) {
114
139
  // TODO(dmaretskyi): Replace this with a proper debounce/throttle.
115
140
 
@@ -5,12 +5,12 @@
5
5
  import { Event, scheduleTask, synchronized, trackLeaks } from '@dxos/async';
6
6
  import { Context } from '@dxos/context';
7
7
  import { CredentialProcessor, FeedInfo, SpecificCredential, checkCredentialType } from '@dxos/credentials';
8
- import { getStateMachineFromItem, ItemManager } from '@dxos/echo-db';
8
+ import { getStateMachineFromItem, ItemManager, TYPE_PROPERTIES } from '@dxos/echo-db';
9
9
  import { CancelledError } from '@dxos/errors';
10
10
  import { FeedWriter } from '@dxos/feed-store';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { PublicKey } from '@dxos/keys';
13
- import { log } from '@dxos/log';
13
+ import { log, omit } from '@dxos/log';
14
14
  import { ModelFactory } from '@dxos/model-factory';
15
15
  import { DataPipelineProcessed } from '@dxos/protocols';
16
16
  import { DataMessage } from '@dxos/protocols/proto/dxos/echo/feed';
@@ -19,6 +19,7 @@ import { ObjectSnapshot } from '@dxos/protocols/proto/dxos/echo/model/document';
19
19
  import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
20
20
  import { Credential, Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
21
21
  import { Timeframe } from '@dxos/timeframe';
22
+ import { TimeSeriesCounter, TimeUsageCounter, trace } from '@dxos/tracing';
22
23
  import { tracer } from '@dxos/util';
23
24
 
24
25
  import { DatabaseHost, SnapshotManager } from '../db-host';
@@ -65,6 +66,7 @@ const TIMEFRAME_SAVE_DEBOUNCE_INTERVAL = 500;
65
66
  * Reacts to new epochs to restart the pipeline.
66
67
  */
67
68
  @trackLeaks('open', 'close')
69
+ @trace.resource()
68
70
  export class DataPipeline implements CredentialProcessor {
69
71
  private _ctx = new Context();
70
72
  private _pipeline?: Pipeline = undefined;
@@ -78,6 +80,12 @@ export class DataPipeline implements CredentialProcessor {
78
80
  private _lastProcessedEpoch = -1;
79
81
  private _epochCtx?: Context;
80
82
 
83
+ @trace.metricsCounter()
84
+ private _usage = new TimeUsageCounter();
85
+
86
+ @trace.metricsCounter()
87
+ private _mutations = new TimeSeriesCounter();
88
+
81
89
  constructor(private readonly _params: DataPipelineParams) {}
82
90
 
83
91
  public itemManager!: ItemManager;
@@ -132,8 +140,9 @@ export class DataPipeline implements CredentialProcessor {
132
140
 
133
141
  this._pipeline = new Pipeline();
134
142
  await this._params.onPipelineCreated(this._pipeline);
135
- await this._pipeline.start();
143
+
136
144
  await this._pipeline.pause(); // Start paused until we have the first epoch.
145
+ await this._pipeline.start();
137
146
 
138
147
  if (this._targetTimeframe) {
139
148
  this._pipeline.state.setTargetTimeframe(this._targetTimeframe);
@@ -205,6 +214,9 @@ export class DataPipeline implements CredentialProcessor {
205
214
 
206
215
  invariant(this._pipeline, 'Pipeline is not initialized.');
207
216
  for await (const msg of this._pipeline.consume()) {
217
+ const span = this._usage.beginRecording();
218
+ this._mutations.inc();
219
+
208
220
  const { feedKey, seq, data } = msg;
209
221
  log('processing message', { feedKey, seq });
210
222
 
@@ -241,6 +253,8 @@ export class DataPipeline implements CredentialProcessor {
241
253
  } catch (err: any) {
242
254
  log.catch(err);
243
255
  }
256
+
257
+ span.end();
244
258
  }
245
259
  }
246
260
 
@@ -268,7 +282,7 @@ export class DataPipeline implements CredentialProcessor {
268
282
  (item) =>
269
283
  item.modelMeta?.type === 'dxos:model/document' &&
270
284
  // TODO(burdon): Document?
271
- (getStateMachineFromItem(item)?.snapshot() as ObjectSnapshot).type === 'dxos.sdk.client.Properties',
285
+ (getStateMachineFromItem(item)?.snapshot() as ObjectSnapshot).type === TYPE_PROPERTIES,
272
286
  );
273
287
  if (propertiesItem) {
274
288
  cache.properties = getStateMachineFromItem(propertiesItem)?.snapshot() as ObjectSnapshot;
@@ -318,7 +332,6 @@ export class DataPipeline implements CredentialProcessor {
318
332
  // Space closed before we got to process the epoch.
319
333
  return;
320
334
  }
321
- log('process epoch', { epoch });
322
335
  await this._processEpoch(ctx, epoch.subject.assertion);
323
336
 
324
337
  this.appliedEpoch = epoch;
@@ -332,16 +345,13 @@ export class DataPipeline implements CredentialProcessor {
332
345
  invariant(this._pipeline);
333
346
  this._lastProcessedEpoch = epoch.number;
334
347
 
335
- log('Processing epoch', { epoch });
348
+ log('processing', { epoch: omit(epoch, 'proof') });
336
349
  if (epoch.snapshotCid) {
337
350
  const snapshot = await this._params.snapshotManager.load(ctx, epoch.snapshotCid);
338
-
339
- // TODO(dmaretskyi): Clearing old items + events.
340
351
  this.databaseHost!._itemDemuxer.restoreFromSnapshot(snapshot.database);
341
352
  }
342
353
 
343
- log('restarting pipeline for epoch');
344
-
354
+ log('restarting pipeline from epoch');
345
355
  await this._pipeline.pause();
346
356
  await this._pipeline.setCursor(epoch.timeframe);
347
357
  await this._pipeline.unpause();
@@ -122,6 +122,7 @@ export class SpaceManager {
122
122
  memberKey,
123
123
  });
124
124
  this._spaces.set(space.key, space);
125
+
125
126
  log.trace('dxos.echo.space-manager.construct-space', trace.end({ id: this._instanceId }));
126
127
  return space;
127
128
  }
@@ -87,6 +87,7 @@ export class SpaceProtocol {
87
87
  this._onAuthFailure = onAuthFailure;
88
88
  this.blobSync = new BlobSync({ blobStore });
89
89
 
90
+ // TODO(burdon): Async race condition? Move to start?
90
91
  this._topic = subtleCrypto.digest('SHA-256', topic.asBuffer()).then(discoveryKey).then(PublicKey.from);
91
92
  }
92
93
 
@@ -100,6 +101,7 @@ export class SpaceProtocol {
100
101
  }
101
102
  }
102
103
 
104
+ // TODO(burdon): Rename open? Common open/close interfaces for all services?
103
105
  async start() {
104
106
  if (this._connection) {
105
107
  return;
@@ -124,7 +126,7 @@ export class SpaceProtocol {
124
126
  peerId: this._swarmIdentity.peerKey,
125
127
  topic,
126
128
  topology: new MMSTTopology(topologyConfig),
127
- label: `Protocol swarm: ${topic}`,
129
+ label: `space swarm ${topic.truncate()}`,
128
130
  });
129
131
 
130
132
  log('started');
@@ -140,6 +140,7 @@ export class Space {
140
140
  }
141
141
 
142
142
  @logInfo
143
+ @trace.info()
143
144
  get key() {
144
145
  return this._key;
145
146
  }