ixfx
Version:
Bundle of ixfx libraries
1,695 lines (1,665 loc) • 95.4 kB
JavaScript
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