@dxos/echo-pipeline 0.3.11-main.ec7a2b3 → 0.3.11-main.ee2b64c
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-WDT56L4E.mjs → chunk-IOUMNVGJ.mjs} +266 -79
- package/dist/lib/browser/chunk-IOUMNVGJ.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +5 -5
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-C6GFWBRK.cjs → chunk-PDU65RAS.cjs} +260 -77
- package/dist/lib/node/chunk-PDU65RAS.cjs.map +7 -0
- package/dist/lib/node/index.cjs +26 -28
- package/dist/lib/node/index.cjs.map +2 -2
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +20 -20
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/automerge/automerge-host.d.ts +27 -2
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +1 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/space/data-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +2 -2
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +1 -1
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/automerge/automerge-host.test.ts +353 -35
- package/src/automerge/automerge-host.ts +138 -21
- package/src/automerge/index.ts +1 -1
- package/src/pipeline/pipeline-stress.test.ts +9 -2
- package/src/space/data-pipeline.ts +4 -3
- package/src/space/space-manager.ts +3 -3
- package/src/space/space-protocol.ts +4 -0
- package/src/space/space.ts +8 -3
- package/src/testing/database-test-rig.ts +4 -1
- package/src/testing/test-agent-builder.ts +1 -1
- package/dist/lib/browser/chunk-WDT56L4E.mjs.map +0 -7
- package/dist/lib/node/chunk-C6GFWBRK.cjs.map +0 -7
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Trigger } from '@dxos/async';
|
|
6
|
+
import { next as automerge } from '@dxos/automerge/automerge';
|
|
5
7
|
import {
|
|
6
8
|
Repo,
|
|
7
9
|
NetworkAdapter,
|
|
@@ -15,19 +17,27 @@ import {
|
|
|
15
17
|
import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
|
|
16
18
|
import { Stream } from '@dxos/codec-protobuf';
|
|
17
19
|
import { invariant } from '@dxos/invariant';
|
|
20
|
+
import { PublicKey } from '@dxos/keys';
|
|
18
21
|
import { log } from '@dxos/log';
|
|
19
22
|
import { type HostInfo, type SyncRepoRequest, type SyncRepoResponse } from '@dxos/protocols/proto/dxos/echo/service';
|
|
20
23
|
import { type PeerInfo } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';
|
|
21
24
|
import { StorageType, type Directory } from '@dxos/random-access-storage';
|
|
22
25
|
import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
23
|
-
import {
|
|
26
|
+
import { trace } from '@dxos/tracing';
|
|
27
|
+
import { ComplexMap, ComplexSet, arrayToBuffer, bufferToArray, defaultMap, mapValues } from '@dxos/util';
|
|
24
28
|
|
|
29
|
+
@trace.resource()
|
|
25
30
|
export class AutomergeHost {
|
|
26
31
|
private readonly _repo: Repo;
|
|
27
32
|
private readonly _meshNetwork: MeshNetworkAdapter;
|
|
28
33
|
private readonly _clientNetwork: LocalHostNetworkAdapter;
|
|
29
34
|
private readonly _storage: StorageAdapter;
|
|
30
35
|
|
|
36
|
+
/**
|
|
37
|
+
* spaceKey -> deviceKey[]
|
|
38
|
+
*/
|
|
39
|
+
private readonly _authorizedDevices = new ComplexMap<PublicKey, ComplexSet<PublicKey>>(PublicKey.hash);
|
|
40
|
+
|
|
31
41
|
constructor(storageDirectory: Directory) {
|
|
32
42
|
this._meshNetwork = new MeshNetworkAdapter();
|
|
33
43
|
this._clientNetwork = new LocalHostNetworkAdapter();
|
|
@@ -38,11 +48,52 @@ export class AutomergeHost {
|
|
|
38
48
|
? new IndexedDBStorageAdapter(storageDirectory.path, 'data')
|
|
39
49
|
: new AutomergeStorageAdapter(storageDirectory);
|
|
40
50
|
this._repo = new Repo({
|
|
51
|
+
peerId: `host-${PublicKey.random().toHex()}` as PeerId,
|
|
41
52
|
network: [this._clientNetwork, this._meshNetwork],
|
|
42
53
|
storage: this._storage,
|
|
43
54
|
|
|
44
55
|
// TODO(dmaretskyi): Share based on HALO permissions and space affinity.
|
|
45
|
-
|
|
56
|
+
// Hosts, running in the worker, don't share documents unless requested by other peers.
|
|
57
|
+
sharePolicy: async (peerId /* device key */, documentId /* space key */) => {
|
|
58
|
+
if (peerId.startsWith('client-')) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!documentId) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const doc = this._repo.handles[documentId]?.docSync();
|
|
67
|
+
if (!doc) {
|
|
68
|
+
log('doc not found for share policy check', { peerId, documentId });
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
if (!doc.experimental_spaceKey) {
|
|
74
|
+
log('space key not found for share policy check', { peerId, documentId });
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const spaceKey = PublicKey.from(doc.experimental_spaceKey);
|
|
79
|
+
const authorizedDevices = this._authorizedDevices.get(spaceKey);
|
|
80
|
+
|
|
81
|
+
// TODO(mykola): Hack, stop abusing `peerMetadata` field.
|
|
82
|
+
const deviceKeyHex = (this.repo.peerMetadataByPeerId[peerId] as any)?.dxos_deviceKey;
|
|
83
|
+
if (!deviceKeyHex) {
|
|
84
|
+
log('device key not found for share policy check', { peerId, documentId });
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const deviceKey = PublicKey.from(deviceKeyHex);
|
|
88
|
+
|
|
89
|
+
const isAuthorized = authorizedDevices?.has(deviceKey) ?? false;
|
|
90
|
+
log('share policy check', { peerId, documentId, deviceKey, spaceKey, isAuthorized });
|
|
91
|
+
return isAuthorized;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log.catch(err);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}, // Share everything.
|
|
46
97
|
});
|
|
47
98
|
this._clientNetwork.ready();
|
|
48
99
|
this._meshNetwork.ready();
|
|
@@ -52,7 +103,22 @@ export class AutomergeHost {
|
|
|
52
103
|
return this._repo;
|
|
53
104
|
}
|
|
54
105
|
|
|
106
|
+
@trace.info({ depth: null })
|
|
107
|
+
private _automergeDocs() {
|
|
108
|
+
return mapValues(this._repo.handles, (handle) => ({
|
|
109
|
+
state: handle.state,
|
|
110
|
+
hasDoc: !!handle.docSync(),
|
|
111
|
+
heads: handle.docSync() ? automerge.getHeads(handle.docSync()) : null,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@trace.info({ depth: null })
|
|
116
|
+
private _automergePeers() {
|
|
117
|
+
return this._repo.peers;
|
|
118
|
+
}
|
|
119
|
+
|
|
55
120
|
async close() {
|
|
121
|
+
this._storage instanceof AutomergeStorageAdapter && (await this._storage.close());
|
|
56
122
|
await this._clientNetwork.close();
|
|
57
123
|
}
|
|
58
124
|
|
|
@@ -68,7 +134,7 @@ export class AutomergeHost {
|
|
|
68
134
|
return this._clientNetwork.sendSyncMessage(request);
|
|
69
135
|
}
|
|
70
136
|
|
|
71
|
-
getHostInfo(): HostInfo {
|
|
137
|
+
async getHostInfo(): Promise<HostInfo> {
|
|
72
138
|
return this._clientNetwork.getHostInfo();
|
|
73
139
|
}
|
|
74
140
|
|
|
@@ -79,6 +145,10 @@ export class AutomergeHost {
|
|
|
79
145
|
createExtension(): AutomergeReplicator {
|
|
80
146
|
return this._meshNetwork.createExtension();
|
|
81
147
|
}
|
|
148
|
+
|
|
149
|
+
authorizeDevice(spaceKey: PublicKey, deviceKey: PublicKey) {
|
|
150
|
+
defaultMap(this._authorizedDevices, spaceKey, () => new ComplexSet(PublicKey.hash)).add(deviceKey);
|
|
151
|
+
}
|
|
82
152
|
}
|
|
83
153
|
|
|
84
154
|
type ClientSyncState = {
|
|
@@ -104,8 +174,11 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
104
174
|
});
|
|
105
175
|
}
|
|
106
176
|
|
|
177
|
+
private _connected = new Trigger();
|
|
178
|
+
|
|
107
179
|
override connect(peerId: PeerId): void {
|
|
108
180
|
this.peerId = peerId;
|
|
181
|
+
this._connected.wake();
|
|
109
182
|
// No-op. Client always connects first
|
|
110
183
|
}
|
|
111
184
|
|
|
@@ -146,18 +219,26 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
146
219
|
},
|
|
147
220
|
});
|
|
148
221
|
|
|
149
|
-
this.
|
|
150
|
-
|
|
151
|
-
|
|
222
|
+
this._connected
|
|
223
|
+
.wait({ timeout: 1_000 })
|
|
224
|
+
.then(() => {
|
|
225
|
+
this.emit('peer-candidate', {
|
|
226
|
+
peerMetadata: {},
|
|
227
|
+
peerId,
|
|
228
|
+
});
|
|
229
|
+
})
|
|
230
|
+
.catch((err) => log.catch(err));
|
|
152
231
|
});
|
|
153
232
|
}
|
|
154
233
|
|
|
155
234
|
async sendSyncMessage({ id, syncMessage }: SyncRepoRequest): Promise<void> {
|
|
235
|
+
await this._connected.wait({ timeout: 1_000 });
|
|
156
236
|
const message = cbor.decode(syncMessage!) as Message;
|
|
157
237
|
this.emit('message', message);
|
|
158
238
|
}
|
|
159
239
|
|
|
160
|
-
getHostInfo(): HostInfo {
|
|
240
|
+
async getHostInfo(): Promise<HostInfo> {
|
|
241
|
+
await this._connected.wait({ timeout: 1_000 });
|
|
161
242
|
invariant(this.peerId, 'Peer id not set.');
|
|
162
243
|
return {
|
|
163
244
|
peerId: this.peerId,
|
|
@@ -172,8 +253,9 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
172
253
|
/**
|
|
173
254
|
* Used to replicate with other peers over the network.
|
|
174
255
|
*/
|
|
175
|
-
class MeshNetworkAdapter extends NetworkAdapter {
|
|
256
|
+
export class MeshNetworkAdapter extends NetworkAdapter {
|
|
176
257
|
private readonly _extensions: Map<string, AutomergeReplicator> = new Map();
|
|
258
|
+
private _connected = new Trigger();
|
|
177
259
|
|
|
178
260
|
/**
|
|
179
261
|
* Emits `ready` event. That signals to `Repo` that it can start using the adapter.
|
|
@@ -188,6 +270,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
188
270
|
|
|
189
271
|
override connect(peerId: PeerId): void {
|
|
190
272
|
this.peerId = peerId;
|
|
273
|
+
this._connected.wake();
|
|
191
274
|
}
|
|
192
275
|
|
|
193
276
|
override send(message: Message): void {
|
|
@@ -210,20 +293,28 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
210
293
|
peerId: this.peerId,
|
|
211
294
|
},
|
|
212
295
|
{
|
|
213
|
-
onStartReplication: async (info) => {
|
|
296
|
+
onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
|
|
297
|
+
await this._connected.wait();
|
|
298
|
+
|
|
214
299
|
// Note: We store only one extension per peer.
|
|
215
300
|
// There can be a case where two connected peers have more than one teleport connection between them
|
|
216
301
|
// and each of them uses different teleport connections to send messages.
|
|
217
302
|
// It works because we receive messages from all teleport connections and Automerge Repo dedup them.
|
|
218
303
|
// TODO(mykola): Use only one teleport connection per peer.
|
|
219
|
-
if (this._extensions.has(info.id)) {
|
|
220
|
-
|
|
304
|
+
if (!this._extensions.has(info.id)) {
|
|
305
|
+
peerInfo = info;
|
|
306
|
+
// TODO(mykola): Fix race condition?
|
|
307
|
+
this._extensions.set(info.id, extension);
|
|
308
|
+
} else {
|
|
309
|
+
// TODO(mykola): retry hack.
|
|
310
|
+
this.emit('peer-disconnected', { peerId: info.id as PeerId });
|
|
221
311
|
}
|
|
222
312
|
|
|
223
|
-
peerInfo = info;
|
|
224
|
-
// TODO(mykola): Fix race condition?
|
|
225
|
-
this._extensions.set(info.id, extension);
|
|
226
313
|
this.emit('peer-candidate', {
|
|
314
|
+
// TODO(mykola): Hack, stop abusing `peerMetadata` field.
|
|
315
|
+
peerMetadata: {
|
|
316
|
+
dxos_deviceKey: remotePeerId.toHex(),
|
|
317
|
+
} as any,
|
|
227
318
|
peerId: info.id as PeerId,
|
|
228
319
|
});
|
|
229
320
|
},
|
|
@@ -233,10 +324,13 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
233
324
|
this.emit('message', message);
|
|
234
325
|
},
|
|
235
326
|
onClose: async () => {
|
|
236
|
-
peerInfo
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
327
|
+
if (!peerInfo) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.emit('peer-disconnected', {
|
|
331
|
+
peerId: peerInfo.id as PeerId,
|
|
332
|
+
});
|
|
333
|
+
this._extensions.delete(peerInfo.id);
|
|
240
334
|
},
|
|
241
335
|
},
|
|
242
336
|
);
|
|
@@ -245,11 +339,18 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
245
339
|
}
|
|
246
340
|
|
|
247
341
|
export class AutomergeStorageAdapter extends StorageAdapter {
|
|
342
|
+
// TODO(mykola): Hack for restricting automerge Repo to access storage if Host is `closed`.
|
|
343
|
+
// Automerge Repo do not have any lifetime management.
|
|
344
|
+
private _state: 'opened' | 'closed' = 'opened';
|
|
345
|
+
|
|
248
346
|
constructor(private readonly _directory: Directory) {
|
|
249
347
|
super();
|
|
250
348
|
}
|
|
251
349
|
|
|
252
350
|
override async load(key: StorageKey): Promise<Uint8Array | undefined> {
|
|
351
|
+
if (this._state !== 'opened') {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
253
354
|
const filename = this._getFilename(key);
|
|
254
355
|
const file = this._directory.getOrCreateFile(filename);
|
|
255
356
|
const { size } = await file.stat();
|
|
@@ -261,6 +362,9 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
261
362
|
}
|
|
262
363
|
|
|
263
364
|
override async save(key: StorageKey, data: Uint8Array): Promise<void> {
|
|
365
|
+
if (this._state !== 'opened') {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
264
368
|
const filename = this._getFilename(key);
|
|
265
369
|
const file = this._directory.getOrCreateFile(filename);
|
|
266
370
|
await file.write(0, arrayToBuffer(data));
|
|
@@ -270,13 +374,19 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
270
374
|
}
|
|
271
375
|
|
|
272
376
|
override async remove(key: StorageKey): Promise<void> {
|
|
377
|
+
if (this._state !== 'opened') {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
273
380
|
// TODO(dmaretskyi): Better deletion.
|
|
274
381
|
const filename = this._getFilename(key);
|
|
275
382
|
const file = this._directory.getOrCreateFile(filename);
|
|
276
|
-
await file.
|
|
383
|
+
await file.destroy();
|
|
277
384
|
}
|
|
278
385
|
|
|
279
386
|
override async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
|
|
387
|
+
if (this._state !== 'opened') {
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
280
390
|
const filename = this._getFilename(keyPrefix);
|
|
281
391
|
const entries = await this._directory.list();
|
|
282
392
|
return Promise.all(
|
|
@@ -295,18 +405,25 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
295
405
|
}
|
|
296
406
|
|
|
297
407
|
override async removeRange(keyPrefix: StorageKey): Promise<void> {
|
|
408
|
+
if (this._state !== 'opened') {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
298
411
|
const filename = this._getFilename(keyPrefix);
|
|
299
412
|
const entries = await this._directory.list();
|
|
300
413
|
await Promise.all(
|
|
301
414
|
entries
|
|
302
415
|
.filter((entry) => entry.startsWith(filename))
|
|
303
416
|
.map(async (entry): Promise<void> => {
|
|
304
|
-
const file = this._directory.getOrCreateFile(
|
|
305
|
-
await file.
|
|
417
|
+
const file = this._directory.getOrCreateFile(entry);
|
|
418
|
+
await file.destroy();
|
|
306
419
|
}),
|
|
307
420
|
);
|
|
308
421
|
}
|
|
309
422
|
|
|
423
|
+
async close(): Promise<void> {
|
|
424
|
+
this._state = 'closed';
|
|
425
|
+
}
|
|
426
|
+
|
|
310
427
|
private _getFilename(key: StorageKey): string {
|
|
311
428
|
return key.map((k) => k.replaceAll('%', '%25').replaceAll('-', '%2D')).join('-');
|
|
312
429
|
}
|
package/src/automerge/index.ts
CHANGED
|
@@ -80,7 +80,11 @@ class Agent {
|
|
|
80
80
|
public messages: FeedMessageBlock[] = [];
|
|
81
81
|
public writePromise: Promise<any> = Promise.resolve();
|
|
82
82
|
|
|
83
|
-
constructor(
|
|
83
|
+
constructor(
|
|
84
|
+
private readonly builder: TestFeedBuilder,
|
|
85
|
+
public feedStore: FeedStore<FeedMessage>,
|
|
86
|
+
public id: string,
|
|
87
|
+
) {}
|
|
84
88
|
|
|
85
89
|
async open() {
|
|
86
90
|
const key = await this.builder.keyring.createKey();
|
|
@@ -131,7 +135,10 @@ type Real = {
|
|
|
131
135
|
};
|
|
132
136
|
|
|
133
137
|
class WriteCommand implements fc.AsyncCommand<Model, Real> {
|
|
134
|
-
constructor(
|
|
138
|
+
constructor(
|
|
139
|
+
public agent: string,
|
|
140
|
+
public count: number,
|
|
141
|
+
) {}
|
|
135
142
|
|
|
136
143
|
check = () => true;
|
|
137
144
|
|
|
@@ -216,6 +216,7 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
private async _consumePipeline() {
|
|
219
|
+
const pipeline = this._pipeline;
|
|
219
220
|
if (this.currentEpoch) {
|
|
220
221
|
const waitForOneEpoch = this.onNewEpoch.waitForCount(1);
|
|
221
222
|
await this._processEpochInSeparateTask(this.currentEpoch);
|
|
@@ -225,8 +226,8 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
225
226
|
// CPU bottleneck control.
|
|
226
227
|
let messageCounter = 0;
|
|
227
228
|
|
|
228
|
-
invariant(
|
|
229
|
-
for await (const msg of
|
|
229
|
+
invariant(pipeline, 'Pipeline is not initialized.');
|
|
230
|
+
for await (const msg of pipeline.consume()) {
|
|
230
231
|
const span = this._usage.beginRecording();
|
|
231
232
|
this._mutations.inc();
|
|
232
233
|
|
|
@@ -261,7 +262,7 @@ export class DataPipeline implements CredentialProcessor {
|
|
|
261
262
|
} satisfies DataPipelineProcessed);
|
|
262
263
|
|
|
263
264
|
// Timeframe clock is not updated yet.
|
|
264
|
-
await this._noteTargetStateIfNeeded(
|
|
265
|
+
await this._noteTargetStateIfNeeded(pipeline.state.pendingTimeframe);
|
|
265
266
|
}
|
|
266
267
|
} catch (err: any) {
|
|
267
268
|
log.catch(err);
|
|
@@ -42,7 +42,7 @@ export type ConstructSpaceParams = {
|
|
|
42
42
|
/**
|
|
43
43
|
* Called when connection auth passed successful.
|
|
44
44
|
*/
|
|
45
|
-
|
|
45
|
+
onAuthorizedConnection: (session: Teleport) => void;
|
|
46
46
|
onAuthFailure?: (session: Teleport) => void;
|
|
47
47
|
};
|
|
48
48
|
|
|
@@ -93,7 +93,7 @@ export class SpaceManager {
|
|
|
93
93
|
async constructSpace({
|
|
94
94
|
metadata,
|
|
95
95
|
swarmIdentity,
|
|
96
|
-
|
|
96
|
+
onAuthorizedConnection,
|
|
97
97
|
onAuthFailure,
|
|
98
98
|
memberKey,
|
|
99
99
|
}: ConstructSpaceParams) {
|
|
@@ -108,7 +108,7 @@ export class SpaceManager {
|
|
|
108
108
|
topic: spaceKey,
|
|
109
109
|
swarmIdentity,
|
|
110
110
|
networkManager: this._networkManager,
|
|
111
|
-
onSessionAuth:
|
|
111
|
+
onSessionAuth: onAuthorizedConnection,
|
|
112
112
|
onAuthFailure,
|
|
113
113
|
blobStore: this._blobStore,
|
|
114
114
|
});
|
|
@@ -19,6 +19,7 @@ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
|
19
19
|
import { type MuxerStats, Teleport } from '@dxos/teleport';
|
|
20
20
|
import { type BlobStore, BlobSync } from '@dxos/teleport-extension-object-sync';
|
|
21
21
|
import { ReplicatorExtension } from '@dxos/teleport-extension-replicator';
|
|
22
|
+
import { trace } from '@dxos/tracing';
|
|
22
23
|
import { ComplexMap } from '@dxos/util';
|
|
23
24
|
|
|
24
25
|
import { AuthExtension, type AuthProvider, type AuthVerifier } from './auth';
|
|
@@ -51,6 +52,7 @@ export type SpaceProtocolOptions = {
|
|
|
51
52
|
/**
|
|
52
53
|
* Manages Teleport protocol stream creation and joining swarms with replication and presence extensions.
|
|
53
54
|
*/
|
|
55
|
+
@trace.resource()
|
|
54
56
|
export class SpaceProtocol {
|
|
55
57
|
private readonly _networkManager: NetworkManager;
|
|
56
58
|
private readonly _swarmIdentity: SwarmIdentity;
|
|
@@ -60,8 +62,10 @@ export class SpaceProtocol {
|
|
|
60
62
|
public readonly blobSync: BlobSync;
|
|
61
63
|
|
|
62
64
|
@logInfo
|
|
65
|
+
@trace.info()
|
|
63
66
|
private readonly _topic: Promise<PublicKey>;
|
|
64
67
|
|
|
68
|
+
@trace.info()
|
|
65
69
|
private readonly _spaceKey: PublicKey;
|
|
66
70
|
|
|
67
71
|
private readonly _feeds = new Set<FeedWrapper<FeedMessage>>();
|
package/src/space/space.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Event, synchronized, trackLeaks,
|
|
5
|
+
import { Event, synchronized, trackLeaks, Mutex } from '@dxos/async';
|
|
6
6
|
import { type Context } from '@dxos/context';
|
|
7
7
|
import { type FeedInfo } from '@dxos/credentials';
|
|
8
8
|
import { type FeedOptions, type FeedWrapper } from '@dxos/feed-store';
|
|
@@ -52,17 +52,22 @@ export type CreatePipelineParams = {
|
|
|
52
52
|
@trackLeaks('open', 'close')
|
|
53
53
|
@trace.resource()
|
|
54
54
|
export class Space {
|
|
55
|
-
private readonly
|
|
55
|
+
private readonly _addFeedMutex = new Mutex();
|
|
56
56
|
|
|
57
57
|
public readonly onCredentialProcessed = new Callback<AsyncCallback<Credential>>();
|
|
58
58
|
public readonly stateUpdate = new Event();
|
|
59
|
+
@trace.info()
|
|
59
60
|
public readonly protocol: SpaceProtocol;
|
|
60
61
|
|
|
61
62
|
private readonly _key: PublicKey;
|
|
62
63
|
private readonly _genesisFeedKey: PublicKey;
|
|
63
64
|
private readonly _feedProvider: FeedProvider;
|
|
65
|
+
@trace.info()
|
|
64
66
|
private readonly _controlPipeline: ControlPipeline;
|
|
67
|
+
|
|
68
|
+
@trace.info()
|
|
65
69
|
private readonly _dataPipeline: DataPipeline;
|
|
70
|
+
|
|
66
71
|
private readonly _snapshotManager: SnapshotManager;
|
|
67
72
|
|
|
68
73
|
private _isOpen = false;
|
|
@@ -130,7 +135,7 @@ export class Space {
|
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
// Add existing feeds.
|
|
133
|
-
await this.
|
|
138
|
+
await this._addFeedMutex.executeSynchronized(async () => {
|
|
134
139
|
for (const feed of this._controlPipeline.spaceState.feeds.values()) {
|
|
135
140
|
if (feed.assertion.designation === AdmittedFeed.Designation.DATA && !pipeline.hasFeed(feed.key)) {
|
|
136
141
|
await pipeline.addFeed(await this._feedProvider(feed.key, { sparse: true }));
|
|
@@ -74,7 +74,10 @@ export class DatabaseTestPeer {
|
|
|
74
74
|
|
|
75
75
|
private readonly _writes = new Set<WriteRequest>();
|
|
76
76
|
|
|
77
|
-
constructor(
|
|
77
|
+
constructor(
|
|
78
|
+
public readonly rig: DatabaseTestBuilder,
|
|
79
|
+
public readonly spaceKey: PublicKey,
|
|
80
|
+
) {}
|
|
78
81
|
|
|
79
82
|
async open() {
|
|
80
83
|
this.hostItems = new ItemManager(this.modelFactory);
|
|
@@ -189,7 +189,7 @@ export class TestAgent {
|
|
|
189
189
|
credentialAuthenticator: MOCK_AUTH_VERIFIER,
|
|
190
190
|
},
|
|
191
191
|
memberKey: identityKey,
|
|
192
|
-
|
|
192
|
+
onAuthorizedConnection: (session) => {
|
|
193
193
|
session.addExtension(
|
|
194
194
|
'dxos.mesh.teleport.gossip',
|
|
195
195
|
this.createGossip().createExtension({ remotePeerId: session.remotePeerId }),
|