@dxos/echo-pipeline 0.5.2 → 0.5.3-main.056e7da
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/{chunk-VQQD32DM.mjs → chunk-GANAND63.mjs} +49 -30
- package/dist/lib/browser/chunk-GANAND63.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +431 -291
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +3 -1
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-P7L7ICAH.cjs → chunk-M475BGBI.cjs} +50 -32
- package/dist/lib/node/chunk-M475BGBI.cjs.map +7 -0
- package/dist/lib/node/index.cjs +441 -301
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +13 -11
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/automerge/automerge-host.d.ts +4 -11
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +6 -0
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +2 -0
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +2 -2
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts +23 -0
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -0
- package/dist/types/src/automerge/migrations.d.ts +2 -2
- package/dist/types/src/automerge/migrations.d.ts.map +1 -1
- package/dist/types/src/common/feeds.d.ts.map +1 -1
- package/dist/types/src/db-host/snapshot-manager.d.ts +2 -2
- package/dist/types/src/db-host/snapshot-manager.d.ts.map +1 -1
- package/dist/types/src/db-host/snapshot-store.d.ts +2 -2
- package/dist/types/src/db-host/snapshot-store.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +3 -2
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +2 -0
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +4 -3
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/automerge/automerge-host.test.ts +22 -9
- package/src/automerge/automerge-host.ts +68 -90
- package/src/automerge/automerge-repo.test.ts +10 -1
- package/src/automerge/echo-network-adapter.ts +19 -0
- package/src/automerge/echo-replicator.ts +3 -0
- package/src/automerge/index.ts +1 -1
- package/src/automerge/leveldb-storage-adapter.ts +2 -2
- package/src/automerge/mesh-echo-replicator.ts +231 -0
- package/src/automerge/migrations.ts +2 -2
- package/src/db-host/snapshot-manager.ts +1 -1
- package/src/db-host/snapshot-store.ts +1 -1
- package/src/metadata/metadata-store.ts +2 -2
- package/src/space/space-manager.ts +4 -1
- package/src/space/space-protocol.ts +11 -8
- package/src/space/space.ts +8 -3
- package/src/testing/test-agent-builder.ts +1 -0
- package/dist/lib/browser/chunk-VQQD32DM.mjs.map +0 -7
- package/dist/lib/node/chunk-P7L7ICAH.cjs.map +0 -7
- package/dist/types/src/automerge/mesh-network-adapter.d.ts +0 -18
- package/dist/types/src/automerge/mesh-network-adapter.d.ts.map +0 -1
- package/src/automerge/mesh-network-adapter.ts +0 -107
|
@@ -6,10 +6,15 @@ import { Trigger, synchronized } from '@dxos/async';
|
|
|
6
6
|
import { type Message, NetworkAdapter, type PeerId, type PeerMetadata } from '@dxos/automerge/automerge-repo';
|
|
7
7
|
import { LifecycleState } from '@dxos/context';
|
|
8
8
|
import { invariant } from '@dxos/invariant';
|
|
9
|
+
import { type PublicKey } from '@dxos/keys';
|
|
9
10
|
import { log } from '@dxos/log';
|
|
10
11
|
|
|
11
12
|
import { type EchoReplicator, type ReplicatorConnection, type ShouldAdvertizeParams } from './echo-replicator';
|
|
12
13
|
|
|
14
|
+
export type EchoNetworkAdapterParams = {
|
|
15
|
+
getContainingSpaceForDocument: (documentId: string) => Promise<PublicKey | null>;
|
|
16
|
+
};
|
|
17
|
+
|
|
13
18
|
/**
|
|
14
19
|
* Manages a set of {@link EchoReplicator} instances.
|
|
15
20
|
*/
|
|
@@ -22,6 +27,10 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
22
27
|
private _lifecycleState: LifecycleState = LifecycleState.CLOSED;
|
|
23
28
|
private readonly _connected = new Trigger();
|
|
24
29
|
|
|
30
|
+
constructor(private readonly _params: EchoNetworkAdapterParams) {
|
|
31
|
+
super();
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
override connect(peerId: PeerId, peerMetadata?: PeerMetadata | undefined): void {
|
|
26
35
|
this.peerId = peerId;
|
|
27
36
|
this.peerMetadata = peerMetadata;
|
|
@@ -51,6 +60,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
51
60
|
invariant(this._lifecycleState === LifecycleState.CLOSED);
|
|
52
61
|
this._lifecycleState = LifecycleState.OPEN;
|
|
53
62
|
|
|
63
|
+
log('emit ready');
|
|
54
64
|
this.emit('ready', {
|
|
55
65
|
network: this,
|
|
56
66
|
});
|
|
@@ -74,20 +84,26 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
74
84
|
|
|
75
85
|
@synchronized
|
|
76
86
|
async addReplicator(replicator: EchoReplicator) {
|
|
87
|
+
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
77
88
|
invariant(this.peerId);
|
|
78
89
|
invariant(!this._replicators.has(replicator));
|
|
79
90
|
|
|
91
|
+
this._replicators.add(replicator);
|
|
80
92
|
await replicator.connect({
|
|
81
93
|
peerId: this.peerId,
|
|
82
94
|
onConnectionOpen: this._onConnectionOpen.bind(this),
|
|
83
95
|
onConnectionClosed: this._onConnectionClosed.bind(this),
|
|
96
|
+
getContainingSpaceForDocument: this._params.getContainingSpaceForDocument,
|
|
84
97
|
});
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
@synchronized
|
|
88
101
|
async removeReplicator(replicator: EchoReplicator) {
|
|
102
|
+
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
89
103
|
invariant(this._replicators.has(replicator));
|
|
104
|
+
('');
|
|
90
105
|
await replicator.disconnect();
|
|
106
|
+
this._replicators.delete(replicator);
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
async shouldAdvertize(peerId: PeerId, params: ShouldAdvertizeParams): Promise<boolean> {
|
|
@@ -100,6 +116,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
100
116
|
}
|
|
101
117
|
|
|
102
118
|
private _onConnectionOpen(connection: ReplicatorConnection) {
|
|
119
|
+
log('Connection opened', { peerId: connection.peerId });
|
|
103
120
|
invariant(!this._connections.has(connection.peerId as PeerId));
|
|
104
121
|
const reader = connection.readable.getReader();
|
|
105
122
|
const writer = connection.writable.getWriter();
|
|
@@ -124,6 +141,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
124
141
|
}
|
|
125
142
|
});
|
|
126
143
|
|
|
144
|
+
log('emit peer-candidate', { peerId: connection.peerId });
|
|
127
145
|
this.emit('peer-candidate', {
|
|
128
146
|
peerId: connection.peerId as PeerId,
|
|
129
147
|
peerMetadata: {
|
|
@@ -134,6 +152,7 @@ export class EchoNetworkAdapter extends NetworkAdapter {
|
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
private _onConnectionClosed(connection: ReplicatorConnection) {
|
|
155
|
+
log('Connection closed', { peerId: connection.peerId });
|
|
137
156
|
const entry = this._connections.get(connection.peerId as PeerId);
|
|
138
157
|
invariant(entry);
|
|
139
158
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Message } from '@dxos/automerge/automerge-repo';
|
|
6
|
+
import { type PublicKey } from '@dxos/keys';
|
|
6
7
|
|
|
7
8
|
export interface EchoReplicator {
|
|
8
9
|
/**
|
|
@@ -25,6 +26,8 @@ export interface EchoReplicatorContext {
|
|
|
25
26
|
onConnectionOpen(connection: ReplicatorConnection): void;
|
|
26
27
|
|
|
27
28
|
onConnectionClosed(connection: ReplicatorConnection): void;
|
|
29
|
+
|
|
30
|
+
getContainingSpaceForDocument(documentId: string): Promise<PublicKey | null>;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export interface ReplicatorConnection {
|
package/src/automerge/index.ts
CHANGED
|
@@ -7,5 +7,5 @@ export * from './automerge-storage-adapter';
|
|
|
7
7
|
export * from './automerge-doc-loader';
|
|
8
8
|
export * from './leveldb-storage-adapter';
|
|
9
9
|
export * from './local-host-network-adapter';
|
|
10
|
-
export * from './mesh-
|
|
10
|
+
export * from './mesh-echo-replicator';
|
|
11
11
|
export * from './echo-replicator';
|
|
@@ -6,11 +6,11 @@ import { type MixedEncoding } from 'level-transcoder';
|
|
|
6
6
|
|
|
7
7
|
import { type StorageAdapterInterface, type Chunk, type StorageKey } from '@dxos/automerge/automerge-repo';
|
|
8
8
|
import { LifecycleState, Resource } from '@dxos/context';
|
|
9
|
-
import { type BatchLevel, type
|
|
9
|
+
import { type BatchLevel, type SublevelDB } from '@dxos/kv-store';
|
|
10
10
|
import { type MaybePromise } from '@dxos/util';
|
|
11
11
|
|
|
12
12
|
export type LevelDBStorageAdapterParams = {
|
|
13
|
-
db:
|
|
13
|
+
db: SublevelDB;
|
|
14
14
|
callbacks?: StorageCallbacks;
|
|
15
15
|
};
|
|
16
16
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Message, cbor } from '@dxos/automerge/automerge-repo';
|
|
6
|
+
import { Resource } from '@dxos/context';
|
|
7
|
+
import { invariant } from '@dxos/invariant';
|
|
8
|
+
import { PublicKey } from '@dxos/keys';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
11
|
+
import { ComplexMap, ComplexSet, defaultMap } from '@dxos/util';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type EchoReplicator,
|
|
15
|
+
type EchoReplicatorContext,
|
|
16
|
+
type ReplicatorConnection,
|
|
17
|
+
type ShouldAdvertizeParams,
|
|
18
|
+
} from './echo-replicator';
|
|
19
|
+
|
|
20
|
+
// TODO(dmaretskyi): Move out of @dxos/echo-pipeline.
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Used to replicate with other peers over the network.
|
|
24
|
+
*/
|
|
25
|
+
export class MeshEchoReplicator implements EchoReplicator {
|
|
26
|
+
private readonly _connections = new Set<MeshReplicatorConnection>();
|
|
27
|
+
/**
|
|
28
|
+
* Using automerge peerId as a key.
|
|
29
|
+
*/
|
|
30
|
+
private readonly _connectionsPerPeer = new Map<string, MeshReplicatorConnection>();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* spaceKey -> deviceKey[]
|
|
34
|
+
*/
|
|
35
|
+
private readonly _authorizedDevices = new ComplexMap<PublicKey, ComplexSet<PublicKey>>(PublicKey.hash);
|
|
36
|
+
|
|
37
|
+
private _context: EchoReplicatorContext | null = null;
|
|
38
|
+
|
|
39
|
+
async connect(context: EchoReplicatorContext): Promise<void> {
|
|
40
|
+
this._context = context;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async disconnect() {
|
|
44
|
+
for (const connection of this._connections) {
|
|
45
|
+
await connection.close();
|
|
46
|
+
}
|
|
47
|
+
this._connections.clear();
|
|
48
|
+
this._connectionsPerPeer.clear();
|
|
49
|
+
|
|
50
|
+
this._context = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
createExtension(): AutomergeReplicator {
|
|
54
|
+
invariant(this._context);
|
|
55
|
+
|
|
56
|
+
const connection: MeshReplicatorConnection = new MeshReplicatorConnection({
|
|
57
|
+
ownPeerId: this._context.peerId,
|
|
58
|
+
onRemoteConnected: async () => {
|
|
59
|
+
log('onRemoteConnected', { peerId: connection.peerId });
|
|
60
|
+
invariant(this._context);
|
|
61
|
+
|
|
62
|
+
if (!this._connectionsPerPeer.has(connection.peerId)) {
|
|
63
|
+
this._connectionsPerPeer.set(connection.peerId, connection);
|
|
64
|
+
await connection.enable();
|
|
65
|
+
this._context.onConnectionOpen(connection);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onRemoteDisconnected: async () => {
|
|
69
|
+
log('onRemoteDisconnected', { peerId: connection.peerId });
|
|
70
|
+
this._context?.onConnectionClosed(connection);
|
|
71
|
+
await connection.disable();
|
|
72
|
+
this._connectionsPerPeer.delete(connection.peerId);
|
|
73
|
+
this._connections.delete(connection);
|
|
74
|
+
},
|
|
75
|
+
shouldAdvertize: async (params: ShouldAdvertizeParams) => {
|
|
76
|
+
log('shouldAdvertize', { peerId: connection.peerId, documentId: params.documentId });
|
|
77
|
+
invariant(this._context);
|
|
78
|
+
try {
|
|
79
|
+
const spaceKey = await this._context.getContainingSpaceForDocument(params.documentId);
|
|
80
|
+
if (!spaceKey) {
|
|
81
|
+
log('space key not found for share policy check', {
|
|
82
|
+
peerId: connection.peerId,
|
|
83
|
+
documentId: params.documentId,
|
|
84
|
+
});
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const authorizedDevices = this._authorizedDevices.get(spaceKey);
|
|
89
|
+
|
|
90
|
+
if (!connection.remoteDeviceKey) {
|
|
91
|
+
log('device key not found for share policy check', {
|
|
92
|
+
peerId: connection.peerId,
|
|
93
|
+
documentId: params.documentId,
|
|
94
|
+
});
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isAuthorized = authorizedDevices?.has(connection.remoteDeviceKey) ?? false;
|
|
99
|
+
log('share policy check', {
|
|
100
|
+
localPeer: this._context.peerId,
|
|
101
|
+
remotePeer: connection.peerId,
|
|
102
|
+
documentId: params.documentId,
|
|
103
|
+
deviceKey: connection.remoteDeviceKey,
|
|
104
|
+
spaceKey,
|
|
105
|
+
isAuthorized,
|
|
106
|
+
});
|
|
107
|
+
return isAuthorized;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
log.catch(err);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
this._connections.add(connection);
|
|
115
|
+
|
|
116
|
+
return connection.replicatorExtension;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
authorizeDevice(spaceKey: PublicKey, deviceKey: PublicKey) {
|
|
120
|
+
log('authorizeDevice', { spaceKey, deviceKey });
|
|
121
|
+
defaultMap(this._authorizedDevices, spaceKey, () => new ComplexSet(PublicKey.hash)).add(deviceKey);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type MeshReplicatorConnectionParams = {
|
|
126
|
+
ownPeerId: string;
|
|
127
|
+
onRemoteConnected: () => Promise<void>;
|
|
128
|
+
onRemoteDisconnected: () => Promise<void>;
|
|
129
|
+
shouldAdvertize: (params: ShouldAdvertizeParams) => Promise<boolean>;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
class MeshReplicatorConnection extends Resource implements ReplicatorConnection {
|
|
133
|
+
public readable: ReadableStream<Message>;
|
|
134
|
+
public writable: WritableStream<Message>;
|
|
135
|
+
public remoteDeviceKey: PublicKey | null = null;
|
|
136
|
+
|
|
137
|
+
public readonly replicatorExtension: AutomergeReplicator;
|
|
138
|
+
|
|
139
|
+
private _remotePeerId: string | null = null;
|
|
140
|
+
private _isEnabled = false;
|
|
141
|
+
|
|
142
|
+
constructor(private readonly _params: MeshReplicatorConnectionParams) {
|
|
143
|
+
super();
|
|
144
|
+
|
|
145
|
+
let readableStreamController!: ReadableStreamDefaultController<Message>;
|
|
146
|
+
this.readable = new ReadableStream<Message>({
|
|
147
|
+
start: (controller) => {
|
|
148
|
+
readableStreamController = controller;
|
|
149
|
+
this._ctx.onDispose(() => controller.close());
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.writable = new WritableStream<Message>({
|
|
154
|
+
write: async (message: Message, controller) => {
|
|
155
|
+
// TODO(dmaretskyi): Show we block on RPC completing here?
|
|
156
|
+
this.replicatorExtension.sendSyncMessage({ payload: cbor.encode(message) }).catch((err) => {
|
|
157
|
+
controller.error(err);
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this.replicatorExtension = new AutomergeReplicator(
|
|
163
|
+
{
|
|
164
|
+
peerId: this._params.ownPeerId,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
|
|
168
|
+
// Note: We store only one extension per peer.
|
|
169
|
+
// There can be a case where two connected peers have more than one teleport connection between them
|
|
170
|
+
// and each of them uses different teleport connections to send messages.
|
|
171
|
+
// It works because we receive messages from all teleport connections and Automerge Repo dedup them.
|
|
172
|
+
// TODO(mykola): Use only one teleport connection per peer.
|
|
173
|
+
|
|
174
|
+
// TODO(dmaretskyi): Critical bug.
|
|
175
|
+
// - two peers get connected via swarm 1
|
|
176
|
+
// - they get connected via swarm 2
|
|
177
|
+
// - swarm 1 gets disconnected
|
|
178
|
+
// - automerge repo thinks that peer 2 got disconnected even though swarm 2 is still active
|
|
179
|
+
|
|
180
|
+
this.remoteDeviceKey = remotePeerId;
|
|
181
|
+
|
|
182
|
+
// Set automerge id.
|
|
183
|
+
this._remotePeerId = info.id;
|
|
184
|
+
|
|
185
|
+
log('onStartReplication', { id: info.id, thisPeerId: this.peerId, remotePeerId: remotePeerId.toHex() });
|
|
186
|
+
|
|
187
|
+
await this._params.onRemoteConnected();
|
|
188
|
+
},
|
|
189
|
+
onSyncMessage: async ({ payload }) => {
|
|
190
|
+
if (!this._isEnabled) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const message = cbor.decode(payload) as Message;
|
|
194
|
+
// Note: automerge Repo dedup messages.
|
|
195
|
+
readableStreamController.enqueue(message);
|
|
196
|
+
},
|
|
197
|
+
onClose: async () => {
|
|
198
|
+
if (!this._isEnabled) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await this._params.onRemoteDisconnected();
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
get peerId(): string {
|
|
208
|
+
invariant(this._remotePeerId != null, 'Remote peer has not connected yet.');
|
|
209
|
+
return this._remotePeerId;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async shouldAdvertize(params: ShouldAdvertizeParams): Promise<boolean> {
|
|
213
|
+
return this._params.shouldAdvertize(params);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Start exchanging messages with the remote peer.
|
|
218
|
+
* Call after the remote peer has connected.
|
|
219
|
+
*/
|
|
220
|
+
async enable() {
|
|
221
|
+
invariant(this._remotePeerId != null, 'Remote peer has not connected yet.');
|
|
222
|
+
this._isEnabled = true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Stop exchanging messages with the remote peer.
|
|
227
|
+
*/
|
|
228
|
+
async disable() {
|
|
229
|
+
this._isEnabled = false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import { type StorageKey } from '@dxos/automerge/automerge-repo';
|
|
6
6
|
import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
|
|
7
|
-
import { type
|
|
7
|
+
import { type SublevelDB } from '@dxos/kv-store';
|
|
8
8
|
import { log } from '@dxos/log';
|
|
9
9
|
import { StorageType, type Directory } from '@dxos/random-access-storage';
|
|
10
10
|
|
|
11
11
|
import { AutomergeStorageAdapter } from './automerge-storage-adapter';
|
|
12
12
|
import { encodingOptions } from './leveldb-storage-adapter';
|
|
13
13
|
|
|
14
|
-
export const levelMigration = async ({ db, directory }: { db:
|
|
14
|
+
export const levelMigration = async ({ db, directory }: { db: SublevelDB; directory: Directory }) => {
|
|
15
15
|
// Note: Make automigration from previous storage to leveldb here.
|
|
16
16
|
const isNewLevel = !(await db
|
|
17
17
|
.iterator<StorageKey, Uint8Array>({
|
|
@@ -6,7 +6,7 @@ import { type Context, cancelWithContext } from '@dxos/context';
|
|
|
6
6
|
import { PublicKey } from '@dxos/keys';
|
|
7
7
|
import { schema } from '@dxos/protocols';
|
|
8
8
|
import { BlobMeta } from '@dxos/protocols/proto/dxos/echo/blob';
|
|
9
|
-
import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
9
|
+
import { type SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
10
10
|
import { type BlobStore, type BlobSync } from '@dxos/teleport-extension-object-sync';
|
|
11
11
|
|
|
12
12
|
import { type SnapshotStore } from './snapshot-store';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { subtleCrypto } from '@dxos/crypto';
|
|
6
6
|
import { schema } from '@dxos/protocols';
|
|
7
7
|
import { type StoredSnapshotInfo } from '@dxos/protocols/proto/dxos/devtools/host';
|
|
8
|
-
import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
8
|
+
import { type SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
9
9
|
import { type Directory } from '@dxos/random-access-storage';
|
|
10
10
|
|
|
11
11
|
const SpaceSnapshot = schema.getCodecForType('dxos.echo.snapshot.SpaceSnapshot');
|
|
@@ -14,11 +14,11 @@ import { DataCorruptionError, STORAGE_VERSION, schema } from '@dxos/protocols';
|
|
|
14
14
|
import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
15
15
|
import {
|
|
16
16
|
type ControlPipelineSnapshot,
|
|
17
|
-
EchoMetadata,
|
|
17
|
+
type EchoMetadata,
|
|
18
18
|
type SpaceMetadata,
|
|
19
19
|
type IdentityRecord,
|
|
20
20
|
type SpaceCache,
|
|
21
|
-
LargeSpaceMetadata,
|
|
21
|
+
type LargeSpaceMetadata,
|
|
22
22
|
} from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
23
23
|
import { type Directory, type File } from '@dxos/random-access-storage';
|
|
24
24
|
import { type Timeframe } from '@dxos/timeframe';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { synchronized, trackLeaks } from '@dxos/async';
|
|
6
|
-
import { type DelegateInvitationCredential } from '@dxos/credentials';
|
|
6
|
+
import { type DelegateInvitationCredential, type MemberInfo } from '@dxos/credentials';
|
|
7
7
|
import { failUndefined } from '@dxos/debug';
|
|
8
8
|
import { type FeedStore } from '@dxos/feed-store';
|
|
9
9
|
import { PublicKey } from '@dxos/keys';
|
|
@@ -44,6 +44,7 @@ export type ConstructSpaceParams = {
|
|
|
44
44
|
onAuthorizedConnection: (session: Teleport) => void;
|
|
45
45
|
onAuthFailure?: (session: Teleport) => void;
|
|
46
46
|
onDelegatedInvitationStatusChange: (invitation: DelegateInvitationCredential, isActive: boolean) => Promise<void>;
|
|
47
|
+
onMemberRolesChanged: (member: MemberInfo[]) => Promise<void>;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
/**
|
|
@@ -87,6 +88,7 @@ export class SpaceManager {
|
|
|
87
88
|
onAuthorizedConnection,
|
|
88
89
|
onAuthFailure,
|
|
89
90
|
onDelegatedInvitationStatusChange,
|
|
91
|
+
onMemberRolesChanged,
|
|
90
92
|
memberKey,
|
|
91
93
|
}: ConstructSpaceParams) {
|
|
92
94
|
log.trace('dxos.echo.space-manager.construct-space', trace.begin({ id: this._instanceId }));
|
|
@@ -115,6 +117,7 @@ export class SpaceManager {
|
|
|
115
117
|
snapshotManager,
|
|
116
118
|
memberKey,
|
|
117
119
|
onDelegatedInvitationStatusChange,
|
|
120
|
+
onMemberRolesChanged,
|
|
118
121
|
});
|
|
119
122
|
this._spaces.set(space.key, space);
|
|
120
123
|
|
|
@@ -70,6 +70,12 @@ export class SpaceProtocol {
|
|
|
70
70
|
|
|
71
71
|
private readonly _feeds = new Set<FeedWrapper<FeedMessage>>();
|
|
72
72
|
private readonly _sessions = new ComplexMap<PublicKey, SpaceProtocolSession>(PublicKey.hash);
|
|
73
|
+
// TODO(burdon): Move to config (with sensible defaults).
|
|
74
|
+
private readonly _topology = new MMSTTopology({
|
|
75
|
+
originateConnections: 4,
|
|
76
|
+
maxPeers: 10,
|
|
77
|
+
sampleSize: 20,
|
|
78
|
+
});
|
|
73
79
|
|
|
74
80
|
private _connection?: SwarmConnection;
|
|
75
81
|
|
|
@@ -117,13 +123,6 @@ export class SpaceProtocol {
|
|
|
117
123
|
// TODO(burdon): Document why empty buffer.
|
|
118
124
|
const credentials = await this._swarmIdentity.credentialProvider(Buffer.from(''));
|
|
119
125
|
|
|
120
|
-
// TODO(burdon): Move to config (with sensible defaults).
|
|
121
|
-
const topologyConfig = {
|
|
122
|
-
originateConnections: 4,
|
|
123
|
-
maxPeers: 10,
|
|
124
|
-
sampleSize: 20,
|
|
125
|
-
};
|
|
126
|
-
|
|
127
126
|
await this.blobSync.open();
|
|
128
127
|
|
|
129
128
|
log('starting...');
|
|
@@ -132,13 +131,17 @@ export class SpaceProtocol {
|
|
|
132
131
|
protocolProvider: this._createProtocolProvider(credentials),
|
|
133
132
|
peerId: this._swarmIdentity.peerKey,
|
|
134
133
|
topic,
|
|
135
|
-
topology:
|
|
134
|
+
topology: this._topology,
|
|
136
135
|
label: `swarm ${topic.truncate()} for space ${this._spaceKey.truncate()}`,
|
|
137
136
|
});
|
|
138
137
|
|
|
139
138
|
log('started');
|
|
140
139
|
}
|
|
141
140
|
|
|
141
|
+
public updateTopology() {
|
|
142
|
+
this._topology.forceUpdate();
|
|
143
|
+
}
|
|
144
|
+
|
|
142
145
|
async stop() {
|
|
143
146
|
await this.blobSync.close();
|
|
144
147
|
|
package/src/space/space.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Event, Mutex, synchronized, trackLeaks } from '@dxos/async';
|
|
6
|
-
import {
|
|
7
|
-
import { type FeedInfo, type
|
|
6
|
+
import { type Context, LifecycleState, Resource } from '@dxos/context';
|
|
7
|
+
import { type DelegateInvitationCredential, type FeedInfo, type MemberInfo } from '@dxos/credentials';
|
|
8
8
|
import { type FeedOptions, type FeedWrapper } from '@dxos/feed-store';
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
10
|
import { type PublicKey } from '@dxos/keys';
|
|
@@ -13,7 +13,7 @@ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
|
13
13
|
import { AdmittedFeed, type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
14
14
|
import { type Timeframe } from '@dxos/timeframe';
|
|
15
15
|
import { trace } from '@dxos/tracing';
|
|
16
|
-
import {
|
|
16
|
+
import { type AsyncCallback, Callback } from '@dxos/util';
|
|
17
17
|
|
|
18
18
|
import { ControlPipeline } from './control-pipeline';
|
|
19
19
|
import { type SpaceProtocol } from './space-protocol';
|
|
@@ -37,6 +37,7 @@ export type SpaceParams = {
|
|
|
37
37
|
snapshotId?: string | undefined;
|
|
38
38
|
|
|
39
39
|
onDelegatedInvitationStatusChange: (invitation: DelegateInvitationCredential, isActive: boolean) => Promise<void>;
|
|
40
|
+
onMemberRolesChanged: (member: MemberInfo[]) => Promise<void>;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export type CreatePipelineParams = {
|
|
@@ -109,6 +110,10 @@ export class Space extends Resource {
|
|
|
109
110
|
log('onDelegatedInvitationRemoved', { invitation });
|
|
110
111
|
await params.onDelegatedInvitationStatusChange(invitation, false);
|
|
111
112
|
});
|
|
113
|
+
this._controlPipeline.onMemberRoleChanged.set(async (changedMembers) => {
|
|
114
|
+
log('onMemberRoleChanged', () => ({ changedMembers: changedMembers.map((m) => [m.key, m.role]) }));
|
|
115
|
+
await params.onMemberRolesChanged(changedMembers);
|
|
116
|
+
});
|
|
112
117
|
|
|
113
118
|
// Start replicating the genesis feed.
|
|
114
119
|
this.protocol = params.protocol;
|