@dxos/echo-pipeline 0.4.8-next.fff1521 → 0.4.9-main.1057b49

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 (64) hide show
  1. package/dist/lib/browser/{chunk-3FVT6KX6.mjs → chunk-RTEEJ723.mjs} +289 -1807
  2. package/dist/lib/browser/chunk-RTEEJ723.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +633 -14
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +4 -332
  7. package/dist/lib/browser/testing/index.mjs.map +4 -4
  8. package/dist/lib/node/chunk-7VZVCCNF.cjs +1948 -0
  9. package/dist/lib/node/chunk-7VZVCCNF.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +643 -34
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +13 -338
  14. package/dist/lib/node/testing/index.cjs.map +4 -4
  15. package/dist/types/src/db-host/data-service.d.ts +3 -13
  16. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  17. package/dist/types/src/db-host/index.d.ts +0 -2
  18. package/dist/types/src/db-host/index.d.ts.map +1 -1
  19. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  20. package/dist/types/src/space/index.d.ts +0 -1
  21. package/dist/types/src/space/index.d.ts.map +1 -1
  22. package/dist/types/src/space/space-manager.d.ts +1 -4
  23. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  24. package/dist/types/src/space/space.d.ts +1 -7
  25. package/dist/types/src/space/space.d.ts.map +1 -1
  26. package/dist/types/src/testing/index.d.ts +0 -2
  27. package/dist/types/src/testing/index.d.ts.map +1 -1
  28. package/dist/types/src/testing/test-agent-builder.d.ts +1 -3
  29. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  30. package/package.json +30 -33
  31. package/src/automerge/automerge-host.ts +1 -1
  32. package/src/db-host/data-service.ts +10 -56
  33. package/src/db-host/index.ts +0 -2
  34. package/src/space/control-pipeline.ts +3 -1
  35. package/src/space/index.ts +0 -1
  36. package/src/space/space-manager.ts +1 -13
  37. package/src/space/space.test.ts +2 -112
  38. package/src/space/space.ts +2 -60
  39. package/src/testing/index.ts +0 -2
  40. package/src/testing/test-agent-builder.ts +3 -8
  41. package/dist/lib/browser/chunk-3FVT6KX6.mjs.map +0 -7
  42. package/dist/lib/node/chunk-WZ4WTAN6.cjs +0 -3454
  43. package/dist/lib/node/chunk-WZ4WTAN6.cjs.map +0 -7
  44. package/dist/types/src/db-host/data-service-host.d.ts +0 -38
  45. package/dist/types/src/db-host/data-service-host.d.ts.map +0 -1
  46. package/dist/types/src/db-host/database-host.d.ts +0 -27
  47. package/dist/types/src/db-host/database-host.d.ts.map +0 -1
  48. package/dist/types/src/space/data-pipeline.d.ts +0 -80
  49. package/dist/types/src/space/data-pipeline.d.ts.map +0 -1
  50. package/dist/types/src/space/data-pipeline.test.d.ts +0 -1
  51. package/dist/types/src/space/data-pipeline.test.d.ts.map +0 -1
  52. package/dist/types/src/testing/database-test-rig.d.ts +0 -67
  53. package/dist/types/src/testing/database-test-rig.d.ts.map +0 -1
  54. package/dist/types/src/testing/util.d.ts +0 -14
  55. package/dist/types/src/testing/util.d.ts.map +0 -1
  56. package/dist/types/src/tests/database.test.d.ts +0 -2
  57. package/dist/types/src/tests/database.test.d.ts.map +0 -1
  58. package/src/db-host/data-service-host.ts +0 -233
  59. package/src/db-host/database-host.ts +0 -63
  60. package/src/space/data-pipeline.test.ts +0 -3
  61. package/src/space/data-pipeline.ts +0 -468
  62. package/src/testing/database-test-rig.ts +0 -289
  63. package/src/testing/util.ts +0 -85
  64. package/src/tests/database.test.ts +0 -100
