@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,144 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import expect from 'expect';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
|
|
8
|
+
import { CredentialGenerator } from '@dxos/credentials';
|
|
9
|
+
import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
10
|
+
import { describe, test, afterTest } from '@dxos/test';
|
|
11
|
+
|
|
12
|
+
import { TestAgentBuilder, testLocalDatabase } from '../testing';
|
|
13
|
+
|
|
14
|
+
// TODO(burdon): Factor out?
|
|
15
|
+
const run = <T>(cb: () => Promise<T>): Promise<T> => cb();
|
|
16
|
+
|
|
17
|
+
describe('space/space', () => {
|
|
18
|
+
test('crates a database with object model', async () => {
|
|
19
|
+
const builder = new TestAgentBuilder();
|
|
20
|
+
afterTest(async () => await builder.close());
|
|
21
|
+
const agent = await builder.createPeer();
|
|
22
|
+
const [space, db] = await agent.createSpace();
|
|
23
|
+
|
|
24
|
+
await space.open();
|
|
25
|
+
expect(space.isOpen).toBeTruthy();
|
|
26
|
+
afterTest(() => space.close());
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
// Genesis
|
|
30
|
+
const generator = new CredentialGenerator(agent.keyring, agent.identityKey, agent.deviceKey);
|
|
31
|
+
const credentials = [
|
|
32
|
+
...(await generator.createSpaceGenesis(space.key, space.controlFeedKey!)),
|
|
33
|
+
await generator.createFeedAdmission(space.key, space.dataFeedKey!, AdmittedFeed.Designation.DATA)
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const credential of credentials) {
|
|
37
|
+
await space.controlPipeline.writer.write({
|
|
38
|
+
credential: { credential }
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// TODO(burdon): Debugging only.
|
|
43
|
+
await space.controlPipeline.state!.waitUntilTimeframe(space.controlPipeline.state!.endTimeframe);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assert(db.databaseBackend);
|
|
47
|
+
await testLocalDatabase(db);
|
|
48
|
+
|
|
49
|
+
await builder.close();
|
|
50
|
+
expect(space.isOpen).toBeFalsy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('two spaces replicating', async () => {
|
|
54
|
+
const builder = new TestAgentBuilder();
|
|
55
|
+
afterTest(async () => await builder.close());
|
|
56
|
+
|
|
57
|
+
//
|
|
58
|
+
// Agent 1
|
|
59
|
+
//
|
|
60
|
+
const [agent1, space1, db1] = await run(async () => {
|
|
61
|
+
const agent = await builder.createPeer();
|
|
62
|
+
const [space, db] = await agent.createSpace(agent.identityKey);
|
|
63
|
+
|
|
64
|
+
await space.open();
|
|
65
|
+
expect(space.isOpen).toBeTruthy();
|
|
66
|
+
afterTest(() => space.close());
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
// Genesis
|
|
70
|
+
const generator = new CredentialGenerator(agent.keyring, agent.identityKey, agent.deviceKey);
|
|
71
|
+
const credentials = [
|
|
72
|
+
...(await generator.createSpaceGenesis(space.key, space.controlFeedKey!)),
|
|
73
|
+
await generator.createFeedAdmission(space.key, space.dataFeedKey!, AdmittedFeed.Designation.DATA)
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
for (const credential of credentials) {
|
|
77
|
+
await space.controlPipeline.writer.write({
|
|
78
|
+
credential: { credential }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await space.controlPipeline.state!.waitUntilTimeframe(space.controlPipeline.state!.endTimeframe);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return [agent, space, db];
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
//
|
|
89
|
+
// Agent 2
|
|
90
|
+
//
|
|
91
|
+
const [agent2, space2, db2] = await run(async () => {
|
|
92
|
+
// NOTE: The genesisKey would be passed as part of the invitation.
|
|
93
|
+
const agent = await builder.createPeer();
|
|
94
|
+
const [space, db] = await agent.createSpace(agent.identityKey, space1.key, space1.genesisFeedKey);
|
|
95
|
+
|
|
96
|
+
await space.open();
|
|
97
|
+
expect(space.isOpen).toBeTruthy();
|
|
98
|
+
afterTest(() => space.close());
|
|
99
|
+
|
|
100
|
+
return [agent, space, db];
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(agent1).toBeDefined();
|
|
104
|
+
expect(agent2).toBeDefined();
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
// Write invitation from agent 1.
|
|
108
|
+
const generator = new CredentialGenerator(agent1.keyring, agent1.identityKey, agent1.deviceKey);
|
|
109
|
+
const credentials = await generator.createMemberInvitation(
|
|
110
|
+
space1.key,
|
|
111
|
+
agent2.identityKey,
|
|
112
|
+
agent2.deviceKey,
|
|
113
|
+
space2.controlFeedKey!,
|
|
114
|
+
space2.dataFeedKey!,
|
|
115
|
+
space1.genesisFeedKey
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
for (const credential of credentials) {
|
|
119
|
+
await space1.controlPipeline.writer.write({
|
|
120
|
+
credential: { credential }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
// Initial data exchange.
|
|
127
|
+
|
|
128
|
+
// Agent 1 reads all feed messages.
|
|
129
|
+
await space1.controlPipeline.state!.waitUntilTimeframe(space1.controlPipeline.state!.endTimeframe);
|
|
130
|
+
|
|
131
|
+
// Agent 2 reads all feed messages.
|
|
132
|
+
await space2.controlPipeline.state!.waitUntilTimeframe(space1.controlPipeline.state!.endTimeframe);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// TODO(burdon): Write multiple items (extract for all tests).
|
|
136
|
+
|
|
137
|
+
await testLocalDatabase(db1, db2);
|
|
138
|
+
await testLocalDatabase(db2, db1);
|
|
139
|
+
|
|
140
|
+
await builder.close();
|
|
141
|
+
expect(space1.isOpen).toBeFalsy();
|
|
142
|
+
expect(space2.isOpen).toBeFalsy();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import { Event, synchronized, trackLeaks } from '@dxos/async';
|
|
8
|
+
import { FeedInfo } from '@dxos/credentials';
|
|
9
|
+
import { FeedWrapper } from '@dxos/feed-store';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { log, logInfo } from '@dxos/log';
|
|
12
|
+
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
13
|
+
import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
14
|
+
import { AsyncCallback, Callback } from '@dxos/util';
|
|
15
|
+
|
|
16
|
+
import { Pipeline, PipelineAccessor } from '../pipeline';
|
|
17
|
+
import { ControlPipeline } from './control-pipeline';
|
|
18
|
+
import { DataPipelineController } from './data-pipeline-controller';
|
|
19
|
+
import { SpaceProtocol } from './space-protocol';
|
|
20
|
+
|
|
21
|
+
// TODO(burdon): Factor out?
|
|
22
|
+
type FeedProvider = (feedKey: PublicKey) => Promise<FeedWrapper<FeedMessage>>;
|
|
23
|
+
|
|
24
|
+
export type SpaceParams = {
|
|
25
|
+
spaceKey: PublicKey;
|
|
26
|
+
protocol: SpaceProtocol;
|
|
27
|
+
genesisFeed: FeedWrapper<FeedMessage>;
|
|
28
|
+
feedProvider: FeedProvider;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Spaces are globally addressable databases with access control.
|
|
33
|
+
*/
|
|
34
|
+
// TODO(dmaretskyi): Extract database stuff.
|
|
35
|
+
@trackLeaks('open', 'close')
|
|
36
|
+
export class Space {
|
|
37
|
+
public readonly onCredentialProcessed = new Callback<AsyncCallback<Credential>>();
|
|
38
|
+
public readonly stateUpdate = new Event();
|
|
39
|
+
public readonly protocol: SpaceProtocol;
|
|
40
|
+
|
|
41
|
+
private readonly _key: PublicKey;
|
|
42
|
+
private readonly _genesisFeedKey: PublicKey;
|
|
43
|
+
private readonly _feedProvider: FeedProvider;
|
|
44
|
+
private readonly _controlPipeline: ControlPipeline;
|
|
45
|
+
|
|
46
|
+
private _isOpen = false;
|
|
47
|
+
private _controlFeed?: FeedWrapper<FeedMessage>;
|
|
48
|
+
private _dataFeed?: FeedWrapper<FeedMessage>;
|
|
49
|
+
private _dataPipeline?: Pipeline;
|
|
50
|
+
private _dataPipelineController?: DataPipelineController;
|
|
51
|
+
|
|
52
|
+
constructor({ spaceKey, protocol, genesisFeed, feedProvider }: SpaceParams) {
|
|
53
|
+
assert(spaceKey && feedProvider);
|
|
54
|
+
this._key = spaceKey;
|
|
55
|
+
this._genesisFeedKey = genesisFeed.key;
|
|
56
|
+
this._feedProvider = feedProvider;
|
|
57
|
+
|
|
58
|
+
this._controlPipeline = new ControlPipeline({ spaceKey, genesisFeed, feedProvider });
|
|
59
|
+
this._controlPipeline.onFeedAdmitted.set(async (info) => {
|
|
60
|
+
if (info.assertion.designation === AdmittedFeed.Designation.DATA) {
|
|
61
|
+
// We will add all existing data feeds when the data pipeline is initialized.
|
|
62
|
+
if (this._dataPipeline) {
|
|
63
|
+
await this._dataPipeline.addFeed(await feedProvider(info.key));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!info.key.equals(genesisFeed.key)) {
|
|
68
|
+
this.protocol.addFeed(await feedProvider(info.key));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
this._controlPipeline.onCredentialProcessed.set(async (credential) => {
|
|
73
|
+
await this.onCredentialProcessed.callIfSet(credential);
|
|
74
|
+
log('onCredentialProcessed', { credential });
|
|
75
|
+
this.stateUpdate.emit();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Start replicating the genesis feed.
|
|
79
|
+
this.protocol = protocol;
|
|
80
|
+
this.protocol.addFeed(genesisFeed);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@logInfo
|
|
84
|
+
get key() {
|
|
85
|
+
return this._key;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get isOpen() {
|
|
89
|
+
return this._isOpen;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get genesisFeedKey(): PublicKey {
|
|
93
|
+
return this._genesisFeedKey;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get controlFeedKey() {
|
|
97
|
+
return this._controlFeed?.key;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get dataFeedKey() {
|
|
101
|
+
return this._dataFeed?.key;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get spaceState() {
|
|
105
|
+
return this._controlPipeline.spaceState;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @test-only
|
|
110
|
+
*/
|
|
111
|
+
get controlPipeline(): PipelineAccessor {
|
|
112
|
+
return this._controlPipeline.pipeline;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setControlFeed(feed: FeedWrapper<FeedMessage>) {
|
|
116
|
+
assert(!this._controlFeed, 'Control feed already set.');
|
|
117
|
+
this._controlFeed = feed;
|
|
118
|
+
this._controlPipeline.setWriteFeed(feed);
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setDataFeed(feed: FeedWrapper<FeedMessage>) {
|
|
123
|
+
assert(!this._dataFeed, 'Data feed already set.');
|
|
124
|
+
this._dataFeed = feed;
|
|
125
|
+
this._dataPipeline?.setWriteFeed(feed);
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Use for diagnostics.
|
|
131
|
+
*/
|
|
132
|
+
getControlFeeds(): FeedInfo[] {
|
|
133
|
+
return Array.from(this._controlPipeline.spaceState.feeds.values());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Use for diagnostics.
|
|
138
|
+
*/
|
|
139
|
+
// getDataFeeds(): FeedInfo[] {
|
|
140
|
+
// return this._dataPipeline?.getFeeds();
|
|
141
|
+
// }
|
|
142
|
+
|
|
143
|
+
@synchronized
|
|
144
|
+
async open() {
|
|
145
|
+
log('opening...');
|
|
146
|
+
if (this._isOpen) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Order is important.
|
|
151
|
+
await this._controlPipeline.start();
|
|
152
|
+
await this.protocol.start();
|
|
153
|
+
|
|
154
|
+
this._isOpen = true;
|
|
155
|
+
log('opened');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@synchronized
|
|
159
|
+
async close() {
|
|
160
|
+
log('closing...', { key: this._key });
|
|
161
|
+
if (!this._isOpen) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Closes in reverse order to open.
|
|
166
|
+
await this.protocol.stop();
|
|
167
|
+
await this._dataPipelineController?.close();
|
|
168
|
+
await this._controlPipeline.stop();
|
|
169
|
+
|
|
170
|
+
this._isOpen = false;
|
|
171
|
+
log('closed');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async initDataPipeline(controller: DataPipelineController) {
|
|
175
|
+
assert(this._isOpen, 'Space must be open to initialize data pipeline.');
|
|
176
|
+
assert(!this._dataPipelineController, 'Data pipeline already initialized.');
|
|
177
|
+
assert(!this._dataPipeline, 'Data pipeline already initialized.');
|
|
178
|
+
|
|
179
|
+
this._dataPipelineController = controller;
|
|
180
|
+
await this._dataPipelineController.open({
|
|
181
|
+
openPipeline: async (start) => {
|
|
182
|
+
assert(!this._dataPipeline, 'Data pipeline already initialized.'); // TODO(dmaretskyi): Allow concurrent pipelines.
|
|
183
|
+
// Create pipeline.
|
|
184
|
+
this._dataPipeline = new Pipeline(start);
|
|
185
|
+
if (this._dataFeed) {
|
|
186
|
+
this._dataPipeline.setWriteFeed(this._dataFeed);
|
|
187
|
+
}
|
|
188
|
+
for (const feed of this._controlPipeline.spaceState.feeds.values()) {
|
|
189
|
+
if (feed.assertion.designation === AdmittedFeed.Designation.DATA) {
|
|
190
|
+
await this._dataPipeline.addFeed(await this._feedProvider(feed.key));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await this._dataPipeline.start();
|
|
195
|
+
return this._dataPipeline;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { DocumentModel } from '@dxos/document-model';
|
|
6
|
+
import { FeedStore } from '@dxos/feed-store';
|
|
7
|
+
import { Keyring } from '@dxos/keyring';
|
|
8
|
+
import { PublicKey } from '@dxos/keys';
|
|
9
|
+
import { MemorySignalManager, MemorySignalManagerContext, WebsocketSignalManager } from '@dxos/messaging';
|
|
10
|
+
import { ModelFactory } from '@dxos/model-factory';
|
|
11
|
+
import { createWebRTCTransportFactory, MemoryTransportFactory, NetworkManager } from '@dxos/network-manager';
|
|
12
|
+
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
13
|
+
import { createStorage, Storage, StorageType } from '@dxos/random-access-storage';
|
|
14
|
+
import { Presence } from '@dxos/teleport-extension-presence';
|
|
15
|
+
import { ComplexMap } from '@dxos/util';
|
|
16
|
+
|
|
17
|
+
import { SnapshotManager, SnapshotStore } from '../dbhost';
|
|
18
|
+
import { MetadataStore } from '../metadata';
|
|
19
|
+
import { MOCK_AUTH_PROVIDER, MOCK_AUTH_VERIFIER, Space, SpaceManager, SpaceProtocol } from '../space';
|
|
20
|
+
import { DataPipelineControllerImpl } from '../space/data-pipeline-controller';
|
|
21
|
+
import { TestFeedBuilder } from './test-feed-builder';
|
|
22
|
+
|
|
23
|
+
export type NetworkManagerProvider = () => NetworkManager;
|
|
24
|
+
|
|
25
|
+
export const MemoryNetworkManagerProvider =
|
|
26
|
+
(signalContext: MemorySignalManagerContext): NetworkManagerProvider =>
|
|
27
|
+
() =>
|
|
28
|
+
new NetworkManager({
|
|
29
|
+
signalManager: new MemorySignalManager(signalContext),
|
|
30
|
+
transportFactory: MemoryTransportFactory
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const WebsocketNetworkManagerProvider =
|
|
34
|
+
(signalUrl: string): NetworkManagerProvider =>
|
|
35
|
+
() =>
|
|
36
|
+
new NetworkManager({
|
|
37
|
+
signalManager: new WebsocketSignalManager([signalUrl]),
|
|
38
|
+
transportFactory: createWebRTCTransportFactory()
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export type TestAgentBuilderOptions = {
|
|
42
|
+
storage?: Storage;
|
|
43
|
+
networkManagerProvider?: NetworkManagerProvider;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Factory for test agents.
|
|
48
|
+
*/
|
|
49
|
+
export class TestAgentBuilder {
|
|
50
|
+
private readonly _agents = new ComplexMap<PublicKey, TestAgent>(PublicKey.hash);
|
|
51
|
+
private readonly _storage: Storage;
|
|
52
|
+
private readonly _networkManagerProvider: NetworkManagerProvider;
|
|
53
|
+
|
|
54
|
+
constructor({ storage, networkManagerProvider }: TestAgentBuilderOptions = {}) {
|
|
55
|
+
this._storage = storage ?? createStorage({ type: StorageType.RAM });
|
|
56
|
+
this._networkManagerProvider =
|
|
57
|
+
networkManagerProvider ?? MemoryNetworkManagerProvider(new MemorySignalManagerContext());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async close() {
|
|
61
|
+
return Promise.all(this.agents.map((agent) => agent.close()));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get agents() {
|
|
65
|
+
return Array.from(this._agents.values());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getAgent(deviceKey: PublicKey) {
|
|
69
|
+
return this._agents.get(deviceKey);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async createPeer(): Promise<TestAgent> {
|
|
73
|
+
// prettier-ignore
|
|
74
|
+
const feedBuilder = new TestFeedBuilder()
|
|
75
|
+
.setStorage(this._storage, `agent-${this._agents.size}`);
|
|
76
|
+
|
|
77
|
+
const identityKey = await feedBuilder.keyring.createKey();
|
|
78
|
+
const deviceKey = await feedBuilder.keyring.createKey();
|
|
79
|
+
|
|
80
|
+
const agent = new TestAgent(this._networkManagerProvider, feedBuilder, identityKey, deviceKey);
|
|
81
|
+
this._agents.set(deviceKey, agent);
|
|
82
|
+
return agent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Test agent that enables the creation and replication of multiple spaces.
|
|
88
|
+
*/
|
|
89
|
+
export class TestAgent {
|
|
90
|
+
private readonly _spaces = new ComplexMap<PublicKey, Space>(PublicKey.hash);
|
|
91
|
+
|
|
92
|
+
public readonly keyring: Keyring;
|
|
93
|
+
public readonly feedStore: FeedStore<FeedMessage>;
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
private readonly _networkManagerProvider: NetworkManagerProvider,
|
|
97
|
+
private readonly _feedBuilder: TestFeedBuilder,
|
|
98
|
+
public readonly identityKey: PublicKey,
|
|
99
|
+
public readonly deviceKey: PublicKey
|
|
100
|
+
) {
|
|
101
|
+
this.keyring = this._feedBuilder.keyring;
|
|
102
|
+
this.feedStore = this._feedBuilder.createFeedStore();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async close() {
|
|
106
|
+
return Promise.all([...this.spaces.map((space) => space.close())]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get spaces() {
|
|
110
|
+
return Array.from(this._spaces.values());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getSpace(spaceKey: PublicKey) {
|
|
114
|
+
return this._spaces.get(spaceKey);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
createSpaceManager() {
|
|
118
|
+
return new SpaceManager({
|
|
119
|
+
feedStore: this._feedBuilder.createFeedStore(),
|
|
120
|
+
networkManager: this._networkManagerProvider()
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async createSpace(
|
|
125
|
+
identityKey: PublicKey = this.identityKey,
|
|
126
|
+
spaceKey?: PublicKey,
|
|
127
|
+
genesisKey?: PublicKey
|
|
128
|
+
): Promise<[Space, DataPipelineControllerImpl]> {
|
|
129
|
+
if (!spaceKey) {
|
|
130
|
+
spaceKey = await this.keyring.createKey();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const dataFeedKey = await this.keyring.createKey();
|
|
134
|
+
const dataFeed = await this.feedStore.openFeed(dataFeedKey, { writable: true });
|
|
135
|
+
|
|
136
|
+
const controlFeedKey = await this.keyring.createKey();
|
|
137
|
+
const controlFeed = await this.feedStore.openFeed(controlFeedKey, { writable: true });
|
|
138
|
+
const genesisFeed = genesisKey ? await this.feedStore.openFeed(genesisKey) : controlFeed;
|
|
139
|
+
const snapshotManager = new SnapshotManager(new SnapshotStore(createStorage().createDirectory('snapshots')));
|
|
140
|
+
|
|
141
|
+
const metadataStore = new MetadataStore(createStorage().createDirectory('metadata'));
|
|
142
|
+
await metadataStore.addSpace({ key: spaceKey });
|
|
143
|
+
const dataPipelineController: DataPipelineControllerImpl = new DataPipelineControllerImpl({
|
|
144
|
+
modelFactory: new ModelFactory().registerModel(DocumentModel),
|
|
145
|
+
metadataStore,
|
|
146
|
+
snapshotManager,
|
|
147
|
+
memberKey: identityKey,
|
|
148
|
+
spaceKey,
|
|
149
|
+
feedInfoProvider: (feedKey) => space.spaceState.feeds.get(feedKey),
|
|
150
|
+
snapshotId: undefined
|
|
151
|
+
});
|
|
152
|
+
const space = new Space({
|
|
153
|
+
spaceKey,
|
|
154
|
+
protocol: this.createSpaceProtocol(spaceKey),
|
|
155
|
+
genesisFeed,
|
|
156
|
+
feedProvider: (feedKey) => this.feedStore.openFeed(feedKey)
|
|
157
|
+
})
|
|
158
|
+
.setControlFeed(controlFeed)
|
|
159
|
+
.setDataFeed(dataFeed);
|
|
160
|
+
await space.open();
|
|
161
|
+
await space.initDataPipeline(dataPipelineController);
|
|
162
|
+
|
|
163
|
+
this._spaces.set(spaceKey, space);
|
|
164
|
+
return [space, dataPipelineController];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
createSpaceProtocol(topic: PublicKey, presence?: Presence) {
|
|
168
|
+
return new SpaceProtocol({
|
|
169
|
+
topic,
|
|
170
|
+
swarmIdentity: {
|
|
171
|
+
peerKey: this.deviceKey,
|
|
172
|
+
credentialProvider: MOCK_AUTH_PROVIDER,
|
|
173
|
+
credentialAuthenticator: MOCK_AUTH_VERIFIER
|
|
174
|
+
},
|
|
175
|
+
networkManager: this._networkManagerProvider(),
|
|
176
|
+
onSessionAuth: (session) => {
|
|
177
|
+
session.addExtension(
|
|
178
|
+
'dxos.mesh.teleport.presence',
|
|
179
|
+
(presence ?? this.createPresence()).createExtension({ remotePeerId: session.remotePeerId })
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
createPresence() {
|
|
186
|
+
return new Presence({
|
|
187
|
+
localPeerId: this.deviceKey,
|
|
188
|
+
announceInterval: 30,
|
|
189
|
+
offlineTimeout: 200,
|
|
190
|
+
identityKey: this.identityKey
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { TestBuilder } from '@dxos/feed-store/testing';
|
|
6
|
+
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
7
|
+
|
|
8
|
+
import { valueEncoding } from '../common';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builder with default encoder and generator.
|
|
12
|
+
*/
|
|
13
|
+
export class TestFeedBuilder extends TestBuilder<FeedMessage> {
|
|
14
|
+
constructor() {
|
|
15
|
+
super({
|
|
16
|
+
valueEncoding
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2021 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { asyncTimeout } from '@dxos/async';
|
|
6
|
+
import { DocumentModel } from '@dxos/document-model';
|
|
7
|
+
import { DatabaseBackendProxy, ItemManager } from '@dxos/echo-db';
|
|
8
|
+
import { MockFeedWriter } from '@dxos/feed-store/testing';
|
|
9
|
+
import { PublicKey } from '@dxos/keys';
|
|
10
|
+
import { ModelFactory } from '@dxos/model-factory';
|
|
11
|
+
import { DataMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
12
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
13
|
+
|
|
14
|
+
import { DatabaseBackendHost, DataServiceHost, DataServiceImpl, DataServiceSubscriptions } from '../dbhost';
|
|
15
|
+
import { DataPipelineControllerImpl } from '../space';
|
|
16
|
+
|
|
17
|
+
export const createMemoryDatabase = async (modelFactory: ModelFactory) => {
|
|
18
|
+
const feed = new MockFeedWriter<DataMessage>();
|
|
19
|
+
const backend = new DatabaseBackendHost(feed, undefined, { snapshots: true });
|
|
20
|
+
|
|
21
|
+
feed.written.on(([data, meta]) =>
|
|
22
|
+
backend.echoProcessor({
|
|
23
|
+
data: data.object,
|
|
24
|
+
meta: {
|
|
25
|
+
...meta,
|
|
26
|
+
memberKey: PublicKey.random(),
|
|
27
|
+
timeframe: new Timeframe([[meta.feedKey, meta.seq]])
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const itemManager = new ItemManager(modelFactory);
|
|
33
|
+
await backend.open(itemManager, new ModelFactory().registerModel(DocumentModel));
|
|
34
|
+
return {
|
|
35
|
+
backend,
|
|
36
|
+
itemManager
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const createRemoteDatabaseFromDataServiceHost = async (
|
|
41
|
+
modelFactory: ModelFactory,
|
|
42
|
+
dataServiceHost: DataServiceHost
|
|
43
|
+
) => {
|
|
44
|
+
const dataServiceSubscriptions = new DataServiceSubscriptions();
|
|
45
|
+
const dataService = new DataServiceImpl(dataServiceSubscriptions);
|
|
46
|
+
|
|
47
|
+
const spaceKey = PublicKey.random();
|
|
48
|
+
dataServiceSubscriptions.registerSpace(spaceKey, dataServiceHost);
|
|
49
|
+
|
|
50
|
+
const backend = new DatabaseBackendProxy(dataService, spaceKey);
|
|
51
|
+
const itemManager = new ItemManager(modelFactory);
|
|
52
|
+
await backend.open(itemManager, new ModelFactory().registerModel(DocumentModel));
|
|
53
|
+
return {
|
|
54
|
+
itemManager,
|
|
55
|
+
backend
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const testLocalDatabase = async (
|
|
60
|
+
create: DataPipelineControllerImpl,
|
|
61
|
+
check: DataPipelineControllerImpl = create
|
|
62
|
+
) => {
|
|
63
|
+
const objectId = PublicKey.random().toHex();
|
|
64
|
+
await create.databaseBackend!.getWriteStream()?.write({
|
|
65
|
+
object: {
|
|
66
|
+
objectId,
|
|
67
|
+
genesis: {
|
|
68
|
+
modelType: DocumentModel.meta.type
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await asyncTimeout(
|
|
74
|
+
check._itemManager.update.waitForCondition(() => check._itemManager.entities.has(objectId)),
|
|
75
|
+
500
|
|
76
|
+
);
|
|
77
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import expect from 'expect';
|
|
6
|
+
|
|
7
|
+
import { DocumentModel, MutationBuilder } from '@dxos/document-model';
|
|
8
|
+
import { createModelMutation, encodeModelMutation, genesisMutation } from '@dxos/echo-db';
|
|
9
|
+
import { PublicKey } from '@dxos/keys';
|
|
10
|
+
import { ModelFactory } from '@dxos/model-factory';
|
|
11
|
+
import { test } from '@dxos/test';
|
|
12
|
+
|
|
13
|
+
import { createMemoryDatabase, createRemoteDatabaseFromDataServiceHost } from '../testing';
|
|
14
|
+
|
|
15
|
+
const createDatabase = async () => {
|
|
16
|
+
// prettier-ignore
|
|
17
|
+
const modelFactory = new ModelFactory()
|
|
18
|
+
.registerModel(DocumentModel);
|
|
19
|
+
|
|
20
|
+
// TODO(dmaretskyi): Fix.
|
|
21
|
+
const host = await createMemoryDatabase(modelFactory);
|
|
22
|
+
const proxy = await createRemoteDatabaseFromDataServiceHost(modelFactory, host.backend.createDataServiceHost());
|
|
23
|
+
return proxy;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('database', () => {
|
|
27
|
+
describe('proxy-service mode', () => {
|
|
28
|
+
test('create object', async () => {
|
|
29
|
+
const database = await createDatabase();
|
|
30
|
+
|
|
31
|
+
const result = database.backend.mutate(genesisMutation(PublicKey.random().toHex(), DocumentModel.meta.type));
|
|
32
|
+
expect(result.objectsCreated.length).toEqual(1);
|
|
33
|
+
expect(database.itemManager.entities.has(result.objectsCreated[0].id));
|
|
34
|
+
|
|
35
|
+
await (await result.getReceipt()).waitToBeProcessed();
|
|
36
|
+
expect(database.itemManager.entities.has(result.objectsCreated[0].id));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('mutate document', async () => {
|
|
40
|
+
const database = await createDatabase();
|
|
41
|
+
|
|
42
|
+
const id = PublicKey.random().toHex();
|
|
43
|
+
database.backend.mutate(genesisMutation(id, DocumentModel.meta.type));
|
|
44
|
+
const result = database.backend.mutate(
|
|
45
|
+
createModelMutation(id, encodeModelMutation(DocumentModel.meta, new MutationBuilder().set('test', 42).build()))
|
|
46
|
+
);
|
|
47
|
+
expect(database.itemManager.getItem(id)!.state.data.test).toEqual(42);
|
|
48
|
+
|
|
49
|
+
await (await result.getReceipt()).waitToBeProcessed();
|
|
50
|
+
expect(database.itemManager.getItem(id)!.state.data.test).toEqual(42);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|