@dxos/echo-pipeline 0.1.35 → 0.1.36

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 (50) hide show
  1. package/dist/lib/browser/{chunk-6CRSMR4G.mjs → chunk-4YAT2XJW.mjs} +221 -160
  2. package/dist/lib/browser/chunk-4YAT2XJW.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +5 -3
  4. package/dist/lib/browser/index.mjs.map +1 -1
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +5 -5
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/index.cjs +212 -150
  9. package/dist/lib/node/index.cjs.map +4 -4
  10. package/dist/lib/node/meta.json +1 -1
  11. package/dist/lib/node/testing/index.cjs +208 -152
  12. package/dist/lib/node/testing/index.cjs.map +4 -4
  13. package/dist/types/src/dbhost/data-service-host.d.ts.map +1 -1
  14. package/dist/types/src/dbhost/{database-backend.d.ts → database-host.d.ts} +2 -2
  15. package/dist/types/src/dbhost/database-host.d.ts.map +1 -0
  16. package/dist/types/src/dbhost/index.d.ts +1 -1
  17. package/dist/types/src/dbhost/index.d.ts.map +1 -1
  18. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  19. package/dist/types/src/pipeline/pipeline.d.ts +2 -0
  20. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  21. package/dist/types/src/pipeline/timeframe-clock.d.ts +12 -1
  22. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  23. package/dist/types/src/space/data-pipeline.d.ts +8 -9
  24. package/dist/types/src/space/data-pipeline.d.ts.map +1 -1
  25. package/dist/types/src/space/data-pipeline.test.d.ts +1 -0
  26. package/dist/types/src/space/data-pipeline.test.d.ts.map +1 -0
  27. package/dist/types/src/space/space-manager.d.ts +1 -0
  28. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  29. package/dist/types/src/testing/database-test-rig.d.ts +4 -4
  30. package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
  31. package/dist/types/src/testing/util.d.ts +4 -4
  32. package/dist/types/src/testing/util.d.ts.map +1 -1
  33. package/package.json +32 -29
  34. package/src/common/feeds.ts +1 -1
  35. package/src/dbhost/data-service-host.ts +16 -9
  36. package/src/dbhost/{database-backend.ts → database-host.ts} +1 -1
  37. package/src/dbhost/index.ts +1 -1
  38. package/src/metadata/metadata-store.ts +24 -20
  39. package/src/pipeline/pipeline.test.ts +214 -0
  40. package/src/pipeline/pipeline.ts +20 -6
  41. package/src/pipeline/timeframe-clock.ts +26 -4
  42. package/src/space/data-pipeline.test.ts +3 -0
  43. package/src/space/data-pipeline.ts +65 -66
  44. package/src/space/space-manager.ts +2 -1
  45. package/src/testing/database-test-rig.ts +6 -6
  46. package/src/testing/util.ts +4 -4
  47. package/src/tests/database-unit.test.ts +13 -0
  48. package/src/tests/database.test.ts +39 -1
  49. package/dist/lib/browser/chunk-6CRSMR4G.mjs.map +0 -7
  50. package/dist/types/src/dbhost/database-backend.d.ts.map +0 -1
@@ -2,7 +2,16 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ import expect from 'expect';
6
+ import * as fc from 'fast-check';
7
+ import { inspect } from 'util';
8
+
9
+ import { asyncTimeout } from '@dxos/async';
5
10
  import { checkType } from '@dxos/debug';
11
+ import { FeedStore, FeedWrapper } from '@dxos/feed-store';
12
+ import { PublicKey } from '@dxos/keys';
13
+ import { log } from '@dxos/log';
14
+ import { FeedMessageBlock } from '@dxos/protocols';
6
15
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
7
16
  import { describe, test, afterTest } from '@dxos/test';
8
17
  import { Timeframe } from '@dxos/timeframe';
@@ -76,4 +85,209 @@ describe('pipeline/Pipeline', () => {
76
85
  }
77
86
  }
78
87
  });
