@dxos/echo-pipeline 0.1.56-main.e47dfd1 → 0.1.56-main.e79d64a

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 (55) hide show
  1. package/dist/lib/browser/{chunk-7U2NXI2P.mjs → chunk-GDF4DQU2.mjs} +692 -355
  2. package/dist/lib/browser/chunk-GDF4DQU2.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1 -1
  4. package/dist/lib/browser/index.mjs.map +1 -1
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +14 -18
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/index.cjs +699 -362
  9. package/dist/lib/node/index.cjs.map +3 -3
  10. package/dist/lib/node/meta.json +1 -1
  11. package/dist/lib/node/testing/index.cjs +696 -363
  12. package/dist/lib/node/testing/index.cjs.map +3 -3
  13. package/dist/types/src/common/feeds.d.ts.map +1 -1
  14. package/dist/types/src/db-host/data-service-host.d.ts.map +1 -1
  15. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  16. package/dist/types/src/db-host/snapshot-manager.d.ts.map +1 -1
  17. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  18. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  19. package/dist/types/src/pipeline/pipeline-stress.test.d.ts +2 -0
  20. package/dist/types/src/pipeline/pipeline-stress.test.d.ts.map +1 -0
  21. package/dist/types/src/pipeline/pipeline.d.ts +4 -3
  22. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  23. package/dist/types/src/space/auth.d.ts.map +1 -1
  24. package/dist/types/src/space/control-pipeline.d.ts +4 -0
  25. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  26. package/dist/types/src/space/data-pipeline.d.ts +2 -0
  27. package/dist/types/src/space/data-pipeline.d.ts.map +1 -1
  28. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  29. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  30. package/dist/types/src/space/space.d.ts +2 -1
  31. package/dist/types/src/space/space.d.ts.map +1 -1
  32. package/dist/types/src/testing/database-test-rig.d.ts +4 -1
  33. package/dist/types/src/testing/database-test-rig.d.ts.map +1 -1
  34. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  35. package/package.json +32 -31
  36. package/src/common/feeds.ts +1 -2
  37. package/src/db-host/data-service-host.ts +10 -4
  38. package/src/db-host/data-service.ts +1 -2
  39. package/src/db-host/snapshot-manager.ts +0 -2
  40. package/src/metadata/metadata-store.ts +2 -2
  41. package/src/pipeline/message-selector.ts +1 -2
  42. package/src/pipeline/pipeline-stress.test.ts +226 -0
  43. package/src/pipeline/pipeline.test.ts +30 -206
  44. package/src/pipeline/pipeline.ts +50 -24
  45. package/src/space/auth.ts +1 -2
  46. package/src/space/control-pipeline.ts +47 -22
  47. package/src/space/data-pipeline.ts +21 -12
  48. package/src/space/space-manager.ts +1 -0
  49. package/src/space/space-protocol.ts +3 -1
  50. package/src/space/space.test.ts +18 -17
  51. package/src/space/space.ts +7 -3
  52. package/src/testing/database-test-rig.ts +27 -2
  53. package/src/testing/test-agent-builder.ts +2 -1
  54. package/src/tests/database-unit.test.ts +39 -0
  55. package/dist/lib/browser/chunk-7U2NXI2P.mjs.map +0 -7
