UNPKG

ixfx

Version:

Bundle of ixfx libraries

1,695 lines (1,665 loc) 95.4 kB
import { n as __exportAll } from "./chunk-CaR5F9JI.js"; import { A as wildcard, Ct as zipKeyValue, Dt as isEqualValueDefault, G as compareData, H as continuously, Ht as mapObjectShallow, I as elapsedInterval, K as getField, Tt as isEqualContextString, Ut as compareArrays, Y as updateByPath, _t as some, h as intervalToMs, n as sleep } from "./src-BUqDa_u7.js"; import { A as resultIsError, P as resultToError, o as testPlainObjectOrPrimitive } from "./src-C_hvyftg.js"; import { n as getErrorMessage } from "./src-B7f_ks6F.js"; import { _ as shuffle, p as remove, w as insertAt } from "./src-CxEyGbiK.js"; import { g as graph, h as connect, v as QueueMutable } from "./src-DOorb7Rs.js"; import { G as isIterable, U as nextWithTimeout, W as isAsyncIterable } from "./src-F3bdGIjS.js"; import { a as sum$1, i as rank$1, n as max$1, o as tally$1, r as min$1, t as average$1 } from "./basic-Bepd6Tc6.js"; import { P as interpolate$1 } from "./src-Cebc3sfq.js"; import { D as setProperty } from "./src-BoQVyMUt.js"; import { H as DispatchList, a as to$1, i as init, j as timeout } from "./src-C3g81yvt.js"; //#region ../packages/rx/src/from/function.ts /** * Produces a reactive from the basis of a function. `callback` is executed, with its result emitted via the returned reactive. * * ```js * // Produce a random number every second * const r = Rx.From.func(Math.random, { interval: 1000 }); * ``` * * `callback` can be called repeatedly by providing the `interval` option to set the rate of repeat. * Looping can be limited with `options.maximumRepeats`, or passing a signal `options.signal` * and then activating it. * ```js * // Reactive that emits a random number every second, five times * const r1 = Rx.From.func(Math.random, { interval: 1000, maximumRepeats: 5 } * ``` * * ```js * // Generate a random number every second until ac.abort() is called * const ac = new AbortController(); * const r2 = Rx.From.func(Math.random, { interval: 1000, signal: ac.signal }); * ``` * * The third option is for `callback` to fire the provided abort function. * ```js * Rx.From.func((abort) => { * if (Math.random() > 0.5) abort('Random exit'); * return 1; * }); * ``` * * By default has a laziness of 'very' meaning that `callback` is run only when there's a subscriber * By default stream closes if `callback` throws an error. Use `options.closeOnError:'ignore'` to change. * @param callback * @param options * @returns */ function func(callback, options = {}) { const maximumRepeats = options.maximumRepeats ?? Number.MAX_SAFE_INTEGER; const closeOnError = options.closeOnError ?? true; const intervalMs = options.interval ? intervalToMs(options.interval) : -1; let manual = options.manual ?? false; if (options.interval === void 0 && options.manual === void 0) manual = true; if (manual && options.interval) throw new Error(`If option 'manual' is set, option 'interval' cannot be used`); const predelay = intervalToMs(options.predelay, 0); const lazy = options.lazy ?? `very`; const signal = options.signal; const internalAbort = new AbortController(); const internalAbortCallback = (reason) => { internalAbort.abort(reason); }; let sentResults = 0; let enabled = false; const done = (reason) => { events.dispose(reason); enabled = false; if (run) run.cancel(); }; const ping = async () => { if (!enabled) return false; if (predelay) await sleep(predelay); if (sentResults >= maximumRepeats) { done(`Maximum repeats reached ${maximumRepeats.toString()}`); return false; } try { if (signal?.aborted) { done(`Signal (${signal.aborted})`); return false; } const value = await callback(internalAbortCallback); sentResults++; events.set(value); return true; } catch (error) { if (closeOnError) { done(`Function error: ${getErrorMessage(error)}`); return false; } else { events.signal(`warn`, getErrorMessage(error)); return true; } } }; const run = manual ? void 0 : continuously(async () => { if (!await ping()) return false; if (internalAbort.signal.aborted) { done(`callback function aborted (${internalAbort.signal.reason})`); return false; } }, intervalMs); const events = initLazyStream({ lazy, onStart() { enabled = true; if (run) run.start(); }, onStop() { enabled = false; if (run) run.cancel(); } }); if (lazy === `never` && run) run.start(); return { ...events, ping }; } //#endregion //#region ../packages/rx/src/from/iterator.ts /** * Creates a Reactive from an AsyncGenerator or Generator * @param gen * @returns */ /** * Creates a readable reactive based on a (async)generator or iterator * ```js * // Generator a random value every 5 seconds * const valuesOverTime = Flow.interval(() => Math.random(), 5000); * // Wrap the generator * const r = Rx.From.iterator(time); * // Get notified when there is a new value * r.onValue(v => { * console.log(v); * }); * ``` * * Awaiting values could potentially hang code. Thus there is a `readTimeout`, the maximum time to wait for a value from the generator. Default: 5 minutes. * If `signal` is given, this will also cancel waiting for the value. * @param source */ function iterator(source, options = {}) { const lazy = options.lazy ?? `very`; const log = options.traceLifecycle ? (message) => { console.log(`Rx.From.iterator ${message}`); } : (_) => {}; const readIntervalMs = intervalToMs(options.readInterval, 5); const readTimeoutMs = intervalToMs(options.readTimeout, 300 * 1e3); const whenStopped = options.whenStopped ?? `continue`; let iterator; let ourAc; let sm = init({ idle: [`wait_for_next`], wait_for_next: [ `processing_result`, `stopping`, `disposed` ], processing_result: [ `queued`, `disposed`, `stopping` ], queued: [ `wait_for_next`, `disposed`, `stopping` ], stopping: `idle`, disposed: null }, `idle`); const onExternalSignal = () => { log(`onExternalSignal`); ourAc?.abort(options.signal?.reason); }; if (options.signal) options.signal.addEventListener(`abort`, onExternalSignal, { once: true }); const read = async () => { log(`read. State: ${sm.value}`); ourAc = new AbortController(); try { sm = to$1(sm, `wait_for_next`); const v = await nextWithTimeout(iterator, { signal: ourAc.signal, millis: readTimeoutMs }); sm = to$1(sm, `processing_result`); ourAc.abort(`nextWithTimeout completed`); if (v.done) { log(`read v.done true`); events.dispose(`Generator complete`); sm = to$1(sm, `disposed`); } if (sm.value === `stopping`) { log(`read. sm.value = stopping`); sm = to$1(sm, `idle`); return; } if (sm.value === `disposed`) { log(`read. sm.value = disposed`); return; } events.set(v.value); } catch (error) { events.dispose(`Generator error: ${error.toString()}`); return; } if (sm.value === `processing_result`) { sm = to$1(sm, `queued`); log(`scheduling read. State: ${sm.value}`); setTimeout(read, readIntervalMs); } else sm = to$1(sm, `idle`); }; const events = initLazyStream({ ...options, lazy, onStart() { log(`onStart state: ${sm.value} whenStopped: ${whenStopped}`); if (sm.value !== `idle`) return; if (sm.value === `idle` && whenStopped === `reset` || iterator === void 0) iterator = isAsyncIterable(source) ? source[Symbol.asyncIterator]() : source[Symbol.iterator](); read(); }, onStop() { log(`onStop state: ${sm.value} whenStopped: ${whenStopped}`); sm = to$1(sm, `stopping`); if (whenStopped === `reset`) { log(`onStop reiniting iterator`); iterator = isAsyncIterable(source) ? source[Symbol.asyncIterator]() : source[Symbol.iterator](); } }, onDispose(reason) { log(`onDispose (${reason})`); ourAc?.abort(`Rx.From.iterator disposed (${reason})`); if (options.signal) options.signal.removeEventListener(`abort`, onExternalSignal); } }); return events; } //#endregion //#region ../packages/rx/src/util.ts function messageIsSignal(message) { if (message.value !== void 0) return false; if (`signal` in message && message.signal !== void 0) return true; return false; } function messageIsDoneSignal(message) { if (message.value !== void 0) return false; if (`signal` in message && message.signal === `done`) return true; return false; } /** * Returns _true_ if `v` has a non-undefined value. Note that sometimes * _undefined_ is a legal value to pass * @param v * @returns */ function messageHasValue(v) { if (v.value !== void 0) return true; return false; } const isPingable = (rx) => { if (!isReactive(rx)) return false; if (`ping` in rx) return true; return false; }; const hasLast = (rx) => { if (!isReactive(rx)) return false; if (`last` in rx) { if (rx.last() !== void 0) return true; } return false; }; /** * Returns _true_ if `rx` is a Reactive * @param rx * @returns */ const isReactive = (rx) => { if (typeof rx !== `object`) return false; if (rx === null) return false; if (!(`on` in rx) || !(`onValue` in rx)) return false; return typeof rx.on === "function" && typeof rx.onValue === "function"; }; /** * Returns true if `rx` is a disposable reactive. * @param rx * @returns */ /** * Returns _true_ if `rx` is a writable Reactive * @param rx * @returns */ const isWritable = (rx) => { if (!isReactive(rx)) return false; if (`set` in rx) return true; return false; }; const isWrapped = (v) => { if (typeof v !== `object`) return false; if (v === null) return false; if (!(`source` in v)) return false; if (!(`annotate` in v)) return false; return true; }; const opify = (fn, ...args) => { return (source) => { return fn(source, ...args); }; }; const isTriggerValue = (t) => `value` in t; const isTriggerFunction = (t) => `fn` in t; const isTriggerGenerator = (t) => isIterable(t); const isTrigger = (t) => { if (typeof t !== `object`) return false; if (isTriggerValue(t)) return true; if (isTriggerFunction(t)) return true; if (isTriggerGenerator(t)) return true; return false; }; /** * Resolves a trigger value. * * A trigger can be a value, a function or generator. Value triggers never complete. * A trigger function is considered complete if it returns undefined. * A trigger generator is considered complete if it returns done. * * Returns `[value, _false_]` if we have a value and trigger is not completed. * Returns `[value, _true_]` trigger is completed * @param t * @returns */ function resolveTriggerValue(t) { if (isTriggerValue(t)) return [t.value, false]; if (isTriggerFunction(t)) { const v = t.fn(); if (v === void 0) return [void 0, true]; return [v, false]; } if (isTriggerGenerator(t)) { const v = t.gen.next(); if (v.done) return [void 0, true]; return [v.value, false]; } throw new Error(`Invalid trigger. Missing 'value' or 'fn' fields`); } //#endregion //#region ../packages/rx/src/resolve-source.ts /** * Resolves various kinds of sources into a Reactive. * If `source` is an iterable/generator, it gets wrapped via `generator()`. * * Default options: * * generator: `{ lazy: 'initial', interval: 5 }` * * function: `{ lazy: 'very' }` * @param source * @returns */ const resolveSource = (source, options = {}) => { if (isReactive(source)) return source; const generatorOptions = options.generator ?? { lazy: `initial`, interval: 5 }; const functionOptions = options.function ?? { lazy: `very` }; if (Array.isArray(source)) return iterator(source.values(), generatorOptions); else if (typeof source === `function`) return func(source, functionOptions); else if (typeof source === `object`) { if (isWrapped(source)) return source.source; if (isIterable(source) || isAsyncIterable(source)) return iterator(source, generatorOptions); } throw new TypeError(`Unable to resolve source. Supports: array, Reactive, Async/Iterable. Got type: ${typeof source}`); }; //#endregion //#region ../packages/rx/src/cache.ts /** * Wrapes an input stream to cache values, optionally providing an initial value * @param r * @param initialValue * @returns */ function cache(r, initialValue) { let lastValue = initialValue; r.onValue((value) => { lastValue = value; }); return { ...r, last() { return lastValue; }, resetCachedValue() { lastValue = void 0; } }; } //#endregion //#region ../packages/rx/src/init-stream.ts /** * Initialise a stream based on an upstream source. * Calls initLazyStream under the hood. * * Options: * * onValue: called when upstream emits a value (default: does nothing with upstream value) * * lazy: laziness of stream (default: 'initial') * * disposeIfSourceDone: disposes stream if upstream disposes (default: true) * @ignore * @param upstreamSource * @param options * @returns */ function initUpstream(upstreamSource, options) { const lazy = options.lazy ?? `initial`; const disposeIfSourceDone = options.disposeIfSourceDone ?? true; const onValue = options.onValue ?? ((_v) => { /** no-op */}); const source = resolveSource(upstreamSource); let unsub; const debugLabel = options.debugLabel ? `[${options.debugLabel}]` : ``; const onStop = () => { if (unsub === void 0) return; unsub(); unsub = void 0; if (options.onStop) options.onStop(); }; const onStart = () => { if (unsub !== void 0) return; if (options.onStart) options.onStart(); unsub = source.on((value) => { if (messageIsSignal(value)) if (value.signal === `done`) { onStop(); events.signal(value.signal, value.context); if (disposeIfSourceDone) events.dispose(`Upstream source ${debugLabel} has completed (${value.context ?? ``})`); } else events.signal(value.signal, value.context); else if (messageHasValue(value)) onValue(value.value); }); }; const events = initLazyStream({ ...options, lazy, onStart, onStop }); return events; } /** * Initialises a lazy stream with an initial value. * Uses {@link initLazyStream} and {@link cache} together. * @param options * @returns */ function initLazyStreamWithInitial(options) { return cache(initLazyStream(options), options.initialValue); } /** * Initialises a lazy stream * Consider also: {@link initLazyStreamWithInitial} * * @param options * @returns */ function initLazyStream(options) { const lazy = options.lazy ?? `initial`; const onStop = options.onStop ?? (() => {}); const onStart = options.onStart ?? (() => {}); options.debugLabel && `${options.debugLabel}`; const events = initStream({ ...options, onFirstSubscribe() { if (lazy !== `never`) onStart(); }, onNoSubscribers() { if (lazy === `very`) onStop(); } }); if (lazy === `never`) onStart(); return events; } /** * Initialises a new stream. * * Options: * * onFirstSubscribe: Called when there is a subscriber after there have been no subscribers. * * onNoSubscribers: Called when there are no more subscribers. 'onFirstSubscriber' will be called next time a subscriber is added. * * Alternatives: * * {@link initLazyStream} - a stream with callbacks for when there is some/none subscribers * @ignore * @param options * @returns */ function initStream(options = {}) { let dispatcher; let disposed = false; let firstSubscribe = false; let emptySubscriptions = true; const onFirstSubscribe = options.onFirstSubscribe ?? void 0; const onNoSubscribers = options.onNoSubscribers ?? void 0; const debugLabel = options.debugLabel ? `[${options.debugLabel}]` : ``; const isEmpty = () => { if (dispatcher === void 0) return; if (!dispatcher.isEmpty) return; if (!emptySubscriptions) { emptySubscriptions = true; firstSubscribe = false; if (onNoSubscribers) onNoSubscribers(); } }; const subscribe = (handler) => { if (disposed) throw new Error(`Disposed, cannot subscribe ${debugLabel}`); if (dispatcher === void 0) dispatcher = new DispatchList(); const id = dispatcher.add(handler); emptySubscriptions = false; if (!firstSubscribe) { firstSubscribe = true; if (onFirstSubscribe) onFirstSubscribe(); } return () => { dispatcher?.remove(id); isEmpty(); }; }; return { dispose: (reason) => { if (disposed) return; dispatcher?.notify({ value: void 0, signal: `done`, context: `Disposed: ${reason}` }); disposed = true; if (options.onDispose) options.onDispose(reason); }, isDisposed: () => { return disposed; }, removeAllSubscribers: () => { dispatcher?.clear(); isEmpty(); }, set: (v) => { if (disposed) throw new Error(`${debugLabel} Disposed, cannot set`); dispatcher?.notify({ value: v }); }, signal: (signal, context) => { if (disposed) throw new Error(`${debugLabel} Disposed, cannot signal`); dispatcher?.notify({ signal, value: void 0, context }); }, on: (handler) => subscribe(handler), onValue: (handler) => { return subscribe((message) => { if (messageHasValue(message)) handler(message.value); }); } }; } //#endregion //#region ../packages/rx/src/to-readable.ts /*** * Returns a read-only version of `stream` */ const toReadable = (stream) => ({ on: stream.on, dispose: stream.dispose, isDisposed: stream.isDisposed, onValue: stream.onValue }); //#endregion //#region ../packages/rx/src/ops/annotate.ts /** * Annotates values from `source`. Output values will be * in the form `{ value: TIn, annotation: TAnnotation }`. * Where `TIn` is the type of the input, and `TAnnotation` is * the return type of the annotator function. * * Example calculating area from width & height: * ```js * const data = Rx.From.array( * { w: 1, h: 3 }, { w: 1, h: 1 }, { w: 2, h: 2 } * ); * const annotated = Rx.Ops.annotate(data, v => { * return { area: v.w * v.h } * }); * const data = await Rx.toArray(annotated); * // Data = [ { value: { w:1, h:3 }, annotation: { area:3 } } ...] * ``` * * If you would rather annotate and have values merge with the input, * use `transform`: * ```js * const data = Rx.From.array( * { w: 1, h: 3 }, { w: 1, h: 1 }, { w: 2, h: 2 } * ); * const withArea = Rx.Ops.transform(data, v => { * return { ...v, area: v.w * v.h } * }); * const data = await Rx.toArray(withArea); * // Data = [ { w:1, h:3, area:3 }, ...] * ``` */ function annotate(input, annotator, options = {}) { const upstream = initUpstream(input, { ...options, onValue(value) { const annotation = annotator(value); upstream.set({ value, annotation }); } }); return toReadable(upstream); } /** * Annotates the input stream using {@link ReactiveOp} as the source of annotations. * The output values will have the shape of `{ value: TIn, annotation: TAnnotation }`. * Meaning that the original value is stored under `.value`, and the annotation under `.annotation`. * * ```js * const data = Rx.From.array([ 1, 2, 3 ]); * const annotated = Rx.Ops.annotateWithOp(data, Rx.Ops.sum()); * const data = await annotated.toArray(annotated); * // Data = [ { value: 1, annotation: 1 }, { value: 2, annotation: 3 }, { value: 3, annotation: 6 } ] * ``` * @param annotatorOp Operator to generate annotations * @param input Input stream * @returns */ function annotateWithOp(input, annotatorOp) { const inputStream = resolveSource(input); return syncToObject({ value: inputStream, annotation: annotatorOp(inputStream) }); } //#endregion //#region ../packages/rx/src/ops/chunk.ts /** * Queue from `source`, emitting when thresholds are reached. * The resulting Reactive produces arrays. * * Can use a combination of elapsed time or number of data items. * * By default options are OR'ed together. * * ```js * // Emit data in chunks of 5 items * chunk(source, { quantity: 5 }); * // Emit a chunk of data every second * chunk(source, { elapsed: 1000 }); * ``` * @param source * @param options * @returns */ function chunk(source, options = {}) { const queue = new QueueMutable(); const quantity = options.quantity ?? 0; const returnRemainder = options.returnRemainder ?? true; const upstream = initUpstream(source, { ...options, onStop() { if (returnRemainder && !queue.isEmpty) { const data = queue.toArray(); queue.clear(); upstream.set(data); } }, onValue(value) { queue.enqueue(value); if (quantity > 0 && queue.length >= quantity) send(); if (timer !== void 0 && timer.runState === `idle`) timer.start(); } }); const send = () => { if (queue.isEmpty) return; if (timer !== void 0) timer.start(); const data = queue.toArray(); queue.clear(); setTimeout(() => { upstream.set(data); }); }; const timer = options.elapsed ? timeout(send, options.elapsed) : void 0; return toReadable(upstream); } //#endregion //#region ../packages/rx/src/ops/transform.ts /** * Transforms values from `source` using the `transformer` function. * @param transformer * @returns */ function transform(input, transformer, options = {}) { const traceInput = options.traceInput ?? false; const traceOutput = options.traceOutput ?? false; const upstream = initUpstream(input, { lazy: `initial`, ...options, onValue(value) { const t = transformer(value); if (traceInput && traceOutput) console.log(`Rx.Ops.transform input: ${JSON.stringify(value)} output: ${JSON.stringify(t)}`); else if (traceInput) console.log(`Rx.Ops.transform input: ${JSON.stringify(value)}`); else if (traceOutput) console.log(`Rx.Ops.transform output: ${JSON.stringify(t)}`); upstream.set(t); } }); return toReadable(upstream); } //#endregion //#region ../packages/rx/src/ops/clone-from-fields.ts /** * Create a new object from input, based on cloning fields rather than a destructured copy. * This is useful for event args. * @param source * @returns */ const cloneFromFields = (source) => { return transform(source, (v) => { const entries = []; for (const field in v) { const value = v[field]; if (value === null || Array.isArray(value) || testPlainObjectOrPrimitive(value).success) entries.push([field, value]); } return Object.fromEntries(entries); }); }; //#endregion //#region ../packages/rx/src/ops/combine-latest-to-array.ts /** * Monitors input reactive values, storing values as they happen to an array. * Whenever a new value is emitted, the whole array is sent out, containing current * values from each source, or _undefined_ if not yet emitted. * * See {@link combineLatestToObject} to combine streams by name into an object, rather than array. * * ``` * const sources = [ * Rx.fromFunction(Math.random, { loop: true, interval: 100 }), * Rx.fromFunction(Math.random, { loop: true, interval: 200 }) * ]; * const r = Rx.combineLatestToArray(sources); * r.onValue(value => { * // Value will be an array of last value from each source: * // [number,number] * }); * ``` * * The tempo of this stream will be set by the fastest source stream. * See {@link syncToArray} to have pace determined by slowest source, and only * send when each source has produce a new value compared to last time. * * Set `onSourceDone` to choose behaviour if a source stops. By default it * is 'break', meaning the whole merged stream stops. * * Note: unlike RxJS's `combineLatest`, does not wait for each source to emit once * before emitting first value. * @param reactiveSources Sources to merge * @param options Options for merging * @returns */ function combineLatestToArray(reactiveSources, options = {}) { const event = initStream(); const onSourceDone = options.onSourceDone ?? `break`; const data = []; const sources = reactiveSources.map((source) => resolveSource(source)); const noop = () => { /** no-op */}; const sourceOff = sources.map((_) => noop); const doneSources = sources.map((_) => false); const unsub = () => { for (const v of sourceOff) v(); }; for (const [index, v] of sources.entries()) { data[index] = void 0; sourceOff[index] = v.on((message) => { if (messageIsDoneSignal(message)) { doneSources[index] = true; sourceOff[index](); sourceOff[index] = noop; if (onSourceDone === `break`) { unsub(); event.dispose(`Source has completed and 'break' is set`); return; } if (!doneSources.includes(false)) { unsub(); event.dispose(`All sources completed`); } } else if (messageHasValue(message)) { data[index] = message.value; event.set([...data]); } }); } return { dispose: event.dispose, isDisposed: event.isDisposed, on: event.on, onValue: event.onValue }; } //#endregion //#region ../packages/rx/src/from/object.ts /** * Creates a Reactive wrapper with the shape of the input object. * * Changing the wrapped object directly does not update the Reactive. * Instead, to update values use: * * `set()`, 'resets' the whole object * * `update()` changes a particular field * * Consider using {@link From.objectProxy} to return a object with properties that can be * set in the usual way yet is also Reactive. * * ```js * const o = Rx.From.object({ name: `bob`, level: 2 }); * o.onValue(changed => { * }); * o.set({ name: `mary`, level: 3 }); * * // `onValue` will get called, with `changed` having a value of: * // { name: `mary`, level: 3 } * ``` * * Use `last()` to get the most recently set value. * * `onDiff` subscribes to a rough diff of the object. * * ```js * const o = Rx.From.object({ name: `bob`, level: 2 }); * o.onDiff(diffValue => { * const diff = diffValue.value; * }) * o.set({ name: `mary`, level: 3 }); * * // onDiff would fire with `diff` of: * [ * { path: `name`, previous: `bob`, value: `mary` }, * { path: `level`, previous: 2, value: 3 } * ] * ``` * * You can also listen to updates on a field via `onField`. * ```js * o.onField(`name`, value => { * // Called whenever the 'name' field is updated * }); * ``` * @param initialValue Initial value * @param options Options * @returns */ function object(initialValue, options = {}) { const eq = options.eq ?? isEqualContextString; const setEvent = initStream(); const diffEvent = initStream(); const fieldChangeEvents = []; let value = initialValue; let disposed = false; const set = (v) => { const diff = [...compareData(value ?? {}, v, { ...options, includeMissingFromA: true })]; if (diff.length === 0) return; value = v; setEvent.set(v); diffEvent.set(diff); }; const fireFieldUpdate = (field, value) => { for (const [matcher, pattern, list] of fieldChangeEvents) if (matcher(field)) list.notify({ fieldName: field, pattern, value }); }; const updateCompareOptions = { asPartial: true, includeParents: true }; const update = (toMerge) => { if (value === void 0) { value = toMerge; setEvent.set(value); for (const [k, v] of Object.entries(toMerge)) fireFieldUpdate(k, v); return value; } else { const diff = [...compareData(value, toMerge, updateCompareOptions)]; if (diff.length === 0) return value; value = { ...value, ...toMerge }; setEvent.set(value); diffEvent.set(diff); for (const d of diff) fireFieldUpdate(d.path, d.value); return value; } }; const updateField = (path, valueForField) => { if (value === void 0) throw new Error(`Cannot update value when it has not already been set`); const existing = getField(value, path); if (resultIsError(existing)) throw resultToError(existing); if (eq(existing.value, valueForField, path)) return; let diff = [...compareData(existing.value, valueForField, { ...options, includeMissingFromA: true })]; diff = diff.map((d) => { if (d.path.length > 0) return { ...d, path: path + `.` + d.path }; return { ...d, path }; }); const o = updateByPath(value, path, valueForField, true); value = o; setEvent.set(o); diffEvent.set(diff); fireFieldUpdate(path, valueForField); }; const dispose = (reason) => { if (disposed) return; diffEvent.dispose(reason); setEvent.dispose(reason); disposed = true; }; return { dispose, isDisposed() { return disposed; }, updateField, last: () => value, on: setEvent.on, onValue: setEvent.onValue, onDiff: diffEvent.onValue, onField(fieldPattern, handler) { const matcher = wildcard(fieldPattern); const listeners = new DispatchList(); fieldChangeEvents.push([ matcher, fieldPattern, listeners ]); const id = listeners.add(handler); return () => listeners.remove(id); }, set, update }; } //#endregion //#region ../packages/rx/src/ops/combine-latest-to-object.ts /** * Monitors input reactive values, storing values as they happen to an object. * Whenever a new value is emitted, the whole object is sent out, containing current * values from each source (or _undefined_ if not yet emitted) * * See {@link combineLatestToArray} to combine streams by name into an array instead. * * ``` * const sources = { * fast: Rx.fromFunction(Math.random, { loop: true, interval: 100 }), * slow: Rx.fromFunction(Math.random, { loop: true, interval: 200 }) * ]; * const r = Rx.combineLatestToObject(sources); * r.onValue(value => { * // 'value' will be an object containing the labelled latest * // values from each source. * // { fast: number, slow: number } * }); * ``` * * The tempo of this stream will be set by the fastest source stream. * See {@link syncToObject} to have pace determined by slowest source, and only * send when each source has produce a new value compared to last time. * * This source ends if all source streams end. * @param reactiveSources Sources to merge * @param options Options for merging * @returns */ function combineLatestToObject(reactiveSources, options = {}) { const disposeSources = options.disposeSources ?? true; const event = object(void 0); const onSourceDone = options.onSourceDone ?? `break`; const emitInitial = options.emitInitial ?? true; let emitInitialDone = false; const states = /* @__PURE__ */ new Map(); for (const [key, source] of Object.entries(reactiveSources)) { const initialData = `last` in source ? source.last() : void 0; const s = { source: resolveSource(source), done: false, data: initialData, off: () => { /** no-op */} }; states.set(key, s); } const sources = Object.fromEntries(Object.entries(states).map((entry) => [entry[0], entry[1].source])); const someUnfinished = () => some(states, (v) => !v.done); const unsub = () => { for (const state of states.values()) state.off(); }; const getData = () => { const r = {}; for (const [key, state] of states) if (state.data !== void 0) r[key] = state.data; return r; }; const trigger = () => { emitInitialDone = true; const d = getData(); event.set(d); }; const wireUpState = (state) => { state.off = state.source.on((message) => { if (messageIsDoneSignal(message)) { state.done = true; state.off(); state.off = () => { /**no-op */}; if (onSourceDone === `break`) { unsub(); event.dispose(`Source has completed and 'break' is behaviour`); return; } if (!someUnfinished()) { unsub(); event.dispose(`All sources completed`); } } else if (messageHasValue(message)) { state.data = message.value; trigger(); } }); }; for (const state of states.values()) wireUpState(state); if (!emitInitialDone && emitInitial) trigger(); return { ...event, hasSource(field) { return states.has(field); }, replaceSource(field, source) { const state = states.get(field); if (state === void 0) throw new Error(`Field does not exist: '${field}'`); state.off(); state.source = resolveSource(source); wireUpState(state); }, setWith(data) { const written = {}; for (const [key, value] of Object.entries(data)) { const state = states.get(key); if (state !== void 0) { if (isWritable(state.source)) { state.source.set(value); written[key] = value; } state.data = value; } } return written; }, sources, last() { return getData(); }, dispose(reason) { unsub(); event.dispose(reason); if (disposeSources) for (const v of states.values()) v.source.dispose(`Part of disposed mergeToObject`); } }; } //#endregion //#region ../packages/rx/src/ops/compute-with-previous.ts /** * When there is a value from `input`, or the reactive is pinged, * this reactive emits the result of `fn`. * * `fn` is provided the previous value as well as the most recent value. * * If no previous value is available, the current value is emitted and `fn` is not called. * @param input * @param fn * @returns */ function computeWithPrevious(input, fn) { let previousValue; let currentValue; if (hasLast(input)) currentValue = previousValue = input.last(); const trigger = () => { if (previousValue === void 0 && currentValue !== void 0) { previousValue = currentValue; upstream.set(previousValue); } else if (previousValue !== void 0 && currentValue !== void 0) { const vv = fn(previousValue, currentValue); previousValue = vv; upstream.set(vv); } }; const upstream = initUpstream(input, { lazy: "very", debugLabel: `computeWithPrevious`, onValue(value) { currentValue = value; trigger(); } }); if (currentValue) trigger(); return { ...toReadable(upstream), ping: () => { if (currentValue !== void 0) trigger(); } }; } //#endregion //#region ../packages/rx/src/reactives/debounce.ts /** * Debounce waits for `elapsed` time after the last received value before emitting it. * * If a flurry of values are received that are within the interval, it won't emit anything. But then * as soon as there is a gap in the messages that meets the interval, the last received value is sent out. * * `debounce` always emits with at least `elapsed` as a delay after a value received. While {@link throttle} potentially * sends immediately, if it's outside of the elapsed period. * * This is a subtly different logic to {@link throttle}. `throttle` more eagerly sends the first value, potentially * not sending later values. `debouce` however will send later values, potentially ignoring earlier ones. * @param source * @param options * @returns */ function debounce$1(source, options = {}) { const elapsed = intervalToMs(options.elapsed, 50); let lastValue; const timer = timeout(() => { const v = lastValue; if (v) { upstream.set(v); lastValue = void 0; } }, elapsed); const upstream = initUpstream(source, { ...options, onValue(value) { lastValue = value; timer.start(); } }); return toReadable(upstream); } //#endregion //#region ../packages/rx/src/ops/debounce.ts function debounce(options) { return (source) => { return debounce$1(source, options); }; } //#endregion //#region ../packages/rx/src/ops/elapsed.ts /** * Emits time in milliseconds since last message. * If it is the first value, 0 is used. * @param input * @returns */ const elapsed = (input) => { let last = 0; return transform(input, (_ignored) => { const elapsed = last === 0 ? 0 : Date.now() - last; last = Date.now(); return elapsed; }); }; //#endregion //#region ../packages/rx/src/ops/field.ts /** * From a source value, yields a field from it. Only works * if stream values are objects. * * If a source value doesn't have that field, it is skipped. * * @returns */ function field(fieldSource, fieldName, options = {}) { const fallbackFieldValue = options.fallbackFieldValue; const fallbackObject = options.fallbackObject; const upstream = initUpstream(fieldSource, { disposeIfSourceDone: true, ...options, onValue(value) { let v; if (fieldName in value) v = value[fieldName]; else if (fallbackObject && fieldName in fallbackObject) v = fallbackObject[fieldName]; if (v === void 0) v = fallbackFieldValue; if (v !== void 0) upstream.set(v); } }); return toReadable(upstream); } //#endregion //#region ../packages/rx/src/ops/filter.ts /** * Passes all values where `predicate` function returns _true_. */ function filter(input, predicate, options) { if (typeof predicate !== `function`) throw new TypeError(`Param 'predicate' should be a function`); const upstream = initUpstream(input, { ...options, onValue(value) { if (predicate(value)) upstream.set(value); } }); return toReadable(upstream); } /** * Drops all values where `predicate` function returns _true_. */ function drop(input, predicate, options) { if (typeof predicate !== `function`) throw new TypeError(`Param 'predicate' should be a function`); const upstream = initUpstream(input, { ...options, onValue(value) { if (!predicate(value)) upstream.set(value); } }); return toReadable(upstream); } //#endregion //#region ../packages/rx/src/ops/interpolate.ts /** * Interpolates to the source value. * * Outputs one value for every input value. Thus, to interpolation * over time, it's necessary to get the source to emit values at the desired rate. * * Options can specify an easing name or custom transform of easing progress. * @param input * @param options * @returns */ function interpolate(input, options = {}) { const amount = options.amount ?? .1; const snapAt = options.snapAt ?? .99; const index = interpolate$1(amount, options); return computeWithPrevious(input, (previous, target) => { const v = index(previous, target); if (target > previous) { if (v / target >= snapAt) return target; } return v; }); } /** * From the basis of an input stream of values, run a function over * each value. The function takes in the last value from the stream as well as the current. * @param input * @param fn * @returns */ //#endregion //#region ../packages/rx/src/ops/math.ts function max(input, options) { return process(max$1(), `max`, input, options); } function min(input, options) { return process(min$1(), `min`, input, options); } function average(input, options) { return process(average$1(), `average`, input, options); } function sum(input, options) { return process(sum$1(), `sum`, input, options); } function tally(input, options = {}) { const countArrayItems = options.countArrayItems ?? true; return process(tally$1(countArrayItems), `tally`, input, options); } function rank(input, rank, options) { return process(rank$1(rank, options), `rank`, input, options); } function process(processor, annotationField, input, options = {}) { const annotate = options.annotate; let previous; const skipUndefined = options.skipUndefined ?? true; const skipIdentical = options.skipIdentical ?? true; const upstream = initUpstream(input, { ...options, onValue(value) { const x = processor(value); if (x === void 0 && skipUndefined) return; if (skipIdentical && x === previous) return; previous = x; if (annotate) { const ret = { value }; ret[annotationField] = x; upstream.set(ret); } else upstream.set(x); } }); return toReadable(upstream); } //#endregion //#region ../packages/rx/src/ops/pipe.ts /** * Pipes the output of one stream into another, in order. * The stream returned is a new stream which captures the final output. * * If any stream in the pipe closes the whole pipe is closed. * @param streams * @returns */ const pipe = (...streams) => { const event = initStream(); const unsubs = []; const performDispose = (reason) => { for (const s of streams) if (!s.isDisposed) s.dispose(reason); for (const s of unsubs) s(); event.dispose(reason); }; for (let index = 0; index < streams.length; index++) unsubs.push(streams[index].on((message) => { const isLast = index === streams.length - 1; if (messageHasValue(message)) if (isLast) event.set(message.value); else streams[index + 1].set(message.value); else if (messageIsDoneSignal(message)) performDispose(`Upstream disposed`); })); return { on: event.on, onValue: event.onValue, dispose(reason) { performDispose(reason); }, isDisposed() { return event.isDisposed(); } }; }; //#endregion //#region ../packages/rx/src/ops/single-from-array.ts /** * For a stream that emits arrays of values, this op will select a single value. * * Can select based on: * * predicate: a function that returns _true_ for a value * * at: selection based on array index (can be combined with random ordering to select a random value) * * ```js * // If source is Reactive<Array<number>>, picks the first even number * singleFromArray(source, { * predicate: v => v % 2 === 0 * }); * * // Selects a random value from source * singleFromArray(source, { * order: `random`, * at: 0 * }); * ``` * * If neither `predicate` or `at` options are given, exception is thrown. * @param source Source to read from * @param options Options for selection * @returns */ function singleFromArray(source, options = {}) { const order = options.order ?? `default`; if (!options.at && !options.predicate) throw new Error(`Options must have 'predicate' or 'at' fields`); let preprocess = (values) => values; if (order === `random`) preprocess = shuffle; else if (typeof order === `function`) preprocess = (values) => values.toSorted(order); const upstream = initUpstream(source, { onValue(values) { values = preprocess(values); if (options.predicate) { for (const v of values) if (options.predicate(v)) upstream.set(v); } else if (options.at) upstream.set(values.at(options.at)); } }); return upstream; } //#endregion //#region ../packages/rx/src/ops/split.ts /** * Creates a set of streams each of which receives data from `source`. * By default these are lazy and dispose if the upstream source closes. * * See also {@link splitLabelled} to split into named streams. * @param rxOrSource * @param options * @returns */ const split = (rxOrSource, options = {}) => { const quantity = options.quantity ?? 2; const outputs = []; const source = resolveSource(rxOrSource); for (let index = 0; index < quantity; index++) outputs.push(initUpstream(source, { disposeIfSourceDone: true, lazy: `initial` })); return outputs; }; /** * Splits `source` into several duplicated streams. * Returns an object with keys according to `labels`. * Each value is a stream which echos the values from `source`. * ```js * const { a, b, c} = splitLabelled(source, `a`, `b`, `c`); * // a, b, c are Reactive types * ``` * * See also {@link split} to get an unlabelled split * @param rxOrSource * @param labels * @returns */ const splitLabelled = (rxOrSource, labels) => { const source = resolveSource(rxOrSource); const t = {}; for (const label of labels) t[label] = initUpstream(source, { lazy: `initial`, disposeIfSourceDone: true }); return t; }; //#endregion //#region ../packages/rx/src/ops/switcher.ts /** * Switcher generates several output streams, labelled according to the values of `cases`. * Values from `source` are fed to the output streams if their associated predicate function returns _true_. * * In this way, we can split one input stream into several output streams, each potentially getting a different * subset of the input. * * With `options`, you can specify whether to send to multiple outputs if several match, or just the first (default behaviour). * * The below example shows setting up a switcher and consuming the output streams. * @example * ```js * // Initialise a reactive number, starting at 0 * const switcherSource = Reactive.number(0); * // Set up the switcher * const x = Reactive.switcher(switcherSource, { * even: v => v % 2 === 0, * odd: v => v % 2 !== 0 * }); * // Listen for outputs from each of the resulting streams * x.even.on(msg => { * log(`even: ${msg.value}`); * }); * x.odd.on(msg => { * log(`odd: ${msg.value}`); * }) * // Set new values to the number source, counting upwards * // ...this will in turn trigger the outputs above * setInterval(() => { * switcherSource.set(switcherSource.last() + 1); * }, 1000); * ``` * * If `source` closes, all the output streams will be closed as well. * @param reactiveOrSource * @param cases * @param options * @returns */ const switcher = (reactiveOrSource, cases, options = {}) => { const match = options.match ?? `first`; const source = resolveSource(reactiveOrSource); let disposed = false; const t = {}; for (const label of Object.keys(cases)) t[label] = initStream(); const performDispose = () => { if (disposed) return; unsub(); disposed = true; for (const stream of Object.values(t)) stream.dispose(`switcher source dispose`); }; const unsub = source.on((message) => { if (messageHasValue(message)) { for (const [lbl, pred] of Object.entries(cases)) if (pred(message.value)) { t[lbl].set(message.value); if (match === `first`) break; } } else if (messageIsDoneSignal(message)) performDispose(); }); return t; }; //#endregion //#region ../packages/rx/src/ops/sync-to-array.ts /** * Waits for all sources to produce a value, sending the combined results as an array. * After sending, it waits again for each source to send at least one value. * * Use {@link syncToObject} to output objects based on labelled sources rather than an array of values. * * Pace will be set by the slowest source. Alternatively, use {@link combineLatestToArray} where the rate is determined by fastest source. * * Only complete results are sent. For example if source A & B finish and source C is still producing values, * synchronisation is not possible because A & B stopped producing values. Thus the stream will self-terminate * after `maximumWait` (2 seconds). The newer values from C are lost. */ function syncToArray(reactiveSources, options = {}) { const onSourceDone = options.onSourceDone ?? `break`; const finalValue = options.finalValue ?? `undefined`; const maximumWait = intervalToMs(options.maximumWait, 2e3); let watchdog; const data = []; const states = reactiveSources.map((source) => ({ finalData: void 0, done: false, source: resolveSource(source), unsub: () => { /**no-op */} })); const unsubscribe = () => { for (const s of states) { s.unsub(); s.unsub = () => { /**no-op */}; } }; const isDataSetComplete = () => { for (let index = 0; index < data.length; index++) { if (onSourceDone === `allow` && states[index].done) continue; if (data[index] === void 0) return false; } return true; }; const hasIncompleteSource = () => states.some((s) => !s.done); const resetDataSet = () => { for (let index = 0; index < data.length; index++) { if (finalValue === `last` && states[index].done) continue; data[index] = void 0; } }; const onWatchdog = () => { done(`Sync timeout exceeded (${maximumWait.toString()})`); }; const done = (reason) => { if (watchdog) clearTimeout(watchdog); unsubscribe(); event.dispose(reason); }; const init = () => { watchdog = setTimeout(onWatchdog, maximumWait); for (const [index, state] of states.entries()) { data[index] = void 0; state.unsub = state.source.on((valueChanged) => { if (messageIsSignal(valueChanged)) { if (valueChanged.signal === `done`) { state.finalData = data[index]; state.unsub(); state.done = true; state.unsub = () => { /** no-op */}; if (finalValue === `undefined`) data[index] = void 0; if (onSourceDone === `break`) { done(`Source '${index.toString()}' done, and onSourceDone:'break' is set`); return; } if (!hasIncompleteSource()) { done(`All sources done`); return; } } return; } data[index] = valueChanged.value; if (isDataSetComplete()) { event.set([...data]); resetDataSet(); if (watchdog) clearTimeout(watchdog); watchdog = setTimeout(onWatchdog, maximumWait); } }); } }; const event = initStream({ onFirstSubscribe() { unsubscribe(); init(); }, onNoSubscribers() { if (watchdog) clearTimeout(watchdog); unsubscribe(); } }); return { dispose: event.dispose, isDisposed: event.isDisposed, on: event.on, onValue: event.onValue }; } //#endregion //#region ../packages/rx/src/ops/sync-to-object.ts function syncToObject(reactiveSources, options = {}) { const keys = Object.keys(reactiveSources); return transform(syncToArray(Object.values(reactiveSources), options), (streamValues) => { return zipKeyValue(keys, streamValues); }); } //#endregion //#region ../packages/rx/src/ops/tap.ts /** * 'Taps' the values from 'input', passing them to the 'process' function. * Return stream is the input stream, unaffected by what 'process' does. * @param input Input stream * @param processors List of processors * @returns */ function tapProcess(input, ...processors) { const inputStream = resolveSource(input); const chain = Process.flow(...processors); inputStream.onValue((value) => { chain(value); }); return inputStream; } /** * 'Taps' the values from 'input', passing them to 'diverged' * Returns the original input stream, unaffected by what 'diverged' does. * @param input Input stream * @param diverged Stream to write to * @returns */ function tapStream(input, diverged) { const inputStream = resolveSource(input); inputStream.onValue((value) => { diverged.set(value); }); return inputStream; } /** * Create a parallel 'tap' of processing * @param input Input stream * @param ops Series of ops to process data * @returns */ const tapOps = (input, ...ops) => { for (const op of ops) input = op(input); return input; }; //#endregion //#region ../packages/rx/src/ops/throttle.ts /** * Only allow a value through if a minimum amount of time has elapsed. * since the last value. This effectively slows down a source to a given number * of values/ms. Values emitted by the source which are too fast are discarded. * * Throttle will fire on the first value received. * * In more detail: * Every time throttle passes a value, it records the time it allowed something through. For every * value received, it checks the elapsed time against this timestamp, throwing away values if * the period hasn't elapsed. * * With this logic, a fury of values of the source might be discarded if they fall within the elapsed time * window. But then if there is not a new value for a while, the actual duration between values can be longer * than expected. This is in contrast to {@link debounce}, which will emit the last value received