time-queues 1.3.1 → 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 +4 -0
- package/llms-full.txt +19 -9
- package/llms.txt +9 -5
- package/package.json +23 -11
- package/src/Counter.d.ts +16 -2
- package/src/Counter.js +12 -13
- package/src/FrameQueue.d.ts +10 -3
- package/src/FrameQueue.js +1 -17
- package/src/IdleQueue.d.ts +16 -6
- package/src/IdleQueue.js +1 -17
- package/src/LimitedQueue.d.ts +3 -3
- package/src/LimitedQueue.js +11 -12
- package/src/ListQueue.d.ts +19 -5
- package/src/ListQueue.js +23 -18
- package/src/MicroTask.d.ts +27 -6
- package/src/MicroTask.js +22 -17
- package/src/MicroTaskQueue.d.ts +3 -3
- package/src/MicroTaskQueue.js +9 -27
- package/src/PageWatcher.d.ts +2 -2
- package/src/PageWatcher.js +14 -9
- package/src/Retainer.d.ts +4 -2
- package/src/Retainer.js +25 -13
- package/src/Scheduler.d.ts +44 -42
- package/src/Scheduler.js +21 -3
- package/src/Throttler.d.ts +23 -10
- package/src/Throttler.js +9 -8
- package/src/audit.js +1 -1
- package/src/defer.js +1 -0
- package/src/index.d.ts +23 -25
- package/src/index.js +29 -0
- package/src/random-dist.d.ts +1 -1
- package/src/random-dist.js +9 -6
- package/src/random-sleep.d.ts +4 -4
- package/src/sample.js +1 -1
- package/src/when-dom-loaded.js +2 -1
- package/src/when-loaded.js +2 -1
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
|
@@ -5,7 +5,7 @@ export declare class MicroTask {
|
|
|
5
5
|
/**
|
|
6
6
|
* The function to execute when the microtask is scheduled.
|
|
7
7
|
*/
|
|
8
|
-
fn: () => unknown;
|
|
8
|
+
fn: (...args: any[]) => unknown;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Whether the microtask has been canceled.
|
|
@@ -16,10 +16,14 @@ export declare class MicroTask {
|
|
|
16
16
|
* Creates a new microtask.
|
|
17
17
|
* @param fn The function to execute when the microtask is scheduled.
|
|
18
18
|
*/
|
|
19
|
-
constructor(fn: () => unknown);
|
|
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.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import MicroTask from './MicroTask';
|
|
1
|
+
import MicroTask from './MicroTask.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A queue of microtasks that will be executed when scheduled.
|
|
@@ -29,7 +29,7 @@ export declare class MicroTaskQueue {
|
|
|
29
29
|
* @param fn The function to execute when the microtask is scheduled.
|
|
30
30
|
* @returns The enqueued microtask.
|
|
31
31
|
*/
|
|
32
|
-
enqueue(fn: () => unknown): MicroTask;
|
|
32
|
+
enqueue(fn: (...args: any[]) => unknown, ...args: unknown[]): MicroTask;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Dequeues a microtask from the queue.
|
|
@@ -46,7 +46,7 @@ export declare class MicroTaskQueue {
|
|
|
46
46
|
* @param args Additional arguments to pass to `enqueue()`. They are there to accommodate custom implementations in subclasses.
|
|
47
47
|
* @returns The scheduled microtask.
|
|
48
48
|
*/
|
|
49
|
-
schedule(fn: (() => unknown) | null | undefined, ...args: unknown[]): MicroTask;
|
|
49
|
+
schedule(fn: ((...args: any[]) => unknown) | null | undefined, ...args: unknown[]): MicroTask;
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Clears all tasks from the queue.
|
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.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ListQueue, Task} from './ListQueue';
|
|
1
|
+
import {ListQueue, Task} from './ListQueue.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A page state.
|
|
@@ -82,7 +82,7 @@ export declare class PageWatcher extends ListQueue {
|
|
|
82
82
|
*/
|
|
83
83
|
export declare const watchStates: (
|
|
84
84
|
queue: ListQueue,
|
|
85
|
-
resumeStatesList
|
|
85
|
+
resumeStatesList?: PageState[]
|
|
86
86
|
) => (state: PageState) => void;
|
|
87
87
|
|
|
88
88
|
/**
|
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
|
|
|
@@ -27,7 +24,6 @@ export class PageWatcher extends ListQueue {
|
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
pause() {
|
|
30
|
-
this.paused = true;
|
|
31
27
|
watchedEvents.forEach(type => removeEventListener(type, this, eventHandlerOptions));
|
|
32
28
|
return super.pause();
|
|
33
29
|
}
|
|
@@ -45,12 +41,21 @@ export class PageWatcher extends ListQueue {
|
|
|
45
41
|
|
|
46
42
|
// Implemented in ListQueue: dequeue()
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
/** @returns {never} */
|
|
45
|
+
schedule(_fn) {
|
|
49
46
|
throw new Error('Not implemented');
|
|
50
47
|
}
|
|
51
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.
|
|
52
54
|
clear() {
|
|
53
|
-
this.list.
|
|
55
|
+
while (!this.list.isEmpty) {
|
|
56
|
+
const task = this.list.popFront();
|
|
57
|
+
task.cancel();
|
|
58
|
+
}
|
|
54
59
|
return this;
|
|
55
60
|
}
|
|
56
61
|
|
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,42 +1,54 @@
|
|
|
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
|
|
48
|
+
this.#handle = setTimeout(async () => {
|
|
49
|
+
const value = this.#value;
|
|
50
|
+
this.#value = null;
|
|
51
|
+
this.#handle = null;
|
|
40
52
|
await this.destroy(value);
|
|
41
53
|
}, this.retentionPeriod);
|
|
42
54
|
return this;
|
package/src/Scheduler.d.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import MinHeap from 'list-toolkit/heap.js';
|
|
2
|
+
import MicroTask from './MicroTask.js';
|
|
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
|
-
fn: ({task: Task
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Whether the task has been canceled.
|
|
15
|
-
*/
|
|
16
|
-
isCanceled: boolean;
|
|
14
|
+
fn: (arg: {task: Task; scheduler: Scheduler}) => unknown;
|
|
17
15
|
|
|
18
16
|
/**
|
|
19
17
|
* The time in milliseconds (Unix timestamp) when the task is scheduled to run.
|
|
@@ -30,46 +28,37 @@ export declare class Task extends MicroTask {
|
|
|
30
28
|
* @param delay The delay before the task is executed. It can be a number of milliseconds or a `Date` object as an absolute time.
|
|
31
29
|
* @param fn The function to execute.
|
|
32
30
|
*/
|
|
33
|
-
constructor(delay: number | Date, fn: ({task: Task
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Makes a promise that will be resolved when the microtask is executed.
|
|
37
|
-
* @returns The microtask.
|
|
38
|
-
*/
|
|
39
|
-
makePromise(): this;
|
|
31
|
+
constructor(delay: number | Date, fn: (arg: {task: Task; scheduler: Scheduler}) => unknown);
|
|
32
|
+
}
|
|
40
33
|
|
|
34
|
+
/**
|
|
35
|
+
* A scheduler that manages tasks to be executed at a later time.
|
|
36
|
+
*/
|
|
37
|
+
export declare class Scheduler extends MicroTaskQueue {
|
|
41
38
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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.
|
|
@@ -102,14 +91,14 @@ export declare class Scheduler extends MicroTaskQueue {
|
|
|
102
91
|
* @param delay The delay before the task is executed. It can be a number of milliseconds or a `Date` object as an absolute time.
|
|
103
92
|
* @returns The task object that was enqueued.
|
|
104
93
|
*/
|
|
105
|
-
enqueue(fn: ({task: Task
|
|
94
|
+
enqueue(fn: (arg: {task: Task; scheduler: Scheduler}) => unknown, delay: number | Date): Task;
|
|
106
95
|
|
|
107
96
|
/**
|
|
108
97
|
* Removes a task from the scheduler.
|
|
109
98
|
* @param task The task to remove.
|
|
110
99
|
* @returns The scheduler object for chaining.
|
|
111
100
|
*/
|
|
112
|
-
dequeue(task:
|
|
101
|
+
dequeue(task: MicroTask): this;
|
|
113
102
|
|
|
114
103
|
/**
|
|
115
104
|
* Schedules a task to run in the future.
|
|
@@ -118,7 +107,7 @@ export declare class Scheduler extends MicroTaskQueue {
|
|
|
118
107
|
* @returns The task object that was scheduled.
|
|
119
108
|
*/
|
|
120
109
|
schedule(
|
|
121
|
-
fn: (({task: Task
|
|
110
|
+
fn: ((arg: {task: Task; scheduler: Scheduler}) => unknown) | null | undefined,
|
|
122
111
|
delay: number | Date
|
|
123
112
|
): Task;
|
|
124
113
|
|
|
@@ -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
|
/**
|
|
@@ -150,9 +152,9 @@ export declare class Scheduler extends MicroTaskQueue {
|
|
|
150
152
|
* @returns A function that can be used to enqueue the task to the scheduler.
|
|
151
153
|
*/
|
|
152
154
|
export declare const repeat: (
|
|
153
|
-
fn: ({task: Task
|
|
155
|
+
fn: (arg: {task: Task; scheduler: Scheduler}) => void,
|
|
154
156
|
delay: number | Date
|
|
155
|
-
) => ({task: Task
|
|
157
|
+
) => (arg: {task: Task; scheduler: Scheduler}) => void;
|
|
156
158
|
|
|
157
159
|
/**
|
|
158
160
|
* A scheduler instance usually used as a global scheduler.
|
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() {
|
|
@@ -71,6 +74,7 @@ export class Scheduler extends MicroTaskQueue {
|
|
|
71
74
|
task.cancel();
|
|
72
75
|
if (this.queue.isEmpty) return this;
|
|
73
76
|
if (this.paused || this.queue.top !== task) {
|
|
77
|
+
// MinHeap.remove no-ops on missing items, so no pre-check needed
|
|
74
78
|
this.queue.remove(task);
|
|
75
79
|
return this;
|
|
76
80
|
}
|
|
@@ -108,7 +112,19 @@ export class Scheduler extends MicroTaskQueue {
|
|
|
108
112
|
this.queue.top.time <= Date.now() + this.tolerance
|
|
109
113
|
) {
|
|
110
114
|
const task = this.queue.pop();
|
|
111
|
-
task.
|
|
115
|
+
if (task.isCanceled) continue;
|
|
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
|
+
}
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
if (!this.paused && !this.queue.isEmpty) this.stopQueue = this.startQueue();
|
|
@@ -118,7 +134,9 @@ export class Scheduler extends MicroTaskQueue {
|
|
|
118
134
|
export const repeat = (fn, delay) => {
|
|
119
135
|
const repeatableFunction = ({task, scheduler}) => {
|
|
120
136
|
fn({task, scheduler});
|
|
121
|
-
|
|
137
|
+
if (task.isCanceled) return;
|
|
138
|
+
const next = isNaN(delay) ? task.delay : delay;
|
|
139
|
+
scheduler.enqueue(repeatableFunction, typeof next == 'number' && next < 1 ? 1 : next);
|
|
122
140
|
};
|
|
123
141
|
return repeatableFunction;
|
|
124
142
|
};
|