@dxos/echo-pipeline 0.1.24-next.6b1f434

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 (134) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +22 -0
  3. package/dist/lib/browser/chunk-3GAE5CKD.mjs +1621 -0
  4. package/dist/lib/browser/chunk-3GAE5CKD.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +114 -0
  6. package/dist/lib/browser/index.mjs.map +7 -0
  7. package/dist/lib/browser/meta.json +1 -0
  8. package/dist/lib/browser/testing/index.mjs +238 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/node/index.cjs +1747 -0
  11. package/dist/lib/node/index.cjs.map +7 -0
  12. package/dist/lib/node/meta.json +1 -0
  13. package/dist/lib/node/testing/index.cjs +1835 -0
  14. package/dist/lib/node/testing/index.cjs.map +7 -0
  15. package/dist/types/src/common/codec.d.ts +12 -0
  16. package/dist/types/src/common/codec.d.ts.map +1 -0
  17. package/dist/types/src/common/feeds.d.ts +7 -0
  18. package/dist/types/src/common/feeds.d.ts.map +1 -0
  19. package/dist/types/src/common/index.d.ts +3 -0
  20. package/dist/types/src/common/index.d.ts.map +1 -0
  21. package/dist/types/src/dbhost/data-service-host.d.ts +22 -0
  22. package/dist/types/src/dbhost/data-service-host.d.ts.map +1 -0
  23. package/dist/types/src/dbhost/data-service.d.ts +21 -0
  24. package/dist/types/src/dbhost/data-service.d.ts.map +1 -0
  25. package/dist/types/src/dbhost/database-backend.d.ts +28 -0
  26. package/dist/types/src/dbhost/database-backend.d.ts.map +1 -0
  27. package/dist/types/src/dbhost/index.d.ts +6 -0
  28. package/dist/types/src/dbhost/index.d.ts.map +1 -0
  29. package/dist/types/src/dbhost/snapshot-manager.d.ts +17 -0
  30. package/dist/types/src/dbhost/snapshot-manager.d.ts.map +1 -0
  31. package/dist/types/src/dbhost/snapshot-store.d.ts +9 -0
  32. package/dist/types/src/dbhost/snapshot-store.d.ts.map +1 -0
  33. package/dist/types/src/dbhost/snapshot-store.test.d.ts +2 -0
  34. package/dist/types/src/dbhost/snapshot-store.test.d.ts.map +1 -0
  35. package/dist/types/src/errors/errors.d.ts +26 -0
  36. package/dist/types/src/errors/errors.d.ts.map +1 -0
  37. package/dist/types/src/errors/index.d.ts +2 -0
  38. package/dist/types/src/errors/index.d.ts.map +1 -0
  39. package/dist/types/src/index.d.ts +7 -0
  40. package/dist/types/src/index.d.ts.map +1 -0
  41. package/dist/types/src/metadata/index.d.ts +2 -0
  42. package/dist/types/src/metadata/index.d.ts.map +1 -0
  43. package/dist/types/src/metadata/metadata-store.d.ts +42 -0
  44. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -0
  45. package/dist/types/src/pipeline/index.d.ts +3 -0
  46. package/dist/types/src/pipeline/index.d.ts.map +1 -0
  47. package/dist/types/src/pipeline/message-selector.d.ts +11 -0
  48. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -0
  49. package/dist/types/src/pipeline/pipeline.d.ts +98 -0
  50. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -0
  51. package/dist/types/src/pipeline/pipeline.test.d.ts +2 -0
  52. package/dist/types/src/pipeline/pipeline.test.d.ts.map +1 -0
  53. package/dist/types/src/pipeline/timeframe-clock.d.ts +19 -0
  54. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -0
  55. package/dist/types/src/space/auth.d.ts +23 -0
  56. package/dist/types/src/space/auth.d.ts.map +1 -0
  57. package/dist/types/src/space/control-pipeline.d.ts +31 -0
  58. package/dist/types/src/space/control-pipeline.d.ts.map +1 -0
  59. package/dist/types/src/space/control-pipeline.test.d.ts +2 -0
  60. package/dist/types/src/space/control-pipeline.test.d.ts.map +1 -0
  61. package/dist/types/src/space/data-pipeline-controller.d.ts +63 -0
  62. package/dist/types/src/space/data-pipeline-controller.d.ts.map +1 -0
  63. package/dist/types/src/space/genesis.d.ts +5 -0
  64. package/dist/types/src/space/genesis.d.ts.map +1 -0
  65. package/dist/types/src/space/index.d.ts +7 -0
  66. package/dist/types/src/space/index.d.ts.map +1 -0
  67. package/dist/types/src/space/replication.browser.test.d.ts +2 -0
  68. package/dist/types/src/space/replication.browser.test.d.ts.map +1 -0
  69. package/dist/types/src/space/space-manager.browser.test.d.ts +2 -0
  70. package/dist/types/src/space/space-manager.browser.test.d.ts.map +1 -0
  71. package/dist/types/src/space/space-manager.d.ts +41 -0
  72. package/dist/types/src/space/space-manager.d.ts.map +1 -0
  73. package/dist/types/src/space/space-protocol.browser.test.d.ts +2 -0
  74. package/dist/types/src/space/space-protocol.browser.test.d.ts.map +1 -0
  75. package/dist/types/src/space/space-protocol.d.ts +76 -0
  76. package/dist/types/src/space/space-protocol.d.ts.map +1 -0
  77. package/dist/types/src/space/space-protocol.test.d.ts +2 -0
  78. package/dist/types/src/space/space-protocol.test.d.ts.map +1 -0
  79. package/dist/types/src/space/space.d.ts +59 -0
  80. package/dist/types/src/space/space.d.ts.map +1 -0
  81. package/dist/types/src/space/space.test.d.ts +2 -0
  82. package/dist/types/src/space/space.test.d.ts.map +1 -0
  83. package/dist/types/src/testing/index.d.ts +4 -0
  84. package/dist/types/src/testing/index.d.ts.map +1 -0
  85. package/dist/types/src/testing/test-agent-builder.d.ts +52 -0
  86. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -0
  87. package/dist/types/src/testing/test-feed-builder.d.ts +9 -0
  88. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -0
  89. package/dist/types/src/testing/util.d.ts +14 -0
  90. package/dist/types/src/testing/util.d.ts.map +1 -0
  91. package/dist/types/src/tests/database.test.d.ts +2 -0
  92. package/dist/types/src/tests/database.test.d.ts.map +1 -0
  93. package/package.json +60 -0
  94. package/src/common/codec.ts +18 -0
  95. package/src/common/feeds.ts +23 -0
  96. package/src/common/index.ts +6 -0
  97. package/src/dbhost/data-service-host.ts +103 -0
  98. package/src/dbhost/data-service.ts +66 -0
  99. package/src/dbhost/database-backend.ts +64 -0
  100. package/src/dbhost/index.ts +9 -0
  101. package/src/dbhost/snapshot-manager.ts +68 -0
  102. package/src/dbhost/snapshot-store.test.ts +30 -0
  103. package/src/dbhost/snapshot-store.ts +45 -0
  104. package/src/errors/errors.ts +52 -0
  105. package/src/errors/index.ts +5 -0
  106. package/src/index.ts +10 -0
  107. package/src/metadata/index.ts +5 -0
  108. package/src/metadata/metadata-store.ts +173 -0
  109. package/src/pipeline/index.ts +6 -0
  110. package/src/pipeline/message-selector.ts +38 -0
  111. package/src/pipeline/pipeline.test.ts +71 -0
  112. package/src/pipeline/pipeline.ts +249 -0
  113. package/src/pipeline/timeframe-clock.ts +56 -0
  114. package/src/space/auth.ts +88 -0
  115. package/src/space/control-pipeline.test.ts +162 -0
  116. package/src/space/control-pipeline.ts +103 -0
  117. package/src/space/data-pipeline-controller.ts +234 -0
  118. package/src/space/genesis.ts +29 -0
  119. package/src/space/index.ts +10 -0
  120. package/src/space/replication.browser.test.ts +64 -0
  121. package/src/space/space-manager.browser.test.ts +44 -0
  122. package/src/space/space-manager.ts +92 -0
  123. package/src/space/space-protocol.browser.test.ts +135 -0
  124. package/src/space/space-protocol.test.ts +136 -0
  125. package/src/space/space-protocol.ts +222 -0
  126. package/src/space/space.test.ts +144 -0
  127. package/src/space/space.ts +199 -0
  128. package/src/testing/index.ts +7 -0
  129. package/src/testing/test-agent-builder.ts +193 -0
  130. package/src/testing/test-feed-builder.ts +19 -0
  131. package/src/testing/util.ts +77 -0
  132. package/src/tests/database.test.ts +53 -0
  133. package/testing.d.ts +11 -0
  134. package/testing.js +5 -0
