@dxos/echo-pipeline 0.4.10-main.fd4f2a3 → 0.4.10-main.fd8ea31

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 (68) hide show
  1. package/dist/lib/browser/{chunk-RA6MLCZM.mjs → chunk-KMWJLYEQ.mjs} +68 -35
  2. package/dist/lib/browser/chunk-KMWJLYEQ.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +305 -231
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +10 -2
  7. package/dist/lib/browser/testing/index.mjs.map +4 -4
  8. package/dist/lib/node/{chunk-KGIYLJBT.cjs → chunk-YZA42CKA.cjs} +70 -37
  9. package/dist/lib/node/chunk-YZA42CKA.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +327 -256
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +20 -13
  14. package/dist/lib/node/testing/index.cjs.map +4 -4
  15. package/dist/types/src/automerge/automerge-host.d.ts +19 -17
  16. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  17. package/dist/types/src/automerge/automerge-repo.test.d.ts +2 -0
  18. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -0
  19. package/dist/types/src/automerge/index.d.ts +1 -0
  20. package/dist/types/src/automerge/index.d.ts.map +1 -1
  21. package/dist/types/src/automerge/level.test.d.ts +2 -0
  22. package/dist/types/src/automerge/level.test.d.ts.map +1 -0
  23. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +30 -0
  24. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -0
  25. package/dist/types/src/automerge/local-host-network-adapter.d.ts +8 -1
  26. package/dist/types/src/automerge/local-host-network-adapter.d.ts.map +1 -1
  27. package/dist/types/src/automerge/migrations.d.ts +7 -0
  28. package/dist/types/src/automerge/migrations.d.ts.map +1 -0
  29. package/dist/types/src/automerge/reference.d.ts +1 -1
  30. package/dist/types/src/automerge/reference.d.ts.map +1 -1
  31. package/dist/types/src/automerge/storage-adapter.test.d.ts +2 -0
  32. package/dist/types/src/automerge/storage-adapter.test.d.ts.map +1 -0
  33. package/dist/types/src/automerge/types.d.ts +6 -0
  34. package/dist/types/src/automerge/types.d.ts.map +1 -1
  35. package/dist/types/src/metadata/metadata-store.d.ts +2 -1
  36. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  37. package/dist/types/src/space/control-pipeline.d.ts +3 -1
  38. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  39. package/dist/types/src/space/space-manager.d.ts +3 -1
  40. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  41. package/dist/types/src/space/space.d.ts +2 -1
  42. package/dist/types/src/space/space.d.ts.map +1 -1
  43. package/dist/types/src/testing/index.d.ts +1 -0
  44. package/dist/types/src/testing/index.d.ts.map +1 -1
  45. package/dist/types/src/testing/level.d.ts +3 -0
  46. package/dist/types/src/testing/level.d.ts.map +1 -0
  47. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  48. package/package.json +33 -30
  49. package/src/automerge/automerge-host.test.ts +22 -8
  50. package/src/automerge/automerge-host.ts +53 -114
  51. package/src/automerge/automerge-repo.test.ts +29 -0
  52. package/src/automerge/index.ts +1 -0
  53. package/src/automerge/level.test.ts +82 -0
  54. package/src/automerge/leveldb-storage-adapter.ts +117 -0
  55. package/src/automerge/local-host-network-adapter.ts +19 -13
  56. package/src/automerge/migrations.ts +41 -0
  57. package/src/automerge/reference.ts +1 -1
  58. package/src/automerge/storage-adapter.test.ts +90 -0
  59. package/src/automerge/types.ts +6 -3
  60. package/src/metadata/metadata-store.ts +17 -8
  61. package/src/space/control-pipeline.ts +11 -1
  62. package/src/space/space-manager.ts +4 -0
  63. package/src/space/space.ts +11 -1
  64. package/src/testing/index.ts +1 -0
  65. package/src/testing/level.ts +11 -0
  66. package/src/testing/test-agent-builder.ts +1 -0
  67. package/dist/lib/browser/chunk-RA6MLCZM.mjs.map +0 -7
  68. package/dist/lib/node/chunk-KGIYLJBT.cjs.map +0 -7
@@ -6,7 +6,6 @@ import { Trigger } from '@dxos/async';
6
6
  import { NetworkAdapter, type Message, type PeerId, cbor } from '@dxos/automerge/automerge-repo';
7
7
  import { Stream } from '@dxos/codec-protobuf';
