UNPKG

ixfx

Version:

Bundle of ixfx libraries

1,747 lines (1,733 loc) 102 kB
import { n as __exportAll } from "./chunk-CaR5F9JI.js"; import { F as elapsedInfinity, H as continuously, R as elapsedSince, h as intervalToMs, jt as defaultComparer, m as elapsedToHumanString, n as sleep, o as resolve, s as resolveSync } from "./src-BUqDa_u7.js"; import { A as resultIsError, C as numberTest, M as resultThrow, P as resultToError, v as integerTest } from "./src-C_hvyftg.js"; import { a as logSet, n as getErrorMessage, s as resolveLogOption } from "./src-B7f_ks6F.js"; import { _ as shuffle, h as randomElement, s as unique } from "./src-CxEyGbiK.js"; import { y as mutable } from "./src-DOorb7Rs.js"; import { n as SimpleEventEmitter } from "./src-CRR1VQls.js"; import { E as movingAverageLight, rt as clamp } from "./src-Cebc3sfq.js"; //#region ../packages/flow/src/behaviour-tree.ts const getName = (t, defaultValue = ``) => { if (typeof t === `object` && `name` in t && t.name !== void 0) return t.name; return defaultValue; }; function* iterateBreadth(t, pathPrefix) { if (typeof pathPrefix === `undefined`) pathPrefix = getName(t); for (const [index, n] of entries(t)) yield [n, pathPrefix]; for (const [index, n] of entries(t)) { const name = getName(n, `?`); yield* iterateBreadth(n, pathPrefix.length > 0 ? pathPrefix + `.` + name : name); } } function* iterateDepth(t, pathPrefix) { if (typeof pathPrefix === `undefined`) pathPrefix = getName(t); for (const [index, n] of entries(t)) { let prefix; if (typeof n === `string`) prefix = pathPrefix; else { const name = getName(n, `?`); prefix = pathPrefix.length > 0 ? pathPrefix + `.` + name : name; } yield [n, prefix]; yield* iterateDepth(n, prefix); } } function isSeqNode(n) { return n.seq !== void 0; } function isSelNode(n) { return n.sel !== void 0; } function* entries(n) { if (isSeqNode(n)) yield* n.seq.entries(); else if (isSelNode(n)) yield* n.sel.entries(); else if (typeof n === `string`) {} else throw new TypeError(`Unexpected shape of node. seq/sel missing`); } //#endregion //#region ../packages/flow/src/delay.ts /** * Pauses execution for interval after which the asynchronous `callback` is executed and awaited. * Must be called with `await` if you want the pause effect. * * @example Pause and wait for function * ```js * const result = await delay(async () => Math.random(), 1000); * console.log(result); // Prints out result after one second * ``` * * If the `interval` option is a number its treated as milliseconds. {@link Interval} can also be used: * ```js * const result = await delay(async () => Math.random(), { mins: 1 }); * ``` * * If `await` is omitted, the function will run after the provided timeout, and code will continue to run. * * @example Schedule a function without waiting * ```js * await delay(async () => { * console.log(Math.random()) * }, 1000); * // Prints out a random number after 1 second. * ``` * * {@link delay} and {@link sleep} are similar. `delay()` takes a parameter of what code to execute after the timeout, while `sleep()` just resolves after the timeout. * * Optionally takes an AbortSignal to cancel delay. * ```js * const ac = new AbortController(); * // Super long wait * await delay(someFn, { signal: ac.signal, hours: 1 }} * ... * ac.abort(); // Cancels long delay * ``` * * It also allows choice of when delay should happen. * If you want to be able to cancel or re-run a delayed function, consider using * {@link timeout} instead. * * @typeParam V - Type of callback return value * @param callback What to run after interval * @param optsOrMillis Options for delay, or millisecond delay. By default delay is before `callback` is executed. * @return Returns result of `callback`. */ const delay = async (callback, optsOrMillis) => { const opts = typeof optsOrMillis === `number` ? { millis: optsOrMillis } : optsOrMillis; const delayWhen = opts.delay ?? `before`; if (delayWhen === `before` || delayWhen === `both`) await sleep(opts); const r = Promise.resolve(await callback()); if (delayWhen === `after` || delayWhen === `both`) await sleep(opts); return r; }; /** * Iterate over a source iterable with some delay between results. * Delay can be before, after or both before and after each result from the * source iterable. * * Since it's an async iterable, `for await ... of` is needed. * * ```js * const opts = { intervalMs: 1000, delay: 'before' }; * const iterable = count(10); * for await (const i of delayIterable(iterable, opts)) { * // Prints 0..9 with one second between * } * ``` * * Use {@link delay} to return a result after some delay * * @param iter * @param opts */ /** * Async generator that loops via `requestAnimationFrame`. * * We can use `for await of` to run code: * ```js * const loop = delayAnimationLoop(); * for await (const o of loop) { * // Do something... * // Warning: loops forever * } * // Warning: execution doesn't continue to this point * // unless there is a 'break' in loop. * ``` * * Or use the generator in manually: * ```js * // Loop forever * (async () => { * const loop = delayAnimationLoop(); * while (true) { * await loop.next(); * * // Do something... * // Warning: loops forever * } * })(); * ``` * * Practically, these approaches are not so useful * because execution blocks until the loop finishes. * * Instead, we might want to continually loop a bit * of code while other bits of code continue to run. * * The below example shows how to do this. * * ```js * setTimeout(async () => { * for await (const _ of delayAnimationLoop()) { * // Do soething at animation speed * } * }); * * // Execution continues while loop also runs * ``` * */ async function* delayAnimationLoop() { let resolve; let p = new Promise((r) => resolve = r); let timer = 0; const callback = () => { if (resolve) resolve(); p = new Promise((r) => resolve = r); }; try { while (true) { timer = globalThis.requestAnimationFrame(callback); yield await p; } } finally { if (resolve) resolve(); globalThis.cancelAnimationFrame(timer); } } /** * Async generator that loops at a given interval. * * @example * For Await loop every second * ```js * const loop = delayLoop(1000); * // Or: const loop = delayLoop({ secs: 1 }); * for await (const o of loop) { * // Do something... * // Warning: loops forever * } * ``` * * @example * Loop runs every second * ```js * (async () => { * const loop = delayLoop(1000); * // or: loop = delayLoop({ secs: 1 }); * while (true) { * await loop.next(); * * // Do something... * // Warning: loops forever * } * })(); * ``` * * Alternatives: * * {@link delay} to run a single function after a delay * * {@link sleep} pause execution * * {@link continuously} to start/stop/adjust a constantly running loop * * @param timeout Delay. If 0 is given, `requestAnimationFrame` is used over `setTimeout`. */ async function* delayLoop(timeout) { const timeoutMs = intervalToMs(timeout); if (typeof timeoutMs === `undefined`) throw new Error(`timeout is undefined`); if (timeoutMs < 0) throw new Error(`Timeout is less than zero`); if (timeoutMs === 0) return yield* delayAnimationLoop(); let resolve; let p = new Promise((r) => resolve = r); let timer; const callback = () => { if (resolve) resolve(); p = new Promise((r) => resolve = r); }; try { while (true) { timer = globalThis.setTimeout(callback, timeoutMs); yield await p; } } finally { if (resolve) resolve(); if (timer !== void 0) globalThis.clearTimeout(timer); timer = void 0; } } //#endregion //#region ../packages/flow/src/debounce.ts /** * Returns a debounce function which acts to filter calls to a given function `fn`. * * Eg, Let's create a debounced wrapped for a function: * ```js * const fn = () => console.log('Hello'); * const debouncedFn = debounce(fn, 1000); * ``` * * Now we can call `debouncedFn()` as often as we like, but it will only execute * `fn()` after 1 second has elapsed since the last invocation. It essentially filters * many calls to fewer calls. Each time `debounceFn()` is called, the timeout is * reset, so potentially `fn` could never be called if the rate of `debounceFn` being called * is faster than the provided timeout. * * Remember that to benefit from `debounce`, you must call the debounced wrapper, not the original function. * * ```js * // Create * const d = debounce(fn, 1000); * * // Don't do this if we want to benefit from the debounce * fn(); * * // Use the debounced wrapper * d(); // Only calls fn after 1000s * ``` * * A practical use for this is handling high-frequency streams of data, where we don't really * care about processing every event, only last event after a period. Debouncing is commonly * used on microcontrollers to prevent button presses being counted twice. * * @example Handle most recent pointermove event after 1000ms * ```js * // Set up debounced handler * const moveDebounced = debounce((evt) => { * // Handle event * }, 500); * * // Wire up event * el.addEventListener(`pointermove`, moveDebounced); * ``` * * Arguments can be passed to the debounced function: * * ```js * const fn = (x) => console.log(x); * const d = debounce(fn, 1000); * d(10); * ``` * * If you want the result of a debounced function when it finally executes, pass * in the `onResult` parameter: * ```js * const isRed = (colour) => colour === `red`; * const onResult = (result) => { * if (result.success) { * console.log(`Value: ${result.value}`); * } * } * const d = debounce(fn, 1000, onResult); * ``` * * Note that only the `onResult` handler for the function call that succeeeds is called, * there's no queuing of callbacks. * * If the debounced function throws an error, this will be reported as well: * ```js * if (!result.success) { * console.error(result.error); * } * ``` * @param callback Function to filter access to * @param interval Minimum time between invocations * @param onResult Callback when the result from the wrapped function is available * @returns Debounce function */ function debounce(callback, interval, onResult) { let timer; return (...args) => { if (timer) clearTimeout(timer); timer = setTimeout(() => { try { const result = callback(...args); if (onResult) onResult({ success: true, value: result }); } catch (error) { if (onResult) onResult({ success: false, error }); } }, intervalToMs(interval)); }; } //#endregion //#region ../packages/flow/src/dispatch-list.ts /** * Maintains a list of listeners to receive data. * * Type parameter is the type of events sent. * * ```js * const d = new DispatchList(); * * // Eg: add a listener * d.add(v => { * // Handle a value * }); * * // Eg. send a value to all listeners * d.notify(`some value`); * ``` * * If event handler returns true, additional handlers are not called. */ var DispatchList = class { #handlers; #counter = 0; #id = Math.floor(Math.random() * 100); constructor() { this.#handlers = []; } /** * Returns _true_ if list is empty * @returns */ isEmpty() { return this.#handlers.length === 0; } /** * Adds a handler. You get back an id which can be used * to remove the handler later. * * Handlers can be added with 'once' flag set to _true_. This will * automatically remove them after the first value is sent to them. * * If handler returns _true_, subsequent handlers are not invoked. * @param handler * @param options * @returns */ add(handler, options = {}) { this.#counter++; const once = options.once ?? false; const wrap = { id: `${this.#id} - ${this.#counter}`, handler, once }; this.#handlers.push(wrap); return wrap.id; } /** * Remove a handler by its id. * @param id * @returns _True_ if handler was removed, _false_ if not found. */ remove(id) { const length = this.#handlers.length; this.#handlers = this.#handlers.filter((handler) => handler.id !== id); return this.#handlers.length !== length; } /** * Emit a value to all handlers * Returns _true_ if at least one handler reported 'true' as a response. * Also returns true * @param value */ notify(value) { for (const handler of this.#handlers) { const r = handler.handler(value); if (typeof r === `boolean`) { if (r) { if (handler.once) this.remove(handler.id); return true; } } else if (handler.once) this.remove(handler.id); } return false; } /** * Remove all handlers */ clear() { this.#handlers = []; } }; //#endregion //#region ../packages/flow/src/every.ts /** * Returns true for every _n_th call, eg 2 for every second call. * * If `nth` is 1, returns true for everything. 0 will be false for everything. * * Usage: * ```js * const tenth = everyNth(10); * window.addEventListener(`pointermove`, evt => { * if (!tenth(evt)) return; // Filter out * // Continue processing, it is the 10th thing. * * }); * ``` * * Alternative: * ```js * window.addEventListener(`pointermove`, everyNth(10, evt => { * // Do something with tenth item... * }); * ``` * @param nth Every nth item * @param callback * @returns Function which in turn returns true if nth call has been hit, false otherwise */ const everyNth = (nth, callback) => { resultThrow(integerTest(nth, `positive`, `nth`)); let counter = 0; return (data) => { counter++; if (counter === nth) { counter = 0; if (callback) callback(data); return true; } return false; }; }; //#endregion //#region ../packages/flow/src/execute.ts /** * Runs a series of async expressions, returning the results. * Use {@link runSingle} if it's only a single result you care about. * * @example Run three functions, returning the highest-ranked result. * ```js * const result = runSingle([ * () => 10, * () => 2, * () => 3 * ]); * // Yields: 10 * ``` * * Options can be passed for evaluation: * ```js * const result = run([ * (args) => { * if (args === 'apple') return 100; * }, * () => { * return 10; * } * ]) * ``` * * ```js * const expr = [ * (opts) => 10, * (opts) => 2, * (opts) => 3 * ]; * const opts = { * rank: (a, b) => { * if (a < b) return -1; * if (a > b) return 1; * return 0; * } * } * const result = await run(expr, opts); * // Returns: 2 * ``` * * In terms of typing, it takes an generic arguments `ArgsType` and `ResultType`: * - `ArgsType`: type of expression arguments. This might be `void` if no arguments are used. * - `ResultType`: return type of expression functions * * Thus the `expressions` parameter is an array of functions: * ```js * (args:ArgsType|undefined) => ResultType|undefined * // or * (args:ArgsType|undefined) => Promise<ResultType|undefined> * ``` * * Example: * ```js * const expressions = [ * // Function takes a string arg * (args:string) => return true; // boolean is the necessary return type * ]; * const run<string,boolean>(expressions, opts, 'hello'); * ``` * @param expressions * @param opts * @param args * @returns */ const run = async (expressions, opts = {}, args) => { const results = []; const compareFunction = opts.rank ?? defaultComparer; let expressionsArray = Array.isArray(expressions) ? expressions : [expressions]; if (opts.shuffle) expressionsArray = shuffle(expressionsArray); for (let index = 0; index < expressionsArray.length; index++) { const exp = expressionsArray[index]; let r; if (typeof exp === "function") r = await exp(args); else r = exp; if (r !== void 0) { results.push(r); results.sort(compareFunction); } if (typeof opts.stop !== "undefined") { if (opts.stop(r, results)) break; } } if (opts.filter) return results.filter(opts.filter); return results; }; /** * Like {@link run}, but it returns a single result or _undefined_. * Use the `at` option to specify which index of results to use. * By default it's -1, which is the presumably the highest-ranked result. * * @param expressions * @param opts * @param args * @returns */ const runSingle = async (expressions, opts = {}, args) => { const results = await run(expressions, opts, args); if (!results) return; if (results.length === 0) return; const at = opts.at ?? -1; return results.at(at); }; //#endregion //#region ../packages/flow/src/event-race.ts /** * Subscribes to events on `target`, returning the event data * from the first event that fires. * * By default waits a maximum of 1 minute. * * Automatically unsubscribes on success or failure (ie. timeout) * * ```js * // Event will be data from either event, whichever fires first * // Exception is thrown if neither fires within 1 second * const event = await eventRace(document.body, [`pointermove`, `pointerdown`], { timeout: 1000 }); * ``` * @param target Event source * @param eventNames Event name(s) * @param options Options * @returns */ const eventRace = (target, eventNames, options = {}) => { const intervalMs = options.timeoutMs ?? 601e3; const signal = options.signal; let triggered = false; let disposed = false; let timeout; return new Promise((resolve, reject) => { const onEvent = (event) => { if (`type` in event) if (eventNames.includes(event.type)) { triggered = true; resolve(event); dispose(); } else console.warn(`eventRace: Got event '${event.type}' that is not in race list`); else { console.warn(`eventRace: Event data does not have expected 'type' field`); console.log(event); } }; for (const name of eventNames) target.addEventListener(name, onEvent); const dispose = () => { if (disposed) return; if (timeout !== void 0) clearTimeout(timeout); timeout = void 0; disposed = true; for (const name of eventNames) target.removeEventListener(name, onEvent); }; timeout = setTimeout(() => { if (triggered || disposed) return; dispose(); reject(/* @__PURE__ */ new Error(`eventRace: Events not fired within interval. Events: ${JSON.stringify(eventNames)} Interval: ${intervalMs}`)); }, intervalMs); signal?.addEventListener(`abort`, () => { if (triggered || disposed) return; dispose(); reject(/* @__PURE__ */ new Error(`Abort signal received ${signal.reason}`)); }); }); }; //#endregion //#region ../packages/flow/src/moving-average.ts /** * Uses the same algorithm as {@link movingAverageLight}, but adds values automatically if * nothing has been manually added. * * ```js * // By default, 0 is added if interval elapses * const mat = movingAverageTimed({ interval: 1000 }); * mat(10); // Add value of 10, returns latest average * * mat(); // Get current average * ``` * * This is useful if you are averaging something based on events. For example calculating the * average speed of the pointer. If there is no speed, there is no pointer move event. Using * this function, `value` is added at a rate of `updateRateMs`. This timer is reset * every time a value is added, a bit like the `debounce` function. * * Use an AbortSignal to cancel the timer associated with the `movingAverageTimed` function. * @param options * @returns */ const movingAverageTimed = (options) => { const average = movingAverageLight(); const rm = rateMinimum({ ...options, whatToCall: (distance) => { average(distance); }, fallback() { return options.default ?? 0; } }); return (v) => { rm(v); return average(); }; }; //#endregion //#region ../packages/flow/src/pool.ts /** * A use of a pool resource * * Has two events, _disposed_ and _released_. */ var PoolUser = class extends SimpleEventEmitter { _lastUpdate; _pool; _state; _userExpireAfterMs; /** * Constructor * @param key User key * @param resource Resource being used */ constructor(key, resource) { super(); this.key = key; this.resource = resource; this._lastUpdate = performance.now(); this._pool = resource.pool; this._userExpireAfterMs = this._pool.userExpireAfterMs; this._state = `idle`; this._pool.log.log(`PoolUser ctor key: ${this.key}`); } /** * Returns a human readable debug string * @returns */ toString() { if (this.isDisposed) return `PoolUser. State: disposed`; return `PoolUser. State: ${this._state} Elapsed: ${performance.now() - this._lastUpdate} Data: ${JSON.stringify(this.resource.data)}`; } /** * Resets countdown for instance expiry. * Throws an error if instance is disposed. */ keepAlive() { if (this._state === `disposed`) throw new Error(`PoolItem disposed`); this._lastUpdate = performance.now(); } /** * @internal * @param reason * @returns */ _dispose(reason, data) { if (this._state === `disposed`) return; const resource = this.resource; this._state = `disposed`; resource._release(this); this._pool.log.log(`PoolUser dispose key: ${this.key} reason: ${reason}`); this.fireEvent(`disposed`, { data, reason }); super.clearEventListeners(); } /** * Release this instance * @param reason */ release(reason) { if (this.isDisposed) throw new Error(`User disposed`); const data = this.resource.data; this._pool.log.log(`PoolUser release key: ${this.key} reason: ${reason}`); this.fireEvent(`released`, { data, reason }); this._dispose(`release-${reason}`, data); } get data() { if (this.isDisposed) throw new Error(`User disposed`); return this.resource.data; } /** * Returns true if this instance has expired. * Expiry counts if elapsed time is greater than `userExpireAfterMs` */ get isExpired() { if (this._userExpireAfterMs > 0) return performance.now() > this._lastUpdate + this._userExpireAfterMs; return false; } /** * Returns elapsed time since last 'update' */ get elapsed() { return performance.now() - this._lastUpdate; } /** * Returns true if instance is disposed */ get isDisposed() { return this._state === `disposed`; } /** * Returns true if instance is neither disposed nor expired */ get isValid() { if (this.isDisposed || this.isExpired) return false; if (this.resource.isDisposed) return false; return true; } }; /** * A resource allocated in the Pool */ var Resource = class { #state; #data; #users; #capacityPerResource; #resourcesWithoutUserExpireAfterMs; #lastUsersChange; /** * Constructor. * @param pool Pool * @param data Data */ constructor(pool, data) { this.pool = pool; if (data === void 0) throw new Error(`Parameter 'data' is undefined`); if (pool === void 0) throw new Error(`Parameter 'pool' is undefined`); this.#data = data; this.#lastUsersChange = 0; this.#resourcesWithoutUserExpireAfterMs = pool.resourcesWithoutUserExpireAfterMs; this.#capacityPerResource = pool.capacityPerResource; this.#users = []; this.#state = `idle`; } /** * Gets data associated with resource. * Throws an error if disposed */ get data() { if (this.#state === `disposed`) throw new Error(`Resource disposed`); return this.#data; } /** * Changes the data associated with this resource. * Throws an error if disposed or `data` is undefined. * @param data */ updateData(data) { if (this.#state === `disposed`) throw new Error(`Resource disposed`); if (data === void 0) throw new Error(`Parameter 'data' is undefined`); this.#data = data; } /** * Returns a human-readable debug string for resource * @returns */ toString() { return `Resource (expired: ${this.isExpiredFromUsers} users: ${this.#users.length}, state: ${this.#state}) data: ${JSON.stringify(this.data)}`; } /** * Assigns a user to this resource. * @internal * @param user */ _assign(user) { if (this.#users.find((u) => u === user || u.key === user.key)) throw new Error(`User instance already assigned to resource`); this.#users.push(user); this.#lastUsersChange = performance.now(); } /** * Releases a user from this resource * @internal * @param user */ _release(user) { this.#users = this.#users.filter((u) => u !== user); this.pool._release(user); this.#lastUsersChange = performance.now(); } /** * Returns true if resource can have additional users allocated */ get hasUserCapacity() { return this.usersCount < this.#capacityPerResource; } /** * Returns number of uses of the resource */ get usersCount() { return this.#users.length; } /** * Returns true if automatic expiry is enabled, and that interval * has elapsed since the users list has changed for this resource */ get isExpiredFromUsers() { if (this.#resourcesWithoutUserExpireAfterMs <= 0) return false; if (this.#users.length > 0) return false; return performance.now() > this.#resourcesWithoutUserExpireAfterMs + this.#lastUsersChange; } /** * Returns true if instance is disposed */ get isDisposed() { return this.#state === `disposed`; } /** * Disposes the resource. * If it is already disposed, it does nothing. * @param reason * @returns */ dispose(reason) { if (this.#state === `disposed`) return; const data = this.#data; this.#state = `disposed`; this.pool.log.log(`Resource disposed (${reason})`); for (const u of this.#users) u._dispose(`resource-${reason}`, data); this.#users = []; this.#lastUsersChange = performance.now(); this.pool._releaseResource(this, reason); if (this.pool.freeResource) this.pool.freeResource(data); } }; /** * Resource pool * It does the housekeeping of managing a limited set of resources which are shared by 'users'. * All resources in the Pool are meant to be the same kind of object. * * An example is an audio sketch driven by TensorFlow. We might want to allocate a sound oscillator per detected human body. A naive implementation would be to make an oscillator for each detected body. However, because poses appear/disappear unpredictably, it's a lot of extra work to maintain the binding between pose and oscillator. * * Instead, we might use the Pool to allocate oscillators to poses. This will allow us to limit resources and clean up automatically if they haven't been used for a while. * * Resources can be added manually with `addResource()`, or automatically by providing a `generate()` function in the Pool options. They can then be accessed via a _user key_. This is meant to associated with a single 'user' of a resource. For example, if we are associating oscillators with TensorFlow poses, the 'user key' might be the id of the pose. */ var Pool = class { _resources; _users; capacity; userExpireAfterMs; resourcesWithoutUserExpireAfterMs; capacityPerResource; fullPolicy; generateResource; freeResource; log = logSet(`Pool`); /** * Constructor. * * By default, no capacity limit, one user per resource * @param options Pool options */ constructor(options = {}) { this.capacity = options.capacity ?? -1; this.fullPolicy = options.fullPolicy ?? `error`; this.capacityPerResource = options.capacityPerResource ?? 1; this.userExpireAfterMs = options.userExpireAfterMs ?? -1; this.resourcesWithoutUserExpireAfterMs = options.resourcesWithoutUserExpireAfterMs ?? -1; this.generateResource = options.generate; this.freeResource = options.free; this._users = /* @__PURE__ */ new Map(); this._resources = []; this.log = logSet(`Pool`, options.debug ?? false); const timer = Math.max(this.userExpireAfterMs, this.resourcesWithoutUserExpireAfterMs); if (timer > 0) setInterval(() => { this.maintain(); }, timer * 1.1); } /** * Returns a debug string of Pool state * @returns */ dumpToString() { let r = `Pool capacity: ${this.capacity} userExpireAfterMs: ${this.userExpireAfterMs} capacityPerResource: ${this.capacityPerResource} resources count: ${this._resources.length}`; const resource = this._resources.map((r) => r.toString()).join(`\r\n\t`); r += `\r\nResources:\r\n\t` + resource; r += `\r\nUsers: \r\n`; for (const [k, v] of this._users.entries()) r += `\tk: ${k} v: ${v.toString()}\r\n`; return r; } /** * Sorts users by longest elapsed time since update * @returns */ getUsersByLongestElapsed() { return [...this._users.values()].sort((a, b) => { const aa = a.elapsed; const bb = b.elapsed; if (aa === bb) return 0; if (aa < bb) return 1; return -1; }); } /** * Returns resources sorted with least used first * @returns */ getResourcesSortedByUse() { return [...this._resources].sort((a, b) => { if (a.usersCount === b.usersCount) return 0; if (a.usersCount < b.usersCount) return -1; return 1; }); } /** * Adds a shared resource to the pool * @throws Error if the capacity limit is reached or resource is null * @param resource * @returns */ addResource(resource) { if (resource === void 0) throw new Error(`Cannot add undefined resource`); if (resource === null) throw new TypeError(`Cannot add null resource`); if (this.capacity > 0 && this._resources.length === this.capacity) throw new Error(`Capacity limit (${this.capacity}) reached. Cannot add more.`); this.log.log(`Adding resource: ${JSON.stringify(resource)}`); const pi = new Resource(this, resource); this._resources.push(pi); return pi; } /** * Performs maintenance, removing disposed/expired resources & users. * This is called automatically when using a resource. */ maintain() { let changed = false; const nuke = []; for (const p of this._resources) if (p.isDisposed) { this.log.log(`Maintain, disposed resource: ${JSON.stringify(p.data)}`); nuke.push(p); } else if (p.isExpiredFromUsers) { this.log.log(`Maintain, expired resource: ${JSON.stringify(p.data)}`); nuke.push(p); } if (nuke.length > 0) { for (const resource of nuke) resource.dispose(`diposed/expired`); changed = true; } const userKeysToRemove = []; for (const [key, user] of this._users.entries()) if (!user.isValid) { this.log.log(`Maintain. Invalid user: ${user.key} (Disposed: ${user.isDisposed} Expired: ${user.isExpired} Resource disposed: ${user.resource.isDisposed})`); userKeysToRemove.push(key); user._dispose(`invalid`, user.data); } for (const userKey of userKeysToRemove) { this._users.delete(userKey); changed = true; } if (changed) this.log.log(`End: resource len: ${this._resources.length} users: ${this.usersLength}`); } /** * Iterate over resources in the pool. * To iterate over the data associated with each resource, use * `values`. */ *resources() { const resource = [...this._resources]; for (const r of resource) yield r; } /** * Iterate over resource values in the pool. * to iterate over the resources, use `resources`. * * Note that values may be returned even though there is no * active user. */ *values() { const resource = [...this._resources]; for (const r of resource) yield r.data; } /** * Unassociate a key with a pool item * @param userKey */ release(userKey, reason) { const pi = this._users.get(userKey); if (!pi) return; pi.release(reason ?? `Pool.release`); } /** * @internal * @param user */ _release(user) { this._users.delete(user.key); } /** * @internal * @param resource * @param _ */ _releaseResource(resource, _) { this._resources = this._resources.filter((v) => v !== resource); } /** * Returns true if `v` has an associted resource in the pool * @param resource * @returns */ hasResource(resource) { return this._resources.find((v) => v.data === resource) !== void 0; } /** * Returns true if a given `userKey` is in use. * @param userKey * @returns */ hasUser(userKey) { return this._users.has(userKey); } /** * @internal * @param key * @param resource * @returns */ _assign(key, resource) { const u = new PoolUser(key, resource); this._users.set(key, u); resource._assign(u); return u; } /** * Allocates a resource for `userKey` * @internal * @param userKey * @returns */ #allocateResource(userKey) { const sorted = this.getResourcesSortedByUse(); if (sorted.length > 0 && sorted[0].hasUserCapacity) return this._assign(userKey, sorted[0]); if (this.generateResource && (this.capacity < 0 || this._resources.length < this.capacity)) { this.log.log(`capacity: ${this.capacity} resources: ${this._resources.length}`); const resourceGenerated = this.addResource(this.generateResource()); return this._assign(userKey, resourceGenerated); } } /** * Return the number of users */ get usersLength() { return [...this._users.values()].length; } /** * 'Uses' a resource, returning the value * @param userKey * @returns */ useValue(userKey) { return this.use(userKey).resource.data; } /** * Gets a pool item based on a 'user' key. * * The same key should return the same pool item, * for as long as it still exists. * * If a 'user' already has a resource, it will 'keep alive' their use. * If a 'user' does not already have resource * - if there is capacity, a resource is allocated to user * - if pool is full * - fullPolicy = 'error': an error is thrown * - fullPolicy = 'evictOldestUser': evicts an older user * - Throw error * @param userKey * @throws Error If all resources are used and fullPolicy = 'error' * @returns */ use(userKey) { const pi = this._users.get(userKey); if (pi) { pi.keepAlive(); return pi; } this.maintain(); const match = this.#allocateResource(userKey); if (match) return match; if (this.fullPolicy === `error`) throw new Error(`Pool is fully used (fullPolicy: ${this.fullPolicy}, capacity: ${this.capacity})`); if (this.fullPolicy === `evictOldestUser`) { const users = this.getUsersByLongestElapsed(); if (users.length > 0) { this.release(users[0].key, `evictedOldestUser`); const match2 = this.#allocateResource(userKey); if (match2) return match2; } } throw new Error(`Pool is fully used (${this.fullPolicy})`); } }; /** * Creates an instance of a Pool * @param options * @returns */ const create = (options = {}) => new Pool(options); //#endregion //#region ../packages/flow/src/promise-with-resolvers.ts /** * Creates a new Promise, returning the promise * along with its resolve and reject functions. * * ```js * const { promise, resolve, reject } = promiseWithResolvers(); * * setTimeout(() => { * resolve(); * }, 1000); * * await promise; * ``` * * Promise would be passed somewhere that expects a promise, * and you're free to call `resolve` or `reject` when needed. * @returns */ function promiseWithResolvers() { let resolve; let reject; return { promise: new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }), resolve, reject }; } //#endregion //#region ../packages/flow/src/timeout.ts /** * Returns a {@link Timeout} that can be triggered, cancelled and reset. Use {@link continuously} for interval- * based loops. * * Once `start()` is called, `callback` will be scheduled to execute after `interval`. * If `start()` is called again, the waiting period will be reset to `interval`. * * @example Essential functionality * ```js * const fn = () => { * console.log(`Executed`); * }; * const t = timeout(fn, 60*1000); * t.start(); // After 1 minute `fn` will run, printing to the console * ``` * * @example Control execution functionality * ``` * t.cancel(); // Cancel it from running * t.start(); // Schedule again after 1 minute * t.start(30*1000); // Cancel that, and now scheduled after 30s * * // Get the current state of timeout * t.runState; // "idle", "scheduled" or "running" * ``` * * Callback function receives any additional parameters passed in from start. This can be useful for passing through event data: * * @example * ```js * const t = timeout( (elapsedMs, ...args) => { * // args contains event data * }, 1000); * el.addEventListener(`click`, t.start); * ``` * * Asynchronous callbacks can be used as well: * ```js * timeout(async () => {...}, 100); * ``` * * If you don't expect to need to control the timeout, consider using {@link delay}, * which can run a given function after a specified delay. * @param callback * @param interval * @returns {@link Timeout} */ const timeout = (callback, interval) => { if (callback === void 0) throw new Error(`callback parameter is undefined`); resultThrow(integerTest(intervalToMs(interval), `aboveZero`, `interval`)); let timer; let startedAt = 0; let startCount = 0; let startCountTotal = 0; let state = `idle`; const clear = () => { startedAt = 0; globalThis.clearTimeout(timer); state = `idle`; }; const start = async (altInterval = interval, args) => { return new Promise((resolve, reject) => { startedAt = performance.now(); const altTimeoutMs = intervalToMs(altInterval); const it = integerTest(altTimeoutMs, `aboveZero`, `altTimeoutMs`); if (resultIsError(it)) { reject(resultToError(it)); return; } switch (state) { case `scheduled`: cancel(); break; case `running`: break; } state = `scheduled`; timer = globalThis.setTimeout(async () => { if (state !== `scheduled`) { console.warn(`Timeout skipping execution since state is not 'scheduled'`); clear(); return; } const args_ = args ?? []; startCount++; startCountTotal++; state = `running`; await callback(performance.now() - startedAt, ...args_); state = `idle`; clear(); resolve(); }, altTimeoutMs); }); }; const cancel = () => { if (state === `idle`) return; clear(); }; return { start, cancel, get runState() { return state; }, get startCount() { return startCount; }, get startCountTotal() { return startCountTotal; } }; }; //#endregion //#region ../packages/flow/src/rate-minimum.ts /** * Ensures that `whatToCall` is executed with a given tempo. * * ```js * const rm = rateMinimum({ * fallback: () => { * return Math.random(); * }, * whatToCall: (value:number) => { * console.log(value); * }, * interval: { secs: 10 } * }); * * // Invokes `whatToCall`, resetting timeout * rm(10); * * // If we don't call rm() before 'interval' has elapsed, * // 'fallback' will be invoked * ``` * * A practical use for this is to update calculations based on firing of events * as well as when they don't fire. For example user input. * * ```js * // Average distances * const average = movingAverageLight(); * const rm = rateMinimum({ * interval: { secs: 1 }, * whatToCall: (distance: number) => { * average(distance); * }, * // If there are no pointermove events, distance is 0 * fallback() { * return 0; * } * }) * * // Report total movemeent * document.addEventListener(`pointermove`, event => { * rm(event.movementX + event.movementY); * }); * ``` * * @param options * @returns */ const rateMinimum = (options) => { let disposed = false; const t = timeout(() => { if (disposed) return; t.start(); options.whatToCall(options.fallback()); }, options.interval); if (options.abort) options.abort.addEventListener(`abort`, (_) => { disposed = true; t.cancel(); }); t.start(); return (args) => { if (disposed) throw new Error(`AbortSignal has been fired`); t.start(); options.whatToCall(args); }; }; //#endregion //#region ../packages/flow/src/repeat.ts /** * Generates values from `produce` with a time delay. * `produce` can be a simple function that returns a value, an async function, or a generator. * If `produce` returns _undefined_, generator exits. * * @example * Produce a random number every 500ms * ```js * const randomGenerator = repeat(() => Math.random(), 500); * for await (const r of randomGenerator) { * // Random value every 1 second * // Warning: does not end by itself, a `break` statement is needed * } * ``` * * @example * Return values from a generator every 500ms * ```js * import { repeat } from '@ixfx/flow.js' * import { count } from '@ixfx/numbers.js' * for await (const v of repeat(count(10), { fixed: 1000 })) { * // Do something with `v` * } * ``` * * Options allow either fixed interval (wait this long between iterations), or a minimum interval (wait at least this long). The latter is useful if `produce` takes some time - it will only wait the remaining time or not at all. * * If the AbortSignal is triggered, an exception will be thrown, stopping iteration. * * @see {@link continuously}: loop that runs at a constant speed. Able to be started and stopped * @see {@link repeat}: run a function a certain number of times, collecting results * * @param produce Function/generator to use * @param opts * @typeParam T - Data type * @returns Returns value of `produce` function */ async function* repeat(produce, opts) { const signal = opts.signal ?? void 0; const delayWhen = opts.delayWhen ?? `before`; const count = opts.count ?? void 0; const allowUndefined = opts.allowUndefined ?? false; const minIntervalMs = opts.delayMinimum ? intervalToMs(opts.delayMinimum) : void 0; const whileFunction = opts.while; let cancelled = false; let sleepMs = intervalToMs(opts.delay, intervalToMs(opts.delayMinimum, 0)); let started = performance.now(); const doDelay = async () => { const elapsed = performance.now() - started; if (typeof minIntervalMs !== `undefined`) sleepMs = Math.max(0, minIntervalMs - elapsed); if (sleepMs) await sleep({ millis: sleepMs, signal }); started = performance.now(); if (signal?.aborted) throw new Error(`Signal aborted ${signal.reason}`); }; if (Array.isArray(produce)) produce = produce.values(); if (opts.onStart) opts.onStart(); let errored = true; let loopedTimes = 0; try { while (!cancelled) { loopedTimes++; if (delayWhen === `before` || delayWhen === `both`) await doDelay(); const result = await resolve(produce); if (typeof result === `undefined` && !allowUndefined) cancelled = true; else { yield result; if (delayWhen === `after` || delayWhen === `both`) await doDelay(); if (count !== void 0 && loopedTimes >= count) cancelled = true; } if (whileFunction) { if (!whileFunction(loopedTimes)) cancelled = true; } } errored = false; } finally { cancelled = true; if (opts.onComplete) opts.onComplete(errored); } } /** * Generates values from `produce` with a time delay. * `produce` can be a simple function that returns a value, an function, or a generator. * If `produce` returns _undefined_, generator exits. * * This is the synchronous version. {@link repeat} allows for delays between loops * as well as asynchronous callbacks. * * If the AbortSignal is triggered, an exception will be thrown, stopping iteration. * * @param produce Function/generator to use * @param opts Options * @typeParam T - Data type * @returns Returns value of `produce` function */ function* repeatSync(produce, opts) { const signal = opts.signal ?? void 0; const count = opts.count ?? void 0; const allowUndefined = opts.allowUndefined ?? false; let cancelled = false; if (Array.isArray(produce)) produce = produce.values(); if (opts.onStart) opts.onStart(); let errored = true; let loopedTimes = 0; try { while (!cancelled) { loopedTimes++; const result = resolveSync(produce); if (typeof result === `undefined` && !allowUndefined) cancelled = true; else { yield result; if (count !== void 0 && loopedTimes >= count) cancelled = true; if (signal?.aborted) cancelled = true; } } errored = false; } finally { cancelled = true; if (opts.onComplete) opts.onComplete(errored); } } /** * Logic for continuing repeats */ /** * Calls and waits for the async function `fn` repeatedly, yielding each result asynchronously. * Use {@link repeat} if `fn` does not need to be awaited. * * ```js * // Eg. iterate * const r = Flow.repeat(5, async () => Math.random()); * for await (const v of r) { * * } * // Eg read into array * const results = await Array.fromAsync(Flow.repeatAwait(5, async () => Math.random())); * ``` * * The number of repeats is determined by the first parameter. If it's a: * - number: how many times to repeat * - function: it gets called before each repeat, if it returns _false_ repeating stops. * * Using a fixed number of repeats: * ```js * // Calls - and waits - for Flow.sleep(1) 5 times * await Flow.repeatAwait(5, async () => { * // some kind of async function where we can use await * // eg. sleep for 1s * await Flow.sleep(1); * }); * ``` * * Using a function to dynamically determine number of repeats. The function gets * passed the number of repeats so far as well as the number of values produced. This * is count of non-undefined results from `cb` that is being repeated. * * ```js * async function task() { * // do something * } * * await Flow.repeatAwait( * (repeats, valuesProduced) => { * // Logic for deciding whether to repeat or not * if (repeats > 5) return false; // Stop repeating * }, * task * ); * ``` * * In the above cases we're not using the return value from `fn`. This would look like: * ```js * const g = Flow.repeatAwait(5, async () => Math.random); * for await (const v of g) { * // Loops 5 times, v is the return value of calling `fn` (Math.random) * } * ``` * @param countOrPredicate Number of times to repeat, or a function that returns _false_ to stop the loop. * @param fn Function to execute. Asynchronous functions will be awited * @typeParam V - Return type of repeating function * @returns Asynchronous generator of `fn` results. */ /** * Calls `fn` repeatedly, yielding each result. * Use {@link repeatAwait} if `fn` is asynchronous and you want to wait for it. * * The number of repeats is determined by the first parameter. If it's a: * - number: how many times to repeat * - function: it gets called before each repeat, if it returns _false_ repeating stops. * * Example: using a fixed number of repeats * ```js * // Results will be an array with five random numbers * const results = [...repeat(5, () => Math.random())]; * * // Or as an generator (note also the simpler expression form) * for (const result of repeat(5, Math.random)) { * } * ``` * * Example: Using a function to dynamically determine number of repeats * ```js * function task() { * } * * Flow.repeat( * (repeats, valuesProduced) => { * if (repeats > 5) return false; // Stop repeating * }, * task * ); * ``` * * In the above cases we're not using the return value from `fn`. To do so, * this would look like: * ```js * const g = Flow.repeat(5, () => Math.random); * for (const v of g) { * // Loops 5 times, v is the return value of calling `fn` (Math.random) * } * ``` * * Alternatives: * * {@link Flow.forEach | Flow.forEach} - if you don't need return values * * {@link Flow.interval} - if you want to repeatedly call something with an interval between * @param countOrPredicate Numnber of repeats, or a function that returns _false_ for when to stop. * @param fn Function to execute. Asynchronous functions will be awited * @typeParam V - Return type of repeating function * @returns Asynchronous generator of `fn` results. */ /** * Calls `fn` until `predicate` returns _false_. Awaits result of `fn` each time. * Yields result of `fn` asynchronously * @param predicate * @param fn * @typeParam V - Return type of repeating function */ /** * Calls `fn` until `predicate` returns _false_. Yields result of `fn`. * @param predicate Determiner for whether repeating continues * @param fn Function to call * @typeParam V - Return type of repeating function */ /** * Calls `fn`, `count` number of times, waiting for the result of `fn`. * Yields result of `fn` asynchronously * @param count Number of times to run * @param fn Function to run * @typeParam V - Return type of repeating function */ /** * Calls `fn`, `count` times. Assumes a synchronous function. Yields result of `fn`. * * Note that if `fn` returns _undefined_ repeats will stop. * @typeParam V - Return type of repeating function * @param count Number of times to run * @param fn Function to run */ /** * Repeatedly calls `fn`, reducing via `reduce`. * * ```js * repeatReduce(10, () => 1, (acc, v) => acc + v); * // Yields: 10 * * // Multiplies random values against each other 10 times * repeatReduce(10, Math.random, (acc, v) => acc * v); * // Yields a single number * ``` * @param countOrPredicate Number of times to run, or function to keep running * @param fn Function to call * @param initial Initial value * @param reduce Function to reduce value * @typeParam V - Return type of repeating function * @returns Final result */ //#endregion //#region ../packages/flow/src/req-resp-match.ts /** * Matches responses with requests, expiring requests if they do not get a response in a timely manner. * * Basic usage: * ```js * const m = new RequestResponseMatch(options); * // Listen for when a response matches a request * m.addEventListener(`match`, event => { * // event: { request:Req, response:Resp} * }); * // Or alternatively, listen for success and failures * m.addEventListener(`completed`, event => { * // { request:Resp, response:Req|undefined, success:boolean } * // 'response' will be data or a string error message * }); * m.request(req); // Note that some request was sent * ... * m.response(resp); // Call when a response is received * ``` * * It's also possible to wait for specific replies: * ```js * // With a promise * const resp = await m.requestAwait(req); * // With a callback * m.requestCallback(req, (success, resp) => { * // Runs on success or failure * }) * ``` * * It relies on creating an id of a request/response for them to be matched up. Use the `key` * option if the function can generate a key from either request or response. * Or alternatively set both `keyRequest` and `keyResponse` for two functions that can generate a key for request and response respectively. * * * The easy case is if req & resp both have the same field: * ```js * const m = new RequestResponseMatch({ * key: (reqOrResp) => { * // Requests has an 'id' field * // Response also has an 'id' field that corresponds to the request id * return reqOrResp.id; * } * }); * ``` * * A more complicated case: * ```js * const m = new RequestResponseMatch({ * keyRequest: (req) => { * // Requests have an 'id' field * return req.id; * }, * keyResponse: (resp) => { * // Responses have id under a different field * return resp.reply_to * } * }) * ``` * * By default, error will be thrown if a response is received that doesn't match up to any request. */ var RequestResponseMatch = class extends SimpleEventEmitter { timeou