@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/decorators.d.ts +3 -3
  3. package/dist/decorators.d.ts.map +1 -1
  4. package/dist/decorators.js +23 -38
  5. package/dist/decorators.js.map +1 -1
  6. package/dist/pool.d.ts.map +1 -1
  7. package/dist/pool.js +64 -43
  8. package/dist/pool.js.map +1 -1
  9. package/dist/queue.d.ts.map +1 -1
  10. package/dist/queue.js +53 -50
  11. package/dist/queue.js.map +1 -1
  12. package/dist/task.d.ts.map +1 -1
  13. package/dist/task.js +39 -33
  14. package/dist/task.js.map +1 -1
  15. package/dist/utils.d.ts +12 -0
  16. package/dist/utils.d.ts.map +1 -1
  17. package/dist/utils.js +42 -0
  18. package/dist/utils.js.map +1 -1
  19. package/package.json +15 -6
  20. package/src/decorators.ts +37 -38
  21. package/src/pool.ts +64 -46
  22. package/src/queue.ts +60 -56
  23. package/src/task.ts +38 -34
  24. package/src/utils.ts +55 -0
  25. package/dist/__tests__/decorators.test.d.ts +0 -2
  26. package/dist/__tests__/decorators.test.d.ts.map +0 -1
  27. package/dist/__tests__/decorators.test.js +0 -195
  28. package/dist/__tests__/decorators.test.js.map +0 -1
  29. package/dist/__tests__/pool.test.d.ts +0 -2
  30. package/dist/__tests__/pool.test.d.ts.map +0 -1
  31. package/dist/__tests__/pool.test.js +0 -196
  32. package/dist/__tests__/pool.test.js.map +0 -1
  33. package/dist/__tests__/queue.test.d.ts +0 -2
  34. package/dist/__tests__/queue.test.d.ts.map +0 -1
  35. package/dist/__tests__/queue.test.js +0 -173
  36. package/dist/__tests__/queue.test.js.map +0 -1
  37. package/dist/__tests__/task.test.d.ts +0 -2
  38. package/dist/__tests__/task.test.d.ts.map +0 -1
  39. package/dist/__tests__/task.test.js +0 -182
  40. package/dist/__tests__/task.test.js.map +0 -1
  41. package/dist/__tests__/utils.test.d.ts +0 -2
  42. package/dist/__tests__/utils.test.d.ts.map +0 -1
  43. package/dist/__tests__/utils.test.js +0 -32
  44. package/dist/__tests__/utils.test.js.map +0 -1
  45. package/src/__tests__/decorators.test.ts +0 -267
  46. package/src/__tests__/pool.test.ts +0 -212
  47. package/src/__tests__/queue.test.ts +0 -197
  48. package/src/__tests__/task.test.ts +0 -205
  49. package/src/__tests__/utils.test.ts +0 -48
  50. 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;;;;;;GAMG;AACH,8DAA8D;AAC9D,MAAM,UAAU,aAAa,CAAC,EAA2B;IACvD,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"}
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.8",
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.2.0",
18
- "@praxisjs/decorators": "1.7.0",
19
- "@praxisjs/shared": "0.3.1"
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 { createFieldDecorator } from "@praxisjs/decorators";
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 createFieldDecorator({
65
- bind(instance: object, _name: string) {
66
- const inst = instance as Record<string, AnyAsyncFn>;
67
- // Detect signal on the unbound method — bound functions lose their source.
68
- const wantsSignal = acceptsSignal(inst[methodName]);
69
- return {
70
- descriptor: {
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 createFieldDecorator({
83
- bind(instance: object, _name: string) {
84
- const inst = instance as Record<string, AnyAsyncFn>;
85
- const wantsSignal = acceptsSignal(inst[methodName]);
86
- return {
87
- descriptor: {
88
- value: queue(inst[methodName].bind(instance), { signal: wantsSignal }) as unknown,
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 createFieldDecorator({
100
- bind(instance: object, _name: string) {
101
- const inst = instance as Record<string, AnyAsyncFn>;
102
- const wantsSignal = acceptsSignal(inst[methodName]);
103
- return {
104
- descriptor: {
105
- value: pool(concurrency, inst[methodName].bind(instance), { signal: wantsSignal }) as unknown,
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 { signal, computed } from "@praxisjs/core/internal";
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
- function isAbortError(err: unknown): boolean {
16
- return err instanceof Error && err.name === "AbortError";
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 _active = signal(0);
26
- const _pending = signal(0);
27
- const _error = signal<Error | null>(null);
28
- const _wantsSignal = options?.signal ?? acceptsSignal(fn);
29
- const _queue: Array<{
30
- args: unknown[];
31
- resolve: (v: T | undefined) => void;
32
- controller: AbortController;
33
- }> = [];
34
- const _activeControllers = new Set<AbortController>();
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
- async function tryRun(): Promise<void> {
37
- if (_active() >= concurrency || _queue.length === 0) return;
38
- const item = _queue[0];
39
- _queue.shift();
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
- _pending.update((n) => n - 1);
43
- _active.update((n) => n + 1);
44
- _activeControllers.add(controller);
49
+ if (controller) activeControllers.add(controller);
45
50
  try {
46
- const result = await (_wantsSignal ? fn(controller.signal, ...args) : fn(...args));
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
- _activeControllers.delete(controller);
55
- _active.update((n) => n - 1);
56
- void tryRun();
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
- _queue.push({ args, resolve, controller });
64
- _pending.update((n) => n + 1);
65
- void tryRun();
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 = computed(() => _active() > 0);
70
- run.active = computed(() => _active());
71
- run.pending = computed(() => _pending());
72
- run.error = computed(() => _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 _activeControllers) {
75
- c.abort();
76
- }
77
- _activeControllers.clear();
78
- while (_queue.length > 0) {
79
- const item = _queue.shift();
80
- item?.controller.abort();
81
- item?.resolve(undefined);
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
- _pending.set(0);
101
+ publish();
84
102
  };
85
103
 
86
104
  return run as PoolInstance<T>;
package/src/queue.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { signal, computed } from "@praxisjs/core/internal";
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 _loading = signal(false);
30
- const _pending = signal(0);
31
- const _error = signal<Error | null>(null);
32
- const _wantsSignal = options?.signal ?? acceptsSignal(fn);
33
- const _queue: Array<{
34
- args: unknown[];
35
- resolve: (v: T) => void;
36
- reject: (e: Error) => void;
37
- controller: AbortController;
38
- }> = [];
39
- let _running = false;
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 (_running || _queue.length === 0) return;
46
- _running = true;
47
- _loading.set(true);
48
- while (_queue.length > 0) {
49
- const item = _queue[0];
50
- _queue.shift();
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
- _activeController = controller;
54
- _activeAbortedByClear = false;
55
- _pending.update((n) => n - 1);
57
+ activeController = controller;
58
+ activeAbortedByClear = false;
59
+ inCall = true;
60
+ writeSignal(pending, items.length - head);
56
61
  try {
57
- const result = await (_wantsSignal ? fn(controller.signal, ...args) : fn(...args));
62
+ const result = await (controller ? fn(controller.signal, ...args) : fn(...args));
58
63
  resolve(result);
59
64
  } catch (err) {
60
- // _activeAbortedByClear is set asynchronously by clear() between the await and this catch
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) && _activeAbortedByClear) {
67
+ if (isAbortError(err) && activeAbortedByClear) {
63
68
  reject(new QueueClearedError());
64
69
  } else {
65
- const e = err instanceof Error ? err : new Error(String(err));
66
- _error.set(e);
70
+ const e = toError(err);
71
+ writeSignal(error, e);
67
72
  reject(e);
68
73
  }
69
74
  } finally {
70
- _activeController = null;
75
+ inCall = false;
76
+ activeController = undefined;
71
77
  }
72
78
  }
73
- _cleared = false;
74
- _running = false;
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
- _queue.push({ args, resolve, reject, controller });
85
- _pending.update((n) => n + 1);
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 = computed(() => _loading());
91
- enqueue.pending = computed(() => _pending());
92
- enqueue.error = computed(() => _error());
91
+ enqueue.loading = readonlyView(loading);
92
+ enqueue.pending = readonlyView(pending);
93
+ enqueue.error = readonlyView(error);
93
94
  enqueue.clear = () => {
94
- if (_activeController) {
95
- _activeAbortedByClear = true;
96
- _activeController.abort();
95
+ if (inCall) {
96
+ activeAbortedByClear = true;
97
+ activeController?.abort();
97
98
  }
98
- while (_queue.length > 0) {
99
- const item = _queue.shift();
100
- item?.controller.abort();
101
- item?.reject(new QueueClearedError());
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
- _pending.set(0);
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 { signal, computed } from "@praxisjs/core/internal";
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 _loading = signal(false);
23
- const _error = signal<Error | null>(null);
24
- const _lastResult = signal<T | null>(null);
25
- let _runId = 0;
26
- let _controller: AbortController | null = null;
27
- const _wantsSignal = options?.signal ?? acceptsSignal(fn);
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
- _controller?.abort();
31
- const controller = new AbortController();
32
- _controller = controller;
33
- const id = ++_runId;
34
- _loading.set(true);
35
- _error.set(null);
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 (_wantsSignal ? fn(controller.signal, ...args) : fn(...args));
38
- if (id !== _runId) return undefined;
39
- _lastResult.set(result);
40
- _loading.set(false);
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 !== _runId) return undefined;
44
- if (!isAbortError(err)) {
45
- _error.set(err instanceof Error ? err : new Error(String(err)));
46
- }
47
- _loading.set(false);
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 = computed(() => _loading());
53
- run.error = computed(() => _error());
54
- run.lastResult = computed(() => _lastResult());
56
+ run.loading = readonlyView(loading);
57
+ run.error = readonlyView(error);
58
+ run.lastResult = readonlyView(lastResult);
55
59
  run.cancelAll = () => {
56
- _controller?.abort();
57
- _controller = null;
58
- _runId++;
59
- _loading.set(false);
60
+ controller?.abort();
61
+ controller = null;
62
+ runId++;
63
+ writeSignal(loading, false);
60
64
  };
61
65
 
62
66
  return run as TaskInstance<T>;