@dxos/echo-pipeline 0.4.10-main.0302e13 → 0.4.10-main.05dbd64

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 (74) hide show
  1. package/dist/lib/browser/{chunk-RTEEJ723.mjs → chunk-KMWJLYEQ.mjs} +77 -57
  2. package/dist/lib/browser/chunk-KMWJLYEQ.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +368 -246
  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-7VZVCCNF.cjs → chunk-YZA42CKA.cjs} +82 -62
  9. package/dist/lib/node/chunk-YZA42CKA.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +391 -270
  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-doc-loader.d.ts +2 -0
  16. package/dist/types/src/automerge/automerge-doc-loader.d.ts.map +1 -1
  17. package/dist/types/src/automerge/automerge-host.d.ts +21 -18
  18. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  19. package/dist/types/src/automerge/automerge-repo.test.d.ts +2 -0
  20. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -0
  21. package/dist/types/src/automerge/index.d.ts +2 -0
  22. package/dist/types/src/automerge/index.d.ts.map +1 -1
  23. package/dist/types/src/automerge/level.test.d.ts +2 -0
  24. package/dist/types/src/automerge/level.test.d.ts.map +1 -0
  25. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +30 -0
  26. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -0
  27. package/dist/types/src/automerge/local-host-network-adapter.d.ts +8 -1
  28. package/dist/types/src/automerge/local-host-network-adapter.d.ts.map +1 -1
  29. package/dist/types/src/automerge/migrations.d.ts +7 -0
  30. package/dist/types/src/automerge/migrations.d.ts.map +1 -0
  31. package/dist/types/src/automerge/reference.d.ts +15 -0
  32. package/dist/types/src/automerge/reference.d.ts.map +1 -0
  33. package/dist/types/src/automerge/storage-adapter.test.d.ts +2 -0
  34. package/dist/types/src/automerge/storage-adapter.test.d.ts.map +1 -0
  35. package/dist/types/src/automerge/types.d.ts +8 -2
  36. package/dist/types/src/automerge/types.d.ts.map +1 -1
  37. package/dist/types/src/metadata/metadata-store.d.ts +2 -1
  38. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  39. package/dist/types/src/space/control-pipeline.d.ts +3 -1
  40. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  41. package/dist/types/src/space/space-manager.d.ts +3 -1
  42. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  43. package/dist/types/src/space/space.d.ts +6 -9
  44. package/dist/types/src/space/space.d.ts.map +1 -1
  45. package/dist/types/src/testing/index.d.ts +1 -0
  46. package/dist/types/src/testing/index.d.ts.map +1 -1
  47. package/dist/types/src/testing/level.d.ts +3 -0
  48. package/dist/types/src/testing/level.d.ts.map +1 -0
  49. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  50. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  51. package/package.json +33 -30
  52. package/src/automerge/automerge-doc-loader.ts +9 -0
  53. package/src/automerge/automerge-host.test.ts +22 -8
  54. package/src/automerge/automerge-host.ts +66 -118
  55. package/src/automerge/automerge-repo.test.ts +29 -0
  56. package/src/automerge/index.ts +2 -0
  57. package/src/automerge/level.test.ts +82 -0
  58. package/src/automerge/leveldb-storage-adapter.ts +117 -0
  59. package/src/automerge/local-host-network-adapter.ts +19 -13
  60. package/src/automerge/migrations.ts +41 -0
  61. package/src/automerge/reference.ts +31 -0
  62. package/src/automerge/storage-adapter.test.ts +90 -0
  63. package/src/automerge/types.ts +8 -5
  64. package/src/db-host/data-service.ts +1 -1
  65. package/src/metadata/metadata-store.ts +17 -8
  66. package/src/space/control-pipeline.ts +11 -1
  67. package/src/space/space-manager.ts +4 -0
  68. package/src/space/space.test.ts +7 -7
  69. package/src/space/space.ts +17 -22
  70. package/src/testing/index.ts +1 -0
  71. package/src/testing/level.ts +11 -0
  72. package/src/testing/test-agent-builder.ts +1 -0
  73. package/dist/lib/browser/chunk-RTEEJ723.mjs.map +0 -7
  74. package/dist/lib/node/chunk-7VZVCCNF.cjs.map +0 -7
