@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.
- package/dist/lib/browser/{chunk-7U2NXI2P.mjs → chunk-GDF4DQU2.mjs} +692 -355
- package/dist/lib/browser/chunk-GDF4DQU2.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +14 -18
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/index.cjs +699 -362
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +696 -363
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/common/feeds.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service-host.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/snapshot-manager.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
- package/dist/types/src/pipeline/pipeline-stress.test.d.ts +2 -0
- package/dist/types/src/pipeline/pipeline-stress.test.d.ts.map +1 -0
- package/dist/types/src/pipeline/pipeline.d.ts +4 -3
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/space/auth.d.ts.map +1 -1
- package/dist/types/src/space/control-pipeline.d.ts +4 -0
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/data-pipeline.d.ts +2 -0
- package/dist/types/src/space/data-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +2 -1
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/database-test-rig.d.ts +4 -1
- package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/package.json +32 -31
- package/src/common/feeds.ts +1 -2
- package/src/db-host/data-service-host.ts +10 -4
- package/src/db-host/data-service.ts +1 -2
- package/src/db-host/snapshot-manager.ts +0 -2
- package/src/metadata/metadata-store.ts +2 -2
- package/src/pipeline/message-selector.ts +1 -2
- package/src/pipeline/pipeline-stress.test.ts +226 -0
- package/src/pipeline/pipeline.test.ts +30 -206
- package/src/pipeline/pipeline.ts +50 -24
- package/src/space/auth.ts +1 -2
- package/src/space/control-pipeline.ts +47 -22
- package/src/space/data-pipeline.ts +21 -12
- package/src/space/space-manager.ts +1 -0
- package/src/space/space-protocol.ts +3 -1
- package/src/space/space.test.ts +18 -17
- package/src/space/space.ts +7 -3
- package/src/testing/database-test-rig.ts +27 -2
- package/src/testing/test-agent-builder.ts +2 -1
- package/src/tests/database-unit.test.ts +39 -0
- 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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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('
|
|
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
|
|
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();
|
|
@@ -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: `
|
|
129
|
+
label: `space swarm ${topic.truncate()}`,
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
log('started');
|
package/src/space/space.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
invariant(space2.dataPipeline.databaseHost);
|
|
267
268
|
expect(space2.dataPipeline.itemManager.entities.size).toEqual(objectCount);
|
|
268
269
|
|
|
269
270
|
await testLocalDatabase(space2.dataPipeline);
|
package/src/space/space.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|