@praxisjs/concurrent 1.3.8 → 1.3.9
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/CHANGELOG.md +23 -0
- package/dist/decorators.d.ts +3 -3
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +23 -38
- package/dist/decorators.js.map +1 -1
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +64 -43
- package/dist/pool.js.map +1 -1
- package/dist/queue.d.ts.map +1 -1
- package/dist/queue.js +53 -50
- package/dist/queue.js.map +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +39 -33
- package/dist/task.js.map +1 -1
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +42 -0
- package/dist/utils.js.map +1 -1
- package/package.json +15 -6
- package/src/decorators.ts +37 -38
- package/src/pool.ts +64 -46
- package/src/queue.ts +60 -56
- package/src/task.ts +38 -34
- package/src/utils.ts +55 -0
- package/dist/__tests__/decorators.test.d.ts +0 -2
- package/dist/__tests__/decorators.test.d.ts.map +0 -1
- package/dist/__tests__/decorators.test.js +0 -195
- package/dist/__tests__/decorators.test.js.map +0 -1
- package/dist/__tests__/pool.test.d.ts +0 -2
- package/dist/__tests__/pool.test.d.ts.map +0 -1
- package/dist/__tests__/pool.test.js +0 -196
- package/dist/__tests__/pool.test.js.map +0 -1
- package/dist/__tests__/queue.test.d.ts +0 -2
- package/dist/__tests__/queue.test.d.ts.map +0 -1
- package/dist/__tests__/queue.test.js +0 -173
- package/dist/__tests__/queue.test.js.map +0 -1
- package/dist/__tests__/task.test.d.ts +0 -2
- package/dist/__tests__/task.test.d.ts.map +0 -1
- package/dist/__tests__/task.test.js +0 -182
- package/dist/__tests__/task.test.js.map +0 -1
- package/dist/__tests__/utils.test.d.ts +0 -2
- package/dist/__tests__/utils.test.d.ts.map +0 -1
- package/dist/__tests__/utils.test.js +0 -32
- package/dist/__tests__/utils.test.js.map +0 -1
- package/src/__tests__/decorators.test.ts +0 -267
- package/src/__tests__/pool.test.ts +0 -212
- package/src/__tests__/queue.test.ts +0 -197
- package/src/__tests__/task.test.ts +0 -205
- package/src/__tests__/utils.test.ts +0 -48
- package/tsconfig.json +0 -8
package/dist/utils.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
+
import { createEffect, disposeEffect, pauseTracking, readSignal, resumeTracking, } from "@praxisjs/core/internal";
|
|
2
|
+
const signalParams = new WeakMap();
|
|
1
3
|
/**
|
|
2
4
|
* Returns true if the function declares `signal` as its first parameter name.
|
|
3
5
|
* Used to decide whether to inject an AbortSignal at call time.
|
|
4
6
|
*
|
|
5
7
|
* Works on unminified source (class methods, arrow functions, async functions).
|
|
6
8
|
* Bound functions always return false — check the original method before binding.
|
|
9
|
+
*
|
|
10
|
+
* Cached per function: a decorated method is the same prototype function for every instance, and
|
|
11
|
+
* reading its source on each construction was the main cost of creating a decorated component.
|
|
7
12
|
*/
|
|
8
13
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
14
|
export function acceptsSignal(fn) {
|
|
15
|
+
let result = signalParams.get(fn);
|
|
16
|
+
if (result === undefined) {
|
|
17
|
+
result = readsSignalParam(fn);
|
|
18
|
+
signalParams.set(fn, result);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
function readsSignalParam(fn) {
|
|
10
24
|
try {
|
|
11
25
|
const src = fn.toString();
|
|
12
26
|
// Match the first character of the parameter list, e.g.:
|
|
@@ -23,4 +37,32 @@ export function acceptsSignal(fn) {
|
|
|
23
37
|
return false;
|
|
24
38
|
}
|
|
25
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* A read-only `Computed` view of a signal node: reading it tracks the node directly, without the
|
|
42
|
+
* extra computed node, getter closure and recomputation a `computed(() => sig())` wrapper costs.
|
|
43
|
+
*/
|
|
44
|
+
export function readonlyView(node) {
|
|
45
|
+
const view = (() => readSignal(node));
|
|
46
|
+
view.subscribe = (fn) => {
|
|
47
|
+
const effect = createEffect(() => {
|
|
48
|
+
const value = readSignal(node);
|
|
49
|
+
const prev = pauseTracking();
|
|
50
|
+
try {
|
|
51
|
+
fn(value);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
resumeTracking(prev);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return () => { disposeEffect(effect); };
|
|
58
|
+
};
|
|
59
|
+
view.__isComputed = true;
|
|
60
|
+
return view;
|
|
61
|
+
}
|
|
62
|
+
export function isAbortError(err) {
|
|
63
|
+
return err instanceof Error && err.name === "AbortError";
|
|
64
|
+
}
|
|
65
|
+
export function toError(err) {
|
|
66
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
67
|
+
}
|
|
26
68
|
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,aAAa,EACb,UAAU,EACV,cAAc,GAEf,MAAM,yBAAyB,CAAC;AAGjC,MAAM,YAAY,GAAG,IAAI,OAAO,EAAmB,CAAC;AAEpD;;;;;;;;;GASG;AACH,8DAA8D;AAC9D,MAAM,UAAU,aAAa,CAAC,EAA2B;IACvD,IAAI,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,GAAG,gBAAgB,CAAC,EAAE,CAAC,CAAC;QAC9B,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,8DAA8D;AAC9D,SAAS,gBAAgB,CAAC,EAA2B;IACnD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;QAC1B,yDAAyD;QACzD,4CAA4C;QAC5C,6EAA6E;QAC7E,yCAAyC;QACzC,sCAAsC;QACtC,MAAM,CAAC,GAAG,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACnD,OAAO,SAAS,KAAK,QAAQ,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAI,IAAmB;IACjD,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAgB,CAAC;IACrD,IAAI,CAAC,SAAS,GAAG,CAAC,EAAsB,EAAE,EAAE;QAC1C,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE;YAC/B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACH,EAAE,CAAC,KAAK,CAAC,CAAC;YACZ,CAAC;oBAAS,CAAC;gBACT,cAAc,CAAC,IAAI,CAAC,CAAC;YACvB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC;IACF,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;IACzB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,OAAO,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,GAAY;IAClC,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7D,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/concurrent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.9",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"types": "./dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
@@ -10,16 +11,24 @@
|
|
|
10
11
|
"types": "./dist/index.d.ts"
|
|
11
12
|
}
|
|
12
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"!dist/**/__tests__",
|
|
18
|
+
"!src/**/__tests__",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
13
21
|
"devDependencies": {
|
|
14
22
|
"typescript": "^6.0.3"
|
|
15
23
|
},
|
|
16
24
|
"dependencies": {
|
|
17
|
-
"@praxisjs/core": "2.
|
|
18
|
-
"@praxisjs/
|
|
19
|
-
"@praxisjs/
|
|
25
|
+
"@praxisjs/core": "2.3.0",
|
|
26
|
+
"@praxisjs/shared": "0.3.2",
|
|
27
|
+
"@praxisjs/decorators": "2.0.0"
|
|
20
28
|
},
|
|
21
29
|
"scripts": {
|
|
22
|
-
"build": "tsc",
|
|
23
|
-
"dev": "tsc --watch"
|
|
30
|
+
"build": "tsc -p tsconfig.build.json",
|
|
31
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
24
33
|
}
|
|
25
34
|
}
|
package/src/decorators.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertFieldContext } from "@praxisjs/decorators";
|
|
2
2
|
import type { Computed } from "@praxisjs/shared";
|
|
3
3
|
|
|
4
4
|
import { pool } from "./pool";
|
|
@@ -58,54 +58,53 @@ export type QueueOf<C, K extends keyof C> =
|
|
|
58
58
|
export type PoolOf<C, K extends keyof C> =
|
|
59
59
|
C[K] extends AnyAsyncFn ? PoolDecorated<C[K]> : never;
|
|
60
60
|
|
|
61
|
+
// ── Field decorators ───────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
// Each decorator returns the field's initializer: the value is created per instance while the
|
|
64
|
+
// field is defined, instead of redefining the field afterwards with Object.defineProperty.
|
|
65
|
+
function fieldFrom(
|
|
66
|
+
methodName: string,
|
|
67
|
+
create: (method: AnyAsyncFn, wantsSignal: boolean) => unknown,
|
|
68
|
+
): (this: object) => unknown {
|
|
69
|
+
return function (this: object): unknown {
|
|
70
|
+
const method = (this as Record<string, AnyAsyncFn>)[methodName];
|
|
71
|
+
// Detect signal on the unbound method — bound functions lose their source.
|
|
72
|
+
return create(method.bind(this), acceptsSignal(method));
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
// ── @Task ──────────────────────────────────────────────────────────────────────
|
|
62
77
|
|
|
63
78
|
export function Task(methodName: string) {
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
value: task(inst[methodName].bind(instance), { signal: wantsSignal }) as unknown,
|
|
72
|
-
writable: true,
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
});
|
|
79
|
+
return function <This extends object, Value = unknown>(
|
|
80
|
+
_value: undefined,
|
|
81
|
+
context: ClassFieldDecoratorContext<This, Value>,
|
|
82
|
+
): (this: This) => Value {
|
|
83
|
+
assertFieldContext(context);
|
|
84
|
+
return fieldFrom(methodName, (method, signal) => task(method, { signal })) as (this: This) => Value;
|
|
85
|
+
};
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
// ── @Queue ─────────────────────────────────────────────────────────────────────
|
|
80
89
|
|
|
81
90
|
export function Queue(methodName: string) {
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
writable: true,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
},
|
|
93
|
-
});
|
|
91
|
+
return function <This extends object, Value = unknown>(
|
|
92
|
+
_value: undefined,
|
|
93
|
+
context: ClassFieldDecoratorContext<This, Value>,
|
|
94
|
+
): (this: This) => Value {
|
|
95
|
+
assertFieldContext(context);
|
|
96
|
+
return fieldFrom(methodName, (method, signal) => queue(method, { signal })) as (this: This) => Value;
|
|
97
|
+
};
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
// ── @Pool ──────────────────────────────────────────────────────────────────────
|
|
97
101
|
|
|
98
102
|
export function Pool(methodName: string, concurrency = 1) {
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
writable: true,
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
},
|
|
110
|
-
});
|
|
103
|
+
return function <This extends object, Value = unknown>(
|
|
104
|
+
_value: undefined,
|
|
105
|
+
context: ClassFieldDecoratorContext<This, Value>,
|
|
106
|
+
): (this: This) => Value {
|
|
107
|
+
assertFieldContext(context);
|
|
108
|
+
return fieldFrom(methodName, (method, signal) => pool(concurrency, method, { signal })) as (this: This) => Value;
|
|
109
|
+
};
|
|
111
110
|
}
|
package/src/pool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { endBatch, SignalNode, startBatch, writeSignal } from "@praxisjs/core/internal";
|
|
2
2
|
import type { Computed } from "@praxisjs/shared";
|
|
3
3
|
|
|
4
|
-
import { acceptsSignal } from "./utils";
|
|
4
|
+
import { acceptsSignal, isAbortError, readonlyView, toError } from "./utils";
|
|
5
5
|
|
|
6
6
|
export interface PoolInstance<T> {
|
|
7
7
|
(...args: unknown[]): Promise<T | undefined>;
|
|
@@ -12,8 +12,10 @@ export interface PoolInstance<T> {
|
|
|
12
12
|
cancelAll(): void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
interface PoolItem<T> {
|
|
16
|
+
args: unknown[];
|
|
17
|
+
resolve: (v: T | undefined) => void;
|
|
18
|
+
controller: AbortController | undefined;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function pool<T>(
|
|
@@ -22,65 +24,81 @@ export function pool<T>(
|
|
|
22
24
|
options?: { signal?: boolean },
|
|
23
25
|
): PoolInstance<T> {
|
|
24
26
|
concurrency = Math.max(1, concurrency);
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
const activeNode = new SignalNode(0);
|
|
28
|
+
const pendingNode = new SignalNode(0);
|
|
29
|
+
const loadingNode = new SignalNode(false);
|
|
30
|
+
const error = new SignalNode<Error | null>(null);
|
|
31
|
+
const wantsSignal = options?.signal ?? acceptsSignal(fn);
|
|
32
|
+
// Plain counters drive scheduling: reading the signals here would subscribe whatever effect
|
|
33
|
+
// happens to call the pool.
|
|
34
|
+
let active = 0;
|
|
35
|
+
let items: Array<PoolItem<T> | undefined> = [];
|
|
36
|
+
let head = 0;
|
|
37
|
+
const activeControllers = new Set<AbortController>();
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
function publish(): void {
|
|
40
|
+
startBatch();
|
|
41
|
+
writeSignal(activeNode, active);
|
|
42
|
+
writeSignal(pendingNode, items.length - head);
|
|
43
|
+
writeSignal(loadingNode, active > 0);
|
|
44
|
+
endBatch();
|
|
45
|
+
}
|
|
40
46
|
|
|
47
|
+
async function execute(item: PoolItem<T>): Promise<void> {
|
|
41
48
|
const { args, resolve, controller } = item;
|
|
42
|
-
|
|
43
|
-
_active.update((n) => n + 1);
|
|
44
|
-
_activeControllers.add(controller);
|
|
49
|
+
if (controller) activeControllers.add(controller);
|
|
45
50
|
try {
|
|
46
|
-
|
|
47
|
-
resolve(result);
|
|
51
|
+
resolve(await (controller ? fn(controller.signal, ...args) : fn(...args)));
|
|
48
52
|
} catch (err) {
|
|
49
|
-
if (!isAbortError(err))
|
|
50
|
-
_error.set(err instanceof Error ? err : new Error(String(err)));
|
|
51
|
-
}
|
|
53
|
+
if (!isAbortError(err)) writeSignal(error, toError(err));
|
|
52
54
|
resolve(undefined);
|
|
53
55
|
} finally {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
if (controller) activeControllers.delete(controller);
|
|
57
|
+
active--;
|
|
58
|
+
pump();
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
function pump(): void {
|
|
63
|
+
while (active < concurrency && head < items.length) {
|
|
64
|
+
const item = items[head] as PoolItem<T>;
|
|
65
|
+
items[head++] = undefined;
|
|
66
|
+
if (head === items.length) {
|
|
67
|
+
items = [];
|
|
68
|
+
head = 0;
|
|
69
|
+
}
|
|
70
|
+
active++;
|
|
71
|
+
void execute(item);
|
|
72
|
+
}
|
|
73
|
+
publish();
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
function run(...args: unknown[]): Promise<T | undefined> {
|
|
61
|
-
const controller = new AbortController();
|
|
62
77
|
return new Promise<T | undefined>((resolve) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
items.push({ args, resolve, controller: wantsSignal ? new AbortController() : undefined });
|
|
79
|
+
// While every slot is busy only the backlog grows.
|
|
80
|
+
if (active < concurrency) pump();
|
|
81
|
+
else writeSignal(pendingNode, items.length - head);
|
|
66
82
|
});
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
run.loading =
|
|
70
|
-
run.active =
|
|
71
|
-
run.pending =
|
|
72
|
-
run.error =
|
|
85
|
+
run.loading = readonlyView(loadingNode);
|
|
86
|
+
run.active = readonlyView(activeNode);
|
|
87
|
+
run.pending = readonlyView(pendingNode);
|
|
88
|
+
run.error = readonlyView(error);
|
|
73
89
|
run.cancelAll = () => {
|
|
74
|
-
for (const c of
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
item
|
|
90
|
+
for (const c of activeControllers) c.abort();
|
|
91
|
+
activeControllers.clear();
|
|
92
|
+
const cleared = items;
|
|
93
|
+
const from = head;
|
|
94
|
+
items = [];
|
|
95
|
+
head = 0;
|
|
96
|
+
for (let i = from; i < cleared.length; i++) {
|
|
97
|
+
const item = cleared[i] as PoolItem<T>;
|
|
98
|
+
item.controller?.abort();
|
|
99
|
+
item.resolve(undefined);
|
|
82
100
|
}
|
|
83
|
-
|
|
101
|
+
publish();
|
|
84
102
|
};
|
|
85
103
|
|
|
86
104
|
return run as PoolInstance<T>;
|
package/src/queue.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SignalNode, writeSignal } from "@praxisjs/core/internal";
|
|
2
2
|
import type { Computed } from "@praxisjs/shared";
|
|
3
3
|
|
|
4
|
-
import { acceptsSignal } from "./utils";
|
|
4
|
+
import { acceptsSignal, isAbortError, readonlyView, toError } from "./utils";
|
|
5
5
|
|
|
6
6
|
export class QueueClearedError extends Error {
|
|
7
7
|
constructor() {
|
|
@@ -10,10 +10,6 @@ export class QueueClearedError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function isAbortError(err: unknown): boolean {
|
|
14
|
-
return err instanceof Error && err.name === "AbortError";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
13
|
export interface QueueInstance<T> {
|
|
18
14
|
(...args: unknown[]): Promise<T>;
|
|
19
15
|
loading: Computed<boolean>;
|
|
@@ -22,86 +18,94 @@ export interface QueueInstance<T> {
|
|
|
22
18
|
clear(): void;
|
|
23
19
|
}
|
|
24
20
|
|
|
21
|
+
interface QueueItem<T> {
|
|
22
|
+
args: unknown[];
|
|
23
|
+
resolve: (v: T) => void;
|
|
24
|
+
reject: (e: Error) => void;
|
|
25
|
+
controller: AbortController | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
25
28
|
export function queue<T>(
|
|
26
29
|
fn: (...args: unknown[]) => Promise<T>,
|
|
27
30
|
options?: { signal?: boolean },
|
|
28
31
|
): QueueInstance<T> {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let
|
|
40
|
-
let _cleared = false;
|
|
41
|
-
let _activeController: AbortController | null = null;
|
|
42
|
-
let _activeAbortedByClear = false;
|
|
32
|
+
const loading = new SignalNode(false);
|
|
33
|
+
const pending = new SignalNode(0);
|
|
34
|
+
const error = new SignalNode<Error | null>(null);
|
|
35
|
+
const wantsSignal = options?.signal ?? acceptsSignal(fn);
|
|
36
|
+
// FIFO read from `head`: shifting the array instead made draining n calls O(n²).
|
|
37
|
+
let items: Array<QueueItem<T> | undefined> = [];
|
|
38
|
+
let head = 0;
|
|
39
|
+
let running = false;
|
|
40
|
+
let inCall = false;
|
|
41
|
+
let activeController: AbortController | undefined;
|
|
42
|
+
let activeAbortedByClear = false;
|
|
43
43
|
|
|
44
44
|
async function drain(): Promise<void> {
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
while (
|
|
49
|
-
const item =
|
|
50
|
-
|
|
45
|
+
if (running || head === items.length) return;
|
|
46
|
+
running = true;
|
|
47
|
+
writeSignal(loading, true);
|
|
48
|
+
while (head < items.length) {
|
|
49
|
+
const item = items[head] as QueueItem<T>;
|
|
50
|
+
items[head++] = undefined;
|
|
51
|
+
if (head === items.length) {
|
|
52
|
+
items = [];
|
|
53
|
+
head = 0;
|
|
54
|
+
}
|
|
51
55
|
|
|
52
56
|
const { args, resolve, reject, controller } = item;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
activeController = controller;
|
|
58
|
+
activeAbortedByClear = false;
|
|
59
|
+
inCall = true;
|
|
60
|
+
writeSignal(pending, items.length - head);
|
|
56
61
|
try {
|
|
57
|
-
const result = await (
|
|
62
|
+
const result = await (controller ? fn(controller.signal, ...args) : fn(...args));
|
|
58
63
|
resolve(result);
|
|
59
64
|
} catch (err) {
|
|
60
|
-
//
|
|
65
|
+
// activeAbortedByClear is set by clear() while the call is awaited
|
|
61
66
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
62
|
-
if (isAbortError(err) &&
|
|
67
|
+
if (isAbortError(err) && activeAbortedByClear) {
|
|
63
68
|
reject(new QueueClearedError());
|
|
64
69
|
} else {
|
|
65
|
-
const e =
|
|
66
|
-
|
|
70
|
+
const e = toError(err);
|
|
71
|
+
writeSignal(error, e);
|
|
67
72
|
reject(e);
|
|
68
73
|
}
|
|
69
74
|
} finally {
|
|
70
|
-
|
|
75
|
+
inCall = false;
|
|
76
|
+
activeController = undefined;
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
_loading.set(false);
|
|
79
|
+
running = false;
|
|
80
|
+
writeSignal(loading, false);
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
function enqueue(...args: unknown[]): Promise<T> {
|
|
79
|
-
if (_cleared) {
|
|
80
|
-
_cleared = false;
|
|
81
|
-
}
|
|
82
|
-
const controller = new AbortController();
|
|
83
84
|
return new Promise<T>((resolve, reject) => {
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
items.push({ args, resolve, reject, controller: wantsSignal ? new AbortController() : undefined });
|
|
86
|
+
writeSignal(pending, items.length - head);
|
|
86
87
|
void drain();
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
enqueue.loading =
|
|
91
|
-
enqueue.pending =
|
|
92
|
-
enqueue.error =
|
|
91
|
+
enqueue.loading = readonlyView(loading);
|
|
92
|
+
enqueue.pending = readonlyView(pending);
|
|
93
|
+
enqueue.error = readonlyView(error);
|
|
93
94
|
enqueue.clear = () => {
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
if (inCall) {
|
|
96
|
+
activeAbortedByClear = true;
|
|
97
|
+
activeController?.abort();
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
const cleared = items;
|
|
100
|
+
const from = head;
|
|
101
|
+
items = [];
|
|
102
|
+
head = 0;
|
|
103
|
+
for (let i = from; i < cleared.length; i++) {
|
|
104
|
+
const item = cleared[i] as QueueItem<T>;
|
|
105
|
+
item.controller?.abort();
|
|
106
|
+
item.reject(new QueueClearedError());
|
|
102
107
|
}
|
|
103
|
-
|
|
104
|
-
_cleared = true;
|
|
108
|
+
writeSignal(pending, 0);
|
|
105
109
|
};
|
|
106
110
|
|
|
107
111
|
return enqueue as QueueInstance<T>;
|
package/src/task.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { endBatch, SignalNode, startBatch, writeSignal } from "@praxisjs/core/internal";
|
|
2
2
|
import type { Computed } from "@praxisjs/shared";
|
|
3
3
|
|
|
4
|
-
import { acceptsSignal } from "./utils";
|
|
4
|
+
import { acceptsSignal, isAbortError, readonlyView, toError } from "./utils";
|
|
5
5
|
|
|
6
6
|
export interface TaskInstance<T> {
|
|
7
7
|
(...args: unknown[]): Promise<T | undefined>;
|
|
@@ -11,52 +11,56 @@ export interface TaskInstance<T> {
|
|
|
11
11
|
cancelAll(): void;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function isAbortError(err: unknown): boolean {
|
|
15
|
-
return err instanceof Error && err.name === "AbortError";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
14
|
export function task<T>(
|
|
19
15
|
fn: (...args: unknown[]) => Promise<T>,
|
|
20
16
|
options?: { signal?: boolean },
|
|
21
17
|
): TaskInstance<T> {
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
const loading = new SignalNode(false);
|
|
19
|
+
const error = new SignalNode<Error | null>(null);
|
|
20
|
+
const lastResult = new SignalNode<T | null>(null);
|
|
21
|
+
let runId = 0;
|
|
22
|
+
// Only created when `fn` takes a signal: nothing else could observe the abort.
|
|
23
|
+
let controller: AbortController | null = null;
|
|
24
|
+
const wantsSignal = options?.signal ?? acceptsSignal(fn);
|
|
28
25
|
|
|
29
26
|
async function run(...args: unknown[]): Promise<T | undefined> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
const id = ++runId;
|
|
28
|
+
let signal: AbortSignal | undefined;
|
|
29
|
+
if (wantsSignal) {
|
|
30
|
+
controller?.abort();
|
|
31
|
+
controller = new AbortController();
|
|
32
|
+
signal = controller.signal;
|
|
33
|
+
}
|
|
34
|
+
startBatch();
|
|
35
|
+
writeSignal(loading, true);
|
|
36
|
+
writeSignal(error, null);
|
|
37
|
+
endBatch();
|
|
36
38
|
try {
|
|
37
|
-
const result = await (
|
|
38
|
-
if (id !==
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const result = await (signal ? fn(signal, ...args) : fn(...args));
|
|
40
|
+
if (id !== runId) return undefined;
|
|
41
|
+
startBatch();
|
|
42
|
+
writeSignal(lastResult, result);
|
|
43
|
+
writeSignal(loading, false);
|
|
44
|
+
endBatch();
|
|
41
45
|
return result;
|
|
42
46
|
} catch (err) {
|
|
43
|
-
if (id !==
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
if (id !== runId) return undefined;
|
|
48
|
+
startBatch();
|
|
49
|
+
if (!isAbortError(err)) writeSignal(error, toError(err));
|
|
50
|
+
writeSignal(loading, false);
|
|
51
|
+
endBatch();
|
|
48
52
|
return undefined;
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
run.loading =
|
|
53
|
-
run.error =
|
|
54
|
-
run.lastResult =
|
|
56
|
+
run.loading = readonlyView(loading);
|
|
57
|
+
run.error = readonlyView(error);
|
|
58
|
+
run.lastResult = readonlyView(lastResult);
|
|
55
59
|
run.cancelAll = () => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
controller?.abort();
|
|
61
|
+
controller = null;
|
|
62
|
+
runId++;
|
|
63
|
+
writeSignal(loading, false);
|
|
60
64
|
};
|
|
61
65
|
|
|
62
66
|
return run as TaskInstance<T>;
|