stream-demux 10.0.1 → 10.2.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 +31 -0
- package/index.js +51 -3
- package/package.json +4 -4
- package/test/test.js +371 -0
package/README.md
CHANGED
|
@@ -96,6 +96,37 @@ function wait(duration) {
|
|
|
96
96
|
})();
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
## Consumer lifetime
|
|
100
|
+
|
|
101
|
+
A `StreamDemux` owns its streams; each one is created on demand when a consumer
|
|
102
|
+
asks for it by name and is dropped as soon as its last consumer goes away. That
|
|
103
|
+
is why no cleanup is needed beyond ending consumption, which you do by breaking
|
|
104
|
+
out of the `for-await-of` loop, or by calling `consumer.return()`, `kill()` or
|
|
105
|
+
`killAll()`.
|
|
106
|
+
|
|
107
|
+
A consequence is that a consumer must not be reused once its iteration has
|
|
108
|
+
ended. Create a new one instead:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
let consumer = demux.stream('abc').createConsumer();
|
|
112
|
+
for await (let packet of consumer) {
|
|
113
|
+
if (packet === 'stop') break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Do not iterate `consumer` again; ask the demux for a new one.
|
|
117
|
+
let nextConsumer = demux.stream('abc').createConsumer();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Reusing a consumer after its loop has ended leaves it attached to a stream the
|
|
121
|
+
demux has already dropped, so it will never receive anything written through
|
|
122
|
+
`demux.write()` and cannot be reached by `kill()` or `close()`. (A consumer
|
|
123
|
+
created directly on a `WritableConsumableStream` *can* be reused, because there
|
|
124
|
+
the stream's lifetime is yours to manage rather than the demux's.)
|
|
125
|
+
|
|
126
|
+
`demux.unstream(streamName)` detaches a stream from the demux and kills its
|
|
127
|
+
consumers, so their loops end rather than hanging on a stream nothing can
|
|
128
|
+
reach any more.
|
|
129
|
+
|
|
99
130
|
## Goal
|
|
100
131
|
|
|
101
132
|
The goal of this module is to facilitate functional programming patterns which decrease the probability of memory leaks and race conditions.
|
package/index.js
CHANGED
|
@@ -3,7 +3,14 @@ const DemuxedConsumableStream = require('./demuxed-consumable-stream');
|
|
|
3
3
|
|
|
4
4
|
class StreamDemux {
|
|
5
5
|
constructor() {
|
|
6
|
-
|
|
6
|
+
// A null-prototype object is used so that stream names which collide with
|
|
7
|
+
// Object.prototype members (such as __proto__, constructor or toString)
|
|
8
|
+
// are treated as ordinary stream names instead of resolving to inherited
|
|
9
|
+
// properties. Without this, lookups such as this.streams[streamName]
|
|
10
|
+
// return an inherited value for those names; the truthiness checks
|
|
11
|
+
// throughout this class then pass and the subsequent stream method call
|
|
12
|
+
// throws a TypeError.
|
|
13
|
+
this.streams = Object.create(null);
|
|
7
14
|
this._nextConsumerId = 1;
|
|
8
15
|
this.generateConsumerId = () => {
|
|
9
16
|
return this._nextConsumerId++;
|
|
@@ -126,6 +133,32 @@ class StreamDemux {
|
|
|
126
133
|
return 0;
|
|
127
134
|
}
|
|
128
135
|
|
|
136
|
+
// Backpressure is pending processing work: a stream counts every unprocessed
|
|
137
|
+
// item in its queue, a consumer only its own share. Queue depth instead
|
|
138
|
+
// counts the nodes a consumer still pins in memory, addressed to it or not.
|
|
139
|
+
getQueueDepth(streamName) {
|
|
140
|
+
if (this.streams[streamName]) {
|
|
141
|
+
return this.streams[streamName].getQueueDepth();
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getQueueDepthAll() {
|
|
147
|
+
return Object.values(this.streams).reduce(
|
|
148
|
+
(max, stream) => Math.max(max, stream.getQueueDepth()),
|
|
149
|
+
0
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getConsumerQueueDepth(consumerId) {
|
|
154
|
+
for (let stream of Object.values(this.streams)) {
|
|
155
|
+
if (stream.hasConsumer(consumerId)) {
|
|
156
|
+
return stream.getConsumerQueueDepth(consumerId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
129
162
|
hasConsumer(streamName, consumerId) {
|
|
130
163
|
if (this.streams[streamName]) {
|
|
131
164
|
return this.streams[streamName].hasConsumer(consumerId);
|
|
@@ -153,14 +186,21 @@ class StreamDemux {
|
|
|
153
186
|
|
|
154
187
|
createConsumer(streamName, timeout) {
|
|
155
188
|
if (!this.streams[streamName]) {
|
|
156
|
-
|
|
189
|
+
// The callback checks the stream it belongs to rather than whatever is
|
|
190
|
+
// currently registered under streamName, so a stream which has already
|
|
191
|
+
// been replaced can never remove its successor from the map. Capturing
|
|
192
|
+
// the stream allocates nothing: the callback is stored on the stream
|
|
193
|
+
// either way, so this only adds a binding to a closure which the stream
|
|
194
|
+
// already holds, and the resulting cycle is collected along with it.
|
|
195
|
+
let stream = new WritableConsumableStream({
|
|
157
196
|
generateConsumerId: this.generateConsumerId,
|
|
158
197
|
removeConsumerCallback: () => {
|
|
159
|
-
if (
|
|
198
|
+
if (this.streams[streamName] === stream && !stream.getConsumerCount()) {
|
|
160
199
|
delete this.streams[streamName];
|
|
161
200
|
}
|
|
162
201
|
}
|
|
163
202
|
});
|
|
203
|
+
this.streams[streamName] = stream;
|
|
164
204
|
}
|
|
165
205
|
return this.streams[streamName].createConsumer(timeout);
|
|
166
206
|
}
|
|
@@ -171,7 +211,15 @@ class StreamDemux {
|
|
|
171
211
|
return new DemuxedConsumableStream(this, streamName);
|
|
172
212
|
}
|
|
173
213
|
|
|
214
|
+
// Kills the stream's consumers before detaching it. A consumer left attached
|
|
215
|
+
// to a detached stream could not be reached again through the demux: writes
|
|
216
|
+
// would go to a freshly created stream and kill()/close() could not see it,
|
|
217
|
+
// so its for-await-of loop would never end.
|
|
174
218
|
unstream(streamName) {
|
|
219
|
+
let stream = this.streams[streamName];
|
|
220
|
+
if (stream) {
|
|
221
|
+
stream.kill();
|
|
222
|
+
}
|
|
175
223
|
delete this.streams[streamName];
|
|
176
224
|
}
|
|
177
225
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stream-demux",
|
|
3
|
-
"version": "10.0
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"description": "A consumable stream demultiplexer.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "mocha --reporter spec --timeout 10000 --slow 10000"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/SocketCluster/stream-demux#readme",
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"mocha": "^
|
|
28
|
+
"mocha": "^12.0.3"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"consumable-stream": "^3.0.0",
|
|
32
|
-
"writable-consumable-stream": "^
|
|
32
|
+
"writable-consumable-stream": "^5.0.0"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/test/test.js
CHANGED
|
@@ -895,4 +895,375 @@ describe('StreamDemux', () => {
|
|
|
895
895
|
assert.equal(demux.hasConsumerAll(consumerA.id), true);
|
|
896
896
|
assert.equal(demux.hasConsumerAll(consumerB.id), true);
|
|
897
897
|
});
|
|
898
|
+
|
|
899
|
+
it('should not attribute packets addressed to one consumer to the other consumers of the same stream', async () => {
|
|
900
|
+
let consumerA = demux.stream('hello').createConsumer();
|
|
901
|
+
let consumerB = demux.stream('hello').createConsumer();
|
|
902
|
+
|
|
903
|
+
for (let i = 0; i < 10; i++) {
|
|
904
|
+
demux.writeToConsumer(consumerA.id, 'for-a' + i);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
assert.equal(demux.getConsumerBackpressure(consumerA.id), 10);
|
|
908
|
+
assert.equal(demux.getConsumerBackpressure(consumerB.id), 0);
|
|
909
|
+
assert.equal(demux.getBackpressure('hello'), 10);
|
|
910
|
+
assert.equal(demux.getBackpressureAll(), 10);
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('should attribute broadcast packets to every consumer of the stream', async () => {
|
|
914
|
+
let consumerA = demux.stream('hello').createConsumer();
|
|
915
|
+
let consumerB = demux.stream('hello').createConsumer();
|
|
916
|
+
|
|
917
|
+
demux.write('hello', 'one');
|
|
918
|
+
demux.writeToConsumer(consumerA.id, 'for-a');
|
|
919
|
+
demux.write('hello', 'two');
|
|
920
|
+
|
|
921
|
+
assert.equal(demux.getConsumerBackpressure(consumerA.id), 3);
|
|
922
|
+
assert.equal(demux.getConsumerBackpressure(consumerB.id), 2);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('should not reset a consumer timeout when a packet is addressed to a different consumer of the same stream', async () => {
|
|
926
|
+
let victim = demux.stream('hello').createConsumer(100);
|
|
927
|
+
let other = demux.stream('hello').createConsumer();
|
|
928
|
+
|
|
929
|
+
let error;
|
|
930
|
+
let consumeVictim = (async () => {
|
|
931
|
+
try {
|
|
932
|
+
for await (let packet of victim) {}
|
|
933
|
+
} catch (err) {
|
|
934
|
+
error = err;
|
|
935
|
+
}
|
|
936
|
+
})();
|
|
937
|
+
|
|
938
|
+
let spam = (async () => {
|
|
939
|
+
for (let i = 0; i < 15; i++) {
|
|
940
|
+
demux.writeToConsumer(other.id, 'spam' + i);
|
|
941
|
+
await wait(20);
|
|
942
|
+
}
|
|
943
|
+
})();
|
|
944
|
+
|
|
945
|
+
await Promise.all([consumeVictim, spam]);
|
|
946
|
+
|
|
947
|
+
assert.notEqual(error, null);
|
|
948
|
+
assert.equal(error.name, 'TimeoutError');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('should deliver targeted packets only to the consumer they were addressed to', async () => {
|
|
952
|
+
let consumerA = demux.stream('hello').createConsumer();
|
|
953
|
+
let consumerB = demux.stream('hello').createConsumer();
|
|
954
|
+
|
|
955
|
+
let receivedA = [];
|
|
956
|
+
let receivedB = [];
|
|
957
|
+
|
|
958
|
+
let consumeA = (async () => {
|
|
959
|
+
while (true) {
|
|
960
|
+
let packet = await consumerA.next();
|
|
961
|
+
if (packet.done) break;
|
|
962
|
+
receivedA.push(packet.value);
|
|
963
|
+
}
|
|
964
|
+
})();
|
|
965
|
+
let consumeB = (async () => {
|
|
966
|
+
while (true) {
|
|
967
|
+
let packet = await consumerB.next();
|
|
968
|
+
if (packet.done) break;
|
|
969
|
+
receivedB.push(packet.value);
|
|
970
|
+
}
|
|
971
|
+
})();
|
|
972
|
+
|
|
973
|
+
await wait(10);
|
|
974
|
+
|
|
975
|
+
demux.writeToConsumer(consumerA.id, 'secret-a');
|
|
976
|
+
demux.write('hello', 'public');
|
|
977
|
+
demux.writeToConsumer(consumerB.id, 'secret-b');
|
|
978
|
+
demux.close('hello');
|
|
979
|
+
|
|
980
|
+
await Promise.all([consumeA, consumeB]);
|
|
981
|
+
|
|
982
|
+
assert.equal(JSON.stringify(receivedA), JSON.stringify(['secret-a', 'public']));
|
|
983
|
+
assert.equal(JSON.stringify(receivedB), JSON.stringify(['public', 'secret-b']));
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it('should report queue depth separately from backpressure', async () => {
|
|
987
|
+
let idle = demux.stream('hello').createConsumer();
|
|
988
|
+
let active = demux.stream('hello').createConsumer();
|
|
989
|
+
|
|
990
|
+
let consumeActive = (async () => {
|
|
991
|
+
for await (let packet of active) {}
|
|
992
|
+
})();
|
|
993
|
+
|
|
994
|
+
await wait(10);
|
|
995
|
+
|
|
996
|
+
for (let i = 0; i < 10; i++) {
|
|
997
|
+
demux.writeToConsumer(active.id, 'for-active' + i);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
assert.equal(demux.getConsumerBackpressure(idle.id), 0);
|
|
1001
|
+
assert.equal(demux.getConsumerQueueDepth(idle.id), 10);
|
|
1002
|
+
assert.equal(demux.getQueueDepth('hello'), 10);
|
|
1003
|
+
assert.equal(demux.getQueueDepthAll(), 10);
|
|
1004
|
+
|
|
1005
|
+
await wait(50);
|
|
1006
|
+
|
|
1007
|
+
// The active consumer has caught up, so its own backpressure is zero, but
|
|
1008
|
+
// the 10 items are still unprocessed by the idle consumer, so the stream
|
|
1009
|
+
// still reports them as pending work.
|
|
1010
|
+
assert.equal(demux.getConsumerBackpressure(active.id), 0);
|
|
1011
|
+
assert.equal(demux.getBackpressureAll(), 10);
|
|
1012
|
+
assert.equal(demux.getQueueDepthAll(), 10);
|
|
1013
|
+
|
|
1014
|
+
demux.close('hello');
|
|
1015
|
+
await consumeActive;
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
it('should report zero queue depth for unknown streams and consumers', async () => {
|
|
1019
|
+
assert.equal(demux.getQueueDepth('does-not-exist'), 0);
|
|
1020
|
+
assert.equal(demux.getQueueDepthAll(), 0);
|
|
1021
|
+
assert.equal(demux.getConsumerQueueDepth(12345), 0);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it('should drop back to zero queue depth once every consumer has caught up', async () => {
|
|
1025
|
+
let consumerA = demux.stream('hello').createConsumer();
|
|
1026
|
+
let consumerB = demux.stream('hello').createConsumer();
|
|
1027
|
+
|
|
1028
|
+
let consumeA = (async () => {
|
|
1029
|
+
for await (let packet of consumerA) {}
|
|
1030
|
+
})();
|
|
1031
|
+
let consumeB = (async () => {
|
|
1032
|
+
for await (let packet of consumerB) {}
|
|
1033
|
+
})();
|
|
1034
|
+
|
|
1035
|
+
await wait(10);
|
|
1036
|
+
|
|
1037
|
+
for (let i = 0; i < 10; i++) {
|
|
1038
|
+
demux.write('hello', 'hello' + i);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
await wait(50);
|
|
1042
|
+
|
|
1043
|
+
assert.equal(demux.getQueueDepthAll(), 0);
|
|
1044
|
+
|
|
1045
|
+
demux.close('hello');
|
|
1046
|
+
await Promise.all([consumeA, consumeB]);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it('should end a consuming loop when return() is called in the same tick as a write', async () => {
|
|
1050
|
+
let consumer = demux.stream('hello').createConsumer();
|
|
1051
|
+
|
|
1052
|
+
let error;
|
|
1053
|
+
let ended = false;
|
|
1054
|
+
let consume = (async () => {
|
|
1055
|
+
try {
|
|
1056
|
+
for await (let packet of consumer) {}
|
|
1057
|
+
ended = true;
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
error = err;
|
|
1060
|
+
}
|
|
1061
|
+
})();
|
|
1062
|
+
|
|
1063
|
+
await wait(10);
|
|
1064
|
+
|
|
1065
|
+
// The write resolves the parked next() call; the return() which follows in
|
|
1066
|
+
// the same tick used to leave that call with no packet to terminate on and
|
|
1067
|
+
// no currentNode to read, throwing a TypeError out of the consuming loop.
|
|
1068
|
+
demux.write('hello', 'packet');
|
|
1069
|
+
consumer.return();
|
|
1070
|
+
|
|
1071
|
+
await consume;
|
|
1072
|
+
|
|
1073
|
+
assert.equal(error, undefined);
|
|
1074
|
+
assert.equal(ended, true);
|
|
1075
|
+
assert.equal(demux.getConsumerCountAll(), 0);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('should end a consuming loop when return() is called in the same tick as a targeted write', async () => {
|
|
1079
|
+
let consumer = demux.stream('hello').createConsumer();
|
|
1080
|
+
|
|
1081
|
+
let error;
|
|
1082
|
+
let ended = false;
|
|
1083
|
+
let consume = (async () => {
|
|
1084
|
+
try {
|
|
1085
|
+
for await (let packet of consumer) {}
|
|
1086
|
+
ended = true;
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
error = err;
|
|
1089
|
+
}
|
|
1090
|
+
})();
|
|
1091
|
+
|
|
1092
|
+
await wait(10);
|
|
1093
|
+
|
|
1094
|
+
demux.writeToConsumer(consumer.id, 'packet');
|
|
1095
|
+
consumer.return();
|
|
1096
|
+
|
|
1097
|
+
await consume;
|
|
1098
|
+
|
|
1099
|
+
assert.equal(error, undefined);
|
|
1100
|
+
assert.equal(ended, true);
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
it('should end a consuming loop when return() is called in the same tick as a close', async () => {
|
|
1104
|
+
let consumer = demux.stream('hello').createConsumer();
|
|
1105
|
+
|
|
1106
|
+
let error;
|
|
1107
|
+
let ended = false;
|
|
1108
|
+
let consume = (async () => {
|
|
1109
|
+
try {
|
|
1110
|
+
for await (let packet of consumer) {}
|
|
1111
|
+
ended = true;
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
error = err;
|
|
1114
|
+
}
|
|
1115
|
+
})();
|
|
1116
|
+
|
|
1117
|
+
await wait(10);
|
|
1118
|
+
|
|
1119
|
+
demux.close('hello', 'end');
|
|
1120
|
+
consumer.return();
|
|
1121
|
+
|
|
1122
|
+
await consume;
|
|
1123
|
+
|
|
1124
|
+
assert.equal(error, undefined);
|
|
1125
|
+
assert.equal(ended, true);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it('should reclaim the stream after return() races a write', async () => {
|
|
1129
|
+
let consumer = demux.stream('hello').createConsumer();
|
|
1130
|
+
|
|
1131
|
+
let consume = (async () => {
|
|
1132
|
+
for await (let packet of consumer) {}
|
|
1133
|
+
})();
|
|
1134
|
+
|
|
1135
|
+
await wait(10);
|
|
1136
|
+
|
|
1137
|
+
demux.write('hello', 'packet');
|
|
1138
|
+
consumer.return();
|
|
1139
|
+
|
|
1140
|
+
await consume;
|
|
1141
|
+
|
|
1142
|
+
assert.equal(demux.getConsumerCount('hello'), 0);
|
|
1143
|
+
assert.equal(demux.hasConsumerAll(consumer.id), false);
|
|
1144
|
+
assert.equal(Object.keys(demux.streams).length, 0);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it('should terminate consumers when their stream is detached with unstream', async () => {
|
|
1148
|
+
let consumerA = demux.stream('hello').createConsumer();
|
|
1149
|
+
let consumerB = demux.stream('hello').createConsumer();
|
|
1150
|
+
|
|
1151
|
+
let endedA = false;
|
|
1152
|
+
let endedB = false;
|
|
1153
|
+
let error;
|
|
1154
|
+
|
|
1155
|
+
let consumeA = (async () => {
|
|
1156
|
+
try {
|
|
1157
|
+
for await (let packet of consumerA) {}
|
|
1158
|
+
endedA = true;
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
error = err;
|
|
1161
|
+
}
|
|
1162
|
+
})();
|
|
1163
|
+
let consumeB = (async () => {
|
|
1164
|
+
try {
|
|
1165
|
+
for await (let packet of consumerB) {}
|
|
1166
|
+
endedB = true;
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
error = err;
|
|
1169
|
+
}
|
|
1170
|
+
})();
|
|
1171
|
+
|
|
1172
|
+
await wait(10);
|
|
1173
|
+
|
|
1174
|
+
demux.unstream('hello');
|
|
1175
|
+
|
|
1176
|
+
await Promise.all([consumeA, consumeB]);
|
|
1177
|
+
|
|
1178
|
+
assert.equal(error, undefined);
|
|
1179
|
+
assert.equal(endedA, true);
|
|
1180
|
+
assert.equal(endedB, true);
|
|
1181
|
+
assert.equal(consumerA.isAlive, false);
|
|
1182
|
+
assert.equal(consumerB.isAlive, false);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it('should leave no stream or consumer behind after unstream', async () => {
|
|
1186
|
+
let consumer = demux.stream('hello').createConsumer();
|
|
1187
|
+
|
|
1188
|
+
let consume = (async () => {
|
|
1189
|
+
for await (let packet of consumer) {}
|
|
1190
|
+
})();
|
|
1191
|
+
|
|
1192
|
+
await wait(10);
|
|
1193
|
+
|
|
1194
|
+
demux.unstream('hello');
|
|
1195
|
+
await consume;
|
|
1196
|
+
|
|
1197
|
+
assert.equal(Object.keys(demux.streams).length, 0);
|
|
1198
|
+
assert.equal(demux.getConsumerCountAll(), 0);
|
|
1199
|
+
assert.equal(demux.hasConsumerAll(consumer.id), false);
|
|
1200
|
+
assert.equal(demux.getConsumerStats(consumer.id), undefined);
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
it('should support unstream on a stream which does not exist', async () => {
|
|
1204
|
+
demux.unstream('does-not-exist');
|
|
1205
|
+
|
|
1206
|
+
assert.equal(Object.keys(demux.streams).length, 0);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it('should let a stream be recreated after unstream', async () => {
|
|
1210
|
+
let oldConsumer = demux.stream('hello').createConsumer();
|
|
1211
|
+
let consumeOld = (async () => {
|
|
1212
|
+
for await (let packet of oldConsumer) {}
|
|
1213
|
+
})();
|
|
1214
|
+
|
|
1215
|
+
await wait(10);
|
|
1216
|
+
demux.unstream('hello');
|
|
1217
|
+
await consumeOld;
|
|
1218
|
+
|
|
1219
|
+
let newConsumer = demux.stream('hello').createConsumer();
|
|
1220
|
+
let received = [];
|
|
1221
|
+
let consumeNew = (async () => {
|
|
1222
|
+
for await (let packet of newConsumer) {
|
|
1223
|
+
received.push(packet);
|
|
1224
|
+
}
|
|
1225
|
+
})();
|
|
1226
|
+
|
|
1227
|
+
await wait(10);
|
|
1228
|
+
demux.write('hello', 'fresh');
|
|
1229
|
+
demux.close('hello');
|
|
1230
|
+
await consumeNew;
|
|
1231
|
+
|
|
1232
|
+
assert.equal(JSON.stringify(received), JSON.stringify(['fresh']));
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
it('should not let a replaced stream remove its successor from the map', async () => {
|
|
1236
|
+
// A consumer can outlive the stream it was created on, by being reused
|
|
1237
|
+
// after return(). When it is finally removed, its stream must not delete
|
|
1238
|
+
// the entry which a newer stream of the same name now owns.
|
|
1239
|
+
let oldConsumer = demux.createConsumer('hello');
|
|
1240
|
+
oldConsumer.return();
|
|
1241
|
+
assert.equal(demux.streams['hello'], undefined);
|
|
1242
|
+
|
|
1243
|
+
// Reusing it re-registers it on the stream which is no longer in the map.
|
|
1244
|
+
oldConsumer.next();
|
|
1245
|
+
await wait(10);
|
|
1246
|
+
|
|
1247
|
+
let newConsumer = demux.createConsumer('hello');
|
|
1248
|
+
let newStream = demux.streams['hello'];
|
|
1249
|
+
let received = [];
|
|
1250
|
+
let consumeNew = (async () => {
|
|
1251
|
+
for await (let packet of newConsumer) {
|
|
1252
|
+
received.push(packet);
|
|
1253
|
+
}
|
|
1254
|
+
})();
|
|
1255
|
+
|
|
1256
|
+
await wait(10);
|
|
1257
|
+
|
|
1258
|
+
oldConsumer.return();
|
|
1259
|
+
|
|
1260
|
+
assert.equal(demux.streams['hello'], newStream);
|
|
1261
|
+
assert.equal(demux.hasConsumerAll(newConsumer.id), true);
|
|
1262
|
+
|
|
1263
|
+
demux.write('hello', 'still-working');
|
|
1264
|
+
demux.close('hello');
|
|
1265
|
+
await consumeNew;
|
|
1266
|
+
|
|
1267
|
+
assert.equal(JSON.stringify(received), JSON.stringify(['still-working']));
|
|
1268
|
+
});
|
|
898
1269
|
});
|