@dxos/echo-pipeline 0.1.36-next.ef27157 → 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 (48) hide show
  1. package/dist/lib/browser/{chunk-LIPDNX3G.mjs → chunk-4YAT2XJW.mjs} +159 -125
  2. package/dist/lib/browser/chunk-4YAT2XJW.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +3 -3
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +5 -5
  6. package/dist/lib/browser/testing/index.mjs.map +3 -3
  7. package/dist/lib/node/index.cjs +158 -124
  8. package/dist/lib/node/index.cjs.map +4 -4
  9. package/dist/lib/node/meta.json +1 -1
  10. package/dist/lib/node/testing/index.cjs +158 -124
  11. package/dist/lib/node/testing/index.cjs.map +4 -4
  12. package/dist/types/src/dbhost/data-service-host.d.ts.map +1 -1
  13. package/dist/types/src/dbhost/{database-backend.d.ts → database-host.d.ts} +2 -2
  14. package/dist/types/src/dbhost/database-host.d.ts.map +1 -0
  15. package/dist/types/src/dbhost/index.d.ts +1 -1
  16. package/dist/types/src/dbhost/index.d.ts.map +1 -1
  17. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  18. package/dist/types/src/pipeline/pipeline.d.ts +1 -0
  19. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  20. package/dist/types/src/pipeline/timeframe-clock.d.ts +11 -1
  21. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  22. package/dist/types/src/space/data-pipeline.d.ts +8 -9
  23. package/dist/types/src/space/data-pipeline.d.ts.map +1 -1
  24. package/dist/types/src/space/data-pipeline.test.d.ts +1 -0
  25. package/dist/types/src/space/data-pipeline.test.d.ts.map +1 -0
  26. package/dist/types/src/space/space-manager.d.ts +1 -0
  27. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  28. package/dist/types/src/testing/database-test-rig.d.ts +4 -4
  29. package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
  30. package/dist/types/src/testing/util.d.ts +4 -4
  31. package/dist/types/src/testing/util.d.ts.map +1 -1
  32. package/package.json +31 -29
  33. package/src/common/feeds.ts +1 -1
  34. package/src/dbhost/data-service-host.ts +16 -9
  35. package/src/dbhost/{database-backend.ts → database-host.ts} +1 -1
  36. package/src/dbhost/index.ts +1 -1
  37. package/src/metadata/metadata-store.ts +24 -20
  38. package/src/pipeline/pipeline.ts +6 -1
  39. package/src/pipeline/timeframe-clock.ts +22 -3
  40. package/src/space/data-pipeline.test.ts +3 -0
  41. package/src/space/data-pipeline.ts +63 -64
  42. package/src/space/space-manager.ts +2 -1
  43. package/src/testing/database-test-rig.ts +6 -6
  44. package/src/testing/util.ts +4 -4
  45. package/src/tests/database-unit.test.ts +13 -0
  46. package/src/tests/database.test.ts +39 -1
  47. package/dist/lib/browser/chunk-LIPDNX3G.mjs.map +0 -7
  48. package/dist/types/src/dbhost/database-backend.d.ts.map +0 -1
@@ -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,7 +46,7 @@ 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
 
@@ -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;
@@ -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
  }
@@ -4,19 +4,19 @@
4
4
 
5
5
  import { asyncTimeout } 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 { MockFeedWriter } from '@dxos/feed-store/testing';
9
9
  import { PublicKey } from '@dxos/keys';
10
10
  import { ModelFactory } from '@dxos/model-factory';
11
11
  import { DataMessage } from '@dxos/protocols/proto/dxos/echo/feed';
12
12
  import { Timeframe } from '@dxos/timeframe';
13
13
 
14
- import { DatabaseBackendHost, DataServiceHost, DataServiceImpl, DataServiceSubscriptions } from '../dbhost';
14
+ import { DatabaseHost, DataServiceHost, DataServiceImpl, DataServiceSubscriptions } from '../dbhost';
15
15
  import { DataPipeline } from '../space';
16
16
 
