@dxos/echo-pipeline 0.1.56-main.e47dfd1 → 0.1.56-main.e79d64a

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 (55) hide show
  1. package/dist/lib/browser/{chunk-7U2NXI2P.mjs → chunk-GDF4DQU2.mjs} +692 -355
  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 +14 -18
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/index.cjs +699 -362
  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 +696 -363
  12. package/dist/lib/node/testing/index.cjs.map +3 -3
  13. package/dist/types/src/common/feeds.d.ts.map +1 -1
  14. package/dist/types/src/db-host/data-service-host.d.ts.map +1 -1
  15. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  16. package/dist/types/src/db-host/snapshot-manager.d.ts.map +1 -1
  17. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  18. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  19. package/dist/types/src/pipeline/pipeline-stress.test.d.ts +2 -0
  20. package/dist/types/src/pipeline/pipeline-stress.test.d.ts.map +1 -0
  21. package/dist/types/src/pipeline/pipeline.d.ts +4 -3
  22. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  23. package/dist/types/src/space/auth.d.ts.map +1 -1
  24. package/dist/types/src/space/control-pipeline.d.ts +4 -0
  25. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  26. package/dist/types/src/space/data-pipeline.d.ts +2 -0
  27. package/dist/types/src/space/data-pipeline.d.ts.map +1 -1
  28. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  29. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  30. package/dist/types/src/space/space.d.ts +2 -1
  31. package/dist/types/src/space/space.d.ts.map +1 -1
  32. package/dist/types/src/testing/database-test-rig.d.ts +4 -1
  33. package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
  34. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  35. package/package.json +32 -31
  36. package/src/common/feeds.ts +1 -2
  37. package/src/db-host/data-service-host.ts +10 -4
  38. package/src/db-host/data-service.ts +1 -2
  39. package/src/db-host/snapshot-manager.ts +0 -2
  40. package/src/metadata/metadata-store.ts +2 -2
  41. package/src/pipeline/message-selector.ts +1 -2
  42. package/src/pipeline/pipeline-stress.test.ts +226 -0
  43. package/src/pipeline/pipeline.test.ts +30 -206
  44. package/src/pipeline/pipeline.ts +50 -24
  45. package/src/space/auth.ts +1 -2
  46. package/src/space/control-pipeline.ts +47 -22
  47. package/src/space/data-pipeline.ts +21 -12
  48. package/src/space/space-manager.ts +1 -0
  49. package/src/space/space-protocol.ts +3 -1
  50. package/src/space/space.test.ts +18 -17
  51. package/src/space/space.ts +7 -3
  52. package/src/testing/database-test-rig.ts +27 -2
  53. package/src/testing/test-agent-builder.ts +2 -1
  54. package/src/tests/database-unit.test.ts +39 -0
  55. package/dist/lib/browser/chunk-7U2NXI2P.mjs.map +0 -7
@@ -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
 
@@ -2,16 +2,15 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
6
-
7
5
  import { Event, scheduleTask, synchronized, trackLeaks } from '@dxos/async';
8
6
  import { Context } from '@dxos/context';
9
7
  import { CredentialProcessor, FeedInfo, SpecificCredential, checkCredentialType } from '@dxos/credentials';
10
- import { getStateMachineFromItem, ItemManager } from '@dxos/echo-db';
8
+ import { getStateMachineFromItem, ItemManager, TYPE_PROPERTIES } from '@dxos/echo-db';
11
9
  import { CancelledError } from '@dxos/errors';
12
10
  import { FeedWriter } from '@dxos/feed-store';
11
+ import { invariant } from '@dxos/invariant';
13
12
  import { PublicKey } from '@dxos/keys';
14
- import { log } from '@dxos/log';
13
+ import { log, omit } from '@dxos/log';
15
14
  import { ModelFactory } from '@dxos/model-factory';
16
15
  import { DataPipelineProcessed } from '@dxos/protocols';
