@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.
Files changed (46) hide show
  1. package/dist/lib/browser/{chunk-W3SSYW3X.mjs → chunk-D7UMNYLJ.mjs} +272 -85
  2. package/dist/lib/browser/chunk-D7UMNYLJ.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1 -1
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +33 -11
  6. package/dist/lib/browser/testing/index.mjs.map +4 -4
  7. package/dist/lib/node/{chunk-KTFCZMAY.cjs → chunk-GQW6RLGD.cjs} +267 -83
  8. package/dist/lib/node/chunk-GQW6RLGD.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +26 -26
  10. package/dist/lib/node/index.cjs.map +1 -1
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +46 -25
  13. package/dist/lib/node/testing/index.cjs.map +4 -4
  14. package/dist/types/src/automerge/automerge-host.d.ts +37 -2
  15. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  16. package/dist/types/src/automerge/index.d.ts +1 -1
  17. package/dist/types/src/automerge/index.d.ts.map +1 -1
  18. package/dist/types/src/metadata/metadata-store.d.ts +1 -3
  19. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  20. package/dist/types/src/space/data-pipeline.d.ts.map +1 -1
  21. package/dist/types/src/space/space-manager.d.ts +2 -2
  22. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  23. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  24. package/dist/types/src/space/space.d.ts +1 -1
  25. package/dist/types/src/space/space.d.ts.map +1 -1
  26. package/dist/types/src/testing/change-metadata.d.ts +8 -0
  27. package/dist/types/src/testing/change-metadata.d.ts.map +1 -0
  28. package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
  29. package/dist/types/src/testing/index.d.ts +1 -0
  30. package/dist/types/src/testing/index.d.ts.map +1 -1
  31. package/package.json +33 -33
  32. package/src/automerge/automerge-host.test.ts +319 -34
  33. package/src/automerge/automerge-host.ts +137 -20
  34. package/src/automerge/index.ts +1 -1
  35. package/src/metadata/metadata-store.ts +12 -2
  36. package/src/pipeline/pipeline-stress.test.ts +9 -2
  37. package/src/space/data-pipeline.ts +4 -3
  38. package/src/space/space-manager.ts +3 -3
  39. package/src/space/space-protocol.ts +4 -0
  40. package/src/space/space.ts +8 -3
  41. package/src/testing/change-metadata.ts +27 -0
  42. package/src/testing/database-test-rig.ts +4 -1
  43. package/src/testing/index.ts +1 -0
  44. package/src/testing/test-agent-builder.ts +1 -1
  45. package/dist/lib/browser/chunk-W3SSYW3X.mjs.map +0 -7
  46. 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 { arrayToBuffer, bufferToArray } from '@dxos/util';
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: AutomergeStorageAdapter;
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
- this._storage = new AutomergeStorageAdapter(storageDirectory);
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
- sharePolicy: async (peerId, documentId) => true, // Share everything.
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.emit('peer-candidate', {
144
- peerId,
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
- this.emit('peer-disconnected', {
232
- peerId: peerInfo.id as PeerId,
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.truncate?.(0);
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(filename);
299
- await file.truncate?.(0);
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
  }
@@ -2,4 +2,4 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './automerge-host';
5
+ export { AutomergeHost } from './automerge-host';
@@ -48,7 +48,14 @@ export class MetadataStore {
48
48
 
49
49
  public readonly update = new Event<EchoMetadata>();
50
50
 
51
- constructor(private readonly _directory: Directory) {}
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
- private async _writeFile<T>(file: File, codec: Codec<T>, data: T): Promise<void> {
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(private readonly builder: TestFeedBuilder, public feedStore: FeedStore<FeedMessage>, public id: string) {}
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(public agent: string, public count: number) {}
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(this._pipeline, 'Pipeline is not initialized.');
229
- for await (const msg of this._pipeline.consume()) {
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(this._pipeline.state.pendingTimeframe);
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
- onNetworkConnection: (session: Teleport) => void;
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
- onNetworkConnection,
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: onNetworkConnection,
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>>();
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Event, synchronized, trackLeaks, Lock } from '@dxos/async';
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 _addFeedLock = new Lock();
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._addFeedLock.executeSynchronized(async () => {
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(public readonly rig: DatabaseTestBuilder, public readonly spaceKey: PublicKey) {}
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);
@@ -2,6 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ export * from './change-metadata';
5
6
  export * from './test-agent-builder';
6
7
  export * from './test-feed-builder';
7
8
  export * from './util';
@@ -189,7 +189,7 @@ export class TestAgent {
189
189
  credentialAuthenticator: MOCK_AUTH_VERIFIER,
190
190
  },
191
191
  memberKey: identityKey,
192
- onNetworkConnection: (session) => {
192
+ onAuthorizedConnection: (session) => {
193
193
  session.addExtension(
194
194
  'dxos.mesh.teleport.gossip',
195
195
  this.createGossip().createExtension({ remotePeerId: session.remotePeerId }),