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
JavaScript
"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;