@@ -0,0 +1,173 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ import assert from 'node:assert';
6
+
7
+ import { synchronized } from '@dxos/async';
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { log } from '@dxos/log';
10
+ import { schema } from '@dxos/protocols';
11
+ import { EchoMetadata, SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
12
+ import { IdentityRecord } from '@dxos/protocols/proto/dxos/halo/credentials';
13
+ import { Directory } from '@dxos/random-access-storage';
14
+ import { Timeframe } from '@dxos/timeframe';
15
+
16
+ /**
17
+ * Version for the schema of the stored data as defined in dxos.echo.metadata.EchoMetadata.
18
+ *
19
+ * Should be incremented every time there's a breaking change to the stored data.
20
+ */
21
+ export const STORAGE_VERSION = 1;
22
+
23
+ export interface AddSpaceOptions {
24
+ key: PublicKey;
25
+ genesisFeed: PublicKey;
26
+ }
27
+
28
+ const emptyEchoMetadata = (): EchoMetadata => ({
29
+ version: STORAGE_VERSION,
30
+ spaces: [],
31
+ created: new Date(),
32
+ updated: new Date()
33
+ });
34
+
35
+ export class MetadataStore {
36
+ private _metadata: EchoMetadata = emptyEchoMetadata();
37
+
38
+ // prettier-ignore
39
+ constructor(
40
+ private readonly _directory: Directory
41
+ ) {}
42
+
43
+ get version(): number {
44
+ return this._metadata.version ?? 0;
45
+ }
46
+
47
+ /**
48
+ * Returns a list of currently saved spaces. The list and objects in it can be modified addSpace and
49
+ * addSpaceFeed functions.
50
+ */
51
+ get spaces(): SpaceMetadata[] {
52
+ return this._metadata.spaces ?? [];
53
+ }
54
+
55
+ /**
56
+ * Loads metadata from persistent storage.
57
+ */
58
+ @synchronized
59
+ async load(): Promise<void> {
60
+ const file = this._directory.getOrCreateFile('EchoMetadata');
61
+ try {
62
+ const { size: fileLength } = await file.stat();
63
+ if (fileLength < 4) {
64
+ return;
65
+ }
66
+ // Loading file size from first 4 bytes.
67
+ const dataSize = fromBytesInt32(await file.read(0, 4));
68
+ log('loaded', { size: dataSize });
69
+
70
+ // Sanity check.
71
+ {
72
+ if (fileLength < dataSize + 4) {
73
+ throw new Error('Metadata storage is corrupted');
74
+ }
75
+ }
76
+
77
+ const data = await file.read(4, dataSize);
78
+ this._metadata = schema.getCodecForType('dxos.echo.metadata.EchoMetadata').decode(data);
79
+ } catch (err: any) {
80
+ log.error('failed to load metadata', { err });
81
+ this._metadata = emptyEchoMetadata();
82
+ } finally {
83
+ await file.close();
84
+ }
85
+ }
86
+
87
+ @synchronized
88
+ private async _save(): Promise<void> {
89
+ const data: EchoMetadata = {
90
+ ...this._metadata,
91
+ version: STORAGE_VERSION,
92
+ created: this._metadata.created ?? new Date(),
93
+ updated: new Date()
94
+ };
95
+
96
+ const file = this._directory.getOrCreateFile('EchoMetadata');
97
+
98
+ try {
99
+ const encoded = Buffer.from(schema.getCodecForType('dxos.echo.metadata.EchoMetadata').encode(data));
100
+
101
+ // Saving file size at first 4 bytes.
102
+ await file.write(0, toBytesInt32(encoded.length));
103
+ log('saved', { size: encoded.length });
104
+
105
+ // Saving data.
106
+ await file.write(4, encoded);
107
+ } finally {
108
+ await file.close();
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Clears storage - doesn't work for now.
114
+ */
115
+ async clear(): Promise<void> {
116
+ log('clearing all metadata');
117
+ await this._directory.delete();
118
+ }
119
+
120
+ getIdentityRecord(): IdentityRecord | undefined {
121
+ return this._metadata.identity;
122
+ }
123
+
124
+ async setIdentityRecord(record: IdentityRecord) {
125
+ assert(!this._metadata.identity, 'Cannot overwrite existing identity in metadata');
126
+
127
+ this._metadata.identity = record;
128
+ await this._save();
129
+ }
130
+
131
+ async addSpace(record: SpaceMetadata) {
132
+ assert(
133
+ !(this._metadata.spaces ?? []).find((space) => space.key === record.key),
134
+ 'Cannot overwrite existing space in metadata'
135
+ );
136
+
137
+ (this._metadata.spaces ??= []).push(record);
138
+ await this._save();
139
+ }
140
+
141
+ async setSpaceLatestTimeframe(spaceKey: PublicKey, timeframe: Timeframe) {
142
+ const space = this.spaces.find((space) => space.key === spaceKey);
143
+ assert(space, 'Space not found');
144
+
145
+ space.dataTimeframe = timeframe;
146
+ await this._save();
147
+ }
148
+
149
+ async setSpaceSnapshot(spaceKey: PublicKey, snapshot: string) {
150
+ const space = this.spaces.find((space) => space.key === spaceKey);
151
+ assert(space, 'Space not found');
152
+
153
+ space.snapshot = snapshot;
154
+ await this._save();
155
+ }
156
+
157
+ async setWritableFeedKeys(spaceKey: PublicKey, controlFeedKey: PublicKey, dataFeedKey: PublicKey) {
158
+ const space = this.spaces.find((space) => space.key === spaceKey);
159
+ assert(space, 'Space not found');
160
+
161
+ space.controlFeedKey = controlFeedKey;
162
+ space.dataFeedKey = dataFeedKey;
163
+ await this._save();
164
+ }
165
+ }
166
+
167
+ const toBytesInt32 = (num: number) => {
168
+ const buf = Buffer.alloc(4);
169
+ buf.writeInt32LE(num, 0);
170
+ return buf;
171
+ };
172
+
173
+ const fromBytesInt32 = (buf: Buffer) => buf.readInt32LE(0);
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export { Pipeline, PipelineAccessor } from './pipeline';
6
+ export * from './timeframe-clock';
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2020 DXOS.org
3
+ //
4
+
5
+ import debug from 'debug';
6
+ import assert from 'node:assert';
7
+
8
+ import { FeedBlock, FeedBlockSelector } from '@dxos/feed-store';
9
+ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
10
+
11
+ import { TimeframeClock } from './timeframe-clock';
12
+
13
+ const log = debug('dxos:echo-db:message-selector');
14
+
15
+ /**
16
+ * The MessageSelector makes sure that we read in a trusted order.
17
+ * The first message we wish to process is the SpaceGenesis, which will admit a Feed.
18
+ * As we encounter and process FeedAdmit messages those are added to the Space's trust,
19
+ * and we begin processing messages from them as well.
20
+ */
21
+ export const createMessageSelector = (timeframeClock: TimeframeClock): FeedBlockSelector<FeedMessage> => {
22
+ return (messages: FeedBlock<FeedMessage>[]) => {
23
+ // Pick the first candidate with a valid timeframe that has no gaps.
24
+ for (let i = 0; i < messages.length; i++) {
25
+ const {
26
+ data: { timeframe }
27
+ } = messages[i];
28
+ assert(timeframe);
29
+
30
+ if (!timeframeClock.hasGaps(timeframe)) {
31
+ return i;
32
+ }
33
+ }
34
+
35
+ // Not ready for this message yet.
36
+ log('Skipping...');
37
+ };
38
+ };
@@ -0,0 +1,71 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { checkType } from '@dxos/debug';
6
+ import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
7
+ import { describe, test, afterTest } from '@dxos/test';
8
+ import { Timeframe } from '@dxos/timeframe';
9
+ import { range } from '@dxos/util';
10
+
11
+ import { TestFeedBuilder } from '../testing';
12
+ import { Pipeline } from './pipeline';
13
+
14
+ describe('pipeline/Pipeline', () => {
15
+ test('asynchronous reader & writer without ordering', async () => {
16
+ const pipeline = new Pipeline(new Timeframe());
17
+ afterTest(() => pipeline.stop());
18
+
19
+ const builder = new TestFeedBuilder();
20
+ const feedStore = builder.createFeedStore();
21
+
22
+ // Remote feeds from other peers.
23
+ const numFeeds = 5;
24
+ const messagesPerFeed = 10;
25
+ for (const feedIdx in range(numFeeds)) {
26
+ const key = await builder.keyring.createKey();
27
+ const feed = await feedStore.openFeed(key, { writable: true });
28
+ void pipeline.addFeed(feed);
29
+
30
+ setTimeout(async () => {
31
+ for (const msgIdx in range(messagesPerFeed)) {
32
+ await feed.append(
33
+ checkType<FeedMessage>({
34
+ timeframe: new Timeframe(),
35
+ payload: {
36
+ data: {
37
+ object: {
38
+ objectId: `${feedIdx}-${msgIdx}`
39
+ }
40
+ }
41
+ }
42
+ })
43
+ );
44
+ }
45
+ });
46
+ }
47
+
48
+ // Local feed.
49
+ const key = await builder.keyring.createKey();
50
+ const feed = await feedStore.openFeed(key, { writable: true });
51
+ void pipeline.addFeed(feed);
52
+ pipeline.setWriteFeed(feed);
53
+
54
+ for (const msgIdx in range(messagesPerFeed)) {
55
+ await pipeline.writer!.write({
56
+ data: {
57
+ object: {
58
+ objectId: `local-${msgIdx}`
59
+ }
60
+ }
61
+ });
62
+ }
63
+
64
+ let msgCount = 0;
65
+ for await (const _ of pipeline.consume()) {
66
+ if (++msgCount === numFeeds * messagesPerFeed) {
67
+ void pipeline.stop();
68
+ }
69
+ }
70
+ });
71
+ });
@@ -0,0 +1,249 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import assert from 'node:assert';
6
+
7
+ import { sleep, Trigger } from '@dxos/async';
8
+ import { FeedSetIterator, FeedWrapper, FeedWriter } from '@dxos/feed-store';
9
+ import { PublicKey } from '@dxos/keys';
10
+ import { log } from '@dxos/log';
11
+ import { FeedMessageBlock } from '@dxos/protocols';
12
+ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
13
+ import { Timeframe } from '@dxos/timeframe';
14
+
15
+ import { createMappedFeedWriter } from '../common';
16
+ import { createMessageSelector } from './message-selector';
17
+ import { mapFeedIndexesToTimeframe, mapTimeframeToFeedIndexes, TimeframeClock } from './timeframe-clock';
18
+
19
+ export type WaitUntilReachedTargetParams = {
20
+ timeout?: number;
21
+ };
22
+
23
+ /**
24
+ * External state accessor.
25
+ */
26
+ export class PipelineState {
27
+ // TODO(dmaretskyi): Remove?.
28
+ public readonly timeframeUpdate = this._timeframeClock.update;
29
+
30
+ /**
31
+ * Target timeframe we are waiting to reach.
32
+ */
33
+ private _targetTimeframe: Timeframe | undefined;
34
+
35
+ private _reachedTargetPromise: Promise<void> | undefined;
36
+
37
+ // prettier-ignore
38
+ constructor(
39
+ private _iterator: FeedSetIterator<any>,
40
+ private _timeframeClock: TimeframeClock
41
+ ) { }
42
+
43
+ /**
44
+ * Latest theoretical timeframe based on the last mutation in each feed.
45
+ * NOTE: This might never be reached if the mutation dependencies
46
+ */
47
+ get endTimeframe() {
48
+ return mapFeedIndexesToTimeframe(
49
+ this._iterator.feeds
50
+ .filter((feed) => feed.properties.length > 0)
51
+ .map((feed) => ({
52
+ feedKey: feed.key,
53
+ index: feed.properties.length - 1
54
+ }))
55
+ );
56
+ }
57
+
58
+ get timeframe() {
59
+ return this._timeframeClock.timeframe;
60
+ }
61
+
62
+ get targetTimeframe() {
63
+ return this._targetTimeframe ? Timeframe.merge(this.endTimeframe, this._targetTimeframe) : this.endTimeframe;
64
+ }
65
+
66
+ async waitUntilTimeframe(target: Timeframe) {
67
+ await this._timeframeClock.waitUntilReached(target);
68
+ }
69
+
70
+ setTargetTimeframe(target: Timeframe) {
71
+ this._targetTimeframe = target;
72
+ }
73
+
74
+ /**
75
+ * Wait until the pipeline processes all messages in the feed and reaches the target timeframe if that is set.
76
+ *
77
+ * This function will resolve immediately if the pipeline is stalled.
78
+ *
79
+ * @param timeout Timeout in milliseconds to specify the maximum wait time.
80
+ */
81
+ async waitUntilReachedTargetTimeframe({ timeout }: WaitUntilReachedTargetParams = {}) {
82
+ log('waitUntilReachedTargetTimeframe', {
83
+ timeout,
84
+ current: this.timeframe,
85
+ target: this.targetTimeframe
86
+ });
87
+
88
+ this._reachedTargetPromise ??= Promise.race([
89
+ this._timeframeClock.update.waitForCondition(() => {
90
+ return Timeframe.dependencies(this.targetTimeframe, this.timeframe).isEmpty();
91
+ }),
92
+ this._iterator.stalled.discardParameter().waitForCount(1)
93
+ ]);
94
+
95
+ let done = false;
96
+
97
+ if (timeout) {
98
+ return Promise.race([
99
+ this._reachedTargetPromise.then(() => {
100
+ done = true;
101
+ }),
102
+ sleep(timeout).then(() => {
103
+ if (done) {
104
+ return;
105
+ }
106
+
107
+ log.warn('waitUntilReachedTargetTimeframe timed out', {
108
+ timeout,
109
+ current: this.timeframe,
110
+ target: this.targetTimeframe,
111
+ dependencies: Timeframe.dependencies(this.targetTimeframe, this.timeframe)
112
+ });
113
+ })
114
+ ]);
115
+ } else {
116
+ return this._reachedTargetPromise;
117
+ }
118
+ }
119
+ }
120
+
121
+ export interface PipelineAccessor {
122
+ state: PipelineState;
123
+ writer: FeedWriter<FeedMessage.Payload>;
124
+ }
125
+
126
+ /**
127
+ * A multi-reader pipeline that operates on feeds.
128
+ * Might have a single writable feed.
129
+ * Has a timeframe clock to handle message ordering.
130
+ *
131
+ * NOTE:
132
+ * - Feeds passed in must have value encoding consistent with the type expected by the iterator/writer.
133
+ *
134
+ * # Usage examples
135
+ *
136
+ * ## Create a new space.
137
+ *
138
+ * 1. Generate space key, genesis feed key.
139
+ * 2. Create and open pipeline reading from {}.
140
+ * 3. Create and add the writable genesis feed.
141
+ * 4. Write the initial sequence of control and credential messages.
142
+ *
143
+ * ## Load an existing space from storage
144
+ *
145
+ * 1. Load space key, genesis feed key, get starting timeframe from saved snapshot.
146
+ * 2. Create and open pipeline reading from the initial timeframe.
147
+ * 3. Open and add the genesis feed.
148
+ * 4. Once the writable feed is added, the pipeline becomes writable.
149
+ *
150
+ * ## Join an existing space created by another agent/device.
151
+ *
152
+ * 1. Get the space key, genesis feed key from another agent.
153
+ * 2. (optionally) Download the snapshot from another agent.
154
+ * 3. Create and open pipeline.
155
+ * 4. Generate the writable feed key.
156
+ * 5. Wait for the writable feed to be added.
157
+ */
158
+ export class Pipeline implements PipelineAccessor {
159
+ private readonly _timeframeClock = new TimeframeClock(this._initialTimeframe);
160
+
161
+ // Inbound feed stream.
162
+ private readonly _feedSetIterator = new FeedSetIterator<FeedMessage>(createMessageSelector(this._timeframeClock), {
163
+ start: mapTimeframeToFeedIndexes(this._initialTimeframe),
164
+ stallTimeout: 1000
165
+ });
166
+
167
+ // External state accessor.
168
+ private readonly _state: PipelineState = new PipelineState(this._feedSetIterator, this._timeframeClock);
169
+
170
+ // Waits for the message consumer to process the message and yield control back to the pipeline.
171
+ private readonly _processingTrigger = new Trigger().wake();
172
+
173
+ // Outbound feed writer.
174
+ private _writer: FeedWriter<FeedMessage.Payload> | undefined;
175
+
176
+ private _isOpen = false;
177
+
178
+ // prettier-ignore
179
+ constructor(
180
+ private readonly _initialTimeframe: Timeframe = new Timeframe()
181
+ ) {
182
+ this._feedSetIterator.stalled.on((iterator) => {
183
+ log.warn(`Stalled after ${iterator.options.stallTimeout}ms with ${iterator.size} feeds.`);
184
+ });
185
+ }
186
+
187
+ get state() {
188
+ return this._state;
189
+ }
190
+
191
+ get writer(): FeedWriter<FeedMessage.Payload> {
192
+ assert(this._writer, 'Writer not set.');
193
+ return this._writer;
194
+ }
195
+
196
+ getFeeds(): FeedWrapper<FeedMessage>[] {
197
+ return this._feedSetIterator.feeds;
198
+ }
199
+
200
+ async addFeed(feed: FeedWrapper<FeedMessage>) {
201
+ await this._feedSetIterator.addFeed(feed);
202
+ }
203
+
204
+ setWriteFeed(feed: FeedWrapper<FeedMessage>) {
205
+ assert(!this._writer, 'Writer already set.');
206
+ assert(feed.properties.writable, 'Feed must be writable.');
207
+
208
+ this._writer = createMappedFeedWriter<FeedMessage.Payload, FeedMessage>(
209
+ (payload: FeedMessage.Payload) => ({
210
+ timeframe: this._timeframeClock.timeframe,
211
+ payload
212
+ }),
213
+ feed.createFeedWriter()
214
+ );
215
+ }
216
+
217
+ async start() {
218
+ log('starting...', {});
219
+ await this._feedSetIterator.open();
220
+ log('started');
221
+ }
222
+
223
+ async stop() {
224
+ log('stopping...', {});
225
+ await this._feedSetIterator.close();
226
+ await this._processingTrigger.wait(); // Wait for the in-flight message to be processed.
227
+ log('stopped');
228
+ }
229
+
230
+ /**
231
+ * Starts to iterate over the ordered messages from the added feeds.
232
+ * Updates the timeframe clock after the message has bee processed.
233
+ */
234
+ async *consume(): AsyncIterable<FeedMessageBlock> {
235
+ assert(!this._isOpen, 'Pipeline is already being consumed.');
236
+ this._isOpen = true;
237
+
238
+ for await (const block of this._feedSetIterator) {
239
+ this._processingTrigger.reset();
240
+ yield block;
241
+ this._processingTrigger.wake();
242
+
243
+ this._timeframeClock.updateTimeframe(PublicKey.from(block.feedKey), block.seq);
244
+ }
245
+
246
+ // TODO(burdon): Test re-entrant?
247
+ this._isOpen = false;
248
+ }
249
+ }
@@ -0,0 +1,56 @@
1
+ //
2
+ // Copyright 2020 DXOS.org
3
+ //
4
+
5
+ import { Event } from '@dxos/async';
6
+ import { timed } from '@dxos/debug';
7
+ import { FeedIndex } from '@dxos/feed-store';
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { log } from '@dxos/log';
10
+ import { Timeframe } from '@dxos/timeframe';
11
+
12
+ export const mapTimeframeToFeedIndexes = (timeframe: Timeframe): FeedIndex[] =>
13
+ timeframe.frames().map(([feedKey, index]) => ({ feedKey, index }));
14
+
15
+ export const mapFeedIndexesToTimeframe = (indexes: FeedIndex[]): Timeframe =>
16
+ new Timeframe(indexes.map(({ feedKey, index }) => [feedKey, index]));
17
+
18
+ /**
19
+ * Keeps state of the last timeframe that was processed by ECHO.
20
+ */
21
+ export class TimeframeClock {
22
+ readonly update = new Event<Timeframe>();
23
+
24
+ // prettier-ignore
25
+ constructor(
26
+ private _timeframe = new Timeframe()
27
+ ) {}
28
+
29
+ get timeframe() {
30
+ return this._timeframe;
31
+ }
32
+
33
+ updateTimeframe(key: PublicKey, seq: number) {
34
+ this._timeframe = Timeframe.merge(this._timeframe, new Timeframe([[key, seq]]));
35
+ this.update.emit(this._timeframe);
36
+ }
37
+
38
+ hasGaps(timeframe: Timeframe) {
39
+ const gaps = Timeframe.dependencies(timeframe, this._timeframe);
40
+ return !gaps.isEmpty();
41
+ }
42
+
43
+ @timed(5_000)
44
+ async waitUntilReached(target: Timeframe) {
45
+ log.debug('waitUntilReached', { target, current: this._timeframe });
46
+ await this.update.waitForCondition(() => {
47
+ log('check if reached', {
48
+ target,
49
+ current: this._timeframe,
50
+ deps: Timeframe.dependencies(target, this._timeframe)
51
+ });
52
+
53
+ return Timeframe.dependencies(target, this._timeframe).isEmpty();
54
+ });
55
+ }
56
+ }
@@ -0,0 +1,88 @@
1
+ //
2
+ // Copyright 2019 DXOS.org
3
+ //
4
+
5
+ import assert from 'node:assert';
6
+
7
+ import { runInContext, scheduleTask } from '@dxos/async';
8
+ import { Context } from '@dxos/context';
9
+ import { randomBytes } from '@dxos/crypto';
10
+ import { log } from '@dxos/log';
11
+ import { schema } from '@dxos/protocols';
12
+ import { AuthService } from '@dxos/protocols/proto/dxos/mesh/teleport/auth';
13
+ import { ExtensionContext, RpcExtension } from '@dxos/teleport';
14
+
15
+ export type AuthProvider = (nonce: Uint8Array) => Promise<Uint8Array | undefined>;
16
+
17
+ export type AuthVerifier = (nonce: Uint8Array, credential: Uint8Array) => Promise<boolean>;
18
+
19
+ export type AuthExtensionParams = {
20
+ provider: AuthProvider;
21
+ verifier: AuthVerifier;
22
+ onAuthSuccess: () => void;
23
+ onAuthFailure: () => void;
24
+ };
25
+
26
+ export class AuthExtension extends RpcExtension<Services, Services> {
27
+ private readonly _ctx = new Context({
28
+ onError: (err) => {
29
+ log.catch(err);
30
+ }
31
+ });
32
+
33
+ constructor(private readonly _authParams: AuthExtensionParams) {
34
+ super({
35
+ requested: {
36
+ AuthService: schema.getService('dxos.mesh.teleport.auth.AuthService')
37
+ },
38
+ exposed: {
39
+ AuthService: schema.getService('dxos.mesh.teleport.auth.AuthService')
40
+ },
41
+ timeout: 60 * 1000 // Long timeout because auth can wait for sync in certain cases.
42
+ });
43
+ }
44
+
45
+ protected async getHandlers(): Promise<Services> {
46
+ return {
47
+ AuthService: {
48
+ authenticate: async ({ challenge }) => {
49
+ try {
50
+ const credential = await this._authParams.provider(challenge);
51
+ if (!credential) {
52
+ throw new Error('auth rejected');
53
+ }
54
+ return { credential };
55
+ } catch (err) {
56
+ log.error('failed to generate auth credentials', err);
57
+ throw new Error('auth rejected');
58
+ }
59
+ }
60
+ }
61
+ };
62
+ }
63
+
64
+ override async onOpen(context: ExtensionContext): Promise<void> {
65
+ await super.onOpen(context);
66
+ scheduleTask(this._ctx, async () => {
67
+ try {
68
+ const challenge = randomBytes(32);
69
+ const { credential } = await this.rpc.AuthService.authenticate({ challenge });
70
+ assert(credential?.length > 0, 'invalid credential');
71
+ const success = await this._authParams.verifier(challenge, credential);
72
+ assert(success, 'credential not verified');
73
+ runInContext(this._ctx, () => this._authParams.onAuthSuccess());
74
+ } catch (err) {
75
+ log.warn('auth failed', err);
76
+ this.close();
77
+ this._authParams.onAuthFailure();
78
+ }
79
+ });
80
+ }
81
+
82
+ override async onClose(): Promise<void> {
83
+ await this._ctx.dispose();
84
+ await super.onClose();
85
+ }
86
+ }
87
+
88
+ type Services = { AuthService: AuthService };