@dxos/echo-pipeline 0.3.11-next.0fb359e → 0.3.11-next.e28df4f
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-W3SSYW3X.mjs → chunk-D7UMNYLJ.mjs} +272 -85
- package/dist/lib/browser/chunk-D7UMNYLJ.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +33 -11
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node/{chunk-KTFCZMAY.cjs → chunk-GQW6RLGD.cjs} +267 -83
- package/dist/lib/node/chunk-GQW6RLGD.cjs.map +7 -0
- package/dist/lib/node/index.cjs +26 -26
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +46 -25
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/types/src/automerge/automerge-host.d.ts +37 -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/metadata/metadata-store.d.ts +1 -3
- package/dist/types/src/metadata/metadata-store.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/change-metadata.d.ts +8 -0
- package/dist/types/src/testing/change-metadata.d.ts.map +1 -0
- package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/automerge/automerge-host.test.ts +319 -34
- package/src/automerge/automerge-host.ts +137 -20
- package/src/automerge/index.ts +1 -1
- package/src/metadata/metadata-store.ts +12 -2
- 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/change-metadata.ts +27 -0
- package/src/testing/database-test-rig.ts +4 -1
- package/src/testing/index.ts +1 -0
- package/src/testing/test-agent-builder.ts +1 -1
- package/dist/lib/browser/chunk-W3SSYW3X.mjs.map +0 -7
- package/dist/lib/node/chunk-KTFCZMAY.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,
|
|
@@ -12,31 +14,86 @@ import {
|
|
|
12
14
|
type StorageKey,
|
|
13
15
|
cbor,
|
|
14
16
|
} from '@dxos/automerge/automerge-repo';
|
|
17
|
+
import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
|
|
15
18
|
import { Stream } from '@dxos/codec-protobuf';
|
|
16
19
|
import { invariant } from '@dxos/invariant';
|
|
20
|
+
import { PublicKey } from '@dxos/keys';
|
|
17
21
|
import { log } from '@dxos/log';
|
|
18
22
|
import { type HostInfo, type SyncRepoRequest, type SyncRepoResponse } from '@dxos/protocols/proto/dxos/echo/service';
|
|
19
23
|
import { type PeerInfo } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';
|
|
20
|
-
import { type Directory } from '@dxos/random-access-storage';
|
|
24
|
+
import { StorageType, type Directory } from '@dxos/random-access-storage';
|
|
21
25
|
import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
22
|
-
import {
|
|
26
|
+
import { trace } from '@dxos/tracing';
|
|
27
|
+
import { ComplexMap, ComplexSet, arrayToBuffer, bufferToArray, defaultMap, mapValues } from '@dxos/util';
|
|
23
28
|
|
|
29
|
+
@trace.resource()
|
|
24
30
|
export class AutomergeHost {
|
|
25
31
|
private readonly _repo: Repo;
|
|
26
32
|
private readonly _meshNetwork: MeshNetworkAdapter;
|
|
27
33
|
private readonly _clientNetwork: LocalHostNetworkAdapter;
|
|
28
|
-
private readonly _storage:
|
|
34
|
+
private readonly _storage: StorageAdapter;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* spaceKey -> deviceKey[]
|
|
38
|
+
*/
|
|
39
|
+
private readonly _authorizedDevices = new ComplexMap<PublicKey, ComplexSet<PublicKey>>(PublicKey.hash);
|
|
29
40
|
|
|
30
41
|
constructor(storageDirectory: Directory) {
|
|
31
42
|
this._meshNetwork = new MeshNetworkAdapter();
|
|
32
43
|
this._clientNetwork = new LocalHostNetworkAdapter();
|
|
33
|
-
|
|
44
|
+
|
|
45
|
+
// TODO(mykola): Delete specific handling of IDB storage.
|
|
46
|
+
this._storage =
|
|
47
|
+
storageDirectory.type === StorageType.IDB
|
|
48
|
+
? new IndexedDBStorageAdapter(storageDirectory.path, 'data')
|
|
49
|
+
: new AutomergeStorageAdapter(storageDirectory);
|
|
34
50
|
this._repo = new Repo({
|
|
51
|
+
peerId: `host-${PublicKey.random().toHex()}` as PeerId,
|
|
35
52
|
network: [this._clientNetwork, this._meshNetwork],
|
|
36
53
|
storage: this._storage,
|
|
37
54
|
|
|
38
55
|
// TODO(dmaretskyi): Share based on HALO permissions and space affinity.
|
|
39
|
-
|
|
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.
|
|
40
97
|
});
|
|
41
98
|
this._clientNetwork.ready();
|
|
42
99
|
this._meshNetwork.ready();
|
|
@@ -46,7 +103,22 @@ export class AutomergeHost {
|
|
|
46
103
|
return this._repo;
|
|
47
104
|
}
|
|
48
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
|
+
|
|
49
120
|
async close() {
|
|
121
|
+
this._storage instanceof AutomergeStorageAdapter && (await this._storage.close());
|
|
50
122
|
await this._clientNetwork.close();
|
|
51
123
|
}
|
|
52
124
|
|
|
@@ -62,7 +134,7 @@ export class AutomergeHost {
|
|
|
62
134
|
return this._clientNetwork.sendSyncMessage(request);
|
|
63
135
|
}
|
|
64
136
|
|
|
65
|
-
getHostInfo(): HostInfo {
|
|
137
|
+
async getHostInfo(): Promise<HostInfo> {
|
|
66
138
|
return this._clientNetwork.getHostInfo();
|
|
67
139
|
}
|
|
68
140
|
|
|
@@ -73,6 +145,10 @@ export class AutomergeHost {
|
|
|
73
145
|
createExtension(): AutomergeReplicator {
|
|
74
146
|
return this._meshNetwork.createExtension();
|
|
75
147
|
}
|
|
148
|
+
|
|
149
|
+
authorizeDevice(spaceKey: PublicKey, deviceKey: PublicKey) {
|
|
150
|
+
defaultMap(this._authorizedDevices, spaceKey, () => new ComplexSet(PublicKey.hash)).add(deviceKey);
|
|
151
|
+
}
|
|
76
152
|
}
|
|
77
153
|
|
|
78
154
|
type ClientSyncState = {
|
|
@@ -98,8 +174,11 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
98
174
|
});
|
|
99
175
|
}
|
|
100
176
|
|
|
177
|
+
private _connected = new Trigger();
|
|
178
|
+
|
|
101
179
|
override connect(peerId: PeerId): void {
|
|
102
180
|
this.peerId = peerId;
|
|
181
|
+
this._connected.wake();
|
|
103
182
|
// No-op. Client always connects first
|
|
104
183
|
}
|
|
105
184
|
|
|
@@ -140,18 +219,26 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
140
219
|
},
|
|
141
220
|
});
|
|
142
221
|
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
|
|
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));
|
|
146
231
|
});
|
|
147
232
|
}
|
|
148
233
|
|
|
149
234
|
async sendSyncMessage({ id, syncMessage }: SyncRepoRequest): Promise<void> {
|
|
235
|
+
await this._connected.wait({ timeout: 1_000 });
|
|
150
236
|
const message = cbor.decode(syncMessage!) as Message;
|
|
151
237
|
this.emit('message', message);
|
|
152
238
|
}
|
|
153
239
|
|
|
154
|
-
getHostInfo(): HostInfo {
|
|
240
|
+
async getHostInfo(): Promise<HostInfo> {
|
|
241
|
+
await this._connected.wait({ timeout: 1_000 });
|
|
155
242
|
invariant(this.peerId, 'Peer id not set.');
|
|
156
243
|
return {
|
|
157
244
|
peerId: this.peerId,
|
|
@@ -166,7 +253,7 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
166
253
|
/**
|
|
167
254
|
* Used to replicate with other peers over the network.
|
|
168
255
|
*/
|
|
169
|
-
class MeshNetworkAdapter extends NetworkAdapter {
|
|
256
|
+
export class MeshNetworkAdapter extends NetworkAdapter {
|
|
170
257
|
private readonly _extensions: Map<string, AutomergeReplicator> = new Map();
|
|
171
258
|
|
|
172
259
|
/**
|
|
@@ -204,7 +291,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
204
291
|
peerId: this.peerId,
|
|
205
292
|
},
|
|
206
293
|
{
|
|
207
|
-
onStartReplication: async (info) => {
|
|
294
|
+
onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
|
|
208
295
|
// Note: We store only one extension per peer.
|
|
209
296
|
// There can be a case where two connected peers have more than one teleport connection between them
|
|
210
297
|
// and each of them uses different teleport connections to send messages.
|
|
@@ -218,6 +305,10 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
218
305
|
// TODO(mykola): Fix race condition?
|
|
219
306
|
this._extensions.set(info.id, extension);
|
|
220
307
|
this.emit('peer-candidate', {
|
|
308
|
+
// TODO(mykola): Hack, stop abusing `peerMetadata` field.
|
|
309
|
+
peerMetadata: {
|
|
310
|
+
dxos_deviceKey: remotePeerId.toHex(),
|
|
311
|
+
} as any,
|
|
221
312
|
peerId: info.id as PeerId,
|
|
222
313
|
});
|
|
223
314
|
},
|
|
@@ -227,10 +318,13 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
227
318
|
this.emit('message', message);
|
|
228
319
|
},
|
|
229
320
|
onClose: async () => {
|
|
230
|
-
peerInfo
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
321
|
+
if (!peerInfo) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.emit('peer-disconnected', {
|
|
325
|
+
peerId: peerInfo.id as PeerId,
|
|
326
|
+
});
|
|
327
|
+
this._extensions.delete(peerInfo.id);
|
|
234
328
|
},
|
|
235
329
|
},
|
|
236
330
|
);
|
|
@@ -238,12 +332,19 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
238
332
|
}
|
|
239
333
|
}
|
|
240
334
|
|
|
241
|
-
class AutomergeStorageAdapter extends StorageAdapter {
|
|
335
|
+
export class AutomergeStorageAdapter extends StorageAdapter {
|
|
336
|
+
// TODO(mykola): Hack for restricting automerge Repo to access storage if Host is `closed`.
|
|
337
|
+
// Automerge Repo do not have any lifetime management.
|
|
338
|
+
private _state: 'opened' | 'closed' = 'opened';
|
|
339
|
+
|
|
242
340
|
constructor(private readonly _directory: Directory) {
|
|
243
341
|
super();
|
|
244
342
|
}
|
|
245
343
|
|
|
246
344
|
override async load(key: StorageKey): Promise<Uint8Array | undefined> {
|
|
345
|
+
if (this._state !== 'opened') {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
247
348
|
const filename = this._getFilename(key);
|
|
248
349
|
const file = this._directory.getOrCreateFile(filename);
|
|
249
350
|
const { size } = await file.stat();
|
|
@@ -255,6 +356,9 @@ class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
255
356
|
}
|
|
256
357
|
|
|
257
358
|
override async save(key: StorageKey, data: Uint8Array): Promise<void> {
|
|
359
|
+
if (this._state !== 'opened') {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
258
362
|
const filename = this._getFilename(key);
|
|
259
363
|
const file = this._directory.getOrCreateFile(filename);
|
|
260
364
|
await file.write(0, arrayToBuffer(data));
|
|
@@ -264,13 +368,19 @@ class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
264
368
|
}
|
|
265
369
|
|
|
266
370
|
override async remove(key: StorageKey): Promise<void> {
|
|
371
|
+
if (this._state !== 'opened') {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
267
374
|
// TODO(dmaretskyi): Better deletion.
|
|
268
375
|
const filename = this._getFilename(key);
|
|
269
376
|
const file = this._directory.getOrCreateFile(filename);
|
|
270
|
-
await file.
|
|
377
|
+
await file.destroy();
|
|
271
378
|
}
|
|
272
379
|
|
|
273
380
|
override async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
|
|
381
|
+
if (this._state !== 'opened') {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
274
384
|
const filename = this._getFilename(keyPrefix);
|
|
275
385
|
const entries = await this._directory.list();
|
|
276
386
|
return Promise.all(
|
|
@@ -289,18 +399,25 @@ class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
289
399
|
}
|
|
290
400
|
|
|
291
401
|
override async removeRange(keyPrefix: StorageKey): Promise<void> {
|
|
402
|
+
if (this._state !== 'opened') {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
292
405
|
const filename = this._getFilename(keyPrefix);
|
|
293
406
|
const entries = await this._directory.list();
|
|
294
407
|
await Promise.all(
|
|
295
408
|
entries
|
|
296
409
|
.filter((entry) => entry.startsWith(filename))
|
|
297
410
|
.map(async (entry): Promise<void> => {
|
|
298
|
-
const file = this._directory.getOrCreateFile(
|
|
299
|
-
await file.
|
|
411
|
+
const file = this._directory.getOrCreateFile(entry);
|
|
412
|
+
await file.destroy();
|
|
300
413
|
}),
|
|
301
414
|
);
|
|
302
415
|
}
|
|
303
416
|
|
|
417
|
+
async close(): Promise<void> {
|
|
418
|
+
this._state = 'closed';
|
|
419
|
+
}
|
|
420
|
+
|
|
304
421
|
private _getFilename(key: StorageKey): string {
|
|
305
422
|
return key.map((k) => k.replaceAll('%', '%25').replaceAll('-', '%2D')).join('-');
|
|
306
423
|
}
|
package/src/automerge/index.ts
CHANGED
|
@@ -48,7 +48,14 @@ export class MetadataStore {
|
|
|
48
48
|
|
|
49
49
|
public readonly update = new Event<EchoMetadata>();
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
readonly _directory: Directory;
|
|
55
|
+
|
|
56
|
+
constructor(directory: Directory) {
|
|
57
|
+
this._directory = directory;
|
|
58
|
+
}
|
|
52
59
|
|
|
53
60
|
get metadata(): EchoMetadata {
|
|
54
61
|
return this._metadata;
|
|
@@ -94,7 +101,10 @@ export class MetadataStore {
|
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
|
|
104
|
+
/**
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
async _writeFile<T>(file: File, codec: Codec<T>, data: T): Promise<void> {
|
|
98
108
|
const encoded = arrayToBuffer(codec.encode(data));
|
|
99
109
|
const checksum = CRC32.buf(encoded);
|
|
100
110
|
|
|
@@ -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 }));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { log } from '@dxos/log';
|
|
6
|
+
import { schema } from '@dxos/protocols';
|
|
7
|
+
import type { Storage } from '@dxos/random-access-storage';
|
|
8
|
+
|
|
9
|
+
import { MetadataStore } from '../metadata';
|
|
10
|
+
|
|
11
|
+
const EchoMetadata = schema.getCodecForType('dxos.echo.metadata.EchoMetadata');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This function will change the storage version in the metadata.
|
|
15
|
+
* This will break your storage and make it unusable.
|
|
16
|
+
* Use this only for testing purposes.
|
|
17
|
+
*/
|
|
18
|
+
export const changeStorageVersionInMetadata = async (storage: Storage, version: number) => {
|
|
19
|
+
log.info('Changing storage version in metadata. USE ONLY FOR TESTING.');
|
|
20
|
+
const metadata = new MetadataStore(storage.createDirectory('metadata'));
|
|
21
|
+
await metadata.load();
|
|
22
|
+
const echoMetadata = metadata.metadata;
|
|
23
|
+
echoMetadata.version = version;
|
|
24
|
+
const file = metadata._directory.getOrCreateFile('EchoMetadata');
|
|
25
|
+
await metadata._writeFile(file, EchoMetadata, echoMetadata);
|
|
26
|
+
await metadata._directory.flush();
|
|
27
|
+
};
|
|
@@ -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);
|
package/src/testing/index.ts
CHANGED
|
@@ -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 }),
|