8
8
  import { invariant } from '@dxos/invariant';
9
- import { log } from '@dxos/log';
10
9
  import { type HostInfo, type SyncRepoRequest, type SyncRepoResponse } from '@dxos/protocols/proto/dxos/echo/service';
11
10
 
12
11
  type ClientSyncState = {
@@ -32,10 +31,17 @@ export class LocalHostNetworkAdapter extends NetworkAdapter {
32
31
  });
33
32
  }
34
33
 
35
- private _connected = new Trigger();
34
+ private readonly _connected = new Trigger();
35
+ private _isConnected: boolean = false;
36
36
 
37
+ /**
38
+ * Called by `Repo` to connect to the network.
39
+ *
40
+ * @param peerId Our peer Id.
41
+ */
37
42
  override connect(peerId: PeerId): void {
38
43
  this.peerId = peerId;
44
+ this._isConnected = true;
39
45
  this._connected.wake();
40
46
  // No-op. Client always connects first
41
47
  }
@@ -56,6 +62,10 @@ export class LocalHostNetworkAdapter extends NetworkAdapter {
56
62
  // No-op
57
63
  }
58
64
 
65
+ async whenConnected(): Promise<void> {
66
+ await this._connected.wait({ timeout: 10_000 });
67
+ }
68
+
59
69
  syncRepo({ id, syncMessage }: SyncRepoRequest): Stream<SyncRepoResponse> {
60
70
  const peerId = this._getPeerId(id);
61
71
 
@@ -77,26 +87,22 @@ export class LocalHostNetworkAdapter extends NetworkAdapter {
77
87
  },
78
88
  });
79
89
 
80
- this._connected
81
- .wait({ timeout: 1_000 })
82
- .then(() => {
83
- this.emit('peer-candidate', {
84
- peerMetadata: {},
85
- peerId,
86
- });
87
- })
88
- .catch((err) => log.catch(err));
90
+ invariant(this._isConnected);
91
+ this.emit('peer-candidate', {
92
+ peerMetadata: {},
93
+ peerId,
94
+ });
89
95
  });
90
96
  }
91
97
 
92
98
  async sendSyncMessage({ id, syncMessage }: SyncRepoRequest): Promise<void> {
93
- await this._connected.wait({ timeout: 1_000 });
99
+ invariant(this._isConnected);
94
100
  const message = cbor.decode(syncMessage!) as Message;
95
101
  this.emit('message', message);
96
102
  }
97
103
 
98
104
  async getHostInfo(): Promise<HostInfo> {
99
- await this._connected.wait({ timeout: 1_000 });
105
+ invariant(this._isConnected);
100
106
  invariant(this.peerId, 'Peer id not set.');
101
107
  return {
102
108
  peerId: this.peerId,
@@ -0,0 +1,41 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type StorageKey } from '@dxos/automerge/automerge-repo';
6
+ import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
7
+ import { log } from '@dxos/log';
8
+ import { StorageType, type Directory } from '@dxos/random-access-storage';
9
+
10
+ import { AutomergeStorageAdapter } from './automerge-storage-adapter';
11
+ import { encodingOptions } from './leveldb-storage-adapter';
12
+ import { type SubLevelDB } from './types';
13
+
14
+ export const levelMigration = async ({ db, directory }: { db: SubLevelDB; directory: Directory }) => {
15
+ // Note: Make automigration from previous storage to leveldb here.
16
+ const isNewLevel = !(await db
17
+ .iterator<StorageKey, Uint8Array>({
18
+ ...encodingOptions,
19
+ })
20
+ .next());
21
+
22
+ if (!isNewLevel) {
23
+ return;
24
+ }
25
+
26
+ const oldStorageAdapter =
27
+ directory.type === StorageType.IDB
28
+ ? new IndexedDBStorageAdapter(directory.path, 'data')
29
+ : new AutomergeStorageAdapter(directory);
30
+
31
+ const chunks = await oldStorageAdapter.loadRange([]);
32
+ if (chunks.length === 0) {
33
+ return;
34
+ }
35
+ const batch = db.batch();
36
+ log.info('found chunks on old storage adapter', { chunks: chunks.length });
37
+ for (const { key, data } of await oldStorageAdapter.loadRange([])) {
38
+ data && batch.put<StorageKey, Uint8Array>(key, data, { ...encodingOptions });
39
+ }
40
+ await batch.write();
41
+ };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Reference } from '@dxos/echo-db';
5
+ import { Reference } from '@dxos/echo-schema';
6
6
 
