@oazmi/superbuild 0.1.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/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +57 -0
- package/esm/deps/jsr.io/@oazmi/esbuild-types/0.28.0/src/mod.d.ts +598 -0
- package/esm/deps/jsr.io/@oazmi/esbuild-types/0.28.0/src/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/esbuild-types/0.28.0/src/mod.js +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/alias.d.ts +617 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/alias.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/alias.js +671 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array1d.d.ts +487 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array1d.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array1d.js +536 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array2d.d.ts +266 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array2d.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array2d.js +318 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/binder.d.ts +368 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/binder.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/binder.js +380 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/crossenv.d.ts +711 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/crossenv.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/crossenv.js +1173 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/cryptoman.d.ts +171 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/cryptoman.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/cryptoman.js +308 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/deps.d.ts +10 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/deps.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/deps.js +10 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack.d.ts +168 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack.js +236 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack_varint.d.ts +90 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack_varint.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack_varint.js +237 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericarray.d.ts +213 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericarray.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericarray.js +363 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericmethods.d.ts +233 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericmethods.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericmethods.js +256 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/pathman.d.ts +1436 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/pathman.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/pathman.js +1730 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/promiseman.d.ts +647 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/promiseman.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/promiseman.js +762 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/stringman.d.ts +538 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/stringman.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/stringman.js +626 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/struct.d.ts +734 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/struct.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/struct.js +764 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedbuffer.d.ts +137 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedbuffer.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedbuffer.js +234 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedefs.d.ts +391 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedefs.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedefs.js +8 -0
- package/esm/deps.d.ts +56 -0
- package/esm/deps.d.ts.map +1 -0
- package/esm/deps.js +37 -0
- package/esm/esbuild/metafile.d.ts +90 -0
- package/esm/esbuild/metafile.d.ts.map +1 -0
- package/esm/esbuild/metafile.js +275 -0
- package/esm/esbuild/native.d.ts +95 -0
- package/esm/esbuild/native.d.ts.map +1 -0
- package/esm/esbuild/native.js +127 -0
- package/esm/esbuild/outputfile.d.ts +86 -0
- package/esm/esbuild/outputfile.d.ts.map +1 -0
- package/esm/esbuild/outputfile.js +221 -0
- package/esm/esbuild/strongtypes.d.ts +90 -0
- package/esm/esbuild/strongtypes.d.ts.map +1 -0
- package/esm/esbuild/strongtypes.js +8 -0
- package/esm/esbuild/typedefs.d.ts +121 -0
- package/esm/esbuild/typedefs.d.ts.map +1 -0
- package/esm/esbuild/typedefs.js +55 -0
- package/esm/esbuild/weaktypes.d.ts +46 -0
- package/esm/esbuild/weaktypes.d.ts.map +1 -0
- package/esm/esbuild/weaktypes.js +8 -0
- package/esm/funcdefs.d.ts +89 -0
- package/esm/funcdefs.d.ts.map +1 -0
- package/esm/funcdefs.js +145 -0
- package/esm/mod.d.ts +4 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +2 -0
- package/esm/package.json +3 -0
- package/esm/plugins/emissions_driver.d.ts +33 -0
- package/esm/plugins/emissions_driver.d.ts.map +1 -0
- package/esm/plugins/emissions_driver.js +260 -0
- package/esm/plugins/long_build.d.ts +138 -0
- package/esm/plugins/long_build.d.ts.map +1 -0
- package/esm/plugins/long_build.js +288 -0
- package/esm/plugins/native_replica.d.ts +48 -0
- package/esm/plugins/native_replica.d.ts.map +1 -0
- package/esm/plugins/native_replica.js +122 -0
- package/esm/super/build.d.ts +53 -0
- package/esm/super/build.d.ts.map +1 -0
- package/esm/super/build.js +39 -0
- package/esm/super/build_context.d.ts +83 -0
- package/esm/super/build_context.d.ts.map +1 -0
- package/esm/super/build_context.js +124 -0
- package/esm/super/mod.d.ts +12 -0
- package/esm/super/mod.d.ts.map +1 -0
- package/esm/super/mod.js +9 -0
- package/esm/super/plugin.d.ts +23 -0
- package/esm/super/plugin.d.ts.map +1 -0
- package/esm/super/plugin.js +31 -0
- package/esm/super/plugin_build.d.ts +38 -0
- package/esm/super/plugin_build.d.ts.map +1 -0
- package/esm/super/plugin_build.js +198 -0
- package/esm/super/typedefs.d.ts +323 -0
- package/esm/super/typedefs.d.ts.map +1 -0
- package/esm/super/typedefs.js +10 -0
- package/esm/typedefs.d.ts +21 -0
- package/esm/typedefs.d.ts.map +1 -0
- package/esm/typedefs.js +5 -0
- package/license.md +37 -0
- package/package.json +88 -0
- package/readme.md +181 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
/** this submodule contains _promise_, _task_, and _scheduling_ related utility functions.
|
|
2
|
+
*
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import { date_now, dom_clearTimeout, dom_setTimeout, promise_forever, promise_outside, promise_resolve } from "./alias.js";
|
|
6
|
+
import { DEBUG } from "./deps.js";
|
|
7
|
+
import { min } from "./numericmethods.js";
|
|
8
|
+
import { isFunction } from "./struct.js";
|
|
9
|
+
/** this is a re-export of {@link promise_outside}.
|
|
10
|
+
*
|
|
11
|
+
* {@inheritDoc promise_outside}
|
|
12
|
+
*/
|
|
13
|
+
export const promiseOutside = promise_outside;
|
|
14
|
+
// TODO: consider creating disposable timeout/schedule functions that return a dispose function, which when called, will clear the timeout.
|
|
15
|
+
// I can't quite come up with a good design that functions both, as a promise,
|
|
16
|
+
// and as a dispose function, without an array or a record as the return value.
|
|
17
|
+
// this is because I want to be be able to chain the returned promise.
|
|
18
|
+
// one option is to accept an object parameter that will be side-loaded with a "dispose" function for the user to use.
|
|
19
|
+
/** a promise that resolves (or rejects if `should_reject = true`) after a certain number of milliseconds.
|
|
20
|
+
*
|
|
21
|
+
* this is a useful shorthand for creating delays, and then following them up with a `.then` call.
|
|
22
|
+
* you may also use this as a sleep/wait function in an async context where `await` is available.
|
|
23
|
+
*/
|
|
24
|
+
export const promiseTimeout = (wait_time_ms, should_reject) => {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
dom_setTimeout(should_reject ? reject : resolve, wait_time_ms, TIMEOUT);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
/** schedule a promise that resolves when a specific local epoch-time (based on `Date.now()`) is reached.
|
|
30
|
+
*
|
|
31
|
+
* see what configuration options are available to you in {@link ScheduleTimeoutConfig}.
|
|
32
|
+
*
|
|
33
|
+
* // TODO: add tests maybe?
|
|
34
|
+
*/
|
|
35
|
+
export const scheduleTimeout = (epoch_time_ms, config = {}) => {
|
|
36
|
+
const delay = epoch_time_ms - date_now();
|
|
37
|
+
let { reject = false, runExpired = true } = config;
|
|
38
|
+
if (delay < 0) {
|
|
39
|
+
if (isFunction(runExpired)) {
|
|
40
|
+
runExpired = runExpired();
|
|
41
|
+
}
|
|
42
|
+
if (runExpired === false) {
|
|
43
|
+
return promise_forever();
|
|
44
|
+
}
|
|
45
|
+
reject = runExpired === "resolve" ? false
|
|
46
|
+
: runExpired === "reject" ? true
|
|
47
|
+
: reject;
|
|
48
|
+
}
|
|
49
|
+
return promiseTimeout(delay, reject);
|
|
50
|
+
};
|
|
51
|
+
/** a special symbol that signifies when a throttle function
|
|
52
|
+
* (such as {@link throttle} and {@link throttleAndTrail})
|
|
53
|
+
* has rejected to run a function, due to calling rate throttling.
|
|
54
|
+
*/
|
|
55
|
+
export const THROTTLE_REJECT = /*@__PURE__*/ Symbol(DEBUG.MINIFY || "a rejection by a throttled function");
|
|
56
|
+
/** a special symbol that signifies when a timeout function (such as {@link promiseTimeout}) has timed out. */
|
|
57
|
+
export const TIMEOUT = /*@__PURE__*/ Symbol(DEBUG.MINIFY || "a timeout by an awaited promiseTimeout function");
|
|
58
|
+
/** creates a debounced version of the provided function that returns a new promise.
|
|
59
|
+
*
|
|
60
|
+
* the debounced function delays the execution of the provided function `fn` until the debouncing interval `wait_time_ms` amount of time has passed without any subsequent calls.
|
|
61
|
+
*
|
|
62
|
+
* if a `rejection_value` is provided, then any subsequent calls to the debounced function that are made within the debouncing interval, will reject the previous promises.
|
|
63
|
+
* thus you will have to `catch` them in that case (otherwise it will result in an error).
|
|
64
|
+
*
|
|
65
|
+
* you may worry that too many calls to a non-rejectable debounced function (i.e. when `rejection_value === undefined`)
|
|
66
|
+
* will create too many promise objects, possibly resulting in memory leaks.
|
|
67
|
+
* however, luckily, modern javascript engines are not afflicted by too many pending promise objects.
|
|
68
|
+
* in fact, choosing to reject promises (i.e. by setting `rejection_value`), might be more expensive down the line, as error catching is typically expensive.
|
|
69
|
+
*
|
|
70
|
+
* also check out {@link debounceAndShare}, which avoids this "lots of promise objects" issue by sharing the same promise across all quick callers of the debounce.
|
|
71
|
+
* but it will require careful usage, as all promised callers will eventually get resolved, which may create an unintended avalaunch of subsequent `then` calls if not used carefully.
|
|
72
|
+
*
|
|
73
|
+
* @param wait_time_ms the time interval in milliseconds for debouncing
|
|
74
|
+
* @param fn the function to be debounced
|
|
75
|
+
* @param rejection_value if a rejection value is provided, then old unresolved pending promises will be rejected with the given value,
|
|
76
|
+
* when a new call to the debounced function is made (within the debouncing waiting period)
|
|
77
|
+
* @returns a function (that takes arguments intended for `fn`) that returns a promise, which is resolved once `wait_time_ms` amount of time has passed with no further calls
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* import {
|
|
82
|
+
* assertEquals as assertEq,
|
|
83
|
+
* assertLessOrEqual as assertLe,
|
|
84
|
+
* assertGreaterOrEqual as assertGe,
|
|
85
|
+
* } from "jsr:@std/assert"
|
|
86
|
+
*
|
|
87
|
+
* // a function that creates an asynchronous delay of the given number of milliseconds
|
|
88
|
+
* const sleep = (time_ms: number) => (new Promise((resolve) => (setTimeout(resolve, time_ms))))
|
|
89
|
+
*
|
|
90
|
+
* const
|
|
91
|
+
* log_history: Array<[time: number, value: number]> = [],
|
|
92
|
+
* error_history: Array<[time: number, message: string]> = [],
|
|
93
|
+
* t0 = performance.now(),
|
|
94
|
+
* current_time = () => (performance.now() - t0)
|
|
95
|
+
*
|
|
96
|
+
* // the function that we plan to debounce
|
|
97
|
+
* const fn = (v: number) => {
|
|
98
|
+
* log_history.push([current_time(), v])
|
|
99
|
+
* return v + 100
|
|
100
|
+
* }
|
|
101
|
+
*
|
|
102
|
+
* // the debounced version of `fn`, that, when called too quickly, rejects with the reason `"SEPPUKU!"`.
|
|
103
|
+
* const debounced_fn = debounce(1000, fn, "SEPPUKU!")
|
|
104
|
+
*
|
|
105
|
+
* // `a` is a promise that should resolve after 1000ms. but if it fails (which it will), it will log to `error_history`.
|
|
106
|
+
* const a = debounced_fn(24).catch(
|
|
107
|
+
* (reason) => { error_history.push([current_time(), `they want me to ${reason}`]) }
|
|
108
|
+
* )
|
|
109
|
+
*
|
|
110
|
+
* await sleep(500) // promise `a` is still pending after the 500ms sleep
|
|
111
|
+
*
|
|
112
|
+
* // the new `debounced_fn(42)` below rejects `a`'s promise, and then returns a new promise (`b`) that will resolve in 1000ms.
|
|
113
|
+
* // since `a` gets rejected as a consequence, it's error handling chain will immediately log `"they want me to SEPPUKU!"` in the console.
|
|
114
|
+
* const b = debounced_fn(42)
|
|
115
|
+
*
|
|
116
|
+
* // we delay for a tiny while in order for the js-runtime to get `a` rejected. otherwise, without the sleep, `error_history` will be empty.
|
|
117
|
+
* await sleep(50)
|
|
118
|
+
* assertEq(log_history, [])
|
|
119
|
+
* assertGe(error_history[0][0], 499)
|
|
120
|
+
* assertLe(error_history[0][0], 540)
|
|
121
|
+
* assertEq(error_history[0][1], "they want me to SEPPUKU!")
|
|
122
|
+
*
|
|
123
|
+
* // after 1000ms, the value `42` will be logged into `log_history` (due to promise `b`).
|
|
124
|
+
* // however, no entry will be logged by `a`, since it gets rejected as soon as `b` is created.
|
|
125
|
+
* await sleep(1000)
|
|
126
|
+
* assertGe(log_history[0][0], 500 + 999)
|
|
127
|
+
* assertLe(log_history[0][0], 500 + 1040)
|
|
128
|
+
* assertEq(log_history[0][1], 42)
|
|
129
|
+
* assertEq(await b, 100 + 42)
|
|
130
|
+
*
|
|
131
|
+
* // we can now safely create a new debounced call without rejecting the already resolved `b` promise, since it is over 1000ms since `b` was created.
|
|
132
|
+
* const c = debounced_fn(99)
|
|
133
|
+
*
|
|
134
|
+
* // 1000ms later, the value `99` will be logged into `log_history` (due to promise `c`).
|
|
135
|
+
* await sleep(1050)
|
|
136
|
+
* assertGe(log_history[1][0], 1550 + 999)
|
|
137
|
+
* assertLe(log_history[1][0], 1550 + 1100)
|
|
138
|
+
* assertEq(log_history[1][1], 99)
|
|
139
|
+
* assertEq(await c, 100 + 99)
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export const debounce = (wait_time_ms, fn, rejection_value) => {
|
|
143
|
+
let prev_timer, prev_reject = () => { };
|
|
144
|
+
return (...args) => {
|
|
145
|
+
dom_clearTimeout(prev_timer);
|
|
146
|
+
if (rejection_value !== undefined) {
|
|
147
|
+
prev_reject(rejection_value);
|
|
148
|
+
}
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
prev_reject = reject;
|
|
151
|
+
prev_timer = dom_setTimeout(() => resolve(fn(...args)), wait_time_ms);
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/** creates a debounced version of the provided function that returns a shared promise.
|
|
156
|
+
*
|
|
157
|
+
* unlike conventional {@link debounce}, this function reuses and returns the same promise object for all calls that are made within the debouncing interval.
|
|
158
|
+
* this means that all callers within this interval will receive the same promise, which will be resolved once `wait_time_ms` amount of time has passed with no further calls.
|
|
159
|
+
*
|
|
160
|
+
* if subsequent calls are made within the debouncing interval, the debounced function will return the same promise as before, further delaying its resolution.
|
|
161
|
+
* however, once the debouncing interval has elapsed and the promise is resolved, any new calls to the debounced function will create and return a new promise.
|
|
162
|
+
*
|
|
163
|
+
* @param wait_time_ms the time interval in milliseconds for debouncing
|
|
164
|
+
* @param fn the function to be debounced
|
|
165
|
+
* @returns a function (that takes arguments intended for `fn`) that returns a promise, which is resolved once `wait_time_ms` amount of time has passed with no further calls
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* import {
|
|
170
|
+
* assertEquals as assertEq,
|
|
171
|
+
* assertLessOrEqual as assertLe,
|
|
172
|
+
* assertGreaterOrEqual as assertGe,
|
|
173
|
+
* } from "jsr:@std/assert"
|
|
174
|
+
*
|
|
175
|
+
* // a function that creates an asynchronous delay of the given number of milliseconds
|
|
176
|
+
* const sleep = (time_ms: number) => (new Promise((resolve) => (setTimeout(resolve, time_ms))))
|
|
177
|
+
*
|
|
178
|
+
* const
|
|
179
|
+
* log_history: Array<[time: number, value: number]> = [],
|
|
180
|
+
* t0 = performance.now(),
|
|
181
|
+
* current_time = () => (performance.now() - t0)
|
|
182
|
+
*
|
|
183
|
+
* // the function that we plan to apply shareable-debouncing to
|
|
184
|
+
* const fn = (v: number) => {
|
|
185
|
+
* log_history.push([current_time(), v])
|
|
186
|
+
* return v + 100
|
|
187
|
+
* }
|
|
188
|
+
*
|
|
189
|
+
* // the debounced version of `fn`, that, when called too quickly, shares the existing promise, and prolongs its resolution by the wait time
|
|
190
|
+
* const debounced_fn = debounceAndShare(1000, fn)
|
|
191
|
+
*
|
|
192
|
+
* // `a` is a promise that should resolve after 1000ms since the last interruption call to `debounced_fn` (i.e. 1000ms from now).
|
|
193
|
+
* const a = debounced_fn(24)
|
|
194
|
+
*
|
|
195
|
+
* await sleep(500) // promise `a` is still pending after the 500ms sleep
|
|
196
|
+
*
|
|
197
|
+
* // since `a` has not been resolved yet, calling `debounced_fn` again to construct `b` will result in the reusage of the old existing promise `a`.
|
|
198
|
+
* // in addition, due to the interuptive call, the resolution of the `a` and `b` promises will be delayed by another 1000ms from here on.
|
|
199
|
+
* const b = debounced_fn(42)
|
|
200
|
+
*
|
|
201
|
+
* assertEq(a === b, true) // the promises `a` and `b` are one and the same, since `debounced_fn` shares the existing non-settled promises.
|
|
202
|
+
*
|
|
203
|
+
* // after the sleep below, 1050ms would have passed since the creation of `a`, and it would have been resolved had we _not_ created `b`.
|
|
204
|
+
* // however, since we created `b` within the debounce interval of 1000ms, the promise's timer has been reset.
|
|
205
|
+
* await sleep(550)
|
|
206
|
+
* assertEq(log_history, [])
|
|
207
|
+
*
|
|
208
|
+
* // 1000ms after the creation of `b`, the value `42` will be logged into `log_history` (due to promise `a` and `b`).
|
|
209
|
+
* await sleep(500)
|
|
210
|
+
* assertGe(log_history[0][0], 500 + 999)
|
|
211
|
+
* assertLe(log_history[0][0], 500 + 1040)
|
|
212
|
+
* assertEq(log_history[0][1], 42)
|
|
213
|
+
* assertEq(await a, 100 + 42) // notice that the value of `a` has changed to the expected output value of `b`, because they are one and the same.
|
|
214
|
+
* assertEq(await b, 100 + 42)
|
|
215
|
+
*
|
|
216
|
+
* // now that promises `a` and `b` have been resolved, executing the shared-debounced function again will create a new promise that will resolve after 1000ms from now.
|
|
217
|
+
* const c = debounced_fn(99)
|
|
218
|
+
* assertEq(c === b, false)
|
|
219
|
+
*
|
|
220
|
+
* // 1000ms later, the value `99` will be logged into `log_history` (due to promise `c`).
|
|
221
|
+
* await sleep(1050)
|
|
222
|
+
* assertGe(log_history[1][0], 1550 + 999)
|
|
223
|
+
* assertLe(log_history[1][0], 1550 + 1100)
|
|
224
|
+
* assertEq(log_history[1][1], 99)
|
|
225
|
+
* assertEq(await c, 100 + 99)
|
|
226
|
+
*
|
|
227
|
+
* // key takeaway:
|
|
228
|
+
* // - notice that the promises made within the debounce interval are the same pomise objects (i.e. `a === b`).
|
|
229
|
+
* // - however, once out of that interval, an entirely new promise is generated (i.e. `b !== c`)
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
export const debounceAndShare = (wait_time_ms, fn) => {
|
|
233
|
+
let prev_timer, current_resolve, current_promise;
|
|
234
|
+
const swap_current_promise_with_a_new_one = (value) => {
|
|
235
|
+
current_promise = new Promise((resolve, reject) => (current_resolve = resolve)).then(swap_current_promise_with_a_new_one);
|
|
236
|
+
return value;
|
|
237
|
+
};
|
|
238
|
+
swap_current_promise_with_a_new_one();
|
|
239
|
+
return (...args) => {
|
|
240
|
+
dom_clearTimeout(prev_timer);
|
|
241
|
+
prev_timer = dom_setTimeout(() => (current_resolve(fn(...args))), wait_time_ms);
|
|
242
|
+
return current_promise;
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
/** a throttled function blocks the execution of `fn`, if less than `delta_time_ms` amount of time has passed since the previous non-rejected call.
|
|
246
|
+
* a rejected caller would receive the {@link THROTTLE_REJECT} symbol, instead of the `fn` function's evaluated value.
|
|
247
|
+
*
|
|
248
|
+
* ### Visualization
|
|
249
|
+
*
|
|
250
|
+
* the following visual illustration should give you an idea of how a throttled function with `delta_time_ms = 1000` would behave.
|
|
251
|
+
* - the intersections in the `call made` axis specifies the time where a call is made to the throttled function.
|
|
252
|
+
* - successfully evaluated calls are labeled as `resolved` (with the legend marker `o`), and their caller will receive the evaluated value of `fn()`
|
|
253
|
+
* - unsuccessful calls (i.e. rejected) are labeled as `rejected` (with the legend marker `x`), and their caller will receive the symbol value {@link THROTTLE_REJECT}.
|
|
254
|
+
*
|
|
255
|
+
* ```text
|
|
256
|
+
* │resolved │ o o o
|
|
257
|
+
* │rejected │ │ x x x │ x │ x x
|
|
258
|
+
* │call made├──┴─┴──┴─┴──┴─┴────────────────┴─┴─┴─────────────────► time
|
|
259
|
+
* │time │ 0 1 2 3 4 5
|
|
260
|
+
* ```
|
|
261
|
+
*
|
|
262
|
+
* @param delta_time_ms the time interval in milliseconds for throttling
|
|
263
|
+
* @param fn the function to be throttled
|
|
264
|
+
* @returns a function (that takes arguments intended for `fn`) that returns the value of `fn` if it was not throttled,
|
|
265
|
+
* otherwise a {@link THROTTLE_REJECT} symbol is returned.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* import {
|
|
270
|
+
* assertEquals as assertEq,
|
|
271
|
+
* assertLessOrEqual as assertLe,
|
|
272
|
+
* assertGreaterOrEqual as assertGe,
|
|
273
|
+
* } from "jsr:@std/assert"
|
|
274
|
+
*
|
|
275
|
+
* // a function that creates an asynchronous delay of the given number of milliseconds
|
|
276
|
+
* const sleep = (time_ms: number) => (new Promise((resolve) => (setTimeout(resolve, time_ms))))
|
|
277
|
+
*
|
|
278
|
+
* const
|
|
279
|
+
* log_history: Array<[time: number, value: number]> = [],
|
|
280
|
+
* t0 = performance.now(),
|
|
281
|
+
* current_time = () => (performance.now() - t0)
|
|
282
|
+
*
|
|
283
|
+
* // the function that we plan to apply throttling to
|
|
284
|
+
* const fn = (v: number) => {
|
|
285
|
+
* log_history.push([current_time(), v])
|
|
286
|
+
* return v + 100
|
|
287
|
+
* }
|
|
288
|
+
*
|
|
289
|
+
* // the throttled version of `fn`, that blocks subsequent calls when they are made under the `delta_time` interval (1000ms here).
|
|
290
|
+
* const throttled_fn = throttle(1000, fn)
|
|
291
|
+
*
|
|
292
|
+
* // first call to the `throttled_fn` will be evaluated successfully and immediately
|
|
293
|
+
* const a = throttled_fn(24)
|
|
294
|
+
*
|
|
295
|
+
* assertEq(a, 100 + 24)
|
|
296
|
+
* assertGe(log_history[0][0], 0)
|
|
297
|
+
* assertLe(log_history[0][0], 100)
|
|
298
|
+
* assertEq(log_history[0][1], 24)
|
|
299
|
+
*
|
|
300
|
+
* // subsequent calls to the `throttled_fn` that are made under 1000ms, will be immediately rejected with the symbol value `THROTTLE_REJECT`.
|
|
301
|
+
* await sleep(200)
|
|
302
|
+
* const b1 = throttled_fn(37)
|
|
303
|
+
* const b2 = throttled_fn(42)
|
|
304
|
+
*
|
|
305
|
+
* assertEq(b1, THROTTLE_REJECT)
|
|
306
|
+
* assertEq(b2, THROTTLE_REJECT)
|
|
307
|
+
* assertGe(current_time(), 199)
|
|
308
|
+
* assertLe(current_time(), 400)
|
|
309
|
+
*
|
|
310
|
+
* // finally, when 1000ms has passed, `throttled_fn` will permit another successful call
|
|
311
|
+
* await sleep(800)
|
|
312
|
+
* const c = throttled_fn(99)
|
|
313
|
+
*
|
|
314
|
+
* assertEq(c, 100 + 99)
|
|
315
|
+
* assertGe(log_history[1][0], 999)
|
|
316
|
+
* assertLe(log_history[1][0], 1300)
|
|
317
|
+
* assertEq(log_history[1][1], 99)
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
export const throttle = (delta_time_ms, fn) => {
|
|
321
|
+
let last_call = 0;
|
|
322
|
+
return (...args) => {
|
|
323
|
+
const time_now = date_now();
|
|
324
|
+
if (time_now - last_call > delta_time_ms) {
|
|
325
|
+
last_call = time_now;
|
|
326
|
+
return fn(...args);
|
|
327
|
+
}
|
|
328
|
+
return THROTTLE_REJECT;
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
/** a throttle function, similar to {@link throttle}, that also insures that the **final** call (aka trailing call) made to the throttled function **always** resolves eventually.
|
|
332
|
+
*
|
|
333
|
+
* this is useful in cases where it is of utmost importance that the throttled function is called **one last time** with before a prolonged delay.
|
|
334
|
+
*
|
|
335
|
+
* ### Visualization
|
|
336
|
+
*
|
|
337
|
+
* the following visual illustration shows the difference between the regular {@link throttle}, and {@link throttleAndTrail} functions:
|
|
338
|
+
*
|
|
339
|
+
* #### this {@link throttleAndTrail} function
|
|
340
|
+
*
|
|
341
|
+
* here is a function `fn` throttled with `trailing_time_ms = 1500`, and `delta_time_ms = 1000`.
|
|
342
|
+
* as you can see below, the trailing calls to the throttled function do get resolved eventually (1500ms after the last call).
|
|
343
|
+
*
|
|
344
|
+
* ```text
|
|
345
|
+
* │time │ ╭╶╶╮ 1.2 2.7 3.3 4.8 5.2
|
|
346
|
+
* ├─────────│ ╭╶┤ │ ┌───(delayed)──┐ ┌───(delayed)──┐ (rejected)
|
|
347
|
+
* │ │ ╭╶╶┤ │ │ │ ▼ ╭╶┤ ▼ ╭╶╶╶╶╶╶╶╮
|
|
348
|
+
* │resolved │ o ▼ ▼ ▼ o │ o o ▼ │ o o ▼ o
|
|
349
|
+
* │rejected │ │ x x x │ │ │ x │ │ x │
|
|
350
|
+
* │call made├──┴─┴──┴─┴──┴─┴────────────────┴─┴─┴────────────────┴─┴───────┴──► time
|
|
351
|
+
* │time │ 0 1 2 3 4 5 6
|
|
352
|
+
* ```
|
|
353
|
+
*
|
|
354
|
+
* #### regular {@link throttle} function
|
|
355
|
+
*
|
|
356
|
+
* here is a function `fn` throttled with `delta_time_ms = 1000`.
|
|
357
|
+
* as it can be seen below, the final call to the throttled function gets rejected, because it was called too quickly.
|
|
358
|
+
*
|
|
359
|
+
* ```text
|
|
360
|
+
* │resolved │ o o o
|
|
361
|
+
* │rejected │ │ x x x │ x │ x x
|
|
362
|
+
* │call made├──┴─┴──┴─┴──┴─┴────────────────┴─┴─┴─────────────────► time
|
|
363
|
+
* │time │ 0 1 2 3 4 5
|
|
364
|
+
* ```
|
|
365
|
+
*
|
|
366
|
+
* @param trailing_time_ms the time in milliseconds after which a trailing (pending) call to the function gets resolved if no other calls are made during that time interval.
|
|
367
|
+
* you would definitely want this to be some value greater than {@link delta_time_ms}, otherwise it will be weird because if this value is smaller,
|
|
368
|
+
* then `trailing_time_ms` will become the "effective" throttling time interval, but also one that always resolved later rather than immediately.
|
|
369
|
+
* @param delta_time_ms the time interval in milliseconds for throttling
|
|
370
|
+
* @param fn the function to be throttled
|
|
371
|
+
* @param rejection_value if a rejection value is provided, then old unresolved pending promises will be rejected with the given value,
|
|
372
|
+
* when a new call to the throttled function is made within the {@link trailing_time_ms} waiting period.
|
|
373
|
+
* if no rejection value is provided, then the promise will linger around unsettled forever.
|
|
374
|
+
* do note that having a rejection value means that you **will** have to catch any rejections if you wait for the promise,
|
|
375
|
+
* otherwise it will bubble to the top level as an unhandled error.
|
|
376
|
+
* @returns a function (that takes arguments intended for `fn`) that returns a `Promise` to the value of `fn` if it is resolved (i.e. not throttled or when trailing),
|
|
377
|
+
* otherwise if throttled, then that promise will either be never be resolved, or rejected based on if a {@link rejection_value} was provided.
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```ts
|
|
381
|
+
* import {
|
|
382
|
+
* assertEquals as assertEq,
|
|
383
|
+
* assertLessOrEqual as assertLe,
|
|
384
|
+
* assertGreaterOrEqual as assertGe,
|
|
385
|
+
* } from "jsr:@std/assert"
|
|
386
|
+
*
|
|
387
|
+
* // a function that creates an asynchronous delay of the given number of milliseconds
|
|
388
|
+
* const sleep = (time_ms: number) => (new Promise((resolve) => (setTimeout(resolve, time_ms))))
|
|
389
|
+
*
|
|
390
|
+
* const
|
|
391
|
+
* log_history: Array<[time: number, value: number]> = [],
|
|
392
|
+
* t0 = performance.now(),
|
|
393
|
+
* current_time = () => (performance.now() - t0)
|
|
394
|
+
*
|
|
395
|
+
* // the function that we plan to apply throttling to
|
|
396
|
+
* const fn = (v: number) => {
|
|
397
|
+
* log_history.push([current_time(), v])
|
|
398
|
+
* return v + 100
|
|
399
|
+
* }
|
|
400
|
+
*
|
|
401
|
+
* // the throttled version of `fn` with trailing enabled.
|
|
402
|
+
* // a call is considered to be a trailing call if more than `trailing_time_ms` of 1500ms has passed since its inception.
|
|
403
|
+
* // however after a successful call, subsequent non-trailing calls made under the `delta_time` interval of 1000ms will be rejected with the value `"REJECTED!"`.
|
|
404
|
+
* const throttled_fn = throttleAndTrail(1500, 1000, fn, "REJECTED!")
|
|
405
|
+
*
|
|
406
|
+
* // first call to the `throttled_fn` will be evaluated successfully and immediately (albeit being wrapped under a promise)
|
|
407
|
+
* const a = throttled_fn(24)
|
|
408
|
+
*
|
|
409
|
+
* await sleep(100)
|
|
410
|
+
* assertGe(log_history[0][0], 0)
|
|
411
|
+
* assertLe(log_history[0][0], 100)
|
|
412
|
+
* assertEq(log_history[0][1], 24)
|
|
413
|
+
* assertEq(await a, 100 + 24)
|
|
414
|
+
*
|
|
415
|
+
* // subsequent non-trailing calls to the `throttled_fn` that are made under 1000ms, will be rejected with the custom value `"REJECTED!"`.
|
|
416
|
+
* await sleep(100)
|
|
417
|
+
* const b1 = throttled_fn(37).catch(reason => reason)
|
|
418
|
+
* const b2 = throttled_fn(42).catch(reason => reason)
|
|
419
|
+
*
|
|
420
|
+
* assertEq(await b1, "REJECTED!")
|
|
421
|
+
* // we don't await for `b2`, because after 1500ms, it will be resolved due to being a trailing call, and we don't want that right now.
|
|
422
|
+
* assertGe(current_time(), 199)
|
|
423
|
+
* assertLe(current_time(), 400)
|
|
424
|
+
*
|
|
425
|
+
* // finally, we create a trailing call, which will take 1500ms to resolve, since less than 1000ms has passed since the last call (`b2`).
|
|
426
|
+
* await sleep(100)
|
|
427
|
+
* const c = throttled_fn(99)
|
|
428
|
+
*
|
|
429
|
+
* assertEq(await c, 100 + 99)
|
|
430
|
+
* assertGe(log_history[1][0], 300 + 1499)
|
|
431
|
+
* assertLe(log_history[1][0], 300 + 1700)
|
|
432
|
+
* assertEq(log_history[1][1], 99)
|
|
433
|
+
* assertEq(await b2, "REJECTED!")
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
export const throttleAndTrail = (trailing_time_ms, delta_time_ms, fn, rejection_value) => {
|
|
437
|
+
let prev_timer, prev_reject = () => { };
|
|
438
|
+
const throttled_fn = throttle(delta_time_ms, fn);
|
|
439
|
+
return (...args) => {
|
|
440
|
+
dom_clearTimeout(prev_timer);
|
|
441
|
+
if (rejection_value !== undefined) {
|
|
442
|
+
prev_reject(rejection_value);
|
|
443
|
+
}
|
|
444
|
+
const result = throttled_fn(...args);
|
|
445
|
+
if (result === THROTTLE_REJECT) {
|
|
446
|
+
return new Promise((resolve, reject) => {
|
|
447
|
+
prev_reject = reject;
|
|
448
|
+
prev_timer = dom_setTimeout(() => resolve(fn(...args)), trailing_time_ms);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return promise_resolve(result);
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
/** a factory function that generates a synchronous task queuer,
|
|
455
|
+
* which ensures that the task-functions it receives are executed sequentially,
|
|
456
|
+
* one after the other, in the order they were enqueued.
|
|
457
|
+
*
|
|
458
|
+
* TODO: consider adding this utility function to `@oazmi/kitchensink/lambda`, or the planned `@oazmi/kitchensink/duty` or `@oazmi/kitchensink/obligate`.
|
|
459
|
+
*
|
|
460
|
+
* @returns see {@link SyncTaskQueue} for the return type, and check out the example below.
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* ```ts
|
|
464
|
+
* import { assert } from "jsr:@std/assert"
|
|
465
|
+
*
|
|
466
|
+
* // some utility functions
|
|
467
|
+
* const
|
|
468
|
+
* getTime = () => (performance.now()),
|
|
469
|
+
* assertBetween = (value: number, min: number, max: number) => (assert(value >= min && value <= max)),
|
|
470
|
+
* promiseTimeout = (wait_time_ms: number): Promise<void> => {
|
|
471
|
+
* return new Promise((resolve, reject) => { setTimeout(resolve, wait_time_ms) })
|
|
472
|
+
* }
|
|
473
|
+
*
|
|
474
|
+
* const
|
|
475
|
+
* my_task_queue = syncTaskQueueFactory(),
|
|
476
|
+
* start_time = getTime()
|
|
477
|
+
*
|
|
478
|
+
* const
|
|
479
|
+
* task1 = my_task_queue(promiseTimeout, 500),
|
|
480
|
+
* task2 = my_task_queue(promiseTimeout, 500),
|
|
481
|
+
* task3 = my_task_queue(promiseTimeout, 500),
|
|
482
|
+
* task4 = my_task_queue((value: string) => (value + " world"), "hello"),
|
|
483
|
+
* task5 = my_task_queue(async (value: string) => (value + " world"), "bye bye")
|
|
484
|
+
*
|
|
485
|
+
* await task2 // will take ~1000ms to resolve.
|
|
486
|
+
* assertBetween(getTime() - start_time, 950, 1100)
|
|
487
|
+
*
|
|
488
|
+
* await task1 // will already be resolved, since `task1` preceded `task2` in the queue.
|
|
489
|
+
* assertBetween(getTime() - start_time, 950, 1100)
|
|
490
|
+
*
|
|
491
|
+
* await task3 // will take an additional ~500ms to resolve (so ~1500ms in total).
|
|
492
|
+
* assertBetween(getTime() - start_time, 1450, 1600)
|
|
493
|
+
*
|
|
494
|
+
* assert(task4 instanceof Promise)
|
|
495
|
+
* assert(await task4, "hello world") // almost instantaneous promise-resolution
|
|
496
|
+
* assertBetween(getTime() - start_time, 1450, 1600)
|
|
497
|
+
*
|
|
498
|
+
* assert(task5 instanceof Promise)
|
|
499
|
+
* assert(await task5, "bye bye world") // almost instantaneous promise-resolution
|
|
500
|
+
* assertBetween(getTime() - start_time, 1450, 1600)
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
export const syncTaskQueueFactory = () => {
|
|
504
|
+
// since javascript is single threaded, the following pattern guarantees that we'll be able to swap the `latest_promise` with a new one,
|
|
505
|
+
// even before the next caller of `task_queuer` gets the chance to query the `latest_promise`.
|
|
506
|
+
// this permits us to chain async-task generators, so that they execute synchronously, one after the other.
|
|
507
|
+
let latest_promise = promise_resolve();
|
|
508
|
+
const task_queuer = (task_fn, ...args) => {
|
|
509
|
+
const original_latest_promise = latest_promise, [promise_current_task_value, resolve_current_task_value] = promise_outside();
|
|
510
|
+
latest_promise = promise_current_task_value;
|
|
511
|
+
original_latest_promise.finally(() => {
|
|
512
|
+
resolve_current_task_value(task_fn(...args));
|
|
513
|
+
});
|
|
514
|
+
return promise_current_task_value;
|
|
515
|
+
};
|
|
516
|
+
return task_queuer;
|
|
517
|
+
};
|
|
518
|
+
/** create a queue list that can be asynchronously awaited to receive new items.
|
|
519
|
+
*
|
|
520
|
+
* TODO: should I also make the `items` non-private, so that they're accessible from super classes?
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* ```ts
|
|
524
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
525
|
+
*
|
|
526
|
+
* const
|
|
527
|
+
* queue = new AwaitableQueue<string>(),
|
|
528
|
+
* promise1 = queue.shift(),
|
|
529
|
+
* promise2 = queue.shift()
|
|
530
|
+
*
|
|
531
|
+
* assertEquals(queue.getSize(), -2)
|
|
532
|
+
*
|
|
533
|
+
* queue.push("hello")
|
|
534
|
+
* assertEquals(await promise1, "hello")
|
|
535
|
+
* assertEquals(queue.getSize(), -1)
|
|
536
|
+
*
|
|
537
|
+
* queue.push("world")
|
|
538
|
+
* assertEquals(await promise2, "world")
|
|
539
|
+
* assertEquals(queue.getSize(), 0)
|
|
540
|
+
*
|
|
541
|
+
* queue.push("abcd")
|
|
542
|
+
* assertEquals(queue.getSize(), 1)
|
|
543
|
+
* // notice that when extra items are present,
|
|
544
|
+
* // an immediate value is returned rather than a promise.
|
|
545
|
+
* assertEquals(queue.shift(), "abcd")
|
|
546
|
+
*
|
|
547
|
+
* queue.push("1", "2", "3")
|
|
548
|
+
* // dump all immediately available items, clearing off the internal list.
|
|
549
|
+
* assertEquals(queue.dump(), ["1", "2", "3"])
|
|
550
|
+
* assertEquals(queue.dump(), []) // nothing else to dump.
|
|
551
|
+
*
|
|
552
|
+
* const
|
|
553
|
+
* promise3 = queue.shift(),
|
|
554
|
+
* promise4 = queue.shift(),
|
|
555
|
+
* promise5 = queue.shift(),
|
|
556
|
+
* // drop all promise resolvers, clearing off the internal list.
|
|
557
|
+
* [resolve3, resolve4, resolve5] = queue.drop()
|
|
558
|
+
*
|
|
559
|
+
* assertEquals(queue.drop(), []) // no more promise resolvers remain to be dropped.
|
|
560
|
+
* queue.push("1", "2", "3", "4", "5")
|
|
561
|
+
* assertEquals(queue.shift(), "1")
|
|
562
|
+
* assertEquals(queue.shift(), "2")
|
|
563
|
+
*
|
|
564
|
+
* // `promise3` to `promise5` remain unresolved right now.
|
|
565
|
+
* // but since we don't want forever hagging promises, we will resolve them below:
|
|
566
|
+
* resolve3(await queue.shift())
|
|
567
|
+
* resolve4(await queue.shift())
|
|
568
|
+
* resolve5(await queue.shift())
|
|
569
|
+
* assertEquals(await promise3, "3")
|
|
570
|
+
* assertEquals(await promise4, "4")
|
|
571
|
+
* assertEquals(await promise5, "5")
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
export class AwaitableQueue {
|
|
575
|
+
#items = [];
|
|
576
|
+
#queuedResolvers = [];
|
|
577
|
+
// TODO: consider also adding a `queuedRejectors: Array<(reason?: string) => void>`,
|
|
578
|
+
// which will be triggered when a user pushes a specific `REJECT_VALUE: unique symbol` as the `item`.
|
|
579
|
+
/** push items into the queue, and immediately resolve any queued up promises,
|
|
580
|
+
* otherwise just queue up the new items in the internal array.
|
|
581
|
+
*/
|
|
582
|
+
push(...items) {
|
|
583
|
+
const queued_resolvers = this.#queuedResolvers, queued_resolvers_len = queued_resolvers.length, items_len = items.length, iterations = min(queued_resolvers_len, items_len), items_in_excess = items_len - queued_resolvers_len,
|
|
584
|
+
// the reason why we splice below instead of shifting is because splicing,
|
|
585
|
+
// even with one element, is faster than shifting.
|
|
586
|
+
// after the splice, either `items` or `queued_resolvers` will have a zero length,
|
|
587
|
+
// or both might get a zero length.
|
|
588
|
+
items_for_resolving = items.splice(0, iterations), resolvers_to_resolve = queued_resolvers.splice(0, iterations);
|
|
589
|
+
for (let i = 0; i < iterations; i++) {
|
|
590
|
+
resolvers_to_resolve[i](items_for_resolving[i]);
|
|
591
|
+
}
|
|
592
|
+
return items_in_excess > 0
|
|
593
|
+
? this.#items.push(...items)
|
|
594
|
+
: items_in_excess;
|
|
595
|
+
}
|
|
596
|
+
// TODO: I was initially returning `Promise<T>` and keeping the function `async`,
|
|
597
|
+
// however, if it might be more performant if we do not always generate a promised return value.
|
|
598
|
+
// so for now, I'm staying with a return value of `MaybePromise<T>`.
|
|
599
|
+
// the method consumer can always add an `await` if they want to be certain that their value is resolved before consuming.
|
|
600
|
+
/** make a request to pop the first item in the queue.
|
|
601
|
+
* if no queued item is currently available,
|
|
602
|
+
* you will receive a promise that will resolve as soon as an item is pushed,
|
|
603
|
+
* and after all other promises that came before you have been served.
|
|
604
|
+
*/
|
|
605
|
+
shift() {
|
|
606
|
+
const items = this.#items;
|
|
607
|
+
if (items.length > 0) {
|
|
608
|
+
return items.shift();
|
|
609
|
+
}
|
|
610
|
+
const [promise, resolve, reject] = promise_outside();
|
|
611
|
+
this.#queuedResolvers.push(resolve);
|
|
612
|
+
return promise;
|
|
613
|
+
}
|
|
614
|
+
/** dump off all currently available queued items, and receive them as a returned value.
|
|
615
|
+
*
|
|
616
|
+
* > [!note]
|
|
617
|
+
* > the first item in the returned array is the first item in the queue (highest priority/first to be served),
|
|
618
|
+
* > while the last item in the array is at the end of the queue (least priority/last to be served).
|
|
619
|
+
*/
|
|
620
|
+
dump() {
|
|
621
|
+
return this.#items.splice(0);
|
|
622
|
+
}
|
|
623
|
+
/** drop off the resolvers of all currently unresolved promises in the queue.
|
|
624
|
+
* this means that all existing promises will remain hanging,
|
|
625
|
+
* unless you resolve them yourself with the returned resolver functions.
|
|
626
|
+
*
|
|
627
|
+
* > [!note]
|
|
628
|
+
* > the first item in the returned array is the first resolver function in the queue (highest priority/first to be served),
|
|
629
|
+
* > while the last item in the array is the last resolver that should be served in the queue (least priority).
|
|
630
|
+
*/
|
|
631
|
+
drop() {
|
|
632
|
+
return this.#queuedResolvers.splice(0);
|
|
633
|
+
}
|
|
634
|
+
/** returns the number of immediately available items, or the number of queued up requests.
|
|
635
|
+
*
|
|
636
|
+
* - when the return value is a positive value `n`, it indicates that `|n|` number of items are queued up,
|
|
637
|
+
* and you can immediately {@link shift} `|n|` number of times.
|
|
638
|
+
* - when the return value is a negative value `n`, it indicates that `|n|` number of promises are waiting to be resolved,
|
|
639
|
+
* and that if you {@link shift} right now, you will be receiving a promise to the `|n| + 1` item in the future (relative to now).
|
|
640
|
+
*/
|
|
641
|
+
getSize() {
|
|
642
|
+
const items_len = this.#items.length;
|
|
643
|
+
return items_len > 0 ? (items_len) : (-this.#queuedResolvers.length);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/** create a stack list that can be asynchronously awaited to receive new items.
|
|
647
|
+
*
|
|
648
|
+
* @example
|
|
649
|
+
* ```ts
|
|
650
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
651
|
+
*
|
|
652
|
+
* const
|
|
653
|
+
* stack = new AwaitableStack<string>(),
|
|
654
|
+
* promise1 = stack.pop(),
|
|
655
|
+
* promise2 = stack.pop()
|
|
656
|
+
*
|
|
657
|
+
* assertEquals(stack.getSize(), -2)
|
|
658
|
+
*
|
|
659
|
+
* // since this is a stack, the last promise is the first one to be served.
|
|
660
|
+
* stack.push("hello")
|
|
661
|
+
* assertEquals(await promise2, "hello")
|
|
662
|
+
* assertEquals(stack.getSize(), -1)
|
|
663
|
+
*
|
|
664
|
+
* stack.push("world")
|
|
665
|
+
* assertEquals(await promise1, "world")
|
|
666
|
+
* assertEquals(stack.getSize(), 0)
|
|
667
|
+
*
|
|
668
|
+
* stack.push("abcd")
|
|
669
|
+
* assertEquals(stack.getSize(), 1)
|
|
670
|
+
* // notice that when extra items are present,
|
|
671
|
+
* // an immediate value is returned rather than a promise.
|
|
672
|
+
* assertEquals(stack.pop(), "abcd")
|
|
673
|
+
*
|
|
674
|
+
* stack.push("1", "2", "3")
|
|
675
|
+
* // dump all immediately available items, clearing off the internal list.
|
|
676
|
+
* // the last item in the array (`"3"`) is at the top of the stack.
|
|
677
|
+
* assertEquals(stack.dump(), ["1", "2", "3"])
|
|
678
|
+
* assertEquals(stack.dump(), []) // nothing else to dump.
|
|
679
|
+
*
|
|
680
|
+
* const
|
|
681
|
+
* promise3 = stack.pop(),
|
|
682
|
+
* promise4 = stack.pop(),
|
|
683
|
+
* promise5 = stack.pop(),
|
|
684
|
+
* // drop all promise resolvers, clearing off the internal list.
|
|
685
|
+
* [resolve3, resolve4, resolve5] = stack.drop()
|
|
686
|
+
*
|
|
687
|
+
* assertEquals(stack.drop(), []) // no more promise resolvers remain to be dropped.
|
|
688
|
+
* stack.push("1", "2", "3", "4", "5")
|
|
689
|
+
* assertEquals(stack.pop(), "5")
|
|
690
|
+
* assertEquals(stack.pop(), "4")
|
|
691
|
+
*
|
|
692
|
+
* // `promise3` to `promise5` remain unresolved right now.
|
|
693
|
+
* // but since we don't want forever hagging promises, we will resolve them below:
|
|
694
|
+
* resolve5(await stack.pop())
|
|
695
|
+
* resolve4(await stack.pop())
|
|
696
|
+
* resolve3(await stack.pop())
|
|
697
|
+
* assertEquals(await promise5, "3")
|
|
698
|
+
* assertEquals(await promise4, "2")
|
|
699
|
+
* assertEquals(await promise3, "1")
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
export class AwaitableStack {
|
|
703
|
+
#items = [];
|
|
704
|
+
#stackedResolvers = [];
|
|
705
|
+
/** push a items into the stack, and immediately resolve any lingering promises,
|
|
706
|
+
* otherwise the new items will just get stacked up in the internal items array.
|
|
707
|
+
*/
|
|
708
|
+
push(...items) {
|
|
709
|
+
const stacked_resolvers = this.#stackedResolvers, stacked_resolvers_len = stacked_resolvers.length, items_len = items.length, iterations = min(stacked_resolvers_len, items_len), items_in_excess = items_len - stacked_resolvers_len, items_for_resolving = items.splice(0, iterations);
|
|
710
|
+
for (let i = 0; i < iterations; i++) {
|
|
711
|
+
stacked_resolvers.pop()(items_for_resolving[i]);
|
|
712
|
+
}
|
|
713
|
+
return items_in_excess > 0
|
|
714
|
+
? this.#items.push(...items)
|
|
715
|
+
: items_in_excess;
|
|
716
|
+
}
|
|
717
|
+
/** make a request to pop the top item in the stack.
|
|
718
|
+
* if no stacked item is currently available,
|
|
719
|
+
* you will receive a promise that will resolve if an item is pushed,
|
|
720
|
+
* after all other promises that came _after_ you have been served.
|
|
721
|
+
*/
|
|
722
|
+
pop() {
|
|
723
|
+
const items = this.#items;
|
|
724
|
+
if (items.length > 0) {
|
|
725
|
+
return items.pop();
|
|
726
|
+
}
|
|
727
|
+
const [promise, resolve, reject] = promise_outside();
|
|
728
|
+
this.#stackedResolvers.push(resolve);
|
|
729
|
+
return promise;
|
|
730
|
+
}
|
|
731
|
+
/** dump off all currently available stacked items, and receive them as a returned value.
|
|
732
|
+
*
|
|
733
|
+
* > [!note]
|
|
734
|
+
* > the last item in the returned array is at the top of the stack,
|
|
735
|
+
* > while the first item in the array is the bottom of the stack.
|
|
736
|
+
*/
|
|
737
|
+
dump() {
|
|
738
|
+
return this.#items.splice(0);
|
|
739
|
+
}
|
|
740
|
+
/** drop off the resolvers of all currently unresolved promises in the stack.
|
|
741
|
+
* this means that all existing promises will remain hanging,
|
|
742
|
+
* unless you resolve them yourself with the returned resolver functions.
|
|
743
|
+
*
|
|
744
|
+
* > [!note]
|
|
745
|
+
* > the last item in the returned array is at the top of the stack, meaning that it should be served/resolved first,
|
|
746
|
+
* > while the first item in the array is the last resolver that should be served (bottom of the stack).
|
|
747
|
+
*/
|
|
748
|
+
drop() {
|
|
749
|
+
return this.#stackedResolvers.splice(0);
|
|
750
|
+
}
|
|
751
|
+
/** returns the number of immediately available items, or the number of queued up requests.
|
|
752
|
+
*
|
|
753
|
+
* - when the return value is a positive value `n`, it indicates that `|n|` number of items are queued up,
|
|
754
|
+
* and you can immediately {@link shift} `|n|` number of times.
|
|
755
|
+
* - when the return value is a negative value `n`, it indicates that `|n|` number of promises are waiting to be resolved,
|
|
756
|
+
* and that if you {@link shift} right now, you will be receiving a promise to the `|n| + 1` item in the future (relative to now).
|
|
757
|
+
*/
|
|
758
|
+
getSize() {
|
|
759
|
+
const items_len = this.#items.length;
|
|
760
|
+
return items_len > 0 ? (items_len) : (-this.#stackedResolvers.length);
|
|
761
|
+
}
|
|
762
|
+
}
|