diff --git a/core/signal/src/core.ts b/core/signal/src/core.ts index f5d5a0473..d38879e1f 100644 --- a/core/signal/src/core.ts +++ b/core/signal/src/core.ts @@ -6,9 +6,9 @@ import type { ListenerObject, ListenerOptions, SignalObject, - SignalProvider, - SignalProviderOptions, - SignalStack, + ProviderFunction, + ProviderOptions, + SignalStorage, } from './type.js'; export const logger = createLogger('alwatr/signal'); @@ -23,10 +23,12 @@ globalAlwatr.registeredList.push({ */ let _lastListenerId = 0; +const debounceTimeout = 5; + /** * Signal stack database. */ -const _signalStack: SignalStack = {}; +const _signalStorage: SignalStorage = {}; /** * Get signal object by name, If not available, it will create a new signal with default options. @@ -38,25 +40,26 @@ const _signalStack: SignalStack = {}; * signal.disabled = true; * ``` */ -export function _getSignalObject( - signalName: SignalName, -): SignalObject { - if (!_signalStack[signalName]) { - _signalStack[signalName] = { +export function _getSignalObject>(signalName: string): SignalObject { + if (!_signalStorage[signalName]) { + _signalStorage[signalName] = { name: signalName, disabled: false, debounced: false, listenerList: [], }; } - return _signalStack[signalName] as unknown as SignalObject; + return _signalStorage[signalName] as unknown as SignalObject; } -function __callListeners(signal: SignalObject): void { - logger.logMethodArgs('_callListeners', {signalName: signal.name, signalValue: signal.value}); - if (signal.value === undefined) { - // null is a valid value for signal. - logger.accident('_callListeners', 'no_signal_value', 'signal must have a value', { +/** + * Call all listeners callback of special signal. + */ +function __callListeners>(signal: SignalObject): void { + logger.logMethodArgs('__callListeners', {signalName: signal.name, signalDetail: signal.detail}); + if (signal.detail === undefined) { + // null is a valid detail for signal. + logger.accident('__callListeners', 'no_signal_detail', 'signal must have a detail', { signalName: signal.name, }); return; @@ -65,17 +68,17 @@ function __callListeners(signal: SignalO for (const listener of signal.listenerList) { if (listener.disabled) continue; try { - const ret = listener.callback(signal.value); + const ret = listener.callback(signal.detail); if (ret instanceof Promise) { ret.catch((err) => - logger.error('_callListeners', 'call_listener_failed', err, { + logger.error('__callListeners', 'call_listener_failed', err, { signalName: signal.name, }), ); } } catch (err) { - logger.error('_callListeners', 'call_listener_failed', err, { + logger.error('__callListeners', 'call_listener_failed', err, { signalName: signal.name, }); } @@ -83,11 +86,11 @@ function __callListeners(signal: SignalO signal.listenerList .filter((listener) => !listener.disabled && listener.once) - .forEach((listener) => _removeSignalListener(signal, listener.id)); + .forEach((listener) => _removeSignalListener(listener)); } /** - * Adds a new listener to the signal. + * Adds a new listener to a signal. * * Example: * @@ -96,59 +99,57 @@ function __callListeners(signal: SignalO * const listener = _addSignalListener(signal, (content) => console.log(content)); * ``` */ -export function _addSignalListener( - signal: SignalObject, - listenerCallback: ListenerCallback, +export function _addSignalListener>( + signal: string | SignalObject, + listenerCallback: ListenerCallback, options: ListenerOptions = {}, -): ListenerObject { +): ListenerObject { + const _signal = typeof signal === 'string' ? _getSignalObject(signal) : signal; options.once ??= false; options.disabled ??= false; - options.receivePrevious ??= true; + options.receivePrevious ??= 'AnimationFrame'; options.priority ??= false; - logger.logMethodArgs('_addSignalListener', {signalName: signal.name, options}); + logger.logMethodArgs('_addSignalListener', {signal: _signal.name, options}); - const listener: ListenerObject = { + const listener: ListenerObject = { id: ++_lastListenerId, + signalName: _signal.name, once: options.once, disabled: options.disabled, callback: listenerCallback, }; - let callbackCalled = false; + const callbackCall = _signal.detail !== undefined && options.receivePrevious !== 'No'; + if (callbackCall) { + // Run callback for old dispatch signal - // Run callback for old dispatch signal - if (signal.value !== undefined) { - // null is a valid value for signal. - if (options.receivePrevious === 'Immediate') { + const callback = (): void => { try { - listenerCallback(signal.value); + if (_signal.detail !== undefined) listenerCallback(_signal.detail); } catch (err) { logger.error('_addSignalListener', 'call_signal_callback_failed', err, { - signalName: signal.name, + signalName: _signal.name, }); } - callbackCalled = true; + }; + + if (options.receivePrevious === 'AnimationFrame') { + requestAnimationFrame(callback); } - else if (options.receivePrevious === true) { - requestAnimationFrame(() => { - if (signal.value !== undefined) { - // null is a valid value for signal. - listenerCallback(signal.value); - } - }); - callbackCalled = true; // must be outside of requestAnimationFrame. + else { + setTimeout(callback, options.receivePrevious === 'NextCycle' ? 0 : debounceTimeout); } } // if once then must remove listener after fist callback called! then why push it to listenerList?! - if (!(options.once === true && callbackCalled === true)) { + if (!(callbackCall && options.once)) { if (options.priority === true) { - signal.listenerList.unshift(listener); + _signal.listenerList.unshift(listener); } else { - signal.listenerList.push(listener); + _signal.listenerList.push(listener); } } @@ -157,21 +158,11 @@ export function _addSignalListener( /** * Removes a listener from the signal. - * - * Example: - * - * ```ts - * const signal = _getSignalObject('content-change') - * const listener = _addSignalListener(signal, ...); - * _removeSignalListener(signal, listener); - * ``` */ -export function _removeSignalListener( - signal: SignalObject, - listenerId: number, -): void { - logger.logMethodArgs('_removeSignalListener', {signalName: signal.name, listenerId}); - const listenerIndex = signal.listenerList.findIndex((_listener) => _listener.id === listenerId); +export function _removeSignalListener(listener: Pick, 'id' | 'signalName'>): void { + logger.logMethodArgs('_removeSignalListener', {signalName: listener.signalName, listenerId: listener.id}); + const signal = _getSignalObject(listener.signalName); + const listenerIndex = signal.listenerList.findIndex((_listener) => _listener.id === listener.id); if (listenerIndex !== -1) { signal.listenerList.splice(listenerIndex, 1); } @@ -184,63 +175,62 @@ export function _removeSignalListener( * const signal = _getSignalObject('content-change') * _dispatchSignal(signal, content); */ -export function _dispatchSignal( - signal: SignalObject, - value: AlwatrSignals[SignalName], +export function _dispatchSignal>( + signal: SignalObject | string, + detail: T, options: DispatchOptions = {}, ): void { - options.debounce ??= true; + const _signal = typeof signal === 'string' ? _getSignalObject(signal) : signal; + options.debounce ??= 'AnimationFrame'; - logger.logMethodArgs('dispatchSignal', {signalName: signal.name, value, options}); + logger.logMethodArgs('_dispatchSignal', {signalName: _signal.name, detail, options}); - // set value before check signal.debounced for act like throttle (call listeners with last dispatch value). - signal.value = value; + // set detail before check signal.debounced for act like throttle (call listeners with last dispatch detail). + _signal.detail = detail; - if (signal.disabled) return; // signal is disabled. - if (options.debounce === true && signal.debounced === true) return; // last dispatch in progress. + if (_signal.disabled) return; // signal is disabled. - if (options.debounce !== true) { + // Simple debounce noise filtering + if (options.debounce !== 'No' && _signal.debounced === true) return; // last dispatch in progress. + + if (options.debounce === 'No') { // call listeners immediately. - __callListeners(signal); + __callListeners(_signal); return; } - // else: call listeners in next frame. - signal.debounced = true; - requestAnimationFrame(() => { - __callListeners(signal); - signal.debounced = false; - }); + // else + _signal.debounced = true; + const callListeners = (): void => { + __callListeners(_signal); + _signal.debounced = false; + }; + options.debounce === 'AnimationFrame' + ? requestAnimationFrame(callListeners) + : setTimeout(callListeners, debounceTimeout); +} + +/** + * Get current signal detail + */ +export function _getSignalDetail>(signal: string | SignalObject): T | undefined { + const _signal = typeof signal === 'string' ? _getSignalObject(signal) : signal; + return _signal.detail; } /** * Defines the provider of the signal that will be called when the signal requested (addRequestSignalListener). - * - * Example: - * - * ```ts - * const signal = _getSignalObject('content-change'); - * const requestSignal = _getSignalObject('request-content-change'); - * _setSignalProvider(signal, requestSignal, async (requestParam) => { - * const content = await fetchNewContent(requestParam); - * if (content != null) { - * return content; // dispatchSignal('content-change', content); - * } - * else { - * dispatchSignal('content-not-found'); - * } - * } - * ``` */ -export function _setSignalProvider( - signal: SignalObject, - requestSignal: SignalObject, - signalProvider: SignalProvider, - options: SignalProviderOptions = {}, -): ListenerObject { - options.debounce ??= true; - options.receivePrevious ??= true; +export function _setSignalProvider, TRequest extends Record>( + signalName: string, + signalProvider: ProviderFunction, + options: ProviderOptions = {}, +): ListenerObject { + options.debounce ??= 'AnimationFrame'; + options.receivePrevious ??= 'AnimationFrame'; + + logger.logMethodArgs('_setSignalProvider', {signalName, options}); - logger.logMethodArgs('_setSignalProvider', {signal: signal.name, requestSignal: requestSignal.name, options}); + const requestSignal = _getSignalObject('request-' + signalName); if (requestSignal.listenerList.length > 0) { logger.accident( @@ -248,21 +238,21 @@ export function _setSignalProvider( 'another_signal_provider_exist', 'Another provider exist! It will be removed to fix the problem', { - signalName: signal.name, + signalName: signalName, }, ); requestSignal.listenerList = []; } - const _callback = async (requestParam: AlwatrRequestSignals[SignalName]): Promise => { - const signalValue = await signalProvider(requestParam); - if (signalValue !== undefined) { - // null is a valid value for signal. - _dispatchSignal(signal, signalValue, {debounce: options.debounce}); + const _callback = async (requestParam: TRequest): Promise => { + const signalDetail = await signalProvider(requestParam); + if (signalDetail !== undefined) { + // null is a valid detail for signal. + _dispatchSignal(signalName, signalDetail, {debounce: options.debounce}); } }; - return _addSignalListener(requestSignal, _callback as unknown as ListenerCallback, { + return _addSignalListener(requestSignal, _callback, { receivePrevious: options.receivePrevious, }); } diff --git a/core/signal/src/type.ts b/core/signal/src/type.ts index 1e872a70c..99dcea211 100644 --- a/core/signal/src/type.ts +++ b/core/signal/src/type.ts @@ -14,6 +14,8 @@ declare global { } } +export type DebounceType = 'No' | 'AnimationFrame' | 'Timeout'; + /** * addSignalListener options type */ @@ -41,9 +43,9 @@ export interface ListenerOptions { * Calling this listener (callback) with preview signal value (if dispatched before). * If Immediate, the listener will be called immediately without any debounce for preview signal. * - * @default true + * @default `AnimationFrame` */ - receivePrevious?: boolean | 'Immediate'; + receivePrevious?: DebounceType | 'NextCycle'; } /** @@ -55,53 +57,59 @@ export interface DispatchOptions { * If false, every signal is matter and count. * tips: debounce work like throttle this means listeners call with last dispatch value. * - * @default true + * @default `AnimationFrame` */ - debounce?: boolean; + debounce?: DebounceType; } -export interface SignalProviderOptions { +/** + * setSignalProvider options type. + */ +export interface ProviderOptions { /** * Calling signal provider (request signal callback) with preview signal value (if dispatched before). * If Immediate, the listener will be called immediately without any debounce for preview signal. * - * @default true + * @default `NextCycle` */ - receivePrevious?: boolean | 'Immediate'; + receivePrevious?: DebounceType | 'NextCycle'; /** * If true, the dispatch will be send after animation frame debounce. * If false, every signal is matter and count. - * tips: debounce work like throttle this means listeners call with last dispatch value. + * tips: debounce true work like throttle this means listeners call with last dispatch value. * - * @default true + * @default `AnimationFrame` */ - debounce?: boolean; + debounce?: DebounceType; } /** * Signal listeners callback function type. */ -export type ListenerCallback = ( - signalValue: AlwatrSignals[SignalName] -) => void | Promise; +export type ListenerCallback> = (detail: T) => void | Promise; /** * Signal provider function type used to setSignalProvider. */ -export type SignalProvider = ( - requestParam: AlwatrRequestSignals[SignalName] -) => AlwatrSignals[SignalName] | void | Promise; +export type ProviderFunction, TRequest extends Record> = ( + requestDetail: TRequest +) => TSignal | void | Promise; /** - * Signal listeners object in database. + * Signal listeners object in storage. */ -export interface ListenerObject { +export interface ListenerObject> { /** * Unique listener id */ id: number; + /** + * Signal name + */ + signalName: string; + /** * If true, the listener will be called only once and removed automatically after first call */ @@ -112,22 +120,22 @@ export interface ListenerObject { */ disabled: boolean; - callback: ListenerCallback; + callback: ListenerCallback; } /** - * Signal object in database. + * Signal object in storage. */ -export interface SignalObject { +export interface SignalObject> { /** * Signal name for direct access. */ - name: SignalName; + name: string; /** * Last dispatched value. */ - value?: AlwatrSignals[SignalName]; + detail?: T; /** * If true, the signal is disabled. @@ -143,12 +151,10 @@ export interface SignalObject { /** * Signal listeners list. */ - listenerList: Array>; + listenerList: Array>; } /** - * Signal stack database. + * Signal stack storage. */ -export type SignalStack = { - [SignalName in keyof AlwatrSignals]?: SignalObject; -}; +export type SignalStorage = Record> | undefined>;