7
7
  export const REFERENCE_TYPE_TAG = 'dxos.echo.model.document.Reference';
8
8
 
@@ -0,0 +1,90 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { type StorageAdapterInterface } from '@dxos/automerge/automerge-repo';
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { StorageType, createStorage } from '@dxos/random-access-storage';
10
+ import { afterTest, describe, openAndClose, test } from '@dxos/test';
11
+ import { type MaybePromise } from '@dxos/util';
12
+
13
+ import { AutomergeStorageAdapter } from './automerge-storage-adapter';
14
+ import { LevelDBStorageAdapter } from './leveldb-storage-adapter';
15
+ import { createTestLevel } from '../testing';
16
+
17
+ const runTests = (
18
+ testNamespace: string,
19
+ /** Run per test. Expects automatic clean-up with `afterTest`. */ createAdapter: () => MaybePromise<StorageAdapterInterface>,
20
+ ) => {
21
+ describe(testNamespace, () => {
22
+ const chunks = [
23
+ { key: ['a', 'b', 'c', '1'], data: PublicKey.random().asUint8Array() },
24
+ { key: ['a', 'b', 'c', '2'], data: PublicKey.random().asUint8Array() },
25
+ { key: ['a', 'b', 'd', '3'], data: PublicKey.random().asUint8Array() },
26
+ { key: ['a', 'b', 'd', '4'], data: PublicKey.random().asUint8Array() },
27
+ ];
28
+
29
+ test('should store and retrieve data', async () => {
30
+ const adapter = await createAdapter();
31
+
32
+ await adapter.save(chunks[0].key, chunks[0].data);
33
+ expect(await adapter.load(chunks[0].key)).to.deep.equal(chunks[0].data);
34
+ });
35
+
36
+ test('loadRange return inputs with correct prefixes', async () => {
37
+ const adapter = await createAdapter();
38
+
39
+ for (const chunk of chunks) {
40
+ await adapter.save(chunk.key, chunk.data);
41
+ }
42
+
43
+ expect((await adapter.loadRange(['a', 'b'])).length).to.equal(4);
44
+ expect((await adapter.loadRange(['a', 'b', 'c']))[0]).to.deep.equal(chunks[0]);
45
+ expect((await adapter.loadRange(['a', 'b', 'c'])).length).to.equal(2);
46
+ });
47
+
48
+ test('deletion works', async () => {
49
+ const adapter = await createAdapter();
50
+
51
+ for (const chunk of chunks) {
52
+ await adapter.save(chunk.key, chunk.data);
53
+ }
54
+
55
+ await adapter.remove(['a', 'b', 'c', '1']);
56
+
57
+ expect((await adapter.loadRange(['a', 'b'])).length).to.equal(3);
58
+ expect((await adapter.loadRange(['a', 'b', 'c'])).length).to.equal(1);
59
+
60
+ await adapter.removeRange(['a', 'b', 'd']);
61
+
62
+ expect((await adapter.loadRange(['a', 'b'])).length).to.equal(1);
63
+ expect((await adapter.loadRange(['a', 'b']))[0]).to.deep.equal(chunks[1]);
64
+ expect(await adapter.load(['a', 'b', 'c', '2'])).to.deep.equal(chunks[1].data);
65
+ expect(await adapter.load(['a', 'b', 'd', '3'])).to.be.undefined;
66
+ });
67
+ });
68
+ };
69
+
70
+ /**
71
+ * Run tests for AutomergeStorageAdapter.
72
+ */
73
+ runTests('AutomergeStorageAdapter', () => {
74
+ const storage = createStorage({ type: StorageType.RAM });
75
+ afterTest(() => storage.close());
76
+ const dir = storage.createDirectory('automerge');
77
+ const adapter = new AutomergeStorageAdapter(dir);
78
+ afterTest(() => adapter.close());
79
+ return adapter;
80
+ });
81
+
82
+ /**
83
+ * Run tests for LevelDBStorageAdapter.
84
+ */
85
+ runTests('LevelDBStorageAdapter', async () => {
86
+ const level = createTestLevel();
87
+ const adapter = new LevelDBStorageAdapter({ db: level.sublevel('automerge') });
88
+ await openAndClose(level, adapter as any);
89
+ return adapter;
90
+ });
@@ -2,9 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- //
6
- // Copyright 2023 DXOS.org
7
- //
5
+ import { type AbstractChainedBatch, type AbstractSublevel } from 'abstract-level';
6
+ import { type Level } from 'level';
8
7
 
