@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.
Files changed (134) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +22 -0
  3. package/dist/lib/browser/chunk-3GAE5CKD.mjs +1621 -0
  4. package/dist/lib/browser/chunk-3GAE5CKD.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +114 -0
  6. package/dist/lib/browser/index.mjs.map +7 -0
  7. package/dist/lib/browser/meta.json +1 -0
  8. package/dist/lib/browser/testing/index.mjs +238 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/node/index.cjs +1747 -0
  11. package/dist/lib/node/index.cjs.map +7 -0
  12. package/dist/lib/node/meta.json +1 -0
  13. package/dist/lib/node/testing/index.cjs +1835 -0
  14. package/dist/lib/node/testing/index.cjs.map +7 -0
  15. package/dist/types/src/common/codec.d.ts +12 -0
  16. package/dist/types/src/common/codec.d.ts.map +1 -0
  17. package/dist/types/src/common/feeds.d.ts +7 -0
  18. package/dist/types/src/common/feeds.d.ts.map +1 -0
  19. package/dist/types/src/common/index.d.ts +3 -0
  20. package/dist/types/src/common/index.d.ts.map +1 -0
  21. package/dist/types/src/dbhost/data-service-host.d.ts +22 -0
  22. package/dist/types/src/dbhost/data-service-host.d.ts.map +1 -0
  23. package/dist/types/src/dbhost/data-service.d.ts +21 -0
  24. package/dist/types/src/dbhost/data-service.d.ts.map +1 -0
  25. package/dist/types/src/dbhost/database-backend.d.ts +28 -0
  26. package/dist/types/src/dbhost/database-backend.d.ts.map +1 -0
  27. package/dist/types/src/dbhost/index.d.ts +6 -0
  28. package/dist/types/src/dbhost/index.d.ts.map +1 -0
  29. package/dist/types/src/dbhost/snapshot-manager.d.ts +17 -0
  30. package/dist/types/src/dbhost/snapshot-manager.d.ts.map +1 -0
  31. package/dist/types/src/dbhost/snapshot-store.d.ts +9 -0
  32. package/dist/types/src/dbhost/snapshot-store.d.ts.map +1 -0
  33. package/dist/types/src/dbhost/snapshot-store.test.d.ts +2 -0
  34. package/dist/types/src/dbhost/snapshot-store.test.d.ts.map +1 -0
  35. package/dist/types/src/errors/errors.d.ts +26 -0
  36. package/dist/types/src/errors/errors.d.ts.map +1 -0
  37. package/dist/types/src/errors/index.d.ts +2 -0
  38. package/dist/types/src/errors/index.d.ts.map +1 -0
  39. package/dist/types/src/index.d.ts +7 -0
  40. package/dist/types/src/index.d.ts.map +1 -0
  41. package/dist/types/src/metadata/index.d.ts +2 -0
  42. package/dist/types/src/metadata/index.d.ts.map +1 -0
  43. package/dist/types/src/metadata/metadata-store.d.ts +42 -0
  44. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -0
  45. package/dist/types/src/pipeline/index.d.ts +3 -0
  46. package/dist/types/src/pipeline/index.d.ts.map +1 -0
  47. package/dist/types/src/pipeline/message-selector.d.ts +11 -0
  48. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -0
  49. package/dist/types/src/pipeline/pipeline.d.ts +98 -0
  50. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -0
  51. package/dist/types/src/pipeline/pipeline.test.d.ts +2 -0
  52. package/dist/types/src/pipeline/pipeline.test.d.ts.map +1 -0
  53. package/dist/types/src/pipeline/timeframe-clock.d.ts +19 -0
  54. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -0
  55. package/dist/types/src/space/auth.d.ts +23 -0
  56. package/dist/types/src/space/auth.d.ts.map +1 -0
  57. package/dist/types/src/space/control-pipeline.d.ts +31 -0
  58. package/dist/types/src/space/control-pipeline.d.ts.map +1 -0
  59. package/dist/types/src/space/control-pipeline.test.d.ts +2 -0
  60. package/dist/types/src/space/control-pipeline.test.d.ts.map +1 -0
  61. package/dist/types/src/space/data-pipeline-controller.d.ts +63 -0
  62. package/dist/types/src/space/data-pipeline-controller.d.ts.map +1 -0
  63. package/dist/types/src/space/genesis.d.ts +5 -0
  64. package/dist/types/src/space/genesis.d.ts.map +1 -0
  65. package/dist/types/src/space/index.d.ts +7 -0
  66. package/dist/types/src/space/index.d.ts.map +1 -0
  67. package/dist/types/src/space/replication.browser.test.d.ts +2 -0
  68. package/dist/types/src/space/replication.browser.test.d.ts.map +1 -0
  69. package/dist/types/src/space/space-manager.browser.test.d.ts +2 -0
  70. package/dist/types/src/space/space-manager.browser.test.d.ts.map +1 -0
  71. package/dist/types/src/space/space-manager.d.ts +41 -0
  72. package/dist/types/src/space/space-manager.d.ts.map +1 -0
  73. package/dist/types/src/space/space-protocol.browser.test.d.ts +2 -0
  74. package/dist/types/src/space/space-protocol.browser.test.d.ts.map +1 -0
  75. package/dist/types/src/space/space-protocol.d.ts +76 -0
  76. package/dist/types/src/space/space-protocol.d.ts.map +1 -0
  77. package/dist/types/src/space/space-protocol.test.d.ts +2 -0
  78. package/dist/types/src/space/space-protocol.test.d.ts.map +1 -0
  79. package/dist/types/src/space/space.d.ts +59 -0
  80. package/dist/types/src/space/space.d.ts.map +1 -0
  81. package/dist/types/src/space/space.test.d.ts +2 -0
  82. package/dist/types/src/space/space.test.d.ts.map +1 -0
  83. package/dist/types/src/testing/index.d.ts +4 -0
  84. package/dist/types/src/testing/index.d.ts.map +1 -0
  85. package/dist/types/src/testing/test-agent-builder.d.ts +52 -0
  86. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -0
  87. package/dist/types/src/testing/test-feed-builder.d.ts +9 -0
  88. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -0
  89. package/dist/types/src/testing/util.d.ts +14 -0
  90. package/dist/types/src/testing/util.d.ts.map +1 -0
  91. package/dist/types/src/tests/database.test.d.ts +2 -0
  92. package/dist/types/src/tests/database.test.d.ts.map +1 -0
  93. package/package.json +60 -0
  94. package/src/common/codec.ts +18 -0
  95. package/src/common/feeds.ts +23 -0
  96. package/src/common/index.ts +6 -0
  97. package/src/dbhost/data-service-host.ts +103 -0
  98. package/src/dbhost/data-service.ts +66 -0
  99. package/src/dbhost/database-backend.ts +64 -0
  100. package/src/dbhost/index.ts +9 -0
  101. package/src/dbhost/snapshot-manager.ts +68 -0
  102. package/src/dbhost/snapshot-store.test.ts +30 -0
  103. package/src/dbhost/snapshot-store.ts +45 -0
  104. package/src/errors/errors.ts +52 -0
  105. package/src/errors/index.ts +5 -0
  106. package/src/index.ts +10 -0
  107. package/src/metadata/index.ts +5 -0
  108. package/src/metadata/metadata-store.ts +173 -0
  109. package/src/pipeline/index.ts +6 -0
  110. package/src/pipeline/message-selector.ts +38 -0
  111. package/src/pipeline/pipeline.test.ts +71 -0
  112. package/src/pipeline/pipeline.ts +249 -0
  113. package/src/pipeline/timeframe-clock.ts +56 -0
  114. package/src/space/auth.ts +88 -0
  115. package/src/space/control-pipeline.test.ts +162 -0
  116. package/src/space/control-pipeline.ts +103 -0
  117. package/src/space/data-pipeline-controller.ts +234 -0
  118. package/src/space/genesis.ts +29 -0
  119. package/src/space/index.ts +10 -0
  120. package/src/space/replication.browser.test.ts +64 -0
  121. package/src/space/space-manager.browser.test.ts +44 -0
  122. package/src/space/space-manager.ts +92 -0
  123. package/src/space/space-protocol.browser.test.ts +135 -0
  124. package/src/space/space-protocol.test.ts +136 -0
  125. package/src/space/space-protocol.ts +222 -0
  126. package/src/space/space.test.ts +144 -0
  127. package/src/space/space.ts +199 -0
  128. package/src/testing/index.ts +7 -0
  129. package/src/testing/test-agent-builder.ts +193 -0
  130. package/src/testing/test-feed-builder.ts +19 -0
  131. package/src/testing/util.ts +77 -0
  132. package/src/tests/database.test.ts +53 -0
  133. package/testing.d.ts +11 -0
  134. 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,7 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './test-agent-builder';
6
+ export * from './test-feed-builder';
7
+ export * from './util';
@@ -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
+ });