@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,92 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { synchronized, trackLeaks } from '@dxos/async';
6
+ import { CredentialSigner } from '@dxos/credentials';
7
+ import { failUndefined } from '@dxos/debug';
8
+ import { FeedStore } from '@dxos/feed-store';
9
+ import { PublicKey } from '@dxos/keys';
10
+ import { log } from '@dxos/log';
11
+ import { NetworkManager } from '@dxos/network-manager';
12
+ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
13
+ import { SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
14
+ import { Credential, ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
15
+ import { Teleport } from '@dxos/teleport';
16
+ import { ComplexMap } from '@dxos/util';
17
+
18
+ import { Space } from './space';
19
+ import { SpaceProtocol, SwarmIdentity } from './space-protocol';
20
+
21
+ // TODO(burdon): Factor out to DataSpaceManager
22
+ export interface SigningContext {
23
+ identityKey: PublicKey;
24
+ deviceKey: PublicKey;
25
+ credentialSigner: CredentialSigner; // TODO(burdon): Already has keyring.
26
+ recordCredential: (credential: Credential) => Promise<void>;
27
+ profile?: ProfileDocument;
28
+ }
29
+
30
+ export type SpaceManagerParams = {
31
+ feedStore: FeedStore<FeedMessage>;
32
+ networkManager: NetworkManager;
33
+ };
34
+
35
+ export type ConstructSpaceParams = {
36
+ metadata: SpaceMetadata;
37
+ swarmIdentity: SwarmIdentity;
38
+ onNetworkConnection: (session: Teleport) => void;
39
+ };
40
+
41
+ /**
42
+ * Manages a collection of ECHO (Data) Spaces.
43
+ */
44
+ @trackLeaks('open', 'close')
45
+ export class SpaceManager {
46
+ private readonly _spaces = new ComplexMap<PublicKey, Space>(PublicKey.hash);
47
+ private readonly _feedStore: FeedStore<FeedMessage>;
48
+ private readonly _networkManager: NetworkManager;
49
+
50
+ constructor({ feedStore, networkManager }: SpaceManagerParams) {
51
+ // TODO(burdon): Assert.
52
+ this._feedStore = feedStore;
53
+ this._networkManager = networkManager;
54
+ }
55
+
56
+ // TODO(burdon): Remove.
57
+ get spaces() {
58
+ return this._spaces;
59
+ }
60
+
61
+ @synchronized
62
+ async open() {}
63
+
64
+ @synchronized
65
+ async close() {
66
+ await Promise.all([...this._spaces.values()].map((space) => space.close()));
67
+ }
68
+
69
+ async constructSpace({ metadata, swarmIdentity, onNetworkConnection }: ConstructSpaceParams) {
70
+ log('constructing space...', { spaceKey: metadata.genesisFeedKey });
71
+
72
+ // The genesis feed will be the same as the control feed if the space was created by the local agent.
73
+ const genesisFeed = await this._feedStore.openFeed(metadata.genesisFeedKey ?? failUndefined());
74
+
75
+ const spaceKey = metadata.key;
76
+ const protocol = new SpaceProtocol({
77
+ topic: spaceKey,
78
+ swarmIdentity,
79
+ networkManager: this._networkManager,
80
+ onSessionAuth: onNetworkConnection
81
+ });
82
+
83
+ const space = new Space({
84
+ spaceKey,
85
+ protocol,
86
+ genesisFeed,
87
+ feedProvider: (feedKey) => this._feedStore.openFeed(feedKey)
88
+ });
89
+ this._spaces.set(space.key, space);
90
+ return space;
91
+ }
92
+ }
@@ -0,0 +1,135 @@
1
+ //
2
+ // Copyright 2022 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 { Keyring } from '@dxos/keyring';
11
+ import { PublicKey } from '@dxos/keys';
12
+ import { createStorage } from '@dxos/random-access-storage';
13
+ import { describe, test, afterTest } from '@dxos/test';
14
+ import { Timeframe } from '@dxos/timeframe';
15
+
16
+ import { TestFeedBuilder, TestAgentBuilder, WebsocketNetworkManagerProvider } from '../testing';
17
+
18
+ // TODO(burdon): Config.
19
+ // Signal server will be started by the setup script.
20
+ const SIGNAL_URL = 'ws://localhost:4000/.well-known/dx/signal';
21
+
22
+ describe('space/space-protocol', () => {
23
+ test('two peers discover each other', async () => {
24
+ const builder = new TestAgentBuilder();
25
+ afterTest(async () => await builder.close());
26
+ const topic = PublicKey.random();
27
+
28
+ const peer1 = await builder.createPeer();
29
+ const presence1 = peer1.createPresence();
30
+ const protocol1 = peer1.createSpaceProtocol(topic, presence1);
31
+
32
+ const peer2 = await builder.createPeer();
33
+ const presence2 = peer2.createPresence();
34
+ const protocol2 = peer2.createSpaceProtocol(topic, presence2);
35
+
36
+ await protocol1.start();
37
+ await protocol2.start();
38
+
39
+ afterTest(() => protocol1.stop());
40
+ afterTest(() => protocol2.stop());
41
+
42
+ await waitForExpect(() => {
43
+ expect(presence1.getPeersOnline().map(({ peerId }) => peerId)).toContainEqual(peer2.deviceKey);
44
+ expect(presence2.getPeersOnline().map(({ peerId }) => peerId)).toContainEqual(peer1.deviceKey);
45
+ });
46
+ });
47
+
48
+ test('replicates a feed', async () => {
49
+ const builder = new TestAgentBuilder();
50
+ afterTest(async () => await builder.close());
51
+ const topic = PublicKey.random();
52
+
53
+ const peer1 = await builder.createPeer();
54
+ const protocol1 = peer1.createSpaceProtocol(topic);
55
+
56
+ const peer2 = await builder.createPeer();
57
+ const protocol2 = peer2.createSpaceProtocol(topic);
58
+
59
+ await protocol1.start();
60
+ await protocol2.start();
61
+
62
+ afterTest(() => protocol1.stop());
63
+ afterTest(() => protocol2.stop());
64
+
65
+ const builder1 = new TestFeedBuilder();
66
+ const feedStore1 = builder1.createFeedStore();
67
+
68
+ const builder2 = new TestFeedBuilder();
69
+ const feedStore2 = builder2.createFeedStore();
70
+
71
+ const feedKey = await builder1.keyring.createKey();
72
+ const feed1 = await feedStore1.openFeed(feedKey, { writable: true });
73
+ const feed2 = await feedStore2.openFeed(feedKey);
74
+
75
+ await protocol1.addFeed(feed1);
76
+ await protocol2.addFeed(feed2);
77
+
78
+ await feed1.append({ timeframe: new Timeframe() });
79
+ await waitForExpect(() => {
80
+ expect(feed2.properties.length).toEqual(1);
81
+ });
82
+
83
+ await feed1.append({ timeframe: new Timeframe() });
84
+ await waitForExpect(() => {
85
+ expect(feed2.properties.length).toEqual(2);
86
+ });
87
+
88
+ await builder.close();
89
+ });
90
+
91
+ test('replicates a feed through a webrtc connection', async () => {
92
+ const builder = new TestAgentBuilder({
93
+ storage: createStorage(),
94
+ networkManagerProvider: WebsocketNetworkManagerProvider(SIGNAL_URL)
95
+ });
96
+ afterTest(async () => await builder.close());
97
+
98
+ const keyring = new Keyring();
99
+ const topic = await keyring.createKey();
100
+
101
+ const peer1 = await builder.createPeer();
102
+ const protocol1 = peer1.createSpaceProtocol(topic);
103
+
104
+ const peer2 = await builder.createPeer();
105
+ const protocol2 = peer2.createSpaceProtocol(topic);
106
+
107
+ await protocol1.start();
108
+ await protocol2.start();
109
+
110
+ afterTest(() => protocol1.stop());
111
+ afterTest(() => protocol2.stop());
112
+
113
+ const feedKey = await peer1.keyring.createKey();
114
+
115
+ const feed1 = await peer1.feedStore.openFeed(feedKey, { writable: true });
116
+ const feed2 = await peer2.feedStore.openFeed(feedKey);
117
+
118
+ await protocol1.addFeed(feed1);
119
+ await protocol2.addFeed(feed2);
120
+
121
+ await feed1.append({ timeframe: new Timeframe() });
122
+ await waitForExpect(() => {
123
+ expect(feed2.properties.length).toEqual(1);
124
+ });
125
+
126
+ await feed1.append({ timeframe: new Timeframe() });
127
+ await waitForExpect(() => {
128
+ expect(feed2.properties.length).toEqual(2);
129
+ });
130
+
131
+ await builder.close();
132
+ })
133
+ .skipEnvironments('webkit')
134
+ .tag('e2e'); // Some storage drivers may break when there are multiple storage instances.
135
+ });
@@ -0,0 +1,136 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import expect from 'expect';
6
+ import waitForExpect from 'wait-for-expect';
7
+
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { MemorySignalManager, MemorySignalManagerContext } from '@dxos/messaging';
10
+ import { MemoryTransportFactory, NetworkManager } from '@dxos/network-manager';
11
+ import { describe, test, afterTest } from '@dxos/test';
12
+ import { Timeframe } from '@dxos/timeframe';
13
+
14
+ import { TestAgentBuilder, TestFeedBuilder } from '../testing';
15
+ import { AuthStatus, MOCK_AUTH_PROVIDER, MOCK_AUTH_VERIFIER, SpaceProtocol } from './space-protocol';
16
+
17
+ describe('space/space-protocol', () => {
18
+ test('two peers discover each other via presence', async () => {
19
+ const builder = new TestAgentBuilder();
20
+ afterTest(async () => await builder.close());
21
+ const topic = PublicKey.random();
22
+
23
+ const peer1 = await builder.createPeer();
24
+ const presence1 = peer1.createPresence();
25
+ const protocol1 = peer1.createSpaceProtocol(topic, presence1);
26
+
27
+ const peer2 = await builder.createPeer();
28
+ const presence2 = peer2.createPresence();
29
+ const protocol2 = peer2.createSpaceProtocol(topic, presence2);
30
+
31
+ await protocol1.start();
32
+ afterTest(() => protocol1.stop());
33
+
34
+ await protocol2.start();
35
+ afterTest(() => protocol2.stop());
36
+
37
+ await waitForExpect(() => {
38
+ expect(presence1.getPeersOnline().map(({ peerId }) => peerId)).toContainEqual(peer2.deviceKey);
39
+ expect(presence2.getPeersOnline().map(({ peerId }) => peerId)).toContainEqual(peer1.deviceKey);
40
+ }, 1_000);
41
+ });
42
+
43
+ test('failing authentication', async () => {
44
+ const [topic, peerId1, peerId2] = PublicKey.randomSequence();
45
+ const signalContext = new MemorySignalManagerContext();
46
+
47
+ const protocol1 = new SpaceProtocol({
48
+ topic,
49
+ swarmIdentity: {
50
+ peerKey: peerId1,
51
+ credentialProvider: MOCK_AUTH_PROVIDER,
52
+ credentialAuthenticator: async () => false // Reject everyone.
53
+ },
54
+ networkManager: new NetworkManager({
55
+ signalManager: new MemorySignalManager(signalContext),
56
+ transportFactory: MemoryTransportFactory
57
+ })
58
+ });
59
+
60
+ const protocol2 = new SpaceProtocol({
61
+ topic,
62
+ swarmIdentity: {
63
+ peerKey: peerId2,
64
+ credentialProvider: MOCK_AUTH_PROVIDER,
65
+ credentialAuthenticator: MOCK_AUTH_VERIFIER
66
+ },
67
+ networkManager: new NetworkManager({
68
+ signalManager: new MemorySignalManager(signalContext),
69
+ transportFactory: MemoryTransportFactory
70
+ })
71
+ });
72
+
73
+ await protocol1.start();
74
+ afterTest(() => protocol1.stop());
75
+
76
+ await protocol2.start();
77
+ afterTest(() => protocol2.stop());
78
+
79
+ await waitForExpect(() => {
80
+ expect(protocol1.sessions.get(peerId2)?.authStatus).toEqual(AuthStatus.FAILURE);
81
+ });
82
+ });
83
+
84
+ test('replicates a feed', async () => {
85
+ const builder = new TestAgentBuilder();
86
+ afterTest(async () => await builder.close());
87
+
88
+ const topic = PublicKey.random();
89
+
90
+ const peer1 = await builder.createPeer();
91
+ const protocol1 = peer1.createSpaceProtocol(topic);
92
+
93
+ const peer2 = await builder.createPeer();
94
+ const protocol2 = peer2.createSpaceProtocol(topic);
95
+
96
+ await protocol1.start();
97
+ await protocol2.start();
98
+
99
+ afterTest(() => protocol1.stop());
100
+ afterTest(() => protocol2.stop());
101
+
102
+ //
103
+ // Create feeds.
104
+ //
105
+
106
+ const builder1 = new TestFeedBuilder();
107
+ const feedStore1 = builder1.createFeedStore();
108
+
109
+ const builder2 = new TestFeedBuilder();
110
+ const feedStore2 = builder2.createFeedStore();
111
+
112
+ const feed1 = await feedStore1.openFeed(await builder1.keyring.createKey(), { writable: true });
113
+ const feed2 = await feedStore2.openFeed(feed1.key);
114
+
115
+ await protocol1.addFeed(feed1);
116
+ await protocol2.addFeed(feed2);
117
+
118
+ //
119
+ // Append message.
120
+ //
121
+
122
+ // TODO(burdon): Append batch of messages.
123
+ await feed1.append({ timeframe: new Timeframe() });
124
+ await waitForExpect(() => {
125
+ // Received message appended before replication.
126
+ expect(feed2.properties.length).toEqual(1);
127
+ });
128
+
129
+ // TODO(burdon): Append batch of messages.
130
+ await feed1.append({ timeframe: new Timeframe() });
131
+ await waitForExpect(() => {
132
+ // Received message appended after replication.
133
+ expect(feed2.properties.length).toEqual(2);
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,222 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { discoveryKey, sha256 } from '@dxos/crypto';
6
+ import { FeedWrapper } from '@dxos/feed-store';
7
+ import { PublicKey } from '@dxos/keys';
8
+ import { log, logInfo } from '@dxos/log';
9
+ import {
10
+ MMSTTopology,
11
+ NetworkManager,
12
+ SwarmConnection,
13
+ WireProtocol,
14
+ WireProtocolParams,
15
+ WireProtocolProvider
16
+ } from '@dxos/network-manager';
17
+ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
18
+ import { Teleport } from '@dxos/teleport';
19
+ import { ReplicatorExtension } from '@dxos/teleport-extension-replicator';
20
+ import { ComplexMap } from '@dxos/util';
21
+
22
+ import { AuthExtension, AuthProvider, AuthVerifier } from './auth';
23
+
24
+ export const MOCK_AUTH_PROVIDER: AuthProvider = async (nonce: Uint8Array) => Buffer.from('mock');
25
+ export const MOCK_AUTH_VERIFIER: AuthVerifier = async (nonce: Uint8Array, credential: Uint8Array) => true;
26
+
27
+ // TODO(burdon): Reconcile with SigningContext (define types together).
28
+ export interface SwarmIdentity {
29
+ peerKey: PublicKey;
30
+ credentialProvider: AuthProvider;
31
+ credentialAuthenticator: AuthVerifier;
32
+ }
33
+
34
+ export type SpaceProtocolOptions = {
35
+ topic: PublicKey; // TODO(burdon): Rename?
36
+ swarmIdentity: SwarmIdentity;
37
+ networkManager: NetworkManager;
38
+ /**
39
+ * Called when new session is authenticated.
40
+ * Additional extensions can be added here.
41
+ */
42
+ onSessionAuth?: (session: Teleport) => void;
43
+ };
44
+
45
+ /**
46
+ * Manages Teleport protocol stream creation and joining swarms with replication and presence extensions.
47
+ */
48
+ export class SpaceProtocol {
49
+ private readonly _networkManager: NetworkManager;
50
+ private readonly _swarmIdentity: SwarmIdentity;
51
+ private readonly _onSessionAuth?: (session: Teleport) => void;
52
+
53
+ @logInfo
54
+ private readonly _topic: PublicKey;
55
+
56
+ private readonly _feeds = new Set<FeedWrapper<FeedMessage>>();
57
+ private readonly _sessions = new ComplexMap<PublicKey, SpaceProtocolSession>(PublicKey.hash);
58
+
59
+ private _connection?: SwarmConnection;
60
+
61
+ get sessions(): ReadonlyMap<PublicKey, SpaceProtocolSession> {
62
+ return this._sessions;
63
+ }
64
+
65
+ get feeds(): ReadonlySet<FeedWrapper<FeedMessage>> {
66
+ return this._feeds;
67
+ }
68
+
69
+ @logInfo
70
+ private get _ownPeerKey() {
71
+ return this._swarmIdentity.peerKey;
72
+ }
73
+
74
+ constructor({ topic, swarmIdentity, networkManager, onSessionAuth }: SpaceProtocolOptions) {
75
+ this._networkManager = networkManager;
76
+ this._swarmIdentity = swarmIdentity;
77
+ this._onSessionAuth = onSessionAuth;
78
+
79
+ this._topic = PublicKey.from(discoveryKey(sha256(topic.toHex())));
80
+ }
81
+
82
+ // TODO(burdon): Create abstraction for Space (e.g., add keys and have provider).
83
+ addFeed(feed: FeedWrapper<FeedMessage>) {
84
+ log('addFeed', { key: feed.key });
85
+
86
+ this._feeds.add(feed);
87
+ for (const session of this._sessions.values()) {
88
+ session.replicator.addFeed(feed);
89
+ }
90
+ }
91
+
92
+ async start() {
93
+ if (this._connection) {
94
+ return;
95
+ }
96
+
97
+ // TODO(burdon): Document why empty buffer.
98
+ const credentials = await this._swarmIdentity.credentialProvider(Buffer.from(''));
99
+
100
+ // TODO(burdon): Move to config (with sensible defaults).
101
+ const topologyConfig = {
102
+ originateConnections: 4,
103
+ maxPeers: 10,
104
+ sampleSize: 20
105
+ };
106
+
107
+ log('starting...');
108
+ this._connection = await this._networkManager.joinSwarm({
109
+ protocolProvider: this._createProtocolProvider(credentials),
110
+ peerId: this._swarmIdentity.peerKey,
111
+ topic: this._topic,
112
+ topology: new MMSTTopology(topologyConfig),
113
+ label: `Protocol swarm: ${this._topic}`
114
+ });
115
+
116
+ log('started');
117
+ }
118
+
119
+ async stop() {
120
+ if (this._connection) {
121
+ log('stopping...');
122
+ await this._connection.close();
123
+ log('stopped');
124
+ }
125
+ }
126
+
127
+ private _createProtocolProvider(credentials: Uint8Array | undefined): WireProtocolProvider {
128
+ return (wireParams) => {
129
+ const session = new SpaceProtocolSession({
130
+ wireParams,
131
+ swarmIdentity: this._swarmIdentity,
132
+ onSessionAuth: this._onSessionAuth
133
+ });
134
+ this._sessions.set(wireParams.remotePeerId, session);
135
+
136
+ for (const feed of this._feeds) {
137
+ session.replicator.addFeed(feed);
138
+ }
139
+
140
+ return session;
141
+ };
142
+ }
143
+ }
144
+
145
+ export type SpaceProtocolSessionParams = {
146
+ wireParams: WireProtocolParams;
147
+ swarmIdentity: SwarmIdentity;
148
+ /**
149
+ * Called when new session is authenticated.
150
+ * Additional extensions can be added here.
151
+ */
152
+ onSessionAuth?: (session: Teleport) => void;
153
+ };
154
+
155
+ export enum AuthStatus {
156
+ INITIAL = 'INITIAL',
157
+ SUCCESS = 'SUCCESS',
158
+ FAILURE = 'FAILURE'
159
+ }
160
+
161
+ // TODO(dmaretskyi): Move to a separate file.
162
+ /**
163
+ * Represents a single connection to a remote peer
164
+ */
165
+ export class SpaceProtocolSession implements WireProtocol {
166
+ @logInfo
167
+ private readonly _wireParams: WireProtocolParams;
168
+
169
+ private readonly _onSessionAuth?: (session: Teleport) => void;
170
+ private readonly _swarmIdentity: SwarmIdentity;
171
+
172
+ private readonly _teleport: Teleport;
173
+
174
+ // TODO(dmaretskyi): Start with upload=false when switching it on the fly works.
175
+ public readonly replicator = new ReplicatorExtension().setOptions({ upload: true });
176
+
177
+ private _authStatus = AuthStatus.INITIAL;
178
+
179
+ @logInfo
180
+ get authStatus() {
181
+ return this._authStatus;
182
+ }
183
+
184
+ // TODO(dmaretskyi): Allow to pass in extra extensions.
185
+ constructor({ wireParams, swarmIdentity, onSessionAuth }: SpaceProtocolSessionParams) {
186
+ this._wireParams = wireParams;
187
+ this._swarmIdentity = swarmIdentity;
188
+ this._onSessionAuth = onSessionAuth;
189
+
190
+ this._teleport = new Teleport(wireParams);
191
+ }
192
+
193
+ get stream() {
194
+ return this._teleport.stream;
195
+ }
196
+
197
+ async initialize(): Promise<void> {
198
+ await this._teleport.open();
199
+ this._teleport.addExtension(
200
+ 'dxos.mesh.teleport.auth',
201
+ new AuthExtension({
202
+ provider: this._swarmIdentity.credentialProvider,
203
+ verifier: this._swarmIdentity.credentialAuthenticator,
204
+ onAuthSuccess: () => {
205
+ this._authStatus = AuthStatus.SUCCESS;
206
+ log('Peer authenticated');
207
+ this._onSessionAuth?.(this._teleport);
208
+ // TODO(dmaretskyi): Configure replicator to upload.
209
+ },
210
+ onAuthFailure: () => {
211
+ log.warn('Auth failed');
212
+ this._authStatus = AuthStatus.FAILURE;
213
+ }
214
+ })
215
+ );
216
+ this._teleport.addExtension('dxos.mesh.teleport.replicator', this.replicator);
217
+ }
218
+
219
+ async destroy(): Promise<void> {
220
+ await this._teleport.close();
221
+ }
222
+ }