88
+
89
+ test
90
+ .skip('stress', async () => {
91
+ const builder = new TestFeedBuilder();
92
+
93
+ const NUM_AGENTS = 2;
94
+ const NUM_MESSAGES = 100;
95
+
96
+ class Agent {
97
+ public startingTimeframe = new Timeframe();
98
+ public pipeline!: Pipeline;
99
+ public feed!: FeedWrapper<FeedMessage>;
100
+ public messages: FeedMessageBlock[] = [];
101
+ public writePromise: Promise<any> = Promise.resolve();
102
+
103
+ constructor(public id: string, public feedStore: FeedStore<FeedMessage>) {}
104
+
105
+ async open() {
106
+ const key = await builder.keyring.createKey();
107
+ this.feed = await this.feedStore.openFeed(key, { writable: true });
108
+ }
109
+
110
+ async start() {
111
+ this.pipeline = new Pipeline(this.startingTimeframe);
112
+
113
+ await this.pipeline.start();
114
+
115
+ // NOTE: not awaiting here breaks the test.
116
+ await Promise.all(this.feedStore.feeds.map((feed) => this.pipeline.addFeed(feed)));
117
+ this.pipeline.setWriteFeed(this.feed);
118
+
119
+ // consume in async task.
120
+ void this.consume();
121
+ }
122
+
123
+ async stop() {
124
+ await this.pipeline.stop();
125
+ this.startingTimeframe = this.pipeline.state.timeframe;
126
+ }
127
+
128
+ async close() {
129
+ await this.feed.close();
130
+ }
131
+
132
+ write(message: FeedMessage.Payload) {
133
+ const prev = this.writePromise;
134
+ const promise = this.pipeline.writer!.write(message);
135
+ this.writePromise = Promise.all([prev, promise]);
136
+ }
137
+
138
+ async consume() {
139
+ for await (const msg of this.pipeline.consume()) {
140
+ this.messages.push(msg);
141
+ }
142
+ log('stopped consuming');
143
+ }
144
+ }
145
+
146
+ type Model = {};
147
+ type Real = {
148
+ feedStore: FeedStore<FeedMessage>;
149
+ agents: Map<string, Agent>;
150
+ };
151
+
152
+ class WriteCommand implements fc.AsyncCommand<Model, Real> {
153
+ constructor(public agent: string, public count: number) {}
154
+
155
+ check = () => true;
156
+
157
+ async run(model: Model, real: Real) {
158
+ console.log(`WriteCommand(${this.agent}, ${this.count})`);
159
+
160
+ const agent = real.agents.get(this.agent)!;
161
+
162
+ const toWrite = Math.min(this.count, NUM_MESSAGES - agent.feed.length);
163
+ if (toWrite > 0) {
164
+ for (const _ of range(toWrite)) {
165
+ agent.write({}); // Content is not important.
166
+ }
167
+ }
168
+ }
169
+
170
+ toString = () => `WriteCommand(${this.agent}, ${this.count})`;
171
+ }
172
+
173
+ class SyncCommand implements fc.AsyncCommand<Model, Real> {
174
+ check = () => true;
175
+
176
+ async run(model: Model, real: Real) {
177
+ console.log('SyncCommand()');
178
+ const targets: any = {};
179
+
180
+ try {
181
+ for (const agent of real.agents.values()) {
182
+ await agent.writePromise;
183
+ }
184
+
185
+ for (const agent of real.agents.values()) {
186
+ targets[agent.id] = agent.pipeline.state.endTimeframe;
187
+ if (agent.pipeline.state.endTimeframe.isEmpty()) {
188
+ console.log('empty endtimeframe', {
189
+ id: agent.id,
190
+ endTimeframe: agent.pipeline.state.endTimeframe,
191
+ feeds: agent.pipeline.getFeeds().map((feed) => [feed.key.toString(), feed.length])
192
+ });
193
+ }
194
+ await asyncTimeout(agent.pipeline.state.waitUntilTimeframe(agent.pipeline.state.endTimeframe), 1000);
195
+ }
196
+
197
+ const tf: Timeframe = real.agents.values().next().value!.pipeline.state.timeframe;
198
+ for (const agent of real.agents.values()) {
199
+ expect(agent.pipeline.state.timeframe.equals(tf)).toEqual(true);
200
+ }
201
+
202
+ const totalMessages = real.feedStore.feeds.reduce((acc, feed) => acc + feed.length, 0);
203
+ for (const agent of real.agents.values()) {
204
+ expect(agent.messages.length).toEqual(totalMessages);
205
+ }
206
+ } catch (err) {
207
+ console.log(
208
+ inspect(
209
+ {
210
+ agents: Array.from(real.agents.values()).map((agent) => ({
211
+ id: agent.id,
212
+ messages: agent.messages.length,
213
+ feeds: agent.pipeline.getFeeds().map((feed) => [feed.key, feed.length]),
214
+ timeframe: agent.pipeline.state.timeframe,
215
+ endTimeframe: agent.pipeline.state.endTimeframe
216
+ })),
217
+ feeds: real.feedStore.feeds.map((feed) => [feed.key, feed.length]),
218
+ targets
219
+ },
220
+ false,
221
+ null,
222
+ true
223
+ )
224
+ );
225
+ throw err;
226
+ }
227
+ }
228
+
229
+ toString = () => 'SyncCommand()';
230
+ }
231
+
232
+ class RestartCommand implements fc.AsyncCommand<Model, Real> {
233
+ constructor(public agent: string) {}
234
+
235
+ check = () => true;
236
+
237
+ async run(model: Model, real: Real) {
238
+ console.log(`RestartCommand(${this.agent})`);
239
+
240
+ const agent = real.agents.get(this.agent)!;
241
+
242
+ await agent.writePromise;
243
+ await agent.stop();
244
+ await agent.start();
245
+ }
246
+
247
+ toString = () => `RestartCommand(${this.agent})`;
248
+ }
249
+
250
+ const agentIds = range(NUM_AGENTS).map(() => PublicKey.random().toHex().slice(0, 8));
251
+ const anAgentId = fc.constantFrom(...agentIds);
252
+
253
+ const commands = fc.commands(
254
+ [
255
+ fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
256
+ fc.constant(new SyncCommand()),
257
+ anAgentId.map((agent) => new RestartCommand(agent))
258
+ ],
259
+ { size: 'large' }
260
+ );
261
+
262
+ const model = fc.asyncProperty(commands, async (commands) => {
263
+ const feedStore = builder.createFeedStore();
264
+
265
+ const agents = new Map(agentIds.map((id) => [id, new Agent(id, feedStore)]));
266
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
267
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
268
+
269
+ const setup: fc.ModelRunSetup<Model, Real> = () => ({
270
+ model: {},
271
+ real: {
272
+ feedStore,
273
+ agents
274
+ }
275
+ });
276
+
277
+ try {
278
+ await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
279
+ } finally {
280
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
281
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
282
+ }
283
+ });
284
+
285
+ const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
286
+ [[new WriteCommand(agentIds[0], 10), new WriteCommand(agentIds[1], 10), new SyncCommand()]],
287
+ [[new WriteCommand(agentIds[0], 4), new RestartCommand(agentIds[0]), new SyncCommand()]]
288
+ ];
289
+
290
+ await fc.assert(model, { examples });
291
+ })
292
+ .timeout(60_000);
79
293
  });