@@ -0,0 +1,82 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+ import { Level } from 'level';
7
+
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { describe, openAndClose, test } from '@dxos/test';
10
+
11
+ import { type SubLevelDB } from './types';
12
+ import { createTestLevel } from '../testing';
13
+
14
+ describe('Level', () => {
15
+ test('missing keys', async () => {
16
+ const level = createTestLevel();
17
+ await openAndClose(level);
18
+
19
+ expect(() => level.get('missing')).to.throw;
20
+ });
21
+
22
+ test('data persistance after reload', async () => {
23
+ const path = `/tmp/dxos-${PublicKey.random().toHex()}`;
24
+ const level = new Level<string, string>(path);
25
+ await level.open();
26
+
27
+ const key = 'name';
28
+ const value = 'Rich';
29
+ {
30
+ await level.put(key, value);
31
+ expect(await level.get(key)).to.equal(value);
32
+ }
33
+
34
+ await level.close();
35
+
36
+ {
37
+ const level = new Level<string, string>(path);
38
+ await level.open();
39
+ expect(await level.get(key)).to.equal(value);
40
+ await level.clear();
41
+ expect(() => level.get(key)).to.throw;
42
+ }
43
+ });
44
+
45
+ test('batch different sublevels', async () => {
46
+ const level = createTestLevel();
47
+ await openAndClose(level);
48
+
49
+ const first: SubLevelDB = level.sublevel('first');
50
+ const second: SubLevelDB = level.sublevel('second');
51
+
52
+ const batch = first.batch();
53
+
54
+ const key = 'key';
55
+ const value = 'first-level-value';
56
+ batch.put(key, value, { sublevel: second });
57
+ await batch.write();
58
+
59
+ expect(() => first.get(key)).to.throw;
60
+ expect(await level.sublevel('second').get(key)).to.equal(value);
61
+ expect(await second.get(key)).to.equal(value);
62
+ expect(await second.iterator().all()).to.deep.equal([[key, value]]);
63
+ });
64
+
65
+ test('sublevel prefixes', () => {
66
+ const level = createTestLevel();
67
+ expect(level.prefixKey('', 'utf8')).to.equal('');
68
+
69
+ {
70
+ const sublevel = level.sublevel('sublevel');
71
+ expect(sublevel.prefixKey('', 'utf8')).to.equal('!sublevel!');
72
+ }
73
+
74
+ {
75
+ const sublevel = level.sublevel('');
76
+ expect(sublevel.prefixKey('', 'utf8')).to.equal('!!');
77
+
78
+ const sublevel2 = sublevel.sublevel('');
79
+ expect(sublevel2.prefixKey('', 'utf8')).to.equal('!!!!');
80
+ }
81
+ });
82
+ });
@@ -0,0 +1,117 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ // s
4
+
5
+ import { type MixedEncoding } from 'level-transcoder';
6
+
7
+ import { type StorageAdapterInterface, type Chunk, type StorageKey } from '@dxos/automerge/automerge-repo';
8
+ import { LifecycleState, Resource } from '@dxos/context';
9
+ import { type MaybePromise } from '@dxos/util';
10
+
11
+ import { type BatchLevel, type SubLevelDB } from './types';
12
+
13
+ export type LevelDBStorageAdapterParams = {
14
+ db: SubLevelDB;
15
+ callbacks?: StorageCallbacks;
16
+ };
17
+
18
+ export type BeforeSaveParams = { path: StorageKey; batch: BatchLevel };
19
+
20
+ export interface StorageCallbacks {
21
+ beforeSave(params: BeforeSaveParams): MaybePromise<void>;
22
+ afterSave(path: StorageKey): MaybePromise<void>;
23
+ }
24
+
25
+ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInterface {
26
+ constructor(private readonly _params: LevelDBStorageAdapterParams) {
27
+ super();
28
+ }
29
+
30
+ async load(keyArray: StorageKey): Promise<Uint8Array | undefined> {
31
+ try {
32
+ if (this._lifecycleState !== LifecycleState.OPEN) {
33
+ // TODO(mykola): this should be an error.
34
+ return undefined;
35
+ }
36
+ return await this._params.db.get<StorageKey, Uint8Array>(keyArray, { ...encodingOptions });
37
+ } catch (err: any) {
38
+ if (isLevelDbNotFoundError(err)) {
39
+ return undefined;
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ async save(keyArray: StorageKey, binary: Uint8Array): Promise<void> {
46
+ if (this._lifecycleState !== LifecycleState.OPEN) {
47
+ return undefined;
48
+ }
49
+ const batch = this._params.db.batch();
50
+
51
+ await this._params.callbacks?.beforeSave?.({ path: keyArray, batch });
52
+ batch.put<StorageKey, Uint8Array>(keyArray, Buffer.from(binary), {
53
+ ...encodingOptions,
54
+ });
55
+ await batch.write();
56
+
57
+ await this._params.callbacks?.afterSave?.(keyArray);
58
+ }
59
+
60
+ async remove(keyArray: StorageKey): Promise<void> {
61
+ if (this._lifecycleState !== LifecycleState.OPEN) {
62
+ return undefined;
63
+ }
64
+ await this._params.db.del<StorageKey>(keyArray, { ...encodingOptions });
65
+ }
66
+
67
+ async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
68
+ if (this._lifecycleState !== LifecycleState.OPEN) {
69
+ return [];
70
+ }
71
+ const result: Chunk[] = [];
72
+ for await (const [key, value] of this._params.db.iterator<StorageKey, Uint8Array>({
73
+ gte: keyPrefix,
74
+ lte: [...keyPrefix, '\uffff'],
75
+ ...encodingOptions,
76
+ })) {
77
+ result.push({
78
+ key,
79
+ data: value,
80
+ });
81
+ }
82
+ return result;
83
+ }
84
+
85
+ async removeRange(keyPrefix: StorageKey): Promise<void> {
86
+ if (this._lifecycleState !== LifecycleState.OPEN) {
87
+ return undefined;
88
+ }
89
+ const batch = this._params.db.batch();
90
+
91
+ for await (const [key] of this._params.db.iterator<StorageKey, Uint8Array>({
92
+ gte: keyPrefix,
93
+ lte: [...keyPrefix, '\uffff'],
94
+ ...encodingOptions,
95
+ })) {
96
+ batch.del<StorageKey>(key, { ...encodingOptions });
97
+ }
98
+ await batch.write();
99
+ }
100
+ }
101
+
102
+ const keyEncoder: MixedEncoding<StorageKey, Uint8Array, StorageKey> = {
103
+ encode: (key: StorageKey): Uint8Array =>
104
+ Buffer.from(key.map((k) => k.replaceAll('%', '%25').replaceAll('-', '%2D')).join('-')),
105
+ decode: (key: Uint8Array): StorageKey =>
106
+ Buffer.from(key)
107
+ .toString()
108
+ .split('-')
109
+ .map((k) => k.replaceAll('%2D', '-').replaceAll('%25', '%')),
110
+ };
111
+
112
+ export const encodingOptions = {
113
+ keyEncoding: keyEncoder,
114
+ valueEncoding: 'buffer',
115
+ };
116
+
117
+ const isLevelDbNotFoundError = (err: any): boolean => err.code === 'LEVEL_NOT_FOUND';
@@ -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
+ };
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Reference } from '@dxos/echo-schema';
6
+
7
+ export const REFERENCE_TYPE_TAG = 'dxos.echo.model.document.Reference';
8
+
9
+ /**
10
+ * Reference as it is stored in Automerge document.
11
+ */
12
+ export type EncodedReferenceObject = {
13
+ '@type': typeof REFERENCE_TYPE_TAG;
14
+ itemId: string | null;
15
+ protocol: string | null;
16
+ host: string | null;
17
+ };
18
+
19
+ export const encodeReference = (reference: Reference): EncodedReferenceObject => ({
20
+ '@type': REFERENCE_TYPE_TAG,
21
+ // NOTE: Automerge do not support undefined values, so we need to use null instead.
22
+ itemId: reference.itemId ?? null,
23
+ protocol: reference.protocol ?? null,
24
+ host: reference.host ?? null,
25
+ });
26
+
27
+ export const decodeReference = (value: any) =>
28
+ new Reference(value.itemId, value.protocol ?? undefined, value.host ?? undefined);
29
+
30
+ export const isEncodedReferenceObject = (value: any): value is EncodedReferenceObject =>
31
+ typeof value === 'object' && value !== null && value['@type'] === REFERENCE_TYPE_TAG;
@@ -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,11 +2,10 @@
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
- import { type Reference } from '@dxos/echo-db';
8
+ import { type EncodedReferenceObject } from './reference';
10
9
 
