Skip to content

Commit cac31fe

Browse files
committed
fix: add optional concurrency parameter to Task.sequence
1 parent 1a9f785 commit cac31fe

File tree

8 files changed

+2821
-3760
lines changed

8 files changed

+2821
-3760
lines changed

.devcontainer/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.154.2/containers/typescript-node/.devcontainer/base.Dockerfile
22

3-
# [Choice] Node.js version: 14, 12, 10
4-
ARG VARIANT="14-buster"
3+
# [Choice] Node.js version: 16, 14
4+
ARG VARIANT=16
55
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
66

77
# [Optional] Uncomment this section to install additional OS packages.

.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
runs-on: ubuntu-latest
88
strategy:
99
matrix:
10-
node: ["16", "14", "12"]
10+
node: ["16", "14"]
1111
name: Node ${{ matrix.node }}
1212
steps:
1313
- uses: actions/checkout@master

docs/task-static.md

+3
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ Creates a task that will always run an array of tasks **serially**. The result i
196196
197197
All task error and value types must be the same. If you need different types for the items of the array, consider simply chaining the tasks together in order.
198198
199+
Additionally, a second parameter can be added to allow concurrency. The default concurreny is `1` task at a time, but may be increased. Setting this concurrency to Infinity is the same a `Task.all`.
200+
199201
{% tabs %}
200202
{% tab title="Usage" %}
201203
@@ -210,6 +212,7 @@ const task: Task<never, number[]> = Task.sequence([of(5), of(10)])
210212
```typescript
211213
type sequence = <E, S>(
212214
tasksOrPromises: Array<Task<E, S> | Promise<S>>,
215+
maxConcurrent = 1,
213216
) => Task<E, S[]>
214217
```
215218

package.json