@@ -4,7 +4,7 @@
4
4
 
5
5
  import assert from 'node:assert';
6
6
 
7
- import { sleep, Trigger } from '@dxos/async';
7
+ import { sleep, synchronized, Trigger } from '@dxos/async';
8
8
  import { Context, rejectOnDispose } from '@dxos/context';
9
9
  import { FeedSetIterator, FeedWrapper, FeedWriter } from '@dxos/feed-store';
10
10
  import { PublicKey } from '@dxos/keys';
@@ -15,7 +15,7 @@ import { Timeframe } from '@dxos/timeframe';
15
15
 
16
16
  import { createMappedFeedWriter } from '../common';
17
17
  import { createMessageSelector } from './message-selector';
18
- import { mapFeedIndexesToTimeframe, mapTimeframeToFeedIndexes, TimeframeClock } from './timeframe-clock';
18
+ import { mapFeedIndexesToTimeframe, startAfter, TimeframeClock } from './timeframe-clock';
19
19
 
20
20
  export type WaitUntilReachedTargetParams = {
21
21
  /**
@@ -54,13 +54,14 @@ export class PipelineState {
54
54
  * Latest theoretical timeframe based on the last mutation in each feed.
55
55
  * NOTE: This might never be reached if the mutation dependencies
56
56
  */
57
+ // TODO(dmaretskyi): Rename `totalTimeframe`? or `lastTimeframe`.
57
58
  get endTimeframe() {
58
59
  return mapFeedIndexesToTimeframe(
59
60
  this._iterator.feeds
60
- .filter((feed) => feed.properties.length > 0)
61
+ .filter((feed) => feed.length > 0)
61
62
  .map((feed) => ({
62
63
  feedKey: feed.key,
63
- index: feed.properties.length - 1
64
+ index: feed.length - 1
64
65
  }))
65
66
  );
66
67
  }
@@ -69,10 +70,18 @@ export class PipelineState {
69
70
  return this._timeframeClock.timeframe;
70
71
  }
71
72
 
73
+ get pendingTimeframe() {
74
+ return this._timeframeClock.pendingTimeframe;
75
+ }
76
+
72
77
  get targetTimeframe() {
73
78
  return this._targetTimeframe ? this._targetTimeframe : new Timeframe();
74
79
  }
75
80
 
81
+ get feeds() {
82
+ return this._iterator.feeds;
83
+ }
84
+
76
85
  async waitUntilTimeframe(target: Timeframe) {
77
86
  await this._timeframeClock.waitUntilReached(target);
78
87
  }
@@ -175,7 +184,7 @@ export class Pipeline implements PipelineAccessor {
175
184
 
176
185
  // Inbound feed stream.
177
186
  private readonly _feedSetIterator = new FeedSetIterator<FeedMessage>(createMessageSelector(this._timeframeClock), {
178
- start: mapTimeframeToFeedIndexes(this._initialTimeframe),
187
+ start: startAfter(this._initialTimeframe),
179
188
  stallTimeout: 1000
180
189
  });
181
190
 
@@ -212,6 +221,8 @@ export class Pipeline implements PipelineAccessor {
212
221
  return this._feedSetIterator.feeds;
213
222
  }
214
223
 
224
+ // NOTE: This cannot be synchronized with `stop` because stop waits for the mutation processing to complete,
225
+ // which might be opening feeds during the mutation processing, which w
215
226
  async addFeed(feed: FeedWrapper<FeedMessage>) {
216
227
  await this._feedSetIterator.addFeed(feed);
217
228
  }
@@ -233,12 +244,14 @@ export class Pipeline implements PipelineAccessor {
233
244
  );
234
245
  }
235
246
 
247
+ @synchronized
236
248
  async start() {
237
249
  log('starting...', {});
238
250
  await this._feedSetIterator.open();
239
251
  log('started');
240
252
  }
241
253
 
254
+ @synchronized
242
255
  async stop() {
243
256
  log('stopping...', {});
244
257
  await this._feedSetIterator.close();
@@ -256,10 +269,11 @@ export class Pipeline implements PipelineAccessor {
256
269
 
257
270
  for await (const block of this._feedSetIterator) {
258
271
  this._processingTrigger.reset();
272
+ this._timeframeClock.updatePendingTimeframe(PublicKey.from(block.feedKey), block.seq);
259
273
  yield block;
260
274
  this._processingTrigger.wake();
261
275
 
262
- this._timeframeClock.updateTimeframe(PublicKey.from(block.feedKey), block.seq);
276
+ this._timeframeClock.updateTimeframe();
263
277
  }
264
278
 
265
279
  // TODO(burdon): Test re-entrant?
@@ -15,23 +15,45 @@ export const mapTimeframeToFeedIndexes = (timeframe: Timeframe): FeedIndex[] =>
15
15
  export const mapFeedIndexesToTimeframe = (indexes: FeedIndex[]): Timeframe =>
16
16
  new Timeframe(indexes.map(({ feedKey, index }) => [feedKey, index]));
17
17
 
18
+ export const startAfter = (timeframe: Timeframe): FeedIndex[] =>
19
+ timeframe.frames().map(([feedKey, index]) => ({ feedKey, index: index + 1 }));
20
+
18
21
  /**
19
22
  * Keeps state of the last timeframe that was processed by ECHO.
20
23
  */
21
24
  export class TimeframeClock {
22
25
  readonly update = new Event<Timeframe>();
23
26
 
27
+ private _pendingTimeframe: Timeframe;
28
+
24
29
  // prettier-ignore
25
30
  constructor(
26
31
  private _timeframe = new Timeframe()
27
- ) {}
32
+ ) {
33
+ this._pendingTimeframe = _timeframe;
34
+ }
28
35
 
36
+ /**
37
+ * Timeframe that was processed by ECHO.
38
+ */
29
39
  get timeframe() {
30
40
  return this._timeframe;
31
41
  }
32
42
 
33
- updateTimeframe(key: PublicKey, seq: number) {
34
- this._timeframe = Timeframe.merge(this._timeframe, new Timeframe([[key, seq]]));
43
+ /**
44
+ * Timeframe that is currently being processed by ECHO.
45
+ * Will be equal to `timeframe` after the processing is complete.
46
+ */
47
+ get pendingTimeframe() {
48
+ return this._pendingTimeframe;
49
+ }
50
+
51
+ updatePendingTimeframe(key: PublicKey, seq: number) {
52
+ this._pendingTimeframe = Timeframe.merge(this._pendingTimeframe, new Timeframe([[key, seq]]));
53
+ }
54
+
55
+ updateTimeframe() {
56
+ this._timeframe = this._pendingTimeframe;
35
57
  this.update.emit(this._timeframe);
36
58
  }
37
59
 
@@ -42,7 +64,7 @@ export class TimeframeClock {
42
64
 
43
65
  @timed(5_000)
44
66
  async waitUntilReached(target: Timeframe) {
45
- log.debug('waitUntilReached', { target, current: this._timeframe });
67
+ log.info('waitUntilReached', { target, current: this._timeframe });
46
68
  await this.update.waitForCondition(() => {
47
69
  log('check if reached', {
48
70
  target,
@@ -0,0 +1,3 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
@@ -4,7 +4,7 @@
4
4
 
5
5
  import assert from 'node:assert';
6
6
 
7
- import { Event, scheduleTask, synchronized, trackLeaks } from '@dxos/async';
7
+ import { scheduleTask, synchronized, trackLeaks } from '@dxos/async';
8
8
  import { Context } from '@dxos/context';
9
9
  import { FeedInfo } from '@dxos/credentials';
10
10
  import { failUndefined } from '@dxos/debug';
@@ -17,7 +17,7 @@ import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
17
17
  import { Timeframe } from '@dxos/timeframe';
18
18
 
19
19
  import { createMappedFeedWriter } from '../common';
20
- import { DatabaseBackendHost, SnapshotManager } from '../dbhost';
20
+ import { DatabaseHost, SnapshotManager } from '../dbhost';
21
21
  import { MetadataStore } from '../metadata';
22
22
  import { Pipeline } from '../pipeline';
23
23
 
@@ -46,14 +46,14 @@ const MESSAGES_PER_SNAPSHOT = 10;
46
46
  const AUTOMATIC_SNAPSHOT_DEBOUNCE_INTERVAL = 5000;
47
47
 
48
48
  /**
49
- * Minimum time between recording latest timeframe in metadata.
49
+ * Minimum time in MS between recording latest timeframe in metadata.
50
50
  */
51
51
  const TIMEFRAME_SAVE_DEBOUNCE_INTERVAL = 500;
52
52
 
53
53
  /**
54
54
  * Flag to disable automatic local snapshots.
55
55
  */
56
- const DISABLE_SNAPSHOT_CACHE = false;
56
+ const DISABLE_SNAPSHOT_CACHE = true;
57
57
 
58
58
  /**
59
59
  * Controls data pipeline in the space.
@@ -71,12 +71,13 @@ export class DataPipeline {
71
71
  private _lastAutomaticSnapshotTimeframe = new Timeframe();
72
72
  private _isOpen = false;
73
73
 
74
- public readonly onTimeframeReached = new Event<Timeframe>();
74
+ private _lastTimeframeSaveTime = 0;
75
+ private _lastSnapshotSaveTime = 0;
75
76
 
76
77
  constructor(private readonly _params: DataPipelineParams) {}
77
78
 
78
79
  public _itemManager!: ItemManager;
79
- public databaseBackend?: DatabaseBackendHost;
80
+ public databaseBackend?: DatabaseHost;
80
81
 
81
82
  get isOpen() {
82
83
  return this._isOpen;
@@ -106,7 +107,7 @@ export class DataPipeline {
106
107
  }
107
108
 
108
109
  this._spaceContext = spaceContext;
109
- if (this._params.snapshotId) {
110
+ if (this._params.snapshotId && !DISABLE_SNAPSHOT_CACHE) {
110
111
  this._snapshot = await this._params.snapshotManager.load(this._params.snapshotId);
111
112
  this._lastAutomaticSnapshotTimeframe = this._snapshot?.timeframe ?? new Timeframe();
112
113
  }
@@ -122,7 +123,7 @@ export class DataPipeline {
122
123
  this._pipeline.writer ?? failUndefined()
123
124
  );
124
125
 
125
- this.databaseBackend = new DatabaseBackendHost(feedWriter, this._snapshot?.database);
126
+ this.databaseBackend = new DatabaseHost(feedWriter, this._snapshot?.database);
126
127
  this._itemManager = new ItemManager(this._params.modelFactory);
127
128
 
128
129
  // Connect pipeline to the database.
@@ -133,53 +134,9 @@ export class DataPipeline {
133
134
  await this._consumePipeline();
134
135
  });
135
136
 
136
- this._createPeriodicSnapshots();
137
-
138
137
  this._isOpen = true;
139
138
  }
140
139
 
141
- private _createPeriodicSnapshots() {
142
- // Record last timeframe.
143
- this.onTimeframeReached.debounce(TIMEFRAME_SAVE_DEBOUNCE_INTERVAL).on(this._ctx, async () => {
144
- await this._saveLatestTimeframe();
145
- });
146
-
147
- if (!DISABLE_SNAPSHOT_CACHE) {
148
- this.onTimeframeReached.debounce(AUTOMATIC_SNAPSHOT_DEBOUNCE_INTERVAL).on(this._ctx, async () => {
149
- const latestTimeframe = this._pipeline?.state.timeframe;
150
- if (!latestTimeframe) {
151
- return;
152
- }
153
-
154
- // Save snapshot.
155
- if (
156
- latestTimeframe.totalMessages() - this._lastAutomaticSnapshotTimeframe.totalMessages() >
157
- MESSAGES_PER_SNAPSHOT
158
- ) {
159
- const snapshot = await this._saveSnapshot();
160
- this._lastAutomaticSnapshotTimeframe = snapshot.timeframe ?? failUndefined();
161
- log('save', { snapshot });
162
- }
163
- });
164
- }
165
- }
166
-
167
- private async _saveSnapshot() {
168
- const snapshot = await this.createSnapshot();
169
- const snapshotKey = await this._params.snapshotManager.store(snapshot);
170
- await this._params.metadataStore.setSpaceSnapshot(this._params.spaceKey, snapshotKey);
171
- return snapshot;
172
- }
173
-
174
- private async _saveLatestTimeframe() {
175
- const latestTimeframe = this._pipeline?.state.timeframe;
176
- log('save latest timeframe', { latestTimeframe, spaceKey: this._params.spaceKey });
177
- if (latestTimeframe) {
178
- const newTimeframe = Timeframe.merge(this._targetTimeframe ?? new Timeframe(), latestTimeframe);
179
- await this._params.metadataStore.setSpaceLatestTimeframe(this._params.spaceKey, newTimeframe);
180
- }
181
- }
182
-
183
140
  @synchronized
184
141
  async close() {
185
142
  if (!this._isOpen) {
@@ -188,28 +145,26 @@ export class DataPipeline {
188
145
  log('close');
189
146
  this._isOpen = false;
190
147
 
148
+ await this._ctx.dispose();
149
+ await this._pipeline?.stop();
150
+
151
+ // NOTE: Make sure the processing is stopped BEFORE we save the snapshot.
191
152
  try {
192
- await this._saveLatestTimeframe();
193
- await this._saveSnapshot();
153
+ if (this._pipeline) {
154
+ await this._saveTargetTimeframe(this._pipeline.state.timeframe);
155
+ if (!DISABLE_SNAPSHOT_CACHE) {
156
+ await this._saveSnapshot(this._pipeline.state.timeframe);
157
+ }
158
+ }
194
159
  } catch (err) {
195
160
  log.catch(err);
196
161
  }
197
- await this._ctx.dispose();
198
- await this._pipeline?.stop();
162
+
199
163
  await this.databaseBackend?.close();
200
164
  await this._itemManager?.destroy();
201
165
  await this._params.snapshotManager.close();
202
166
  }
203
167
 
204
- createSnapshot(): SpaceSnapshot {
205
- assert(this.databaseBackend, 'Database backend is not initialized.');
206
- return {
207
- spaceKey: this._params.spaceKey.asUint8Array(),
208
- timeframe: this._pipeline?.state.timeframe ?? new Timeframe(),
209
- database: this.databaseBackend!.createSnapshot()
210
- };
211
- }
212
-
213
168
  private async _consumePipeline() {
214
169
  assert(this._pipeline, 'Pipeline is not initialized.');
215
170
  for await (const msg of this._pipeline.consume()) {
@@ -233,7 +188,9 @@ export class DataPipeline {
233
188
  memberKey: feedInfo.assertion.identityKey
234
189
  }
235
190
  });
236
- this.onTimeframeReached.emit(data.timeframe);
191
+
192
+ // Timeframe clock is not updated yet
193
+ await this._noteTargetStateIfNeeded(this._pipeline.state.pendingTimeframe);
237
194
  }
238
195
  } catch (err: any) {
239
196
  log.catch(err);
@@ -241,6 +198,48 @@ export class DataPipeline {
241
198
  }
242
199
  }
243
200
 
201
+ private _createSnapshot(timeframe: Timeframe): SpaceSnapshot {
202
+ assert(this.databaseBackend, 'Database backend is not initialized.');
203
+ return {
204
+ spaceKey: this._params.spaceKey.asUint8Array(),
205
+ timeframe,
206
+ database: this.databaseBackend!.createSnapshot()
207
+ };
208
+ }
209
+
210
+ private async _saveSnapshot(timeframe: Timeframe) {
211
+ const snapshot = await this._createSnapshot(timeframe);
212
+ const snapshotKey = await this._params.snapshotManager.store(snapshot);
213
+ await this._params.metadataStore.setSpaceSnapshot(this._params.spaceKey, snapshotKey);
214
+ return snapshot;
215
+ }
216
+
217
+ private async _saveTargetTimeframe(timeframe: Timeframe) {
218
+ const newTimeframe = Timeframe.merge(this._targetTimeframe ?? new Timeframe(), timeframe);
219
+ await this._params.metadataStore.setSpaceLatestTimeframe(this._params.spaceKey, newTimeframe);
220
+ this._targetTimeframe = newTimeframe;
221
+ }
222
+
223
+ private async _noteTargetStateIfNeeded(timeframe: Timeframe) {
224
+ if (Date.now() - this._lastTimeframeSaveTime > TIMEFRAME_SAVE_DEBOUNCE_INTERVAL) {
225
+ this._lastTimeframeSaveTime = Date.now();
226
+
227
+ await this._saveTargetTimeframe(timeframe);
228
+ }
229
+
230
+ if (
231
+ !DISABLE_SNAPSHOT_CACHE &&
232
+ Date.now() - this._lastSnapshotSaveTime > AUTOMATIC_SNAPSHOT_DEBOUNCE_INTERVAL &&
233
+ timeframe.totalMessages() - this._lastAutomaticSnapshotTimeframe.totalMessages() > MESSAGES_PER_SNAPSHOT
234
+ ) {
235
+ this._lastSnapshotSaveTime = Date.now();
236
+
237
+ const snapshot = await this._saveSnapshot(timeframe);
238
+ this._lastAutomaticSnapshotTimeframe = snapshot.timeframe ?? failUndefined();
239
+ log('save', { snapshot });
240
+ }
241
+ }
242
+
244
243
  async waitUntilTimeframe(timeframe: Timeframe) {
245
244
  assert(this._pipeline, 'Pipeline is not initialized.');
246
245
  await this._pipeline.state.waitUntilTimeframe(timeframe);
@@ -48,6 +48,7 @@ export class SpaceManager {
48
48
  private readonly _feedStore: FeedStore<FeedMessage>;
49
49
  private readonly _networkManager: NetworkManager;
50
50
  private readonly _instanceId = PublicKey.random().toHex();
51
+ public _traceParent?: string;
51
52
 
52
53
  constructor({ feedStore, networkManager }: SpaceManagerParams) {
53
54
  // TODO(burdon): Assert.
@@ -62,7 +63,7 @@ export class SpaceManager {
62
63
 
63
64
  @synchronized
64
65
  async open() {
65
- log.trace('dxos.echo.space-manager', trace.begin({ id: this._instanceId }));
66
+ log.trace('dxos.echo.space-manager', trace.begin({ id: this._instanceId, parentId: this._traceParent }));
66
67
  }
67
68
 
68
69
  @synchronized
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Event } from '@dxos/async';
6
6
  import { DocumentModel } from '@dxos/document-model';
7
- import { DatabaseBackendProxy, ItemManager } from '@dxos/echo-db';
7
+ import { DatabaseProxy, ItemManager } from '@dxos/echo-db';
8
8
  import { PublicKey } from '@dxos/keys';
9
9
  import { ModelFactory } from '@dxos/model-factory';
10
10
  import { FeedMessageBlock } from '@dxos/protocols';
@@ -14,7 +14,7 @@ import { TextModel } from '@dxos/text-model';
14
14
  import { Timeframe } from '@dxos/timeframe';
15
15
  import { ComplexMap, isNotNullOrUndefined } from '@dxos/util';
16
16
 
17
- import { DatabaseBackendHost } from '../dbhost';
17
+ import { DatabaseHost } from '../dbhost';
18
18
 
19
19
  const SPACE_KEY = PublicKey.random();
20
20
 
@@ -33,9 +33,9 @@ export class DatabaseTestPeer {
33
33
  public readonly modelFactory = new ModelFactory().registerModel(DocumentModel).registerModel(TextModel);
34
34
 
35
35
  public items!: ItemManager;
36
- public proxy!: DatabaseBackendProxy;
36
+ public proxy!: DatabaseProxy;
37
37
 
38
- public host!: DatabaseBackendHost;
38
+ public host!: DatabaseHost;
39
39
  public hostItems!: ItemManager;
40
40
 
41
41
  //
@@ -64,7 +64,7 @@ export class DatabaseTestPeer {
64
64
 
65
65
  async open() {
66
66
  this.hostItems = new ItemManager(this.modelFactory);
67
- this.host = new DatabaseBackendHost(
67
+ this.host = new DatabaseHost(
68
68
  {
69
69
  write: async (message) => {
70
70
  const seq =
@@ -86,7 +86,7 @@ export class DatabaseTestPeer {
86
86
  );
87
87
  await this.host.open(this.hostItems, this.modelFactory);
88
88
 
89
- this.proxy = new DatabaseBackendProxy(this.host.createDataServiceHost(), SPACE_KEY);
89
+ this.proxy = new DatabaseProxy(this.host.createDataServiceHost(), SPACE_KEY);
90
90
  this.items = new ItemManager(this.modelFactory);
91
91
  await this.proxy.open(this.items, this.modelFactory);
92
92
  }