UNPKG

badmfck-signal

Version:

An implementation of a signaling mechanism used to connect components and transfer data between them

513 lines (512 loc) 16.1 kB
"use strict"; /** * Signal by Igor Bloom * Copyright (C) 2014-2023 * * An implementation of a signaling mechanism used to connect components and transfer data between them. * * How to use: * * Create an instance of Signal with string data type: * export const S_TEST:Signal<string>=new Signal(); * * Subscribe to signal, with id: test1 * S_TEST.subscribe(str=>{console.log(str)},"test1") * * Call invokation procedure * S_TEST.invoke("test"); * * Remove subscribtion, using id: * S_TEST.remove("test1") * * Remove all subscribtions from signal: * S_TEST.clear(); * */ Object.defineProperty(exports, "__esModule", { value: true }); exports.useSignal = exports.s_destroy = exports.s_removeAll = exports.s_invokeOnce = exports.s_invoke = exports.s_unsubscribe = exports.s_subscribeOnce = exports.s_subscribe = exports.Req = exports.SignalHandler = exports.Signal = exports.DataProvider = exports.Binder = void 0; const Binder_1 = require("./Binder"); Object.defineProperty(exports, "Binder", { enumerable: true, get: function () { return Binder_1.Binder; } }); const DataProvider_1 = require("./DataProvider"); Object.defineProperty(exports, "DataProvider", { enumerable: true, get: function () { return DataProvider_1.DataProvider; } }); class Signal { static nextID = 0; busy = false; tempAdd = []; tempRem = []; tempInvoke = []; callbacks = []; tempClear = false; name = ""; type = "signal"; onSubscribe; static onLog; static reactUseState = null; static reactUseEffect = null; /** * Setup react hook before using it * @param useState - reference to react useState * @param useEffect - reference to react useEffect */ static setupReact(useState, useEffect) { if (Signal.reactUseEffect && Signal.reactUseState) return; Signal.reactUseEffect = useEffect; Signal.reactUseState = useState; Binder_1.Binder.setupReact(useState, useEffect); Req.setupReact(useState, useEffect); } /** * Use signal as react hook * @param group string or Signal * @param deps array of dependencies for react useEffect hook * @param onInvoke callback, invokes when signalling data * @returns */ static useSignal(group, deps, onInvoke) { if (!Signal.reactUseEffect || !Signal.reactUseState) { return null; } const [data, setData] = Signal.reactUseState(null); Signal.reactUseEffect(() => { let id = ""; let ss = null; if (group instanceof Signal) { ss = group; id = ss.subscribe((value) => { setData(value); if (onInvoke) onInvoke(value); }); } else { id = s_subscribe(value => { setData(value); if (onInvoke) onInvoke(value); }, group); } return () => { if (ss) { ss.unsubscribe(id); } else s_unsubscribe(id); }; }, deps); return data; } /** * Create signal instance * @param name @optional, signal name * @param onSubscribe @optional, callback, fires when someone subscribe to signal * @param onLog @optional, onLog callback, with level:number and text:string. */ constructor(name, onSubscribe, handler) { if (!name) name = "S_" + (+new Date()); this.name = name; if (onSubscribe) this.onSubscribe = onSubscribe; if (handler) this.subscribe(handler); } /** * Subscribe to signal * @param callback will be called when the signal is invoked. * @param id Optional parameter, callback identificator * @returns id as string */ subscribe(callback, id) { if (!id) id = "" + (Signal.nextID++); // add to temprary if (this.busy) { for (let i of this.tempAdd) { if (i.cb === callback) return i.id; } this.tempAdd.push({ cb: callback, id: id }); return id; } // add to stocks for (let i of this.callbacks) { if (i.cb === callback) return i.id; } this.callbacks.push({ cb: callback, id: id }); if (this.onSubscribe) { if (this.onSubscribe.length === 1) this.onSubscribe(callback); else this.onSubscribe(); } if (Signal.onLog) Signal.onLog(this, 0, `Subscribtion to ${this.name} added with id: ` + id); return id; } /** * Use signal as react webhook, be sure to call Signal.setupReact(useState,useEffect) before. * @optional @param dependensies - array of dependensies to use with useEffect hook, * @example const test = S_TEST.use(); // when signal invokes, variable test will receive signalling data * @returns dataobject with type <T>, or null */ use(dependensies, onInvke) { return Signal.useSignal(this, dependensies, onInvke); } /** * Get list of ids of subscribtions * @returns array of subscribtion's ids */ getSubscribtions() { const ids = []; for (let i of this.callbacks) ids.push(i.id); return ids; } /** * Removes all subscriptions to a signal * @returns void */ removeAll() { if (this.busy) { this.tempClear = true; return; } this.callbacks = []; if (Signal.onLog) Signal.onLog(this, 0, `${this.name} remove all subscribtions`); } /** * Remove subscribtion from signal * @param id @optional uses to identify and find callback to remove * @returns true if succsess */ unsubscribe(id) { // search in callbacks let found = false; for (let i of this.callbacks) { if (i.id === id) { found = true; break; } } // search in tempAdd this.tempAdd = this.tempAdd.filter(val => val.id !== id); if (!found) { if (Signal.onLog) Signal.onLog(this, 1, `${this.name} unsubscribe failed, wrong id:${id}`); return false; } // add to temprary if (this.busy) { for (let i of this.tempRem) { if (i.id === id) return true; } this.tempRem.push({ id: id }); return true; } let removed = this.callbacks.length; this.callbacks = this.callbacks.filter(val => val.id !== id); removed = removed - this.callbacks.length; if (Signal.onLog) Signal.onLog(this, removed > 0 ? 0 : 1, `${this.name} unsubscribe from ${removed} callback(s)`); return removed > 0; } /** * Invoke signal, all callback will be called with providen data * @param data Data Object passes to each callback * @returns void */ invoke(data) { if (this.busy) { this.tempInvoke.push({ data: data }); if (Signal.onLog) Signal.onLog(this, 0, `${this.name} delayed invokation added`); return; } this.busy = true; for (let i of this.callbacks) { if (i && i.cb && typeof i.cb === "function") { i.cb(data); } } this.busy = false; if (Signal.onLog) Signal.onLog(this, 0, `${this.name} invoked`); if (this.tempAdd && this.tempAdd.length > 0) { if (Signal.onLog) Signal.onLog(this, 0, `${this.name} delayed subscribtion started`); for (let i of this.tempAdd) { this.subscribe(i.cb, i.id); } this.tempAdd = []; } if (this.tempRem && this.tempRem.length > 0) { if (Signal.onLog) Signal.onLog(this, 0, `${this.name} delayed unsubscribtion started`); for (let i of this.tempRem) { this.unsubscribe(i.id); } this.tempRem = []; } if (this.tempInvoke && this.tempInvoke.length > 0) { if (Signal.onLog) Signal.onLog(this, 0, `${this.name} delayed invokation started`); for (let i of this.tempInvoke) { this.invoke(i.data); } this.tempInvoke = []; } if (this.tempClear) { this.tempClear = false; this.removeAll(); } } } exports.Signal = Signal; /** * Uses to handle signal subscribtions * add and remove callbacks from/to signals * clear all signals in handler */ class SignalHandler { static nextID = 0; id; signals = []; constructor() { this.id = SignalHandler.nextID++; } /** * Add to Signal or group of signals callback * @param signal Array of Signals or Signal * @param cb callback to add to signal or to array of signals * @returns void */ add(signal, cb) { if (!Array.isArray(signal)) signal = [signal]; for (let i of this.signals) { const found = signal.filter((s) => i === s); if (found.length > 0) return; } signal.map(val => { this.signals.push(val); val.subscribe(cb, "signaller_" + this.id); }); } /** * Clear all subscribtions with all signals in handler */ clear() { for (let i of this.signals) i.unsubscribe("signaller_" + this.id); this.signals = []; } } exports.SignalHandler = SignalHandler; class Req { static reactUseState = null; static reactUseEffect = null; /** * Setup react hook before using it * @param useState - reference to react useState * @param useEffect - reference to react useEffect */ static setupReact(useState, useEffect) { if (Req.reactUseEffect && Req.reactUseState) return; Req.reactUseEffect = useEffect; Req.reactUseState = useState; Signal.setupReact(useState, useEffect); } static nextID = 1; worker; name; type = "request"; constructor(worker, name) { if (!name) name = "Req_" + Req.nextID++; this.name = name; if (worker) this.worker = worker; } request(data) { if (!this.worker) throw (Error("No worker registere in " + this.name)); return this.worker(data); } /** * React hook for Req * @param request Request data <T> * @param dep dependecies for useEffect * @param onChange invokes when Req change it value * @returns data <K> */ use(request, dep, onChange) { const [data, setData] = Req.reactUseState(null); Req.reactUseEffect(() => { this.request(request).then(r => { setData(r); if (onChange) onChange(r); }); }, dep); return data; } /** * React hook for Req, with initial value and ability to change value directly from component * @param request Request data <T> * @param dep dependencies for useEffect * @param initialValue initial value * @param onChange invokes when Req change it value * @returns array, first item stored value, second item - callback to reset in hook req.value */ useValue(request, onChange, dep) { const [data, setData] = Req.reactUseState(request); if (!dep) dep = [request]; Req.reactUseEffect(() => { this.request(request).then(r => { setData(r); if (onChange) onChange(r); }); }, dep); return [data, (value) => { setData(value); }]; } /** * Complete Request hook <T,K> * @param onChange - will fire when data available in request * @param initialRequest - intial T request param * @returns array: 0 - available request data, 1 - callback to execute request (T), 2 - callback to reset available request data (K), 3 - Busy indicator, a boolean value, true when Request gathering data, false - when request completed */ useRequest(onChange, initialRequest) { const [req, setReq] = Req.reactUseState(initialRequest); const [data, setData] = Req.reactUseState(); const [busy, setBusy] = Req.reactUseState(true); Req.reactUseEffect(() => { this.request(req).then(r => { setData(r); setBusy(false); if (onChange) onChange(r); }); }, [req]); return [data, (value) => { setBusy(true); setReq(value); }, (value) => { setData(value); setBusy(false); }, busy]; } //,response:(data:K)=>void set listener(_listener) { this.worker = _listener; } } exports.Req = Req; let signals = new Map(); /** * Subscribe to signalling group, using type to define group. Be sure to unsubscribe in time. * @param callback callback function, will be called when signal invoking * @param group Signal group * @returns callback id, use it to unsubscribe */ function s_subscribe(callback, group) { if (!group) group = "__default"; // find signal let signal = signals.get(group); if (!signal) { signal = new Signal(); signals.set(group, signal); } return signal.subscribe(callback); } exports.s_subscribe = s_subscribe; /** * Subscribe to signalling group for once, after callback fires, unsubscribe automatically * @param callback * @param group @optional Signal group, if empty, will use global signalling pipe * @returns void */ function s_subscribeOnce(callback, group) { const id = s_subscribe((data) => { callback(data); s_unsubscribe(id); }, group ?? "__default"); } exports.s_subscribeOnce = s_subscribeOnce; /** * Unsubscribe from signal group by callback id * @param id callback id * @returns true if success */ function s_unsubscribe(id) { // find signal let result = false; for (let i of signals) { if (i[1]) i[1].unsubscribe(id); } return result; } exports.s_unsubscribe = s_unsubscribe; /** * Invoke all callback in signal * @param data Data object passes to each callback * @param group Signal group, if empty, fill invoke on global signalling pipeline * @returns void */ function s_invoke(data, group) { if (!group) group = "__default"; let signal = signals.get(group); if (!signal) return; signal.invoke(data); } exports.s_invoke = s_invoke; /** * Invoke all callback in signal and remove all of them after invokation. * @param data Data object passes to each callback * @param group Signal group, if empty, fill invoke on global signalling pipeline * @returns void */ function s_invokeOnce(data, group) { s_invoke(data, group); s_removeAll(group); } exports.s_invokeOnce = s_invokeOnce; /** * Clear all callbacks in Signalling group * @param group Signal group * @returns */ function s_removeAll(group) { if (!group) group = "__default"; let signal = signals.get(group); if (!signal) return; signal.removeAll(); } exports.s_removeAll = s_removeAll; /** * Destroy all signal types */ function s_destroy() { for (let i of signals) i[1].removeAll(); signals = new Map(); } exports.s_destroy = s_destroy; function useSignal(group) { return Signal.useSignal(group); } exports.useSignal = useSignal; exports.default = Signal;