ixfx
Version:
Bundle of ixfx libraries
1,747 lines (1,733 loc) • 102 kB
JavaScript
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