time-queues 1.3.2 → 1.4.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/LICENSE +1 -1
- package/README.md +1 -0
- package/llms-full.txt +17 -7
- package/llms.txt +8 -4
- package/package.json +17 -9
- package/src/Counter.d.ts +15 -1
- package/src/Counter.js +12 -13
- package/src/FrameQueue.d.ts +7 -0
- package/src/FrameQueue.js +1 -17
- package/src/IdleQueue.d.ts +7 -0
- package/src/IdleQueue.js +1 -17
- package/src/LimitedQueue.js +10 -11
- package/src/ListQueue.d.ts +15 -0
- package/src/ListQueue.js +23 -18
- package/src/MicroTask.d.ts +25 -4
- package/src/MicroTask.js +22 -17
- package/src/MicroTaskQueue.js +9 -27
- package/src/PageWatcher.js +10 -7
- package/src/Retainer.d.ts +4 -2
- package/src/Retainer.js +26 -16
- package/src/Scheduler.d.ts +35 -33
- package/src/Scheduler.js +20 -4
- package/src/Throttler.d.ts +22 -5
- package/src/Throttler.js +9 -8
- package/src/audit.js +1 -1
- package/src/defer.js +1 -0
- package/src/index.js +29 -0
- package/src/random-dist.js +9 -6
- package/src/sample.js +1 -1
- package/src/when-dom-loaded.js +2 -1
- package/src/when-loaded.js +2 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -140,6 +140,7 @@ BSD-3-Clause
|
|
|
140
140
|
|
|
141
141
|
## Release History
|
|
142
142
|
|
|
143
|
+
- 1.4.0 _Multiple fixes that improve edge cases, minor additive API changes for edge cases._
|
|
143
144
|
- 1.3.2 _Bug fixes (Scheduler, LimitedQueue, PageWatcher, Retainer), corrected `.d.ts` declarations, expanded tests, documentation fixes._
|
|
144
145
|
- 1.3.1 _Fixed `.d.ts` declarations, consolidated TS typing tests, improved documentation._
|
|
145
146
|
- 1.3.0 _Added `batch()`, `LimitedQueue`, random distributions and random sleep functions._
|
package/llms-full.txt
CHANGED
|
@@ -20,6 +20,10 @@ npm install time-queues
|
|
|
20
20
|
## Quick start
|
|
21
21
|
|
|
22
22
|
```js
|
|
23
|
+
// Bare imports work as a barrel:
|
|
24
|
+
import {sleep, Scheduler, repeat, batch} from 'time-queues';
|
|
25
|
+
|
|
26
|
+
// Or subpath imports for tree-shaking-friendly use:
|
|
23
27
|
import sleep from 'time-queues/sleep.js';
|
|
24
28
|
import {Scheduler, repeat} from 'time-queues/Scheduler.js';
|
|
25
29
|
import {batch} from 'time-queues/batch.js';
|
|
@@ -54,7 +58,7 @@ MicroTaskQueue (abstract queue base)
|
|
|
54
58
|
Standalone: Counter, Throttler, Retainer, CancelTaskError
|
|
55
59
|
```
|
|
56
60
|
|
|
57
|
-
All modules are ESM.
|
|
61
|
+
All modules are ESM. Bare imports (`from 'time-queues'`) and subpath imports (`from 'time-queues/<module>.js'`) are both supported.
|
|
58
62
|
|
|
59
63
|
## API Reference
|
|
60
64
|
|
|
@@ -78,10 +82,12 @@ class MicroTask {
|
|
|
78
82
|
fn: () => unknown;
|
|
79
83
|
isCanceled: boolean;
|
|
80
84
|
get promise(): Promise<unknown> | null;
|
|
85
|
+
get settled(): boolean;
|
|
86
|
+
get cancelError(): Error | null;
|
|
81
87
|
constructor(fn: () => unknown);
|
|
82
|
-
makePromise(): this;
|
|
83
|
-
resolve(value: unknown): this;
|
|
84
|
-
cancel(error?: Error): this;
|
|
88
|
+
makePromise(): this; // idempotent; if already canceled, fresh promise settles immediately as CancelTaskError
|
|
89
|
+
resolve(value: unknown): this; // throws if makePromise() has not been called
|
|
90
|
+
cancel(error?: Error): this; // stores first error reason; replayed on later makePromise() if no promise yet
|
|
85
91
|
}
|
|
86
92
|
export default MicroTask;
|
|
87
93
|
```
|
|
@@ -141,6 +147,9 @@ class Task extends MicroTask {
|
|
|
141
147
|
class Scheduler extends MicroTaskQueue {
|
|
142
148
|
paused: boolean;
|
|
143
149
|
tolerance: number;
|
|
150
|
+
queue: MinHeap<Task>; // pending tasks ordered by `time`
|
|
151
|
+
stopQueue: (() => void) | null;
|
|
152
|
+
onError: ((error: unknown, task: Task) => void) | null; // optional per-task error handler; default null surfaces via unhandled-rejection
|
|
144
153
|
constructor(paused?: boolean, tolerance?: number);
|
|
145
154
|
get isEmpty(): boolean;
|
|
146
155
|
get nextTime(): number;
|
|
@@ -244,9 +253,10 @@ class Counter {
|
|
|
244
253
|
increment(): void;
|
|
245
254
|
decrement(): void;
|
|
246
255
|
advance(amount?: number): void;
|
|
247
|
-
waitForZero(): Promise<number>;
|
|
256
|
+
waitForZero(): Promise<number>; // resolves with NaN if clearWaiters() called first
|
|
248
257
|
waitFor(fn: (count: number) => boolean): Promise<number>;
|
|
249
258
|
clearWaiters(): void;
|
|
259
|
+
notify(): void; // call after direct `count` mutation
|
|
250
260
|
}
|
|
251
261
|
export default Counter;
|
|
252
262
|
```
|
|
@@ -291,12 +301,12 @@ interface RetainerOptions<T = unknown> {
|
|
|
291
301
|
|
|
292
302
|
class Retainer<T = unknown> implements RetainerOptions<T> {
|
|
293
303
|
counter: number;
|
|
294
|
-
value: T | null;
|
|
304
|
+
readonly value: T | null;
|
|
295
305
|
create: () => Promise<T> | T;
|
|
296
306
|
destroy: (value: T) => Promise<void> | void;
|
|
297
307
|
retentionPeriod: number;
|
|
298
308
|
constructor(options: RetainerOptions<T>);
|
|
299
|
-
async get(): Promise<T>;
|
|
309
|
+
async get(): Promise<T>; // concurrent get() calls share one create()
|
|
300
310
|
async release(immediately?: boolean): Promise<this>;
|
|
301
311
|
}
|
|
302
312
|
export default Retainer;
|
package/llms.txt
CHANGED
|
@@ -16,6 +16,10 @@ npm install time-queues
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
18
18
|
```js
|
|
19
|
+
// Bare imports work as a barrel:
|
|
20
|
+
import {sleep, Scheduler, repeat, batch} from 'time-queues';
|
|
21
|
+
|
|
22
|
+
// Or subpath imports for tree-shaking-friendly use:
|
|
19
23
|
import sleep from 'time-queues/sleep.js';
|
|
20
24
|
import {Scheduler, repeat} from 'time-queues/Scheduler.js';
|
|
21
25
|
import {batch} from 'time-queues/batch.js';
|
|
@@ -40,7 +44,7 @@ const results = await batch(
|
|
|
40
44
|
|
|
41
45
|
### Queues
|
|
42
46
|
|
|
43
|
-
- **Scheduler** (`time-queues/Scheduler.js`) — time-based task scheduling with delays, dates, and repeats. Extends MicroTaskQueue. Uses a min-heap. Exports: `Scheduler`, `Task`, `repeat(fn, delay)`, `scheduler` (global instance).
|
|
47
|
+
- **Scheduler** (`time-queues/Scheduler.js`) — time-based task scheduling with delays, dates, and repeats. Extends MicroTaskQueue. Uses a min-heap. Exports: `Scheduler`, `Task`, `repeat(fn, delay)`, `scheduler` (global instance). Optional `scheduler.onError(error, task)` callback handles per-task exceptions; loop continues regardless.
|
|
44
48
|
- **IdleQueue** (`time-queues/IdleQueue.js`) — run tasks during browser idle periods via `requestIdleCallback()`. Extends ListQueue. Exports: `IdleQueue`, `idleQueue` (global instance), `defer(fn)`.
|
|
45
49
|
- **FrameQueue** (`time-queues/FrameQueue.js`) — run tasks in animation frames via `requestAnimationFrame()`. Extends ListQueue. Exports: `FrameQueue`, `frameQueue` (global instance).
|
|
46
50
|
- **LimitedQueue** (`time-queues/LimitedQueue.js`) — concurrency-controlled async queue. Extends ListQueue. Exports: `LimitedQueue`, `Task`.
|
|
@@ -60,12 +64,12 @@ const results = await batch(
|
|
|
60
64
|
|
|
61
65
|
- **Throttler** (`time-queues/Throttler.js`) — key-based rate limiting with vacuum cleanup. Methods: `getDelay(key)`, `wait(key)`, `startVacuum()`, `stopVacuum()`.
|
|
62
66
|
- **Retainer** (`time-queues/Retainer.js`) — resource lifecycle management with reference counting. Methods: `get(): Promise<value>`, `release(): Promise<void>`.
|
|
63
|
-
- **Counter** (`time-queues/Counter.js`) — numeric counter with async waiting. Methods: `increment()`, `decrement()`, `advance(delta)`, `waitForZero()`, `waitFor(
|
|
67
|
+
- **Counter** (`time-queues/Counter.js`) — numeric counter with async waiting. Methods: `increment()`, `decrement()`, `advance(delta)`, `waitForZero()`, `waitFor(predicate)`, `clearWaiters()` (resolves pending with `NaN`), `notify()` (call after direct `count` mutation).
|
|
64
68
|
- **CancelTaskError** (`time-queues/CancelTaskError.js`) — `Error` subclass for task cancellation signals.
|
|
65
69
|
|
|
66
70
|
### Base classes
|
|
67
71
|
|
|
68
|
-
- **MicroTask** (`time-queues/MicroTask.js`) — base task unit with lazy promise creation. Methods: `makePromise()`, `resolve(value)
|
|
72
|
+
- **MicroTask** (`time-queues/MicroTask.js`) — base task unit with lazy promise creation. Methods: `makePromise()`, `resolve(value)` (throws if no promise yet), `cancel(error)` (stores `error`, replays as `cause` on later `makePromise()`). Getters: `promise`, `settled`, `cancelError`.
|
|
69
73
|
- **MicroTaskQueue** (`time-queues/MicroTaskQueue.js`) — abstract queue base class. Methods: `enqueue(fn)`, `dequeue(task)`, `schedule(fn)`, `clear()`, `pause()`, `resume()`.
|
|
70
74
|
- **ListQueue** (`time-queues/ListQueue.js`) — linked-list queue implementation extending MicroTaskQueue. Adds `startQueue()` lifecycle.
|
|
71
75
|
|
|
@@ -81,7 +85,7 @@ const results = await batch(
|
|
|
81
85
|
|
|
82
86
|
## Key concepts
|
|
83
87
|
|
|
84
|
-
- All modules are **ESM
|
|
88
|
+
- All modules are **ESM**. Bare imports (`from 'time-queues'`) and subpath imports (`from 'time-queues/<module>.js'`) both work.
|
|
85
89
|
- **Inheritance**: `MicroTaskQueue` → `ListQueue` → `IdleQueue`/`FrameQueue`/`LimitedQueue`/`PageWatcher`. `Scheduler` extends `MicroTaskQueue` directly.
|
|
86
90
|
- **Lazy promises** — only created when `.makePromise()` is called or via `schedule()`.
|
|
87
91
|
- **Clean cancellation** — all tasks support cancellation via `CancelTaskError`.
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "time-queues",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Lightweight async task scheduling and concurrency control: schedulers, idle/frame/limited queues, throttle, debounce, batch, page lifecycle, random delays. Browsers, Node.js, Deno, Bun.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
8
9
|
"./*": "./src/*"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
"test:seq:bun": "bun run `tape6-seq --self` --flags FO",
|
|
19
20
|
"test:seq:deno": "deno run -A `tape6-seq --self` --flags FO",
|
|
20
21
|
"ts-check": "tsc --noEmit",
|
|
22
|
+
"js-check": "tsc --project tsconfig.check.json",
|
|
21
23
|
"ts-test": "tape6 --flags FO '/ts-tests/test-*.*ts'",
|
|
22
24
|
"ts-test:bun": "tape6-bun --flags FO '/ts-tests/test-*.*ts'",
|
|
23
25
|
"ts-test:deno": "tape6-deno --flags FO '/ts-tests/test-*.*ts'",
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
},
|
|
34
36
|
"repository": {
|
|
35
37
|
"type": "git",
|
|
36
|
-
"url": "git+
|
|
38
|
+
"url": "git+https://github.com/uhop/time-queues.git"
|
|
37
39
|
},
|
|
38
40
|
"keywords": [
|
|
39
41
|
"scheduler",
|
|
@@ -41,17 +43,20 @@
|
|
|
41
43
|
"async-queue",
|
|
42
44
|
"concurrency",
|
|
43
45
|
"throttle",
|
|
46
|
+
"throttler",
|
|
44
47
|
"debounce",
|
|
45
48
|
"rate-limit",
|
|
46
49
|
"batch",
|
|
47
50
|
"sleep",
|
|
48
51
|
"defer",
|
|
49
52
|
"idle-queue",
|
|
53
|
+
"frame-queue",
|
|
54
|
+
"limited-queue",
|
|
50
55
|
"requestIdleCallback",
|
|
51
56
|
"requestAnimationFrame",
|
|
52
57
|
"page-lifecycle",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
58
|
+
"microtask",
|
|
59
|
+
"retainer",
|
|
55
60
|
"queue",
|
|
56
61
|
"esm",
|
|
57
62
|
"typescript"
|
|
@@ -59,6 +64,9 @@
|
|
|
59
64
|
"author": "Eugene Lazutkin <eugene.lazutkin@gmail.com> (https://www.lazutkin.com/)",
|
|
60
65
|
"funding": "https://github.com/sponsors/uhop",
|
|
61
66
|
"license": "BSD-3-Clause",
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=22"
|
|
69
|
+
},
|
|
62
70
|
"bugs": {
|
|
63
71
|
"url": "https://github.com/uhop/time-queues/issues"
|
|
64
72
|
},
|
|
@@ -90,11 +98,11 @@
|
|
|
90
98
|
}
|
|
91
99
|
},
|
|
92
100
|
"devDependencies": {
|
|
93
|
-
"@types/node": "^25.
|
|
94
|
-
"prettier": "^3.8.
|
|
95
|
-
"tape-six": "^1.
|
|
96
|
-
"tape-six-proc": "^1.2.
|
|
97
|
-
"typescript": "^
|
|
101
|
+
"@types/node": "^25.6.1",
|
|
102
|
+
"prettier": "^3.8.3",
|
|
103
|
+
"tape-six": "^1.9.0",
|
|
104
|
+
"tape-six-proc": "^1.2.9",
|
|
105
|
+
"typescript": "^6.0.3"
|
|
98
106
|
},
|
|
99
107
|
"dependencies": {
|
|
100
108
|
"list-toolkit": "^2.3.2"
|
package/src/Counter.d.ts
CHANGED
|
@@ -42,21 +42,35 @@ export declare class Counter {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Waits for the counter to reach zero. If the counter is already zero, the promise is resolved immediately.
|
|
45
|
+
* Resolves with `0` on a normal wait, or with `NaN` if `clearWaiters()` is called before the counter reaches zero.
|
|
45
46
|
* @returns A promise that resolves when the counter reaches zero.
|
|
46
47
|
*/
|
|
47
48
|
waitForZero(): Promise<number>;
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Waits for the counter to reach a specific value. If the counter is already at the desired value, the promise is resolved immediately.
|
|
52
|
+
* Resolves with the matching count, or with `NaN` if `clearWaiters()` is called before the predicate matches.
|
|
51
53
|
* @param fn A function that returns `true` when the counter reaches the desired value.
|
|
52
54
|
* @returns A promise that resolves when the counter reaches the desired value.
|
|
53
55
|
*/
|
|
54
56
|
waitFor(fn: (count: number) => boolean): Promise<number>;
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
|
-
* Clears all waiters
|
|
59
|
+
* Clears all pending waiters by resolving them with `NaN`.
|
|
60
|
+
* Use `Number.isNaN(value)` to distinguish "queue cleared" from a real count of zero.
|
|
61
|
+
* Most consumers `await` the result without inspecting it; checking is only needed
|
|
62
|
+
* when you both clear waiters during teardown and have downstream arithmetic that
|
|
63
|
+
* shouldn't be NaN-poisoned.
|
|
58
64
|
*/
|
|
59
65
|
clearWaiters(): void;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Re-evaluates pending waiters against the current `count` and resolves any
|
|
69
|
+
* whose predicate now matches. Called automatically by `value`, `increment`,
|
|
70
|
+
* `decrement`, and `advance`. Call manually only when mutating `count`
|
|
71
|
+
* directly (which bypasses the setter).
|
|
72
|
+
*/
|
|
73
|
+
notify(): void;
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
export default Counter;
|
package/src/Counter.js
CHANGED
|
@@ -4,15 +4,15 @@ export class Counter {
|
|
|
4
4
|
constructor(initial = 0) {
|
|
5
5
|
this.count = initial;
|
|
6
6
|
this.zeroWaiters = [];
|
|
7
|
-
this.functionWaiters =
|
|
7
|
+
this.functionWaiters = [];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
get value() {
|
|
11
11
|
return this.count;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
set value(
|
|
15
|
-
this.count =
|
|
14
|
+
set value(newValue) {
|
|
15
|
+
this.count = newValue;
|
|
16
16
|
this.notify();
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -38,7 +38,7 @@ export class Counter {
|
|
|
38
38
|
|
|
39
39
|
waitFor(fn) {
|
|
40
40
|
if (fn(this.count)) return Promise.resolve(this.count);
|
|
41
|
-
return new Promise(resolve => this.functionWaiters.
|
|
41
|
+
return new Promise(resolve => this.functionWaiters.push({fn, resolve}));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
clearWaiters() {
|
|
@@ -49,9 +49,9 @@ export class Counter {
|
|
|
49
49
|
resolve(NaN);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
if (this.functionWaiters.
|
|
52
|
+
if (this.functionWaiters.length > 0) {
|
|
53
53
|
const functionWaiters = this.functionWaiters;
|
|
54
|
-
this.functionWaiters =
|
|
54
|
+
this.functionWaiters = [];
|
|
55
55
|
for (const {resolve} of functionWaiters) {
|
|
56
56
|
resolve(NaN);
|
|
57
57
|
}
|
|
@@ -66,17 +66,16 @@ export class Counter {
|
|
|
66
66
|
resolve(0);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
if (this.functionWaiters.
|
|
70
|
-
const
|
|
69
|
+
if (this.functionWaiters.length > 0) {
|
|
70
|
+
const remaining = [];
|
|
71
71
|
for (const waiter of this.functionWaiters) {
|
|
72
|
-
if (waiter.fn(this.count))
|
|
73
|
-
}
|
|
74
|
-
if (ready.length > 0) {
|
|
75
|
-
for (const waiter of ready) {
|
|
72
|
+
if (waiter.fn(this.count)) {
|
|
76
73
|
waiter.resolve(this.count);
|
|
77
|
-
|
|
74
|
+
} else {
|
|
75
|
+
remaining.push(waiter);
|
|
78
76
|
}
|
|
79
77
|
}
|
|
78
|
+
this.functionWaiters = remaining;
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
81
|
}
|
package/src/FrameQueue.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export declare class FrameQueue extends ListQueue {
|
|
|
75
75
|
* @returns The function that stops the queue.
|
|
76
76
|
*/
|
|
77
77
|
startQueue(): (() => void) | null;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Processes pending tasks. Called by `requestAnimationFrame()` — not part of
|
|
81
|
+
* the typical user surface; documented for subclasses that need to override.
|
|
82
|
+
* @param timeStamp The high-resolution timestamp from the rAF callback.
|
|
83
|
+
*/
|
|
84
|
+
processTasks(timeStamp: number): void;
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
/**
|
package/src/FrameQueue.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @ts-self-types="./FrameQueue.d.ts"
|
|
2
2
|
|
|
3
|
-
import List from 'list-toolkit/list.js';
|
|
4
3
|
import ListQueue from './ListQueue.js';
|
|
5
4
|
|
|
6
5
|
export class FrameQueue extends ListQueue {
|
|
@@ -19,22 +18,7 @@ export class FrameQueue extends ListQueue {
|
|
|
19
18
|
this.stopQueue();
|
|
20
19
|
this.stopQueue = null;
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
if (!isNaN(this.batch)) {
|
|
24
|
-
const start = Date.now();
|
|
25
|
-
while (Date.now() - start < this.batch && !this.list.isEmpty) {
|
|
26
|
-
const task = this.list.popFront();
|
|
27
|
-
task.fn({timeStamp, task, queue: this});
|
|
28
|
-
}
|
|
29
|
-
} else {
|
|
30
|
-
const list = this.list;
|
|
31
|
-
this.list = new List();
|
|
32
|
-
while (!list.isEmpty) {
|
|
33
|
-
const task = list.popFront();
|
|
34
|
-
task.fn({timeStamp, task, queue: this});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
21
|
+
this._drainBatch(this.batch, {timeStamp});
|
|
38
22
|
if (!this.list.isEmpty) this.stopQueue = this.startQueue();
|
|
39
23
|
}
|
|
40
24
|
}
|
package/src/IdleQueue.d.ts
CHANGED
|
@@ -85,6 +85,13 @@ export declare class IdleQueue extends ListQueue {
|
|
|
85
85
|
* @returns The function that stops the queue.
|
|
86
86
|
*/
|
|
87
87
|
startQueue(): (() => void) | null;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Processes pending tasks. Called by `requestIdleCallback()` — not part of
|
|
91
|
+
* the typical user surface; documented for subclasses that need to override.
|
|
92
|
+
* @param deadline The `IdleDeadline` from the rIC callback.
|
|
93
|
+
*/
|
|
94
|
+
processTasks(deadline: IdleDeadline): void;
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
/**
|
package/src/IdleQueue.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @ts-self-types="./IdleQueue.d.ts"
|
|
2
2
|
|
|
3
|
-
import List from 'list-toolkit/list.js';
|
|
4
3
|
import ListQueue from './ListQueue.js';
|
|
5
4
|
|
|
6
5
|
// Based on information from https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API
|
|
@@ -22,29 +21,14 @@ export class IdleQueue extends ListQueue {
|
|
|
22
21
|
this.stopQueue();
|
|
23
22
|
this.stopQueue = null;
|
|
24
23
|
}
|
|
25
|
-
|
|
26
24
|
if (deadline.didTimeout) {
|
|
27
|
-
|
|
28
|
-
const start = Date.now();
|
|
29
|
-
while (Date.now() - start < this.timeoutBatch && !this.list.isEmpty) {
|
|
30
|
-
const task = this.list.popFront();
|
|
31
|
-
task.fn({deadline, task, queue: this});
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
const list = this.list;
|
|
35
|
-
this.list = new List();
|
|
36
|
-
while (!list.isEmpty) {
|
|
37
|
-
const task = list.popFront();
|
|
38
|
-
task.fn({deadline, task, queue: this});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
25
|
+
this._drainBatch(this.timeoutBatch, {deadline});
|
|
41
26
|
} else {
|
|
42
27
|
while (deadline.timeRemaining() > 0 && !this.list.isEmpty) {
|
|
43
28
|
const task = this.list.popFront();
|
|
44
29
|
task.fn({deadline, task, queue: this});
|
|
45
30
|
}
|
|
46
31
|
}
|
|
47
|
-
|
|
48
32
|
if (!this.list.isEmpty) this.stopQueue = this.startQueue();
|
|
49
33
|
}
|
|
50
34
|
}
|
package/src/LimitedQueue.js
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import ListQueue from './ListQueue.js';
|
|
4
4
|
|
|
5
|
+
const wrap = fn =>
|
|
6
|
+
new Promise((resolve, reject) => {
|
|
7
|
+
try {
|
|
8
|
+
resolve(fn());
|
|
9
|
+
} catch (error) {
|
|
10
|
+
reject(error);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
5
14
|
export class LimitedQueue extends ListQueue {
|
|
6
15
|
#taskLimit;
|
|
7
16
|
#activeTasks;
|
|
@@ -57,22 +66,12 @@ export class LimitedQueue extends ListQueue {
|
|
|
57
66
|
while (this.#activeTasks < this.#taskLimit && !this.list.isEmpty) {
|
|
58
67
|
const task = this.list.popFront();
|
|
59
68
|
++this.#activeTasks;
|
|
60
|
-
|
|
69
|
+
wrap(() => task.fn({task, queue: this})).finally(() => {
|
|
61
70
|
--this.#activeTasks;
|
|
62
71
|
this.#processTasks();
|
|
63
72
|
});
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
|
-
|
|
67
|
-
static wrap(fn) {
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
try {
|
|
70
|
-
resolve(fn());
|
|
71
|
-
} catch (error) {
|
|
72
|
-
reject(error);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
export default LimitedQueue;
|
package/src/ListQueue.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import List from 'list-toolkit/list.js';
|
|
1
2
|
import {MicroTask} from './MicroTask.js';
|
|
2
3
|
import {MicroTaskQueue} from './MicroTaskQueue.js';
|
|
3
4
|
|
|
@@ -11,6 +12,11 @@ export declare class ListQueue extends MicroTaskQueue {
|
|
|
11
12
|
*/
|
|
12
13
|
paused: boolean;
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The linked list of pending microtasks.
|
|
17
|
+
*/
|
|
18
|
+
list: List<MicroTask>;
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* The function that stops the queue.
|
|
16
22
|
* It is used internally by `pause()` and `resume()`.
|
|
@@ -74,6 +80,15 @@ export declare class ListQueue extends MicroTaskQueue {
|
|
|
74
80
|
* @returns The function that stops the queue.
|
|
75
81
|
*/
|
|
76
82
|
startQueue(): (() => void) | null;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Internal helper used by subclasses (FrameQueue, IdleQueue) to drain pending
|
|
86
|
+
* tasks. If `batchMs` is finite, runs tasks until that many milliseconds have
|
|
87
|
+
* elapsed; otherwise swaps in a fresh list and drains the captured one
|
|
88
|
+
* entirely.
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
_drainBatch(batchMs: number, taskContext: object): void;
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
/**
|
package/src/ListQueue.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
// @ts-self-types="./ListQueue.d.ts"
|
|
2
2
|
|
|
3
3
|
import List from 'list-toolkit/list.js';
|
|
4
|
+
import MicroTask from './MicroTask.js';
|
|
4
5
|
import MicroTaskQueue from './MicroTaskQueue.js';
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
-
* ListQueue extends MicroTaskQueue with linked-list task storage.
|
|
8
|
-
* AI-NOTE: This is the concrete base class most specialized queues extend.
|
|
9
|
-
* Key pattern: startQueue() returns a stop function (or null if not started).
|
|
10
|
-
* @see IdleQueue, FrameQueue, LimitedQueue, Scheduler - All extend ListQueue
|
|
11
|
-
*/
|
|
12
7
|
export class ListQueue extends MicroTaskQueue {
|
|
13
8
|
constructor(paused) {
|
|
14
9
|
super(paused);
|
|
15
|
-
|
|
10
|
+
/** @type {List<MicroTask>} */
|
|
16
11
|
this.list = new List();
|
|
17
|
-
// AI-NOTE: stopQueue holds the stop function returned by startQueue(), or null
|
|
18
12
|
this.stopQueue = null;
|
|
19
13
|
}
|
|
20
14
|
|
|
@@ -25,7 +19,6 @@ export class ListQueue extends MicroTaskQueue {
|
|
|
25
19
|
pause() {
|
|
26
20
|
if (!this.paused) {
|
|
27
21
|
super.pause();
|
|
28
|
-
// AI-NOTE: Pattern: call stop function, then null it
|
|
29
22
|
if (this.stopQueue) this.stopQueue = (this.stopQueue(), null);
|
|
30
23
|
}
|
|
31
24
|
return this;
|
|
@@ -34,7 +27,6 @@ export class ListQueue extends MicroTaskQueue {
|
|
|
34
27
|
resume() {
|
|
35
28
|
if (this.paused) {
|
|
36
29
|
super.resume();
|
|
37
|
-
// AI-NOTE: Auto-start processing if tasks exist and not already running
|
|
38
30
|
if (!this.list.isEmpty) {
|
|
39
31
|
this.stopQueue = this.startQueue();
|
|
40
32
|
}
|
|
@@ -45,7 +37,6 @@ export class ListQueue extends MicroTaskQueue {
|
|
|
45
37
|
enqueue(fn) {
|
|
46
38
|
const task = super.enqueue(fn);
|
|
47
39
|
this.list.pushBack(task);
|
|
48
|
-
// AI-NOTE: Auto-start queue on first task if not paused and not running
|
|
49
40
|
if (!this.paused && !this.stopQueue) this.stopQueue = this.startQueue();
|
|
50
41
|
return task;
|
|
51
42
|
}
|
|
@@ -53,7 +44,6 @@ export class ListQueue extends MicroTaskQueue {
|
|
|
53
44
|
dequeue(task) {
|
|
54
45
|
task.cancel();
|
|
55
46
|
this.list.removeNode(task);
|
|
56
|
-
// AI-NOTE: Auto-stop queue when empty (unless paused)
|
|
57
47
|
if (!this.paused && this.list.isEmpty && this.stopQueue)
|
|
58
48
|
this.stopQueue = (this.stopQueue(), null);
|
|
59
49
|
return this;
|
|
@@ -70,15 +60,30 @@ export class ListQueue extends MicroTaskQueue {
|
|
|
70
60
|
return this;
|
|
71
61
|
}
|
|
72
62
|
|
|
73
|
-
/**
|
|
74
|
-
* Start processing the queue - MUST be overridden by subclasses.
|
|
75
|
-
* AI-NOTE: This is the abstract method pattern - base returns null.
|
|
76
|
-
* Subclasses return a function that stops the processing.
|
|
77
|
-
* @returns {Function|null} Stop function or null if not started
|
|
78
|
-
*/
|
|
79
63
|
startQueue() {
|
|
80
64
|
return null;
|
|
81
65
|
}
|
|
66
|
+
|
|
67
|
+
// Drains pending tasks. If batchMs is a finite number, runs tasks until that
|
|
68
|
+
// many milliseconds have elapsed; otherwise swaps in a fresh list and drains
|
|
69
|
+
// the captured one entirely (so tasks enqueued during draining run on the
|
|
70
|
+
// next tick rather than this one).
|
|
71
|
+
_drainBatch(batchMs, taskContext) {
|
|
72
|
+
if (!isNaN(batchMs)) {
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
while (Date.now() - start < batchMs && !this.list.isEmpty) {
|
|
75
|
+
const task = this.list.popFront();
|
|
76
|
+
task.fn({...taskContext, task, queue: this});
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
const list = this.list;
|
|
80
|
+
this.list = new List();
|
|
81
|
+
while (!list.isEmpty) {
|
|
82
|
+
const task = list.popFront();
|
|
83
|
+
task.fn({...taskContext, task, queue: this});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
export default ListQueue;
|
package/src/MicroTask.d.ts
CHANGED
|
@@ -19,7 +19,11 @@ export declare class MicroTask {
|
|
|
19
19
|
constructor(fn: (...args: any[]) => unknown);
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Creates the promise lazily. Idempotent — subsequent calls return `this`
|
|
23
|
+
* without changing state. If the task was already canceled (via `cancel()`
|
|
24
|
+
* before `makePromise()` ran), the freshly-created promise is settled
|
|
25
|
+
* immediately as a `CancelTaskError` rejection, carrying any `cancelError`
|
|
26
|
+
* stored from the earlier `cancel()` call as `cause`.
|
|
23
27
|
* @returns The microtask.
|
|
24
28
|
*/
|
|
25
29
|
makePromise(): this;
|
|
@@ -37,15 +41,32 @@ export declare class MicroTask {
|
|
|
37
41
|
get settled(): boolean;
|
|
38
42
|
|
|
39
43
|
/**
|
|
40
|
-
*
|
|
44
|
+
* The error supplied to the first `cancel(error)` call, or `null` if the task
|
|
45
|
+
* has not been canceled with a reason. Useful for inspecting why a non-promised
|
|
46
|
+
* task was canceled (when there is no rejection to carry the cause).
|
|
47
|
+
*/
|
|
48
|
+
get cancelError(): Error | null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the microtask. The promise must already exist — call `makePromise()`
|
|
52
|
+
* first, or invoke this only from inside a `schedule()`-wrapped callback (which
|
|
53
|
+
* makes the promise eagerly).
|
|
41
54
|
* @param value The value to resolve the microtask with.
|
|
42
55
|
* @returns The microtask.
|
|
56
|
+
* @throws If `makePromise()` has not been called — without a promise to resolve,
|
|
57
|
+
* the value would be silently dropped, which previously caused subscribers to
|
|
58
|
+
* hang on `task.makePromise().promise` calls made afterwards.
|
|
43
59
|
*/
|
|
44
60
|
resolve(value: unknown): this;
|
|
45
61
|
|
|
46
62
|
/**
|
|
47
|
-
* Cancels the microtask
|
|
48
|
-
*
|
|
63
|
+
* Cancels the microtask. Always sets `isCanceled = true` so queues skip the task.
|
|
64
|
+
* Additionally rejects the promise with a `CancelTaskError` if `makePromise()`
|
|
65
|
+
* has been called.
|
|
66
|
+
* The first `error` passed is stored on the instance and accessible via
|
|
67
|
+
* `cancelError`. If `cancel(error)` runs before `makePromise()`, the stored
|
|
68
|
+
* error is replayed when the promise is later created — `makePromise()` will
|
|
69
|
+
* settle the fresh promise with `CancelTaskError(cause: error)` immediately.
|
|
49
70
|
* It can be overridden in subclasses.
|
|
50
71
|
* @param error The optional error to use as the cause of the cancellation.
|
|
51
72
|
* @returns The microtask.
|
package/src/MicroTask.js
CHANGED
|
@@ -2,37 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
import CancelTaskError from './CancelTaskError.js';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Base class for deferred task execution with lazy promise creation.
|
|
7
|
-
* AI-NOTE: Promises are created lazily via makePromise() - not in constructor.
|
|
8
|
-
* This allows tasks to be created without immediate promise overhead.
|
|
9
|
-
*/
|
|
10
5
|
export class MicroTask {
|
|
11
6
|
#promise;
|
|
12
7
|
#resolve;
|
|
13
8
|
#reject;
|
|
14
9
|
#settled;
|
|
10
|
+
#cancelError;
|
|
15
11
|
constructor(fn) {
|
|
16
12
|
this.fn = fn;
|
|
17
|
-
// AI-NOTE: Private fields initialized to null - lazy initialization pattern
|
|
18
13
|
this.#promise = null;
|
|
19
14
|
this.#resolve = null;
|
|
20
15
|
this.#reject = null;
|
|
21
16
|
this.#settled = false;
|
|
17
|
+
this.#cancelError = null;
|
|
22
18
|
this.isCanceled = false;
|
|
23
19
|
}
|
|
24
|
-
// AI-NOTE: Returns null until makePromise() is called - this is intentional
|
|
25
20
|
get promise() {
|
|
26
21
|
return this.#promise;
|
|
27
22
|
}
|
|
28
23
|
get settled() {
|
|
29
24
|
return this.#settled;
|
|
30
25
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* falls back to manual Promise constructor for broader compatibility.
|
|
35
|
-
*/
|
|
26
|
+
get cancelError() {
|
|
27
|
+
return this.#cancelError;
|
|
28
|
+
}
|
|
36
29
|
makePromise() {
|
|
37
30
|
if (this.#promise) return this;
|
|
38
31
|
if (typeof Promise.withResolvers == 'function') {
|
|
@@ -47,9 +40,20 @@ export class MicroTask {
|
|
|
47
40
|
this.#reject = reject;
|
|
48
41
|
});
|
|
49
42
|
}
|
|
43
|
+
if (this.isCanceled) {
|
|
44
|
+
this.#reject(
|
|
45
|
+
new CancelTaskError(undefined, this.#cancelError ? {cause: this.#cancelError} : undefined)
|
|
46
|
+
);
|
|
47
|
+
this.#resolve = null;
|
|
48
|
+
this.#reject = null;
|
|
49
|
+
this.#settled = true;
|
|
50
|
+
}
|
|
50
51
|
return this;
|
|
51
52
|
}
|
|
52
53
|
resolve(value) {
|
|
54
|
+
if (!this.#promise) {
|
|
55
|
+
throw new Error('MicroTask: resolve() called before makePromise()');
|
|
56
|
+
}
|
|
53
57
|
if (this.#resolve) {
|
|
54
58
|
this.#resolve(value);
|
|
55
59
|
this.#resolve = null;
|
|
@@ -58,14 +62,15 @@ export class MicroTask {
|
|
|
58
62
|
}
|
|
59
63
|
return this;
|
|
60
64
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Cancel the task with optional error cause.
|
|
63
|
-
* AI-NOTE: Always rejects with CancelTaskError to distinguish from other errors.
|
|
64
|
-
*/
|
|
65
65
|
cancel(error) {
|
|
66
66
|
this.isCanceled = true;
|
|
67
|
+
if (error !== undefined && this.#cancelError === null) {
|
|
68
|
+
this.#cancelError = error;
|
|
69
|
+
}
|
|
67
70
|
if (this.#reject) {
|
|
68
|
-
this.#reject(
|
|
71
|
+
this.#reject(
|
|
72
|
+
new CancelTaskError(undefined, this.#cancelError ? {cause: this.#cancelError} : undefined)
|
|
73
|
+
);
|
|
69
74
|
this.#resolve = null;
|
|
70
75
|
this.#reject = null;
|
|
71
76
|
this.#settled = true;
|
package/src/MicroTaskQueue.js
CHANGED
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import MicroTask from './MicroTask.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* AI-NOTE: This is an abstract base class - concrete implementations should extend
|
|
8
|
-
* ListQueue for actual task storage and processing.
|
|
9
|
-
* @see ListQueue - The primary class for queue implementations
|
|
10
|
-
*/
|
|
5
|
+
const returnArgs = (...args) => args;
|
|
6
|
+
|
|
11
7
|
export class MicroTaskQueue {
|
|
12
8
|
constructor(paused) {
|
|
13
9
|
this.paused = Boolean(paused);
|
|
14
10
|
}
|
|
15
|
-
//
|
|
16
|
-
// AI-NOTE: Base implementation returns true - subclasses override with actual logic
|
|
11
|
+
// overridden in subclasses
|
|
17
12
|
get isEmpty() {
|
|
18
13
|
return true;
|
|
19
14
|
}
|
|
@@ -25,12 +20,7 @@ export class MicroTaskQueue {
|
|
|
25
20
|
this.paused = false;
|
|
26
21
|
return this;
|
|
27
22
|
}
|
|
28
|
-
|
|
29
|
-
* Enqueue a function for execution.
|
|
30
|
-
* AI-NOTE: Creates MicroTask but does NOT create promise automatically.
|
|
31
|
-
* Call task.makePromise() if promise access is needed.
|
|
32
|
-
*/
|
|
33
|
-
enqueue(fn) {
|
|
23
|
+
enqueue(fn, ..._args) {
|
|
34
24
|
const task = new MicroTask(fn);
|
|
35
25
|
return task;
|
|
36
26
|
}
|
|
@@ -41,31 +31,23 @@ export class MicroTaskQueue {
|
|
|
41
31
|
clear() {
|
|
42
32
|
return this;
|
|
43
33
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* AI-NOTE: This is the convenience method - it calls both enqueue() and makePromise().
|
|
47
|
-
* Returns a MicroTask with an active promise.
|
|
48
|
-
*/
|
|
49
|
-
schedule(fn, ...args) {
|
|
50
|
-
fn ||= MicroTaskQueue.returnArgs;
|
|
34
|
+
schedule(fn, ...scheduleArgs) {
|
|
35
|
+
fn ||= returnArgs;
|
|
51
36
|
const task = this.enqueue(
|
|
52
|
-
function (...
|
|
37
|
+
function (...invocationArgs) {
|
|
53
38
|
this.makePromise();
|
|
54
39
|
try {
|
|
55
|
-
this.resolve(fn(...
|
|
40
|
+
this.resolve(fn(...invocationArgs));
|
|
56
41
|
} catch (error) {
|
|
57
42
|
this.cancel(error);
|
|
58
43
|
}
|
|
59
44
|
return this.promise;
|
|
60
45
|
},
|
|
61
|
-
...
|
|
46
|
+
...scheduleArgs
|
|
62
47
|
);
|
|
63
48
|
task.makePromise();
|
|
64
49
|
return task;
|
|
65
50
|
}
|
|
66
|
-
static returnArgs(...args) {
|
|
67
|
-
return args;
|
|
68
|
-
}
|
|
69
51
|
}
|
|
70
52
|
|
|
71
53
|
export default MicroTaskQueue;
|
package/src/PageWatcher.js
CHANGED
|
@@ -10,12 +10,9 @@ const eventHandlerOptions = {capture: true},
|
|
|
10
10
|
// valid states: active, passive, hidden, frozen, terminated
|
|
11
11
|
|
|
12
12
|
const getState = () => {
|
|
13
|
-
if (document
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (document.hasFocus()) {
|
|
17
|
-
return 'active';
|
|
18
|
-
}
|
|
13
|
+
if (typeof document == 'undefined') return 'active';
|
|
14
|
+
if (document.visibilityState === 'hidden') return 'hidden';
|
|
15
|
+
if (document.hasFocus()) return 'active';
|
|
19
16
|
return 'passive';
|
|
20
17
|
};
|
|
21
18
|
|
|
@@ -44,10 +41,16 @@ export class PageWatcher extends ListQueue {
|
|
|
44
41
|
|
|
45
42
|
// Implemented in ListQueue: dequeue()
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
/** @returns {never} */
|
|
45
|
+
schedule(_fn) {
|
|
48
46
|
throw new Error('Not implemented');
|
|
49
47
|
}
|
|
50
48
|
|
|
49
|
+
// Override of ListQueue.clear() that skips the parent's pause/resume cycle.
|
|
50
|
+
// PageWatcher.pause/resume add and remove DOM event listeners on every call,
|
|
51
|
+
// so bouncing them around an internal clear() just to drain the task list
|
|
52
|
+
// would thrash listeners for no reason. The watching state is independent
|
|
53
|
+
// of whether tasks are pending.
|
|
51
54
|
clear() {
|
|
52
55
|
while (!this.list.isEmpty) {
|
|
53
56
|
const task = this.list.popFront();
|
package/src/Retainer.d.ts
CHANGED
|
@@ -26,9 +26,11 @@ export declare class Retainer<T = unknown> implements RetainerOptions<T> {
|
|
|
26
26
|
counter: number;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* The value currently retained.
|
|
29
|
+
* The value currently retained. Read-only — managed internally by `get()`
|
|
30
|
+
* and `release()`. Reads return the live value or `null` when nothing is
|
|
31
|
+
* currently held.
|
|
30
32
|
*/
|
|
31
|
-
value: T | null;
|
|
33
|
+
readonly value: T | null;
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* The function to create a value.
|
package/src/Retainer.js
CHANGED
|
@@ -1,45 +1,55 @@
|
|
|
1
1
|
// @ts-self-types="./Retainer.d.ts"
|
|
2
2
|
|
|
3
3
|
export class Retainer {
|
|
4
|
+
#value = null;
|
|
5
|
+
#handle = null;
|
|
6
|
+
/** @type {Promise<*> | null} */
|
|
7
|
+
#creating = null;
|
|
8
|
+
|
|
4
9
|
constructor({create, destroy, retentionPeriod = 1_000}) {
|
|
5
10
|
if (!create || !destroy) throw new Error('Retainer: create and destroy are required');
|
|
6
11
|
this.create = create;
|
|
7
12
|
this.destroy = destroy;
|
|
8
13
|
this.retentionPeriod = retentionPeriod;
|
|
9
14
|
this.counter = 0;
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get value() {
|
|
18
|
+
return this.#value;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
async get() {
|
|
15
22
|
if (!this.counter) {
|
|
16
|
-
if (this
|
|
17
|
-
clearTimeout(this
|
|
18
|
-
this
|
|
23
|
+
if (this.#handle) {
|
|
24
|
+
clearTimeout(this.#handle);
|
|
25
|
+
this.#handle = null;
|
|
19
26
|
} else {
|
|
20
|
-
this
|
|
27
|
+
if (!this.#creating) this.#creating = this.create();
|
|
28
|
+
try {
|
|
29
|
+
this.#value = await this.#creating;
|
|
30
|
+
} finally {
|
|
31
|
+
this.#creating = null;
|
|
32
|
+
}
|
|
21
33
|
}
|
|
22
34
|
}
|
|
23
35
|
++this.counter;
|
|
24
|
-
return this
|
|
36
|
+
return this.#value;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
async release(immediately) {
|
|
28
40
|
if (this.counter <= 0) throw new Error('Retainer: counter is already zero');
|
|
29
41
|
if (--this.counter) return this;
|
|
30
42
|
if (immediately) {
|
|
31
|
-
const value = this
|
|
32
|
-
this
|
|
43
|
+
const value = this.#value;
|
|
44
|
+
this.#value = null;
|
|
33
45
|
await this.destroy(value);
|
|
34
46
|
return this;
|
|
35
47
|
}
|
|
36
|
-
this
|
|
37
|
-
const value = this
|
|
38
|
-
this
|
|
39
|
-
this
|
|
40
|
-
|
|
41
|
-
.then(() => this.destroy(value))
|
|
42
|
-
.catch(() => {});
|
|
48
|
+
this.#handle = setTimeout(async () => {
|
|
49
|
+
const value = this.#value;
|
|
50
|
+
this.#value = null;
|
|
51
|
+
this.#handle = null;
|
|
52
|
+
await this.destroy(value);
|
|
43
53
|
}, this.retentionPeriod);
|
|
44
54
|
return this;
|
|
45
55
|
}
|
package/src/Scheduler.d.ts
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
|
+
import MinHeap from 'list-toolkit/heap.js';
|
|
1
2
|
import MicroTask from './MicroTask.js';
|
|
2
3
|
import MicroTaskQueue from './MicroTaskQueue.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* A task that will be executed at a later time by `Scheduler`.
|
|
7
|
+
* Inherits `makePromise()`, `promise`, `settled`, `cancelError`, `resolve()`,
|
|
8
|
+
* and `cancel()` from `MicroTask` — see `MicroTask.d.ts` for their contracts.
|
|
6
9
|
*/
|
|
7
10
|
export declare class Task extends MicroTask {
|
|
8
11
|
/**
|
|
9
|
-
* The function to execute.
|
|
12
|
+
* The function to execute. Narrower signature than `MicroTask.fn`.
|
|
10
13
|
*/
|
|
11
14
|
fn: (arg: {task: Task; scheduler: Scheduler}) => unknown;
|
|
12
15
|
|
|
13
|
-
/**
|
|
14
|
-
* Whether the task has been canceled.
|
|
15
|
-
*/
|
|
16
|
-
isCanceled: boolean;
|
|
17
|
-
|
|
18
16
|
/**
|
|
19
17
|
* The time in milliseconds (Unix timestamp) when the task is scheduled to run.
|
|
20
18
|
*/
|
|
@@ -31,45 +29,36 @@ export declare class Task extends MicroTask {
|
|
|
31
29
|
* @param fn The function to execute.
|
|
32
30
|
*/
|
|
33
31
|
constructor(delay: number | Date, fn: (arg: {task: Task; scheduler: Scheduler}) => unknown);
|
|
32
|
+
}
|
|
34
33
|
|
|
34
|
+
/**
|
|
35
|
+
* A scheduler that manages tasks to be executed at a later time.
|
|
36
|
+
*/
|
|
37
|
+
export declare class Scheduler extends MicroTaskQueue {
|
|
35
38
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*/
|
|
39
|
-
makePromise(): this;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The promise that could be resolved when the microtask is executed.
|
|
43
|
-
* This is a queue-specific promise. It may be created when there is an associated asynchronous task.
|
|
44
|
-
* If the microtask is canceled, the promise will be rejected with a CancelTaskError.
|
|
39
|
+
* Whether the scheduler is paused.
|
|
40
|
+
* When paused, new tasks are queued but not executed immediately.
|
|
45
41
|
*/
|
|
46
|
-
|
|
42
|
+
paused: boolean;
|
|
47
43
|
|
|
48
44
|
/**
|
|
49
|
-
*
|
|
50
|
-
* @param value The value to resolve the microtask with.
|
|
51
|
-
* @returns The microtask.
|
|
45
|
+
* The min-heap of pending tasks ordered by `time`.
|
|
52
46
|
*/
|
|
53
|
-
|
|
47
|
+
queue: MinHeap<Task>;
|
|
54
48
|
|
|
55
49
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* @param error The optional error to use as the cause of the cancellation.
|
|
59
|
-
* @returns The microtask.
|
|
50
|
+
* The function that stops the scheduler loop.
|
|
51
|
+
* It is used internally by `pause()` and `resume()`.
|
|
60
52
|
*/
|
|
61
|
-
|
|
62
|
-
}
|
|
53
|
+
stopQueue: (() => void) | null;
|
|
63
54
|
|
|
64
|
-
/**
|
|
65
|
-
* A scheduler that manages tasks to be executed at a later time.
|
|
66
|
-
*/
|
|
67
|
-
export declare class Scheduler extends MicroTaskQueue {
|
|
68
55
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
56
|
+
* Optional handler invoked when a scheduled `task.fn` throws.
|
|
57
|
+
* If unset, exceptions are surfaced via `Promise.reject(error)` so they
|
|
58
|
+
* fire the standard unhandled-rejection channel; the scheduler loop
|
|
59
|
+
* always continues regardless.
|
|
71
60
|
*/
|
|
72
|
-
|
|
61
|
+
onError: ((error: unknown, task: Task) => void) | null;
|
|
73
62
|
|
|
74
63
|
/**
|
|
75
64
|
* The tolerance for comparing starting times of tasks.
|
|
@@ -141,6 +130,19 @@ export declare class Scheduler extends MicroTaskQueue {
|
|
|
141
130
|
* @returns The scheduler instance for chaining.
|
|
142
131
|
*/
|
|
143
132
|
resume(): this;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Starts the scheduler loop by arming a `setTimeout` for the next-due task.
|
|
136
|
+
* Used internally by `enqueue()` and `resume()`.
|
|
137
|
+
* @returns The function that stops the scheduler loop.
|
|
138
|
+
*/
|
|
139
|
+
startQueue(): (() => void) | null;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Processes due tasks. Called by the scheduler's internal `setTimeout`
|
|
143
|
+
* timer — not part of the typical user surface.
|
|
144
|
+
*/
|
|
145
|
+
processTasks(): void;
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
/**
|
package/src/Scheduler.js
CHANGED
|
@@ -20,9 +20,12 @@ export class Task extends MicroTask {
|
|
|
20
20
|
export class Scheduler extends MicroTaskQueue {
|
|
21
21
|
constructor(paused, tolerance = 4) {
|
|
22
22
|
super(paused);
|
|
23
|
-
|
|
23
|
+
/** @type {(a: Task, b: Task) => boolean} */
|
|
24
|
+
const less = (a, b) => a.time < b.time;
|
|
25
|
+
this.queue = new MinHeap({less});
|
|
24
26
|
this.tolerance = tolerance;
|
|
25
27
|
this.stopQueue = null;
|
|
28
|
+
this.onError = null;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
get isEmpty() {
|
|
@@ -70,8 +73,8 @@ export class Scheduler extends MicroTaskQueue {
|
|
|
70
73
|
dequeue(task) {
|
|
71
74
|
task.cancel();
|
|
72
75
|
if (this.queue.isEmpty) return this;
|
|
73
|
-
if (!this.queue.has(task)) return this;
|
|
74
76
|
if (this.paused || this.queue.top !== task) {
|
|
77
|
+
// MinHeap.remove no-ops on missing items, so no pre-check needed
|
|
75
78
|
this.queue.remove(task);
|
|
76
79
|
return this;
|
|
77
80
|
}
|
|
@@ -110,7 +113,18 @@ export class Scheduler extends MicroTaskQueue {
|
|
|
110
113
|
) {
|
|
111
114
|
const task = this.queue.pop();
|
|
112
115
|
if (task.isCanceled) continue;
|
|
113
|
-
|
|
116
|
+
try {
|
|
117
|
+
task.fn({task, scheduler: this});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// keep the loop alive; surface via onError callback or unhandled-rejection channel
|
|
120
|
+
if (this.onError) {
|
|
121
|
+
try {
|
|
122
|
+
this.onError(error, task);
|
|
123
|
+
} catch {}
|
|
124
|
+
} else {
|
|
125
|
+
Promise.reject(error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
if (!this.paused && !this.queue.isEmpty) this.stopQueue = this.startQueue();
|
|
@@ -120,7 +134,9 @@ export class Scheduler extends MicroTaskQueue {
|
|
|
120
134
|
export const repeat = (fn, delay) => {
|
|
121
135
|
const repeatableFunction = ({task, scheduler}) => {
|
|
122
136
|
fn({task, scheduler});
|
|
123
|
-
if (
|
|
137
|
+
if (task.isCanceled) return;
|
|
138
|
+
const next = isNaN(delay) ? task.delay : delay;
|
|
139
|
+
scheduler.enqueue(repeatableFunction, typeof next == 'number' && next < 1 ? 1 : next);
|
|
124
140
|
};
|
|
125
141
|
return repeatableFunction;
|
|
126
142
|
};
|
package/src/Throttler.d.ts
CHANGED
|
@@ -17,7 +17,20 @@ export declare type ThrottlerOptions = {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* A throttler that
|
|
20
|
+
* A throttler that paces the execution of work based on a key.
|
|
21
|
+
*
|
|
22
|
+
* Despite the name, this is **rate-limiting / pacing**, not classical
|
|
23
|
+
* "drop excess" throttle. `Throttler` does not execute functions; it
|
|
24
|
+
* exposes `wait(key)` and the caller decides what to do once awaited.
|
|
25
|
+
* Because there is nothing for `Throttler` to drop, every call is honored
|
|
26
|
+
* — N rapid calls produce delays roughly `0, throttleTimeout,
|
|
27
|
+
* 2*throttleTimeout, …`, spacing the work out across time. Use it when
|
|
28
|
+
* you need every operation to run but no faster than `throttleTimeout`
|
|
29
|
+
* apart per key.
|
|
30
|
+
*
|
|
31
|
+
* If you want React/Lodash-style "fire-leading-edge-only-then-drop"
|
|
32
|
+
* semantics, wrap your callback with `throttle()` from this package
|
|
33
|
+
* instead.
|
|
21
34
|
*/
|
|
22
35
|
export declare class Throttler implements ThrottlerOptions {
|
|
23
36
|
/**
|
|
@@ -54,16 +67,20 @@ export declare class Throttler implements ThrottlerOptions {
|
|
|
54
67
|
getLastSeen(key: unknown): number;
|
|
55
68
|
|
|
56
69
|
/**
|
|
57
|
-
* Retrieves the delay for a key.
|
|
70
|
+
* Retrieves the delay for a key and reserves the next slot.
|
|
71
|
+
* Each call extends the per-key queue: stored state advances by the returned
|
|
72
|
+
* delay, so the next call for the same key waits at least `throttleTimeout`
|
|
73
|
+
* past the slot just allocated. Returns the delay in milliseconds.
|
|
58
74
|
* @param key The key to retrieve the delay for.
|
|
59
|
-
* @returns The delay
|
|
75
|
+
* @returns The delay before this caller's slot opens, in milliseconds.
|
|
60
76
|
*/
|
|
61
77
|
getDelay(key: unknown): number;
|
|
62
78
|
|
|
63
79
|
/**
|
|
64
|
-
* Waits for
|
|
80
|
+
* Waits until this caller's slot for `key` opens. See class docs for the
|
|
81
|
+
* pacing semantics — every call is queued, none are dropped.
|
|
65
82
|
* @param key The key to wait for.
|
|
66
|
-
* @returns A promise that resolves when the
|
|
83
|
+
* @returns A promise that resolves when the slot is available.
|
|
67
84
|
*/
|
|
68
85
|
wait(key: unknown): Promise<void>;
|
|
69
86
|
|
package/src/Throttler.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import sleep from './sleep.js';
|
|
4
4
|
|
|
5
5
|
export class Throttler {
|
|
6
|
+
#handle = null;
|
|
7
|
+
|
|
6
8
|
constructor({
|
|
7
9
|
throttleTimeout = 1_000,
|
|
8
10
|
neverSeenTimeout = 0,
|
|
@@ -12,7 +14,6 @@ export class Throttler {
|
|
|
12
14
|
this.neverSeenTimeout = neverSeenTimeout;
|
|
13
15
|
this.vacuumPeriod = vacuumPeriod;
|
|
14
16
|
this.lastSeen = new Map();
|
|
15
|
-
this.handle = null;
|
|
16
17
|
if (this.vacuumPeriod) this.startVacuum();
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -38,7 +39,7 @@ export class Throttler {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
get isVacuuming() {
|
|
41
|
-
return !!this
|
|
42
|
+
return !!this.#handle;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
vacuum() {
|
|
@@ -51,18 +52,18 @@ export class Throttler {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
startVacuum() {
|
|
54
|
-
if (this
|
|
55
|
-
this
|
|
55
|
+
if (this.#handle) return this;
|
|
56
|
+
this.#handle = setInterval(() => {
|
|
56
57
|
this.vacuum();
|
|
57
58
|
}, this.vacuumPeriod);
|
|
58
|
-
if (this
|
|
59
|
+
if (this.#handle.unref) this.#handle.unref();
|
|
59
60
|
return this;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
stopVacuum() {
|
|
63
|
-
if (!this
|
|
64
|
-
clearInterval(this
|
|
65
|
-
this
|
|
64
|
+
if (!this.#handle) return this;
|
|
65
|
+
clearInterval(this.#handle);
|
|
66
|
+
this.#handle = null;
|
|
66
67
|
return this;
|
|
67
68
|
}
|
|
68
69
|
}
|
package/src/audit.js
CHANGED
package/src/defer.js
CHANGED
package/src/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @ts-self-types="./index.d.ts"
|
|
2
|
+
|
|
3
|
+
export * from './audit.js';
|
|
4
|
+
export * from './batch.js';
|
|
5
|
+
export * from './debounce.js';
|
|
6
|
+
export * from './defer.js';
|
|
7
|
+
export * from './sample.js';
|
|
8
|
+
export * from './sleep.js';
|
|
9
|
+
export * from './throttle.js';
|
|
10
|
+
|
|
11
|
+
export * from './CancelTaskError.js';
|
|
12
|
+
export * from './MicroTask.js';
|
|
13
|
+
export * from './MicroTaskQueue.js';
|
|
14
|
+
export * from './LimitedQueue.js';
|
|
15
|
+
export * from './ListQueue.js';
|
|
16
|
+
export * from './FrameQueue.js';
|
|
17
|
+
export {IdleQueue, idleQueue} from './IdleQueue.js';
|
|
18
|
+
|
|
19
|
+
export * from './Counter.js';
|
|
20
|
+
export * from './PageWatcher.js';
|
|
21
|
+
export * from './Retainer.js';
|
|
22
|
+
export {Task as SchedulerTask, Scheduler, repeat, scheduler} from './Scheduler.js';
|
|
23
|
+
export * from './Throttler.js';
|
|
24
|
+
|
|
25
|
+
export * from './random-dist.js';
|
|
26
|
+
export * from './random-sleep.js';
|
|
27
|
+
|
|
28
|
+
export * from './when-dom-loaded.js';
|
|
29
|
+
export {whenLoaded, scheduleWhenLoaded} from './when-loaded.js';
|
package/src/random-dist.js
CHANGED
|
@@ -10,12 +10,15 @@ export const normal = (mean, stdDev, skewness = 0) => {
|
|
|
10
10
|
v = 0;
|
|
11
11
|
while (!u) u = Math.random(); // Converting [0,1) to (0,1)
|
|
12
12
|
while (!v) v = Math.random();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const r = Math.sqrt(-2.0 * Math.log(u)),
|
|
14
|
+
theta = 2.0 * Math.PI * v,
|
|
15
|
+
z1 = r * Math.cos(theta);
|
|
16
|
+
if (!skewness) return z1 * stdDev + mean;
|
|
17
|
+
// Box-Muller yields two independent N(0,1) samples per (u, v) pair; use both for skew-normal.
|
|
18
|
+
const z2 = r * Math.sin(theta),
|
|
19
|
+
delta = skewness / Math.sqrt(1 + skewness * skewness),
|
|
20
|
+
x = delta * z1 + Math.sqrt(1 - delta * delta) * z2;
|
|
21
|
+
return (z1 >= 0 ? x : -x) * stdDev + mean;
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
export const expo = lambda => {
|
package/src/sample.js
CHANGED
package/src/when-dom-loaded.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import ValueList from 'list-toolkit/value-list.js';
|
|
4
4
|
|
|
5
|
+
/** @type {ValueList<() => void>} */
|
|
5
6
|
const waitingForDom = new ValueList();
|
|
6
7
|
|
|
7
8
|
export const remove = fn => {
|
|
8
|
-
for (const node of waitingForDom.
|
|
9
|
+
for (const node of waitingForDom.getNodeIterator()) {
|
|
9
10
|
if (node.value === fn) {
|
|
10
11
|
waitingForDom.removeNode(node);
|
|
11
12
|
return true;
|
package/src/when-loaded.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import ValueList from 'list-toolkit/value-list.js';
|
|
4
4
|
|
|
5
|
+
/** @type {ValueList<() => void>} */
|
|
5
6
|
const waitingForLoad = new ValueList();
|
|
6
7
|
|
|
7
8
|
export const remove = fn => {
|
|
8
|
-
for (const node of waitingForLoad.
|
|
9
|
+
for (const node of waitingForLoad.getNodeIterator()) {
|
|
9
10
|
if (node.value === fn) {
|
|
10
11
|
waitingForLoad.removeNode(node);
|
|
11
12
|
return true;
|