@shutterstock/p-map-iterable 1.0.11 → 1.1.1
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 +110 -16
- package/dist/src/blocking-queue.js +61 -70
- package/dist/src/blocking-queue.js.map +1 -1
- package/dist/src/blocking-queue.test.js +70 -71
- package/dist/src/blocking-queue.test.js.map +1 -1
- package/dist/src/iterable-mapper.d.ts +173 -30
- package/dist/src/iterable-mapper.d.ts.map +1 -1
- package/dist/src/iterable-mapper.js +255 -141
- package/dist/src/iterable-mapper.js.map +1 -1
- package/dist/src/iterable-mapper.test.js +347 -720
- package/dist/src/iterable-mapper.test.js.map +1 -1
- package/dist/src/iterable-queue-mapper-simple.d.ts +44 -29
- package/dist/src/iterable-queue-mapper-simple.d.ts.map +1 -1
- package/dist/src/iterable-queue-mapper-simple.js +65 -57
- package/dist/src/iterable-queue-mapper-simple.js.map +1 -1
- package/dist/src/iterable-queue-mapper-simple.test.js +34 -35
- package/dist/src/iterable-queue-mapper-simple.test.js.map +1 -1
- package/dist/src/iterable-queue-mapper.d.ts +38 -58
- package/dist/src/iterable-queue-mapper.d.ts.map +1 -1
- package/dist/src/iterable-queue-mapper.js +37 -42
- package/dist/src/iterable-queue-mapper.js.map +1 -1
- package/dist/src/iterable-queue-mapper.test.js +41 -81
- package/dist/src/iterable-queue-mapper.test.js.map +1 -1
- package/dist/src/iterable-queue.js +8 -11
- package/dist/src/iterable-queue.js.map +1 -1
- package/dist/src/iterable-queue.test.js +43 -122
- package/dist/src/iterable-queue.test.js.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,25 +1,116 @@
|
|
|
1
|
-
[](https://opensource.org/licenses/MIT) [](https://tech.shutterstock.com/p-map-iterable/) [](https://github.com/shutterstock/p-map-iterable/actions/workflows/ci.yml) [](https://github.com/shutterstock/p-map-iterable/actions/workflows/publish.yml) [](https://github.com/shutterstock/p-map-iterable/actions/workflows/docs.yml)
|
|
1
|
+
[](https://www.npmjs.com/package/@shutterstock/p-map-iterable) [](https://opensource.org/licenses/MIT) [](https://tech.shutterstock.com/p-map-iterable/) [](https://github.com/shutterstock/p-map-iterable/actions/workflows/ci.yml) [](https://github.com/shutterstock/p-map-iterable/actions/workflows/publish.yml) [](https://github.com/shutterstock/p-map-iterable/actions/workflows/docs.yml)
|
|
2
2
|
|
|
3
3
|
# Overview
|
|
4
4
|
|
|
5
|
-
`@shutterstock/p-map-iterable` provides several classes that allow processing results of `p-map`-style mapper functions by iterating the results as they are completed, with
|
|
5
|
+
`@shutterstock/p-map-iterable` provides several classes that allow processing results of `p-map`-style mapper functions by iterating the results as they are completed, with backpressure to limit the number of items that are processed ahead of the consumer.
|
|
6
6
|
|
|
7
7
|
A common use case for `@shutterstock/p-map-iterable` is as a "prefetcher" that will fetch, for example, AWS S3 files in an AWS Lambda function. By prefetching large files the consumer is able to use 100% of the paid-for Lambda CPU time for the JS thread, rather than waiting idle while the next file is fetched. The backpressure (set by `maxUnread`) prevents the prefetcher from consuming unlimited memory or disk space by racing ahead of the consumer.
|
|
8
8
|
|
|
9
9
|
These classes will typically be helpful in batch or queue consumers, not as much in request/response services.
|
|
10
10
|
|
|
11
|
-
# Example Usage
|
|
11
|
+
# Example Usage Scenarios
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
## Typical Processing Loop without `IterableMapper`
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
const source = new SomeSource();
|
|
17
|
+
const sourceIds = [1, 2,... 1000];
|
|
18
|
+
const sink = new SomeSink();
|
|
19
|
+
for (const sourceId of sourceIds) {
|
|
20
|
+
const item = await source.read(sourceId); // takes 300 ms of I/O wait, no CPU
|
|
21
|
+
const outputItem = doSomeOperation(item); // takes 20 ms of CPU
|
|
22
|
+
await sink.write(outputItem); // takes 500 ms of I/O wait, no CPU
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Each iteration takes 820ms total, but we waste time waiting for I/O. We could prefetch the next read (300ms) while processing (20ms) and writing (500ms), without changing the order of reads or writes.
|
|
27
|
+
|
|
28
|
+
## Using `IterableMapper` as Prefetcher with Blocking Sequential Writes
|
|
29
|
+
|
|
30
|
+
`concurrency: 1` on the prefetcher preserves the order of the reads and and writes are sequential and blocking (unchanged).
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const source = new SomeSource();
|
|
34
|
+
const sourceIds = [1, 2,... 1000];
|
|
35
|
+
// Pre-reads up to 8 items serially and releases in sequential order
|
|
36
|
+
const sourcePrefetcher = new IterableMapper(sourceIds,
|
|
37
|
+
async (sourceId) => source.read(sourceId),
|
|
38
|
+
{ concurrency: 1, maxUnread: 10 }
|
|
39
|
+
);
|
|
40
|
+
const sink = new SomeSink();
|
|
41
|
+
for await (const item of sourcePrefetcher) { // may not block for fast sources
|
|
42
|
+
const outputItem = doSomeOperation(item); // takes 20 ms of CPU
|
|
43
|
+
await sink.write(outputItem); // takes 500 ms of I/O wait, no CPU
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This reduces iteration time to 520ms by overlapping reads with processing/writing.
|
|
48
|
+
|
|
49
|
+
## Using `IterableMapper` as Prefetcher with Background Sequential Writes with `IterableQueueMapperSimple`
|
|
50
|
+
|
|
51
|
+
`concurrency: 1` on the prefetcher preserves the order of the reads.
|
|
52
|
+
`concurrency: 1` on the flusher preserves the order of the writes, but allows the loop to iterate while last write is completing.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const source = new SomeSource();
|
|
56
|
+
const sourceIds = [1, 2,... 1000];
|
|
57
|
+
const sourcePrefetcher = new IterableMapper(sourceIds,
|
|
58
|
+
async (sourceId) => source.read(sourceId),
|
|
59
|
+
{ concurrency: 1, maxUnread: 10 }
|
|
60
|
+
);
|
|
61
|
+
const sink = new SomeSink();
|
|
62
|
+
const flusher = new IterableQueueMapperSimple(
|
|
63
|
+
async (outputItem) => sink.write(outputItem),
|
|
64
|
+
{ concurrency: 1 }
|
|
65
|
+
);
|
|
66
|
+
for await (const item of sourcePrefetcher) { // may not block for fast sources
|
|
67
|
+
const outputItem = doSomeOperation(item); // takes 20 ms of CPU
|
|
68
|
+
await flusher.enqueue(outputItem); // will periodically block for portion of write time
|
|
69
|
+
}
|
|
70
|
+
// Wait for all writes to complete
|
|
71
|
+
await flusher.onIdle();
|
|
72
|
+
// Check for errors
|
|
73
|
+
if (flusher.errors.length > 0) {
|
|
74
|
+
// ...
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This reduces iteration time to about `max((max(readTime, writeTime) - cpuOpTime, cpuOpTime))`
|
|
79
|
+
by overlapping reads and writes with the CPU processing step.
|
|
80
|
+
In this contrived example, the loop time is reduced to 500ms - 20ms = 480ms.
|
|
81
|
+
In cases where the CPU usage time is higher, the impact can be greater.
|
|
82
|
+
|
|
83
|
+
## Using `IterableMapper` as Prefetcher with Out of Order Reads and Background Out of Order Writes with `IterableQueueMapperSimple`
|
|
84
|
+
|
|
85
|
+
For maximum throughput, allow out of order reads and writes with
|
|
86
|
+
`IterableQueueMapper` (to iterate results with backpressure when too many unread items) or
|
|
87
|
+
`IterableQueueMapperSimple` (to handle errors at end without custom iteration and applying backpressure to block further enqueues when `concurrency` items are in process):
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const source = new SomeSource();
|
|
91
|
+
const sourceIds = [1, 2,... 1000];
|
|
92
|
+
const sourcePrefetcher = new IterableMapper(sourceIds,
|
|
93
|
+
async (sourceId) => source.read(sourceId),
|
|
94
|
+
{ concurrency: 10, maxUnread: 20 }
|
|
95
|
+
);
|
|
96
|
+
const sink = new SomeSink();
|
|
97
|
+
const flusher = new IterableQueueMapperSimple(
|
|
98
|
+
async (outputItem) => sink.write(outputItem),
|
|
99
|
+
{ concurrency: 10 }
|
|
100
|
+
);
|
|
101
|
+
for await (const item of sourcePrefetcher) { // typically will not block
|
|
102
|
+
const outputItem = doSomeOperation(item); // takes 20 ms of CPU
|
|
103
|
+
await flusher.enqueue(outputItem); // typically will not block
|
|
104
|
+
}
|
|
105
|
+
// Wait for all writes to complete
|
|
106
|
+
await flusher.onIdle();
|
|
107
|
+
// Check for errors
|
|
108
|
+
if (flusher.errors.length > 0) {
|
|
109
|
+
// ...
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This reduces iteration time to about 20ms by overlapping reads and writes with the CPU processing step. In this contrived (but common) example we would get a 41x improvement in throughput, removing 97.5% of the time to process each item and fully utilizing the CPU time available in the JS event loop.
|
|
23
114
|
|
|
24
115
|
# Getting Started
|
|
25
116
|
|
|
@@ -32,7 +123,10 @@ The package is available on npm as [@shutterstock/p-map-iterable](https://www.np
|
|
|
32
123
|
## Importing
|
|
33
124
|
|
|
34
125
|
```typescript
|
|
35
|
-
import {
|
|
126
|
+
import {
|
|
127
|
+
IterableMapper,
|
|
128
|
+
IterableQueueMapper,
|
|
129
|
+
IterableQueueMapperSimple } from '@shutterstock/p-map-iterable';
|
|
36
130
|
```
|
|
37
131
|
|
|
38
132
|
## API Documentation
|
|
@@ -63,7 +157,7 @@ These diagrams illustrate the differences in operation betweeen `p-map`, `p-queu
|
|
|
63
157
|
- User supplied sync or async mapper function
|
|
64
158
|
- Exposes an async iterable interface for consuming mapped items
|
|
65
159
|
- Allows a maximum queue depth of mapped items - if the consumer stops consuming, the queue will fill up, at which point the mapper will stop being invoked until an item is consumed from the queue
|
|
66
|
-
- This allows mapping with
|
|
160
|
+
- This allows mapping with backpressure so that the mapper does not consume unlimited resources (e.g. memory, disk, network, event loop time) by racing ahead of the consumer
|
|
67
161
|
- [IterableQueueMapper](https://tech.shutterstock.com/p-map-iterable/classes/IterableQueueMapper.html)
|
|
68
162
|
- Wraps `IterableMapper`
|
|
69
163
|
- Adds items to the queue via the `enqueue` method
|
|
@@ -88,7 +182,7 @@ These diagrams illustrate the differences in operation betweeen `p-map`, `p-queu
|
|
|
88
182
|
|
|
89
183
|
See [p-map](https://github.com/sindresorhus/p-map) docs for a good start in understanding what this does.
|
|
90
184
|
|
|
91
|
-
The key difference between `IterableMapper` and `pMap` are that `IterableMapper` does not return when the entire mapping is done, rather it exposes an iterable that the caller loops through. This enables results to be processed while the mapping is still happening, while optionally allowing for
|
|
185
|
+
The key difference between `IterableMapper` and `pMap` are that `IterableMapper` does not return when the entire mapping is done, rather it exposes an iterable that the caller loops through. This enables results to be processed while the mapping is still happening, while optionally allowing for backpressure to slow or stop the mapping if the caller is not consuming items fast enough. Common use cases include `prefetching` items from a remote service - the next set of requests are dispatched asyncronously while the current responses are processed and the prefetch requests will pause when the unread queue fills up.
|
|
92
186
|
|
|
93
187
|
See [examples/iterable-mapper.ts](./examples/iterable-mapper.ts) for an example.
|
|
94
188
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BlockingQueue = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
4
|
const queue_1 = require("./queue");
|
|
6
5
|
/**
|
|
7
6
|
* `enqueue` blocks when the queue is full, until an item is read with `dequeue`, or done
|
|
@@ -72,65 +71,61 @@ class BlockingQueue {
|
|
|
72
71
|
*
|
|
73
72
|
* @param item Element to add
|
|
74
73
|
*/
|
|
75
|
-
enqueue(item) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
throw new TypeError('reader queue returned undefined');
|
|
88
|
-
}
|
|
89
|
-
// Resolve the Promise for a waiting reader
|
|
90
|
-
reader(undefined);
|
|
91
|
-
}
|
|
92
|
-
if (this._unreadQueue.length > this._options.maxUnread) {
|
|
93
|
-
// We wait if the queue is at max length
|
|
94
|
-
yield this.waitForRead();
|
|
74
|
+
async enqueue(item) {
|
|
75
|
+
if (this._doneAdding) {
|
|
76
|
+
throw new Error('`enqueue` called after `done` called');
|
|
77
|
+
}
|
|
78
|
+
// We always add to the internal queue, we just wait to return if the queue is full
|
|
79
|
+
this._unreadQueue.enqueue(item);
|
|
80
|
+
if (this._readersWaiting.length > 0) {
|
|
81
|
+
// Release a reader if one is waiting for an item
|
|
82
|
+
// Remove the first waiting reader Promise from the queue
|
|
83
|
+
const reader = this._readersWaiting.dequeue();
|
|
84
|
+
if (reader === undefined) {
|
|
85
|
+
throw new TypeError('reader queue returned undefined');
|
|
95
86
|
}
|
|
96
|
-
|
|
87
|
+
// Resolve the Promise for a waiting reader
|
|
88
|
+
reader(undefined);
|
|
89
|
+
}
|
|
90
|
+
if (this._unreadQueue.length > this._options.maxUnread) {
|
|
91
|
+
// We wait if the queue is at max length
|
|
92
|
+
await this.waitForRead();
|
|
93
|
+
}
|
|
97
94
|
}
|
|
98
95
|
/**
|
|
99
96
|
* Gets an element when one is ready, waits if none are ready.
|
|
100
97
|
*
|
|
101
98
|
* @returns Element or undefined if queue is empty and `done` has been called
|
|
102
99
|
*/
|
|
103
|
-
dequeue() {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throw new TypeError('writer queue returned undefined');
|
|
128
|
-
}
|
|
129
|
-
// Resolve the Promise for a waiting writer
|
|
130
|
-
writer(undefined);
|
|
100
|
+
async dequeue() {
|
|
101
|
+
// Bail out and release all waiters if there are no more items coming
|
|
102
|
+
const done = this.areWeDone();
|
|
103
|
+
if (done) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
// Check if queue has an item
|
|
107
|
+
let item;
|
|
108
|
+
// Get the item if one is ready
|
|
109
|
+
if (this._unreadQueue.length !== 0 && this._readersWaiting.length === 0) {
|
|
110
|
+
// If there are items and no waiters, pop one and return it
|
|
111
|
+
item = this._unreadQueue.dequeue();
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// If there are no items and/or there are already waiters, we have to wait for one to be ready
|
|
115
|
+
await this.waitForWrite();
|
|
116
|
+
item = this._unreadQueue.dequeue();
|
|
117
|
+
}
|
|
118
|
+
if (this._writersWaiting.length > 0) {
|
|
119
|
+
// Release a writer if one is waiting
|
|
120
|
+
// Remove the first waiting reader Promise from the queue
|
|
121
|
+
const writer = this._writersWaiting.dequeue();
|
|
122
|
+
if (writer === undefined) {
|
|
123
|
+
throw new TypeError('writer queue returned undefined');
|
|
131
124
|
}
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
// Resolve the Promise for a waiting writer
|
|
126
|
+
writer(undefined);
|
|
127
|
+
}
|
|
128
|
+
return item;
|
|
134
129
|
}
|
|
135
130
|
releaseAllReaders() {
|
|
136
131
|
// Release all the waiting readers since we're done
|
|
@@ -172,30 +167,26 @@ class BlockingQueue {
|
|
|
172
167
|
/**
|
|
173
168
|
* Waits for an item to be ready in the queue
|
|
174
169
|
*/
|
|
175
|
-
waitForWrite() {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this._readersWaiting.enqueue(resolve);
|
|
181
|
-
});
|
|
182
|
-
// Wait for our item to be ready
|
|
183
|
-
yield itemReady;
|
|
170
|
+
async waitForWrite() {
|
|
171
|
+
// If there are waiters, create a waiter promise and add to end of list
|
|
172
|
+
// Note: the background reader removes the promise from the readers waiting queue
|
|
173
|
+
const itemReady = new Promise((resolve) => {
|
|
174
|
+
this._readersWaiting.enqueue(resolve);
|
|
184
175
|
});
|
|
176
|
+
// Wait for our item to be ready
|
|
177
|
+
await itemReady;
|
|
185
178
|
}
|
|
186
179
|
/**
|
|
187
180
|
* Waits to be able to add an item to the queue when full
|
|
188
181
|
*/
|
|
189
|
-
waitForRead() {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this._writersWaiting.enqueue(resolve);
|
|
195
|
-
});
|
|
196
|
-
// Wait for our turn to add to the queue
|
|
197
|
-
yield writeReady;
|
|
182
|
+
async waitForRead() {
|
|
183
|
+
// If there are waiters, create a waiter promise and add to end of list
|
|
184
|
+
// Note: the background reader removes the promise from the readers waiting queue
|
|
185
|
+
const writeReady = new Promise((resolve) => {
|
|
186
|
+
this._writersWaiting.enqueue(resolve);
|
|
198
187
|
});
|
|
188
|
+
// Wait for our turn to add to the queue
|
|
189
|
+
await writeReady;
|
|
199
190
|
}
|
|
200
191
|
}
|
|
201
192
|
exports.BlockingQueue = BlockingQueue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocking-queue.js","sourceRoot":"","sources":["../../src/blocking-queue.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"blocking-queue.js","sourceRoot":"","sources":["../../src/blocking-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tslib_1 = require("tslib");
|
|
4
3
|
/// <reference types="jest" />
|
|
5
4
|
const blocking_queue_1 = require("./blocking-queue");
|
|
6
5
|
describe('BlockingQueue', () => {
|
|
@@ -11,36 +10,36 @@ describe('BlockingQueue', () => {
|
|
|
11
10
|
jest.clearAllMocks();
|
|
12
11
|
});
|
|
13
12
|
describe('maxUnread: 0', () => {
|
|
14
|
-
it('single item enqueue/dequeue works', () =>
|
|
13
|
+
it('single item enqueue/dequeue works', async () => {
|
|
15
14
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 0 });
|
|
16
15
|
setTimeout(() => {
|
|
17
16
|
void queue.enqueue(1);
|
|
18
17
|
}, 1000);
|
|
19
|
-
const item =
|
|
18
|
+
const item = await queue.dequeue();
|
|
20
19
|
queue.done();
|
|
21
20
|
expect(item).toBe(1);
|
|
22
|
-
})
|
|
23
|
-
it('dequeue after done does not hang', () =>
|
|
21
|
+
});
|
|
22
|
+
it('dequeue after done does not hang', async () => {
|
|
24
23
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 0 });
|
|
25
24
|
setTimeout(() => {
|
|
26
25
|
void queue.enqueue(1);
|
|
27
26
|
}, 1000);
|
|
28
|
-
const item =
|
|
27
|
+
const item = await queue.dequeue();
|
|
29
28
|
queue.done();
|
|
30
|
-
|
|
29
|
+
await queue.dequeue();
|
|
31
30
|
expect(item).toBe(1);
|
|
32
|
-
})
|
|
33
|
-
it('enqueue after done throws', () =>
|
|
31
|
+
});
|
|
32
|
+
it('enqueue after done throws', async () => {
|
|
34
33
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 0 });
|
|
35
34
|
setTimeout(() => {
|
|
36
35
|
void queue.enqueue(1);
|
|
37
36
|
}, 1000);
|
|
38
|
-
const item =
|
|
37
|
+
const item = await queue.dequeue();
|
|
39
38
|
queue.done();
|
|
40
|
-
|
|
39
|
+
await expect(async () => queue.enqueue(2)).rejects.toThrowError('`enqueue` called after `done` called');
|
|
41
40
|
expect(item).toBe(1);
|
|
42
|
-
})
|
|
43
|
-
it('balanced enqueue/dequeue works', () =>
|
|
41
|
+
});
|
|
42
|
+
it('balanced enqueue/dequeue works', async () => {
|
|
44
43
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 0 });
|
|
45
44
|
const writers = [];
|
|
46
45
|
writers.push(queue.enqueue(1));
|
|
@@ -48,13 +47,13 @@ describe('BlockingQueue', () => {
|
|
|
48
47
|
const readers = [];
|
|
49
48
|
readers.push(queue.dequeue());
|
|
50
49
|
readers.push(queue.dequeue());
|
|
51
|
-
|
|
50
|
+
await Promise.all(writers);
|
|
52
51
|
queue.done();
|
|
53
|
-
|
|
54
|
-
expect(
|
|
55
|
-
expect(
|
|
56
|
-
})
|
|
57
|
-
it('full queue blocks enqueue until dequeue', () =>
|
|
52
|
+
await Promise.all(readers);
|
|
53
|
+
expect(await readers[0]).toBe(1);
|
|
54
|
+
expect(await readers[1]).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
it('full queue blocks enqueue until dequeue', async () => {
|
|
58
57
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 1 });
|
|
59
58
|
// Add first item
|
|
60
59
|
void queue.enqueue(1);
|
|
@@ -63,47 +62,47 @@ describe('BlockingQueue', () => {
|
|
|
63
62
|
void queue.enqueue(2);
|
|
64
63
|
}, 2000);
|
|
65
64
|
const startTime = Date.now();
|
|
66
|
-
expect(
|
|
65
|
+
expect(await queue.dequeue()).toBe(1);
|
|
67
66
|
expect(Date.now() - startTime).toBeLessThan(2000);
|
|
68
|
-
expect(
|
|
69
|
-
expect(Date.now() - startTime).toBeGreaterThanOrEqual(2000);
|
|
67
|
+
expect(await queue.dequeue()).toBe(2);
|
|
68
|
+
expect(Math.ceil(Date.now() - startTime)).toBeGreaterThanOrEqual(2000);
|
|
70
69
|
queue.done();
|
|
71
|
-
})
|
|
70
|
+
});
|
|
72
71
|
});
|
|
73
72
|
describe('maxUnread: 1', () => {
|
|
74
|
-
it('single item enqueue/dequeue works', () =>
|
|
73
|
+
it('single item enqueue/dequeue works', async () => {
|
|
75
74
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 1 });
|
|
76
|
-
|
|
77
|
-
const item =
|
|
75
|
+
await queue.enqueue(1);
|
|
76
|
+
const item = await queue.dequeue();
|
|
78
77
|
queue.done();
|
|
79
78
|
expect(item).toBe(1);
|
|
80
|
-
})
|
|
81
|
-
it('dequeue after done does not hang', () =>
|
|
79
|
+
});
|
|
80
|
+
it('dequeue after done does not hang', async () => {
|
|
82
81
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 1 });
|
|
83
|
-
|
|
84
|
-
const item =
|
|
82
|
+
await queue.enqueue(1);
|
|
83
|
+
const item = await queue.dequeue();
|
|
85
84
|
queue.done();
|
|
86
|
-
|
|
85
|
+
await queue.dequeue();
|
|
87
86
|
expect(item).toBe(1);
|
|
88
|
-
})
|
|
89
|
-
it('dequeue after done and empty does not hang', () =>
|
|
87
|
+
});
|
|
88
|
+
it('dequeue after done and empty does not hang', async () => {
|
|
90
89
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 1 });
|
|
91
|
-
|
|
92
|
-
const item =
|
|
90
|
+
await queue.enqueue(1);
|
|
91
|
+
const item = await queue.dequeue();
|
|
93
92
|
queue.done();
|
|
94
|
-
|
|
93
|
+
await queue.dequeue();
|
|
95
94
|
expect(item).toBe(1);
|
|
96
|
-
|
|
97
|
-
})
|
|
98
|
-
it('enqueue after done throws', () =>
|
|
95
|
+
await queue.dequeue();
|
|
96
|
+
});
|
|
97
|
+
it('enqueue after done throws', async () => {
|
|
99
98
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 1 });
|
|
100
|
-
|
|
101
|
-
const item =
|
|
99
|
+
await queue.enqueue(1);
|
|
100
|
+
const item = await queue.dequeue();
|
|
102
101
|
queue.done();
|
|
103
|
-
|
|
102
|
+
await expect(async () => queue.enqueue(2)).rejects.toThrowError('`enqueue` called after `done` called');
|
|
104
103
|
expect(item).toBe(1);
|
|
105
|
-
})
|
|
106
|
-
it('balanced enqueue/dequeue works', () =>
|
|
104
|
+
});
|
|
105
|
+
it('balanced enqueue/dequeue works', async () => {
|
|
107
106
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 1 });
|
|
108
107
|
const writers = [];
|
|
109
108
|
writers.push(queue.enqueue(1));
|
|
@@ -111,13 +110,13 @@ describe('BlockingQueue', () => {
|
|
|
111
110
|
const readers = [];
|
|
112
111
|
readers.push(queue.dequeue());
|
|
113
112
|
readers.push(queue.dequeue());
|
|
114
|
-
|
|
113
|
+
await Promise.all(writers);
|
|
115
114
|
queue.done();
|
|
116
|
-
|
|
117
|
-
expect(
|
|
118
|
-
expect(
|
|
119
|
-
})
|
|
120
|
-
it('more dequeue than enqueue works', () =>
|
|
115
|
+
await Promise.all(readers);
|
|
116
|
+
expect(await readers[0]).toBe(1);
|
|
117
|
+
expect(await readers[1]).toBe(2);
|
|
118
|
+
});
|
|
119
|
+
it('more dequeue than enqueue works', async () => {
|
|
121
120
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 1 });
|
|
122
121
|
const writers = [];
|
|
123
122
|
writers.push(queue.enqueue(1));
|
|
@@ -127,49 +126,49 @@ describe('BlockingQueue', () => {
|
|
|
127
126
|
readers.push(queue.dequeue());
|
|
128
127
|
readers.push(queue.dequeue());
|
|
129
128
|
readers.push(queue.dequeue());
|
|
130
|
-
|
|
129
|
+
await Promise.all(writers);
|
|
131
130
|
queue.done();
|
|
132
|
-
|
|
133
|
-
expect(
|
|
134
|
-
expect(
|
|
135
|
-
expect(
|
|
136
|
-
expect(
|
|
137
|
-
})
|
|
138
|
-
it('full queue blocks enqueue until dequeue', () =>
|
|
131
|
+
await Promise.all(readers);
|
|
132
|
+
expect(await readers[0]).toBe(1);
|
|
133
|
+
expect(await readers[1]).toBe(2);
|
|
134
|
+
expect(await readers[2]).toBeUndefined();
|
|
135
|
+
expect(await readers[3]).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
it('full queue blocks enqueue until dequeue', async () => {
|
|
139
138
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 1 });
|
|
140
139
|
// Wait for first item to add
|
|
141
|
-
|
|
140
|
+
await queue.enqueue(1);
|
|
142
141
|
// Do not wait for second item to add (it will wait until an enqueue completes)
|
|
143
142
|
setTimeout(() => {
|
|
144
143
|
void queue.enqueue(2);
|
|
145
144
|
}, 2000);
|
|
146
|
-
expect(
|
|
145
|
+
expect(await queue.dequeue()).toBe(1);
|
|
147
146
|
const startTime = Date.now();
|
|
148
|
-
expect(
|
|
149
|
-
const duration = Date.now() - startTime;
|
|
147
|
+
expect(await queue.dequeue()).toBe(2);
|
|
148
|
+
const duration = Math.ceil(Date.now() - startTime);
|
|
150
149
|
expect(duration).toBeGreaterThanOrEqual(2000);
|
|
151
150
|
queue.done();
|
|
152
|
-
})
|
|
151
|
+
});
|
|
153
152
|
});
|
|
154
153
|
describe('maxUnread: 2', () => {
|
|
155
|
-
it('no reads until done works', () =>
|
|
154
|
+
it('no reads until done works', async () => {
|
|
156
155
|
const queue = new blocking_queue_1.BlockingQueue({ maxUnread: 2 });
|
|
157
156
|
const writers = [];
|
|
158
157
|
writers.push(queue.enqueue(1));
|
|
159
158
|
writers.push(queue.enqueue(2));
|
|
160
|
-
|
|
159
|
+
await Promise.all(writers);
|
|
161
160
|
queue.done();
|
|
162
161
|
const readers = [];
|
|
163
162
|
readers.push(queue.dequeue());
|
|
164
163
|
readers.push(queue.dequeue());
|
|
165
164
|
readers.push(queue.dequeue());
|
|
166
165
|
readers.push(queue.dequeue());
|
|
167
|
-
|
|
168
|
-
expect(
|
|
169
|
-
expect(
|
|
170
|
-
expect(
|
|
171
|
-
expect(
|
|
172
|
-
})
|
|
166
|
+
await Promise.all(readers);
|
|
167
|
+
expect(await readers[0]).toBe(1);
|
|
168
|
+
expect(await readers[1]).toBe(2);
|
|
169
|
+
expect(await readers[2]).toBeUndefined();
|
|
170
|
+
expect(await readers[3]).toBeUndefined();
|
|
171
|
+
});
|
|
173
172
|
});
|
|
174
173
|
});
|
|
175
174
|
//# sourceMappingURL=blocking-queue.test.js.map
|