@@ -0,0 +1,226 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import expect from 'expect';
6
+ import * as fc from 'fast-check';
7
+ import { inspect } from 'util';
8
+
9
+ import { asyncTimeout } from '@dxos/async';
10
+ import { FeedStore, FeedWrapper } from '@dxos/feed-store';
11
+ import { PublicKey } from '@dxos/keys';
12
+ import { log } from '@dxos/log';
13
+ import { FeedMessageBlock } from '@dxos/protocols';
14
+ import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
15
+ import { describe, test } from '@dxos/test';
16
+ import { Timeframe } from '@dxos/timeframe';
17
+ import { range } from '@dxos/util';
18
+
19
+ import { TestFeedBuilder } from '../testing';
20
+ import { Pipeline } from './pipeline';
21
+
22
+ const NUM_AGENTS = 2;
23
+ const NUM_MESSAGES = 10;
24
+
25
+ // TODO(burdon): Describe test.
26
+ describe('pipeline/stress test', () => {
27
+ test
28
+ .skip('stress', async () => {
29
+ const builder = new TestFeedBuilder();
30
+
31
+ const agentIds = range(NUM_AGENTS).map(() => PublicKey.random().toHex().slice(0, 8));
32
+ const anAgentId = fc.constantFrom(...agentIds);
33
+
34
+ const commands = fc.commands(
35
+ [
36
+ fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
37
+ fc.constant(new SyncCommand()),
38
+ anAgentId.map((agent) => new RestartCommand(agent)),
39
+ ],
40
+ { size: 'large' },
41
+ );
42
+
43
+ const model = fc.asyncProperty(commands, async (commands) => {
44
+ const feedStore = builder.createFeedStore();
45
+
46
+ const agents = new Map(agentIds.map((id) => [id, new Agent(builder, feedStore, id)]));
47
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
48
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
49
+
50
+ const setup: fc.ModelRunSetup<Model, Real> = () => ({
51
+ model: {},
52
+ real: {
53
+ feedStore,
54
+ agents,
55
+ },
56
+ });
57
+
58
+ try {
59
+ await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
60
+ } finally {
61
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
62
+ await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
63
+ }
64
+ });
65
+
66
+ const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
67
+ [[new WriteCommand(agentIds[0], 10), new WriteCommand(agentIds[1], 10), new SyncCommand()]],
68
+ [[new WriteCommand(agentIds[0], 4), new RestartCommand(agentIds[0]), new SyncCommand()]],
69
+ ];
70
+
71
+ await fc.assert(model, { examples });
72
+ })
73
+ .timeout(60_000);
74
+ });
75
+
76
+ class Agent {
77
+ public startingTimeframe = new Timeframe();
78
+ public pipeline!: Pipeline;
79
+ public feed!: FeedWrapper<FeedMessage>;
80
+ public messages: FeedMessageBlock[] = [];
81
+ public writePromise: Promise<any> = Promise.resolve();
82
+
83
+ constructor(private readonly builder: TestFeedBuilder, public feedStore: FeedStore<FeedMessage>, public id: string) {}
84
+
85
+ async open() {
86
+ const key = await this.builder.keyring.createKey();
87
+ this.feed = await this.feedStore.openFeed(key, { writable: true });
88
+ }
89
+
90
+ async start() {
91
+ this.pipeline = new Pipeline();
92
+ await this.pipeline.setCursor(this.startingTimeframe);
93
+ await this.pipeline.start();
94
+
95
+ // NOTE: not awaiting here breaks the test.
96
+ await Promise.all(this.feedStore.feeds.map((feed) => this.pipeline.addFeed(feed)));
97
+ this.pipeline.setWriteFeed(this.feed);
98
+
99
+ // consume in async task.
100
+ void this.consume();
101
+ }
102
+
103
+ async stop() {
104
+ await this.pipeline.stop();
105
+ this.startingTimeframe = this.pipeline.state.timeframe;
106
+ }
107
+
108
+ async close() {
109
+ await this.feed.close();
110
+ }
111
+
112
+ write(message: FeedMessage.Payload) {
113
+ const prev = this.writePromise;
114
+ const promise = this.pipeline.writer!.write(message);
115
+ this.writePromise = Promise.all([prev, promise]);
116
+ }
117
+
118
+ async consume() {
119
+ for await (const msg of this.pipeline.consume()) {
120
+ this.messages.push(msg);
121
+ }
122
+
123
+ log('stopped consuming');
124
+ }
125
+ }
126
+
127
+ type Model = {};
128
+ type Real = {
129
+ feedStore: FeedStore<FeedMessage>;
130
+ agents: Map<string, Agent>;
131
+ };
132
+
133
+ class WriteCommand implements fc.AsyncCommand<Model, Real> {
134
+ constructor(public agent: string, public count: number) {}
135
+
136
+ check = () => true;
137
+
138
+ async run(model: Model, real: Real) {
139
+ // console.log(`WriteCommand(${this.agent}, ${this.count})`);
140
+ const agent = real.agents.get(this.agent)!;
141
+ const toWrite = Math.min(this.count, NUM_MESSAGES - agent.feed.length);
142
+ if (toWrite > 0) {
143
+ for (const _ of range(toWrite)) {
144
+ agent.write({}); // Content is not important.
145
+ }
146
+ }
147
+ }
148
+
149
+ toString = () => `WriteCommand(${this.agent}, ${this.count})`;
150
+ }
151
+
152
+ class SyncCommand implements fc.AsyncCommand<Model, Real> {
153
+ check = () => true;
154
+
155
+ async run(model: Model, real: Real) {
156
+ // console.log('SyncCommand()');
157
+ const targets: any = {};
158
+
159
+ try {
160
+ for (const agent of real.agents.values()) {
161
+ await agent.writePromise;
162
+ }
163
+
164
+ for (const agent of real.agents.values()) {
165
+ targets[agent.id] = agent.pipeline.state.endTimeframe;
166
+ if (agent.pipeline.state.endTimeframe.isEmpty()) {
167
+ log('empty endtimeframe', {
168
+ id: agent.id,
169
+ endTimeframe: agent.pipeline.state.endTimeframe,
170
+ feeds: agent.pipeline.getFeeds().map((feed) => [feed.key.toString(), feed.length]),
171
+ });
172
+ }
173
+ await asyncTimeout(agent.pipeline.state.waitUntilTimeframe(agent.pipeline.state.endTimeframe), 1000);
174
+ }
175
+
176
+ const tf: Timeframe = real.agents.values().next().value!.pipeline.state.timeframe;
177
+ for (const agent of real.agents.values()) {
178
+ expect(agent.pipeline.state.timeframe.equals(tf)).toEqual(true);
179
+ }
180
+
181
+ const totalMessages = real.feedStore.feeds.reduce((acc, feed) => acc + feed.length, 0);
182
+ for (const agent of real.agents.values()) {
183
+ expect(agent.messages.length).toEqual(totalMessages);
184
+ }
185
+ } catch (err) {
186
+ log(
187
+ inspect(
188
+ {
189
+ agents: Array.from(real.agents.values()).map((agent) => ({
190
+ id: agent.id,
191
+ messages: agent.messages.length,
192
+ feeds: agent.pipeline.getFeeds().map((feed) => [feed.key, feed.length]),
193
+ timeframe: agent.pipeline.state.timeframe,
194
+ endTimeframe: agent.pipeline.state.endTimeframe,
195
+ })),
196
+ feeds: real.feedStore.feeds.map((feed) => [feed.key, feed.length]),
197
+ targets,
198
+ },
199
+ false,
200
+ null,
201
+ true,
202
+ ),
203
+ );
204
+ throw err;
205
+ }
206
+ }
207
+
208
+ toString = () => 'SyncCommand()';
209
+ }
210
+
211
+ class RestartCommand implements fc.AsyncCommand<Model, Real> {
212
+ constructor(public agent: string) {}
213
+
214
+ check = () => true;
215
+
216
+ async run(model: Model, real: Real) {
217
+ log(`RestartCommand(${this.agent})`);
218
+
219
+ const agent = real.agents.get(this.agent)!;
220
+ await agent.writePromise;
221
+ await agent.stop();
222
+ await agent.start();
223
+ }
224
+
225
+ toString = () => `RestartCommand(${this.agent})`;
226
+ }
@@ -3,14 +3,8 @@
3
3
  //