+8-8
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"prepare": "husky install"
2020
},
2121
"engines": {
22-
"node": ">=10"
22+
"node": ">=14"
2323
},
2424
"@pika/pack": {
2525
"pipeline": [
@@ -49,20 +49,20 @@
4949
]
5050
},
5151
"volta": {
52-
"node": "14.11.0",
52+
"node": "14.17.0",
5353
"yarn": "1.22.4"
5454
},
5555
"devDependencies": {
5656
"@commitlint/cli": "^13.1.0",
5757
"@commitlint/config-conventional": "^13.2.0",
58-
"@commitlint/prompt": "^12.1.4",
58+
"@commitlint/prompt": "^13.2.0",
5959
"@pika/pack": "^0.5.0",
6060
"@pika/plugin-build-node": "^0.9.2",
6161
"@pika/plugin-build-web": "^0.9.2",
6262
"@pika/plugin-ts-standard-pkg": "^0.9.2",
63-
"@semantic-release/changelog": "^5.0.1",
64-
"@semantic-release/git": "^9.0.0",
65-
"@semantic-release/github": "^7.2.3",
63+
"@semantic-release/changelog": "^6.0.0",
64+
"@semantic-release/git": "^10.0.0",
65+
"@semantic-release/github": "^8.0.1",
6666
"@types/jest": "^27.0.2",
6767
"@types/react": "^17.0.26",
6868
"@typescript-eslint/eslint-plugin": "^4.31.1",
@@ -71,13 +71,13 @@
7171
"cz-conventional-changelog": "3.3.0",
7272
"eslint": "^7.32.0",
7373
"eslint-config-prettier": "^8.3.0",
74-
"eslint-plugin-prettier": "^3.4.0",
74+
"eslint-plugin-prettier": "^4.0.0",
7575
"gzip-size-cli": "^5.0.0",
7676
"husky": "^7.0.1",
7777
"jest": "^27.0.6",
7878
"lint-staged": "^11.1.2",
7979
"prettier": "^2.3.2",
80-
"semantic-release": "^17.4.7",
80+
"semantic-release": "^18.0.0",
8181
"terser": "^5.9.0",
8282
"ts-jest": "^27.0.5",
8383
"ts-toolbelt": "^9.6.0",

src/Task/Task.ts

+57-41
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-misused-promises, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-use-before-define */
2-
import { constant, identity, range, Validation } from "../util"
2+
import { constant, drop, identity, range, take, Validation } from "../util"
33

44
export type Reject<E> = (error: E) => void
55
export type Resolve<S> = (result: S) => void
@@ -393,41 +393,7 @@ export const firstSuccess = <E, S>(tasks: Array<Task<E, S>>): Task<E[], S> =>
393393
* @param tasks The tasks to run in parallel.
394394
*/
395395
export const all = <E, S>(tasks: Array<Task<E, S>>): Task<E, S[]> =>
396-
tasks.length === 0
397-
? of([])
398-
: new Task<E, S[]>((reject, resolve) => {
399-
let isDone = false
400-
let runningTasks = tasks.length
401-
402-
const results: S[] = []
403-
404-
return tasks.map((task, i) =>
405-
task.fork(
406-
(error: E) => {
407-
if (isDone) {
408-
return
409-
}
410-
411-
isDone = true
412-
413-
reject(error)
414-
},
415-
(result: S) => {
416-
if (isDone) {
417-
return
418-
}
419-
420-
runningTasks -= 1
421-
422-
results[i] = result
423-
424-
if (runningTasks === 0) {
425-
resolve(results)
426-
}
427-
},
428-
),
429-
)
430-
})
396+
sequence(tasks, Infinity)
431397

432398
/**
433399
* Given an array of task which return a result, return a new task which returns an array of successful results.
@@ -494,11 +460,61 @@ export const zipWith = <E, E2, S, S2, V>(
494460
* Given an array of task which return a result, return a new task which results an array of results.
495461
* @param tasks The tasks to run in sequence.
496462
*/
497-
export const sequence = <E, S>(tasks: Array<Task<E, S>>): Task<E, S[]> =>
498-
tasks.reduce(
499-
(sum, task) => chain(list => map(result => [...list, result], task), sum),
500-
succeed([]) as Task<E, S[]>,
501-
)
463+
export const sequence = <E, S>(
464+
tasks: Array<Task<E, S>>,
465+
maxConcurrent = 1,
466+
): Task<E, S[]> =>
467+
new Task((reject, resolve) => {
468+
let isDone = false
469+
470+
type TaskPosition = [Task<E, S>, number]
471+
472+
let queue = tasks.map<TaskPosition>((task, i) => [task, i])
473+
const inflight = new Set<Task<E, S>>()
474+
const results: S[] = []
475+
476+
const enqueue = () => {
477+
if (isDone) {
478+
return
479+
}
480+
481+
if (queue.length <= 0 && inflight.size <= 0) {
482+
isDone = true
483+
resolve(results)
484+
return
485+
}
486+
487+
const howMany = Math.min(queue.length, maxConcurrent - inflight.size)
488+
489+
const readyTasks = take(howMany, queue)
490+
queue = drop(howMany, queue)
491+
492+
readyTasks.forEach(([task, i]) => {
493+
inflight.add(task)
494+
495+
task.fork(
496+
(error: E) => {
497+
if (isDone) {
498+
return
499+
}
500+
501+
isDone = true
502+
503+
reject(error)
504+
},
505+
(result: S) => {
506+
results[i] = result
507+
508+
inflight.delete(task)
509+
510+
enqueue()
511+
},
512+
)
513+
})
514+
}
515+
516+
enqueue()
517+
})
502518

503519
/**
504520
* Given a task, swap the error and success values.

src/Task/__tests__/sequence.spec.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ describe("sequence", () => {
66
const resolve = jest.fn()
77
const reject = jest.fn()
88

9-
sequence([succeedIn(100, "A"), succeedIn(100, "B")]).fork(reject, resolve)
9+
sequence([succeedIn(100, "A"), succeedIn(100, "B")], 1).fork(
10+
reject,
11+
resolve,
12+
)
1013

1114
jest.advanceTimersByTime(100)
1215

src/util.ts

+6
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,9 @@ export const failedValidation = <E>(error: E): Validation<E, never> => ({
6969
success: false,
7070
error,
7171
})
72+
73+
export const take = <T>(count: number, arr: Array<T>): Array<T> =>
74+
!isFinite(count) ? [...arr] : arr.slice(0, count)
75+
76+
export const drop = <T>(count: number, arr: Array<T>): Array<T> =>
77+
!isFinite(count) ? [] : arr.splice(count)

0 commit comments

Comments
 (0)