17
17
  export const createMemoryDatabase = async (modelFactory: ModelFactory) => {
18
18
  const feed = new MockFeedWriter<DataMessage>();
19
- const backend = new DatabaseBackendHost(feed, undefined);
19
+ const backend = new DatabaseHost(feed, undefined);
20
20
 
21
21
  feed.written.on(([data, meta]) =>
22
22
  backend.echoProcessor({
@@ -47,7 +47,7 @@ export const createRemoteDatabaseFromDataServiceHost = async (
47
47
  const spaceKey = PublicKey.random();
48
48
  dataServiceSubscriptions.registerSpace(spaceKey, dataServiceHost);
49
49
 
50
- const backend = new DatabaseBackendProxy(dataService, spaceKey);
50
+ const backend = new DatabaseProxy(dataService, spaceKey);
51
51
  const itemManager = new ItemManager(modelFactory);
52
52
  await backend.open(itemManager, new ModelFactory().registerModel(DocumentModel));
53
53
  return {
@@ -104,4 +104,17 @@ describe('database (unit)', () => {
104
104
  // TODO(dmaretskyi): Helper functions to compare state.
105
105
  expect(peer1.items.getItem(id)!.state).toEqual(peer1.items.getItem(id)!.state);
106
106
  });
107
+
108
+ // TODO(dmaretskyi): Flush is broken in this test database fixture.
109
+ test
110
+ .skip('flush', async () => {
111
+ const builder = new DatabaseTestBuilder();
112
+ const peer = await builder.createPeer();
113
+
114
+ const id = PublicKey.random().toHex();
115
+ peer.proxy.mutate(genesisMutation(id, DocumentModel.meta.type));
116
+ peer.confirm();
117
+ await peer.proxy.flush();
118
+ })
119
+ .timeout(100);
107
120
  });
@@ -5,11 +5,15 @@
5
5
  import expect from 'expect';
6
6
 
7
7
  import { DocumentModel, MutationBuilder } from '@dxos/document-model';
8
- import { createModelMutation, encodeModelMutation, genesisMutation } from '@dxos/echo-db';
8
+ import { createModelMutation, DatabaseProxy, encodeModelMutation, genesisMutation, ItemManager } from '@dxos/echo-db';
9
+ import { TestBuilder as FeedTestBuilder } from '@dxos/feed-store/testing';
9
10
  import { PublicKey } from '@dxos/keys';
10
11
  import { ModelFactory } from '@dxos/model-factory';
12
+ import { DataMessage } from '@dxos/protocols/proto/dxos/echo/feed';
11
13
  import { test } from '@dxos/test';
12
14
 
15
+ import { createMappedFeedWriter } from '../common';
16
+ import { DatabaseHost, DataServiceImpl, DataServiceSubscriptions } from '../dbhost';
13
17
  import { createMemoryDatabase, createRemoteDatabaseFromDataServiceHost } from '../testing';
14
18
 
15
19
  const createDatabase = async () => {
@@ -23,6 +27,29 @@ const createDatabase = async () => {
23
27
  return proxy;
24
28
  };
25
29
 
30
+ const createDatabaseWithFeeds = async () => {
31
+ const modelFactory = new ModelFactory().registerModel(DocumentModel);
32
+
33
+ const feedTestBuilder = new FeedTestBuilder();
34
+ const feedStore = feedTestBuilder.createFeedStore();
35
+ const feed = await feedStore.openFeed(await feedTestBuilder.keyring.createKey(), { writable: true });
36
+
37
+ const writer = createMappedFeedWriter((data: DataMessage) => ({ data }), feed.createFeedWriter());
38
+ const host = new DatabaseHost(writer);
39
+ await host.open(new ItemManager(modelFactory), new ModelFactory().registerModel(DocumentModel));
40
+
41
+ const dataServiceSubscriptions = new DataServiceSubscriptions();
42
+ const dataService = new DataServiceImpl(dataServiceSubscriptions);
43
+
44
+ const spaceKey = PublicKey.random();
45
+ dataServiceSubscriptions.registerSpace(spaceKey, host.createDataServiceHost());
46
+
47
+ const proxy = new DatabaseProxy(dataService, spaceKey);
48
+ await proxy.open(new ItemManager(modelFactory), new ModelFactory().registerModel(DocumentModel));
49
+
50
+ return { proxy, host };
51
+ };
52
+
26
53
  describe('database', () => {
27
54
  describe('proxy-service mode', () => {
28
55
  test('create object', async () => {
@@ -49,5 +76,16 @@ describe('database', () => {
49
76
  await result.batch.waitToBeProcessed();
50
77
  expect(database.itemManager.getItem(id)!.state.data.test).toEqual(42);
51
78
  });
79
+
80
+ // TODO(dmaretskyi): Flush is broken in this mock database.
81
+ test
82
+ .skip('flush', async () => {
83
+ const { proxy } = await createDatabaseWithFeeds();
84
+
85
+ const id = PublicKey.random().toHex();
86
+ proxy.mutate(genesisMutation(id, DocumentModel.meta.type));
87
+ await proxy.flush();
88
+ })
89
+ .timeout(100);
52
90
  });
53
91
  });