@dxos/echo-pipeline 0.3.11-main.fbbdc2a → 0.3.11-main.fc97a54
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-IOUMNVGJ.mjs} +282 -89
- package/dist/lib/browser/chunk-IOUMNVGJ.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-PDU65RAS.cjs} +277 -87
- package/dist/lib/node/chunk-PDU65RAS.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 +38 -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 +381 -35
- package/src/automerge/automerge-host.ts +148 -25
- 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,8 +253,9 @@ 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();
|
|
258
|
+
private _connected = new Trigger();
|
|
171
259
|
|
|
172
260
|
/**
|
|
173
261
|
* Emits `ready` event. That signals to `Repo` that it can start using the adapter.
|
|
@@ -182,6 +270,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
182
270
|
|
|
183
271
|
override connect(peerId: PeerId): void {
|
|
184
272
|
this.peerId = peerId;
|
|
273
|
+
this._connected.wake();
|
|
185
274
|
}
|
|
186
275
|
|
|
187
276
|
override send(message: Message): void {
|
|
@@ -204,20 +293,28 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
204
293
|
peerId: this.peerId,
|
|
205
294
|
},
|
|
206
295
|
{
|
|
207
|
-
onStartReplication: async (info) => {
|
|
296
|
+
onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
|
|
297
|
+
await this._connected.wait();
|
|
298
|
+
|
|
208
299
|
// Note: We store only one extension per peer.
|
|
209
300
|
// There can be a case where two connected peers have more than one teleport connection between them
|
|
210
301
|
// and each of them uses different teleport connections to send messages.
|
|
211
302
|
// It works because we receive messages from all teleport connections and Automerge Repo dedup them.
|
|
212
303
|
// TODO(mykola): Use only one teleport connection per peer.
|
|
213
|
-
if (this._extensions.has(info.id)) {
|
|
214
|
-
|
|
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 });
|
|
215
311
|
}
|
|
216
312
|
|
|
217
|
-
peerInfo = info;
|
|
218
|
-
// TODO(mykola): Fix race condition?
|
|
219
|
-
this._extensions.set(info.id, extension);
|
|
220
313
|
this.emit('peer-candidate', {
|
|
314
|
+
// TODO(mykola): Hack, stop abusing `peerMetadata` field.
|
|
315
|
+
peerMetadata: {
|
|
316
|
+
dxos_deviceKey: remotePeerId.toHex(),
|
|
317
|
+
} as any,
|
|
221
318
|
peerId: info.id as PeerId,
|
|
222
319
|
});
|
|
223
320
|
},
|
|
@@ -227,10 +324,13 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
227
324
|
this.emit('message', message);
|
|
228
325
|
},
|
|
229
326
|
onClose: async () => {
|
|
230
|
-
peerInfo
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
327
|
+
if (!peerInfo) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.emit('peer-disconnected', {
|
|
331
|
+
peerId: peerInfo.id as PeerId,
|
|
332
|
+
});
|
|
333
|
+
this._extensions.delete(peerInfo.id);
|
|
234
334
|
},
|
|
235
335
|
},
|
|
236
336
|
);
|
|
@@ -238,12 +338,19 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
238
338
|
}
|
|
239
339
|
}
|
|
240
340
|
|
|
241
|
-
class AutomergeStorageAdapter extends StorageAdapter {
|
|
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
|
+
|
|
242
346
|
constructor(private readonly _directory: Directory) {
|
|
243
347
|
super();
|
|
244
348
|
}
|
|
245
349
|
|
|
246
350
|
override async load(key: StorageKey): Promise<Uint8Array | undefined> {
|
|
351
|
+
if (this._state !== 'opened') {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
247
354
|
const filename = this._getFilename(key);
|
|
248
355
|
const file = this._directory.getOrCreateFile(filename);
|
|
249
356
|
const { size } = await file.stat();
|
|
@@ -255,6 +362,9 @@ class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
255
362
|
}
|
|
256
363
|
|
|
257
364
|
override async save(key: StorageKey, data: Uint8Array): Promise<void> {
|
|
365
|
+
if (this._state !== 'opened') {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
258
368
|
const filename = this._getFilename(key);
|
|
259
369
|
const file = this._directory.getOrCreateFile(filename);
|
|
260
370
|
await file.write(0, arrayToBuffer(data));
|
|
@@ -264,13 +374,19 @@ class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
264
374
|
}
|
|
265
375
|
|
|
266
376
|
override async remove(key: StorageKey): Promise<void> {
|
|
377
|
+
if (this._state !== 'opened') {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
267
380
|
// TODO(dmaretskyi): Better deletion.
|
|
268
381
|
const filename = this._getFilename(key);
|
|
269
382
|
const file = this._directory.getOrCreateFile(filename);
|
|
270
|
-
await file.
|
|
383
|
+
await file.destroy();
|
|
271
384
|
}
|
|
272
385
|
|
|
273
386
|
override async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
|
|
387
|
+
if (this._state !== 'opened') {
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
274
390
|
const filename = this._getFilename(keyPrefix);
|
|
275
391
|
const entries = await this._directory.list();
|
|
276
392
|
return Promise.all(
|
|
@@ -289,18 +405,25 @@ class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
289
405
|
}
|
|
290
406
|
|
|
291
407
|
override async removeRange(keyPrefix: StorageKey): Promise<void> {
|
|
408
|
+
if (this._state !== 'opened') {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
292
411
|
const filename = this._getFilename(keyPrefix);
|
|
293
412
|
const entries = await this._directory.list();
|
|
294
413
|
await Promise.all(
|
|
295
414
|
entries
|
|
296
415
|
.filter((entry) => entry.startsWith(filename))
|
|
297
416
|
.map(async (entry): Promise<void> => {
|
|
298
|
-
const file = this._directory.getOrCreateFile(
|
|
299
|
-
await file.
|
|
417
|
+
const file = this._directory.getOrCreateFile(entry);
|
|
418
|
+
await file.destroy();
|
|
300
419
|
}),
|
|
301
420
|
);
|
|
302
421
|
}
|
|
303
422
|
|
|
423
|
+
async close(): Promise<void> {
|
|
424
|
+
this._state = 'closed';
|
|
425
|
+
}
|
|
426
|
+
|
|
304
427
|
private _getFilename(key: StorageKey): string {
|
|
305
428
|
return key.map((k) => k.replaceAll('%', '%25').replaceAll('-', '%2D')).join('-');
|
|
306
429
|
}
|
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 }),
|