From f04e847f5b38bbfe5a404cab25e1235329b2234b Mon Sep 17 00:00:00 2001 From: Asko Nõmm Date: Wed, 16 Jul 2025 11:41:19 +0300 Subject: Refactor to not use return objects for state changed or dispatches which leads to dual way of doing things, and adds needless complexity. --- src/shapex.ts | 292 +++++++++++++++++++++++----------------------------------- 1 file changed, 115 insertions(+), 177 deletions(-) (limited to 'src/shapex.ts') diff --git a/src/shapex.ts b/src/shapex.ts index 3ff67a8..fa9047f 100644 --- a/src/shapex.ts +++ b/src/shapex.ts @@ -1,78 +1,55 @@ -/** - * Dispatches an event with a given name and passes on - * given arguments to it. - */ -export type SubscriptionResponseDispatch = { - to: string; - with?: W; -}; - -/** - * A response of the subscription callback. Should return new state - * if you want to update state, and/or optionally also any events you - * might want to dispatch. - */ -export type SubscriptionResponse = { - state?: T; - dispatch?: - | SubscriptionResponseDispatch - | SubscriptionResponseDispatch[]; -}; - -const isSubscriptionResponseList = ( - dispatch: SubscriptionResponseDispatch | SubscriptionResponseDispatch[], -): dispatch is SubscriptionResponseDispatch[] => Array.isArray(dispatch); - /** * A callback passed to subscriptions, called when the event * that the subscription is listening to is called. */ -export type EventCallback< - T, - W extends unknown = undefined, - D extends unknown = undefined, +export type EventListener< + TState, + TPayload extends unknown = undefined, > = ( - state: T, - data?: W, -) => - | SubscriptionResponse - | Promise> - | void - | Promise; + state: TState, + payload?: TPayload, +) => void | Promise; type Subscription< - T, - W extends unknown = undefined, - D extends unknown = undefined, + TState, + TPayload extends unknown = undefined, > = { - listener: string; - callback: EventCallback; + callback: EventListener; once: boolean; }; /** * An instance of the ShapeX object. */ -export type ShapeXInstance = { +export type ShapeXInstance = { /** * Subscribe to an event. + * + * @param topic + * @param callback */ - subscribe: ( - listener: string, - callback: EventCallback, + subscribe: ( + topic: string, + callback: EventListener, ) => number; + /** * Subscribe to an event once. + * + * @param topic + * @param callback */ - subscribeOnce: ( - listener: string, - callback: EventCallback, + subscribeOnce: ( + topic: string, + callback: EventListener, ) => number; /** * Unsubscribe from an event. + * + * @param topic */ - unsubscribe: (listener: string) => void; + unsubscribe: (topic: string) => void; /** * Get the number of subscriptions for an event. @@ -86,102 +63,109 @@ export type ShapeXInstance = { /** * Dispatch an event. + * + * @param to + * @param payload */ - dispatch: (to: string, withData?: W) => void; + dispatch: (to: string, payload?: TPayload) => void; /** * Get the current state. */ - state: () => T; + state: () => TState; + + /** + * Update state. + * + * @param updatedState + */ + setState: (updatedState: TState) => void; }; /** * A function that creates an EventX object. * - * @param {T extends object} initialState The initial application state. + * @param {TState extends object} initialState The initial application state. * @returns {ShapeXInstance} The ShapeX object. */ -export function ShapeX(initialState: T): ShapeXInstance { +export function ShapeX(initialState: TState): ShapeXInstance { let _state = initialState; - const _subscriptions: Map< - string, - Array> - > = new Map(); - let subscriptionId = 0; + let _subscriptionId = 0; + const _subscriptions: Map>> = new Map(); /** * Subscribe to an event. * - * @param {string} listener - * @param {EventCallback} callback + * @param topic + * @param {EventListener} callback * @returns */ const subscribe = < - W extends unknown = undefined, - D extends unknown = undefined, + TPayload extends unknown = undefined, >( - listener: string, - callback: EventCallback, + topic: string, + callback: EventListener, ): number => { - if (!_subscriptions.has(listener)) { - _subscriptions.set(listener, []); + // there are no listeners, set as empty array + if (!_subscriptions.has(topic)) { + _subscriptions.set(topic, []); } - const subscriptions = _subscriptions.get(listener); - if (subscriptions) { - subscriptions.push({ - listener, - callback: callback as unknown as EventCallback, - once: false, - }); - } + // add a listener + _subscriptions.get(topic)?.push({ + callback: callback as unknown as EventListener, + once: false, + }); - return ++subscriptionId; + return ++_subscriptionId; }; /** * Subscribe to an event, once. * - * @param {string} listener - * @param {EventCallback} callback + * @param {string} topic + * @param {EventListener} callback * @returns */ - const subscribeOnce = ( - listener: string, - callback: EventCallback, + const subscribeOnce = ( + topic: string, + callback: EventListener, ): number => { - if (!_subscriptions.has(listener)) { - _subscriptions.set(listener, []); + // there are no listeners, set as empty array + if (!_subscriptions.has(topic)) { + _subscriptions.set(topic, []); } - const subscriptions = _subscriptions.get(listener); - if (subscriptions) { - subscriptions.push({ - listener, - callback: callback as unknown as EventCallback, - once: true, - }); - } + // add a listener + _subscriptions.get(topic)?.push({ + callback: callback as unknown as EventListener, + once: true, + }); - return ++subscriptionId; + return ++_subscriptionId; }; - const unsubscribe = (listener: string): void => { - if (_subscriptions.has(listener)) { - _subscriptions.delete(listener); + /** + * Removes all listeners for a topic. + * + * @param topic + */ + const unsubscribe = (topic: string): void => { + if (_subscriptions.has(topic)) { + _subscriptions.delete(topic); } }; /** * Composes a list of changes between two states. * - * @param {T extends object} oldState - * @param {T extends object} newState + * @param {TState extends object} oldState + * @param {TState extends object} newState * @returns {string[]} The list of changes as array of paths. */ - const changedState = ( - oldState: T, - newState: T, + const changedState = ( + oldState: TState, + newState: TState, ): string[] => { const paths = ( state: R, @@ -210,16 +194,16 @@ export function ShapeX(initialState: T): ShapeXInstance { const newPaths = paths(newState, "$"); const newPathKeys = newPaths.map((x) => x.path); - // All new paths + // all new paths const added = newPathKeys.filter((path) => !oldPathKeys.includes(path)); - // All removed paths + // all removed paths const removed = oldPathKeys.filter((path) => !newPathKeys.includes(path)); - // Paths that remained + // paths that remained const same = oldPathKeys.filter((path) => newPathKeys.includes(path)); - // Paths that changed + // paths that changed const changed = same.filter((path) => { const oldValue = oldPaths.find((x) => x.path === path)?.value; const newValue = newPaths.find((x) => x.path === path)?.value; @@ -233,94 +217,33 @@ export function ShapeX(initialState: T): ShapeXInstance { return differ(oldState, newState); }; - const dispatcher = ( - response: SubscriptionResponse, - subscription: Subscription, - callbackCount: number, - remainingSubscriptions: Subscription[], - ) => { - // Updates state, and checks for state changes, and if any changes present, - // fires a dispatch for all the state listeners (if there are any). - if (response?.state !== undefined) { - const changes = changedState(_state, response.state); - _state = response.state; - - for (let i = 0; i < changes.length; i++) { - dispatch(changes[i]); - } - } - - // Dispatches events - if (response?.dispatch !== undefined) { - if (isSubscriptionResponseList(response.dispatch)) { - for (const dispatchee of response.dispatch) { - if (dispatchee?.with) { - dispatch(dispatchee.to, dispatchee.with); - } else { - dispatch(dispatchee.to); - } - } - } else { - if (response.dispatch?.with) { - dispatch(response.dispatch.to, response.dispatch.with); - } else { - dispatch(response.dispatch.to); - } - } - } - - callbackCount++; - - if (!subscription.once) { - remainingSubscriptions.push(subscription); - } - }; - /** * Dispatches an event with the given name and arguments. * * @param {string} to The name of the event to dispatch. - * @param {unknown[]} withData The arguments to pass to the event listeners. + * @param payload * @returns {void} */ - const dispatch = ( + const dispatch = ( to: string, - withData?: W, + payload?: TPayload, ): void => { if (!_subscriptions.has(to)) return; const scopedSubscriptions = _subscriptions.get(to) ?? []; - const remainingSubscriptions = [] as Array>; - let callbackCount = 0; + const remainingSubscriptions = [] as Array>; for (const subscription of scopedSubscriptions) { - const callback = subscription.callback as unknown as EventCallback; - let response = withData ? callback(_state, withData) : callback(_state); - - // Async response - if (response instanceof Promise) { - response.then((result) => { - if (!result) return; - - dispatcher( - result, - subscription, - callbackCount, - remainingSubscriptions, - ); - }); - } + const callback = subscription.callback as unknown as EventListener; - // Sync response - else { - if (!response) return; + if (payload) { + callback(_state, payload); + } else { + callback(_state); + } - dispatcher( - response, - subscription, - callbackCount, - remainingSubscriptions, - ); + if (!subscription.once) { + remainingSubscriptions.push(subscription); } } @@ -355,10 +278,24 @@ export function ShapeX(initialState: T): ShapeXInstance { * * @returns {} The current state. */ - const state = (): T => { + const state = (): TState => { return _state; }; + /** + * Updates state. + * + * @param updatedState updated state + */ + const setState = (updatedState: TState): void => { + const changes = changedState(_state, updatedState); + _state = updatedState; + + for (let i = 0; i < changes.length; i++) { + dispatch(changes[i]); + } + } + return { subscribe, subscribeOnce, @@ -367,5 +304,6 @@ export function ShapeX(initialState: T): ShapeXInstance { subscriptions, dispatch, state, + setState, }; } -- cgit v1.2.3