11
10
  export type SpaceState = {
12
11
  // Url of the root automerge document.
@@ -79,5 +78,9 @@ export type ObjectSystem = {
79
78
  /**
80
79
  * Object reference ('protobuf' protocol) type.
81
80
  */
82
- type?: Reference;
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>;
@@ -33,7 +33,7 @@ export class DataServiceImpl implements DataService {
33
33
  }
34
34
 
35
35
  async flush(request: FlushRequest): Promise<void> {
36
- // TODO(dmaretskyi): Implement with automerge.
36
+ await this._automergeHost.flush(request);
37
37
  }
38
38
 
39
39
  // Automerge specific.
@@ -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
 
@@ -20,7 +20,7 @@ describe('space/space', () => {
20
20
  const agent = await builder.createPeer();
21
21
  const space = await agent.createSpace();
22
22
 
23
- await space.open(new Context());
23
+ await space.open(Context.default());
24
24
  expect(space.isOpen).toBeTruthy();
25
25
  afterTest(() => space.close());
26
26
 
@@ -43,7 +43,7 @@ describe('space/space', () => {
43
43
  const agent = await builder.createPeer();
44
44
  const space = await agent.createSpace(agent.identityKey);
45
45
 
46
- await space.open(new Context());
46
+ await space.open(Context.default());
47
47
  expect(space.isOpen).toBeTruthy();
48
48
  afterTest(() => space.close());
49
49
 
@@ -62,7 +62,7 @@ describe('space/space', () => {
62
62
  const agent = await builder.createPeer();
63
63
  const space = await agent.createSpace(agent.identityKey, space1.key, space1.genesisFeedKey, undefined, true);
64
64
 
65
- await space.open(new Context());
65
+ await space.open(Context.default());
66
66
  expect(space.isOpen).toBeTruthy();
67
67
  afterTest(() => space.close());
68
68
 
@@ -114,7 +114,7 @@ describe('space/space', () => {
114
114
  const agent = await builder.createPeer();
115
115
  const space1 = await agent.createSpace();
116
116
 
117
- await space1.open(new Context());
117
+ await space1.open(Context.default());
118
118
  expect(space1.isOpen).toBeTruthy();
119
119
  afterTest(() => space1.close());
120
120
 
@@ -128,7 +128,7 @@ describe('space/space', () => {
128
128
  // Re-open.
129
129
  const space2 = await agent.createSpace(agent.identityKey, space1.key, space1.genesisFeedKey, space1.dataFeedKey);
130
130
 
131
- await space2.open(new Context());
131
+ await space2.open(Context.default());
132
132
  await space2.controlPipeline.state!.waitUntilTimeframe(space2.controlPipeline.state!.endTimeframe);
133
133
  });
134
134
 
@@ -139,7 +139,7 @@ describe('space/space', () => {
139
139
  const space = await agent.createSpace();
140
140
 
141
141
  {
142
- await space.open(new Context());
142
+ await space.open(Context.default());
143
143
  afterTest(() => space.close());
144
144
  expect(space.isOpen).toBeTruthy();
145
145
 
@@ -153,7 +153,7 @@ describe('space/space', () => {
153
153
 
154
154
  // Re-open.
155
155
  {
156
- await space.open(new Context());
156
+ await space.open(Context.default());
157
157
  expect(space.isOpen).toBeTruthy();
158
158
 
159
159
  await space.controlPipeline.state!.waitUntilTimeframe(space.controlPipeline.state!.endTimeframe);