stream-demux 9.0.1 → 10.0.0

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.
package/README.md CHANGED
@@ -98,8 +98,5 @@ function wait(duration) {
98
98
 
99
99
  ## Goal
100
100
 
101
- The goal of this module is to efficiently distribute data to a large number of named asynchronous streams while facilitating functional programming patterns which decrease the probability of memory leaks.
102
-
103
- Each stream returned by this module is responsible for picking up its own data from a shared source stream - This means that the stream-demux module doesn't hold any references to streams which it produces via its `stream()` method; this reduces the likelihood of programming mistakes which would lead to memory leaks because streams don't need to be destroyed or cleaned up explicitly.
104
-
105
- The downside to making each stream responsible for consuming its own data is that having a lot of concurrent streams can have a negative impact on performance (especially if there are a lot of idle streams). A goal of stream-demux is to keep that overhead to a minimum.
101
+ The goal of this module is to facilitate functional programming patterns which decrease the probability of memory leaks and race conditions.
102
+ It serves as an alternative to callback-based event handling.
@@ -3,8 +3,8 @@ const ConsumableStream = require('consumable-stream');
3
3
  class DemuxedConsumableStream extends ConsumableStream {
4
4
  constructor(streamDemux, name) {
5
5
  super();
6
- this.name = name;
7
6
  this._streamDemux = streamDemux;
7
+ this.name = name;
8
8
  }
9
9
 
10
10
  createConsumer(timeout) {
package/index.js CHANGED
@@ -1,135 +1,168 @@
1
1
  const WritableConsumableStream = require('writable-consumable-stream');
2
2
  const DemuxedConsumableStream = require('./demuxed-consumable-stream');
3
- const StreamConsumer = require('./stream-consumer');
4
3
 
5
4
  class StreamDemux {
6
5
  constructor() {
7
- this._mainStream = new WritableConsumableStream();
6
+ this.streams = {};
7
+ this._nextConsumerId = 1;
8
+ this.generateConsumerId = () => {
9
+ return this._nextConsumerId++;
10
+ };
8
11
  }
9
12
 
10
13
  write(streamName, value) {
11
- this._mainStream.write({
12
- stream: streamName,
13
- data: {
14
- value,
15
- done: false
16
- }
17
- });
14
+ if (this.streams[streamName]) {
15
+ this.streams[streamName].write(value);
16
+ }
18
17
  }
19
18
 
20
19
  close(streamName, value) {
21
- this._mainStream.write({
22
- stream: streamName,
23
- data: {
24
- value,
25
- done: true
26
- }
27
- });
20
+ if (this.streams[streamName]) {
21
+ this.streams[streamName].close(value);
22
+ }
28
23
  }
29
24
 
30
25
  closeAll(value) {
31
- this._mainStream.close(value);
26
+ for (let stream of Object.values(this.streams)) {
27
+ stream.close(value);
28
+ }
32
29
  }
33
30
 
34
31
  writeToConsumer(consumerId, value) {
35
- this._mainStream.writeToConsumer(consumerId, {
36
- consumerId,
37
- data: {
38
- value,
39
- done: false
32
+ for (let stream of Object.values(this.streams)) {
33
+ if (stream.hasConsumer(consumerId)) {
34
+ return stream.writeToConsumer(consumerId, value);
40
35
  }
41
- });
36
+ }
42
37
  }
43
38
 
44
39
  closeConsumer(consumerId, value) {
45
- this._mainStream.closeConsumer(consumerId, {
46
- consumerId,
47
- data: {
48
- value,
49
- done: true
40
+ for (let stream of Object.values(this.streams)) {
41
+ if (stream.hasConsumer(consumerId)) {
42
+ return stream.closeConsumer(consumerId, value);
50
43
  }
51
- });
44
+ }
52
45
  }
53
46
 
54
47
  getConsumerStats(consumerId) {
55
- return this._mainStream.getConsumerStats(consumerId);
48
+ for (let [streamName, stream] of Object.entries(this.streams)) {
49
+ if (stream.hasConsumer(consumerId)) {
50
+ return {
51
+ ...stream.getConsumerStats(consumerId),
52
+ stream: streamName
53
+ };
54
+ }
55
+ }
56
+ return undefined;
56
57
  }
57
58
 
58
59
  getConsumerStatsList(streamName) {
59
- let consumerList = this._mainStream.getConsumerStatsList();
60
- return consumerList.filter((consumerStats) => {
61
- return consumerStats.stream === streamName;
62
- });
60
+ if (this.streams[streamName]) {
61
+ return this.streams[streamName]
62
+ .getConsumerStatsList()
63
+ .map(
64
+ (stats) => {
65
+ return {
66
+ ...stats,
67
+ stream: streamName
68
+ };
69
+ }
70
+ );
71
+ }
72
+ return [];
63
73
  }
64
74
 
65
75
  getConsumerStatsListAll() {
66
- return this._mainStream.getConsumerStatsList();
76
+ let allStatsList = [];
77
+ for (let streamName of Object.keys(this.streams)) {
78
+ let statsList = this.getConsumerStatsList(streamName);
79
+ for (let stats of statsList) {
80
+ allStatsList.push(stats);
81
+ }
82
+ }
83
+ return allStatsList;
67
84
  }
68
85
 
69
86
  kill(streamName, value) {
70
- let consumerList = this.getConsumerStatsList(streamName);
71
- let len = consumerList.length;
72
- for (let i = 0; i < len; i++) {
73
- this.killConsumer(consumerList[i].id, value);
87
+ if (this.streams[streamName]) {
88
+ this.streams[streamName].kill(value);
74
89
  }
75
90
  }
76
91
 
77
92
  killAll(value) {
78
- this._mainStream.kill(value);
93
+ for (let stream of Object.values(this.streams)) {
94
+ stream.kill(value);
95
+ }
79
96
  }
80
97
 
81
98
  killConsumer(consumerId, value) {
82
- this._mainStream.killConsumer(consumerId, value);
99
+ for (let stream of Object.values(this.streams)) {
100
+ if (stream.hasConsumer(consumerId)) {
101
+ return stream.killConsumer(consumerId, value);
102
+ }
103
+ }
83
104
  }
84
105
 
85
106
  getBackpressure(streamName) {
86
- let consumerList = this.getConsumerStatsList(streamName);
87
- let len = consumerList.length;
88
-
89
- let maxBackpressure = 0;
90
- for (let i = 0; i < len; i++) {
91
- let consumer = consumerList[i];
92
- if (consumer.backpressure > maxBackpressure) {
93
- maxBackpressure = consumer.backpressure;
94
- }
107
+ if (this.streams[streamName]) {
108
+ return this.streams[streamName].getBackpressure();
95
109
  }
96
- return maxBackpressure;
110
+ return 0;
97
111
  }
98
112
 
99
113
  getBackpressureAll() {
100
- return this._mainStream.getBackpressure();
114
+ return Object.values(this.streams).reduce(
115
+ (max, stream) => Math.max(max, stream.getBackpressure()),
116
+ 0
117
+ );
101
118
  }
102
119
 
103
120
  getConsumerBackpressure(consumerId) {
104
- return this._mainStream.getConsumerBackpressure(consumerId);
121
+ for (let stream of Object.values(this.streams)) {
122
+ if (stream.hasConsumer(consumerId)) {
123
+ return stream.getConsumerBackpressure(consumerId);
124
+ }
125
+ }
126
+ return 0;
105
127
  }
106
128
 
107
129
  hasConsumer(streamName, consumerId) {
108
- let consumerStats = this._mainStream.getConsumerStats(consumerId);
109
- return !!consumerStats && consumerStats.stream === streamName;
130
+ if (this.streams[streamName]) {
131
+ return this.streams[streamName].hasConsumer(consumerId);
132
+ }
133
+ return false;
110
134
  }
111
135
 
112
136
  hasConsumerAll(consumerId) {
113
- return this._mainStream.hasConsumer(consumerId);
137
+ return Object.values(this.streams).some(stream => stream.hasConsumer(consumerId));
114
138
  }
115
139
 
116
140
  getConsumerCount(streamName) {
117
- return this.getConsumerStatsList(streamName).length;
141
+ if (this.streams[streamName]) {
142
+ return this.streams[streamName].getConsumerCount();
143
+ }
144
+ return 0;
118
145
  }
119
146
 
120
147
  getConsumerCountAll() {
121
- return this.getConsumerStatsListAll().length;
148
+ return Object.values(this.streams).reduce(
149
+ (sum, stream) => sum + stream.getConsumerCount(),
150
+ 0
151
+ );
122
152
  }
123
153
 
124
- createConsumer(streamName, timeout, usabilityMode) {
125
- return new StreamConsumer(
126
- this._mainStream,
127
- this._mainStream.nextConsumerId++,
128
- this._mainStream.tailNode,
129
- streamName,
130
- timeout,
131
- usabilityMode
132
- );
154
+ createConsumer(streamName, timeout) {
155
+ if (!this.streams[streamName]) {
156
+ this.streams[streamName] = new WritableConsumableStream({
157
+ generateConsumerId: this.generateConsumerId,
158
+ removeConsumerCallback: () => {
159
+ if (!this.getConsumerCount(streamName)) {
160
+ delete this.streams[streamName];
161
+ }
162
+ }
163
+ });
164
+ }
165
+ return this.streams[streamName].createConsumer(timeout);
133
166
  }
134
167
 
135
168
  // Unlike individual consumers, consumable streams support being iterated
@@ -137,6 +170,10 @@ class StreamDemux {
137
170
  stream(streamName) {
138
171
  return new DemuxedConsumableStream(this, streamName);
139
172
  }
173
+
174
+ unstream(streamName) {
175
+ delete this.streams[streamName];
176
+ }
140
177
  }
141
178
 
142
179
  module.exports = StreamDemux;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-demux",
3
- "version": "9.0.1",
3
+ "version": "10.0.0",
4
4
  "description": "A consumable stream demultiplexer.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test/test.js CHANGED
@@ -231,6 +231,102 @@ describe('StreamDemux', () => {
231
231
  assert.equal(receivedPacketsB.length, 10);
232
232
  });
233
233
 
234
+ it('should support writing multiple times within the same call stack and across multiple streams', async () => {
235
+ (async () => {
236
+ for (let i = 0; i < 10; i++) {
237
+ await wait(20);
238
+ for (let j = 0; j < 10; j++) {
239
+ demux.write('hello', 'world' + i + '-' + j);
240
+ }
241
+ }
242
+ demux.close('hello');
243
+ })();
244
+
245
+ (async () => {
246
+ for (let i = 0; i < 10; i++) {
247
+ await wait(10);
248
+ for (let j = 0; j < 5; j++) {
249
+ demux.write('other', 'message' + i + '-' + j);
250
+ }
251
+ }
252
+ demux.close('other');
253
+ })();
254
+
255
+ let substream = demux.stream('hello');
256
+ let otherSubstream = demux.stream('other');
257
+
258
+ let otherReceivedPackets = [];
259
+ (async () => {
260
+ for await (let otherPacket of otherSubstream) {
261
+ await wait(10);
262
+ otherReceivedPackets.push(otherPacket);
263
+ }
264
+ })();
265
+
266
+ (async () => {
267
+ for await (let otherPacket of otherSubstream) {
268
+ await wait(20);
269
+ otherReceivedPackets.push(otherPacket);
270
+ }
271
+ })();
272
+
273
+ let receivedPackets = [];
274
+ for await (let packet of substream) {
275
+ await wait(20);
276
+ receivedPackets.push(packet);
277
+ }
278
+
279
+ assert.equal(receivedPackets.length, 100);
280
+ assert.equal(otherReceivedPackets.length, 100);
281
+ });
282
+
283
+ it('should support writing an arbitrarily large number of times within the same call stack', async () => {
284
+ (async () => {
285
+ await wait(50);
286
+ for (let i = 0; i < 100; i++) {
287
+ demux.write('hello', 'world' + i);
288
+ }
289
+ demux.close('hello');
290
+ })();
291
+
292
+ let substream = demux.stream('hello');
293
+
294
+ let receivedPackets = [];
295
+ for await (let packet of substream) {
296
+ receivedPackets.push(packet);
297
+ await wait(10);
298
+ }
299
+
300
+ assert.equal(receivedPackets.length, 100);
301
+ });
302
+
303
+ it('should support writing an arbitrarily large number of times within the same call stack with multiple concurrent consumers', async () => {
304
+ (async () => {
305
+ await wait(50);
306
+ for (let i = 0; i < 50; i++) {
307
+ demux.write('other', 'message' + i);
308
+ }
309
+ demux.close('other');
310
+ })();
311
+
312
+ let otherSubstream = demux.stream('other');
313
+
314
+ let receivedPackets = [];
315
+ (async () => {
316
+ for await (let otherPacket of otherSubstream) {
317
+ await wait(10);
318
+ receivedPackets.push(otherPacket);
319
+ }
320
+ })();
321
+
322
+ for await (let otherPacket of otherSubstream) {
323
+ await wait(20);
324
+ receivedPackets.push(otherPacket);
325
+ }
326
+
327
+ assert.equal(receivedPackets.length, 100);
328
+ });
329
+
234
330
  it('should support the stream.once() method', async () => {
235
331
  (async () => {
236
332
  for (let i = 0; i < 10; i++) {
@@ -248,6 +344,8 @@ describe('StreamDemux', () => {
248
344
  packet = await substream.once();
249
345
  assert.equal(packet, 'world1');
250
346
 
347
+ await wait(1);
348
+
251
349
  packet = await substream.once();
252
350
  assert.equal(packet, 'world2');
253
351
  });
@@ -319,6 +417,36 @@ describe('StreamDemux', () => {
319
417
  assert.equal(packet, null);
320
418
  });
321
419
 
420
+ it('should prevent stream timeout from being reset when writing to other streams when iterating over consumer with timeout', async () => {
421
+ (async () => {
422
+ for (let i = 0; i < 10; i++) {
423
+ await wait(200);
424
+ demux.write('foo', 123);
425
+ await wait(200);
426
+ demux.write('hello', 'test' + i);
427
+ }
428
+ demux.kill('hello');
429
+ })();
430
+
431
+ let consumer = demux.stream('hello').createConsumer(300);
432
+ let error;
433
+
434
+ let packet;
435
+
436
+ try {
437
+ while (true) {
438
+ packet = await consumer.next();
439
+ if (packet.done) break;
440
+ }
441
+ } catch (err) {
442
+ error = err;
443
+ }
444
+
445
+ assert.notEqual(error, null);
446
+ assert.equal(error.name, 'TimeoutError');
447
+ assert.equal(packet, null);
448
+ });
449
+
322
450
  it('should prevent stream.once() timeout from being reset when killing other streams', async () => {
323
451
  (async () => {
324
452
  for (let i = 0; i < 15; i++) {
@@ -1,172 +0,0 @@
1
- class StreamConsumer {
2
- constructor(mainStream, id, startNode, streamName, timeout, usabilityMode) {
3
- this.id = id;
4
- this._backpressure = 0;
5
- this.currentNode = startNode;
6
- this.timeout = timeout;
7
- this.isAlive = true;
8
- this._mainStream = mainStream;
9
- this._mainStream.setConsumer(this.id, this);
10
- this.streamName = streamName;
11
- this.usabilityMode = !!usabilityMode;
12
- }
13
-
14
- getStats() {
15
- let stats = {
16
- id: this.id,
17
- backpressure: this._backpressure,
18
- stream: this.streamName
19
- };
20
- if (this.timeout != null) {
21
- stats.timeout = this.timeout;
22
- }
23
- return stats;
24
- }
25
-
26
- _resetBackpressure() {
27
- this._backpressure = 0;
28
- }
29
-
30
- applyBackpressure(packet) {
31
- this._backpressure++;
32
- }
33
-
34
- releaseBackpressure(packet) {
35
- this._backpressure--;
36
- }
37
-
38
- getBackpressure() {
39
- return this._backpressure;
40
- }
41
-
42
- clearActiveTimeout() {
43
- clearTimeout(this._timeoutId);
44
- delete this._timeoutId;
45
- }
46
-
47
- write(packet) {
48
- if (packet.done || packet.value.stream === this.streamName) {
49
- if (this._timeoutId !== undefined) {
50
- this.clearActiveTimeout(packet);
51
- }
52
- this.applyBackpressure(packet);
53
- }
54
- if (this._resolve) {
55
- this._resolve();
56
- delete this._resolve;
57
- }
58
- }
59
-
60
- kill(value) {
61
- this._killPacket = {value, done: true};
62
- if (this._timeoutId !== undefined) {
63
- this.clearActiveTimeout(this._killPacket);
64
- }
65
- this._destroy();
66
-
67
- if (this._resolve) {
68
- this._resolve();
69
- delete this._resolve;
70
- }
71
- }
72
-
73
- _destroy() {
74
- this.isAlive = false;
75
- this._resetBackpressure();
76
- this._mainStream.removeConsumer(this.id);
77
- }
78
-
79
- async _waitForNextItem(timeout) {
80
- return new Promise((resolve, reject) => {
81
- this._resolve = resolve;
82
- let timeoutId;
83
- if (timeout != null) {
84
- // Create the error object in the outer scope in order
85
- // to get the full stack trace.
86
- let error = new Error('Stream consumer iteration timed out');
87
- (async () => {
88
- let delay = wait(timeout);
89
- timeoutId = delay.timeoutId;
90
- await delay.promise;
91
- error.name = 'TimeoutError';
92
- delete this._resolve;
93
- reject(error);
94
- })();
95
- }
96
- this._timeoutId = timeoutId;
97
- });
98
- }
99
-
100
- async next() {
101
- this._mainStream.setConsumer(this.id, this);
102
-
103
- while (true) {
104
- if (!this.currentNode.next) {
105
- try {
106
- await this._waitForNextItem(this.timeout);
107
- } catch (error) {
108
- this._destroy();
109
- throw error;
110
- }
111
- }
112
- if (this._killPacket) {
113
- this._destroy();
114
- let killPacket = this._killPacket;
115
- delete this._killPacket;
116
-
117
- return killPacket;
118
- }
119
-
120
- // Skip over nodes which belong to different streams.
121
- while (
122
- this.currentNode.next?.data?.value &&
123
- this.currentNode.next.data.value.stream !== this.streamName &&
124
- this.currentNode.next.consumerId !== this.id &&
125
- !this.currentNode.next.data.done
126
- ) {
127
- this.currentNode = this.currentNode.next;
128
- this.usabilityMode && await wait(0);
129
- }
130
-
131
- if (!this.currentNode.next) {
132
- continue;
133
- }
134
-
135
- this.currentNode = this.currentNode.next;
136
-
137
- this.releaseBackpressure(this.currentNode.data);
138
-
139
- if (this.currentNode.data?.value?.data?.done) {
140
- this._destroy();
141
- return this.currentNode.data.value.data;
142
- }
143
-
144
- if (this.currentNode.data.done) {
145
- this._destroy();
146
- return this.currentNode.data;
147
- }
148
-
149
- return this.currentNode.data.value.data;
150
- }
151
- }
152
-
153
- return() {
154
- delete this.currentNode;
155
- this._destroy();
156
- return {};
157
- }
158
-
159
- [Symbol.asyncIterator]() {
160
- return this;
161
- }
162
- }
163
-
164
- function wait(timeout) {
165
- let timeoutId;
166
- let promise = new Promise((resolve) => {
167
- timeoutId = setTimeout(resolve, timeout);
168
- });
169
- return {timeoutId, promise};
170
- }
171
-
172
- module.exports = StreamConsumer;