4
4
 
5
5
  import expect from 'expect';
6
- import * as fc from 'fast-check';
7
- import { inspect } from 'util';
8
6
 
9
- import { asyncTimeout } from '@dxos/async';
10
- import { FeedStore, FeedWrapper } from '@dxos/feed-store';
11
- import { PublicKey } from '@dxos/keys';
12
- import { log } from '@dxos/log';
13
- import { FeedMessageBlock } from '@dxos/protocols';
7
+ import { Event, sleep } from '@dxos/async';
14
8
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
15
9
  import { describe, test, afterTest } from '@dxos/test';
16
10
  import { Timeframe } from '@dxos/timeframe';
@@ -85,7 +79,7 @@ describe('pipeline/Pipeline', () => {
85
79
 
86
80
  const processedSequenceNumbers: number[] = [];
87
81
 
88
- // skip first 10, process the rest, and the repeat the last 10.
82
+ // Skip first 10, process the rest, and the repeat the last 10.
89
83
  const expectedSequenceNumbers = [...sequenceNumbers.slice(10, 30), ...sequenceNumbers.slice(20, 30)];
90
84
 
91
85
  await pipeline.setCursor(new Timeframe([[feed.key, 9]]));
@@ -109,209 +103,39 @@ describe('pipeline/Pipeline', () => {
109
103
  expect(processedSequenceNumbers).toEqual(expectedSequenceNumbers);
110
104
  });
111
105
 
112
- test
113
- .skip('stress', async () => {
114
- const builder = new TestFeedBuilder();
115
-
116
- const NUM_AGENTS = 2;
117
- const NUM_MESSAGES = 100;
118
-
119
- class Agent {
120
- public startingTimeframe = new Timeframe();
121
- public pipeline!: Pipeline;
122
- public feed!: FeedWrapper<FeedMessage>;
123
- public messages: FeedMessageBlock[] = [];
124
- public writePromise: Promise<any> = Promise.resolve();
125
-
126
- constructor(public id: string, public feedStore: FeedStore<FeedMessage>) {}
127
-
128
- async open() {
129
- const key = await builder.keyring.createKey();
130
- this.feed = await this.feedStore.openFeed(key, { writable: true });
131
- }
132
-
133
- async start() {
134
- this.pipeline = new Pipeline();
135
- await this.pipeline.setCursor(this.startingTimeframe);
136
-
137
- await this.pipeline.start();
138
-
139
- // NOTE: not awaiting here breaks the test.
140
- await Promise.all(this.feedStore.feeds.map((feed) => this.pipeline.addFeed(feed)));
141
- this.pipeline.setWriteFeed(this.feed);
142
-
143
- // consume in async task.
144
- void this.consume();
145
- }
146
-
147
- async stop() {
148
- await this.pipeline.stop();
149
- this.startingTimeframe = this.pipeline.state.timeframe;
150
- }
151
-
152
- async close() {
153
- await this.feed.close();
154
- }
155
-
156
- write(message: FeedMessage.Payload) {
157
- const prev = this.writePromise;
158
- const promise = this.pipeline.writer!.write(message);
159
- this.writePromise = Promise.all([prev, promise]);
160
- }
161
-
162
- async consume() {
163
- for await (const msg of this.pipeline.consume()) {
164
- this.messages.push(msg);
165
- }
166
- log('stopped consuming');
167
- }
168
- }
169
-
170
- type Model = {};
171
- type Real = {
172
- feedStore: FeedStore<FeedMessage>;
173
- agents: Map<string, Agent>;
174
- };
175
-
176
- class WriteCommand implements fc.AsyncCommand<Model, Real> {
177
- constructor(public agent: string, public count: number) {}
178
-
179
- check = () => true;
180
-
181
- async run(model: Model, real: Real) {
182
- console.log(`WriteCommand(${this.agent}, ${this.count})`);
183
-
184
- const agent = real.agents.get(this.agent)!;
185
-
186
- const toWrite = Math.min(this.count, NUM_MESSAGES - agent.feed.length);
187
- if (toWrite > 0) {
188
- for (const _ of range(toWrite)) {
189
- agent.write({}); // Content is not important.
190
- }
191
- }
192
- }
193
-
194
- toString = () => `WriteCommand(${this.agent}, ${this.count})`;
195
- }
196
-
197
- class SyncCommand implements fc.AsyncCommand<Model, Real> {
198
- check = () => true;
199
-
200
- async run(model: Model, real: Real) {
201
- console.log('SyncCommand()');
202
- const targets: any = {};
203
-
204
- try {
205
- for (const agent of real.agents.values()) {
206
- await agent.writePromise;
207
- }
208
-
209
- for (const agent of real.agents.values()) {
210
- targets[agent.id] = agent.pipeline.state.endTimeframe;
211
- if (agent.pipeline.state.endTimeframe.isEmpty()) {
212
- console.log('empty endtimeframe', {
213
- id: agent.id,
214
- endTimeframe: agent.pipeline.state.endTimeframe,
215
- feeds: agent.pipeline.getFeeds().map((feed) => [feed.key.toString(), feed.length]),
216
- });
217
- }
218
- await asyncTimeout(agent.pipeline.state.waitUntilTimeframe(agent.pipeline.state.endTimeframe), 1000);
219
- }
220
-
221
- const tf: Timeframe = real.agents.values().next().value!.pipeline.state.timeframe;
222
- for (const agent of real.agents.values()) {
223
- expect(agent.pipeline.state.timeframe.equals(tf)).toEqual(true);
224
- }
225
-
226
- const totalMessages = real.feedStore.feeds.reduce((acc, feed) => acc + feed.length, 0);
227
- for (const agent of real.agents.values()) {
228
- expect(agent.messages.length).toEqual(totalMessages);
229
- }
230
- } catch (err) {
231
- console.log(
232
- inspect(
233
- {
234
- agents: Array.from(real.agents.values()).map((agent) => ({
235
- id: agent.id,
236
- messages: agent.messages.length,
237
- feeds: agent.pipeline.getFeeds().map((feed) => [feed.key, feed.length]),
238
- timeframe: agent.pipeline.state.timeframe,
239
- endTimeframe: agent.pipeline.state.endTimeframe,
240
- })),
241
- feeds: real.feedStore.feeds.map((feed) => [feed.key, feed.length]),
242
- targets,
243
- },
244
- false,
245
- null,
246
- true,
247
- ),
248
- );
249
- throw err;
250
- }
251
- }
252
-
253
- toString = () => 'SyncCommand()';
254
- }
255
-
256
- class RestartCommand implements fc.AsyncCommand<Model, Real> {
257
- constructor(public agent: string) {}
258
-
259
- check = () => true;
260
-
261
- async run(model: Model, real: Real) {
262
- console.log(`RestartCommand(${this.agent})`);
106
+ test('cursor change while polling', async () => {
107
+ const pipeline = new Pipeline();
108
+ afterTest(() => pipeline.stop());
263
109
 
264
- const agent = real.agents.get(this.agent)!;
110
+ const builder = new TestFeedBuilder();
111
+ const feedStore = builder.createFeedStore();
112
+ const key = await builder.keyring.createKey();
113
+ const feed = await feedStore.openFeed(key, { writable: true });
114
+ await pipeline.addFeed(feed);
265
115
 
266
- await agent.writePromise;
267
- await agent.stop();
268
- await agent.start();
269
- }
116
+ for (const _ of range(20)) {
117
+ await feed.appendWithReceipt(TEST_MESSAGE);
118
+ }
119
+ await feed.clear(0, 10);
270
120
 
271
- toString = () => `RestartCommand(${this.agent})`;
121
+ const processedSequenceNumbers: number[] = [];
122
+ const expectedSequenceNumbers = range(20).slice(10);
123
+ const processedEvent = new Event();
124
+ setTimeout(async () => {
125
+ for await (const block of pipeline.consume()) {
126
+ processedSequenceNumbers.push(block.seq);
127
+ processedEvent.emit();
272
128
  }
129
+ });
273
130
 
274
- const agentIds = range(NUM_AGENTS).map(() => PublicKey.random().toHex().slice(0, 8));
275
- const anAgentId = fc.constantFrom(...agentIds);
276
-
277
- const commands = fc.commands(
278
- [
279
- fc.tuple(anAgentId, fc.integer({ min: 1, max: 10 })).map(([agent, count]) => new WriteCommand(agent, count)),
280
- fc.constant(new SyncCommand()),
281
- anAgentId.map((agent) => new RestartCommand(agent)),
282
- ],
283
- { size: 'large' },
284
- );
285
-
286
- const model = fc.asyncProperty(commands, async (commands) => {
287
- const feedStore = builder.createFeedStore();
288
-
289
- const agents = new Map(agentIds.map((id) => [id, new Agent(id, feedStore)]));
290
- await Promise.all(Array.from(agents.values()).map((agent) => agent.open()));
291
- await Promise.all(Array.from(agents.values()).map((agent) => agent.start()));
292
-
293
- const setup: fc.ModelRunSetup<Model, Real> = () => ({
294
- model: {},
295
- real: {
296
- feedStore,
297
- agents,
298
- },
299
- });
300
-
301
- try {
302
- await fc.asyncModelRun(setup, [...commands, new SyncCommand()]);
303
- } finally {
304
- await Promise.all(Array.from(agents.values()).map((agent) => agent.stop()));
305
- await Promise.all(Array.from(agents.values()).map((agent) => agent.close()));
306
- }
307
- });
131
+ await pipeline.start();
132
+ await sleep(1000);
308
133
 
309
- const examples: [commands: Iterable<fc.AsyncCommand<Model, Real, boolean>>][] = [
310
- [[new WriteCommand(agentIds[0], 10), new WriteCommand(agentIds[1], 10), new SyncCommand()]],
311
- [[new WriteCommand(agentIds[0], 4), new RestartCommand(agentIds[0]), new SyncCommand()]],
312
- ];
134
+ await pipeline.pause();
135
+ await pipeline.setCursor(new Timeframe([[feed.key, 9]]));
136
+ await pipeline.unpause();
313
137
 
314
- await fc.assert(model, { examples });
315
- })
316
- .timeout(60_000);
138
+ await processedEvent.waitForCondition(() => processedSequenceNumbers.length === 10);
139
+ expect(processedSequenceNumbers).toEqual(expectedSequenceNumbers);
140
+ });
317
141
  });
@@ -2,12 +2,11 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
6
-
7
5
  import { Event, sleep, synchronized, Trigger } from '@dxos/async';
8
6
  import { Context, rejectOnDispose } from '@dxos/context';
9
7
  import { failUndefined } from '@dxos/debug';
10
8
  import { FeedSetIterator, FeedWrapper, FeedWriter } from '@dxos/feed-store';
9
+ import { invariant } from '@dxos/invariant';
11
10
  import { PublicKey } from '@dxos/keys';
12
11
  import { log } from '@dxos/log';
13
12
  import { FeedMessageBlock } from '@dxos/protocols';
@@ -205,9 +204,6 @@ export class Pipeline implements PipelineAccessor {
205
204
  private readonly _timeframeClock = new TimeframeClock(new Timeframe());
206
205
  private readonly _feeds = new ComplexMap<PublicKey, FeedWrapper<FeedMessage>>(PublicKey.hash);
207
206
 
208
- // Inbound feed stream.
209
- private _feedSetIterator?: FeedSetIterator<FeedMessage>;
210
-
211
207
  // External state accessor.
212
208
  private readonly _state: PipelineState = new PipelineState(this._feeds, this._timeframeClock);
213
209
 
@@ -215,14 +211,20 @@ export class Pipeline implements PipelineAccessor {
215
211
  private readonly _processingTrigger = new Trigger().wake();
216
212
  private readonly _pauseTrigger = new Trigger().wake();
217
213
 
218
- private _isStopping = false;
219
- private _isStarted = false;
220
- private _isOpen = false;
221
- private _isPaused = false;
214
+ // Pending downloads.
215
+ private readonly _downloads = new ComplexMap<FeedWrapper<FeedMessage>, any>((value) => PublicKey.hash(value.key));
216
+
217
+ // Inbound feed stream.
218
+ private _feedSetIterator?: FeedSetIterator<FeedMessage>;
222
219
 
223
220
  // Outbound feed writer.
224
221
  private _writer: FeedWriter<FeedMessage.Payload> | undefined;
225
222
 
223
+ private _isStopping = false;
224
+ private _isStarted = false;
225
+ private _isBeingConsumed = false;
226
+ private _isPaused = false;
227
+
226
228
  get state() {
227
229
  return this._state;
228
230
  }
@@ -244,10 +246,14 @@ export class Pipeline implements PipelineAccessor {
244
246
  // which might be opening feeds during the mutation processing, which w
245
247
  async addFeed(feed: FeedWrapper<FeedMessage>) {
246
248
  this._feeds.set(feed.key, feed);
249
+
247
250
  if (this._feedSetIterator) {
248
251
  await this._feedSetIterator.addFeed(feed);
249
252
  }
250
- this._setFeedDownloadState(feed);
253
+
254
+ if (this._isStarted && !this._isPaused) {
255
+ this._setFeedDownloadState(feed);
256
+ }
251
257
  }
252
258
 
253
259
  setWriteFeed(feed: FeedWrapper<FeedMessage>) {
@@ -265,17 +271,28 @@ export class Pipeline implements PipelineAccessor {
265
271
 
266
272
  @synchronized
267
273
  async start() {
274
+ invariant(!this._isStarted, 'Pipeline is already started.');
268
275
  log('starting...');
269
276
  await this._initIterator();
270
277
  await this._feedSetIterator!.open();
271
278
  this._isStarted = true;
272
279
  log('started');
280
+
281
+ if (!this._isPaused) {
282
+ for (const feed of this._feeds.values()) {
283
+ this._setFeedDownloadState(feed);
284
+ }
285
+ }
273
286
  }
274
287
 
275
288
  @synchronized
276
289
  async stop() {
277
290
  log('stopping...');
278
291
  this._isStopping = true;
292
+ for (const [feed, handle] of this._downloads.entries()) {
293
+ feed.undownload(handle);
294
+ }
295
+ this._downloads.clear();
279
296
  await this._feedSetIterator?.close();
280
297
  await this._processingTrigger.wait(); // Wait for the in-flight message to be processed.
281
298
  await this._state._ctx.dispose();
@@ -297,10 +314,6 @@ export class Pipeline implements PipelineAccessor {
297
314
  this._timeframeClock.setTimeframe(timeframe);
298
315
 
299
316
  // Cancel downloads of mutations before the cursor.
300
- for (const feed of this._feeds.values()) {
301
- this._setFeedDownloadState(feed);
302
- }
303
-
304
317
  if (this._feedSetIterator) {
305
318
  await this._feedSetIterator.close();
306
319
  await this._initIterator();
@@ -313,7 +326,6 @@ export class Pipeline implements PipelineAccessor {
313
326
  */
314
327
  @synchronized
315
328
  async pause() {
316
- invariant(this._isStarted, 'Pipeline is not open.');
317
329
  if (this._isPaused) {
318
330
  return;
319
331
  }
@@ -325,11 +337,14 @@ export class Pipeline implements PipelineAccessor {
325
337
 
326
338
  @synchronized
327
339
  async unpause() {
328
- invariant(this._isStarted, 'Pipeline is not open.');
329
340
  invariant(this._isPaused, 'Pipeline is not paused.');
330
341
 
331
342
  this._pauseTrigger.wake();
332
343
  this._isPaused = false;
344
+
345
+ for (const feed of this._feeds.values()) {
346
+ this._setFeedDownloadState(feed);
347
+ }
333
348
  }
334
349
 
335
350
  /**
@@ -337,8 +352,8 @@ export class Pipeline implements PipelineAccessor {
337
352
  * Updates the timeframe clock after the message has bee processed.
338
353
  */
339
354
  async *consume(): AsyncIterable<FeedMessageBlock> {
340
- invariant(!this._isOpen, 'Pipeline is already being consumed.');
341
- this._isOpen = true;
355
+ invariant(!this._isBeingConsumed, 'Pipeline is already being consumed.');
356
+ this._isBeingConsumed = true;
342
357
 
343
358
  invariant(this._feedSetIterator, 'Iterator not initialized.');
344
359
  let lastFeedSetIterator = this._feedSetIterator;
@@ -354,6 +369,7 @@ export class Pipeline implements PipelineAccessor {
354
369
  iterable = lastFeedSetIterator[Symbol.asyncIterator]();
355
370
  }
356
371
 
372
+ // Will be canceled when the iterator gets closed.
357
373
  const { done, value } = await iterable.next();
358
374
  if (!done) {
359
375
  const block = value ?? failUndefined();
@@ -366,17 +382,27 @@ export class Pipeline implements PipelineAccessor {
366
382
  }
367
383
 
368
384
  // TODO(burdon): Test re-entrant?
369
- this._isOpen = false;
385
+ this._isBeingConsumed = false;
370
386
  }
371
387
 
372
388
  private _setFeedDownloadState(feed: FeedWrapper<FeedMessage>) {
373
- const timeframe = this._state._startTimeframe;
374
- const seq = timeframe.get(feed.key) ?? 0;
389
+ let handle = this._downloads.get(feed); // TODO(burdon): Always undefined.
390
+ if (handle) {
391
+ feed.undownload(handle);
392
+ }
375
393
 
376
- feed.undownload({ callback: () => log('undownload') });
377
- feed.download({ start: seq + 1, linear: true }).catch((err: Error) => {
378
- log('failed to download feed', { err });
394
+ const timeframe = this._state._startTimeframe;
395
+ const seq = timeframe.get(feed.key) ?? -1;
396
+ log.info('download', { feed: feed.key.truncate(), seq, length: feed.length });
397
+ handle = feed.download({ start: seq + 1, linear: true }, (err: any, data: any) => {
398
+ if (err) {
399
+ // log.error(err); // TODO(burdon): Feed is closed.
400
+ } else {
401
+ log.info('data', data); // TODO(burdon): Never called.
402
+ }
379
403
  });
404
+
405
+ this._downloads.set(feed, handle);
380
406
  }
381
407
 
382
408
  private async _initIterator() {
package/src/space/auth.ts CHANGED
@@ -2,11 +2,10 @@
2
2
  // Copyright 2019 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
6
-
7
5
  import { runInContext, scheduleTask } from '@dxos/async';
8
6
  import { Context } from '@dxos/context';
9
7
  import { randomBytes } from '@dxos/crypto';
8
+ import { invariant } from '@dxos/invariant';
10
9
  import { log } from '@dxos/log';
11
10
  import { schema } from '@dxos/protocols';
12
11
  import { AuthService } from '@dxos/protocols/proto/dxos/mesh/teleport/auth';