17
16
  import { DataMessage } from '@dxos/protocols/proto/dxos/echo/feed';
@@ -20,6 +19,7 @@ import { ObjectSnapshot } from '@dxos/protocols/proto/dxos/echo/model/document';
20
19
  import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
21
20
  import { Credential, Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
22
21
  import { Timeframe } from '@dxos/timeframe';
22
+ import { TimeSeriesCounter, TimeUsageCounter, trace } from '@dxos/tracing';
23
23
  import { tracer } from '@dxos/util';
24
24
 
25
25
  import { DatabaseHost, SnapshotManager } from '../db-host';
@@ -66,6 +66,7 @@ const TIMEFRAME_SAVE_DEBOUNCE_INTERVAL = 500;
66
66
  * Reacts to new epochs to restart the pipeline.
67
67
  */
68
68
  @trackLeaks('open', 'close')
69
+ @trace.resource()
69
70
  export class DataPipeline implements CredentialProcessor {
70
71
  private _ctx = new Context();
71
72
  private _pipeline?: Pipeline = undefined;
@@ -79,6 +80,12 @@ export class DataPipeline implements CredentialProcessor {
79
80
  private _lastProcessedEpoch = -1;
80
81
  private _epochCtx?: Context;
81
82
 
83
+ @trace.metricsCounter()
84
+ private _usage = new TimeUsageCounter();
85
+
86
+ @trace.metricsCounter()
87
+ private _mutations = new TimeSeriesCounter();
88
+
82
89
  constructor(private readonly _params: DataPipelineParams) {}
83
90
 
84
91
  public itemManager!: ItemManager;
@@ -133,8 +140,9 @@ export class DataPipeline implements CredentialProcessor {
133
140
 
134
141
  this._pipeline = new Pipeline();
135
142
  await this._params.onPipelineCreated(this._pipeline);
136
- await this._pipeline.start();
143
+
137
144
  await this._pipeline.pause(); // Start paused until we have the first epoch.
145
+ await this._pipeline.start();
138
146
 
139
147
  if (this._targetTimeframe) {
140
148
  this._pipeline.state.setTargetTimeframe(this._targetTimeframe);
@@ -206,6 +214,9 @@ export class DataPipeline implements CredentialProcessor {
206
214
 
207
215
  invariant(this._pipeline, 'Pipeline is not initialized.');
208
216
  for await (const msg of this._pipeline.consume()) {
217
+ const span = this._usage.beginRecording();
218
+ this._mutations.inc();
219
+
209
220
  const { feedKey, seq, data } = msg;
210
221
  log('processing message', { feedKey, seq });
211
222
 
@@ -242,6 +253,8 @@ export class DataPipeline implements CredentialProcessor {
242
253
  } catch (err: any) {
243
254
  log.catch(err);
244
255
  }
256
+
257
+ span.end();
245
258
  }
246
259
  }
247
260
 
@@ -269,7 +282,7 @@ export class DataPipeline implements CredentialProcessor {
269
282
  (item) =>
270
283
  item.modelMeta?.type === 'dxos:model/document' &&
271
284
  // TODO(burdon): Document?
272
- (getStateMachineFromItem(item)?.snapshot() as ObjectSnapshot).type === 'dxos.sdk.client.Properties',
285
+ (getStateMachineFromItem(item)?.snapshot() as ObjectSnapshot).type === TYPE_PROPERTIES,
273
286
  );
274
287
  if (propertiesItem) {
275
288
  cache.properties = getStateMachineFromItem(propertiesItem)?.snapshot() as ObjectSnapshot;
@@ -319,7 +332,6 @@ export class DataPipeline implements CredentialProcessor {
319
332
  // Space closed before we got to process the epoch.
320
333
  return;
321
334
  }
322
- log('process epoch', { epoch });
323
335
  await this._processEpoch(ctx, epoch.subject.assertion);
324
336
 
325
337
  this.appliedEpoch = epoch;
@@ -333,16 +345,13 @@ export class DataPipeline implements CredentialProcessor {
333
345
  invariant(this._pipeline);
334
346
  this._lastProcessedEpoch = epoch.number;
335
347
 
336
- log('Processing epoch', { epoch });
348
+ log('processing', { epoch: omit(epoch, 'proof') });
337
349
  if (epoch.snapshotCid) {
338
350
  const snapshot = await this._params.snapshotManager.load(ctx, epoch.snapshotCid);
339
-
340
- // TODO(dmaretskyi): Clearing old items + events.
341
351
  this.databaseHost!._itemDemuxer.restoreFromSnapshot(snapshot.database);
342
352
  }
343
353
 
344
- log('restarting pipeline for epoch');
345
-
354
+ log('restarting pipeline from epoch');
346
355
  await this._pipeline.pause();
347
356
  await this._pipeline.setCursor(epoch.timeframe);
348
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');
@@ -3,10 +3,11 @@
3
3
  //
4
4
 
5
5
  import expect from 'expect';
6
- import assert from 'node:assert';
7
6
  import { promisify } from 'node:util';
8
7
 
8
+ import { Context } from '@dxos/context';
9
9
  import { createCredential, CredentialGenerator } from '@dxos/credentials';
10
+ import { invariant } from '@dxos/invariant';
10
11
  import { afterTest, describe, test } from '@dxos/test';
11
12
 
12
13
  import { TestAgentBuilder, testLocalDatabase } from '../testing';
@@ -21,7 +22,7 @@ describe('space/space', () => {
21
22
  const agent = await builder.createPeer();
22
23
  const space = await agent.createSpace();
23
24
 
24
- await space.open();
25
+ await space.open(new Context());
25
26
  expect(space.isOpen).toBeTruthy();
26
27
  afterTest(() => space.close());
27
28
 
@@ -31,7 +32,7 @@ describe('space/space', () => {
31
32
  await space.initializeDataPipeline();
32
33
  await space.dataPipeline.ensureEpochInitialized();
33
34
 
34
- assert(space.dataPipeline.databaseHost);
35
+ invariant(space.dataPipeline.databaseHost);
35
36
  await testLocalDatabase(space.dataPipeline);
36
37
 
37
38
  await builder.close();
@@ -49,7 +50,7 @@ describe('space/space', () => {
49
50
  const agent = await builder.createPeer();
50
51
  const space = await agent.createSpace(agent.identityKey);
51
52
 
52
- await space.open();
53
+ await space.open(new Context());
53
54
  expect(space.isOpen).toBeTruthy();
54
55
  afterTest(() => space.close());
55
56
 
@@ -70,7 +71,7 @@ describe('space/space', () => {
70
71
  const agent = await builder.createPeer();
71
72
  const space = await agent.createSpace(agent.identityKey, space1.key, space1.genesisFeedKey, undefined, true);
72
73
 
73
- await space.open();
74
+ await space.open(new Context());
74
75
  expect(space.isOpen).toBeTruthy();
75
76
  afterTest(() => space.close());
76
77
 
@@ -128,7 +129,7 @@ describe('space/space', () => {
128
129
  const agent = await builder.createPeer();
129
130
  const space1 = await agent.createSpace();
130
131
 
131
- await space1.open();
132
+ await space1.open(new Context());
132
133
  expect(space1.isOpen).toBeTruthy();
133
134
  afterTest(() => space1.close());
134
135
 
@@ -138,7 +139,7 @@ describe('space/space', () => {
138
139
  await space1.initializeDataPipeline();
139
140
  await space1.dataPipeline.ensureEpochInitialized();
140
141
 
141
- assert(space1.dataPipeline.databaseHost);
142
+ invariant(space1.dataPipeline.databaseHost);
142
143
  await testLocalDatabase(space1.dataPipeline);
143
144
 
144
145
  const objectCount = space1.dataPipeline.itemManager.entities.size;
@@ -149,7 +150,7 @@ describe('space/space', () => {
149
150
  // Re-open.
150
151
  const space2 = await agent.createSpace(agent.identityKey, space1.key, space1.genesisFeedKey, space1.dataFeedKey);
151
152
 
152
- await space2.open();
153
+ await space2.open(new Context());
153
154
  await space2.controlPipeline.state!.waitUntilTimeframe(space2.controlPipeline.state!.endTimeframe);
154
155
  await space2.initializeDataPipeline();
155
156
  await space2.dataPipeline.ensureEpochInitialized();
@@ -157,7 +158,7 @@ describe('space/space', () => {
157
158
  space2.dataPipeline.setTargetTimeframe(space2.dataPipeline.pipelineState!.endTimeframe);
158
159
  await space2.dataPipeline.pipelineState!.waitUntilReachedTargetTimeframe();
159
160
 
160
- assert(space2.dataPipeline.databaseHost);
161
+ invariant(space2.dataPipeline.databaseHost);
161
162
  expect(space2.dataPipeline.itemManager.entities.size).toEqual(objectCount);
162
163
 
163
164
  await testLocalDatabase(space2.dataPipeline);
@@ -171,7 +172,7 @@ describe('space/space', () => {
171
172
 
172
173
  let objectCount: number;
173
174
  {
174
- await space.open();
175
+ await space.open(new Context());
175
176
  afterTest(() => space.close());
176
177
  expect(space.isOpen).toBeTruthy();
177
178
 
@@ -181,7 +182,7 @@ describe('space/space', () => {
181
182
  await space.initializeDataPipeline();
182
183
  await space.dataPipeline.ensureEpochInitialized();
183
184
 
184
- assert(space.dataPipeline.databaseHost);
185
+ invariant(space.dataPipeline.databaseHost);
185
186
  await testLocalDatabase(space.dataPipeline);
186
187
 
187
188
  objectCount = space.dataPipeline.itemManager.entities.size;
@@ -192,7 +193,7 @@ describe('space/space', () => {
192
193
 
193
194
  // Re-open.
194
195
  {
195
- await space.open();
196
+ await space.open(new Context());
196
197
  expect(space.isOpen).toBeTruthy();
197
198
 
198
199
  await space.controlPipeline.state!.waitUntilTimeframe(space.controlPipeline.state!.endTimeframe);
@@ -202,7 +203,7 @@ describe('space/space', () => {
202
203
  space.dataPipeline.setTargetTimeframe(space.dataPipeline.pipelineState!.endTimeframe);
203
204
  await space.dataPipeline.pipelineState!.waitUntilReachedTargetTimeframe();
204
205
 
205
- assert(space.dataPipeline.databaseHost);
206
+ invariant(space.dataPipeline.databaseHost);
206
207
  expect(space.dataPipeline.itemManager.entities.size).toEqual(objectCount);
207
208
 
208
209
  await testLocalDatabase(space.dataPipeline);
@@ -215,7 +216,7 @@ describe('space/space', () => {
215
216
  const agent = await builder.createPeer();
216
217
  const space1 = await agent.createSpace();
217
218
 
218
- await space1.open();
219
+ await space1.open(new Context());
219
220
  expect(space1.isOpen).toBeTruthy();
220
221
  afterTest(() => space1.close());
221
222
 
@@ -225,7 +226,7 @@ describe('space/space', () => {
225
226
  await space1.initializeDataPipeline();
226
227
  await space1.dataPipeline.ensureEpochInitialized();
227
228
 
228
- assert(space1.dataPipeline.databaseHost);
229
+ invariant(space1.dataPipeline.databaseHost);
229
230
  await testLocalDatabase(space1.dataPipeline);
230
231
 
231
232
  const objectCount = space1.dataPipeline.itemManager.entities.size;
@@ -255,7 +256,7 @@ describe('space/space', () => {
255
256
  // Re-open.
256
257
  const space2 = await agent.createSpace(agent.identityKey, space1.key, space1.genesisFeedKey, space1.dataFeedKey);
257
258
 
258
- await space2.open();
259
+ await space2.open(new Context());
259
260
  await space2.controlPipeline.state!.waitUntilTimeframe(space2.controlPipeline.state!.endTimeframe);
260
261
  await space2.initializeDataPipeline();
261
262
  await space2.dataPipeline.ensureEpochInitialized();
@@ -263,7 +264,7 @@ describe('space/space', () => {
263
264
  space2.dataPipeline.setTargetTimeframe(space2.dataPipeline.pipelineState!.endTimeframe);
264
265
  await space2.dataPipeline.pipelineState!.waitUntilReachedTargetTimeframe();
265
266
 
266
- assert(space2.dataPipeline.databaseHost);
267
+ invariant(space2.dataPipeline.databaseHost);
267
268
  expect(space2.dataPipeline.itemManager.entities.size).toEqual(objectCount);
268
269
 
269
270
  await testLocalDatabase(space2.dataPipeline);
@@ -2,17 +2,18 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
6
-
7
5
  import { Event, synchronized, trackLeaks, Lock } from '@dxos/async';
6
+ import { Context } from '@dxos/context';
8
7
  import { FeedInfo } from '@dxos/credentials';
9
8
  import { FeedOptions, FeedWrapper } from '@dxos/feed-store';
9
+ import { invariant } from '@dxos/invariant';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { log, logInfo } from '@dxos/log';
12
12
  import { ModelFactory } from '@dxos/model-factory';
13
13
  import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
14
14
  import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
15
15
  import { Timeframe } from '@dxos/timeframe';
16
+ import { trace } from '@dxos/tracing';
16
17
  import { AsyncCallback, Callback } from '@dxos/util';
17
18
 
18
19
  import { SnapshotManager } from '../db-host';
@@ -49,6 +50,7 @@ export type CreatePipelineParams = {
49
50
  */
50
51
  // TODO(dmaretskyi): Extract database stuff.
51
52
  @trackLeaks('open', 'close')
53
+ @trace.resource()
52
54
  export class Space {
53
55
  private readonly _addFeedLock = new Lock();
54
56
 
@@ -138,6 +140,7 @@ export class Space {
138
140
  }
139
141
 
140
142
  @logInfo
143
+ @trace.info()
141
144
  get key() {
142
145
  return this._key;
143
146
  }
@@ -205,7 +208,8 @@ export class Space {
205
208
  // return this._dataPipeline?.getFeeds();
206
209
  // }
207
210
  @synchronized
208
- async open() {
211
+ @trace.span()
212
+ async open(ctx: Context) {
209
213
  log('opening...');
210
214
  if (this._isOpen) {
211
215
  return;
@@ -8,9 +8,10 @@ import { DatabaseProxy, ItemManager } from '@dxos/echo-db';
8
8
  import { WriteOptions, WriteReceipt } from '@dxos/feed-store';
9
9
  import { PublicKey } from '@dxos/keys';
10
10
  import { ModelFactory } from '@dxos/model-factory';
11
- import { FeedMessageBlock } from '@dxos/protocols';
11
+ import { FeedMessageBlock, schema } from '@dxos/protocols';
12
12
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
13
- import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
13
+ import { EchoSnapshot, SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
14
+ import { Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
14
15
  import { TextModel } from '@dxos/text-model';
15
16
  import { Timeframe } from '@dxos/timeframe';
16
17
  import { ComplexMap, isNotNullOrUndefined } from '@dxos/util';
@@ -53,6 +54,9 @@ export class DatabaseTestPeer {
53
54
 
54
55
  public feedMessages: FeedMessage[] = [];
55
56
 
57
+ public readonly snapshots = new Map<string, SpaceSnapshot>();
58
+ private currentEpoch?: Epoch;
59
+
56
60
  /**
57
61
  * Sequence number of the last mutation confirmed to be written to the feed store.
58
62
  */
@@ -162,6 +166,27 @@ export class DatabaseTestPeer {
162
166
  return this.snapshot;
163
167
  }
164
168
 
169
+ createEpoch(mockSnapshot?: EchoSnapshot) {
170
+ const snapshot = this.makeSnapshot();
171
+ // Substitute snapshot with the mock one for test purposes (e.g. to test with empty snapshot).
172
+ mockSnapshot && (snapshot.database = mockSnapshot);
173
+ const snapshotCid = PublicKey.from(
174
+ schema.getCodecForType('dxos.echo.snapshot.SpaceSnapshot').encode(snapshot),
175
+ ).toHex();
176
+ this.snapshots.set(snapshotCid, snapshot);
177
+
178
+ const epoch: Epoch = {
179
+ previousId: PublicKey.random(),
180
+ timeframe: this.timeframe,
181
+ number: this.currentEpoch ? this.currentEpoch.number + 1 : 0,
182
+ snapshotCid,
183
+ };
184
+
185
+ this.currentEpoch = epoch;
186
+
187
+ this.host._itemDemuxer.restoreFromSnapshot(snapshot.database);
188
+ }
189
+
165
190
  /**
166
191
  * Gets all candidate messages according to the current timeframe.
167
192
  * Does not take into account the current snapshot, timeframe dependencies, or the confirmed, or replicated state.
@@ -2,6 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ import { Context } from '@dxos/context';
5
6
  import { CredentialGenerator } from '@dxos/credentials';
6
7
  import { DocumentModel } from '@dxos/document-model';
7
8
  import { FeedStore } from '@dxos/feed-store';
@@ -198,7 +199,7 @@ export class TestAgent {
198
199
  space.setControlFeed(controlFeed);
199
200
  space.setDataFeed(dataFeed);
200
201
 
201
- await space.open();
202
+ await space.open(new Context());
202
203
 
203
204
  this._spaces.set(spaceKey, space);
204
205
  return space;
@@ -158,4 +158,43 @@ describe('database (unit)', () => {
158
158
  expect(model.get('tags').toArray()).toHaveLength(1);
159
159
  });
160
160
  });
161
+
162
+ it('epoch correctly resets items', async () => {
163
+ const builder = new DatabaseTestBuilder();
164
+ const peer = await builder.createPeer();
165
+
166
+ const id = PublicKey.random().toHex();
167
+ peer.proxy.mutate(genesisMutation(id, DocumentModel.meta.type));
168
+ peer.proxy.mutate(
169
+ createModelMutation(id, encodeModelMutation(DocumentModel.meta, new MutationBuilder().set('test', 42).build())),
170
+ );
171
+
172
+ await peer.confirm();
173
+ expect(peer.confirmed).toEqual(1);
174
+ expect(peer.timeframe).toEqual(new Timeframe([[peer.key, 1]]));
175
+
176
+ const state = peer.items.getItem(id)!.state;
177
+ const snapshotWithItem = peer.makeSnapshot().database;
178
+
179
+ {
180
+ // Reset on empty epoch.
181
+ const updated = peer.proxy.itemUpdate.waitForCount(1);
182
+
183
+ expect(peer.proxy._itemManager.items.length).toEqual(1);
184
+
185
+ peer.createEpoch({});
186
+
187
+ await updated;
188
+ expect(peer.proxy._itemManager.items.length).toEqual(0);
189
+ }
190
+
191
+ {
192
+ // Reset to epoch with one item.
193
+ const updated = peer.proxy.itemUpdate.waitForCount(1);
194
+ peer.createEpoch(snapshotWithItem);
195
+ await updated;
196
+ expect(peer.proxy._itemManager.items.length).toEqual(1);
197
+ expect(peer.items.getItem(id)!.state).toEqual(state);
198
+ }
199
+ });
161
200
  });