@@ -1,468 +0,0 @@
1
- //
2
- // Copyright 2022 DXOS.org
3
- //
4
-
5
- import { Event, scheduleTask, sleep, synchronized, trackLeaks } from '@dxos/async';
6
- import { Context } from '@dxos/context';
7
- import {
8
- type CredentialProcessor,
9
- type FeedInfo,
10
- type SpecificCredential,
11
- checkCredentialType,
12
- } from '@dxos/credentials';
13
- import { getStateMachineFromItem, ItemManager, TYPE_PROPERTIES } from '@dxos/echo-db';
14
- import { type FeedWriter } from '@dxos/feed-store';
15
- import { invariant } from '@dxos/invariant';
16
- import { type PublicKey } from '@dxos/keys';
17
- import { log, omit } from '@dxos/log';
18
- import { type ModelFactory } from '@dxos/model-factory';
19
- import { CancelledError, type DataPipelineProcessed } from '@dxos/protocols';
20
- import { type CreateEpochRequest } from '@dxos/protocols/proto/dxos/client/services';
21
- import { type DataMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
- import { type SpaceCache } from '@dxos/protocols/proto/dxos/echo/metadata';
23
- import { type ObjectSnapshot } from '@dxos/protocols/proto/dxos/echo/model/document';
24
- import { type SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
25
- import { type Credential, type Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
26
- import { Timeframe } from '@dxos/timeframe';
27
- import { TimeSeriesCounter, TimeUsageCounter, trace } from '@dxos/tracing';
28
- import { tracer } from '@dxos/util';
29
-
30
- import { DatabaseHost, type SnapshotManager } from '../db-host';
31
- import { type MetadataStore } from '../metadata';
32
- import { Pipeline } from '../pipeline';
33
-
34
- export interface PipelineFactory {
35
- openPipeline: (start: Timeframe) => Promise<Pipeline>;
36
- }
37
-
38
- export type DataPipelineParams = {
39
- modelFactory: ModelFactory;
40
- snapshotManager: SnapshotManager;
41
- metadataStore: MetadataStore;
42
- memberKey: PublicKey;
43
- spaceKey: PublicKey;
44
- feedInfoProvider: (feedKey: PublicKey) => FeedInfo | undefined;
45
- snapshotId: string | undefined;
46
-
47
- /**
48
- * Called once.
49
- */
50
- onPipelineCreated: (pipeline: Pipeline) => Promise<void>;
51
- };
52
-
53
- /**
54
- * Number of mutations since the last snapshot before we automatically create another snapshot.
55
- */
56
- const MESSAGES_PER_SNAPSHOT = 10;
57
-
58
- /**
59
- * Minimum time between automatic snapshots.
60
- */
61
- const AUTOMATIC_SNAPSHOT_DEBOUNCE_INTERVAL = 5_000;
62
-
63
- /**
64
- * Minimum time in MS between recording latest timeframe in metadata.
65
- */
66
- const TIMEFRAME_SAVE_DEBOUNCE_INTERVAL = 5_000;
67
-
68
- export type CreateEpochOptions = {
69
- migration?: CreateEpochRequest.Migration;
70
- };
71
-
72
- /**
73
- * Controls data pipeline in the space.
74
- * Consumes the pipeline and updates the database.
75
- * Reacts to new epochs to restart the pipeline.
76
- */
77
- @trackLeaks('open', 'close')
78
- @trace.resource()
79
- export class DataPipeline implements CredentialProcessor {
80
- private _ctx = new Context();
81
- private _pipeline?: Pipeline = undefined;
82
- private _targetTimeframe?: Timeframe = undefined;
83
-
84
- private _lastAutomaticSnapshotTimeframe = new Timeframe();
85
- private _isOpen = false;
86
-
87
- private _lastTimeframeSaveTime = 0;
88
- private _lastSnapshotSaveTime = 0;
89
- private _lastProcessedEpoch = -1;
90
- private _epochCtx?: Context;
91
-
92
- @trace.metricsCounter()
93
- private _usage = new TimeUsageCounter();
94
-
95
- @trace.metricsCounter()
96
- private _mutations = new TimeSeriesCounter();
97
-
98
- public databaseHost?: DatabaseHost;
99
-
100
- public itemManager!: ItemManager;
101
-
102
- /**
103
- * Current epoch. Might be still processing.
104
- */
105
- public currentEpoch?: SpecificCredential<Epoch> = undefined;
106
-
107
- /**
108
- * Epoch currently applied.
109
- */
110
- public appliedEpoch?: SpecificCredential<Epoch> = undefined;
111
-
112
- public readonly onNewEpoch = new Event<Credential>();
113
-
114
- constructor(private readonly _params: DataPipelineParams) {}
115
-
116
- get isOpen() {
117
- return this._isOpen;
118
- }
119
-
120
- get pipeline() {
121
- return this._pipeline;
122
- }
123
-
124
- get pipelineState() {
125
- return this._pipeline?.state;
126
- }
127
-
128
- setTargetTimeframe(timeframe: Timeframe) {
129
- this._targetTimeframe = timeframe;
130
- this._pipeline?.state.setTargetTimeframe(timeframe);
131
- }
132
-
133
- async processCredential(credential: Credential) {
134
- if (!checkCredentialType(credential, 'dxos.halo.credentials.Epoch')) {
135
- return;
136
- }
137
-
138
- this.currentEpoch = credential;
139
- if (this._isOpen) {
140
- // process epoch
141
- await this._processEpochInSeparateTask(credential);
142
- }
143
- }
144
-
145
- @synchronized
146
- async open() {
147
- if (this._isOpen) {
148
- return;
149
- }
150
-
151
- this._pipeline = new Pipeline();
152
- await this._params.onPipelineCreated(this._pipeline);
153
-
154
- await this._pipeline.pause(); // Start paused until we have the first epoch.
155
- await this._pipeline.start();
156
-
157
- if (this._targetTimeframe) {
158
- this._pipeline.state.setTargetTimeframe(this._targetTimeframe);
159
- }
160
-
161
- // Create database backend.
162
- const feedWriter: FeedWriter<DataMessage> = {
163
- write: (data, options) => {
164
- invariant(this._pipeline, 'Pipeline is not initialized.');
165
- invariant(this.currentEpoch, 'Epoch is not initialized.');
166
- return this._pipeline.writer.write({ data }, options);
167
- },
168
- };
169
-
170
- this.databaseHost = new DatabaseHost(feedWriter, () => this._flush());
171
- this.itemManager = new ItemManager(this._params.modelFactory);
172
-
173
- // Connect pipeline to the database.
174
- await this.databaseHost.open(this.itemManager, this._params.modelFactory);
175
-
176
- // Start message processing loop.
177
- scheduleTask(this._ctx, async () => {
178
- await this._consumePipeline();
179
- });
180
-
181
- this._isOpen = true;
182
- }
183
-
184
- @synchronized
185
- async close() {
186
- if (!this._isOpen) {
187
- return;
188
- }
189
- log('close');
190
- this._isOpen = false;
191
-
192
- await this._ctx.dispose();
193
- await this._pipeline?.stop();
194
-
195
- // NOTE: Make sure the processing is stopped BEFORE we save the snapshot.
196
- try {
197
- await this._saveCache();
198
- if (this._pipeline) {
199
- await this._saveTargetTimeframe(this._pipeline.state.timeframe);
200
- }
201
- } catch (err) {
202
- log.catch(err);
203
- }
204
-
205
- await this.databaseHost?.close();
206
- await this.itemManager?.destroy();
207
-
208
- this._ctx = new Context();
209
- this._pipeline = undefined;
210
- this._targetTimeframe = undefined;
211
- this._lastAutomaticSnapshotTimeframe = new Timeframe();
212
- this.currentEpoch = undefined;
213
- this.appliedEpoch = undefined;
214
- this._lastProcessedEpoch = -1;
215
- this._epochCtx = undefined;
216
- }
217
-
218
- private async _consumePipeline() {
219
- const pipeline = this._pipeline;
220
- if (this.currentEpoch) {
221
- const waitForOneEpoch = this.onNewEpoch.waitForCount(1);
222
- await this._processEpochInSeparateTask(this.currentEpoch);
223
- await waitForOneEpoch;
224
- }
225
-
226
- // CPU bottleneck control.
227
- let messageCounter = 0;
228
-
229
- invariant(pipeline, 'Pipeline is not initialized.');
230
- for await (const msg of pipeline.consume()) {
231
- const span = this._usage.beginRecording();
232
- this._mutations.inc();
233
-
234
- const { feedKey, seq, data } = msg;
235
- log('processing message', { feedKey, seq });
236
-
237
- try {
238
- if (data.payload.data) {
239
- const feedInfo = this._params.feedInfoProvider(feedKey);
240
- if (!feedInfo) {
241
- log.warn('Could not find feed', { feedKey });
242
- continue;
243
- }
244
-
245
- const timer = tracer.mark('dxos.echo.pipeline.data'); // TODO(burdon): Add ID to params to filter.
246
- this.databaseHost!.echoProcessor({
247
- batch: data.payload.data.batch,
248
- meta: {
249
- feedKey,
250
- seq,
251
- timeframe: data.timeframe,
252
- memberKey: feedInfo.assertion.identityKey,
253
- },
254
- });
255
-
256
- timer.end();
257
- // TODO(burdon): Reconcile different tracer approaches.
258
- log.trace('dxos.echo.data-pipeline.processed', {
259
- feedKey: feedKey.toHex(), // TODO(burdon): Need to flatten?
260
- seq,
261
- spaceKey: this._params.spaceKey.toHex(),
262
- } satisfies DataPipelineProcessed);
263
-
264
- // Timeframe clock is not updated yet.
265
- await this._noteTargetStateIfNeeded(pipeline.state.pendingTimeframe);
266
- }
267
- } catch (err: any) {
268
- log.catch(err);
269
- }
270
-
271
- span.end();
272
-
273
- if (++messageCounter > 100) {
274
- messageCounter = 0;
275
- // Allow other tasks to process.
276
- await idle(1_000);
277
- }
278
- }
279
- }
280
-
281
- private _createSnapshot(): SpaceSnapshot {
282
- invariant(this.databaseHost, 'Database backend is not initialized.');
283
- return {
284
- spaceKey: this._params.spaceKey.asUint8Array(),
285
- timeframe: this._pipeline!.state.timeframe,
286
- database: this.databaseHost!.createSnapshot(),
287
- };
288
- }
289
-
290
- private async _saveTargetTimeframe(timeframe: Timeframe) {
291
- const newTimeframe = Timeframe.merge(this._targetTimeframe ?? new Timeframe(), timeframe);
292
- await this._params.metadataStore.setSpaceDataLatestTimeframe(this._params.spaceKey, newTimeframe);
293
- this._targetTimeframe = newTimeframe;
294
- }
295
-
296
- private async _saveCache() {
297
- const cache: SpaceCache = {};
298
-
299
- try {
300
- // Add properties to cache.
301
- const propertiesItem = this.itemManager.items.find(
302
- (item) =>
303
- item.modelMeta?.type === 'dxos.org/model/document' &&
304
- // TODO(burdon): Document?
305
- (getStateMachineFromItem(item)?.snapshot() as ObjectSnapshot).type === TYPE_PROPERTIES,
306
- );
307
- if (propertiesItem) {
308
- cache.properties = getStateMachineFromItem(propertiesItem)?.snapshot() as ObjectSnapshot;
309
- }
310
- } catch (err) {
311
- log.warn('Failed to cache properties', err);
312
- }
313
-
314
- // Save cache.
315
- await this._params.metadataStore.setCache(this._params.spaceKey, cache);
316
- }
317
-
318
- private async _noteTargetStateIfNeeded(timeframe: Timeframe) {
319
- if (!this._pipeline?.state.reachedTarget) {
320
- return;
321
- }
322
-
323
- // TODO(dmaretskyi): Replace this with a proper debounce/throttle.
324
-
325
- if (Date.now() - this._lastTimeframeSaveTime > TIMEFRAME_SAVE_DEBOUNCE_INTERVAL) {
326
- this._lastTimeframeSaveTime = Date.now();
327
-
328
- await this._saveTargetTimeframe(timeframe);
329
- }
330
-
331
- if (
332
- Date.now() - this._lastSnapshotSaveTime > AUTOMATIC_SNAPSHOT_DEBOUNCE_INTERVAL &&
333
- timeframe.totalMessages() - this._lastAutomaticSnapshotTimeframe.totalMessages() > MESSAGES_PER_SNAPSHOT
334
- ) {
335
- await this._saveCache();
336
- }
337
- }
338
-
339
- private async _processEpochInSeparateTask(epoch: SpecificCredential<Epoch>) {
340
- if (epoch.subject.assertion.number <= this._lastProcessedEpoch) {
341
- return;
342
- }
343
- await this._epochCtx?.dispose();
344
- const ctx = new Context({
345
- onError: (err) => {
346
- if (err instanceof CancelledError) {
347
- log('Epoch processing cancelled.');
348
- } else {
349
- log.catch(err);
350
- }
351
- },
352
- });
353
- this._epochCtx = ctx;
354
- scheduleTask(ctx, async () => {
355
- if (!this._isOpen) {
356
- // Space closed before we got to process the epoch.
357
- return;
358
- }
359
- await this._processEpoch(ctx, epoch.subject.assertion);
360
-
361
- // Carry over the snapshot CID from the previous epoch.
362
- if (epoch.subject.assertion.snapshotCid === undefined) {
363
- epoch.subject.assertion.snapshotCid = this.appliedEpoch?.subject.assertion.snapshotCid;
364
- }
365
-
366
- this.appliedEpoch = epoch;
367
- this.onNewEpoch.emit(epoch);
368
- });
369
- }
370
-
371
- @synchronized
372
- private async _processEpoch(ctx: Context, epoch: Epoch) {
373
- invariant(this._isOpen, 'Space is closed.');
374
- invariant(this._pipeline);
375
- this._lastProcessedEpoch = epoch.number;
376
-
377
- log('processing', { epoch: omit(epoch, 'proof') });
378
- if (epoch.snapshotCid) {
379
- const snapshot = await this._params.snapshotManager.load(ctx, epoch.snapshotCid);
380
- this.databaseHost!._itemDemuxer.restoreFromSnapshot(snapshot.database);
381
- }
382
-
383
- log('restarting pipeline from epoch');
384
- await this._pipeline.pause();
385
- await this._pipeline.setCursor(epoch.timeframe);
386
- await this._pipeline.unpause();
387
- }
388
-
389
- async waitUntilTimeframe(timeframe: Timeframe) {
390
- invariant(this._pipeline, 'Pipeline is not initialized.');
391
- await this._pipeline.state.waitUntilTimeframe(timeframe);
392
- }
393
-
394
- @synchronized
395
- async createEpoch(): Promise<Epoch> {
396
- invariant(this._pipeline);
397
- invariant(this.currentEpoch);
398
-
399
- await this._pipeline.pause();
400
-
401
- const snapshot = await this._createSnapshot();
402
- const snapshotCid = await this._params.snapshotManager.store(snapshot);
403
-
404
- const epoch: Epoch = {
405
- previousId: this.currentEpoch.id,
406
- timeframe: this._pipeline.state.timeframe,
407
- number: (this.currentEpoch.subject.assertion as Epoch).number + 1,
408
- snapshotCid,
409
- };
410
-
411
- await this._pipeline.unpause();
412
-
413
- return epoch;
414
- }
415
-
416
- async ensureEpochInitialized() {
417
- await this.onNewEpoch.waitForCondition(() => !!this.currentEpoch);
418
- }
419
-
420
- private async _flush() {
421
- try {
422
- await this._saveCache();
423
- if (this._pipeline) {
424
- await this._saveTargetTimeframe(this._pipeline.state.timeframe);
425
- }
426
- } catch (err) {
427
- log.catch(err);
428
- }
429
-
430
- await this._params.metadataStore.flush();
431
- }
432
- }
433
-
434
- /**
435
- * Waits up to `timeout` ms for the browser to be idle.
436
- */
437
- const idle = async (timeout?: number) => {
438
- if (!('scheduler' in globalThis && typeof (globalThis as any).scheduler.postTask === 'function')) {
439
- await sleep(1);
440
- return;
441
- }
442
-
443
- await new Promise<void>((resolve) => {
444
- // const beginTime = performance.now();
445
- const cleanup = () => {
446
- clearTimeout(timer);
447
- controller.abort();
448
- // log.warn('yielded for', { ms: performance.now() - beginTime });
449
- };
450
-
451
- const controller = new AbortController();
452
-
453
- void (globalThis as any).scheduler
454
- .postTask(
455
- () => {
456
- cleanup();
457
- resolve();
458
- },
459
- { priority: 'background', signal: controller.signal },
460
- )
461
- .catch(() => {});
462
-
463
- const timer = setTimeout(() => {
464
- cleanup();
465
- resolve();
466
- }, timeout);
467
- });
468
- };