9
8
  import { type EncodedReferenceObject } from './reference';
10
9
 
@@ -81,3 +80,7 @@ export type ObjectSystem = {
81
80
  */
82
81
  type?: EncodedReferenceObject;
83
82
  };
83
+
84
+ export type LevelDB = Level<string, string>;
85
+ export type SubLevelDB = AbstractSublevel<any, string | Buffer | Uint8Array, string, string>;
86
+ export type BatchLevel = AbstractChainedBatch<any, string, string>;
@@ -11,7 +11,7 @@ import { invariant } from '@dxos/invariant';
11
11
  import { PublicKey } from '@dxos/keys';
12
12
  import { log } from '@dxos/log';
13
13
  import { DataCorruptionError, STORAGE_VERSION, schema } from '@dxos/protocols';
14
- import { type Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
14
+ import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
15
15
  import {
16
16
  type ControlPipelineSnapshot,
17
17
  EchoMetadata,
@@ -174,13 +174,8 @@ export class MetadataStore {
174
174
  scheduleTaskInterval(
175
175
  this._invitationCleanupCtx,
176
176
  async () => {
177
- for (const invitation of this.getInvitations()) {
178
- if (
179
- invitation.created &&
180
- invitation.lifetime &&
181
- invitation.lifetime !== 0 &&
182
- invitation.created.getTime() + invitation.lifetime * 1000 < Date.now()
183
- ) {
177
+ for (const invitation of this._metadata.invitations ?? []) {
178
+ if (hasInvitationExpired(invitation) || isLegacyInvitationFormat(invitation)) {
184
179
  await this.removeInvitation(invitation.invitationId);
185
180
  }
186
181
  }
@@ -343,3 +338,17 @@ export class MetadataStore {
343
338
  }
344
339
 
345
340
  const fromBytesInt32 = (buf: Buffer) => buf.readInt32LE(0);
341
+
342
+ export const hasInvitationExpired = (invitation: Invitation): boolean => {
343
+ return Boolean(
344
+ invitation.created &&
345
+ invitation.lifetime &&
346
+ invitation.lifetime !== 0 &&
347
+ invitation.created.getTime() + invitation.lifetime * 1000 < Date.now(),
348
+ );
349
+ };
350
+
351
+ // TODO: remove once "multiuse" type invitations get removed from local metadata of existing profiles
352
+ const isLegacyInvitationFormat = (invitation: Invitation): boolean => {
353
+ return invitation.type === Invitation.Type.MULTIUSE;
354
+ };
@@ -4,7 +4,13 @@
4
4
 
5
5
  import { DeferredTask, sleepWithContext, trackLeaks } from '@dxos/async';
6
6
  import { Context } from '@dxos/context';
7
- import { SpaceStateMachine, type SpaceState, type MemberInfo, type FeedInfo } from '@dxos/credentials';
7
+ import {
8
+ SpaceStateMachine,
9
+ type SpaceState,
10
+ type MemberInfo,
11
+ type FeedInfo,
12
+ type DelegateInvitationCredential,
13
+ } from '@dxos/credentials';
8
14
  import { type FeedWrapper } from '@dxos/feed-store';
9
15
  import { PublicKey } from '@dxos/keys';
10
16
  import { log } from '@dxos/log';
@@ -50,6 +56,8 @@ export class ControlPipeline {
50
56
  public readonly onFeedAdmitted = new Callback<AsyncCallback<FeedInfo>>();
51
57
  public readonly onMemberAdmitted: Callback<AsyncCallback<MemberInfo>>;
52
58
  public readonly onCredentialProcessed: Callback<AsyncCallback<Credential>>;
59
+ public readonly onDelegatedInvitation: Callback<AsyncCallback<DelegateInvitationCredential>>;
60
+ public readonly onDelegatedInvitationRemoved: Callback<AsyncCallback<DelegateInvitationCredential>>;
53
61
 
54
62
  @trace.metricsCounter()
55
63
  private _usage = new TimeUsageCounter();
@@ -92,6 +100,8 @@ export class ControlPipeline {
92
100
 
93
101
  this.onMemberAdmitted = this._spaceStateMachine.onMemberAdmitted;
94
102
  this.onCredentialProcessed = this._spaceStateMachine.onCredentialProcessed;
103
+ this.onDelegatedInvitation = this._spaceStateMachine.onDelegatedInvitation;
104
+ this.onDelegatedInvitationRemoved = this._spaceStateMachine.onDelegatedInvitationRemoved;
95
105
  }
96
106
 
97
107
  get spaceState(): SpaceState {
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  import { synchronized, trackLeaks } from '@dxos/async';
6
+ import { type DelegateInvitationCredential } from '@dxos/credentials';
6
7
  import { failUndefined } from '@dxos/debug';
7
8
  import { type FeedStore } from '@dxos/feed-store';
8
9
  import { PublicKey } from '@dxos/keys';
@@ -42,6 +43,7 @@ export type ConstructSpaceParams = {
42
43
  */
43
44
  onAuthorizedConnection: (session: Teleport) => void;
44
45
  onAuthFailure?: (session: Teleport) => void;
46
+ onDelegatedInvitationStatusChange: (invitation: DelegateInvitationCredential, isActive: boolean) => Promise<void>;
45
47
  };
46
48
 
47
49
  /**
@@ -84,6 +86,7 @@ export class SpaceManager {
84
86
  swarmIdentity,
85
87
  onAuthorizedConnection,
86
88
  onAuthFailure,
89
+ onDelegatedInvitationStatusChange,
87
90
  memberKey,
88
91
  }: ConstructSpaceParams) {
89
92
  log.trace('dxos.echo.space-manager.construct-space', trace.begin({ id: this._instanceId }));
@@ -111,6 +114,7 @@ export class SpaceManager {
111
114
  metadataStore: this._metadataStore,
112
115
  snapshotManager,
113
116
  memberKey,
117
+ onDelegatedInvitationStatusChange,
114
118
  });
115
119
  this._spaces.set(space.key, space);
116
120
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Event, Mutex, synchronized, trackLeaks } from '@dxos/async';
6
6
  import { Resource, type Context, LifecycleState } from '@dxos/context';
7
- import { type FeedInfo } from '@dxos/credentials';
7
+ import { type FeedInfo, type DelegateInvitationCredential } from '@dxos/credentials';
8
8
  import { type FeedOptions, type FeedWrapper } from '@dxos/feed-store';
9
9
  import { invariant } from '@dxos/invariant';
10
10
  import { type PublicKey } from '@dxos/keys';
@@ -35,6 +35,8 @@ export type SpaceParams = {
35
35
 
36
36
  // TODO(dmaretskyi): Superseded by epochs.
37
37
  snapshotId?: string | undefined;
38
+
39
+ onDelegatedInvitationStatusChange: (invitation: DelegateInvitationCredential, isActive: boolean) => Promise<void>;
38
40
  };
39
41
 
40
42
  export type CreatePipelineParams = {
@@ -99,6 +101,14 @@ export class Space extends Resource {
99
101
  log('onCredentialProcessed', { credential });
100
102
  this.stateUpdate.emit();
101
103
  });
104
+ this._controlPipeline.onDelegatedInvitation.set(async (invitation) => {
105
+ log('onDelegatedInvitation', { invitation });
106
+ await params.onDelegatedInvitationStatusChange(invitation, true);
107
+ });
108
+ this._controlPipeline.onDelegatedInvitationRemoved.set(async (invitation) => {
109
+ log('onDelegatedInvitationRemoved', { invitation });
110
+ await params.onDelegatedInvitationStatusChange(invitation, false);
111
+ });
102
112
 
103
113
  // Start replicating the genesis feed.
104
114
  this.protocol = params.protocol;
@@ -5,3 +5,4 @@
5
5
  export * from './change-metadata';
6
6
  export * from './test-agent-builder';
7
7
  export * from './test-feed-builder';
8
+ export * from './level';
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Level } from 'level';
6
+
7
+ import { PublicKey } from '@dxos/keys';
8
+
9
+ import { type LevelDB } from '../automerge/types';
10
+
11
+ export const createTestLevel = (): LevelDB => new Level<string, string>(`/tmp/dxos-${PublicKey.random().toHex()}`);
@@ -190,6 +190,7 @@ export class TestAgent {
190
190
  this.createGossip().createExtension({ remotePeerId: session.remotePeerId }),
191
191
  );
192
192
  },
193
+ onDelegatedInvitationStatusChange: async () => {},
193
194
  });
194
195
  await space.setControlFeed(controlFeed);
195
196
  await space.setDataFeed(dataFeed);