@dxos/echo-pipeline 0.1.24-next.6b1f434
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/LICENSE +8 -0
- package/README.md +22 -0
- package/dist/lib/browser/chunk-3GAE5CKD.mjs +1621 -0
- package/dist/lib/browser/chunk-3GAE5CKD.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +114 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/testing/index.mjs +238 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node/index.cjs +1747 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node/testing/index.cjs +1835 -0
- package/dist/lib/node/testing/index.cjs.map +7 -0
- package/dist/types/src/common/codec.d.ts +12 -0
- package/dist/types/src/common/codec.d.ts.map +1 -0
- package/dist/types/src/common/feeds.d.ts +7 -0
- package/dist/types/src/common/feeds.d.ts.map +1 -0
- package/dist/types/src/common/index.d.ts +3 -0
- package/dist/types/src/common/index.d.ts.map +1 -0
- package/dist/types/src/dbhost/data-service-host.d.ts +22 -0
- package/dist/types/src/dbhost/data-service-host.d.ts.map +1 -0
- package/dist/types/src/dbhost/data-service.d.ts +21 -0
- package/dist/types/src/dbhost/data-service.d.ts.map +1 -0
- package/dist/types/src/dbhost/database-backend.d.ts +28 -0
- package/dist/types/src/dbhost/database-backend.d.ts.map +1 -0
- package/dist/types/src/dbhost/index.d.ts +6 -0
- package/dist/types/src/dbhost/index.d.ts.map +1 -0
- package/dist/types/src/dbhost/snapshot-manager.d.ts +17 -0
- package/dist/types/src/dbhost/snapshot-manager.d.ts.map +1 -0
- package/dist/types/src/dbhost/snapshot-store.d.ts +9 -0
- package/dist/types/src/dbhost/snapshot-store.d.ts.map +1 -0
- package/dist/types/src/dbhost/snapshot-store.test.d.ts +2 -0
- package/dist/types/src/dbhost/snapshot-store.test.d.ts.map +1 -0
- package/dist/types/src/errors/errors.d.ts +26 -0
- package/dist/types/src/errors/errors.d.ts.map +1 -0
- package/dist/types/src/errors/index.d.ts +2 -0
- package/dist/types/src/errors/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +7 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/metadata/index.d.ts +2 -0
- package/dist/types/src/metadata/index.d.ts.map +1 -0
- package/dist/types/src/metadata/metadata-store.d.ts +42 -0
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -0
- package/dist/types/src/pipeline/index.d.ts +3 -0
- package/dist/types/src/pipeline/index.d.ts.map +1 -0
- package/dist/types/src/pipeline/message-selector.d.ts +11 -0
- package/dist/types/src/pipeline/message-selector.d.ts.map +1 -0
- package/dist/types/src/pipeline/pipeline.d.ts +98 -0
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -0
- package/dist/types/src/pipeline/pipeline.test.d.ts +2 -0
- package/dist/types/src/pipeline/pipeline.test.d.ts.map +1 -0
- package/dist/types/src/pipeline/timeframe-clock.d.ts +19 -0
- package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -0
- package/dist/types/src/space/auth.d.ts +23 -0
- package/dist/types/src/space/auth.d.ts.map +1 -0
- package/dist/types/src/space/control-pipeline.d.ts +31 -0
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -0
- package/dist/types/src/space/control-pipeline.test.d.ts +2 -0
- package/dist/types/src/space/control-pipeline.test.d.ts.map +1 -0
- package/dist/types/src/space/data-pipeline-controller.d.ts +63 -0
- package/dist/types/src/space/data-pipeline-controller.d.ts.map +1 -0
- package/dist/types/src/space/genesis.d.ts +5 -0
- package/dist/types/src/space/genesis.d.ts.map +1 -0
- package/dist/types/src/space/index.d.ts +7 -0
- package/dist/types/src/space/index.d.ts.map +1 -0
- package/dist/types/src/space/replication.browser.test.d.ts +2 -0
- package/dist/types/src/space/replication.browser.test.d.ts.map +1 -0
- package/dist/types/src/space/space-manager.browser.test.d.ts +2 -0
- package/dist/types/src/space/space-manager.browser.test.d.ts.map +1 -0
- package/dist/types/src/space/space-manager.d.ts +41 -0
- package/dist/types/src/space/space-manager.d.ts.map +1 -0
- package/dist/types/src/space/space-protocol.browser.test.d.ts +2 -0
- package/dist/types/src/space/space-protocol.browser.test.d.ts.map +1 -0
- package/dist/types/src/space/space-protocol.d.ts +76 -0
- package/dist/types/src/space/space-protocol.d.ts.map +1 -0
- package/dist/types/src/space/space-protocol.test.d.ts +2 -0
- package/dist/types/src/space/space-protocol.test.d.ts.map +1 -0
- package/dist/types/src/space/space.d.ts +59 -0
- package/dist/types/src/space/space.d.ts.map +1 -0
- package/dist/types/src/space/space.test.d.ts +2 -0
- package/dist/types/src/space/space.test.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +4 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/test-agent-builder.d.ts +52 -0
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -0
- package/dist/types/src/testing/test-feed-builder.d.ts +9 -0
- package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -0
- package/dist/types/src/testing/util.d.ts +14 -0
- package/dist/types/src/testing/util.d.ts.map +1 -0
- package/dist/types/src/tests/database.test.d.ts +2 -0
- package/dist/types/src/tests/database.test.d.ts.map +1 -0
- package/package.json +60 -0
- package/src/common/codec.ts +18 -0
- package/src/common/feeds.ts +23 -0
- package/src/common/index.ts +6 -0
- package/src/dbhost/data-service-host.ts +103 -0
- package/src/dbhost/data-service.ts +66 -0
- package/src/dbhost/database-backend.ts +64 -0
- package/src/dbhost/index.ts +9 -0
- package/src/dbhost/snapshot-manager.ts +68 -0
- package/src/dbhost/snapshot-store.test.ts +30 -0
- package/src/dbhost/snapshot-store.ts +45 -0
- package/src/errors/errors.ts +52 -0
- package/src/errors/index.ts +5 -0
- package/src/index.ts +10 -0
- package/src/metadata/index.ts +5 -0
- package/src/metadata/metadata-store.ts +173 -0
- package/src/pipeline/index.ts +6 -0
- package/src/pipeline/message-selector.ts +38 -0
- package/src/pipeline/pipeline.test.ts +71 -0
- package/src/pipeline/pipeline.ts +249 -0
- package/src/pipeline/timeframe-clock.ts +56 -0
- package/src/space/auth.ts +88 -0
- package/src/space/control-pipeline.test.ts +162 -0
- package/src/space/control-pipeline.ts +103 -0
- package/src/space/data-pipeline-controller.ts +234 -0
- package/src/space/genesis.ts +29 -0
- package/src/space/index.ts +10 -0
- package/src/space/replication.browser.test.ts +64 -0
- package/src/space/space-manager.browser.test.ts +44 -0
- package/src/space/space-manager.ts +92 -0
- package/src/space/space-protocol.browser.test.ts +135 -0
- package/src/space/space-protocol.test.ts +136 -0
- package/src/space/space-protocol.ts +222 -0
- package/src/space/space.test.ts +144 -0
- package/src/space/space.ts +199 -0
- package/src/testing/index.ts +7 -0
- package/src/testing/test-agent-builder.ts +193 -0
- package/src/testing/test-feed-builder.ts +19 -0
- package/src/testing/util.ts +77 -0
- package/src/tests/database.test.ts +53 -0
- package/testing.d.ts +11 -0
- package/testing.js +5 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import expect from 'expect';
|
|
6
|
+
|
|
7
|
+
import { CredentialGenerator, createCredential } from '@dxos/credentials';
|
|
8
|
+
import { FeedFactory, FeedStore } from '@dxos/feed-store';
|
|
9
|
+
import { Keyring } from '@dxos/keyring';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
13
|
+
import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
14
|
+
import { createStorage, StorageType } from '@dxos/random-access-storage';
|
|
15
|
+
import { describe, test, afterTest } from '@dxos/test';
|
|
16
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
17
|
+
|
|
18
|
+
import { valueEncoding } from '../common';
|
|
19
|
+
import { ControlPipeline } from './control-pipeline';
|
|
20
|
+
|
|
21
|
+
describe('space/control-pipeline', () => {
|
|
22
|
+
test('admits feeds', async () => {
|
|
23
|
+
const keyring = new Keyring();
|
|
24
|
+
const spaceKey = await keyring.createKey();
|
|
25
|
+
const identityKey = await keyring.createKey();
|
|
26
|
+
const deviceKey = await keyring.createKey();
|
|
27
|
+
|
|
28
|
+
const feedStore = new FeedStore<FeedMessage>({
|
|
29
|
+
factory: new FeedFactory<FeedMessage>({
|
|
30
|
+
root: createStorage({ type: StorageType.RAM }).createDirectory(),
|
|
31
|
+
signer: keyring,
|
|
32
|
+
hypercore: {
|
|
33
|
+
valueEncoding
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const createFeed = async () => {
|
|
39
|
+
const feedKey = await keyring.createKey();
|
|
40
|
+
return feedStore.openFeed(feedKey, { writable: true });
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// TODO(dmaretskyi): Separate test for cold start after genesis.
|
|
44
|
+
const genesisFeed = await createFeed();
|
|
45
|
+
const controlPipeline = new ControlPipeline({
|
|
46
|
+
spaceKey,
|
|
47
|
+
genesisFeed,
|
|
48
|
+
initialTimeframe: new Timeframe(),
|
|
49
|
+
feedProvider: (key) => feedStore.openFeed(key)
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const admittedFeeds: PublicKey[] = [];
|
|
53
|
+
controlPipeline.onFeedAdmitted.set(async (info) => {
|
|
54
|
+
log.debug('feed admitted');
|
|
55
|
+
admittedFeeds.push(info.key);
|
|
56
|
+
});
|
|
57
|
+
expect(admittedFeeds).toEqual([]);
|
|
58
|
+
|
|
59
|
+
controlPipeline.setWriteFeed(genesisFeed);
|
|
60
|
+
await controlPipeline.start();
|
|
61
|
+
|
|
62
|
+
afterTest(() => controlPipeline.stop());
|
|
63
|
+
|
|
64
|
+
//
|
|
65
|
+
// Genesis
|
|
66
|
+
//
|
|
67
|
+
{
|
|
68
|
+
const generator = new CredentialGenerator(keyring, identityKey, deviceKey);
|
|
69
|
+
const credentials = await generator.createSpaceGenesis(spaceKey, genesisFeed.key);
|
|
70
|
+
expect(credentials).toHaveLength(3);
|
|
71
|
+
|
|
72
|
+
for (const credential of credentials) {
|
|
73
|
+
await controlPipeline.pipeline.writer?.write({
|
|
74
|
+
credential: {
|
|
75
|
+
credential
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await controlPipeline.pipeline.state.waitUntilTimeframe(controlPipeline.pipeline.state.endTimeframe);
|
|
81
|
+
expect(admittedFeeds).toEqual([genesisFeed.key]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// New control feed.
|
|
85
|
+
const controlFeed2 = await createFeed();
|
|
86
|
+
{
|
|
87
|
+
await controlPipeline.pipeline.writer!.write({
|
|
88
|
+
credential: {
|
|
89
|
+
credential: await createCredential({
|
|
90
|
+
signer: keyring,
|
|
91
|
+
issuer: identityKey,
|
|
92
|
+
subject: controlFeed2.key,
|
|
93
|
+
assertion: {
|
|
94
|
+
'@type': 'dxos.halo.credentials.AdmittedFeed',
|
|
95
|
+
spaceKey,
|
|
96
|
+
identityKey,
|
|
97
|
+
deviceKey,
|
|
98
|
+
designation: AdmittedFeed.Designation.CONTROL
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await controlPipeline.pipeline.state.waitUntilTimeframe(controlPipeline.pipeline.state.endTimeframe);
|
|
105
|
+
expect(admittedFeeds).toEqual([genesisFeed.key, controlFeed2.key]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// New data feed.
|
|
109
|
+
const dataFeed1 = await createFeed();
|
|
110
|
+
{
|
|
111
|
+
await controlPipeline.pipeline.writer!.write({
|
|
112
|
+
credential: {
|
|
113
|
+
credential: await createCredential({
|
|
114
|
+
signer: keyring,
|
|
115
|
+
issuer: identityKey,
|
|
116
|
+
subject: dataFeed1.key,
|
|
117
|
+
assertion: {
|
|
118
|
+
'@type': 'dxos.halo.credentials.AdmittedFeed',
|
|
119
|
+
spaceKey,
|
|
120
|
+
identityKey,
|
|
121
|
+
deviceKey,
|
|
122
|
+
designation: AdmittedFeed.Designation.DATA
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const end = controlPipeline.pipeline.state.endTimeframe;
|
|
129
|
+
await controlPipeline.pipeline.state.waitUntilTimeframe(end);
|
|
130
|
+
expect(admittedFeeds).toEqual([genesisFeed.key, controlFeed2.key, dataFeed1.key]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// TODO(dmaretskyi): Move to other test (data feed cannot admit feeds).
|
|
134
|
+
const dataFeed2 = await createFeed();
|
|
135
|
+
{
|
|
136
|
+
await dataFeed1.append({
|
|
137
|
+
payload: {
|
|
138
|
+
'@type': 'dxos.echo.feed.FeedMessage',
|
|
139
|
+
timeframe: controlPipeline.pipeline.state.timeframe,
|
|
140
|
+
credential: {
|
|
141
|
+
credential: await createCredential({
|
|
142
|
+
signer: keyring,
|
|
143
|
+
issuer: identityKey,
|
|
144
|
+
subject: dataFeed2.key,
|
|
145
|
+
assertion: {
|
|
146
|
+
'@type': 'dxos.halo.credentials.AdmittedFeed',
|
|
147
|
+
spaceKey,
|
|
148
|
+
identityKey,
|
|
149
|
+
deviceKey,
|
|
150
|
+
designation: AdmittedFeed.Designation.DATA
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
timeframe: new Timeframe()
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await controlPipeline.pipeline.state.waitUntilTimeframe(controlPipeline.pipeline.state.endTimeframe);
|
|
159
|
+
expect(admittedFeeds).toEqual([genesisFeed.key, controlFeed2.key, dataFeed1.key]);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { SpaceStateMachine, SpaceState, MemberInfo, FeedInfo } from '@dxos/credentials';
|
|
6
|
+
import { FeedWrapper } from '@dxos/feed-store';
|
|
7
|
+
import { PublicKey } from '@dxos/keys';
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
10
|
+
import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
11
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
12
|
+
import { AsyncCallback, Callback } from '@dxos/util';
|
|
13
|
+
|
|
14
|
+
import { Pipeline, PipelineAccessor } from '../pipeline';
|
|
15
|
+
|
|
16
|
+
export type ControlPipelineParams = {
|
|
17
|
+
spaceKey: PublicKey;
|
|
18
|
+
genesisFeed: FeedWrapper<FeedMessage>;
|
|
19
|
+
feedProvider: (feedKey: PublicKey) => Promise<FeedWrapper<FeedMessage>>;
|
|
20
|
+
initialTimeframe?: Timeframe;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Processes HALO credentials, which include genesis and invitations.
|
|
25
|
+
*/
|
|
26
|
+
export class ControlPipeline {
|
|
27
|
+
private readonly _pipeline: Pipeline;
|
|
28
|
+
private readonly _spaceStateMachine: SpaceStateMachine;
|
|
29
|
+
|
|
30
|
+
public readonly onFeedAdmitted = new Callback<AsyncCallback<FeedInfo>>();
|
|
31
|
+
public readonly onMemberAdmitted: Callback<AsyncCallback<MemberInfo>>;
|
|
32
|
+
public readonly onCredentialProcessed: Callback<AsyncCallback<Credential>>;
|
|
33
|
+
|
|
34
|
+
constructor({ spaceKey, genesisFeed, feedProvider, initialTimeframe }: ControlPipelineParams) {
|
|
35
|
+
this._pipeline = new Pipeline(initialTimeframe);
|
|
36
|
+
void this._pipeline.addFeed(genesisFeed); // TODO(burdon): Require async open/close?
|
|
37
|
+
|
|
38
|
+
this._spaceStateMachine = new SpaceStateMachine(spaceKey);
|
|
39
|
+
this._spaceStateMachine.onFeedAdmitted.set(async (info) => {
|
|
40
|
+
// log('feed admitted', { info });
|
|
41
|
+
log('feed admitted', { key: info.key });
|
|
42
|
+
|
|
43
|
+
// TODO(burdon): Check not stopping.
|
|
44
|
+
if (info.assertion.designation === AdmittedFeed.Designation.CONTROL && !info.key.equals(genesisFeed.key)) {
|
|
45
|
+
try {
|
|
46
|
+
const feed = await feedProvider(info.key);
|
|
47
|
+
await this._pipeline.addFeed(feed);
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
log.catch(err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await this.onFeedAdmitted.callIfSet(info);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.onMemberAdmitted = this._spaceStateMachine.onMemberAdmitted;
|
|
57
|
+
this.onCredentialProcessed = this._spaceStateMachine.onCredentialProcessed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get spaceState(): SpaceState {
|
|
61
|
+
return this._spaceStateMachine;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get pipeline(): PipelineAccessor {
|
|
65
|
+
return this._pipeline;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setWriteFeed(feed: FeedWrapper<FeedMessage>) {
|
|
69
|
+
this._pipeline.setWriteFeed(feed);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async start() {
|
|
73
|
+
log('starting...');
|
|
74
|
+
setTimeout(async () => {
|
|
75
|
+
for await (const msg of this._pipeline.consume()) {
|
|
76
|
+
try {
|
|
77
|
+
// log('processing', { msg });
|
|
78
|
+
log('processing', { key: msg.feedKey, seq: msg.seq });
|
|
79
|
+
if (msg.data.payload.credential) {
|
|
80
|
+
const result = await this._spaceStateMachine.process(
|
|
81
|
+
msg.data.payload.credential.credential,
|
|
82
|
+
PublicKey.from(msg.feedKey)
|
|
83
|
+
);
|
|
84
|
+
if (!result) {
|
|
85
|
+
log.warn('processing failed', { msg });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (err: any) {
|
|
89
|
+
log.catch(err);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await this._pipeline.start();
|
|
95
|
+
log('started');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async stop() {
|
|
99
|
+
log('stopping...');
|
|
100
|
+
await this._pipeline.stop();
|
|
101
|
+
log('stopped');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import { Event, scheduleTask, trackLeaks } from '@dxos/async';
|
|
8
|
+
import { Context } from '@dxos/context';
|
|
9
|
+
import { FeedInfo } from '@dxos/credentials';
|
|
10
|
+
import { failUndefined } from '@dxos/debug';
|
|
11
|
+
import { ItemManager } from '@dxos/echo-db';
|
|
12
|
+
import { PublicKey } from '@dxos/keys';
|
|
13
|
+
import { log } from '@dxos/log';
|
|
14
|
+
import { ModelFactory } from '@dxos/model-factory';
|
|
15
|
+
import { DataMessage, FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
16
|
+
import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
17
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
18
|
+
|
|
19
|
+
import { createMappedFeedWriter } from '../common';
|
|
20
|
+
import { DatabaseBackendHost, SnapshotManager } from '../dbhost';
|
|
21
|
+
import { MetadataStore } from '../metadata';
|
|
22
|
+
import { Pipeline } from '../pipeline';
|
|
23
|
+
|
|
24
|
+
export type DataPipelineControllerContext = {
|
|
25
|
+
openPipeline: (start: Timeframe) => Promise<Pipeline>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Controls data pipeline in the space.
|
|
30
|
+
* Consumes mutations from the feed and applies them to the database.
|
|
31
|
+
*/
|
|
32
|
+
export interface DataPipelineController {
|
|
33
|
+
open(context: DataPipelineControllerContext): Promise<void>;
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class NoopDataPipelineController implements DataPipelineController {
|
|
38
|
+
async open(context: DataPipelineControllerContext): Promise<void> {}
|
|
39
|
+
|
|
40
|
+
async close(): Promise<void> {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type DataPipelineControllerImplParams = {
|
|
44
|
+
modelFactory: ModelFactory;
|
|
45
|
+
snapshotManager: SnapshotManager;
|
|
46
|
+
metadataStore: MetadataStore;
|
|
47
|
+
memberKey: PublicKey;
|
|
48
|
+
spaceKey: PublicKey;
|
|
49
|
+
feedInfoProvider: (feedKey: PublicKey) => FeedInfo | undefined;
|
|
50
|
+
snapshotId: string | undefined;
|
|
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 = 5000;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Minimum time between recording latest timeframe in metadata.
|
|
65
|
+
*/
|
|
66
|
+
const TIMEFRAME_SAVE_DEBOUNCE_INTERVAL = 500;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Controls data pipeline in the space.
|
|
70
|
+
* Consumes the pipeline and updates the database.
|
|
71
|
+
* Reacts to new epochs to restart the pipeline.
|
|
72
|
+
*/
|
|
73
|
+
@trackLeaks('open', 'close')
|
|
74
|
+
export class DataPipelineControllerImpl implements DataPipelineController {
|
|
75
|
+
private _ctx = new Context();
|
|
76
|
+
private _spaceContext!: DataPipelineControllerContext;
|
|
77
|
+
private _pipeline?: Pipeline;
|
|
78
|
+
private _snapshot?: SpaceSnapshot;
|
|
79
|
+
|
|
80
|
+
private _lastAutomaticSnapshotTimeframe = new Timeframe();
|
|
81
|
+
|
|
82
|
+
public readonly onTimeframeReached = new Event<Timeframe>();
|
|
83
|
+
|
|
84
|
+
constructor(private readonly _params: DataPipelineControllerImplParams) {}
|
|
85
|
+
|
|
86
|
+
public _itemManager!: ItemManager;
|
|
87
|
+
public databaseBackend?: DatabaseBackendHost;
|
|
88
|
+
|
|
89
|
+
get pipelineState() {
|
|
90
|
+
return this._pipeline?.state;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get snapshotTimeframe() {
|
|
94
|
+
return this._snapshot?.timeframe;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getStartTimeframe(): Timeframe {
|
|
98
|
+
return snapshotTimeframeToStartingTimeframe(this.snapshotTimeframe ?? new Timeframe());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async open(spaceContext: DataPipelineControllerContext) {
|
|
102
|
+
this._spaceContext = spaceContext;
|
|
103
|
+
if (this._params.snapshotId) {
|
|
104
|
+
this._snapshot = await this._params.snapshotManager.load(this._params.snapshotId);
|
|
105
|
+
this._lastAutomaticSnapshotTimeframe = this._snapshot?.timeframe ?? new Timeframe();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this._pipeline = await this._spaceContext.openPipeline(this.getStartTimeframe());
|
|
109
|
+
|
|
110
|
+
// Create database backend.
|
|
111
|
+
const feedWriter = createMappedFeedWriter<DataMessage, FeedMessage.Payload>(
|
|
112
|
+
(data: DataMessage) => ({ data }),
|
|
113
|
+
this._pipeline.writer ?? failUndefined()
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
this.databaseBackend = new DatabaseBackendHost(feedWriter, this._snapshot?.database, {
|
|
117
|
+
snapshots: true // TODO(burdon): Config.
|
|
118
|
+
});
|
|
119
|
+
this._itemManager = new ItemManager(this._params.modelFactory);
|
|
120
|
+
|
|
121
|
+
// Connect pipeline to the database.
|
|
122
|
+
await this.databaseBackend.open(this._itemManager, this._params.modelFactory);
|
|
123
|
+
|
|
124
|
+
// Start message processing loop.
|
|
125
|
+
scheduleTask(this._ctx, async () => {
|
|
126
|
+
await this._consumePipeline();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
this._createPeriodicSnapshots();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private _createPeriodicSnapshots() {
|
|
133
|
+
// Record last timeframe.
|
|
134
|
+
this.onTimeframeReached.debounce(TIMEFRAME_SAVE_DEBOUNCE_INTERVAL).on(this._ctx, async () => {
|
|
135
|
+
await this._saveLatestTimeframe();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.onTimeframeReached.debounce(AUTOMATIC_SNAPSHOT_DEBOUNCE_INTERVAL).on(this._ctx, async () => {
|
|
139
|
+
const latestTimeframe = this._pipeline?.state.timeframe;
|
|
140
|
+
if (!latestTimeframe) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Save snapshot.
|
|
145
|
+
if (
|
|
146
|
+
latestTimeframe.totalMessages() - this._lastAutomaticSnapshotTimeframe.totalMessages() >
|
|
147
|
+
MESSAGES_PER_SNAPSHOT
|
|
148
|
+
) {
|
|
149
|
+
const snapshot = await this._saveSnapshot();
|
|
150
|
+
this._lastAutomaticSnapshotTimeframe = snapshot.timeframe ?? failUndefined();
|
|
151
|
+
log('save', { snapshot });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async _saveSnapshot() {
|
|
157
|
+
const snapshot = await this.createSnapshot();
|
|
158
|
+
const snapshotKey = await this._params.snapshotManager.store(snapshot);
|
|
159
|
+
await this._params.metadataStore.setSpaceSnapshot(this._params.spaceKey, snapshotKey);
|
|
160
|
+
return snapshot;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async _saveLatestTimeframe() {
|
|
164
|
+
const latestTimeframe = this._pipeline?.state.timeframe;
|
|
165
|
+
if (latestTimeframe) {
|
|
166
|
+
await this._params.metadataStore.setSpaceLatestTimeframe(this._params.spaceKey, latestTimeframe);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async close() {
|
|
171
|
+
try {
|
|
172
|
+
await this._saveLatestTimeframe();
|
|
173
|
+
await this._saveSnapshot();
|
|
174
|
+
} catch (err) {
|
|
175
|
+
log.catch(err);
|
|
176
|
+
}
|
|
177
|
+
await this._ctx.dispose();
|
|
178
|
+
await this._pipeline?.stop();
|
|
179
|
+
await this.databaseBackend?.close();
|
|
180
|
+
await this._itemManager?.destroy();
|
|
181
|
+
await this._params.snapshotManager.close();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
createSnapshot(): SpaceSnapshot {
|
|
185
|
+
return {
|
|
186
|
+
spaceKey: this._params.spaceKey.asUint8Array(),
|
|
187
|
+
timeframe: this._pipeline?.state.timeframe ?? new Timeframe(),
|
|
188
|
+
database: this.databaseBackend!.createSnapshot()
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async _consumePipeline() {
|
|
193
|
+
assert(this._pipeline, 'Pipeline is not initialized.');
|
|
194
|
+
for await (const msg of this._pipeline.consume()) {
|
|
195
|
+
const { feedKey, seq, data } = msg;
|
|
196
|
+
log('processing message', { msg });
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
if (data.payload.data) {
|
|
200
|
+
const feedInfo = this._params.feedInfoProvider(feedKey);
|
|
201
|
+
if (!feedInfo) {
|
|
202
|
+
log.error('Could not find feed.', { feedKey });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await this.databaseBackend!.echoProcessor({
|
|
207
|
+
data: data.payload.data.object,
|
|
208
|
+
meta: {
|
|
209
|
+
feedKey,
|
|
210
|
+
seq,
|
|
211
|
+
timeframe: data.timeframe,
|
|
212
|
+
memberKey: feedInfo.assertion.identityKey
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
this.onTimeframeReached.emit(data.timeframe);
|
|
216
|
+
}
|
|
217
|
+
} catch (err: any) {
|
|
218
|
+
log.catch(err);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async waitUntilTimeframe(timeframe: Timeframe) {
|
|
224
|
+
assert(this._pipeline, 'Pipeline is not initialized.');
|
|
225
|
+
await this._pipeline.state.waitUntilTimeframe(timeframe);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Increase all indexes by one so that we start processing the next mutation after the one in the snapshot.
|
|
231
|
+
*/
|
|
232
|
+
const snapshotTimeframeToStartingTimeframe = (snapshotTimeframe: Timeframe) => {
|
|
233
|
+
return snapshotTimeframe.map(([key, seq]) => [key, seq + 1]);
|
|
234
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { CredentialGenerator } from '@dxos/credentials';
|
|
6
|
+
import { failUndefined } from '@dxos/debug';
|
|
7
|
+
import { Keyring } from '@dxos/keyring';
|
|
8
|
+
import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
9
|
+
|
|
10
|
+
import { Space } from './space';
|
|
11
|
+
import { SigningContext } from './space-manager';
|
|
12
|
+
|
|
13
|
+
export const spaceGenesis = async (keyring: Keyring, signingContext: SigningContext, space: Space) => {
|
|
14
|
+
// Write genesis credentials.
|
|
15
|
+
const generator = new CredentialGenerator(keyring, signingContext.identityKey, signingContext.deviceKey);
|
|
16
|
+
|
|
17
|
+
const credentials = [
|
|
18
|
+
...(await generator.createSpaceGenesis(space.key, space.controlFeedKey ?? failUndefined(), signingContext.profile)),
|
|
19
|
+
await generator.createFeedAdmission(space.key, space.dataFeedKey ?? failUndefined(), AdmittedFeed.Designation.DATA)
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const credential of credentials) {
|
|
23
|
+
await space.controlPipeline.writer.write({
|
|
24
|
+
credential: { credential }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return credentials;
|
|
29
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2021 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @dxos/test platform=browser
|
|
6
|
+
|
|
7
|
+
import expect from 'expect';
|
|
8
|
+
import waitForExpect from 'wait-for-expect';
|
|
9
|
+
|
|
10
|
+
import { FeedFactory, FeedStore } from '@dxos/feed-store';
|
|
11
|
+
import { Keyring } from '@dxos/keyring';
|
|
12
|
+
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
13
|
+
import { createStorage } from '@dxos/random-access-storage';
|
|
14
|
+
import { describe, test } from '@dxos/test';
|
|
15
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
16
|
+
|
|
17
|
+
import { valueEncoding } from '../common';
|
|
18
|
+
|
|
19
|
+
describe('replication', () => {
|
|
20
|
+
test('replicates a feed through a direct stream', async () => {
|
|
21
|
+
// Some storage drivers may break when there are multiple storage instances.
|
|
22
|
+
const storage = createStorage();
|
|
23
|
+
|
|
24
|
+
// Creates an appropriate persistent storage for the browser: IDB in Chrome or File storage in Firefox.
|
|
25
|
+
const keyring1 = new Keyring();
|
|
26
|
+
const feedStore1 = new FeedStore<FeedMessage>({
|
|
27
|
+
factory: new FeedFactory<FeedMessage>({
|
|
28
|
+
root: storage.createDirectory('feeds1'),
|
|
29
|
+
signer: keyring1,
|
|
30
|
+
hypercore: {
|
|
31
|
+
valueEncoding
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const keyring2 = new Keyring();
|
|
37
|
+
const feedStore2 = new FeedStore<FeedMessage>({
|
|
38
|
+
factory: new FeedFactory<FeedMessage>({
|
|
39
|
+
root: storage.createDirectory('feeds2'),
|
|
40
|
+
signer: keyring2,
|
|
41
|
+
hypercore: {
|
|
42
|
+
valueEncoding
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const feed1 = await feedStore1.openFeed(await keyring1.createKey(), {
|
|
48
|
+
writable: true
|
|
49
|
+
});
|
|
50
|
+
const feed2 = await feedStore2.openFeed(feed1.key);
|
|
51
|
+
|
|
52
|
+
const stream1 = feed1.replicate(true, { live: true });
|
|
53
|
+
const stream2 = feed2.replicate(false, { live: true });
|
|
54
|
+
stream1.pipe(stream2).pipe(stream1);
|
|
55
|
+
|
|
56
|
+
await feed1.append({
|
|
57
|
+
timeframe: new Timeframe([[feed1.key, 123]])
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await waitForExpect(() => {
|
|
61
|
+
expect(feed2.properties.length).toEqual(1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @dxos/test platform=browser
|
|
6
|
+
|
|
7
|
+
import { createStorage } from '@dxos/random-access-storage';
|
|
8
|
+
import { describe, test, afterTest } from '@dxos/test';
|
|
9
|
+
|
|
10
|
+
import { TestAgentBuilder, WebsocketNetworkManagerProvider } from '../testing';
|
|
11
|
+
|
|
12
|
+
// TODO(burdon): Config.
|
|
13
|
+
// Signal server will be started by the setup script.
|
|
14
|
+
const SIGNAL_URL = 'ws://localhost:4000/.well-known/dx/signal';
|
|
15
|
+
|
|
16
|
+
describe('space-manager', () => {
|
|
17
|
+
test.skip('invitations', async () => {
|
|
18
|
+
const builder = new TestAgentBuilder({
|
|
19
|
+
storage: createStorage(),
|
|
20
|
+
networkManagerProvider: WebsocketNetworkManagerProvider(SIGNAL_URL)
|
|
21
|
+
});
|
|
22
|
+
afterTest(async () => await builder.close());
|
|
23
|
+
|
|
24
|
+
const peer1 = await builder.createPeer();
|
|
25
|
+
const spaceManager1 = peer1.createSpaceManager();
|
|
26
|
+
await spaceManager1.open();
|
|
27
|
+
|
|
28
|
+
const peer2 = await builder.createPeer();
|
|
29
|
+
const spaceManager2 = peer2.createSpaceManager();
|
|
30
|
+
await spaceManager2.open();
|
|
31
|
+
|
|
32
|
+
afterTest(() => spaceManager1.close());
|
|
33
|
+
afterTest(() => spaceManager2.close());
|
|
34
|
+
|
|
35
|
+
// const space1 = await spaceManager1.createSpace();
|
|
36
|
+
// expect(space1.isOpen).to.be.true;
|
|
37
|
+
|
|
38
|
+
// TODO(burdon): Create invitation and join.
|
|
39
|
+
// TODO(burdon): Need to factor out DataInvitations from services.
|
|
40
|
+
|
|
41
|
+
// const space2 = await spaceManager2.acceptSpace();
|
|
42
|
+
// expect(space2.key).not.to.be.undefined;
|
|
43
|
+
});
|
|
44
|
+
});
|