time-queues 1.0.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 ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Eugene Lazutkin
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # time-queues [![NPM version][npm-img]][npm-url]
2
+
3
+ [npm-img]: https://img.shields.io/npm/v/time-queus.svg
4
+ [npm-url]: https://npmjs.org/package/time-queues
5
+
6
+ `time-queues` is an efficient library for organizing asynchronous multitasking and scheduled tasks.
7
+ It can be used in a browser and in server-side environments like `Node`, `Deno` and `Bun`.
8
+ It depends only on [list-toolkit](https://github.com/uhop/list-toolkit), which is a no-dependency library for efficient task queues.
9
+
10
+ The following features are provided:
11
+
12
+ * All environments:
13
+ * **Scheduler**: a `MinHeap`-based task queue that schedules time-based tasks in the future.
14
+ * **repeat()**: a function that creates a repeatable task.
15
+ * **defer()**: a function that executes a task at a later time in the next tick.
16
+ * Browsers:
17
+ * Efficient multitasking:
18
+ * **IdleQueue**: a task queue that executes tasks in the next idle period.
19
+ * Based on [requestIdleCallback()](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback).
20
+ * **defer()**: a function that executes a task at a later time in the next idle period.
21
+ * **FrameQueue**: a task queue that executes tasks in the next frame.
22
+ * Based on [requestAnimationFrame()](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
23
+ * Page state management:
24
+ * **PageWatcher**: a task queue that executes tasks when the page state changes.
25
+ * **watchStates()**: a helper that pauses and resumes queues when the page state changes.
26
+ * **whenDomLoaded()**: a helper that executes tasks when the DOM is loaded.
27
+ * **whenLoaded()**: a helper that executes tasks when the page is loaded.
28
+
29
+ Internally it uses [List Toolkit](https://github.com/uhop/list-toolkit) and leverages the following browser APIs:
30
+
31
+ * [requestIdleCallback()](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)
32
+ * [requestAnimationFrame()](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)
33
+ * [queueMicrotask()](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask)
34
+ * [setTimeout()](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
35
+ * Various events and properties.
36
+
37
+ There are many articles on the subject that detail how to leverage the APIs writing efficient applications.
38
+ Some of them are:
39
+
40
+ * [Background Tasks API](https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API)
41
+ * [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API)
42
+ * [Page Lifecycle API](https://developer.chrome.com/docs/web-platform/page-lifecycle-api)
43
+
44
+ This package eliminates the need to write code that you'll write anyway following best practices.
45
+
46
+ ## Installation
47
+
48
+ ```sh
49
+ npm install time-queues
50
+ ```
51
+
52
+ If you want to check out the source code, you can use the following command:
53
+
54
+ ```sh
55
+ git clone https://github.com/uhop/time-queues.git
56
+ cd time-queues
57
+ npm install
58
+ ```
59
+
60
+ Don't forget to look at a test web application that uses the library. For that you should start a server:
61
+
62
+ ```sh
63
+ npm start
64
+ ```
65
+
66
+ And navigate to http://localhost:3000/tests/web/test.html — don't forget to open the console and
67
+ play around: switch tabs, make other window active, navigate away and come back, and so on.
68
+ See how queues work in [tests/web/test.js](https://github.com/uhop/time-queues/blob/main/tests/web/test.js).
69
+
70
+ ## Usage
71
+
72
+ The full documentation is available in the project's [wiki](https://github.com/uhop/time-queues/wiki). Below is a cheat sheet of the API.
73
+
74
+ ### ListQueue
75
+
76
+ `ListQueue` is a list-based task queue that executes tasks in the order they were added.
77
+ It serves as a base class for other task queues. The following methods are available:
78
+
79
+ | Method | Description |
80
+ |:---|:---|
81
+ | `ListQueue(paused)` | Create a new list queue (paused optionally). |
82
+ | `isEmpty` | Check if the list queue is empty. |
83
+ | `pause()` | Pause the list queue. |
84
+ | `resume()` | Resume the list queue. |
85
+ | `enqueue(fn)` | Schedule a function to be executed. Returns the task. |
86
+ | `dequeue(task)` | Remove a task from the list queue. |
87
+ | `clear()` | Remove all tasks from the list queue. |
88
+
89
+ Subclasses should implement `startQueue()`.
90
+
91
+ ### Scheduler
92
+
93
+ `Scheduler` is a `MinHeap`-based task queue that schedules time-based tasks in the future.
94
+ It can used to run periodic updates or one-time events.
95
+
96
+ `Scheduler` is not based on `ListQueue`, but implements its API.
97
+ The following additional methods are available:
98
+
99
+ | Method | Description |
100
+ |:---|:---|
101
+ | `Scheduler(paused)` | Create a new scheduler (paused optionally). |
102
+ | `nextTime` | Get the next scheduled time or 'Infinity` if the scheduler is empty. |
103
+ | `enqueue(fn, time)` | Schedule a function to be executed at a later time. Returns the task. `time` can be a date or a number in milliseconds from now. |
104
+
105
+ Scheduled functions are called once with the following arguments:
106
+
107
+ * `fn(task, scheduler)`, where:
108
+ * `fn` — the scheduled function.
109
+ * `task` — the task object that corresponds to the scheduled function.
110
+ * `scheduler` — the scheduler object.
111
+
112
+ The return value is ignored.
113
+
114
+ The module provides a singleton ready to be used:
115
+
116
+ ```js
117
+ import scheduler, {repeat} from 'time-queues/Scheduler.js';
118
+
119
+ // schedule a task
120
+ const task = scheduler.enqueue(() => console.log('The first task'), 1000);
121
+
122
+ // run a task every second
123
+ const hello = () => {
124
+ console.log('Hello, world!');
125
+ scheduler.enqueue(hello, 1000);
126
+ };
127
+ scheduler.enqueue(hello, 1000);
128
+
129
+ // pause the scheduler
130
+ scheduler.pause();
131
+
132
+ // remove the first task
133
+ scheduler.dequeue(task);
134
+
135
+ // remove all tasks
136
+ scheduler.clear();
137
+ ```
138
+
139
+ #### repeat()
140
+
141
+ The module provides a helper function `repeat()` that creates a repeatable task:
142
+
143
+ * `repeat(fn, interval)`, where:
144
+ * `fn` — the scheduled function.
145
+ * `interval` — the interval in milliseconds. If not specified the corresponding task delay is used.
146
+
147
+ We can rewrite the above example using `repeat()`:
148
+
149
+ ```js
150
+ // run a task every second
151
+ scheduler.enqueue(repeat(() => console.log('Hello, world!'), 1000));
152
+ ```
153
+
154
+ ### defer()
155
+
156
+ `defer(fn)` is a function that executes an argument function at a later time in the next tick.
157
+
158
+ Deferred functions are called with no arguments. The return value is ignored.
159
+
160
+ ```js
161
+ import defer from 'time-queues/defer.js';
162
+
163
+ // run a task in the next tick
164
+ defer(() => console.log('Goodbye, world!'));
165
+
166
+ // run code now
167
+ console.log('Hello, world!');
168
+ ```
169
+
170
+ ### IdleQueue
171
+
172
+ `IdleQueue` is a task queue that executes tasks in the next idle period. It implements `ListQueue` and is based on [requestIdleCallback()](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback).
173
+
174
+ Efficient web applications should use `IdleQueue` to schedule computations required to prepare data and
175
+ even create necessary DOM elements.
176
+ See [Background Tasks API](https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API) for more information.
177
+
178
+ Queued functions are called once with the following arguments:
179
+
180
+ * `fn(deadline, task, idleQueue)`, where:
181
+ * `fn` — the scheduled function.
182
+ * `deadline` — the deadline object. See [requestIdleCallback()](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) for more information.
183
+ * `task` — the task object that corresponds to the scheduled function.
184
+ * `idleQueue` — the idle queue object.
185
+
186
+ The return value is ignored.
187
+
188
+ The module provides a singleton ready to be used:
189
+
190
+ ```js
191
+ import idleQueue from 'time-queues/IdleQueue.js';
192
+ import frameQueue from 'time-queues/FrameQueue.js';
193
+
194
+ idleQueue.enqueue(() => {
195
+ // prepare our data and generate DOM
196
+ const div = document.createElement('div');
197
+ div.appendChild(document.createTextNode('Hello, world!'));
198
+ // now update the DOM in the next frame
199
+ frameQueue.enqueue(() => document.body.appendChild(div));
200
+ });
201
+ ```
202
+
203
+ ### FrameQueue
204
+
205
+ `FrameQueue` is a task queue that executes tasks in the next frame. It implements `ListQueue` and is based on [requestAnimationFrame()](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
206
+
207
+ Efficient web applications should use `FrameQueue` to schedule DOM updates.
208
+ See [Background Tasks API](https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API) for more information.
209
+
210
+ Queued functions are called once with the following arguments:
211
+
212
+ * `fn(timeStamp, task, frameQueue)`, where:
213
+ * `fn` — the scheduled function.
214
+ * `timeStamp` — the timestamp object. See [requestAnimationFrame()](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) for more information.
215
+ * `task` — the task object that corresponds to the scheduled function.
216
+ * `frameQueue` — the frame queue object.
217
+
218
+ The return value is ignored.
219
+
220
+ The module provides a singleton ready to be used. See the code snippet `IdleQueue` above for more information.
221
+
222
+ ### PageWatcher
223
+
224
+ `PageWatcher` is a task queue that executes tasks when the page state changes. It is based on [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API).
225
+ You can find more information in [Page Lifecycle API](https://developer.chrome.com/docs/web-platform/page-lifecycle-api).
226
+
227
+ Efficient web applications should use `PageWatcher` to watch for page visibility changes and react accordingly, for example, by suspending updates in the hidden state.
228
+
229
+ `PageWatcher` implements `ListQueue`. The following additional/changed methods are available:
230
+
231
+ | Method | Description |
232
+ |:---|:---|
233
+ | `PageWatcher(started)` | Create a new page watcher (started optionally). |
234
+ | `currentState` | Get the current page state (see below). |
235
+ | `enqueue(fn, initialize)` | Schedule a function to be executed. Returns the task. If `initialize` is truthy, the function will be queued and called immediately with the current state. |
236
+
237
+ A page state can be one of the following strings:
238
+
239
+ * `active` — the page is a current window, it is visible and the user can interact with it.
240
+ * `passive` — the page is not a current window, it is visible, but the user cannot interact with it.
241
+ * `hidden` — the page is not visible.
242
+ * `frozen` — the page is suspended, no timers nor fetch callbacks can be executed.
243
+ * `terminated` — the page is terminated, no new tasks can be started.
244
+
245
+ Queued functions are called on every state change with the following arguments:
246
+
247
+ * `fn(state, previousState, task, pageWatcher)`, where:
248
+ * 'fn` — the scheduled function.
249
+ * `state` — the new page state.
250
+ * `previousState` — the previous page state.
251
+ * `task` — the task object that corresponds to the scheduled function.
252
+ * `pageWatcher` — the page watcher object.
253
+
254
+ The return value is ignored.
255
+
256
+ The module provides a singleton ready to be used.
257
+
258
+ ```js
259
+ import pageWatcher from 'time-queues/PageWatcher.js';
260
+
261
+ pageWatcher.enqueue(state => console.log('state:', state), true);
262
+ ```
263
+
264
+ #### watchStates()
265
+
266
+ `watchStates()` is a helper that pauses and resumes queues when the page state changes.
267
+ It can be added to a `PageWatcher` object controlling a `Scheduler` object or any other queue
268
+ to pause it depending on a page state.
269
+
270
+ The function signature is:
271
+
272
+ * `watchStates(queue, resumeStatesList = ['active'])`, where:
273
+ * `queue` — the queue object to be controlled.
274
+ * `resumeStatesList` — the iterable of page states to `resume()`. All other states will pause the queue. Defaults to 'active'.
275
+
276
+ The return value is a function that is suitable for `PageWatcher.enqueue()`.
277
+
278
+ ```js
279
+ import pageWatcher, {watchStates} from 'time-queues/PageWatcher.js';
280
+ import scheduler from 'time-queues/Scheduler.js';
281
+
282
+ // do not process time-based tasks when the page is not visible
283
+ pageWatcher.enqueue(watchStates(scheduler, ['active', 'passive']), true);
284
+ ```
285
+
286
+ ### whenDomLoaded()
287
+
288
+ `whenDomLoaded()` is a helper that executes a function when the DOM is loaded.
289
+ If the DOM is already loaded, the function will be executed with `queueMicrotask()`.
290
+ Otherwise it'll be queued and executed when the DOM is loaded.
291
+ See [DOMContentLoaded](https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event) for more information.
292
+
293
+ The function signature is:
294
+
295
+ * `whenDomLoaded(fn)`, where:
296
+ * `fn` — the function to be executed when the DOM is loaded.
297
+
298
+ It will be called with no arguments. The return value is ignored.
299
+
300
+ ```js
301
+ import whenDomLoaded from 'time-queues/whenDomLoaded.js';
302
+
303
+ whenDomLoaded(() => console.log('The DOM is loaded'));
304
+ ```
305
+
306
+ ### whenLoaded()
307
+
308
+ `whenLoaded()` is a helper that executes a function when the page is fully loaded.
309
+ If the page is already loaded, the function will be executed with `queueMicrotask()`.
310
+ Otherwise it'll be queued and executed when the page is loaded.
311
+ See [load](https://developer.mozilla.org/en-US/docs/Web/Events/load) for more information.
312
+
313
+ The function signature is:
314
+
315
+ * `whenLoaded(fn)`, where:
316
+ * `fn` — the function to be executed when the page is loaded.
317
+
318
+ It will be called with no arguments. The return value is ignored.
319
+
320
+ ```js
321
+ import whenLoaded from 'time-queues/whenLoaded.js';
322
+
323
+ whenLoaded(() => console.log('The page is loaded'));
324
+ ```
325
+
326
+ ## License
327
+
328
+ This project is licensed under the BSD-3-Clause License.
329
+
330
+ ## Release History
331
+
332
+ * 1.0.0 *Initial release.*
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "time-queues",
3
+ "version": "1.0.0",
4
+ "description": "Time queues to organize multitasking and scheduled tasks.",
5
+ "type": "module",
6
+ "exports": {
7
+ "./*": "./src/*"
8
+ },
9
+ "scripts": {
10
+ "test": "tape6 --flags FO",
11
+ "start": "tape6-server --trace"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+ssh://git@github.com/uhop/time-queues.git"
16
+ },
17
+ "keywords": [
18
+ "timer",
19
+ "time",
20
+ "queue"
21
+ ],
22
+ "author": "Eugene Lazutkin <eugene.lazutkin@gmail.com> (https://www.lazutkin.com/)",
23
+ "license": "BSD-3-Clause",
24
+ "bugs": {
25
+ "url": "https://github.com/uhop/time-queues/issues"
26
+ },
27
+ "homepage": "https://github.com/uhop/time-queues#readme",
28
+ "files": [
29
+ "/src"
30
+ ],
31
+ "tape6": {
32
+ "tests": [
33
+ "/tests/test-*.*js"
34
+ ]
35
+ },
36
+ "devDependencies": {
37
+ "tape-six": "^0.9.5"
38
+ },
39
+ "dependencies": {
40
+ "list-toolkit": "^1.0.2"
41
+ }
42
+ }
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ import List from 'list-toolkit/List.js';
4
+ import ListQueue from './ListQueue.js';
5
+
6
+ export class FrameQueue extends ListQueue {
7
+ constructor(paused, batchInMs) {
8
+ super(paused);
9
+ this.batch = batchInMs;
10
+ }
11
+
12
+ startQueue() {
13
+ const handle = requestAnimationFrame(this.processTasks.bind(this));
14
+ return () => void cancelAnimationFrame(handle);
15
+ }
16
+
17
+ processTasks(timeStamp) {
18
+ if (this.stopQueue) {
19
+ this.stopQueue();
20
+ this.stopQueue = null;
21
+ }
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, 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, this);
35
+ }
36
+ }
37
+
38
+ if (!this.list.isEmpty) this.stopQueue = this.startQueue();
39
+ }
40
+ }
41
+
42
+ export const frameQueue = new FrameQueue();
43
+
44
+ export default frameQueue;
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ import List from 'list-toolkit/List.js';
4
+ import ListQueue from './ListQueue.js';
5
+
6
+ // Based on information from https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API
7
+
8
+ export class IdleQueue extends ListQueue {
9
+ constructor(paused, timeoutBatchInMs, options) {
10
+ super(paused);
11
+ this.timeoutBatch = timeoutBatchInMs;
12
+ this.options = options;
13
+ }
14
+
15
+ startQueue() {
16
+ const handle = requestIdleCallback(this.processTasks.bind(this), this.options);
17
+ return () => void cancelIdleCallback(handle);
18
+ }
19
+
20
+ processTasks(deadline) {
21
+ if (this.stopQueue) {
22
+ this.stopQueue();
23
+ this.stopQueue = null;
24
+ }
25
+
26
+ if (deadline.didTimeout) {
27
+ if (!isNaN(this.timeoutBatch)) {
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, 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, this);
39
+ }
40
+ }
41
+ } else {
42
+ while (deadline.timeRemaining() > 0 && !this.list.isEmpty) {
43
+ const task = this.list.popFront();
44
+ task.fn(deadline, task, this);
45
+ }
46
+ }
47
+
48
+ if (!this.list.isEmpty) this.stopQueue = this.startQueue();
49
+ }
50
+ }
51
+
52
+ export const idleQueue = new IdleQueue();
53
+
54
+ export const defer = idleQueue.enqueue.bind(idleQueue);
55
+
56
+ export default idleQueue;
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ import List from 'list-toolkit/List.js';
4
+ import MicroTaskQueue from './MicroTaskQueue.js';
5
+
6
+ export class ListQueue extends MicroTaskQueue {
7
+ constructor(paused) {
8
+ super(paused);
9
+ this.list = new List();
10
+ this.stopQueue = null;
11
+ }
12
+
13
+ get isEmpty() {
14
+ return this.list.isEmpty;
15
+ }
16
+
17
+ pause() {
18
+ if (!this.paused) {
19
+ super.pause();
20
+ if (this.stopQueue) this.stopQueue = (this.stopQueue(), null);
21
+ }
22
+ return this;
23
+ }
24
+
25
+ resume() {
26
+ if (this.paused) {
27
+ super.resume();
28
+ if (!this.list.isEmpty) {
29
+ this.stopQueue = this.startQueue();
30
+ }
31
+ }
32
+ return this;
33
+ }
34
+
35
+ enqueue(fn) {
36
+ const task = super.enqueue(fn);
37
+ this.list.pushBack(task);
38
+ if (!this.paused && !this.stopQueue) this.stopQueue = this.startQueue();
39
+ return task;
40
+ }
41
+
42
+ dequeue(task) {
43
+ for (const node of this.list.getNodeIterator()) {
44
+ if (node.value === task) {
45
+ List.pop(node);
46
+ break;
47
+ }
48
+ }
49
+ if (!this.paused && this.list.isEmpty && this.stopQueue)
50
+ this.stopQueue = (this.stopQueue(), null);
51
+ return this;
52
+ }
53
+
54
+ clear() {
55
+ const paused = this.paused;
56
+ if (!paused) this.pause();
57
+ this.list.clear();
58
+ if (!paused) this.resume();
59
+ return this;
60
+ }
61
+
62
+ // API to be overridden in subclasses
63
+ startQueue() {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ export default ListQueue;
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ export class MicroTask {
4
+ constructor(fn) {
5
+ this.fn = fn;
6
+ }
7
+ }
8
+
9
+ export default MicroTask;
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ import MicroTask from './MicroTask.js';
4
+
5
+ export class MicroTaskQueue {
6
+ constructor(paused) {
7
+ this.paused = Boolean(paused);
8
+ }
9
+ // API to be overridden in subclasses
10
+ get isEmpty() {
11
+ return true;
12
+ }
13
+ pause() {
14
+ this.paused = true;
15
+ return this;
16
+ }
17
+ resume() {
18
+ this.paused = false;
19
+ return this;
20
+ }
21
+ enqueue(fn) {
22
+ const task = new MicroTask(fn);
23
+ return task;
24
+ }
25
+ dequeue(task) {
26
+ return this;
27
+ }
28
+ clear() {
29
+ return this;
30
+ }
31
+ }
32
+
33
+ export default MicroTaskQueue;
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ import ListQueue from './ListQueue.js';
4
+
5
+ // Based on information from https://developer.chrome.com/docs/web-platform/page-lifecycle-api
6
+
7
+ const eventHandlerOptions = {capture: true},
8
+ watchedEvents = ['pageshow', 'pagehide', 'focus', 'blur', 'visibilitychange', 'resume', 'freeze'];
9
+
10
+ // valid states: active, passive, hidden, frozen, terminated
11
+
12
+ const getState = () => {
13
+ if (document.visibilityState === 'hidden') {
14
+ return 'hidden';
15
+ }
16
+ if (document.hasFocus()) {
17
+ return 'active';
18
+ }
19
+ return 'passive';
20
+ };
21
+
22
+ export class PageWatcher extends ListQueue {
23
+ constructor(started) {
24
+ super(!started);
25
+ this.currentState = getState();
26
+ if (started) this.resume();
27
+ }
28
+
29
+ pause() {
30
+ this.paused = true;
31
+ watchedEvents.forEach(type => removeEventListener(type, this, eventHandlerOptions));
32
+ return super.pause();
33
+ }
34
+
35
+ resume() {
36
+ watchedEvents.forEach(type => addEventListener(type, this, eventHandlerOptions));
37
+ return super.resume();
38
+ }
39
+
40
+ enqueue(fn, initialize) {
41
+ const task = super.enqueue(fn);
42
+ if (initialize) queueMicrotask(() => fn(this.currentState, this.currentState, task, this));
43
+ return task;
44
+ }
45
+
46
+ // Implemented in ListQueue: dequeue()
47
+
48
+ clear() {
49
+ this.list.clear();
50
+ return this;
51
+ }
52
+
53
+ startQueue() {
54
+ return null;
55
+ }
56
+
57
+ handleEvent(event) {
58
+ let state = this.currentState;
59
+
60
+ switch (event.type) {
61
+ case 'freeze':
62
+ state = 'frozen';
63
+ break;
64
+ case 'pagehide':
65
+ state = event.persisted ? 'frozen' : 'terminated';
66
+ break;
67
+ default:
68
+ state = getState();
69
+ break;
70
+ }
71
+
72
+ if (this.currentState === state) return;
73
+
74
+ for (const task of this.list) {
75
+ task.fn(state, this.currentState, task, this);
76
+ }
77
+
78
+ this.currentState = state;
79
+ }
80
+ }
81
+
82
+ export const watchStates = (queue, resumeStatesList = ['active']) => {
83
+ const resumeStates = new Set(resumeStatesList);
84
+
85
+ // queues can be paused and resumed at any time
86
+ // so we don't need to check if queue is paused
87
+ // calling pause/resume multiple times is fine
88
+ return state => {
89
+ if (resumeStates.has(state)) {
90
+ queue.resume();
91
+ } else {
92
+ queue.pause();
93
+ }
94
+ };
95
+ };
96
+
97
+ export const pageWatcher = new PageWatcher();
98
+
99
+ export default pageWatcher;
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ import MinHeap from 'list-toolkit/MinHeap.js';
4
+ import MicroTask from './MicroTask.js';
5
+ import MicroTaskQueue from './MicroTaskQueue.js';
6
+
7
+ export class Task extends MicroTask {
8
+ constructor(delay, fn) {
9
+ super(fn);
10
+ if (delay instanceof Date) {
11
+ this.time = delay.getTime();
12
+ this.delay = this.time - Date.now();
13
+ } else {
14
+ this.time = Date.now() + delay;
15
+ this.delay = delay;
16
+ }
17
+ }
18
+ }
19
+
20
+ export class Scheduler extends MicroTaskQueue {
21
+ constructor(paused, tolerance = 4) {
22
+ super(paused);
23
+ this.queue = new MinHeap({less: (a, b) => a.time < b.time});
24
+ this.tolerance = tolerance;
25
+ this.stopQueue = null;
26
+ }
27
+
28
+ get isEmpty() {
29
+ return this.queue.isEmpty;
30
+ }
31
+
32
+ get nextTime() {
33
+ return this.queue.isEmpty ? Infinity : this.queue.top.time;
34
+ }
35
+
36
+ pause() {
37
+ if (!this.paused) {
38
+ this.paused = true;
39
+ if (this.stopQueue) this.stopQueue = (this.stopQueue(), null);
40
+ }
41
+ return this;
42
+ }
43
+
44
+ resume() {
45
+ if (this.paused) {
46
+ this.paused = false;
47
+ this.processTasks();
48
+ }
49
+ return this;
50
+ }
51
+
52
+ enqueue(fn, delay) {
53
+ const task = new Task(delay, fn);
54
+
55
+ if (this.paused) {
56
+ this.queue.push(task);
57
+ return task;
58
+ }
59
+
60
+ const nextTime = this.nextTime;
61
+ this.queue.push(task);
62
+ if (nextTime > this.nextTime) {
63
+ if (this.stopQueue) this.stopQueue();
64
+ this.stopQueue = this.startQueue();
65
+ }
66
+
67
+ return task;
68
+ }
69
+
70
+ dequeue(task) {
71
+ if (this.queue.isEmpty) return this;
72
+ if (this.paused || this.queue.top !== task) {
73
+ this.queue.remove(task);
74
+ return this;
75
+ }
76
+
77
+ // we are not paused and the task is the top => remove top and restart a timer if needed
78
+ this.pause();
79
+ this.queue.pop();
80
+ this.resume();
81
+ return this;
82
+ }
83
+
84
+ clear() {
85
+ const paused = this.paused;
86
+ if (!paused) this.pause();
87
+ this.queue.clear();
88
+ if (!paused) this.resume();
89
+ }
90
+
91
+ startQueue() {
92
+ const handle = setTimeout(
93
+ this.processTasks.bind(this),
94
+ Math.max(this.nextTime - Date.now(), 0)
95
+ );
96
+ return () => void clearTimeout(handle);
97
+ }
98
+
99
+ processTasks() {
100
+ if (this.stopQueue) this.stopQueue = (this.stopQueue(), null);
101
+
102
+ while (
103
+ !this.queue.isEmpty &&
104
+ !this.paused &&
105
+ this.queue.top.time <= Date.now() + this.tolerance
106
+ ) {
107
+ const task = this.queue.pop();
108
+ task.fn(task, this);
109
+ }
110
+
111
+ if (!this.paused && !this.queue.isEmpty) this.stopQueue = this.startQueue();
112
+ }
113
+ }
114
+
115
+ export const repeat = (fn, delay) => {
116
+ const repeatableTask = (task, scheduler) => {
117
+ fn(task, scheduler);
118
+ scheduler.enqueue(repeatableTask, isNaN(delay) ? task.delay : delay);
119
+ };
120
+ return repeatableTask;
121
+ };
122
+
123
+ export const scheduler = new Scheduler();
124
+
125
+ export default scheduler;
package/src/defer.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ let deferImplementation = setTimeout;
4
+
5
+ if (typeof requestIdleCallback == 'function') {
6
+ deferImplementation = requestIdleCallback;
7
+ } else if (typeof setImmediate == 'function') {
8
+ deferImplementation = setImmediate;
9
+ }
10
+
11
+ export const defer = fn => void deferImplementation(() => fn());
12
+
13
+ export default defer;
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ import List from 'list-toolkit/List.js';
4
+
5
+ const waitingForDom = new List();
6
+
7
+ export const remove = fn => {
8
+ for (const node of waitingForDom.getNodeIterable()) {
9
+ if (node.value === fn) {
10
+ List.pop(node);
11
+ return true;
12
+ }
13
+ }
14
+ return false;
15
+ }
16
+
17
+ const handleDomLoaded = () => {
18
+ while (!waitingForDom.isEmpty) waitingForDom.pop()();
19
+ };
20
+
21
+ export const whenDomLoaded = fn => {
22
+ const wasEmpty = waitingForDom.isEmpty;
23
+ waitingForDom.push(fn);
24
+
25
+ switch (document.readyState) {
26
+ case 'complete':
27
+ case 'interactive':
28
+ queueMicrotask(handleDomLoaded);
29
+ return;
30
+ }
31
+
32
+ if (wasEmpty) document.addEventListener('DOMContentLoaded', handleDomLoaded);
33
+ };
34
+
35
+ export default whenDomLoaded;
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ import List from 'list-toolkit/List.js';
4
+
5
+ const waitingForLoad = new List();
6
+
7
+ export const remove = fn => {
8
+ for (const node of waitingForLoad.getNodeIterable()) {
9
+ if (node.value === fn) {
10
+ List.pop(node);
11
+ return true;
12
+ }
13
+ }
14
+ return false;
15
+ }
16
+
17
+ const handleLoaded = () => {
18
+ while (!waitingForLoad.isEmpty) waitingForLoad.pop()();
19
+ };
20
+
21
+ export const whenLoaded = fn => {
22
+ const wasEmpty = waitingForLoad.isEmpty;
23
+ waitingForLoad.push(fn);
24
+
25
+ if (document.readyState === 'complete') {
26
+ queueMicrotask(handleLoaded);
27
+ return;
28
+ }
29
+
30
+ if (wasEmpty) window.addEventListener('load', handleLoaded);
31
+ };
32
+
33
+ export default whenLoaded;