From 477c3d973c3560417d1526660f51647db9b33bce Mon Sep 17 00:00:00 2001 From: jareill Date: Fri, 21 Jun 2024 13:12:46 -0700 Subject: [PATCH 01/12] createSatchel function --- src/actionCreator.ts | 4 +- src/createSatchel.ts | 247 ++++++++++++++++++ src/dispatcher.ts | 2 +- src/globalContext.ts | 2 +- src/interfaces/Mutator.ts | 10 + src/interfaces/Orchestrator.ts | 9 + .../{Subscriber.ts => SubscriberFunction.ts} | 4 +- 7 files changed, 272 insertions(+), 6 deletions(-) create mode 100644 src/createSatchel.ts create mode 100644 src/interfaces/Mutator.ts create mode 100644 src/interfaces/Orchestrator.ts rename src/interfaces/{Subscriber.ts => SubscriberFunction.ts} (69%) diff --git a/src/actionCreator.ts b/src/actionCreator.ts index 22fc128..0903c57 100644 --- a/src/actionCreator.ts +++ b/src/actionCreator.ts @@ -55,7 +55,7 @@ export function getPrivateActionId(target: any) { return target.__SATCHELJS_ACTION_ID; } -function setPrivateActionId(target: any, actionId: string) { +export function setPrivateActionId(target: any, actionId: string) { target.__SATCHELJS_ACTION_ID = actionId; } @@ -63,6 +63,6 @@ export function getPrivateActionType(target: any): string { return target.__SATCHELJS_ACTION_TYPE || 'unknown action'; } -function setActionType(target: any, actionType: string) { +export function setActionType(target: any, actionType: string) { target.__SATCHELJS_ACTION_TYPE = actionType; } diff --git a/src/createSatchel.ts b/src/createSatchel.ts new file mode 100644 index 0000000..db45e9a --- /dev/null +++ b/src/createSatchel.ts @@ -0,0 +1,247 @@ +import { observable, ObservableMap, transaction, action as mobxAction } from 'mobx'; +import { + getPrivateActionId, + getPrivateActionType, + setActionType, + setPrivateActionId, +} from './actionCreator'; +import ActionMessage from './interfaces/ActionMessage'; +import Middleware from './interfaces/Middleware'; +import DispatchFunction from './interfaces/DispatchFunction'; +import SubscriberFunction from './interfaces/SubscriberFunction'; +import ActionContext from './legacy/ActionContext'; +import ActionFunction from './legacy/ActionFunction'; +import ActionCreator from './interfaces/ActionCreator'; +import { Mutator } from './interfaces/Mutator'; +import { Orchestrator } from './interfaces/Orchestrator'; + +const schemaVersion = 3; + +type LegacySatchelProperties = { + legacyInDispatch: number; + legacyDispatchWithMiddleware: ( + action: ActionFunction, + actionType: string, + args: IArguments, + actionContext: ActionContext + ) => Promise | void; + legacyTestMode: boolean; +}; + +type SatchelState = { + schemaVersion: number; + rootStore: ObservableMap; + nextActionId: number; + subscriptions: { [key: string]: SubscriberFunction[] }; + currentMutator: string | null; +} & LegacySatchelProperties; + +export type SatchelInstance = { + /** + * Resolves the target of the subscriber and registers it with the dispatcher. + */ + register: ( + subscriber: Mutator | Orchestrator + ) => SubscriberFunction; + dispatch: (actionMessage: ActionMessage) => void; + createActionCreator: >( + actionType: string, + target: TActionCreator, + shouldDispatch: boolean + ) => TActionCreator; + action: = () => T>( + actionType: string, + target?: TActionCreator + ) => TActionCreator; + getRootStore: () => ObservableMap; + hasSubscribers: (actionCreator: ActionCreator) => boolean; +} & LegacySatchelProperties; + +export type SatchelOptions = { + // TODO: Add options here + middleware?: Array; +}; + +function getInitialSatchelState(): SatchelState { + return { + schemaVersion: schemaVersion, + rootStore: observable.map({}), + nextActionId: 0, + subscriptions: {}, + currentMutator: null, + legacyInDispatch: 0, + legacyDispatchWithMiddleware: null, + legacyTestMode: false, + }; +} + +const setPrivateSubscriberRegistered = (target: any, isRegistered: boolean) => { + target.__SATCHELJS_SUBSCRIBER_REGISTERED = isRegistered; +}; + +const getPrivateSubscriberRegistered = (target: any): boolean => { + return target.__SATCHELJS_SUBSCRIBER_REGISTERED; +}; + +export function createSatchel(options: SatchelOptions = {}): SatchelInstance { + const { middleware = [] } = options; + let { + subscriptions, + currentMutator, + nextActionId, + rootStore, + legacyTestMode, + legacyInDispatch, + legacyDispatchWithMiddleware, + } = getInitialSatchelState(); + // Private functions + const finalDispatch = (actionMessage: ActionMessage): void | Promise => { + let actionId = getPrivateActionId(actionMessage); + let subscribers = subscriptions[actionId]; + + if (subscribers) { + let promises: Promise[] = []; + + for (const subscriber of subscribers) { + let returnValue = subscriber(actionMessage); + if (returnValue) { + promises.push(returnValue); + } + } + + if (promises.length) { + return promises.length == 1 ? promises[0] : Promise.all(promises); + } + } + }; + + const dispatchWithMiddleware: DispatchFunction = middleware.reduce( + (next: DispatchFunction, m: Middleware) => m.bind(null, next), + finalDispatch + ); + + const createActionId = (): string => { + return (nextActionId++).toString(); + }; + + const wrapMutatorTarget = ({ + actionCreator, + target, + }: Mutator) => { + // Wrap the callback in a MobX action so it can modify the store + const actionType = getPrivateActionType(actionCreator); + return mobxAction(actionType, (actionMessage: TAction) => { + try { + currentMutator = actionType; + target(actionMessage); + currentMutator = null; + } catch (e) { + currentMutator = null; + throw e; + } + }); + }; + + // Public functions + const register = ( + subscriber: Mutator | Orchestrator + ): SubscriberFunction => { + if (getPrivateSubscriberRegistered(subscriber)) { + // If the subscriber is already registered, no-op. + return subscriber.target; + } + + const actionId = getPrivateActionId(subscriber.actionCreator); + if (!actionId) { + throw new Error(`A ${subscriber.type} can only subscribe to action creators.`); + } + + const wrappedTarget = + subscriber.type == 'mutator' ? wrapMutatorTarget(subscriber) : subscriber.target; + + if (!subscriptions[actionId]) { + subscriptions[actionId] = []; + } + + subscriptions[actionId].push(wrappedTarget); + // Mark the subscriber as registered + setPrivateSubscriberRegistered(subscriber, true); + + return subscriber.target; + }; + + const dispatch = (actionMessage: ActionMessage): void => { + if (currentMutator) { + throw new Error( + `Mutator (${currentMutator}) may not dispatch action (${actionMessage.type})` + ); + } + + transaction(dispatchWithMiddleware.bind(null, actionMessage)); + }; + + const createActionCreator = >( + actionType: string, + target: TActionCreator, + shouldDispatch: boolean + ): TActionCreator => { + let actionId = createActionId(); + + let decoratedTarget = function createAction(...args: any[]) { + // Create the action message + let actionMessage: ActionMessage = target ? target.apply(null, args) : {}; + + // Stamp the action type + if (actionMessage.type) { + throw new Error('Action creators should not include the type property.'); + } + + // Stamp the action message with the type and the private ID + actionMessage.type = actionType; + setPrivateActionId(actionMessage, actionId); + + // Dispatch if necessary + if (shouldDispatch) { + dispatch(actionMessage); + } + + return actionMessage; + } as TActionCreator; + + // Stamp the action creator function with the private ID + setPrivateActionId(decoratedTarget, actionId); + setActionType(decoratedTarget, actionType); + return decoratedTarget; + }; + + const action = < + T extends ActionMessage = {}, + TActionCreator extends ActionCreator = () => T + >( + actionType: string, + target?: TActionCreator + ): TActionCreator => { + return createActionCreator(actionType, target, true); + }; + + const getRootStore = (): ObservableMap => { + return rootStore; + }; + + const hasSubscribers = (actionCreator: ActionCreator) => { + return !!subscriptions[getPrivateActionId(actionCreator)]; + }; + + return { + register, + dispatch, + createActionCreator, + action, + getRootStore, + hasSubscribers, + // Legacy properties + legacyInDispatch, + legacyDispatchWithMiddleware, + legacyTestMode, + }; +} diff --git a/src/dispatcher.ts b/src/dispatcher.ts index 92d71f7..19d4f9e 100644 --- a/src/dispatcher.ts +++ b/src/dispatcher.ts @@ -1,6 +1,6 @@ import { transaction } from 'mobx'; import ActionMessage from './interfaces/ActionMessage'; -import Subscriber from './interfaces/Subscriber'; +import Subscriber from './interfaces/SubscriberFunction'; import { getPrivateActionId } from './actionCreator'; import { getGlobalContext } from './globalContext'; diff --git a/src/globalContext.ts b/src/globalContext.ts index 06fa566..bb71cd9 100644 --- a/src/globalContext.ts +++ b/src/globalContext.ts @@ -1,7 +1,7 @@ import { observable, ObservableMap } from 'mobx'; import ActionMessage from './interfaces/ActionMessage'; import DispatchFunction from './interfaces/DispatchFunction'; -import Subscriber from './interfaces/Subscriber'; +import Subscriber from './interfaces/SubscriberFunction'; import ActionContext from './legacy/ActionContext'; import ActionFunction from './legacy/ActionFunction'; diff --git a/src/interfaces/Mutator.ts b/src/interfaces/Mutator.ts new file mode 100644 index 0000000..1ae180b --- /dev/null +++ b/src/interfaces/Mutator.ts @@ -0,0 +1,10 @@ +import ActionCreator from './ActionCreator'; +import ActionMessage from './ActionMessage'; +import MutatorFunction from './MutatorFunction'; + +export type Mutator = { + type: 'mutator'; + actionCreator: ActionCreator; + target: MutatorFunction; + isRegistered: boolean; +}; diff --git a/src/interfaces/Orchestrator.ts b/src/interfaces/Orchestrator.ts new file mode 100644 index 0000000..d92316d --- /dev/null +++ b/src/interfaces/Orchestrator.ts @@ -0,0 +1,9 @@ +import ActionCreator from './ActionCreator'; +import ActionMessage from './ActionMessage'; +import OrchestratorFunction from './OrchestratorFunction'; + +export type Orchestrator = { + type: 'orchestrator'; + actionCreator: ActionCreator; + target: OrchestratorFunction; +}; diff --git a/src/interfaces/Subscriber.ts b/src/interfaces/SubscriberFunction.ts similarity index 69% rename from src/interfaces/Subscriber.ts rename to src/interfaces/SubscriberFunction.ts index 66cfbc3..1e15ccb 100644 --- a/src/interfaces/Subscriber.ts +++ b/src/interfaces/SubscriberFunction.ts @@ -2,7 +2,7 @@ import ActionMessage from './ActionMessage'; import MutatorFunction from './MutatorFunction'; import OrchestratorFunction from './OrchestratorFunction'; -type Subscriber = +type SubscriberFunction = | MutatorFunction | OrchestratorFunction; -export default Subscriber; +export default SubscriberFunction; From a49d1943d5e3ba651ba6e11ef19035eece14f9d0 Mon Sep 17 00:00:00 2001 From: jareill Date: Fri, 21 Jun 2024 13:53:44 -0700 Subject: [PATCH 02/12] Remove old unused code --- src/actionCreator.ts | 68 ------------------------------------- src/applyMiddleware.ts | 17 ---------- src/createActionId.ts | 5 --- src/createSatchel.ts | 36 ++++++++++++-------- src/createStore.ts | 18 ---------- src/dispatcher.ts | 46 ------------------------- src/getRootStore.ts | 11 ------ src/globalContext.ts | 68 ------------------------------------- src/index.ts | 7 ++-- src/interfaces/Mutator.ts | 1 - src/mutator.ts | 35 ++++--------------- src/orchestrator.ts | 17 ++++------ src/privatePropertyUtils.ts | 23 +++++++++++++ src/simpleSubscribers.ts | 22 ++++++++---- 14 files changed, 75 insertions(+), 299 deletions(-) delete mode 100644 src/actionCreator.ts delete mode 100644 src/applyMiddleware.ts delete mode 100644 src/createActionId.ts delete mode 100644 src/createStore.ts delete mode 100644 src/dispatcher.ts delete mode 100644 src/getRootStore.ts delete mode 100644 src/globalContext.ts create mode 100644 src/privatePropertyUtils.ts diff --git a/src/actionCreator.ts b/src/actionCreator.ts deleted file mode 100644 index 0903c57..0000000 --- a/src/actionCreator.ts +++ /dev/null @@ -1,68 +0,0 @@ -import ActionMessage from './interfaces/ActionMessage'; -import ActionCreator from './interfaces/ActionCreator'; -import { dispatch } from './dispatcher'; -import createActionId from './createActionId'; - -export function actionCreator< - T extends ActionMessage = {}, - TActionCreator extends ActionCreator = () => T ->(actionType: string, target?: TActionCreator): TActionCreator { - return createActionCreator(actionType, target, false); -} - -export function action< - T extends ActionMessage = {}, - TActionCreator extends ActionCreator = () => T ->(actionType: string, target?: TActionCreator): TActionCreator { - return createActionCreator(actionType, target, true); -} - -function createActionCreator>( - actionType: string, - target: TActionCreator, - shouldDispatch: boolean -): TActionCreator { - let actionId = createActionId(); - - let decoratedTarget = function createAction(...args: any[]) { - // Create the action message - let actionMessage: ActionMessage = target ? target.apply(null, args) : {}; - - // Stamp the action type - if (actionMessage.type) { - throw new Error('Action creators should not include the type property.'); - } - - // Stamp the action message with the type and the private ID - actionMessage.type = actionType; - setPrivateActionId(actionMessage, actionId); - - // Dispatch if necessary - if (shouldDispatch) { - dispatch(actionMessage); - } - - return actionMessage; - } as TActionCreator; - - // Stamp the action creator function with the private ID - setPrivateActionId(decoratedTarget, actionId); - setActionType(decoratedTarget, actionType); - return decoratedTarget; -} - -export function getPrivateActionId(target: any) { - return target.__SATCHELJS_ACTION_ID; -} - -export function setPrivateActionId(target: any, actionId: string) { - target.__SATCHELJS_ACTION_ID = actionId; -} - -export function getPrivateActionType(target: any): string { - return target.__SATCHELJS_ACTION_TYPE || 'unknown action'; -} - -export function setActionType(target: any, actionType: string) { - target.__SATCHELJS_ACTION_TYPE = actionType; -} diff --git a/src/applyMiddleware.ts b/src/applyMiddleware.ts deleted file mode 100644 index 15df81e..0000000 --- a/src/applyMiddleware.ts +++ /dev/null @@ -1,17 +0,0 @@ -import DispatchFunction from './interfaces/DispatchFunction'; -import Middleware from './interfaces/Middleware'; -import { finalDispatch } from './dispatcher'; -import { getGlobalContext } from './globalContext'; - -export default function applyMiddleware(...middleware: Middleware[]) { - var next: DispatchFunction = finalDispatch; - for (var i = middleware.length - 1; i >= 0; i--) { - next = applyNextMiddleware(middleware[i], next); - } - - getGlobalContext().dispatchWithMiddleware = next; -} - -function applyNextMiddleware(middleware: Middleware, next: DispatchFunction): DispatchFunction { - return middleware.bind(null, next); -} diff --git a/src/createActionId.ts b/src/createActionId.ts deleted file mode 100644 index 0286f42..0000000 --- a/src/createActionId.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { getGlobalContext } from './globalContext'; - -export default function createActionId(): string { - return (getGlobalContext().nextActionId++).toString(); -} diff --git a/src/createSatchel.ts b/src/createSatchel.ts index db45e9a..b92cf19 100644 --- a/src/createSatchel.ts +++ b/src/createSatchel.ts @@ -4,7 +4,9 @@ import { getPrivateActionType, setActionType, setPrivateActionId, -} from './actionCreator'; + getPrivateSubscriberRegistered, + setPrivateSubscriberRegistered, +} from './privatePropertyUtils'; import ActionMessage from './interfaces/ActionMessage'; import Middleware from './interfaces/Middleware'; import DispatchFunction from './interfaces/DispatchFunction'; @@ -15,8 +17,6 @@ import ActionCreator from './interfaces/ActionCreator'; import { Mutator } from './interfaces/Mutator'; import { Orchestrator } from './interfaces/Orchestrator'; -const schemaVersion = 3; - type LegacySatchelProperties = { legacyInDispatch: number; legacyDispatchWithMiddleware: ( @@ -29,7 +29,6 @@ type LegacySatchelProperties = { }; type SatchelState = { - schemaVersion: number; rootStore: ObservableMap; nextActionId: number; subscriptions: { [key: string]: SubscriberFunction[] }; @@ -53,18 +52,17 @@ export type SatchelInstance = { actionType: string, target?: TActionCreator ) => TActionCreator; + createStore: (key: string, initialState: T) => () => T; getRootStore: () => ObservableMap; hasSubscribers: (actionCreator: ActionCreator) => boolean; } & LegacySatchelProperties; export type SatchelOptions = { - // TODO: Add options here middleware?: Array; }; function getInitialSatchelState(): SatchelState { return { - schemaVersion: schemaVersion, rootStore: observable.map({}), nextActionId: 0, subscriptions: {}, @@ -75,14 +73,6 @@ function getInitialSatchelState(): SatchelState { }; } -const setPrivateSubscriberRegistered = (target: any, isRegistered: boolean) => { - target.__SATCHELJS_SUBSCRIBER_REGISTERED = isRegistered; -}; - -const getPrivateSubscriberRegistered = (target: any): boolean => { - return target.__SATCHELJS_SUBSCRIBER_REGISTERED; -}; - export function createSatchel(options: SatchelOptions = {}): SatchelInstance { const { middleware = [] } = options; let { @@ -94,7 +84,6 @@ export function createSatchel(options: SatchelOptions = {}): SatchelInstance { legacyInDispatch, legacyDispatchWithMiddleware, } = getInitialSatchelState(); - // Private functions const finalDispatch = (actionMessage: ActionMessage): void | Promise => { let actionId = getPrivateActionId(actionMessage); let subscribers = subscriptions[actionId]; @@ -232,6 +221,22 @@ export function createSatchel(options: SatchelOptions = {}): SatchelInstance { return !!subscriptions[getPrivateActionId(actionCreator)]; }; + const createStoreAction = mobxAction('createStore', function createStoreAction( + key: string, + initialState: any + ) { + if (getRootStore().get(key)) { + throw new Error(`A store named ${key} has already been created.`); + } + + getRootStore().set(key, initialState); + }); + + const createStore = (key: string, initialState: T): (() => T) => { + createStoreAction(key, initialState); + return () => getRootStore().get(key); + }; + return { register, dispatch, @@ -239,6 +244,7 @@ export function createSatchel(options: SatchelOptions = {}): SatchelInstance { action, getRootStore, hasSubscribers, + createStore, // Legacy properties legacyInDispatch, legacyDispatchWithMiddleware, diff --git a/src/createStore.ts b/src/createStore.ts deleted file mode 100644 index 4cee6c2..0000000 --- a/src/createStore.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { action } from 'mobx'; -import getRootStore from './getRootStore'; - -let createStoreAction = action('createStore', function createStoreAction( - key: string, - initialState: any -) { - if (getRootStore().get(key)) { - throw new Error(`A store named ${key} has already been created.`); - } - - getRootStore().set(key, initialState); -}); - -export default function createStore(key: string, initialState: T): () => T { - createStoreAction(key, initialState); - return () => getRootStore().get(key); -} diff --git a/src/dispatcher.ts b/src/dispatcher.ts deleted file mode 100644 index 19d4f9e..0000000 --- a/src/dispatcher.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { transaction } from 'mobx'; -import ActionMessage from './interfaces/ActionMessage'; -import Subscriber from './interfaces/SubscriberFunction'; -import { getPrivateActionId } from './actionCreator'; -import { getGlobalContext } from './globalContext'; - -export function subscribe(actionId: string, callback: Subscriber) { - let subscriptions = getGlobalContext().subscriptions; - if (!subscriptions[actionId]) { - subscriptions[actionId] = []; - } - - subscriptions[actionId].push(callback); -} - -export function dispatch(actionMessage: ActionMessage) { - const currentMutator = getGlobalContext().currentMutator; - if (currentMutator) { - throw new Error( - `Mutator (${currentMutator}) may not dispatch action (${actionMessage.type})` - ); - } - - let dispatchWithMiddleware = getGlobalContext().dispatchWithMiddleware || finalDispatch; - transaction(dispatchWithMiddleware.bind(null, actionMessage)); -} - -export function finalDispatch(actionMessage: ActionMessage): void | Promise { - let actionId = getPrivateActionId(actionMessage); - let subscribers = getGlobalContext().subscriptions[actionId]; - - if (subscribers) { - let promises: Promise[] = []; - - for (const subscriber of subscribers) { - let returnValue = subscriber(actionMessage); - if (returnValue) { - promises.push(returnValue); - } - } - - if (promises.length) { - return promises.length == 1 ? promises[0] : Promise.all(promises); - } - } -} diff --git a/src/getRootStore.ts b/src/getRootStore.ts deleted file mode 100644 index b85fb23..0000000 --- a/src/getRootStore.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* tslint:disable:no-unused-imports */ -import { ObservableMap } from 'mobx'; -/* tslint:enable:no-unused-imports */ -import { getGlobalContext } from './globalContext'; - -/** - * Satchel-provided root store getter - */ -export default function getRootStore() { - return getGlobalContext().rootStore; -} diff --git a/src/globalContext.ts b/src/globalContext.ts deleted file mode 100644 index bb71cd9..0000000 --- a/src/globalContext.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { observable, ObservableMap } from 'mobx'; -import ActionMessage from './interfaces/ActionMessage'; -import DispatchFunction from './interfaces/DispatchFunction'; -import Subscriber from './interfaces/SubscriberFunction'; -import ActionContext from './legacy/ActionContext'; -import ActionFunction from './legacy/ActionFunction'; - -const schemaVersion = 3; - -const globalObject = ((typeof globalThis !== 'undefined' - ? globalThis - : typeof window !== 'undefined' - ? window - : global) as unknown) as { - __satchelGlobalContext: GlobalContext; -}; - -// Interfaces for Global Context -export interface GlobalContext { - schemaVersion: number; - rootStore: ObservableMap; - nextActionId: number; - subscriptions: { [key: string]: Subscriber[] }; - dispatchWithMiddleware: DispatchFunction; - currentMutator: string | null; - - // Legacy properties - legacyInDispatch: number; - legacyDispatchWithMiddleware: ( - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext - ) => Promise | void; - legacyTestMode: boolean; -} - -// A reset global context function to be used INTERNALLY by SatchelJS tests and for initialization ONLY -export function __resetGlobalContext() { - globalObject.__satchelGlobalContext = { - schemaVersion: schemaVersion, - rootStore: observable.map({}), - nextActionId: 0, - subscriptions: {}, - dispatchWithMiddleware: null, - currentMutator: null, - legacyInDispatch: 0, - legacyDispatchWithMiddleware: null, - legacyTestMode: false, - }; -} - -export function ensureGlobalContextSchemaVersion() { - if (schemaVersion != globalObject.__satchelGlobalContext.schemaVersion) { - throw new Error('Detected incompatible SatchelJS versions loaded.'); - } -} - -export function getGlobalContext() { - return globalObject.__satchelGlobalContext; -} - -// Side Effects: actually initialize the global context if it is undefined -if (!globalObject.__satchelGlobalContext) { - __resetGlobalContext(); -} else { - ensureGlobalContextSchemaVersion(); -} diff --git a/src/index.ts b/src/index.ts index 8b90d4b..9d29704 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,13 +7,10 @@ export { default as DispatchFunction } from './interfaces/DispatchFunction'; export { default as Middleware } from './interfaces/Middleware'; export { default as MutatorFunction } from './interfaces/MutatorFunction'; export { default as OrchestratorFunction } from './interfaces/OrchestratorFunction'; -export { action, actionCreator } from './actionCreator'; -export { default as applyMiddleware } from './applyMiddleware'; -export { default as createStore } from './createStore'; -export { dispatch } from './dispatcher'; +export { Mutator } from './interfaces/Mutator'; +export { Orchestrator } from './interfaces/Orchestrator'; export { default as mutator } from './mutator'; import { default as orchestrator } from './orchestrator'; -export { default as getRootStore } from './getRootStore'; export { mutatorAction } from './simpleSubscribers'; export { useStrict }; diff --git a/src/interfaces/Mutator.ts b/src/interfaces/Mutator.ts index 1ae180b..99d72e6 100644 --- a/src/interfaces/Mutator.ts +++ b/src/interfaces/Mutator.ts @@ -6,5 +6,4 @@ export type Mutator = { type: 'mutator'; actionCreator: ActionCreator; target: MutatorFunction; - isRegistered: boolean; }; diff --git a/src/mutator.ts b/src/mutator.ts index 8a0851f..0a37dba 100644 --- a/src/mutator.ts +++ b/src/mutator.ts @@ -1,36 +1,15 @@ -import { action } from 'mobx'; import ActionCreator from './interfaces/ActionCreator'; import ActionMessage from './interfaces/ActionMessage'; import MutatorFunction from './interfaces/MutatorFunction'; -import { getPrivateActionType, getPrivateActionId } from './actionCreator'; -import { subscribe } from './dispatcher'; -import { getGlobalContext } from './globalContext'; +import { Mutator } from './interfaces/Mutator'; export default function mutator( actionCreator: ActionCreator, target: MutatorFunction -): MutatorFunction { - let actionId = getPrivateActionId(actionCreator); - if (!actionId) { - throw new Error('Mutators can only subscribe to action creators.'); - } - - // Wrap the callback in a MobX action so it can modify the store - const actionType = getPrivateActionType(actionCreator); - let wrappedTarget = action(actionType, (actionMessage: TAction) => { - const globalContext = getGlobalContext(); - try { - globalContext.currentMutator = actionType; - target(actionMessage); - globalContext.currentMutator = null; - } catch (e) { - globalContext.currentMutator = null; - throw e; - } - }); - - // Subscribe to the action - subscribe(actionId, wrappedTarget); - - return target; +): Mutator { + return { + type: 'mutator', + actionCreator, + target, + }; } diff --git a/src/orchestrator.ts b/src/orchestrator.ts index 4e42822..5a4a84e 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -1,18 +1,15 @@ import ActionCreator from './interfaces/ActionCreator'; import ActionMessage from './interfaces/ActionMessage'; import OrchestratorFunction from './interfaces/OrchestratorFunction'; -import { getPrivateActionId } from './actionCreator'; -import { subscribe } from './dispatcher'; +import { Orchestrator } from './interfaces/Orchestrator'; export default function orchestrator( actionCreator: ActionCreator, target: OrchestratorFunction -) { - let actionId = getPrivateActionId(actionCreator); - if (!actionId) { - throw new Error('Orchestrators can only subscribe to action creators.'); - } - - subscribe(actionId, target); - return target; +): Orchestrator { + return { + type: 'orchestrator', + actionCreator, + target, + }; } diff --git a/src/privatePropertyUtils.ts b/src/privatePropertyUtils.ts new file mode 100644 index 0000000..4e515ac --- /dev/null +++ b/src/privatePropertyUtils.ts @@ -0,0 +1,23 @@ +export function getPrivateActionId(target: any) { + return target.__SATCHELJS_ACTION_ID; +} + +export function setPrivateActionId(target: any, actionId: string) { + target.__SATCHELJS_ACTION_ID = actionId; +} + +export function getPrivateActionType(target: any): string { + return target.__SATCHELJS_ACTION_TYPE || 'unknown action'; +} + +export function setActionType(target: any, actionType: string) { + target.__SATCHELJS_ACTION_TYPE = actionType; +} + +export const setPrivateSubscriberRegistered = (target: any, isRegistered: boolean) => { + target.__SATCHELJS_SUBSCRIBER_REGISTERED = isRegistered; +}; + +export const getPrivateSubscriberRegistered = (target: any): boolean => { + return target.__SATCHELJS_SUBSCRIBER_REGISTERED; +}; diff --git a/src/simpleSubscribers.ts b/src/simpleSubscribers.ts index 48ccb06..b1eb4b7 100644 --- a/src/simpleSubscribers.ts +++ b/src/simpleSubscribers.ts @@ -1,24 +1,32 @@ import SimpleAction from './interfaces/SimpleAction'; -import { action } from './actionCreator'; import mutator from './mutator'; +import { SatchelInstance } from './createSatchel'; export function createSimpleSubscriber(decorator: Function) { return function simpleSubscriber any>( + satchelInstance: SatchelInstance, actionType: string, target: TFunction ): SimpleAction { // Create the action creator - let simpleActionCreator = action(actionType, function simpleActionCreator() { - return { - args: arguments, - }; - }); + let simpleActionCreator = satchelInstance.action( + actionType, + function simpleActionCreator() { + return { + args: arguments, + }; + } + ); // Create the subscriber - decorator(simpleActionCreator, function simpleSubscriberCallback(actionMessage: any) { + const subscriber = decorator(simpleActionCreator, function simpleSubscriberCallback( + actionMessage: any + ) { return target.apply(null, actionMessage.args); }); + satchelInstance.register(subscriber); + // Return a function that dispatches that action return (simpleActionCreator as any) as SimpleAction; }; From 9530fc7abca2753e10c7c4dabfc9aa842dbf3eaa Mon Sep 17 00:00:00 2001 From: jareill Date: Mon, 24 Jun 2024 12:22:00 -0700 Subject: [PATCH 03/12] Started tests --- src/createSatchel.ts | 78 +-- src/legacy/ActionContext.ts | 5 - src/legacy/ActionFunction.ts | 5 - src/legacy/LegacyDispatchFunction.ts | 13 - src/legacy/LegacyMiddleware.ts | 15 - src/legacy/RawAction.ts | 5 - src/legacy/action.ts | 65 --- src/legacy/createUndo.ts | 199 -------- src/legacy/dispatch.ts | 21 - src/legacy/functionInternals.ts | 21 - src/legacy/index.ts | 11 - src/legacy/legacyApplyMiddleware.ts | 43 -- src/legacy/promise/actionWrappers.ts | 36 -- src/legacy/promise/index.ts | 1 - src/legacy/promise/install.ts | 14 - src/legacy/promise/promiseMiddleware.ts | 39 -- src/legacy/react/index.ts | 1 - src/legacy/react/reactive.ts | 118 ----- src/legacy/select.ts | 58 --- src/legacy/stitch/index.ts | 1 - src/legacy/stitch/stitch.ts | 60 --- src/legacy/testMode.ts | 9 - src/legacy/trace/index.ts | 1 - src/legacy/trace/trace.ts | 29 -- src/privatePropertyUtils.ts | 4 + test/actionCreatorTests.ts | 46 +- test/applyMiddlewareTests.ts | 38 +- test/createActionIdTests.ts | 11 +- test/createStoreTests.ts | 20 +- test/dispatcherTests.ts | 78 ++- test/endToEndTests.ts | 87 ++-- test/globalContextTests.ts | 29 -- test/legacy/actionTests.ts | 99 ---- test/legacy/createUndoTests.ts | 467 ------------------ test/legacy/dispatchTests.ts | 72 --- test/legacy/legacyApplyMiddlewareTests.ts | 101 ---- test/legacy/promise/actionWrappersTests.ts | 80 --- test/legacy/promise/endToEndTests.ts | 74 --- test/legacy/promise/installTests.ts | 55 --- test/legacy/promise/promiseMiddlewareTests.ts | 106 ---- test/legacy/react/reactiveTests.tsx | 297 ----------- test/legacy/selectTests.ts | 222 --------- test/legacy/stitch/raiseActionTests.ts | 22 - test/legacy/stitch/raiseTests.ts | 37 -- test/legacy/stitch/stitchTests.ts | 87 ---- test/legacy/trace/traceTests.ts | 101 ---- test/mutatorTests.ts | 45 +- test/orchestratorTests.ts | 14 +- test/simpleSubscribersTests.ts | 20 +- test/utils/createTestSatchel.ts | 10 + 50 files changed, 222 insertions(+), 2848 deletions(-) delete mode 100644 src/legacy/ActionContext.ts delete mode 100644 src/legacy/ActionFunction.ts delete mode 100644 src/legacy/LegacyDispatchFunction.ts delete mode 100644 src/legacy/LegacyMiddleware.ts delete mode 100644 src/legacy/RawAction.ts delete mode 100644 src/legacy/action.ts delete mode 100644 src/legacy/createUndo.ts delete mode 100644 src/legacy/dispatch.ts delete mode 100644 src/legacy/functionInternals.ts delete mode 100644 src/legacy/index.ts delete mode 100644 src/legacy/legacyApplyMiddleware.ts delete mode 100644 src/legacy/promise/actionWrappers.ts delete mode 100644 src/legacy/promise/index.ts delete mode 100644 src/legacy/promise/install.ts delete mode 100644 src/legacy/promise/promiseMiddleware.ts delete mode 100644 src/legacy/react/index.ts delete mode 100644 src/legacy/react/reactive.ts delete mode 100644 src/legacy/select.ts delete mode 100644 src/legacy/stitch/index.ts delete mode 100644 src/legacy/stitch/stitch.ts delete mode 100644 src/legacy/testMode.ts delete mode 100644 src/legacy/trace/index.ts delete mode 100644 src/legacy/trace/trace.ts delete mode 100644 test/globalContextTests.ts delete mode 100644 test/legacy/actionTests.ts delete mode 100644 test/legacy/createUndoTests.ts delete mode 100644 test/legacy/dispatchTests.ts delete mode 100644 test/legacy/legacyApplyMiddlewareTests.ts delete mode 100644 test/legacy/promise/actionWrappersTests.ts delete mode 100644 test/legacy/promise/endToEndTests.ts delete mode 100644 test/legacy/promise/installTests.ts delete mode 100644 test/legacy/promise/promiseMiddlewareTests.ts delete mode 100644 test/legacy/react/reactiveTests.tsx delete mode 100644 test/legacy/selectTests.ts delete mode 100644 test/legacy/stitch/raiseActionTests.ts delete mode 100644 test/legacy/stitch/raiseTests.ts delete mode 100644 test/legacy/stitch/stitchTests.ts delete mode 100644 test/legacy/trace/traceTests.ts create mode 100644 test/utils/createTestSatchel.ts diff --git a/src/createSatchel.ts b/src/createSatchel.ts index b92cf19..a519a08 100644 --- a/src/createSatchel.ts +++ b/src/createSatchel.ts @@ -11,29 +11,16 @@ import ActionMessage from './interfaces/ActionMessage'; import Middleware from './interfaces/Middleware'; import DispatchFunction from './interfaces/DispatchFunction'; import SubscriberFunction from './interfaces/SubscriberFunction'; -import ActionContext from './legacy/ActionContext'; -import ActionFunction from './legacy/ActionFunction'; import ActionCreator from './interfaces/ActionCreator'; import { Mutator } from './interfaces/Mutator'; import { Orchestrator } from './interfaces/Orchestrator'; -type LegacySatchelProperties = { - legacyInDispatch: number; - legacyDispatchWithMiddleware: ( - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext - ) => Promise | void; - legacyTestMode: boolean; -}; - type SatchelState = { rootStore: ObservableMap; nextActionId: number; subscriptions: { [key: string]: SubscriberFunction[] }; currentMutator: string | null; -} & LegacySatchelProperties; +}; export type SatchelInstance = { /** @@ -43,10 +30,12 @@ export type SatchelInstance = { subscriber: Mutator | Orchestrator ) => SubscriberFunction; dispatch: (actionMessage: ActionMessage) => void; - createActionCreator: >( + actionCreator: < + T extends ActionMessage = {}, + TActionCreator extends ActionCreator = () => T + >( actionType: string, - target: TActionCreator, - shouldDispatch: boolean + target?: TActionCreator ) => TActionCreator; action: = () => T>( actionType: string, @@ -55,7 +44,15 @@ export type SatchelInstance = { createStore: (key: string, initialState: T) => () => T; getRootStore: () => ObservableMap; hasSubscribers: (actionCreator: ActionCreator) => boolean; -} & LegacySatchelProperties; +}; + +export type PrivateSatchelFunctions = { + __createActionId: () => string; + __dispatchWithMiddleware: DispatchFunction; + __finalDispatch: DispatchFunction; + __subscriptions: { [key: string]: SubscriberFunction[] }; + __currentMutator: string | null; +}; export type SatchelOptions = { middleware?: Array; @@ -67,24 +64,15 @@ function getInitialSatchelState(): SatchelState { nextActionId: 0, subscriptions: {}, currentMutator: null, - legacyInDispatch: 0, - legacyDispatchWithMiddleware: null, - legacyTestMode: false, }; } export function createSatchel(options: SatchelOptions = {}): SatchelInstance { const { middleware = [] } = options; - let { - subscriptions, - currentMutator, - nextActionId, - rootStore, - legacyTestMode, - legacyInDispatch, - legacyDispatchWithMiddleware, - } = getInitialSatchelState(); - const finalDispatch = (actionMessage: ActionMessage): void | Promise => { + let { subscriptions, currentMutator, nextActionId, rootStore } = getInitialSatchelState(); + const finalDispatch: DispatchFunction = ( + actionMessage: ActionMessage + ): void | Promise => { let actionId = getPrivateActionId(actionMessage); let subscribers = subscriptions[actionId]; @@ -104,7 +92,7 @@ export function createSatchel(options: SatchelOptions = {}): SatchelInstance { } }; - const dispatchWithMiddleware: DispatchFunction = middleware.reduce( + const dispatchWithMiddleware: DispatchFunction = middleware.reduceRight( (next: DispatchFunction, m: Middleware) => m.bind(null, next), finalDispatch ); @@ -203,6 +191,16 @@ export function createSatchel(options: SatchelOptions = {}): SatchelInstance { return decoratedTarget; }; + const actionCreator = < + T extends ActionMessage = {}, + TActionCreator extends ActionCreator = () => T + >( + actionType: string, + target?: TActionCreator + ): TActionCreator => { + return createActionCreator(actionType, target, false); + }; + const action = < T extends ActionMessage = {}, TActionCreator extends ActionCreator = () => T @@ -237,17 +235,21 @@ export function createSatchel(options: SatchelOptions = {}): SatchelInstance { return () => getRootStore().get(key); }; - return { + const satchelInstance: SatchelInstance & PrivateSatchelFunctions = { register, dispatch, - createActionCreator, + actionCreator, action, getRootStore, hasSubscribers, createStore, - // Legacy properties - legacyInDispatch, - legacyDispatchWithMiddleware, - legacyTestMode, + // Private functions used only for testing + __createActionId: createActionId, + __dispatchWithMiddleware: dispatchWithMiddleware, + __finalDispatch: finalDispatch, + __subscriptions: subscriptions, + __currentMutator: currentMutator, }; + + return satchelInstance; } diff --git a/src/legacy/ActionContext.ts b/src/legacy/ActionContext.ts deleted file mode 100644 index f256db4..0000000 --- a/src/legacy/ActionContext.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface ActionContext { - [key: string]: any; -} - -export default ActionContext; diff --git a/src/legacy/ActionFunction.ts b/src/legacy/ActionFunction.ts deleted file mode 100644 index ac47e1a..0000000 --- a/src/legacy/ActionFunction.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface ActionFunction { - (): Promise | void; -} - -export default ActionFunction; diff --git a/src/legacy/LegacyDispatchFunction.ts b/src/legacy/LegacyDispatchFunction.ts deleted file mode 100644 index d581309..0000000 --- a/src/legacy/LegacyDispatchFunction.ts +++ /dev/null @@ -1,13 +0,0 @@ -import ActionContext from './ActionContext'; -import ActionFunction from './ActionFunction'; - -interface DispatchFunction { - ( - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext - ): Promise | void; -} - -export default DispatchFunction; diff --git a/src/legacy/LegacyMiddleware.ts b/src/legacy/LegacyMiddleware.ts deleted file mode 100644 index 1713648..0000000 --- a/src/legacy/LegacyMiddleware.ts +++ /dev/null @@ -1,15 +0,0 @@ -import ActionContext from './ActionContext'; -import DispatchFunction from './LegacyDispatchFunction'; -import ActionFunction from './ActionFunction'; - -interface Middleware { - ( - next: DispatchFunction, - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext - ): void; -} - -export default Middleware; diff --git a/src/legacy/RawAction.ts b/src/legacy/RawAction.ts deleted file mode 100644 index fdbd83f..0000000 --- a/src/legacy/RawAction.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface RawAction { - (...args: any[]): Promise | void; -} - -export default RawAction; diff --git a/src/legacy/action.ts b/src/legacy/action.ts deleted file mode 100644 index cb55522..0000000 --- a/src/legacy/action.ts +++ /dev/null @@ -1,65 +0,0 @@ -import ActionContext from './ActionContext'; -import dispatch from './dispatch'; -import RawAction from './RawAction'; -import { setActionType, setOriginalTarget } from './functionInternals'; - -export interface ActionFactory { - (target: T): T; - ( - target: any, - propertyKey: string, - descriptor: TypedPropertyDescriptor - ): void; -} - -export default function action(actionType: string, actionContext?: ActionContext): ActionFactory { - return function createAction(arg0: any, arg1: any, arg2: any) { - if (arguments.length == 1 && typeof arg0 == 'function') { - return wrapFunctionInAction(arg0, actionType, actionContext); - } else { - decorateClassMethod(arg0, arg1, arg2, actionType, actionContext); - } - } as ActionFactory; -} - -function wrapFunctionInAction( - target: T, - actionType: string, - actionContext: ActionContext -): T { - let decoratedTarget: T = function() { - let returnValue: any; - let passedArguments = arguments; - - dispatch( - () => { - returnValue = target.apply(this, passedArguments); - return returnValue; - }, - actionType, - arguments, - actionContext - ); - - return returnValue; - }; - - setOriginalTarget(decoratedTarget, target); - setActionType(decoratedTarget, actionType); - - return decoratedTarget; -} - -function decorateClassMethod( - target: any, - propertyKey: string, - descriptor: TypedPropertyDescriptor, - actionType: string, - actionContext: ActionContext -) { - if (descriptor && typeof descriptor.value == 'function') { - descriptor.value = wrapFunctionInAction(descriptor.value, actionType, actionContext); - } else { - throw new Error('The @action decorator can only apply to class methods.'); - } -} diff --git a/src/legacy/createUndo.ts b/src/legacy/createUndo.ts deleted file mode 100644 index afce726..0000000 --- a/src/legacy/createUndo.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { Lambda, spy } from 'mobx'; -import satcheljsAction from './action'; - -let spyRefCount = 0; -let spyDisposer: Lambda = null; - -function initializeSpy() { - if (spyRefCount === 0) { - spyDisposer = spy(spyOnChanges); - } - spyRefCount++; -} - -function disposeSpy() { - spyRefCount--; - if (spyRefCount === 0) { - spyDisposer(); - spyDisposer = null; - } -} - -interface UndoStep { - verify: () => boolean; - objectName: string; - propertyName: string; - undo: () => void; -} - -interface UndoWindow { - steps: UndoStep[]; -} - -// We may have nested "undo" actions, so we need to track those windows separately -let undoWindows: UndoWindow[] = []; - -function spyOnChanges(event: any) { - let undoStep: UndoStep; - let modifiedObject = event.object; - - switch (event.type) { - case 'update': - if (event.index !== undefined) { - // update (array) - undoStep = { - verify: () => modifiedObject[event.index] === event.newValue, - objectName: event.name, - propertyName: event.index, - undo: () => { - modifiedObject[event.index] = event.oldValue; - }, - }; - } else if (typeof modifiedObject.get !== 'undefined') { - // update (map) - undoStep = { - verify: () => modifiedObject.get(event.key) === event.newValue, - objectName: event.name, - propertyName: event.key, - undo: () => { - modifiedObject.set(event.key, event.oldValue); - }, - }; - } else { - // update (object) - undoStep = { - verify: () => modifiedObject[event.key] === event.newValue, - objectName: event.name, - propertyName: event.key, - undo: () => { - modifiedObject[event.key] = event.oldValue; - }, - }; - } - break; - case 'splice': - undoStep = { - verify: () => { - for (let i = 0; i < event.addedCount; i++) { - if (modifiedObject[event.index + i] !== event.added[i]) { - return false; - } - } - return true; - }, - objectName: event.name, - propertyName: event.index, - undo: () => { - // First, remove the added items. - // Then, add items back one at a time, because passing an array in to 'splice' will insert the array as a single item - modifiedObject.splice(event.index, event.addedCount); - for (let i = 0; i < event.removedCount; i++) { - modifiedObject.splice(event.index + i, 0, event.removed[i]); - } - }, - }; - break; - case 'add': - if (typeof modifiedObject.get !== 'undefined') { - // add (map) - undoStep = { - verify: () => modifiedObject.get(event.key) === event.newValue, - objectName: event.name, - propertyName: event.key, - undo: () => { - modifiedObject.delete(event.key); - }, - }; - } else { - // add (object) - undoStep = { - verify: () => modifiedObject[event.key] === event.newValue, - objectName: event.name, - propertyName: event.key, - undo: () => { - delete modifiedObject[event.key]; - }, - }; - } - break; - case 'delete': - undoStep = { - verify: () => !modifiedObject.has(event.key), - objectName: event.name, - propertyName: event.key, - undo: () => { - modifiedObject.set(event.key, event.oldValue); - }, - }; - break; - default: - // Nothing worth tracking - return; - } - - undoWindows.forEach(undoWindow => undoWindow.steps.push(undoStep)); -} - -export interface UndoResult { - actionReturnValue?: T; - (): void; -} - -export type CreateUndoReturnValue = (action: () => T | void) => UndoResult; - -function trackUndo( - actionName: string, - action: () => T, - undoVerifiesChanges: boolean -): UndoResult { - initializeSpy(); - undoWindows.push({ steps: [] }); - - try { - let returnValue: T = action(); - - let undoWindow: UndoWindow = undoWindows[undoWindows.length - 1]; - let undoPreviouslyExecuted = false; - - // Reverse the steps, as changes made later in the action may depend on changes earlier in the action - undoWindow.steps.reverse(); - - let undo: UndoResult = satcheljsAction(`undo-${actionName}`)(() => { - if (undoPreviouslyExecuted) { - throw `This instance of undo-${actionName} has already been executed`; - } - if (undoVerifiesChanges) { - undoWindow.steps.forEach(step => { - if (!step.verify()) { - throw `Property "${step.propertyName} on store object "${step.objectName} changed since action was performed.`; - } - }); - } - undoWindow.steps.forEach(step => step.undo()); - undoPreviouslyExecuted = true; - }); - - undo.actionReturnValue = returnValue; - - return undo; - } finally { - undoWindows.pop(); - disposeSpy(); - } -} - -/** - * Creates a function to undo all store changes done by a supplied action - * @param {string} actionName is the name of the action being tracked. The returned undo action will be given the name undo-. - * @param {boolean} undoVerifiesChanges indicates whether the returned undo action will verify no subsequent changes have been made to - * objects being tracked since the original action has been performed. If true and changes have since been made to modified objects, - * the undo action will not make any changes and will throw an exception. - */ -export default function createUndo( - actionName: string, - undoVerifiesChanges?: boolean -): CreateUndoReturnValue { - return (action: () => T) => { - return trackUndo(actionName, action, !!undoVerifiesChanges); - }; -} diff --git a/src/legacy/dispatch.ts b/src/legacy/dispatch.ts deleted file mode 100644 index 0391ebb..0000000 --- a/src/legacy/dispatch.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { action as mobxAction } from 'mobx'; -import ActionContext from './ActionContext'; -import ActionFunction from './ActionFunction'; -import { dispatchWithMiddleware } from './legacyApplyMiddleware'; -import { getGlobalContext } from '../globalContext'; - -export default function dispatch( - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext -): void { - getGlobalContext().legacyInDispatch++; - - mobxAction( - actionType ? actionType : '(anonymous action)', - dispatchWithMiddleware.bind(null, action, actionType, args, actionContext) - )(); - - getGlobalContext().legacyInDispatch--; -} diff --git a/src/legacy/functionInternals.ts b/src/legacy/functionInternals.ts deleted file mode 100644 index a278a72..0000000 --- a/src/legacy/functionInternals.ts +++ /dev/null @@ -1,21 +0,0 @@ -import RawAction from './RawAction'; - -export function setOriginalTarget(decoratedTarget: any, originalTarget: any) { - decoratedTarget.__SATCHELJS_ORIGINAL_TARGET = originalTarget; -} - -export function getOriginalTarget(decoratedTarget: any) { - if (typeof decoratedTarget.__SATCHELJS_ORIGINAL_TARGET !== typeof undefined) { - return decoratedTarget.__SATCHELJS_ORIGINAL_TARGET; - } - - return undefined; -} - -export function setActionType(decoratedTarget: any, actionType: string) { - decoratedTarget.__SATCHELJS_ACTION_TYPE = actionType; -} - -export function getActionType(decoratedTarget: RawAction) { - return (decoratedTarget).__SATCHELJS_ACTION_TYPE; -} diff --git a/src/legacy/index.ts b/src/legacy/index.ts deleted file mode 100644 index 2540a66..0000000 --- a/src/legacy/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Legacy API -export { default as legacyApplyMiddleware } from './legacyApplyMiddleware'; -export { default as LegacyMiddleware } from './LegacyMiddleware'; -export { default as ActionFunction } from './ActionFunction'; -export { default as ActionContext } from './ActionContext'; -export { default as LegacyDispatchFunction } from './LegacyDispatchFunction'; -export { default as action } from './action'; -export { default as select, SelectorFunction } from './select'; -export { default as createUndo, UndoResult, CreateUndoReturnValue } from './createUndo'; -export { getActionType } from './functionInternals'; -export { initializeTestMode, resetTestMode } from './testMode'; diff --git a/src/legacy/legacyApplyMiddleware.ts b/src/legacy/legacyApplyMiddleware.ts deleted file mode 100644 index 4204212..0000000 --- a/src/legacy/legacyApplyMiddleware.ts +++ /dev/null @@ -1,43 +0,0 @@ -import ActionContext from './ActionContext'; -import ActionFunction from './ActionFunction'; -import LegacyDispatchFunction from './LegacyDispatchFunction'; -import LegacyMiddleware from './LegacyMiddleware'; -import { getGlobalContext } from '../globalContext'; - -export default function applyMiddleware(...middleware: LegacyMiddleware[]) { - var next: LegacyDispatchFunction = finalDispatch; - for (var i = middleware.length - 1; i >= 0; i--) { - next = applyMiddlewareInternal(middleware[i], next); - } - - getGlobalContext().legacyDispatchWithMiddleware = next; -} - -function applyMiddlewareInternal( - middleware: LegacyMiddleware, - next: LegacyDispatchFunction -): LegacyDispatchFunction { - return middleware.bind(null, next); -} - -export function dispatchWithMiddleware( - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext -) { - if (!getGlobalContext().legacyDispatchWithMiddleware) { - getGlobalContext().legacyDispatchWithMiddleware = finalDispatch; - } - - getGlobalContext().legacyDispatchWithMiddleware(action, actionType, args, actionContext); -} - -function finalDispatch( - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext -) { - return action(); -} diff --git a/src/legacy/promise/actionWrappers.ts b/src/legacy/promise/actionWrappers.ts deleted file mode 100644 index 41fa28a..0000000 --- a/src/legacy/promise/actionWrappers.ts +++ /dev/null @@ -1,36 +0,0 @@ -import action from '../action'; -import { getCurrentAction } from './promiseMiddleware'; - -export function wrapThen(originalThen: any) { - return function wrappedThen(onFulfilled?: Function, onRejected?: Function) { - return originalThen.call( - this, - wrapInAction(onFulfilled, 'then'), - wrapInAction(onRejected, 'then_rejected') - ); - }; -} - -export function wrapCatch(originalCatch: any) { - return function wrappedCatch(onRejected?: Function) { - return originalCatch.call(this, wrapInAction(onRejected, 'catch')); - }; -} - -function wrapInAction(callback: Function, callbackType: string) { - let currentAction = getCurrentAction(); - if (!currentAction || !callback) { - return callback; - } - - let actionName = currentAction + ' => ' + callbackType; - return function() { - let returnValue; - let args = arguments; - action(actionName)(() => { - returnValue = callback.apply(null, args); - })(); - - return returnValue; - }; -} diff --git a/src/legacy/promise/index.ts b/src/legacy/promise/index.ts deleted file mode 100644 index 9a47297..0000000 --- a/src/legacy/promise/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { promiseMiddleware } from './promiseMiddleware'; diff --git a/src/legacy/promise/install.ts b/src/legacy/promise/install.ts deleted file mode 100644 index bbc7149..0000000 --- a/src/legacy/promise/install.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { wrapThen, wrapCatch } from './actionWrappers'; - -export default function install() { - let originalThen = Promise.prototype.then; - let originalCatch = Promise.prototype.catch; - - Promise.prototype.then = wrapThen(originalThen); - Promise.prototype.catch = wrapCatch(originalCatch); - - return function uninstall() { - Promise.prototype.then = originalThen; - Promise.prototype.catch = originalCatch; - }; -} diff --git a/src/legacy/promise/promiseMiddleware.ts b/src/legacy/promise/promiseMiddleware.ts deleted file mode 100644 index fb9f665..0000000 --- a/src/legacy/promise/promiseMiddleware.ts +++ /dev/null @@ -1,39 +0,0 @@ -import LegacyDispatchFunction from '../LegacyDispatchFunction'; -import ActionFunction from '../ActionFunction'; -import ActionContext from '../ActionContext'; -import install from './install'; - -let actionStack: string[] = []; -let isInstalled = false; -let uninstall: () => void; - -export function getCurrentAction() { - return actionStack.length ? actionStack[actionStack.length - 1] : null; -} - -export function promiseMiddleware( - next: LegacyDispatchFunction, - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext -) { - // If we're not already installed, install now - if (!isInstalled) { - uninstall = install(); - isInstalled = true; - } - - try { - actionStack.push(actionType); - return next(action, actionType, args, actionContext); - } finally { - actionStack.pop(); - - // If we're no longer in an action, uninstall - if (!actionStack.length) { - uninstall(); - isInstalled = false; - } - } -} diff --git a/src/legacy/react/index.ts b/src/legacy/react/index.ts deleted file mode 100644 index f089a55..0000000 --- a/src/legacy/react/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as reactive, ReactiveTarget } from './reactive'; diff --git a/src/legacy/react/reactive.ts b/src/legacy/react/reactive.ts deleted file mode 100644 index f73e8cb..0000000 --- a/src/legacy/react/reactive.ts +++ /dev/null @@ -1,118 +0,0 @@ -import * as React from 'react'; - -import { SelectorFunction } from '../select'; -import { getGlobalContext } from '../../globalContext'; -import { observer } from 'mobx-react'; - -export interface ReactiveTarget extends React.ClassicComponentClass { - nonReactiveComponent?: React.ComponentClass; - nonReactiveStatelessComponent?: React.StatelessComponent; -} - -function setPropAccessors(props: any, selector: SelectorFunction) { - let newProps: any = {}; - - Object.keys(props).forEach(key => { - newProps[key] = props[key]; - }); - - Object.keys(selector).forEach((key: string) => { - let getter = selector[key as keyof T]; - - if (typeof newProps[key] === typeof undefined) { - Object.defineProperty(newProps, key, { - enumerable: true, - get: () => getter.call(null, newProps), - }); - } - }); - - return newProps; -} - -function createNewConstructor( - original: React.ComponentClass, - selector: SelectorFunction -): React.ComponentClass | React.Component { - if (!selector) { - return original; - } - - return class extends React.Component { - render() { - return React.createElement(original, setPropAccessors(this.props, selector)); - } - }; -} - -function createNewFunctionalComponent( - original: React.StatelessComponent, - selector: SelectorFunction -) { - if (!selector) { - return original; - } - - return function(props: any) { - let newProps = setPropAccessors(props, selector); - return (original).call(original, newProps); - }; -} - -function isReactComponent(target: any) { - return target && target.prototype && target.prototype.isReactComponent; -} - -function isFunction(target: any) { - return target instanceof Function; -} - -/** - * Reactive decorator - */ -export default function reactive( - selectorOrComponentClass?: SelectorFunction | React.ComponentClass -): any { - // this check only applies to ES6 React Class Components - if (isReactComponent(selectorOrComponentClass)) { - let componentClass = selectorOrComponentClass as React.ComponentClass; - return observer(componentClass); - } - - return function(target: Target) { - if (getGlobalContext().legacyTestMode) { - if (isReactComponent(target)) { - return observer(target as React.ComponentClass); - } else if (isFunction(target)) { - return observer(target as React.StatelessComponent); - } - - return target; - } - - let newComponent: any; - - if (isReactComponent(target)) { - // Double layer of observer here so that mobx will flow down the observation - newComponent = observer( - createNewConstructor( - observer(target as React.ComponentClass), - selectorOrComponentClass as SelectorFunction - ) as React.ComponentClass - ); - newComponent.nonReactiveComponent = target as React.ComponentClass; - return newComponent; - } else if (isFunction(target)) { - newComponent = observer( - createNewFunctionalComponent( - target as React.StatelessComponent, - selectorOrComponentClass as SelectorFunction - ) - ); - newComponent.nonReactiveStatelessComponent = target as React.StatelessComponent; - return newComponent; - } - - return newComponent; - }; -} diff --git a/src/legacy/select.ts b/src/legacy/select.ts deleted file mode 100644 index 27e77ba..0000000 --- a/src/legacy/select.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Reaction, IObservableValue, isObservableArray } from 'mobx'; -import { getOriginalTarget, getActionType, setActionType } from './functionInternals'; -import { getGlobalContext } from '../globalContext'; - -export type SelectorFunction = { [key in keyof T]?: (...args: any[]) => T[key] }; - -function createCursorFromSelector(selector: SelectorFunction, args?: any) { - let state: any = {}; - - Object.keys(selector).forEach((key: string) => { - if (typeof state[key] === typeof undefined) { - Object.defineProperty(state, key, { - enumerable: true, - get: () => selector[key as keyof T].apply(null, args), - }); - } - }); - - Object.freeze(state); - - return state; -} - -/** - * Decorator for action functions. Selects a subset from the state tree for the action. - */ -export default function select(selector: SelectorFunction) { - return function decorator(target: Target): Target { - // do not execute the selector function in test mode, simply returning - // the target that was passed in - if (getGlobalContext().legacyTestMode) { - return target; - } - - let context = this; - let argumentPosition = target.length - 1; - let actionTarget = getOriginalTarget(target); - - if (actionTarget) { - argumentPosition = actionTarget.length - 1; - } - - let returnValue: any = function() { - let state = createCursorFromSelector(selector, arguments); - let args = Array.prototype.slice.call(arguments); - if (typeof args[argumentPosition] === typeof undefined) { - for (var i = args.length; i < argumentPosition; i++) { - args[i] = undefined; - } - args[argumentPosition] = state; - } - return target.apply(context, args); - }; - - setActionType(returnValue, getActionType(target)); - return returnValue; - }; -} diff --git a/src/legacy/stitch/index.ts b/src/legacy/stitch/index.ts deleted file mode 100644 index bb8c407..0000000 --- a/src/legacy/stitch/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { stitch, subscribe, raise, raiseAction } from './stitch'; diff --git a/src/legacy/stitch/stitch.ts b/src/legacy/stitch/stitch.ts deleted file mode 100644 index 0ef3a4c..0000000 --- a/src/legacy/stitch/stitch.ts +++ /dev/null @@ -1,60 +0,0 @@ -import action from '../action'; -import ActionFunction from '../ActionFunction'; -import LegacyDispatchFunction from '../LegacyDispatchFunction'; -import ActionContext from '../ActionContext'; - -export interface ActionHandler { - (...args: any[]): Promise | void; -} - -// Keep track of all handlers that have been registered -let handlers: { [key: string]: ActionHandler[] } = {}; - -// The actual middleware function: after dispatching an action, calls any callbacks that are subscribed to that action -export function stitch( - next: LegacyDispatchFunction, - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext -) { - let returnValue = next(action, actionType, args, actionContext); - - if (actionType && handlers[actionType]) { - handlers[actionType].forEach(handler => handler.apply(null, args)); - } - - return returnValue; -} - -// Subscribe to an action -export function subscribe(actionType: string, callback: T) { - if (!handlers[actionType]) { - handlers[actionType] = []; - } - - handlers[actionType].push(callback); -} - -export function raise( - actionType: string, - callback?: (actionToExecute: T) => void -) { - console.error("[satcheljs-stitch] The 'raise' API is deprecated. Use 'raiseAction' instead."); - - // Create a no-op action to execute - let actionToExecute = action(actionType)(() => {}); - - if (callback) { - // Pass it to the callback so that the consumer can call it with arguments - callback(actionToExecute); - } else { - // No callback was provided, so just execute it with no arguments - actionToExecute(); - } -} - -export function raiseAction(actionType: string): T { - // Create a no-op action to execute - return action(actionType)(() => {}); -} diff --git a/src/legacy/testMode.ts b/src/legacy/testMode.ts deleted file mode 100644 index a233f1c..0000000 --- a/src/legacy/testMode.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { getGlobalContext } from '../globalContext'; - -export function initializeTestMode() { - getGlobalContext().legacyTestMode = true; -} - -export function resetTestMode() { - getGlobalContext().legacyTestMode = false; -} diff --git a/src/legacy/trace/index.ts b/src/legacy/trace/index.ts deleted file mode 100644 index cca0eea..0000000 --- a/src/legacy/trace/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as trace } from './trace'; diff --git a/src/legacy/trace/trace.ts b/src/legacy/trace/trace.ts deleted file mode 100644 index feb221f..0000000 --- a/src/legacy/trace/trace.ts +++ /dev/null @@ -1,29 +0,0 @@ -import ActionFunction from '../ActionFunction'; -import LegacyDispatchFunction from '../LegacyDispatchFunction'; -import ActionContext from '../ActionContext'; - -let depth = 0; - -export default function trace( - next: LegacyDispatchFunction, - action: ActionFunction, - actionType: string, - args: IArguments, - actionContext: ActionContext -) { - log('Executing action: ' + (actionType ? actionType : '(anonymous action)')); - - try { - depth++; - return next(action, actionType, args, actionContext); - } finally { - depth--; - } -} - -function log(message: string) { - let indentation = new Array(depth + 1).join(' '); - /* tslint:disable:no-console */ - console.log(indentation + message); - /* tslint:enable:no-console */ -} diff --git a/src/privatePropertyUtils.ts b/src/privatePropertyUtils.ts index 4e515ac..46bd41d 100644 --- a/src/privatePropertyUtils.ts +++ b/src/privatePropertyUtils.ts @@ -21,3 +21,7 @@ export const setPrivateSubscriberRegistered = (target: any, isRegistered: boolea export const getPrivateSubscriberRegistered = (target: any): boolean => { return target.__SATCHELJS_SUBSCRIBER_REGISTERED; }; + +export const setPrivateFunction = (property: string, target: any, func: any) => { + target[property] = func; +}; diff --git a/test/actionCreatorTests.ts b/test/actionCreatorTests.ts index c7dbe66..e58ee13 100644 --- a/test/actionCreatorTests.ts +++ b/test/actionCreatorTests.ts @@ -1,17 +1,12 @@ import 'jasmine'; -import { - action, - actionCreator, - getPrivateActionType, - getPrivateActionId, -} from '../src/actionCreator'; -import * as createActionId from '../src/createActionId'; -import * as dispatcher from '../src/dispatcher'; +import { createTestSatchel } from './utils/createTestSatchel'; +import { getPrivateActionType, getPrivateActionId } from '../src/privatePropertyUtils'; describe('actionCreator', () => { it('returns the created action message', () => { // Arrange - const testAction = actionCreator('testAction', (arg0, arg1) => { + const satchel = createTestSatchel(); + const testAction = satchel.actionCreator('testAction', (arg0, arg1) => { return { arg0, arg1, @@ -28,7 +23,8 @@ describe('actionCreator', () => { it('returns a default action message if no factory is provided', () => { // Arrange - const testAction = actionCreator('testAction'); + const satchel = createTestSatchel(); + const testAction = satchel.actionCreator('testAction'); // Act let actionMessage = testAction(); @@ -39,9 +35,10 @@ describe('actionCreator', () => { it('stamps the action message with the type and private action ID', () => { // Arrange - spyOn(createActionId, 'default').and.returnValue('id0'); + const satchel = createTestSatchel(); + spyOn(satchel, '__createActionId').and.returnValue('id0'); let actionType = 'testAction'; - const testAction = actionCreator(actionType); + const testAction = satchel.actionCreator(actionType); // Act let actionMessage = testAction(); @@ -53,19 +50,21 @@ describe('actionCreator', () => { it('does not dispatch the action message', () => { // Arrange - const testAction = actionCreator('testAction'); - spyOn(dispatcher, 'dispatch'); + const satchel = createTestSatchel(); + const testAction = satchel.actionCreator('testAction'); + spyOn(satchel, 'dispatch'); // Act testAction(); // Assert - expect(dispatcher.dispatch).not.toHaveBeenCalled(); + expect(satchel.dispatch).not.toHaveBeenCalled(); }); it('throws if the action message already has a type', () => { // Arrange - const testAction = actionCreator('testAction', () => { + const satchel = createTestSatchel(); + const testAction = satchel.actionCreator('testAction', () => { return { type: 'testAction' }; }); @@ -75,10 +74,11 @@ describe('actionCreator', () => { it('gets stamped with the private action ID', () => { // Arrange - spyOn(createActionId, 'default').and.returnValue('id1'); + const satchel = createTestSatchel(); + spyOn(satchel, '__createActionId').and.returnValue('id1'); // Act - const testAction = actionCreator('testAction'); + const testAction = satchel.actionCreator('testAction'); // Assert expect(getPrivateActionId(testAction)).toBe('id1'); @@ -86,7 +86,8 @@ describe('actionCreator', () => { it('gets stamped with the action type', () => { // Act - const testAction = actionCreator('testAction'); + const satchel = createTestSatchel(); + const testAction = satchel.actionCreator('testAction'); // Assert expect(getPrivateActionType(testAction)).toBe('testAction'); @@ -97,13 +98,14 @@ describe('action', () => { it('dispatches the action message', () => { // Arrange let actionMessage = {}; - const testAction = action('testAction', () => actionMessage); - spyOn(dispatcher, 'dispatch'); + const satchel = createTestSatchel(); + const testAction = satchel.action('testAction', () => actionMessage); + spyOn(satchel, 'dispatch'); // Act testAction(); // Assert - expect(dispatcher.dispatch).toHaveBeenCalledWith(actionMessage); + expect(satchel.dispatch).toHaveBeenCalledWith(actionMessage); }); }); diff --git a/test/applyMiddlewareTests.ts b/test/applyMiddlewareTests.ts index f308179..b704473 100644 --- a/test/applyMiddlewareTests.ts +++ b/test/applyMiddlewareTests.ts @@ -1,17 +1,14 @@ import 'jasmine'; -import applyMiddleware from '../src/applyMiddleware'; -import * as dispatcher from '../src/dispatcher'; -import { getGlobalContext, __resetGlobalContext } from '../src/globalContext'; +import { createTestSatchel } from './utils/createTestSatchel'; describe('applyMiddleware', () => { it('updates dispatchWithMiddleware to point to the middleware pipeline', () => { // Arrange - __resetGlobalContext(); let testMiddleware = jasmine.createSpy('testMiddleware'); + const satchel = createTestSatchel({ middleware: [testMiddleware] }); // Act - applyMiddleware(testMiddleware); - getGlobalContext().dispatchWithMiddleware({}); + satchel.__dispatchWithMiddleware({}); // Assert expect(testMiddleware).toHaveBeenCalled(); @@ -19,35 +16,29 @@ describe('applyMiddleware', () => { it('the action message and next delegate get passed to middleware', () => { // Arrange - __resetGlobalContext(); - let dispatchedActionMessage = {}; let actualNext; let actualActionMessage; - applyMiddleware((next: any, actionMessage: any) => { + const testMiddleware = (next: any, actionMessage: any) => { actualNext = next; actualActionMessage = actionMessage; - }); + }; + const satchel = createTestSatchel({ middleware: [testMiddleware] }); // Act - getGlobalContext().dispatchWithMiddleware(dispatchedActionMessage); + satchel.__dispatchWithMiddleware(dispatchedActionMessage); // Assert expect(actualActionMessage).toBe(dispatchedActionMessage); - expect(actualNext).toBe(dispatcher.finalDispatch); + expect(actualNext).toBe(satchel.__finalDispatch); }); it('middleware and finalDispatch get called in order', () => { // Arrange - __resetGlobalContext(); let sequence: string[] = []; - spyOn(dispatcher, 'finalDispatch').and.callFake(() => { - sequence.push('finalDispatch'); - }); - - applyMiddleware( + const middleware = [ (next: any, actionMessage: any) => { sequence.push('middleware1'); next(actionMessage); @@ -55,11 +46,16 @@ describe('applyMiddleware', () => { (next: any, actionMessage: any) => { sequence.push('middleware2'); next(actionMessage); - } - ); + }, + ]; + const satchel = createTestSatchel({ middleware }); + + spyOn(satchel, '__finalDispatch').and.callFake(() => { + sequence.push('finalDispatch'); + }); // Act - getGlobalContext().dispatchWithMiddleware({}); + satchel.__dispatchWithMiddleware({}); // Assert expect(sequence).toEqual(['middleware1', 'middleware2', 'finalDispatch']); diff --git a/test/createActionIdTests.ts b/test/createActionIdTests.ts index 9d6d82e..b6d182d 100644 --- a/test/createActionIdTests.ts +++ b/test/createActionIdTests.ts @@ -1,15 +1,14 @@ import 'jasmine'; -import createActionId from '../src/createActionId'; -import { __resetGlobalContext } from '../src/globalContext'; +import { createTestSatchel } from './utils/createTestSatchel'; describe('createActionId', () => { it('returns the next incremental ID for each call', () => { // Arrange - __resetGlobalContext(); + const satchel = createTestSatchel(); // Act / Assert - expect(createActionId()).toBe('0'); - expect(createActionId()).toBe('1'); - expect(createActionId()).toBe('2'); + expect(satchel.__createActionId()).toBe('0'); + expect(satchel.__createActionId()).toBe('1'); + expect(satchel.__createActionId()).toBe('2'); }); }); diff --git a/test/createStoreTests.ts b/test/createStoreTests.ts index 8d1a1fb..89b03c8 100644 --- a/test/createStoreTests.ts +++ b/test/createStoreTests.ts @@ -1,38 +1,34 @@ import 'jasmine'; -import getRootStore from '../src/getRootStore'; -import createStore from '../src/createStore'; -import { __resetGlobalContext } from '../src/globalContext'; +import { createSatchel } from '../src/createSatchel'; describe('createStore', () => { - beforeEach(function() { - __resetGlobalContext(); - }); - it('creates a subtree under rootStore', () => { // Arrange + const satchel = createSatchel(); let initialState = { testProp: 'testValue' }; // Act - let store = createStore('testStore', initialState)(); + let store = satchel.createStore('testStore', initialState)(); // Assert expect(store).toEqual(initialState); - expect(getRootStore().get('testStore')).toEqual(initialState); + expect(satchel.getRootStore().get('testStore')).toEqual(initialState); }); it('prevents creating a store with the same name', () => { // Arrange + const satchel = createSatchel(); let initialState = { testProp: 'testValue' }; let secondaryState = { testProp: 'overwritten' }; // Act - createStore('testStore', initialState)(); + satchel.createStore('testStore', initialState)(); // Assert - expect(() => createStore('testStore', secondaryState)()).toThrow( + expect(() => satchel.createStore('testStore', secondaryState)()).toThrow( 'A store named testStore has already been created.' ); - expect(getRootStore().get('testStore')).toEqual(initialState); + expect(satchel.getRootStore().get('testStore')).toEqual(initialState); }); }); diff --git a/test/dispatcherTests.ts b/test/dispatcherTests.ts index 7b4dd27..b2f7af8 100644 --- a/test/dispatcherTests.ts +++ b/test/dispatcherTests.ts @@ -1,28 +1,17 @@ import 'jasmine'; -import * as actionCreator from '../src/actionCreator'; -import * as dispatcher from '../src/dispatcher'; -import * as globalContext from '../src/globalContext'; +import { createTestSatchel } from './utils/createTestSatchel'; +import * as privateUtils from '../src/privatePropertyUtils'; describe('dispatcher', () => { - let mockGlobalContext: any; - - beforeEach(() => { - mockGlobalContext = { - subscriptions: {}, - dispatchWithMiddleware: jasmine.createSpy('dispatchWithMiddleware'), - currentMutator: null, - }; - - spyOn(globalContext, 'getGlobalContext').and.returnValue(mockGlobalContext); - }); - + /* it('subscribe registers a callback for a given action', () => { // Arrange - let actionId = 'testActionId'; - let callback = () => {}; + const satchel = createTestSatchel(); + const callback = () => ({}); + const action = satchel.actionCreator('TEST_ACTION', callback); // Act - dispatcher.subscribe(actionId, callback); + satchel.register(action); // Assert expect(mockGlobalContext.subscriptions[actionId]).toBeDefined(); @@ -43,56 +32,44 @@ describe('dispatcher', () => { // Assert expect(mockGlobalContext.subscriptions[actionId]).toEqual([callback0, callback1]); }); + */ it('dispatch calls dispatchWithMiddleware', () => { // Arrange let actionMessage = {}; + const satchel = createTestSatchel(); // Act - dispatcher.dispatch(actionMessage); - - // Assert - expect(mockGlobalContext.dispatchWithMiddleware).toHaveBeenCalledWith(actionMessage); - }); - - it('dispatch calls finalDispatch if dispatchWithMiddleware is null', () => { - // Arrange - mockGlobalContext.dispatchWithMiddleware = null; - let actionId = 'testActionId'; - spyOn(actionCreator, 'getPrivateActionId').and.returnValue(actionId); - - let callback = jasmine.createSpy('callback0'); - mockGlobalContext.subscriptions[actionId] = [callback]; - - // Act - dispatcher.dispatch({}); + satchel.dispatch(actionMessage); // Assert - expect(callback).toHaveBeenCalled(); + expect(satchel.__dispatchWithMiddleware).toHaveBeenCalledWith(actionMessage); }); it('dispatch throws if called within a mutator', () => { // Arrange - mockGlobalContext.currentMutator = 'SomeAction'; + const satchel = createTestSatchel(); + satchel.__currentMutator = 'SomeAction'; // Act / Assert expect(() => { - dispatcher.dispatch({}); + satchel.dispatch({}); }).toThrow(); }); it('finalDispatch calls all subscribers for a given action', () => { // Arrange + const satchel = createTestSatchel(); let actionMessage = {}; let actionId = 'testActionId'; - spyOn(actionCreator, 'getPrivateActionId').and.returnValue(actionId); + spyOn(privateUtils, 'getPrivateActionId').and.returnValue(actionId); let callback0 = jasmine.createSpy('callback0'); let callback1 = jasmine.createSpy('callback1'); - mockGlobalContext.subscriptions[actionId] = [callback0, callback1]; + satchel.__subscriptions[actionId] = [callback0, callback1]; // Act - dispatcher.finalDispatch(actionMessage); + satchel.__finalDispatch(actionMessage); // Assert expect(callback0).toHaveBeenCalledWith(actionMessage); @@ -101,25 +78,27 @@ describe('dispatcher', () => { it('finalDispatch handles the case where there are no subscribers', () => { // Arrange - spyOn(actionCreator, 'getPrivateActionId').and.returnValue('testActionId'); + const satchel = createTestSatchel(); + spyOn(privateUtils, 'getPrivateActionId').and.returnValue('testActionId'); // Act / Assert expect(() => { - dispatcher.finalDispatch({}); + satchel.__finalDispatch({}); }).not.toThrow(); }); it('if one subscriber returns a Promise, finalDispatch returns it', () => { // Arrange + const satchel = createTestSatchel(); let actionId = 'testActionId'; - spyOn(actionCreator, 'getPrivateActionId').and.returnValue(actionId); + spyOn(privateUtils, 'getPrivateActionId').and.returnValue(actionId); let promise = Promise.resolve(); let callback = () => promise; - mockGlobalContext.subscriptions[actionId] = [callback]; + satchel.__subscriptions[actionId] = [callback]; // Act - let returnValue = dispatcher.finalDispatch({}); + let returnValue = satchel.__finalDispatch({}); // Assert expect(returnValue).toBe(promise); @@ -127,20 +106,21 @@ describe('dispatcher', () => { it('if multiple subscribers returns Promises, finalDispatch returns an aggregate Promise', () => { // Arrange + const satchel = createTestSatchel(); let actionId = 'testActionId'; - spyOn(actionCreator, 'getPrivateActionId').and.returnValue(actionId); + spyOn(privateUtils, 'getPrivateActionId').and.returnValue(actionId); let promise1 = Promise.resolve(); let callback1 = () => promise1; let promise2 = Promise.resolve(); let callback2 = () => promise2; - mockGlobalContext.subscriptions[actionId] = [callback1, callback2]; + satchel.__subscriptions[actionId] = [callback1, callback2]; let aggregatePromise = Promise.resolve(); spyOn(Promise, 'all').and.returnValue(aggregatePromise); // Act - let returnValue = dispatcher.finalDispatch({}); + let returnValue = satchel.__finalDispatch({}); // Assert expect(Promise.all).toHaveBeenCalledWith([promise1, promise2]); diff --git a/test/endToEndTests.ts b/test/endToEndTests.ts index 4f8fec8..e0398f2 100644 --- a/test/endToEndTests.ts +++ b/test/endToEndTests.ts @@ -1,36 +1,28 @@ import 'jasmine'; import { autorun } from 'mobx'; -import { __resetGlobalContext } from '../src/globalContext'; -import { - action, - applyMiddleware, - createStore, - dispatch, - mutator, - mutatorAction, - orchestrator, -} from '../src/index'; +import { mutator, mutatorAction, orchestrator } from '../src/index'; +import { createTestSatchel } from './utils/createTestSatchel'; describe('satcheljs', () => { - beforeEach(function() { - __resetGlobalContext(); - }); - it('mutators subscribe to actions', () => { + const satchel = createTestSatchel(); let actualValue; // Create an action creator - let testAction = action('testAction', function testAction(value: string) { + let testAction = satchel.action('testAction', function testAction(value: string) { return { value: value, }; }); // Create a mutator that subscribes to it - mutator(testAction, function(actionMessage: any) { + const testMutator = mutator(testAction, function(actionMessage: any) { actualValue = actionMessage.value; }); + // Register the mutator + satchel.register(testMutator); + // Dispatch the action testAction('test'); @@ -40,16 +32,18 @@ describe('satcheljs', () => { it('mutatorAction dispatches an action and subscribes to it', () => { // Arrange + const satchel = createTestSatchel(); let arg1Value; let arg2Value; - let testMutatorAction = mutatorAction('testMutatorAction', function testMutatorAction( - arg1: string, - arg2: number - ) { - arg1Value = arg1; - arg2Value = arg2; - }); + let testMutatorAction = mutatorAction( + satchel, + 'testMutatorAction', + function testMutatorAction(arg1: string, arg2: number) { + arg1Value = arg1; + arg2Value = arg2; + } + ); // Act testMutatorAction('testValue', 2); @@ -61,9 +55,10 @@ describe('satcheljs', () => { it('mutators can modify the store', () => { // Arrange - let store = createStore('testStore', { testProperty: 'testValue' })(); + const satchel = createTestSatchel(); + let store = satchel.createStore('testStore', { testProperty: 'testValue' })(); autorun(() => store.testProperty); // strict mode only applies if store is observed - let modifyStore = action('modifyStore'); + let modifyStore = satchel.action('modifyStore'); mutator(modifyStore, () => { store.testProperty = 'newValue'; @@ -78,9 +73,10 @@ describe('satcheljs', () => { it('orchestrators cannot modify the store', () => { // Arrange - let store = createStore('testStore', { testProperty: 'testValue' })(); + const satchel = createTestSatchel(); + let store = satchel.createStore('testStore', { testProperty: 'testValue' })(); autorun(() => store.testProperty); // strict mode only applies if store is observed - let modifyStore = action('modifyStore'); + let modifyStore = satchel.action('modifyStore'); orchestrator(modifyStore, () => { store.testProperty = 'newValue'; @@ -94,8 +90,9 @@ describe('satcheljs', () => { it('all subscribers are handled in one transaction', () => { // Arrange - let store = createStore('testStore', { testProperty: 0 })(); - let modifyStore = action('modifyStore'); + const satchel = createTestSatchel(); + let store = satchel.createStore('testStore', { testProperty: 0 })(); + let modifyStore = satchel.action('modifyStore'); mutator(modifyStore, () => { store.testProperty++; @@ -122,13 +119,17 @@ describe('satcheljs', () => { let actualValue; let expectedValue = { type: 'testMiddleware' }; - applyMiddleware((next, actionMessage) => { - actualValue = actionMessage; - next(actionMessage); - }); + const middleware = [ + (next: any, actionMessage: any) => { + actualValue = actionMessage; + next(actionMessage); + }, + ]; + + const satchel = createTestSatchel({ middleware }); // Act - dispatch(expectedValue); + satchel.dispatch(expectedValue); // Assert expect(actualValue).toBe(expectedValue); @@ -136,14 +137,16 @@ describe('satcheljs', () => { it('middleware can handle promises returned from orchestrators', async () => { // Arrange - let testAction = action('testAction'); - orchestrator(testAction, () => Promise.resolve(1)); - orchestrator(testAction, () => Promise.resolve(2)); - - let returnedPromise; - applyMiddleware((next, actionMessage) => { - returnedPromise = next(actionMessage); - }); + let returnedPromise: Promise>; + const middleware = [ + (next: any, actionMessage: any) => { + returnedPromise = next(actionMessage); + }, + ]; + const satchel = createTestSatchel({ middleware }); + let testAction = satchel.action('testAction'); + satchel.register(orchestrator(testAction, () => Promise.resolve(1))); + satchel.register(orchestrator(testAction, () => Promise.resolve(2))); // Act testAction(); diff --git a/test/globalContextTests.ts b/test/globalContextTests.ts deleted file mode 100644 index 7ae4b41..0000000 --- a/test/globalContextTests.ts +++ /dev/null @@ -1,29 +0,0 @@ -import 'jasmine'; -import { isObservableMap } from 'mobx'; -import { - __resetGlobalContext, - getGlobalContext, - ensureGlobalContextSchemaVersion, -} from '../src/globalContext'; - -describe('globalContext', () => { - beforeEach(() => { - __resetGlobalContext(); - }); - - it('will throw error if the wrong schema version is detected', () => { - // Arrange - getGlobalContext().schemaVersion = -999; - - // Act / Assert - expect(ensureGlobalContextSchemaVersion).toThrow(); - }); - - it('rootStore is an ObservableMap', () => { - // Act - let rootStore = getGlobalContext().rootStore; - - // Assert - expect(isObservableMap(rootStore)).toBeTruthy(); - }); -}); diff --git a/test/legacy/actionTests.ts b/test/legacy/actionTests.ts deleted file mode 100644 index 53d3bca..0000000 --- a/test/legacy/actionTests.ts +++ /dev/null @@ -1,99 +0,0 @@ -import 'jasmine'; -import action from '../../src/legacy/action'; -import * as dispatchImports from '../../src/legacy/dispatch'; -import { getGlobalContext } from '../../src/globalContext'; -import { getActionType } from '../../src/legacy/functionInternals'; - -describe('action', () => { - it('wraps the function call in a dispatch', () => { - let testFunctionCalled = false; - let testFunction = (a: string) => { - testFunctionCalled = true; - }; - - spyOn(dispatchImports, 'default').and.callThrough(); - - testFunction = action('testFunction')(testFunction); - testFunction('testArgument'); - - expect(testFunctionCalled).toBeTruthy(); - expect(dispatchImports.default).toHaveBeenCalledTimes(1); - - // The second argument to dispatch should be the actionType - expect((dispatchImports.default).calls.argsFor(0)[1]).toBe('testFunction'); - - // The third argument to dispatch should be the IArguments object for the action - expect((dispatchImports.default).calls.argsFor(0)[2].length).toBe(1); - expect((dispatchImports.default).calls.argsFor(0)[2][0]).toBe('testArgument'); - }); - - it('sets the actionType as a property on the wrapped action', () => { - let testFunction = () => {}; - let actionType = 'testFunction'; - - let wrappedAction = action(actionType)(testFunction); - - expect(getActionType(wrappedAction)).toBe(actionType); - }); - - it('passes on the original arguments', () => { - let passedArguments: IArguments; - - let testFunction = function(a: number, b: number) { - passedArguments = arguments; - }; - - testFunction = action('testFunction')(testFunction); - testFunction(0, 1); - - expect(passedArguments[0]).toEqual(0); - expect(passedArguments[1]).toEqual(1); - }); - - it('returns the original return value', () => { - /* tslint:disable:promise-must-complete */ - let originalReturnValue = new Promise(() => {}); - /* tslint:enable:promise-must-complete */ - - let testFunction = function() { - return originalReturnValue; - }; - - testFunction = action('testFunction')(testFunction); - let returnValue = testFunction(); - - expect(returnValue).toBe(originalReturnValue); - }); - - it('can decorate a class method', () => { - let thisValue; - let inDispatchValue; - - class TestClass { - @action('testMethod') - testMethod() { - thisValue = this; - inDispatchValue = getGlobalContext().legacyInDispatch; - } - } - - let testInstance = new TestClass(); - testInstance.testMethod(); - - expect(thisValue).toBe(testInstance); - expect(inDispatchValue).toBe(1); - }); - - it('sets the actionType as a property on the wrapped class method', () => { - let actionType = 'testFunction'; - - class TestClass { - @action(actionType) - testMethod() {} - } - - let testInstance = new TestClass(); - - expect(getActionType(testInstance.testMethod)).toBe(actionType); - }); -}); diff --git a/test/legacy/createUndoTests.ts b/test/legacy/createUndoTests.ts deleted file mode 100644 index 0476f6d..0000000 --- a/test/legacy/createUndoTests.ts +++ /dev/null @@ -1,467 +0,0 @@ -import 'jasmine'; -import action from '../../src/legacy/action'; -import createUndo, { UndoResult } from '../../src/legacy/createUndo'; -import { extendObservable, observable, _resetGlobalState } from 'mobx'; -import { __resetGlobalContext } from '../../src/globalContext'; - -function resetState() { - _resetGlobalState(); - __resetGlobalContext(); -} - -describe('createUndo', () => { - beforeEach(resetState); - - describe('undo without verification', () => { - beforeEach(resetState); - - it('undoes an update to an array', () => { - let index = 1; - let newValue = 5; - let oldValue = 2; - - let array = observable([1, oldValue, 3]); - let undoableAction = action('updateArray')(() => { - array[index] = newValue; - }); - - let undoResult = createUndo('updateArray')(undoableAction); - - expect(array[index]).toBe(newValue); - - undoResult(); - - expect(array[index]).toBe(oldValue); - }); - - it('undoes an update to a map', () => { - let index = 'key'; - let newValue = 5; - let oldValue = 2; - - let object = observable.map({ key: oldValue }); - let undoableAction = action('updateMap')(() => { - object.set(index, newValue); - }); - - let undoResult = createUndo('updateMap')(undoableAction); - - expect(object.get(index)).toBe(newValue); - - undoResult(); - - expect(object.get(index)).toBe(oldValue); - }); - - it('undoes an update to an object', () => { - let newValue = 5; - let oldValue = 2; - - let object = observable({ key: oldValue }); - let undoableAction = action('updateObject')(() => { - object.key = newValue; - }); - - let undoResult = createUndo('updateObject')(undoableAction); - - expect(object.key).toBe(newValue); - - undoResult(); - - expect(object.key).toBe(oldValue); - }); - - it('undoes an array splice', () => { - let object = observable([1, 2, 3, 4, 5, 6] as any[]); - let undoableAction = action('spliceArray')(() => { - object.splice(2, 3, 'a'); - }); - - let undoResult = createUndo('spliceArray')(undoableAction); - - expect(object.slice(0)).toEqual([1, 2, 'a', 6]); - - undoResult(); - - expect(object.slice(0)).toEqual([1, 2, 3, 4, 5, 6]); - }); - - it('undoes an add to a map', () => { - let index = 'key'; - let newValue = 5; - - let object = observable.map({}); - let undoableAction = action('addMap')(() => { - object.set(index, newValue); - }); - - let undoResult = createUndo('addMap')(undoableAction); - - expect(object.get(index)).toBe(newValue); - - undoResult(); - - expect(object.has(index)).toBeFalsy; - }); - - it('undoes an add to an object', () => { - let index = 'key'; - let newValue = 5; - - let object: any = observable({}); - let undoableAction = action('addObject')(() => { - extendObservable(object, { [index]: newValue }); - }); - - let undoResult = createUndo('addObject')(undoableAction); - - expect(object[index]).toBe(newValue); - - undoResult(); - - expect(Object.getOwnPropertyNames(object)).not.toContain(index); - }); - - it('undoes a delete to a map', () => { - let index = 'key'; - let oldValue = 5; - - let object = observable.map({ [index]: oldValue }); - let undoableAction = action('deleteMap')(() => { - object.delete(index); - }); - - let undoResult = createUndo('deleteMap')(undoableAction); - - expect(object.has(index)).toBeFalsy; - - undoResult(); - - expect(object.get(index)).toBe(oldValue); - }); - }); - - describe('undo with passing verification', () => { - beforeEach(resetState); - - it('undoes an update to an array', () => { - let index = 1; - let newValue = 5; - let oldValue = 2; - - let array = observable([1, oldValue, 3]); - let undoableAction = action('updateArray')(() => { - array[index] = newValue; - }); - - let undoResult = createUndo('updateArray', true)(undoableAction); - - expect(array[index]).toBe(newValue); - - undoResult(); - - expect(array[index]).toBe(oldValue); - }); - - it('undoes an update to a map', () => { - let index = 'key'; - let newValue = 5; - let oldValue = 2; - - let object = observable.map({ key: oldValue }); - let undoableAction = action('updateMap')(() => { - object.set(index, newValue); - }); - - let undoResult = createUndo('updateMap', true)(undoableAction); - - expect(object.get(index)).toBe(newValue); - - undoResult(); - - expect(object.get(index)).toBe(oldValue); - }); - - it('undoes an update to an object', () => { - let newValue = 5; - let oldValue = 2; - - let object = observable({ key: oldValue }); - let undoableAction = action('updateObject')(() => { - object.key = newValue; - }); - - let undoResult = createUndo('updateObject', true)(undoableAction); - - expect(object.key).toBe(newValue); - - undoResult(); - - expect(object.key).toBe(oldValue); - }); - - it('undoes an array splice', () => { - let object = observable([1, 2, 3, 4, 5, 6] as any[]); - let undoableAction = action('spliceArray')(() => { - object.splice(2, 3, 'a'); - }); - - let undoResult = createUndo('spliceArray', true)(undoableAction); - - expect(object.slice(0)).toEqual([1, 2, 'a', 6]); - - undoResult(); - - expect(object.slice(0)).toEqual([1, 2, 3, 4, 5, 6]); - }); - - it('undoes an add to a map', () => { - let index = 'key'; - let newValue = 5; - - let object = observable.map({}); - let undoableAction = action('addMap')(() => { - object.set(index, newValue); - }); - - let undoResult = createUndo('addMap', true)(undoableAction); - - expect(object.get(index)).toBe(newValue); - - undoResult(); - - expect(object.has(index)).toBeFalsy; - }); - - it('undoes an add to an object', () => { - let index = 'key'; - let newValue = 5; - - let object: any = observable({}); - let undoableAction = action('addObject')(() => { - extendObservable(object, { [index]: newValue }); - }); - - let undoResult = createUndo('addObject', true)(undoableAction); - - expect(object[index]).toBe(newValue); - - undoResult(); - - expect(Object.getOwnPropertyNames(object)).not.toContain(index); - }); - - it('undoes a delete to a map', () => { - let index = 'key'; - let oldValue = 5; - - let object = observable.map({ [index]: oldValue }); - let undoableAction = action('deleteMap')(() => { - object.delete(index); - }); - - let undoResult = createUndo('deleteMap', true)(undoableAction); - - expect(object.has(index)).toBeFalsy; - - undoResult(); - - expect(object.get(index)).toBe(oldValue); - }); - }); - - describe('undo with failing verification', () => { - beforeEach(resetState); - - it('throws an exception when it undoes an update to an array', () => { - let index = 1; - let newValue = 5; - let oldValue = 2; - - let array = observable([1, oldValue, 3]); - let undoableAction = action('updateArray')(() => { - array[index] = newValue; - }); - - let undoResult = createUndo('updateArray', true)(undoableAction); - action('updateArray-again')(() => { - array[index] = 100; - })(); - - expect(undoResult).toThrow(); - }); - - it('throws an exception when it undoes an update to a map', () => { - let index = 'key'; - let newValue = 5; - let oldValue = 2; - - let object = observable.map({ key: oldValue }); - let undoableAction = action('updateMap')(() => { - object.set(index, newValue); - }); - - let undoResult = createUndo('updateMap', true)(undoableAction); - action('updateMap-again')(() => { - object.set(index, 100); - })(); - - expect(undoResult).toThrow(); - }); - - it('throws an exception when it undoes an update to an object', () => { - let newValue = 5; - let oldValue = 2; - - let object = observable({ key: oldValue }); - let undoableAction = action('updateObject')(() => { - object.key = newValue; - }); - - let undoResult = createUndo('updateObject', true)(undoableAction); - action('updateObject-again')(() => { - object.key = 100; - })(); - - expect(undoResult).toThrow(); - }); - - it('throws an exception when it undoes an array splice', () => { - let object = observable([1, 2, 3, 4, 5, 6] as any[]); - let undoableAction = action('spliceArray')(() => { - object.splice(2, 3, 'a'); - }); - - let undoResult = createUndo('spliceArray', true)(undoableAction); - action('spliceArray-again')(() => { - object[2] = 100; - })(); - - expect(undoResult).toThrow(); - }); - - it('throws an exception when it undoes an add to a map', () => { - let index = 'key'; - let newValue = 5; - - let object = observable.map({}); - let undoableAction = action('addMap')(() => { - object.set(index, newValue); - }); - - let undoResult = createUndo('addMap', true)(undoableAction); - action('addMap-again')(() => { - object.set(index, 100); - })(); - - expect(undoResult).toThrow(); - }); - - it('throws an exception when it undoes an add to an object', () => { - let index = 'key'; - let newValue = 5; - - let object: any = observable({}); - let undoableAction = action('addObject')(() => { - extendObservable(object, { [index]: newValue }); - }); - - let undoResult = createUndo('addObject', true)(undoableAction); - action('addMap-again')(() => { - object[index] = 100; - })(); - - expect(undoResult).toThrow(); - }); - - it('throws an exception when it undoes a delete to a map', () => { - let index = 'key'; - let oldValue = 5; - - let object = observable.map({ [index]: oldValue }); - let undoableAction = action('deleteMap')(() => { - object.delete(index); - }); - - let undoResult = createUndo('deleteMap', true)(undoableAction); - action('deleteMap-again')(() => { - object.set(index, 100); - })(); - - expect(undoResult).toThrow(); - }); - }); - - it('handles nested undo windows', () => { - let array = observable([1, 2, 3, 4, 5]); - - let outerUndo = createUndo('outerUndo')( - action('outerUndo')(() => { - array[0] = 0; - - expect(array.slice(0)).toEqual([0, 2, 3, 4, 5]); - - let innerUndo = createUndo('innerUndo')( - action('innerUndo')(() => { - array[1] = 0; - }) - ); - - array[2] = 0; - - expect(array.slice(0)).toEqual([0, 0, 0, 4, 5]); - - innerUndo(); - - expect(array.slice(0)).toEqual([0, 2, 0, 4, 5]); - }) - ); - - outerUndo(); - - expect(array.slice(0)).toEqual([1, 2, 3, 4, 5]); - }); - - it('throws if an undo instance is called twice', () => { - let object = observable.map({ key: 2 }); - let undoableAction = action('updateMap')(() => { - object.set('key', 5); - }); - - let undoResult = createUndo('updateMap')(undoableAction); - undoResult(); - - expect(undoResult).toThrow(); - }); - - it('handles nested undo windows called out of order', () => { - let array = observable([1, 2, 3, 4, 5]); - - let innerUndo: Function; - let outerUndo = createUndo('outerUndo')( - action('outerUndo')(() => { - array[0] = 0; - - expect(array.slice(0)).toEqual([0, 2, 3, 4, 5]); - - innerUndo = createUndo('innerUndo')( - action('innerUndo')(() => { - array[1] = 0; - }) - ); - - array[2] = 0; - - expect(array.slice(0)).toEqual([0, 0, 0, 4, 5]); - }) - ); - - outerUndo(); - - expect(array.slice(0)).toEqual([1, 2, 3, 4, 5]); - - innerUndo && innerUndo(); - - expect(array.slice(0)).toEqual([1, 2, 3, 4, 5]); - }); -}); diff --git a/test/legacy/dispatchTests.ts b/test/legacy/dispatchTests.ts deleted file mode 100644 index 7634da8..0000000 --- a/test/legacy/dispatchTests.ts +++ /dev/null @@ -1,72 +0,0 @@ -import 'jasmine'; -import { autorun, _resetGlobalState } from 'mobx'; - -import getRootStore from '../../src/getRootStore'; -import dispatch from '../../src/legacy/dispatch'; -import * as legacyApplyMiddlewareImports from '../../src/legacy/legacyApplyMiddleware'; -import { __resetGlobalContext } from '../../src/globalContext'; - -var backupConsoleError = console.error; - -describe('dispatch', () => { - beforeEach(() => { - _resetGlobalState(); - __resetGlobalContext(); - }); - - beforeAll(() => { - // Some of these tests cause MobX to write to console.error, so we need to supress that output - console.error = (message: any): void => null; - }); - - afterAll(() => { - console.error = backupConsoleError; - }); - - it('calls dispatchWithMiddleware with same arguments', () => { - spyOn(legacyApplyMiddlewareImports, 'dispatchWithMiddleware'); - let originalAction = () => {}; - let originalActionType = 'testAction'; - let originalArguments: IArguments = {}; - let options = { a: 1 }; - dispatch(originalAction, originalActionType, originalArguments, options); - expect(legacyApplyMiddlewareImports.dispatchWithMiddleware).toHaveBeenCalledWith( - originalAction, - originalActionType, - originalArguments, - options - ); - }); - - it('executes middleware in the same transaction as the action', () => { - getRootStore().set('foo', 0); - - // Count how many times the autorun gets executed - let count = 0; - autorun(() => { - getRootStore().get('foo'); - count++; - }); - - // Autorun executes once when it is defined - expect(count).toBe(1); - - // Change the state twice, once in middleware and once in the action - legacyApplyMiddlewareImports.default((next, action, actionType, actionContext) => { - getRootStore().set('foo', 1); - next(action, actionType, null, actionContext); - }); - - dispatch( - () => { - getRootStore().set('foo', 2); - }, - null, - null, - null - ); - - // Autorun should have executed exactly one more time - expect(count).toBe(2); - }); -}); diff --git a/test/legacy/legacyApplyMiddlewareTests.ts b/test/legacy/legacyApplyMiddlewareTests.ts deleted file mode 100644 index be4ffe2..0000000 --- a/test/legacy/legacyApplyMiddlewareTests.ts +++ /dev/null @@ -1,101 +0,0 @@ -import 'jasmine'; - -import { - default as legacyApplyMiddleware, - dispatchWithMiddleware, -} from '../../src/legacy/legacyApplyMiddleware'; -import ActionFunction from '../../src/legacy/ActionFunction'; -import ActionContext from '../../src/legacy/ActionContext'; - -describe('legacyApplyMiddleware', () => { - beforeEach(() => { - legacyApplyMiddleware(); - }); - - it('Calls middleware during dispatchWithMiddleware', () => { - let actionCalled = false; - let middlewareCalled = false; - - legacyApplyMiddleware((next, action, actionType, actionContext) => { - middlewareCalled = true; - next(action, actionType, null, actionContext); - }); - - dispatchWithMiddleware( - () => { - actionCalled = true; - }, - null, - null, - null - ); - expect(actionCalled).toBeTruthy(); - expect(middlewareCalled).toBeTruthy(); - }); - - it('Calls middleware in order', () => { - var middleware0Called = false; - var middleware1Called = false; - - legacyApplyMiddleware( - (next, action, actionType, actionContext) => { - expect(middleware1Called).toBeFalsy(); - middleware0Called = true; - next(action, actionType, null, actionContext); - }, - (next, action, actionType, actionContext) => { - expect(middleware0Called).toBeTruthy(); - middleware1Called = true; - next(action, actionType, null, actionContext); - } - ); - - dispatchWithMiddleware(() => {}, null, null, null); - expect(middleware1Called).toBeTruthy(); - }); - - it('Passes action parameters to middleware', () => { - let originalAction = () => {}; - let originalActionType = 'testAction'; - let originalArguments = {}; - let originalOptions = { a: 1 }; - - var passedAction: ActionFunction; - var passedActionType: string; - var passedArguments: IArguments; - var passedOptions: ActionContext; - - legacyApplyMiddleware((next, action, actionType, args, actionContext) => { - passedAction = action; - passedActionType = actionType; - passedArguments = args; - passedOptions = actionContext; - }); - - dispatchWithMiddleware( - originalAction, - originalActionType, - originalArguments, - originalOptions - ); - expect(passedAction).toBe(originalAction); - expect(passedActionType).toBe(originalActionType); - expect(passedArguments).toBe(originalArguments); - expect(passedOptions).toBe(originalOptions); - }); - - it('Returns the action return value to middleware', () => { - let originalReturnValue = Promise.resolve({}); - let originalAction = () => { - return originalReturnValue; - }; - let receivedReturnValue: Promise | void; - - legacyApplyMiddleware((next, action, actionType, args, actionContext) => { - receivedReturnValue = next(action, actionType, args, actionContext); - }); - - dispatchWithMiddleware(originalAction, null, null, null); - expect(receivedReturnValue).toBe(originalReturnValue); - }); -}); diff --git a/test/legacy/promise/actionWrappersTests.ts b/test/legacy/promise/actionWrappersTests.ts deleted file mode 100644 index fa658a2..0000000 --- a/test/legacy/promise/actionWrappersTests.ts +++ /dev/null @@ -1,80 +0,0 @@ -import 'jasmine'; -import * as actionImports from '../../../src/legacy/action'; -import * as promiseMiddleware from '../../../src/legacy/promise/promiseMiddleware'; -import { wrapThen, wrapCatch } from '../../../src/legacy/promise/actionWrappers'; - -describe('actionWrappers', () => { - let originalThenSpy: jasmine.Spy; - let originalCatchSpy: jasmine.Spy; - let getCurrentActionSpy: jasmine.Spy; - - beforeEach(() => { - spyOn(actionImports, 'default').and.returnValue((callback: Function) => callback); - getCurrentActionSpy = spyOn(promiseMiddleware, 'getCurrentAction'); - originalThenSpy = jasmine.createSpy('originalThen'); - originalCatchSpy = jasmine.createSpy('originalCatch'); - }); - - it('just pass through null callbacks', () => { - // Act - wrapThen(originalThenSpy)(null, null); - wrapCatch(originalCatchSpy)(null); - - // Assert - expect(originalThenSpy).toHaveBeenCalledWith(null, null); - expect(originalCatchSpy).toHaveBeenCalledWith(null); - }); - - it('wrap the callbacks in actions', () => { - // Arrange - getCurrentActionSpy.and.returnValue('testAction'); - - let onFulfilled = jasmine.createSpy('onFulfilled'); - let onRejectedInThen = jasmine.createSpy('onRejectedInThen'); - wrapThen(originalThenSpy)(onFulfilled, onRejectedInThen); - - let onRejectedInCatch = jasmine.createSpy('onRejectedInCatch'); - wrapCatch(originalCatchSpy)(onRejectedInCatch); - - // Act / Assert - fulfillPromise(); - expect(actionImports.default).toHaveBeenCalledWith('testAction => then'); - expect(onFulfilled).toHaveBeenCalled(); - - rejectPromiseInThen(); - expect(actionImports.default).toHaveBeenCalledWith('testAction => then_rejected'); - expect(onRejectedInThen).toHaveBeenCalled(); - - rejectPromiseInCatch(); - expect(actionImports.default).toHaveBeenCalledWith('testAction => catch'); - expect(onRejectedInCatch).toHaveBeenCalled(); - }); - - it('handle callback parameters and return value', () => { - // Arrange - getCurrentActionSpy.and.returnValue('testAction'); - let onFulfilled = jasmine.createSpy('onFulfilled').and.returnValue('returnValue'); - - // Act - wrapThen(originalThenSpy)(onFulfilled, null); - - // Simulate the promise being fulfilled - let returnValue = fulfillPromise('arg'); - - // Assert - expect(returnValue).toBe('returnValue'); - expect(onFulfilled).toHaveBeenCalledWith('arg'); - }); - - function fulfillPromise(arg?: any) { - return originalThenSpy.calls.argsFor(0)[0](arg); - } - - function rejectPromiseInThen(arg?: any) { - return originalThenSpy.calls.argsFor(0)[1](arg); - } - - function rejectPromiseInCatch(arg?: any) { - return originalCatchSpy.calls.argsFor(0)[0](arg); - } -}); diff --git a/test/legacy/promise/endToEndTests.ts b/test/legacy/promise/endToEndTests.ts deleted file mode 100644 index 1853e83..0000000 --- a/test/legacy/promise/endToEndTests.ts +++ /dev/null @@ -1,74 +0,0 @@ -import 'jasmine'; -import { autorun } from 'mobx'; -import { action, legacyApplyMiddleware } from '../../../src/legacy'; -import { createStore } from '../../../src'; -import { getCurrentAction, promiseMiddleware } from '../../../src/legacy/promise/promiseMiddleware'; -import { __resetGlobalContext } from '../../../src/globalContext'; - -let testAction = action('testAction')(function testAction(store: any, newValue: any) { - return Promise.resolve(newValue).then(value => { - store.testValue = value; - store.currentAction = getCurrentAction(); - }); -}); - -describe('promiseMiddleware', () => { - beforeEach(function() { - __resetGlobalContext(); - }); - - it('wraps callbacks in promises when applied', done => { - // Arrange - legacyApplyMiddleware(promiseMiddleware); - let store = createStore('testStore', { testValue: 1, currentAction: null })(); - let newValue = 2; - observeStore(store); - - // Act - testAction(store, newValue) - .then(() => { - // The new value should have been set - expect(store.testValue).toBe(newValue); - - // The action name should indicate that it was a promise's "then" callback - expect(store.currentAction).toBe('testAction => then'); - - // At this point there should be no current action - expect(getCurrentAction()).toBe(null); - done(); - }) - .catch(error => { - // Assert that the action does not fail - fail('Action failed with error: ' + error); - done(); - }); - }); - - it('does not wrap callbacks in promises when not applied', done => { - // Arrange - legacyApplyMiddleware(); - let store = createStore('testStore', { testValue: null })(); - let newValue = {}; - observeStore(store); - - // Act - testAction(store, newValue) - .then(() => { - // Assert that the action fails - fail('The action should fail.'); - done(); - }) - .catch(error => { - // Assert that the value was not set - expect(store.testValue).not.toBe(newValue); - done(); - }); - }); -}); - -function observeStore(store: any) { - // Strict mode only requires actions if the store is actually observed - autorun(() => { - store.testValue; - }); -} diff --git a/test/legacy/promise/installTests.ts b/test/legacy/promise/installTests.ts deleted file mode 100644 index de282ef..0000000 --- a/test/legacy/promise/installTests.ts +++ /dev/null @@ -1,55 +0,0 @@ -import 'jasmine'; -import * as actionWrappers from '../../../src/legacy/promise/actionWrappers'; -import install from '../../../src/legacy/promise/install'; - -describe('install', () => { - let originalThen = Promise.prototype.then; - let originalCatch = Promise.prototype.catch; - - let wrappedThen = () => {}; - let wrappedCatch = () => {}; - - it('wraps Promise.then and Promise.catch', () => { - try { - // Arrange - setupPromise(); - - // Act - install(); - - // Assert - expect(Promise.prototype.then).toBe(wrappedThen); - expect(Promise.prototype.catch).toBe(wrappedCatch); - } finally { - resetPromise(); - } - }); - - it('returns an uninstall function to restore the original then and catch', () => { - try { - // Arrange - setupPromise(); - let uninstall = install(); - - // Act - uninstall(); - - // Assert - expect(Promise.prototype.then).toBe(originalThen); - expect(Promise.prototype.catch).toBe(originalCatch); - } finally { - resetPromise(); - } - }); - - // NOTE!!!! Promise can only be overridden INSIDE the test function body, or else jest will not finish - function setupPromise() { - spyOn(actionWrappers, 'wrapThen').and.returnValue(wrappedThen); - spyOn(actionWrappers, 'wrapCatch').and.returnValue(wrappedCatch); - } - - function resetPromise() { - Promise.prototype.then = originalThen; - Promise.prototype.catch = originalCatch; - } -}); diff --git a/test/legacy/promise/promiseMiddlewareTests.ts b/test/legacy/promise/promiseMiddlewareTests.ts deleted file mode 100644 index b9b767c..0000000 --- a/test/legacy/promise/promiseMiddlewareTests.ts +++ /dev/null @@ -1,106 +0,0 @@ -import 'jasmine'; -import { getCurrentAction, promiseMiddleware } from '../../../src/legacy/promise/promiseMiddleware'; -import * as install from '../../../src/legacy/promise/install'; - -describe('promiseMiddleware', () => { - let uninstallSpy: jasmine.Spy; - - beforeEach(() => { - uninstallSpy = jasmine.createSpy('uninstall'); - spyOn(install, 'default').and.returnValue(uninstallSpy); - }); - - it('calls install to monkeypatch Promise for the duration of the action', () => { - // Arrange - let next = () => { - expect(install.default).toHaveBeenCalled(); - expect(uninstallSpy).not.toHaveBeenCalled(); - }; - - // Act - promiseMiddleware(next, null, null, null, null); - - // Assert - expect(uninstallSpy).toHaveBeenCalled(); - }); - - it('does not uninstall until all recursive actions have completed', () => { - // Act - let outerNext = () => { - let innerNext = () => { - expect(uninstallSpy).not.toHaveBeenCalled(); - }; - - promiseMiddleware(innerNext, null, null, null, null); - expect(uninstallSpy).not.toHaveBeenCalled(); - }; - - promiseMiddleware(outerNext, null, null, null, null); - - // Assert - expect(uninstallSpy).toHaveBeenCalled(); - }); - - it('calls next with arguments', () => { - // Arrange - let originalAction = () => {}; - let originalActionType = 'testAction'; - let originalArguments = {}; - let originalActionContext = { a: 1 }; - let next = jasmine.createSpy('next'); - - // Act - promiseMiddleware( - next, - originalAction, - originalActionType, - originalArguments, - originalActionContext - ); - - // Assert - expect(next).toHaveBeenCalledWith( - originalAction, - originalActionType, - originalArguments, - originalActionContext - ); - }); - - it('keeps track of the current action', () => { - // Arrange - let actionType = 'testAction'; - let currentAction; - let next = () => { - currentAction = getCurrentAction(); - }; - - // Act - promiseMiddleware(next, null, actionType, null, null); - - // Assert - expect(currentAction).toBe(actionType); - }); - - it('keeps track of recursive actions', () => { - // Arrange - let outerAction = 'outerAction'; - let innerAction = 'innerAction'; - let currentActionValues: string[] = []; - - // Act - let outerNext = () => { - currentActionValues.push(getCurrentAction()); - let innerNext = () => { - currentActionValues.push(getCurrentAction()); - }; - promiseMiddleware(innerNext, null, innerAction, null, null); - currentActionValues.push(getCurrentAction()); - }; - - promiseMiddleware(outerNext, null, outerAction, null, null); - - // Assert - expect(currentActionValues).toEqual([outerAction, innerAction, outerAction]); - }); -}); diff --git a/test/legacy/react/reactiveTests.tsx b/test/legacy/react/reactiveTests.tsx deleted file mode 100644 index 2da85fe..0000000 --- a/test/legacy/react/reactiveTests.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import 'jasmine'; -import { JSDOM } from 'jsdom'; - -import * as React from 'react'; - -import { action, initializeTestMode, resetTestMode } from '../../../src/legacy'; -import { createStore } from '../../../src'; -import { mount, shallow } from 'enzyme'; - -import { isObservable } from 'mobx'; -import reactive from '../../../src/legacy/react/reactive'; - -import { __resetGlobalContext } from '../../../src/globalContext'; - -describe('reactive decorator', () => { - beforeAll(() => { - // https://github.com/enzymejs/enzyme/issues/75 - let dom = new JSDOM(''); - (global as any).window = dom.window; - (global as any).document = dom.window.document; - }); - - beforeEach(function() { - __resetGlobalContext(); - }); - - it('observes changes from store', () => { - let store = createStore('testStore', { - foo: 'value', - })(); - - let renderSpy = jasmine.createSpy(null); - - let Wrapped = reactive({ - foo: () => store.foo, - })( - class extends React.Component { - render() { - let { foo } = this.props; - renderSpy(); - return
{foo}
; - } - } - ); - - let wrapper = mount(); - - action('dummy')(() => { - store.foo = 'hello'; - })(); - - expect(renderSpy).toHaveBeenCalledTimes(2); - expect(wrapper.find('.testClass').text()).toBe('hello'); - }); - - it('passes through to @observer if no args are passed', () => { - let store = createStore('testStore', { - foo: 'value', - })(); - - let TestComponent = reactive( - class extends React.Component { - componentWillMount() {} - render() { - return
{store.foo}
; - } - } - ); - - let component = new TestComponent({ hello: 'world' }); - component.componentWillMount(); - component.render(); - - expect(isObservable(component.render)).toBeTruthy(); - }); - - it('creates a mountable classical component', () => { - let store = createStore('testStore', { - foo: 'value', - })(); - - let Wrapped = reactive({ - foo: () => store.foo, - })( - class extends React.Component { - render() { - let { foo, hello } = this.props; - expect(foo).toBe(store.foo); - expect(hello).toBe('world'); - return
{foo}
; - } - } - ); - - expect(mount().find('.testClass').length).toBe(1); - }); - - it('injects subtree as props for classical components', () => { - let store = createStore('testStore', { - foo: 'value', - })(); - - @reactive({ - foo: () => store.foo, - }) - class TestComponent extends React.Component { - render() { - let { foo, hello } = this.props; - expect(foo).toBe(store.foo); - expect(hello).toBe('world'); - return
{foo}
; - } - } - - let comp = new TestComponent({ hello: 'world' }); - comp.render(); - }); - - it('allows classical components to be tested in a pure manner', () => { - let store = createStore('testStore', { - foo: null, - })(); - - @reactive({ - foo: () => store.foo, - }) - class TestComponent extends React.Component { - render() { - let { foo } = this.props; - - expect(foo).toBe('somevalue'); - - return
{foo}
; - } - } - - let comp = new TestComponent({ foo: 'somevalue' }); - comp.render(); - }); - - it('allows functional components to be tested in a pure manner', () => { - let store = createStore('testStore', { - foo: null, - })(); - - let TestComponent = reactive({ - foo: () => store.foo, - })((props: any) => { - let { foo, hello } = props; - expect(foo).toBe('world'); - return
{foo}
; - }); - - TestComponent.nonReactiveStatelessComponent({ foo: 'world' }); - - let comp = new TestComponent({ foo: 'world' }); - comp.render(); - - expect(store.foo).toBeNull(); - }); - - it('creates a mountable functional component', () => { - let store = createStore('testStore', { - foo: 'value', - })(); - - let Wrapped = reactive({ - foo: () => store.foo, - })((props: any) => { - let { foo, hello } = props; - expect(foo).toBe('value'); - expect(hello).toBe('world'); - return
{foo}
; - }); - - expect(mount().find('.testClass').length).toBe(1); - }); - - it('injects subtree as props for functional components', () => { - let store = createStore('testStore', { - foo: 'value', - })(); - - let TestComponent = reactive({ - foo: () => store.foo, - })((props: any) => { - let { foo, hello } = props; - expect(foo).toBe(store.foo); - expect(hello).toBe('world'); - return
{foo}
; - }); - - // Reactive stateless component are converted to classical components by @observer - let comp = new TestComponent({ hello: 'world' }); - comp.render(); - }); - - it('injects props as param to the selector functions', () => { - let store: any = createStore('testStore', { - id0: 'value', - })(); - - let TestComponent = reactive({ - foo: (p: any) => store[p.id], - })((props: any) => { - let { foo } = props; - expect(foo).toBe('value'); - return
{foo}
; - }); - - // Reactive stateless component are converted to classical components by @observer - let comp = new TestComponent({ id: 'id0' }); - comp.render(); - }); - - it('decorates over component classes with public members', () => { - @reactive - class TestComponent extends React.Component { - foo: string; - - bar() {} - - render() { - let { foo } = this.props; - expect(foo).toBe('somevalue'); - return
{foo}
; - } - } - - let comp = new TestComponent({ foo: 'somevalue' }); - comp.render(); - }); - - it('allows for type checking for generic decorator with TS 2.1', () => { - let store = createStore('testStore', { - foo: 'value', - dontuse: 2, - })(); - - interface Props { - foo: string; - bar: string; - } - - @reactive({ - foo: () => store.foo, - }) - class TestComponent extends React.Component { - render() { - let { foo, bar } = this.props; - expect(foo).toBe('value'); - return
{foo}
; - } - } - }); - - it('does not execute the selector function in test mode', () => { - initializeTestMode(); - - let fooSelector = jasmine.createSpy(null); - let TestComponent = reactive({ - foo: fooSelector, - })( - class extends React.Component { - render() { - return
; - } - } - ); - - shallow(); - - expect(fooSelector).not.toHaveBeenCalled(); - - resetTestMode(); - }); - - it('does execute the selector function after test mode has been cleared', () => { - initializeTestMode(); - resetTestMode(); - - let fooSelector = jasmine.createSpy(null); - let TestComponent = reactive({ - foo: fooSelector, - })( - class extends React.Component { - render() { - return
; - } - } - ); - - shallow(); - - expect(fooSelector).toHaveBeenCalled(); - }); -}); diff --git a/test/legacy/selectTests.ts b/test/legacy/selectTests.ts deleted file mode 100644 index e595c8d..0000000 --- a/test/legacy/selectTests.ts +++ /dev/null @@ -1,222 +0,0 @@ -import 'jasmine'; -import action from '../../src/legacy/action'; -import select from '../../src/legacy/select'; -import createStore from '../../src/createStore'; -import { initializeTestMode, resetTestMode } from '../../src/legacy/testMode'; -import { getActionType } from '../../src/legacy/functionInternals'; -import { __resetGlobalContext } from '../../src/globalContext'; - -describe('select', () => { - beforeEach(function() { - __resetGlobalContext(); - }); - - it('creates a state scoped to subset of state tree', () => { - let fooStore = createStore('foo', { - key1: 'value1', - obj1: { - obj1Key1: 'obj1_value1', - obj1Key2: 'obj1_value2', - }, - array1: ['array1_value1', 'array1_value2'], - })(); - - interface ReadOnlyActionState { - key1: string; - obj1Key1: string; - obj1: any; - array1: string[]; - array1Value1: string; - } - - let readOnlyAction = jasmine.createSpy( - 'action', - action('readOnly')((state?: ReadOnlyActionState) => { - expect(state.key1).toBe(fooStore.key1); - expect(state.obj1Key1).toBe(fooStore.obj1.obj1Key1); - expect(state.obj1).toBe(fooStore.obj1); - expect(state.array1).toBe(fooStore.array1); - expect(state.array1Value1).toBe(fooStore.array1[0]); - }) - ); - - let newAction = select({ - key1: () => fooStore.key1, - obj1Key1: () => fooStore.obj1.obj1Key1, - obj1: () => fooStore.obj1, - array1: () => fooStore.array1, - array1Value1: () => fooStore.array1[0], - })(readOnlyAction); - - newAction(); - - expect(readOnlyAction).toHaveBeenCalledTimes(1); - }); - - it('propagates action params to the selector function', () => { - let fooStore: any = createStore('foo', { - id0: 'value', - array0: ['a', 'b', 'c'], - })(); - - let readAction = action('read')(function readAction( - id: string, - arrayIndex: number, - state?: any - ) { - expect(state.value).toBe('value'); - expect(state.arrayValue).toBe('c'); - }); - - let newAction = select({ - value: (id: string, arrayIndex: number) => fooStore[id], - arrayValue: (id: string, arrayIndex: number) => fooStore.array0[arrayIndex], - })(readAction); - - newAction('id0', 2); - }); - - it('places state at the right argument position even if the wrapped function has optional arguments before state', () => { - let fooStore: any = createStore('foo', { - key: 'value', - })(); - - let someAction = action( - 'someAction' - )((required: string, optional?: string, state?: any) => { - expect(state.key).toBe('value'); - expect(optional).not.toBeDefined(); - expect(required).toBe('required value'); - }); - - let newAction = select({ - key: () => fooStore.key, - })(someAction); - - newAction('required value'); - }); - - it('can handle having action be the outer decorator', () => { - let fooStore: any = createStore('foo', { - key: 'value', - })(); - - let functionIsCalled = false; - - let someAction = select({ - key: () => fooStore.key, - })((required: string, optional?: string, state?: any) => { - expect(state.key).toBe('value'); - expect(optional).not.toBeDefined(); - expect(required).toBe('required value'); - functionIsCalled = true; - }); - - let newAction = action('action')(someAction); - - newAction('required value'); - - expect(functionIsCalled).toBeTruthy(); - }); - - it('allows tests to passthrough state param', () => { - let fooStore: any = createStore('foo', { - key: 'value', - })(); - - let someAction = select({ - key: () => fooStore.key, - })((required: string, optional?: string, state?: any) => { - expect(state.key).toBe('testValue'); - expect(optional).toBe('optional'); - expect(required).toBe('required value'); - }); - - let newAction = action('action')(someAction); - - newAction('required value', 'optional', { key: 'testValue' }); - }); - - it('can use new TS 2.1 mapped types to describe selector functions', () => { - let fooStore = createStore('foo', { - k: 'v', - })(); - - interface ActionState { - key: string; - } - - let updateAction = action('update')((state?: ActionState) => { - expect(state.key).toBe(fooStore.k); - }); - - let newAction = select({ - key: () => fooStore.k, - })(updateAction); - - newAction(); - }); - - it('does not execute the selector function in test mode', () => { - initializeTestMode(); - - let fooSelector = jasmine.createSpy('fooSelector'); - let actionSpy = jasmine.createSpy('action'); - - select({ - foo: fooSelector, - })(actionSpy)(); - - expect(fooSelector).not.toHaveBeenCalled(); - expect(actionSpy).toHaveBeenCalled(); - - resetTestMode(); - }); - - it('does execute the selector function after test mode has been cleared', () => { - initializeTestMode(); - resetTestMode(); - - let fooSelector = jasmine.createSpy('fooSelector').and.returnValue('fooValue'); - let action = (state?: any) => { - expect(state.foo).toBe('fooValue'); - }; - - select({ - foo: fooSelector, - })(action)(); - - expect(fooSelector).toHaveBeenCalled(); - }); - - it('when wrapping an action, preserves the action type', () => { - // Arrange - let actionName = 'testAction'; - let testAction = action(actionName)(() => {}); - - // Act - let wrappedAction = select({})(testAction); - - // Assert - expect(getActionType(wrappedAction)).toBe(actionName); - }); - - it('throws if state is being changed inside a select-decorated function', () => { - // Arrange - let store = { - foo: 'foo', - bar: 5, - }; - - // Act - let wrapped = select({ - foo: () => store.foo, - bar: () => store.bar, - })((state: typeof store) => { - state.foo = 'bar'; - }); - - // Assert - expect(wrapped).toThrow(); - }); -}); diff --git a/test/legacy/stitch/raiseActionTests.ts b/test/legacy/stitch/raiseActionTests.ts deleted file mode 100644 index 11e2fe1..0000000 --- a/test/legacy/stitch/raiseActionTests.ts +++ /dev/null @@ -1,22 +0,0 @@ -import 'jasmine'; -import * as actionImport from '../../../src/legacy/action'; -import { raiseAction } from '../../../src/legacy/stitch'; - -interface TestActionType { - (arg1: string, arg2: string): void; -} - -describe('raiseAction', () => { - it('returns a dummy action of the given type', () => { - // Arrange - let createdAction = jasmine.createSpy('createdAction'); - spyOn(actionImport, 'default').and.returnValue((rawAction: Function) => createdAction); - - // Act - raiseAction('testAction')('arg1', 'arg2'); - - // Assert - expect(actionImport.default).toHaveBeenCalledWith('testAction'); - expect(createdAction).toHaveBeenCalledWith('arg1', 'arg2'); - }); -}); diff --git a/test/legacy/stitch/raiseTests.ts b/test/legacy/stitch/raiseTests.ts deleted file mode 100644 index e707385..0000000 --- a/test/legacy/stitch/raiseTests.ts +++ /dev/null @@ -1,37 +0,0 @@ -import 'jasmine'; -import * as actionImports from '../../../src/legacy/action'; -import { raise } from '../../../src/legacy/stitch'; - -describe('raise', () => { - beforeEach(() => { - spyOn(console, 'error'); - }); - - it('creates an action of the given type', () => { - spyOn(actionImports, 'default').and.returnValue((rawAction: Function) => rawAction); - raise('testAction'); - expect(actionImports.default).toHaveBeenCalledWith('testAction'); - }); - - it('passes the action to the callback', () => { - let actionToCreate = () => {}; - let passedAction: Function; - spyOn(actionImports, 'default').and.returnValue((rawAction: Function) => actionToCreate); - - raise('testAction', actionToExecute => { - passedAction = actionToExecute; - }); - - expect(passedAction).toBe(actionToCreate); - }); - - it('executes the action if no callback was provided', () => { - let actionExecuted = false; - let actionToCreate = () => { - actionExecuted = true; - }; - spyOn(actionImports, 'default').and.returnValue((rawAction: Function) => actionToCreate); - raise('testAction'); - expect(actionExecuted).toBeTruthy(); - }); -}); diff --git a/test/legacy/stitch/stitchTests.ts b/test/legacy/stitch/stitchTests.ts deleted file mode 100644 index 7e6150c..0000000 --- a/test/legacy/stitch/stitchTests.ts +++ /dev/null @@ -1,87 +0,0 @@ -import 'jasmine'; -import { LegacyDispatchFunction, ActionContext } from '../../../src/legacy'; -import { stitch, subscribe } from '../../../src/legacy/stitch'; - -let sequenceOfEvents: any[]; - -describe('stitch', () => { - beforeAll(() => { - subscribe('testAction1', args => { - sequenceOfEvents.push('callback'); - sequenceOfEvents.push(args); - }); - }); - - beforeEach(() => { - sequenceOfEvents = []; - }); - - it('calls next with the given arguments', () => { - let actionType = 'testAction1'; - let args = {}; - let actionContext: ActionContext = {}; - stitch(getNext(), () => {}, actionType, args, actionContext); - expect(sequenceOfEvents[0]).toEqual({ - actionType, - args, - }); - }); - - it('returns the return value from next', () => { - let originalReturnValue = Promise.resolve({}); - let returnValue = stitch(getNext(originalReturnValue), null, null, null, null); - expect(returnValue).toBe(originalReturnValue); - }); - - it('calls callback for subscribed actions', () => { - stitch(getNext(), () => {}, 'testAction1', null, null); - expect(sequenceOfEvents).toContain('callback'); - }); - - it("passes the action's arguments to the callback", () => { - let arg0 = {}; - let args = getArguments(arg0); - stitch(getNext(), () => {}, 'testAction1', args, null); - expect(sequenceOfEvents).toContain('callback'); - expect(sequenceOfEvents).toContain(arg0); - }); - - it("doesn't call callback for non-subscribed actions", () => { - stitch(getNext(), () => {}, 'testAction2', null, null); - expect(sequenceOfEvents).not.toContain('callback'); - }); - - it('does nothing if actionType is null', () => { - stitch(getNext(), () => {}, null, null, null); - expect(sequenceOfEvents).not.toContain('callback'); - }); - - it('calls callbacks AFTER dispatching action', () => { - let actionType = 'testAction1'; - let args = getArguments({}); - stitch(getNext(), () => {}, actionType, args, null); - expect(sequenceOfEvents).toEqual([ - { - actionType, - args, - }, - 'callback', - args[0], - ]); - }); -}); - -function getNext(returnValue?: Promise): LegacyDispatchFunction { - return (action, actionType, args) => { - sequenceOfEvents.push({ - actionType, - args, - }); - - return returnValue; - }; -} - -function getArguments(arg0: any) { - return arguments; -} diff --git a/test/legacy/trace/traceTests.ts b/test/legacy/trace/traceTests.ts deleted file mode 100644 index f9bbb6b..0000000 --- a/test/legacy/trace/traceTests.ts +++ /dev/null @@ -1,101 +0,0 @@ -import 'jasmine'; -import trace from '../../../src/legacy/trace/trace'; -import { ActionContext } from '../../../src/legacy'; - -describe('trace', () => { - beforeEach(() => { - spyOn(console, 'log'); - }); - - it('calls next with arguments', () => { - let originalAction = () => {}; - let originalActionType = 'testAction'; - let originalArguments = {}; - let originalActionContext = { a: 1 }; - let passedAction: any; - let passedActionType: any; - let passedArguments: IArguments; - let passedActionContext: ActionContext; - trace( - (action, actionType, args, actionContext) => { - passedAction = action; - passedActionType = actionType; - passedArguments = args; - passedActionContext = actionContext; - }, - originalAction, - originalActionType, - originalArguments, - originalActionContext - ); - - expect(passedAction).toBe(originalAction); - expect(passedActionType).toBe(originalActionType); - expect(passedArguments).toBe(originalArguments); - expect(passedActionContext).toBe(originalActionContext); - }); - - it('returns the return value from next', () => { - let originalReturnValue = Promise.resolve({}); - - let returnValue = trace( - (action, actionType, args, actionContext) => { - return originalReturnValue; - }, - null, - null, - null, - null - ); - - expect(returnValue).toBe(originalReturnValue); - }); - - it('logs actions', () => { - trace((action, actionType, args, actionContext) => {}, null, 'testAction', null, null); - - expect(console.log).toHaveBeenCalledTimes(1); - expect((console.log).calls.argsFor(0)[0]).toMatch(/testAction/); - }); - - it('logs anonymous actions', () => { - trace((action, actionType, args, actionContext) => {}, null, null, null, null); - - expect(console.log).toHaveBeenCalledTimes(1); - expect((console.log).calls.argsFor(0)[0]).toMatch(/anonymous action/); - }); - - it('indents nested actions', () => { - let next = () => { - trace(() => {}, null, 'innerAction', null, null); - }; - - trace(next, null, 'outerAction', null, null); - - let logCalls = (console.log).calls; - expect(logCalls.argsFor(0)[0]).toBe('Executing action: outerAction'); - expect(logCalls.argsFor(1)[0]).toMatch(' Executing action: innerAction'); - }); - - it('indents correctly after an exception', () => { - let next = () => { - trace( - () => { - throw new Error(); - }, - null, - 'action2', - null, - null - ); - }; - - try { - trace(next, null, 'action1', null, null); - } catch (ex) {} - - trace(() => {}, null, 'action3', null, null); - - expect((console.log).calls.argsFor(2)[0]).toBe('Executing action: action3'); - }); -}); diff --git a/test/mutatorTests.ts b/test/mutatorTests.ts index c811b6e..87cdbb9 100644 --- a/test/mutatorTests.ts +++ b/test/mutatorTests.ts @@ -1,18 +1,9 @@ import 'jasmine'; import mutator from '../src/mutator'; -import * as dispatcher from '../src/dispatcher'; -import * as globalContext from '../src/globalContext'; import * as mobx from 'mobx'; +import { createTestSatchel } from './utils/createTestSatchel'; describe('mutator', () => { - let mockGlobalContext: any; - - beforeEach(() => { - mockGlobalContext = { currentMutator: null }; - spyOn(globalContext, 'getGlobalContext').and.returnValue(mockGlobalContext); - spyOn(dispatcher, 'subscribe'); - }); - it('throws if the action creator does not have an action ID', () => { // Arrange let actionCreator: any = {}; @@ -25,26 +16,29 @@ describe('mutator', () => { it('subscribes the target function to the action', () => { // Arrange + const satchel = createTestSatchel(); let actionId = 'testAction'; let actionCreator: any = { __SATCHELJS_ACTION_ID: actionId }; // Act - mutator(actionCreator, () => {}); + const testMutator = mutator(actionCreator, () => {}); + satchel.register(testMutator); // Assert - expect(dispatcher.subscribe).toHaveBeenCalled(); - expect((dispatcher.subscribe).calls.argsFor(0)[0]).toBe(actionId); + expect(satchel.__subscriptions[actionId]).toBeDefined(); }); it('wraps the subscribed callback in a MobX action', () => { // Arrange + const satchel = createTestSatchel(); let callback = () => {}; let wrappedCallback = () => {}; let actionCreator: any = { __SATCHELJS_ACTION_ID: 'testAction' }; spyOn(mobx, 'action').and.returnValue(wrappedCallback); // Act - mutator(actionCreator, callback); + const testMutator = mutator(actionCreator, callback); + satchel.register(testMutator); // Assert expect(mobx.action).toHaveBeenCalled(); @@ -52,11 +46,12 @@ describe('mutator', () => { it('returns the target function', () => { // Arrange + const satchel = createTestSatchel(); let actionCreator: any = { __SATCHELJS_ACTION_ID: 'testAction' }; let callback = () => {}; // Act - let returnValue = mutator(actionCreator, callback); + let returnValue = satchel.register(mutator(actionCreator, callback)); // Assert expect(returnValue).toBe(callback); @@ -64,25 +59,27 @@ describe('mutator', () => { it('sets the currentMutator to actionMessage type for the duration of the mutator callback', () => { // Arrange + const satchel = createTestSatchel(); let actionCreator: any = { __SATCHELJS_ACTION_ID: 'testAction', __SATCHELJS_ACTION_TYPE: 'testActionType', }; let callback = () => { - expect(mockGlobalContext.currentMutator).toBe('testActionType'); + expect(satchel.__currentMutator).toBe('testActionType'); }; - mutator(actionCreator, callback); + const testMutator = mutator(actionCreator, callback); // Act - let subscribedCallback = (dispatcher.subscribe as jasmine.Spy).calls.argsFor(0)[1]; - subscribedCallback(); + let subscribedCallback = (satchel.register as jasmine.Spy).calls.argsFor(0)[1]; + subscribedCallback(testMutator); // Assert - expect(mockGlobalContext.currentMutator).toBe(null); + expect(satchel.__currentMutator).toBe(null); }); it('sets the currentMutator back to null if error is thrown', () => { // Arrange + const satchel = createTestSatchel(); let actionCreator: any = { __SATCHELJS_ACTION_ID: 'testAction', __SATCHELJS_ACTION_TYPE: 'testActionType', @@ -90,17 +87,17 @@ describe('mutator', () => { let callback: any = () => { throw new Error('Error in Mutator'); }; - mutator(actionCreator, callback); + const testMutator = mutator(actionCreator, callback); // Act - let subscribedCallback = (dispatcher.subscribe as jasmine.Spy).calls.argsFor(0)[1]; + let subscribedCallback = (satchel.register as jasmine.Spy).calls.argsFor(0)[1]; try { - subscribedCallback(); + subscribedCallback(testMutator); } catch { // no op } // Assert - expect(mockGlobalContext.currentMutator).toBe(null); + expect(satchel.__currentMutator).toBe(null); }); }); diff --git a/test/orchestratorTests.ts b/test/orchestratorTests.ts index b0ed1b7..5fe5722 100644 --- a/test/orchestratorTests.ts +++ b/test/orchestratorTests.ts @@ -1,6 +1,6 @@ import 'jasmine'; import orchestrator from '../src/orchestrator'; -import * as dispatcher from '../src/dispatcher'; +import { createTestSatchel } from './utils/createTestSatchel'; describe('orchestrator', () => { it('throws if the action creator does not have an action ID', () => { @@ -15,25 +15,29 @@ describe('orchestrator', () => { it('subscribes the target function to the action', () => { // Arrange + const satchel = createTestSatchel(); let callback = () => {}; let actionId = 'testAction'; let actionCreator: any = { __SATCHELJS_ACTION_ID: actionId }; - spyOn(dispatcher, 'subscribe'); // Act - orchestrator(actionCreator, callback); + const testOrchestator = orchestrator(actionCreator, callback); + satchel.register(testOrchestator); // Assert - expect(dispatcher.subscribe).toHaveBeenCalledWith(actionId, callback); + expect(satchel.__subscriptions[actionId]).toBeDefined(); + expect(satchel.__subscriptions[actionId][0]).toBe(callback); }); it('returns the target function', () => { // Arrange let actionCreator: any = { __SATCHELJS_ACTION_ID: 'testAction' }; let callback = () => {}; + const satchel = createTestSatchel(); + const testOrchestator = orchestrator(actionCreator, callback); // Act - let returnValue = orchestrator(actionCreator, callback); + const returnValue = satchel.register(testOrchestator); // Assert expect(returnValue).toBe(callback); diff --git a/test/simpleSubscribersTests.ts b/test/simpleSubscribersTests.ts index b6793a1..384ca84 100644 --- a/test/simpleSubscribersTests.ts +++ b/test/simpleSubscribersTests.ts @@ -1,16 +1,16 @@ import 'jasmine'; -import { createSimpleSubscriber, mutatorAction } from '../src/simpleSubscribers'; -import { __resetGlobalContext } from '../src/globalContext'; -import * as actionCreator from '../src/actionCreator'; +import { createSimpleSubscriber } from '../src/simpleSubscribers'; +import { createTestSatchel } from './utils/createTestSatchel'; describe('simpleSubscribers', () => { let actionCreatorSpy: jasmine.Spy; let decoratorSpy: jasmine.Spy; - let simpleSubscriber: Function; + let simpleSubscriber: ReturnType; + let satchel: ReturnType; beforeEach(() => { - __resetGlobalContext(); - actionCreatorSpy = spyOn(actionCreator, 'action').and.callThrough(); + satchel = createTestSatchel(); + actionCreatorSpy = spyOn(satchel, 'action').and.callThrough(); decoratorSpy = jasmine.createSpy('decoratorSpy'); simpleSubscriber = createSimpleSubscriber(decoratorSpy); }); @@ -20,7 +20,7 @@ describe('simpleSubscribers', () => { let actionId = 'testSubscriber'; // Act - let returnValue = simpleSubscriber(actionId, () => {}); + let returnValue = simpleSubscriber(satchel, actionId, () => {}); // Assert expect(actionCreatorSpy).toHaveBeenCalled(); @@ -30,7 +30,7 @@ describe('simpleSubscribers', () => { it('includes arguments in the action message', () => { // Act - let returnValue: Function = simpleSubscriber('testSubscriber', () => {}); + let returnValue: Function = simpleSubscriber(satchel, 'testSubscriber', () => {}); let createdAction = returnValue(1, 2, 3); // Assert @@ -39,7 +39,7 @@ describe('simpleSubscribers', () => { it('subscribes a callback to the action', () => { // Act - simpleSubscriber('testSubscriber', () => {}); + simpleSubscriber(satchel, 'testSubscriber', () => {}); // Assert expect(decoratorSpy).toHaveBeenCalled(); @@ -52,7 +52,7 @@ describe('simpleSubscribers', () => { let actionMessage = { args: [1, 2, 3] }; // Act - simpleSubscriber('testSubscriber', callback); + simpleSubscriber(satchel, 'testSubscriber', callback); let decoratorCallback = decoratorSpy.calls.argsFor(0)[1]; decoratorCallback(actionMessage); diff --git a/test/utils/createTestSatchel.ts b/test/utils/createTestSatchel.ts new file mode 100644 index 0000000..82c95bd --- /dev/null +++ b/test/utils/createTestSatchel.ts @@ -0,0 +1,10 @@ +import { + createSatchel, + SatchelInstance, + SatchelOptions, + PrivateSatchelFunctions, +} from '../../src/createSatchel'; + +type TestSatchelFunction = (options?: SatchelOptions) => SatchelInstance & PrivateSatchelFunctions; + +export const createTestSatchel = createSatchel as TestSatchelFunction; From 889ee03bee8cc51c4b7f8fe759aae4cf19605986 Mon Sep 17 00:00:00 2001 From: jareill Date: Mon, 24 Jun 2024 15:53:18 -0700 Subject: [PATCH 04/12] Adjust to using object to store functions and update docs --- README.md | 81 ++++--- src/createSatchel.ts | 395 +++++++++++++++++--------------- src/index.ts | 1 + src/interfaces/Subscriber.ts | 7 + src/mutator.ts | 7 +- src/orchestrator.ts | 7 +- src/simpleSubscribers.ts | 14 +- test/applyMiddlewareTests.ts | 11 +- test/dispatcherTests.ts | 3 +- test/endToEndTests.ts | 12 +- test/mutatorTests.ts | 25 +- test/orchestratorTests.ts | 20 +- test/registerTests.ts | 24 ++ test/simpleSubscribersTests.ts | 3 +- test/utils/createTestSatchel.ts | 12 +- 15 files changed, 375 insertions(+), 247 deletions(-) create mode 100644 src/interfaces/Subscriber.ts create mode 100644 test/registerTests.ts diff --git a/README.md b/README.md index 3e80118..0ead1b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Satchel -Satchel is a dataflow framework based on the [Flux architecture](http://facebook.github.io/react/blog/2014/05/06/flux.html). It is characterized by exposing an observable state that makes view updates painless and efficient. +Satchel is a dataflow framework based on the [Flux architecture](http://facebook.github.io/react/blog/2014/05/06/flux.html). It is characterized by exposing an observable state that makes view updates painless and efficient. [![npm](https://img.shields.io/npm/v/satcheljs.svg)](https://www.npmjs.com/package/satcheljs) [![Build Status](https://travis-ci.org/Microsoft/satcheljs.svg?branch=master)](https://travis-ci.org/Microsoft/satcheljs) @@ -8,20 +8,20 @@ Satchel is a dataflow framework based on the [Flux architecture](http://facebook ## Influences -Satchel is an attempt to synthesize the best of several dataflow patterns typically used to drive a React-based UI. In particular: +Satchel is an attempt to synthesize the best of several dataflow patterns typically used to drive a React-based UI. In particular: -* [Flux](http://facebook.github.io/react/blog/2014/05/06/flux.html) is not a library itself, but is a dataflow pattern conceived for use with React. In Flux, dataflow is unidirectional, and the only way to modify state is by dispatching actions through a central dispatcher. -* [Redux](http://redux.js.org/index.html) is an implementation of Flux that consolidates stores into a single state tree and attempts to simplify state changes by making all mutations via pure functions called reducers. Ultimately, however, we found reducers and immutable state cumbersome to deal with, particularly in a large, interconnected app. -* [MobX](http://mobxjs.github.io/mobx/index.html) provides a seamless way to make state observable, and allows React to listen to state changes and rerender in a very performant way. Satchel uses MobX under the covers to allow React components to observe the data they depend on. +- [Flux](http://facebook.github.io/react/blog/2014/05/06/flux.html) is not a library itself, but is a dataflow pattern conceived for use with React. In Flux, dataflow is unidirectional, and the only way to modify state is by dispatching actions through a central dispatcher. +- [Redux](http://redux.js.org/index.html) is an implementation of Flux that consolidates stores into a single state tree and attempts to simplify state changes by making all mutations via pure functions called reducers. Ultimately, however, we found reducers and immutable state cumbersome to deal with, particularly in a large, interconnected app. +- [MobX](http://mobxjs.github.io/mobx/index.html) provides a seamless way to make state observable, and allows React to listen to state changes and rerender in a very performant way. Satchel uses MobX under the covers to allow React components to observe the data they depend on. ## Advantages There are a number of advantages to using Satchel to maintain your application state: -* Satchel enables a very **performant UI**, only rerendering the minimal amount necessary. MobX makes UI updates very efficient by automatically detecting specifically what components need to rerender for a given state change. -* Satchel's datastore allows for **isomorphic JavaScript** by making it feasible to render on the server and then serialize and pass the application state down to the client. -* Satchel supports **middleware** that can act on each action that is dispatched. (For example, for tracing or performance instrumentation.) -* Satchel is **type-safe** out of the box, without any extra effort on the consumer's part. +- Satchel enables a very **performant UI**, only rerendering the minimal amount necessary. MobX makes UI updates very efficient by automatically detecting specifically what components need to rerender for a given state change. +- Satchel's datastore allows for **isomorphic JavaScript** by making it feasible to render on the server and then serialize and pass the application state down to the client. +- Satchel supports **middleware** that can act on each action that is dispatched. (For example, for tracing or performance instrumentation.) +- Satchel is **type-safe** out of the box, without any extra effort on the consumer's part. ## Installation @@ -39,15 +39,20 @@ In order to use Satchel with React, you'll also need MobX and the MobX React bin The following examples assume you're developing in Typescript. +### Create a satchel instance + +```typescript +import { createSatchel } from 'satcheljs'; + +export const mySatchel = createSatchel(options); +``` + ### Create a store with some initial state ```typescript -import { createStore } from 'satcheljs'; +import { mySatchel } from './mySatchel'; -let getStore = createStore( - 'todoStore', - { todos: [] } -); +let getStore = mySatchel.createStore('todoStore', { todos: [] }); ``` ### Create a component that consumes your state @@ -62,7 +67,9 @@ class TodoListComponent extends React.Component { render() { return (
- {getStore().todos.map(todo =>
{todo.text}
)} + {getStore().todos.map(todo => ( +
{todo.text}
+ ))}
); } @@ -71,17 +78,14 @@ class TodoListComponent extends React.Component { ### Implement an action creator -Note that, as a convenience, Satchel action creators created with the `action` API both *create* and *dispatch* the action. +Note that, as a convenience, Satchel action creators created with the `action` API both _create_ and _dispatch_ the action. This is typically how you want to use action creators. If you want to create and dispatch the actions separately you can use the `actionCreator` and `dispatch` APIs. ```typescript -import { action } from 'satcheljs'; +import { mySatchel } from './mySatchel'; -let addTodo = action( - 'ADD_TODO', - (text: string) => ({ text: text }) -); +let addTodo = mySatchel.action('ADD_TODO', (text: string) => ({ text: text })); // This creates and dispatches an ADD_TODO action addTodo('Take out trash'); @@ -94,13 +98,18 @@ If you're using TypeScript, the type of `actionMessage` is automatically inferre ```typescript import { mutator } from 'satcheljs'; +import { mySatchel } from './mySatchel'; -mutator(addTodo, (actionMessage) => { +const todoMutator = mutator(addTodo, (actionMessage) => { getStore().todos.push({ id: Math.random(), text: actionMessage.text }); }; + +export function initializeMutators() { + mySatchel.register(todoMutator); +} ``` ### Orchestrators @@ -113,16 +122,18 @@ The following example shows how an orchestrator can persist a value to a server ```typescript import { action, orchestrator } from 'satcheljs'; +import { mySatchel } from './mySatchel'; -let requestAddTodo = action( - 'REQUEST_ADD_TODO', - (text: string) => ({ text: text }) -); +let requestAddTodo = mySatchel.action('REQUEST_ADD_TODO', (text: string) => ({ text: text })); -orchestrator(requestAddTodo, async (actionMessage) => { +const requestAddTodoOrchestrator = orchestrator(requestAddTodo, async actionMessage => { await addTodoOnServer(actionMessage.text); addTodo(actionMessage.text); }); + +export function initializeOrchestrators() { + mySatchel.register(requestAddTodoOrchestrator); +} ``` ### mutatorAction @@ -133,18 +144,18 @@ Satchel provides this utility API which encapsulates action creation, dispatch, The `addTodo` mutator above could be implemented as follows: ```typescript -let addTodo = mutatorAction( - 'ADD_TODO', - function addTodo(text: string) { - getStore().todos.push({ - id: Math.random(), - text: actionMessage.text - }); +import { mySatchel } from './mySatchel'; + +let addTodo = mutatorAction(mySatchel, 'ADD_TODO', function addTodo(text: string) { + getStore().todos.push({ + id: Math.random(), + text: actionMessage.text, }); +}); ``` This is a succinct and easy way to write mutators, but it comes with a restriction: -the action creator is not exposed, so no *other* mutators or orchestrators can subscribe to it. +the action creator is not exposed, so no _other_ mutators or orchestrators can subscribe to it. If an action needs multiple handlers then it must use the full pattern with action creators and handlers implemented separately. ## License - MIT diff --git a/src/createSatchel.ts b/src/createSatchel.ts index a519a08..4dfcc51 100644 --- a/src/createSatchel.ts +++ b/src/createSatchel.ts @@ -1,4 +1,4 @@ -import { observable, ObservableMap, transaction, action as mobxAction } from 'mobx'; +import { observable, ObservableMap, transaction, action as mobxAction, IAction } from 'mobx'; import { getPrivateActionId, getPrivateActionType, @@ -13,13 +13,13 @@ import DispatchFunction from './interfaces/DispatchFunction'; import SubscriberFunction from './interfaces/SubscriberFunction'; import ActionCreator from './interfaces/ActionCreator'; import { Mutator } from './interfaces/Mutator'; -import { Orchestrator } from './interfaces/Orchestrator'; +import { Subscriber } from './interfaces/Subscriber'; type SatchelState = { - rootStore: ObservableMap; - nextActionId: number; - subscriptions: { [key: string]: SubscriberFunction[] }; - currentMutator: string | null; + __rootStore: ObservableMap; + __nextActionId: number; + __subscriptions: { [key: string]: SubscriberFunction[] }; + __currentMutator: string | null; }; export type SatchelInstance = { @@ -27,9 +27,21 @@ export type SatchelInstance = { * Resolves the target of the subscriber and registers it with the dispatcher. */ register: ( - subscriber: Mutator | Orchestrator + subscriber: Subscriber ) => SubscriberFunction; + /** + * Dispatches the action message + * @param actionMessage {ActionMessage} The action message to dispatch + * @returns {void} + */ dispatch: (actionMessage: ActionMessage) => void; + /** + * Decorates a function as an action creator. + * @template T (type parameter) An interface describing the shape of the action message to create. + * @param actionType {string}:A string which identifies the type of the action. + * @param target {((...) => T)=} A function which creates and returns an action message + * @returns {ActionCreator} An action creator + */ actionCreator: < T extends ActionMessage = {}, TActionCreator extends ActionCreator = () => T @@ -37,219 +49,234 @@ export type SatchelInstance = { actionType: string, target?: TActionCreator ) => TActionCreator; + /** + * Decorates a function as an action creator which also dispatches the action message after creating it. + * + * @template T (type parameter) An interface describing the shape of the action message to create. + * @param actionType {string}:A string which identifies the type of the action. + * @param target {((...) => T)=} A function which creates and returns an action message + * @returns {ActionCreator} An action creator + */ action: = () => T>( actionType: string, target?: TActionCreator ) => TActionCreator; + /** + * Creates a Satchel store and returns a selector to it. + * + * @template T (type parameter) An interface describing the shape of the store. + * @param name {string} A unique identifier for the store. + * @param initialState {T} The initial state of the store. + * @returns {() => T} A selector to the store. + */ createStore: (key: string, initialState: T) => () => T; + /** + * Returns Satchel's root store object of the satchel instance. + * @returns {ObservableMap} The root store object + */ getRootStore: () => ObservableMap; + /** + * Returns whether the action creator has any subscribers. + * @returns {boolean} True if the action creator has subscribers, false otherwise. + */ hasSubscribers: (actionCreator: ActionCreator) => boolean; }; -export type PrivateSatchelFunctions = { +export type SatchelPrivateInstanceFunctions = { __createActionId: () => string; __dispatchWithMiddleware: DispatchFunction; __finalDispatch: DispatchFunction; - __subscriptions: { [key: string]: SubscriberFunction[] }; - __currentMutator: string | null; + __createStoreAction: (key: string, initialState: any) => void; + __createActionCreator: >( + actionType: string, + target: TActionCreator, + shouldDispatch: boolean + ) => TActionCreator; + __wrapMutatorTarget: ( + mutator: Mutator + ) => ((actionMessage: TAction) => void) & IAction; }; +export type SatchelInternalInstance = SatchelInstance & + SatchelPrivateInstanceFunctions & + SatchelState; + export type SatchelOptions = { middleware?: Array; }; -function getInitialSatchelState(): SatchelState { - return { - rootStore: observable.map({}), - nextActionId: 0, - subscriptions: {}, - currentMutator: null, - }; -} - -export function createSatchel(options: SatchelOptions = {}): SatchelInstance { +export function createSatchelInternal( + options: SatchelOptions = {}, + // This is only used for testing purposes + finalDispatch?: DispatchFunction +): SatchelInternalInstance { const { middleware = [] } = options; - let { subscriptions, currentMutator, nextActionId, rootStore } = getInitialSatchelState(); - const finalDispatch: DispatchFunction = ( - actionMessage: ActionMessage - ): void | Promise => { - let actionId = getPrivateActionId(actionMessage); - let subscribers = subscriptions[actionId]; - - if (subscribers) { - let promises: Promise[] = []; - - for (const subscriber of subscribers) { - let returnValue = subscriber(actionMessage); - if (returnValue) { - promises.push(returnValue); - } - } - if (promises.length) { - return promises.length == 1 ? promises[0] : Promise.all(promises); + const satchel: SatchelInternalInstance = { + // State + __subscriptions: {}, + __nextActionId: 0, + __rootStore: observable.map({}), + __currentMutator: null, + // Public functions + register: ( + subscriber: Subscriber + ): SubscriberFunction => { + if (getPrivateSubscriberRegistered(subscriber)) { + // If the subscriber is already registered, no-op. + return subscriber.target; } - } - }; - const dispatchWithMiddleware: DispatchFunction = middleware.reduceRight( - (next: DispatchFunction, m: Middleware) => m.bind(null, next), - finalDispatch - ); - - const createActionId = (): string => { - return (nextActionId++).toString(); - }; - - const wrapMutatorTarget = ({ - actionCreator, - target, - }: Mutator) => { - // Wrap the callback in a MobX action so it can modify the store - const actionType = getPrivateActionType(actionCreator); - return mobxAction(actionType, (actionMessage: TAction) => { - try { - currentMutator = actionType; - target(actionMessage); - currentMutator = null; - } catch (e) { - currentMutator = null; - throw e; + const actionId = getPrivateActionId(subscriber.actionCreator); + if (!actionId) { + throw new Error(`A ${subscriber.type} can only subscribe to action creators.`); } - }); - }; - - // Public functions - const register = ( - subscriber: Mutator | Orchestrator - ): SubscriberFunction => { - if (getPrivateSubscriberRegistered(subscriber)) { - // If the subscriber is already registered, no-op. - return subscriber.target; - } - - const actionId = getPrivateActionId(subscriber.actionCreator); - if (!actionId) { - throw new Error(`A ${subscriber.type} can only subscribe to action creators.`); - } - - const wrappedTarget = - subscriber.type == 'mutator' ? wrapMutatorTarget(subscriber) : subscriber.target; - - if (!subscriptions[actionId]) { - subscriptions[actionId] = []; - } - subscriptions[actionId].push(wrappedTarget); - // Mark the subscriber as registered - setPrivateSubscriberRegistered(subscriber, true); + const wrappedTarget = + subscriber.type == 'mutator' + ? satchel.__wrapMutatorTarget(subscriber) + : subscriber.target; - return subscriber.target; - }; - - const dispatch = (actionMessage: ActionMessage): void => { - if (currentMutator) { - throw new Error( - `Mutator (${currentMutator}) may not dispatch action (${actionMessage.type})` - ); - } - - transaction(dispatchWithMiddleware.bind(null, actionMessage)); - }; - - const createActionCreator = >( - actionType: string, - target: TActionCreator, - shouldDispatch: boolean - ): TActionCreator => { - let actionId = createActionId(); - - let decoratedTarget = function createAction(...args: any[]) { - // Create the action message - let actionMessage: ActionMessage = target ? target.apply(null, args) : {}; - - // Stamp the action type - if (actionMessage.type) { - throw new Error('Action creators should not include the type property.'); + if (!satchel.__subscriptions[actionId]) { + satchel.__subscriptions[actionId] = []; } - // Stamp the action message with the type and the private ID - actionMessage.type = actionType; - setPrivateActionId(actionMessage, actionId); + satchel.__subscriptions[actionId].push(wrappedTarget); + // Mark the subscriber as registered + setPrivateSubscriberRegistered(subscriber, true); - // Dispatch if necessary - if (shouldDispatch) { - dispatch(actionMessage); + return subscriber.target; + }, + dispatch: (actionMessage: ActionMessage): void => { + if (satchel.__currentMutator) { + throw new Error( + `Mutator (${satchel.__currentMutator}) may not dispatch action (${actionMessage.type})` + ); } - return actionMessage; - } as TActionCreator; - - // Stamp the action creator function with the private ID - setPrivateActionId(decoratedTarget, actionId); - setActionType(decoratedTarget, actionType); - return decoratedTarget; - }; - - const actionCreator = < - T extends ActionMessage = {}, - TActionCreator extends ActionCreator = () => T - >( - actionType: string, - target?: TActionCreator - ): TActionCreator => { - return createActionCreator(actionType, target, false); - }; - - const action = < - T extends ActionMessage = {}, - TActionCreator extends ActionCreator = () => T - >( - actionType: string, - target?: TActionCreator - ): TActionCreator => { - return createActionCreator(actionType, target, true); - }; + transaction(satchel.__dispatchWithMiddleware.bind(null, actionMessage)); + }, + actionCreator: < + T extends ActionMessage = {}, + TActionCreator extends ActionCreator = () => T + >( + actionType: string, + target?: TActionCreator + ): TActionCreator => { + return satchel.__createActionCreator(actionType, target, false); + }, + action: = () => T>( + actionType: string, + target?: TActionCreator + ): TActionCreator => { + return satchel.__createActionCreator(actionType, target, true); + }, + getRootStore: (): ObservableMap => { + return satchel.__rootStore; + }, + hasSubscribers: (actionCreator: ActionCreator) => { + return !!satchel.__subscriptions[getPrivateActionId(actionCreator)]; + }, + createStore: (key: string, initialState: T): (() => T) => { + satchel.__createStoreAction(key, initialState); + return () => satchel.getRootStore().get(key); + }, + // Private functions + __createActionId: (): string => { + return (satchel.__nextActionId++).toString(); + }, + __finalDispatch: + finalDispatch ?? + ((actionMessage: ActionMessage): void | Promise => { + let actionId = getPrivateActionId(actionMessage); + let subscribers = satchel.__subscriptions[actionId]; + + if (subscribers) { + let promises: Promise[] = []; + + for (const subscriber of subscribers) { + let returnValue = subscriber(actionMessage); + if (returnValue) { + promises.push(returnValue); + } + } + + if (promises.length) { + return promises.length == 1 ? promises[0] : Promise.all(promises); + } + } + }), + __wrapMutatorTarget: ({ + actionCreator, + target, + }: Mutator) => { + // Wrap the callback in a MobX action so it can modify the store + const actionType = getPrivateActionType(actionCreator); + return mobxAction(actionType, (actionMessage: TAction) => { + try { + satchel.__currentMutator = actionType; + target(actionMessage); + satchel.__currentMutator = null; + } catch (e) { + satchel.__currentMutator = null; + throw e; + } + }); + }, + __createStoreAction: mobxAction('createStore', function createStoreAction( + key: string, + initialState: any + ) { + if (satchel.getRootStore().get(key)) { + throw new Error(`A store named ${key} has already been created.`); + } - const getRootStore = (): ObservableMap => { - return rootStore; - }; + satchel.getRootStore().set(key, initialState); + }), + __createActionCreator: >( + actionType: string, + target: TActionCreator, + shouldDispatch: boolean + ): TActionCreator => { + let actionId = satchel.__createActionId(); + + let decoratedTarget = function createAction(...args: any[]) { + // Create the action message + let actionMessage: ActionMessage = target ? target.apply(null, args) : {}; + + // Stamp the action type + if (actionMessage.type) { + throw new Error('Action creators should not include the type property.'); + } - const hasSubscribers = (actionCreator: ActionCreator) => { - return !!subscriptions[getPrivateActionId(actionCreator)]; - }; + // Stamp the action message with the type and the private ID + actionMessage.type = actionType; + setPrivateActionId(actionMessage, actionId); - const createStoreAction = mobxAction('createStore', function createStoreAction( - key: string, - initialState: any - ) { - if (getRootStore().get(key)) { - throw new Error(`A store named ${key} has already been created.`); - } + // Dispatch if necessary + if (shouldDispatch) { + satchel.dispatch(actionMessage); + } - getRootStore().set(key, initialState); - }); + return actionMessage; + } as TActionCreator; - const createStore = (key: string, initialState: T): (() => T) => { - createStoreAction(key, initialState); - return () => getRootStore().get(key); + // Stamp the action creator function with the private ID + setPrivateActionId(decoratedTarget, actionId); + setActionType(decoratedTarget, actionType); + return decoratedTarget; + }, + // This gets initialized below since we need __finalDispatch to be defined + __dispatchWithMiddleware: undefined as DispatchFunction, }; - const satchelInstance: SatchelInstance & PrivateSatchelFunctions = { - register, - dispatch, - actionCreator, - action, - getRootStore, - hasSubscribers, - createStore, - // Private functions used only for testing - __createActionId: createActionId, - __dispatchWithMiddleware: dispatchWithMiddleware, - __finalDispatch: finalDispatch, - __subscriptions: subscriptions, - __currentMutator: currentMutator, - }; + satchel.__dispatchWithMiddleware = middleware.reduceRight( + (next: DispatchFunction, m: Middleware) => m.bind(null, next), + satchel.__finalDispatch + ); - return satchelInstance; + return satchel; } + +// Exclude the private functions from the public API +export const createSatchel: (options?: SatchelOptions) => SatchelInstance = createSatchelInternal; diff --git a/src/index.ts b/src/index.ts index 9d29704..d7d8a66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ export { Orchestrator } from './interfaces/Orchestrator'; export { default as mutator } from './mutator'; import { default as orchestrator } from './orchestrator'; export { mutatorAction } from './simpleSubscribers'; +export { createSatchel, SatchelInstance } from './createSatchel'; export { useStrict }; // exporting an alias for orchestrator called "flow" diff --git a/src/interfaces/Subscriber.ts b/src/interfaces/Subscriber.ts new file mode 100644 index 0000000..669eecd --- /dev/null +++ b/src/interfaces/Subscriber.ts @@ -0,0 +1,7 @@ +import ActionMessage from './ActionMessage'; +import { Mutator } from './Mutator'; +import { Orchestrator } from './Orchestrator'; + +export type Subscriber = + | Mutator + | Orchestrator; diff --git a/src/mutator.ts b/src/mutator.ts index 0a37dba..f4b682a 100644 --- a/src/mutator.ts +++ b/src/mutator.ts @@ -2,14 +2,19 @@ import ActionCreator from './interfaces/ActionCreator'; import ActionMessage from './interfaces/ActionMessage'; import MutatorFunction from './interfaces/MutatorFunction'; import { Mutator } from './interfaces/Mutator'; +import { setPrivateSubscriberRegistered } from './privatePropertyUtils'; export default function mutator( actionCreator: ActionCreator, target: MutatorFunction ): Mutator { - return { + const mutator: Mutator = { type: 'mutator', actionCreator, target, }; + + setPrivateSubscriberRegistered(mutator, false); + + return mutator; } diff --git a/src/orchestrator.ts b/src/orchestrator.ts index 5a4a84e..b7ecfa2 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -2,14 +2,19 @@ import ActionCreator from './interfaces/ActionCreator'; import ActionMessage from './interfaces/ActionMessage'; import OrchestratorFunction from './interfaces/OrchestratorFunction'; import { Orchestrator } from './interfaces/Orchestrator'; +import { setPrivateSubscriberRegistered } from './privatePropertyUtils'; export default function orchestrator( actionCreator: ActionCreator, target: OrchestratorFunction ): Orchestrator { - return { + const orchestrator: Orchestrator = { type: 'orchestrator', actionCreator, target, }; + + setPrivateSubscriberRegistered(orchestrator, false); + + return orchestrator; } diff --git a/src/simpleSubscribers.ts b/src/simpleSubscribers.ts index b1eb4b7..4cd8494 100644 --- a/src/simpleSubscribers.ts +++ b/src/simpleSubscribers.ts @@ -1,8 +1,20 @@ import SimpleAction from './interfaces/SimpleAction'; import mutator from './mutator'; import { SatchelInstance } from './createSatchel'; +import { Mutator } from './interfaces/Mutator'; +import { Orchestrator } from './interfaces/Orchestrator'; +import ActionMessage from './interfaces/ActionMessage'; -export function createSimpleSubscriber(decorator: Function) { +type Decorator = ( + actionCreator: () => { + args: IArguments; + }, + callback: (actionMessage: any) => any +) => Mutator | Orchestrator; + +export function createSimpleSubscriber( + decorator: Decorator +) { return function simpleSubscriber any>( satchelInstance: SatchelInstance, actionType: string, diff --git a/test/applyMiddlewareTests.ts b/test/applyMiddlewareTests.ts index b704473..b8f3978 100644 --- a/test/applyMiddlewareTests.ts +++ b/test/applyMiddlewareTests.ts @@ -1,4 +1,5 @@ import 'jasmine'; +import { createSatchel, createSatchelInternal, SatchelInstance } from '../src/createSatchel'; import { createTestSatchel } from './utils/createTestSatchel'; describe('applyMiddleware', () => { @@ -48,11 +49,13 @@ describe('applyMiddleware', () => { next(actionMessage); }, ]; - const satchel = createTestSatchel({ middleware }); - spyOn(satchel, '__finalDispatch').and.callFake(() => { - sequence.push('finalDispatch'); - }); + const satchel = createTestSatchel( + { middleware }, + jasmine.createSpy('finalDispatch').and.callFake(() => { + sequence.push('finalDispatch'); + }) + ); // Act satchel.__dispatchWithMiddleware({}); diff --git a/test/dispatcherTests.ts b/test/dispatcherTests.ts index b2f7af8..77c412d 100644 --- a/test/dispatcherTests.ts +++ b/test/dispatcherTests.ts @@ -2,7 +2,7 @@ import 'jasmine'; import { createTestSatchel } from './utils/createTestSatchel'; import * as privateUtils from '../src/privatePropertyUtils'; -describe('dispatcher', () => { +describe('dispatch', () => { /* it('subscribe registers a callback for a given action', () => { // Arrange @@ -38,6 +38,7 @@ describe('dispatcher', () => { // Arrange let actionMessage = {}; const satchel = createTestSatchel(); + spyOn(satchel, '__dispatchWithMiddleware'); // Act satchel.dispatch(actionMessage); diff --git a/test/endToEndTests.ts b/test/endToEndTests.ts index e0398f2..1e9e6af 100644 --- a/test/endToEndTests.ts +++ b/test/endToEndTests.ts @@ -60,9 +60,10 @@ describe('satcheljs', () => { autorun(() => store.testProperty); // strict mode only applies if store is observed let modifyStore = satchel.action('modifyStore'); - mutator(modifyStore, () => { + let testMutator = mutator(modifyStore, () => { store.testProperty = 'newValue'; }); + satchel.register(testMutator); // Act modifyStore(); @@ -78,9 +79,10 @@ describe('satcheljs', () => { autorun(() => store.testProperty); // strict mode only applies if store is observed let modifyStore = satchel.action('modifyStore'); - orchestrator(modifyStore, () => { + let testOrchestator = orchestrator(modifyStore, () => { store.testProperty = 'newValue'; }); + satchel.register(testOrchestator); // Act / Assert expect(() => { @@ -94,13 +96,15 @@ describe('satcheljs', () => { let store = satchel.createStore('testStore', { testProperty: 0 })(); let modifyStore = satchel.action('modifyStore'); - mutator(modifyStore, () => { + const testMutator1 = mutator(modifyStore, () => { store.testProperty++; }); - mutator(modifyStore, () => { + const testMutator2 = mutator(modifyStore, () => { store.testProperty++; }); + satchel.register(testMutator1); + satchel.register(testMutator2); let values: number[] = []; autorun(() => { diff --git a/test/mutatorTests.ts b/test/mutatorTests.ts index 87cdbb9..6f424f9 100644 --- a/test/mutatorTests.ts +++ b/test/mutatorTests.ts @@ -2,15 +2,32 @@ import 'jasmine'; import mutator from '../src/mutator'; import * as mobx from 'mobx'; import { createTestSatchel } from './utils/createTestSatchel'; +import { getPrivateSubscriberRegistered } from '../src/privatePropertyUtils'; describe('mutator', () => { + it('returns a object describing the mutator', () => { + // Arrange + const callback = () => {}; + const actionId = 'testAction'; + const actionCreator: any = { __SATCHELJS_ACTION_ID: actionId }; + + // Act + const testOrchestator = mutator(actionCreator, callback); + + // Assert + expect(testOrchestator.type).toBe('mutator'); + expect(testOrchestator.actionCreator).toBe(actionCreator); + expect(testOrchestator.target).toBe(callback); + expect(getPrivateSubscriberRegistered(testOrchestator)).toBe(false); + }); it('throws if the action creator does not have an action ID', () => { // Arrange + const satchel = createTestSatchel(); let actionCreator: any = {}; // Act / Assert expect(() => { - mutator(actionCreator, () => {}); + satchel.register(mutator(actionCreator, () => {})); }).toThrow(); }); @@ -70,8 +87,7 @@ describe('mutator', () => { const testMutator = mutator(actionCreator, callback); // Act - let subscribedCallback = (satchel.register as jasmine.Spy).calls.argsFor(0)[1]; - subscribedCallback(testMutator); + satchel.register(testMutator); // Assert expect(satchel.__currentMutator).toBe(null); @@ -90,9 +106,8 @@ describe('mutator', () => { const testMutator = mutator(actionCreator, callback); // Act - let subscribedCallback = (satchel.register as jasmine.Spy).calls.argsFor(0)[1]; try { - subscribedCallback(testMutator); + satchel.register(testMutator); } catch { // no op } diff --git a/test/orchestratorTests.ts b/test/orchestratorTests.ts index 5fe5722..d9316b0 100644 --- a/test/orchestratorTests.ts +++ b/test/orchestratorTests.ts @@ -1,15 +1,32 @@ import 'jasmine'; import orchestrator from '../src/orchestrator'; +import { getPrivateSubscriberRegistered } from '../src/privatePropertyUtils'; import { createTestSatchel } from './utils/createTestSatchel'; describe('orchestrator', () => { + it('returns a object describing the orchestrator', () => { + // Arrange + const callback = () => {}; + const actionId = 'testAction'; + const actionCreator: any = { __SATCHELJS_ACTION_ID: actionId }; + + // Act + const testOrchestator = orchestrator(actionCreator, callback); + + // Assert + expect(testOrchestator.type).toBe('orchestrator'); + expect(testOrchestator.actionCreator).toBe(actionCreator); + expect(testOrchestator.target).toBe(callback); + expect(getPrivateSubscriberRegistered(testOrchestator)).toBe(false); + }); it('throws if the action creator does not have an action ID', () => { // Arrange + const satchel = createTestSatchel(); let actionCreator: any = {}; // Act / Assert expect(() => { - orchestrator(actionCreator, () => {}); + satchel.register(orchestrator(actionCreator, () => {})); }).toThrow(); }); @@ -27,6 +44,7 @@ describe('orchestrator', () => { // Assert expect(satchel.__subscriptions[actionId]).toBeDefined(); expect(satchel.__subscriptions[actionId][0]).toBe(callback); + expect(getPrivateSubscriberRegistered(testOrchestator)).toBe(true); }); it('returns the target function', () => { diff --git a/test/registerTests.ts b/test/registerTests.ts new file mode 100644 index 0000000..3eee726 --- /dev/null +++ b/test/registerTests.ts @@ -0,0 +1,24 @@ +import 'jasmine'; +import mutator from '../src/mutator'; +import * as privatePropertyUtils from '../src/privatePropertyUtils'; +import { createTestSatchel } from './utils/createTestSatchel'; + +describe('register', () => { + it('registers the subscriber once', () => { + const callback = () => {}; + const actionId = 'testAction'; + const actionCreator: any = { __SATCHELJS_ACTION_ID: actionId }; + spyOn(privatePropertyUtils, 'getPrivateActionId').and.callThrough(); + const testOrchestator = mutator(actionCreator, callback); + const satchel = createTestSatchel(); + + // Act + satchel.register(testOrchestator); + satchel.register(testOrchestator); + + // Assert + // This this the first function used after the check for if the subscriber is already registered + // so it should only be called once + expect(privatePropertyUtils.getPrivateActionId).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/simpleSubscribersTests.ts b/test/simpleSubscribersTests.ts index 384ca84..cc07884 100644 --- a/test/simpleSubscribersTests.ts +++ b/test/simpleSubscribersTests.ts @@ -1,6 +1,7 @@ import 'jasmine'; import { createSimpleSubscriber } from '../src/simpleSubscribers'; import { createTestSatchel } from './utils/createTestSatchel'; +import * as mutator from '../src/mutator'; describe('simpleSubscribers', () => { let actionCreatorSpy: jasmine.Spy; @@ -11,7 +12,7 @@ describe('simpleSubscribers', () => { beforeEach(() => { satchel = createTestSatchel(); actionCreatorSpy = spyOn(satchel, 'action').and.callThrough(); - decoratorSpy = jasmine.createSpy('decoratorSpy'); + decoratorSpy = spyOn(mutator, 'default').and.callThrough(); simpleSubscriber = createSimpleSubscriber(decoratorSpy); }); diff --git a/test/utils/createTestSatchel.ts b/test/utils/createTestSatchel.ts index 82c95bd..da5e88a 100644 --- a/test/utils/createTestSatchel.ts +++ b/test/utils/createTestSatchel.ts @@ -1,10 +1,4 @@ -import { - createSatchel, - SatchelInstance, - SatchelOptions, - PrivateSatchelFunctions, -} from '../../src/createSatchel'; +import { createSatchelInternal } from '../../src/createSatchel'; -type TestSatchelFunction = (options?: SatchelOptions) => SatchelInstance & PrivateSatchelFunctions; - -export const createTestSatchel = createSatchel as TestSatchelFunction; +// Decorator for the internal createSatchel function +export const createTestSatchel = createSatchelInternal; From 47ea4867cd7a71b70e6bb11d1aed90b6f02ab080 Mon Sep 17 00:00:00 2001 From: jareill Date: Mon, 24 Jun 2024 15:55:50 -0700 Subject: [PATCH 05/12] bump to v5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f8eff08..315d2e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "satcheljs", - "version": "4.3.1", + "version": "5.0.0", "description": "Store implementation for functional reactive flux.", "lint-staged": { "*.{ts,tsx}": [ From d42459d98fdaa5b15027430606969db5ec608453 Mon Sep 17 00:00:00 2001 From: jareill Date: Wed, 24 Jul 2024 14:10:52 -0700 Subject: [PATCH 06/12] Fix typing on register functions and pr comments and update prettier --- package.json | 4 +- src/createSatchel.ts | 137 ++++----------------- src/index.ts | 7 +- src/interfaces/ActionCreator.ts | 2 +- src/interfaces/ActionMessage.ts | 5 +- src/interfaces/DispatchFunction.ts | 2 +- src/interfaces/Middleware.ts | 4 +- src/interfaces/Mutator.ts | 7 +- src/interfaces/Orchestrator.ts | 7 +- src/interfaces/OrchestratorFunction.ts | 2 +- src/interfaces/Satchel.ts | 72 +++++++++++ src/interfaces/SatchelInternal.ts | 6 + src/interfaces/SatchelInternalFunctions.ts | 21 ++++ src/interfaces/SatchelOptions.ts | 7 ++ src/interfaces/SatchelState.ts | 12 ++ src/interfaces/Subscriber.ts | 10 +- src/interfaces/SubscriberFunction.ts | 6 +- src/mutator.ts | 8 +- src/orchestrator.ts | 8 +- src/privatePropertyUtils.ts | 4 - src/simpleSubscribers.ts | 33 ++--- yarn.lock | 8 +- 22 files changed, 197 insertions(+), 175 deletions(-) create mode 100644 src/interfaces/Satchel.ts create mode 100644 src/interfaces/SatchelInternal.ts create mode 100644 src/interfaces/SatchelInternalFunctions.ts create mode 100644 src/interfaces/SatchelOptions.ts create mode 100644 src/interfaces/SatchelState.ts diff --git a/package.json b/package.json index 315d2e2..7434715 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "satcheljs", - "version": "5.0.0", + "version": "5.0.0-beta.1", "description": "Store implementation for functional reactive flux.", "lint-staged": { "*.{ts,tsx}": [ @@ -40,7 +40,7 @@ "mobx": "^4.4.0", "mobx-react": "^5.2.0", "npm-run-all": "^4.0.2", - "prettier": "^1.19.1", + "prettier": "^3.3.3", "react": "15.4.2", "react-addons-test-utils": "~15.4.0", "react-dom": "15.4.2", diff --git a/src/createSatchel.ts b/src/createSatchel.ts index 4dfcc51..913c5ba 100644 --- a/src/createSatchel.ts +++ b/src/createSatchel.ts @@ -1,4 +1,4 @@ -import { observable, ObservableMap, transaction, action as mobxAction, IAction } from 'mobx'; +import { observable, ObservableMap, transaction, action as mobxAction } from 'mobx'; import { getPrivateActionId, getPrivateActionType, @@ -7,112 +7,25 @@ import { getPrivateSubscriberRegistered, setPrivateSubscriberRegistered, } from './privatePropertyUtils'; -import ActionMessage from './interfaces/ActionMessage'; -import Middleware from './interfaces/Middleware'; -import DispatchFunction from './interfaces/DispatchFunction'; -import SubscriberFunction from './interfaces/SubscriberFunction'; -import ActionCreator from './interfaces/ActionCreator'; -import { Mutator } from './interfaces/Mutator'; -import { Subscriber } from './interfaces/Subscriber'; - -type SatchelState = { - __rootStore: ObservableMap; - __nextActionId: number; - __subscriptions: { [key: string]: SubscriberFunction[] }; - __currentMutator: string | null; -}; - -export type SatchelInstance = { - /** - * Resolves the target of the subscriber and registers it with the dispatcher. - */ - register: ( - subscriber: Subscriber - ) => SubscriberFunction; - /** - * Dispatches the action message - * @param actionMessage {ActionMessage} The action message to dispatch - * @returns {void} - */ - dispatch: (actionMessage: ActionMessage) => void; - /** - * Decorates a function as an action creator. - * @template T (type parameter) An interface describing the shape of the action message to create. - * @param actionType {string}:A string which identifies the type of the action. - * @param target {((...) => T)=} A function which creates and returns an action message - * @returns {ActionCreator} An action creator - */ - actionCreator: < - T extends ActionMessage = {}, - TActionCreator extends ActionCreator = () => T - >( - actionType: string, - target?: TActionCreator - ) => TActionCreator; - /** - * Decorates a function as an action creator which also dispatches the action message after creating it. - * - * @template T (type parameter) An interface describing the shape of the action message to create. - * @param actionType {string}:A string which identifies the type of the action. - * @param target {((...) => T)=} A function which creates and returns an action message - * @returns {ActionCreator} An action creator - */ - action: = () => T>( - actionType: string, - target?: TActionCreator - ) => TActionCreator; - /** - * Creates a Satchel store and returns a selector to it. - * - * @template T (type parameter) An interface describing the shape of the store. - * @param name {string} A unique identifier for the store. - * @param initialState {T} The initial state of the store. - * @returns {() => T} A selector to the store. - */ - createStore: (key: string, initialState: T) => () => T; - /** - * Returns Satchel's root store object of the satchel instance. - * @returns {ObservableMap} The root store object - */ - getRootStore: () => ObservableMap; - /** - * Returns whether the action creator has any subscribers. - * @returns {boolean} True if the action creator has subscribers, false otherwise. - */ - hasSubscribers: (actionCreator: ActionCreator) => boolean; -}; - -export type SatchelPrivateInstanceFunctions = { - __createActionId: () => string; - __dispatchWithMiddleware: DispatchFunction; - __finalDispatch: DispatchFunction; - __createStoreAction: (key: string, initialState: any) => void; - __createActionCreator: >( - actionType: string, - target: TActionCreator, - shouldDispatch: boolean - ) => TActionCreator; - __wrapMutatorTarget: ( - mutator: Mutator - ) => ((actionMessage: TAction) => void) & IAction; -}; - -export type SatchelInternalInstance = SatchelInstance & - SatchelPrivateInstanceFunctions & - SatchelState; - -export type SatchelOptions = { - middleware?: Array; -}; +import type ActionMessage from './interfaces/ActionMessage'; +import type Middleware from './interfaces/Middleware'; +import type DispatchFunction from './interfaces/DispatchFunction'; +import type SubscriberFunction from './interfaces/SubscriberFunction'; +import type ActionCreator from './interfaces/ActionCreator'; +import type Mutator from './interfaces/Mutator'; +import type Subscriber from './interfaces/Subscriber'; +import type SatchelOptions from './interfaces/SatchelOptions'; +import type SatchelInternal from './interfaces/SatchelInternal'; +import type Satchel from './interfaces/Satchel'; export function createSatchelInternal( options: SatchelOptions = {}, // This is only used for testing purposes finalDispatch?: DispatchFunction -): SatchelInternalInstance { +): SatchelInternal { const { middleware = [] } = options; - const satchel: SatchelInternalInstance = { + const satchel: SatchelInternal = { // State __subscriptions: {}, __nextActionId: 0, @@ -158,7 +71,7 @@ export function createSatchelInternal( }, actionCreator: < T extends ActionMessage = {}, - TActionCreator extends ActionCreator = () => T + TActionCreator extends ActionCreator = () => T, >( actionType: string, target?: TActionCreator @@ -187,7 +100,7 @@ export function createSatchelInternal( }, __finalDispatch: finalDispatch ?? - ((actionMessage: ActionMessage): void | Promise => { + ((actionMessage) => { let actionId = getPrivateActionId(actionMessage); let subscribers = satchel.__subscriptions[actionId]; @@ -223,16 +136,16 @@ export function createSatchelInternal( } }); }, - __createStoreAction: mobxAction('createStore', function createStoreAction( - key: string, - initialState: any - ) { - if (satchel.getRootStore().get(key)) { - throw new Error(`A store named ${key} has already been created.`); - } + __createStoreAction: mobxAction( + 'createStore', + function createStoreAction(key: string, initialState: any) { + if (satchel.getRootStore().get(key)) { + throw new Error(`A store named ${key} has already been created.`); + } - satchel.getRootStore().set(key, initialState); - }), + satchel.getRootStore().set(key, initialState); + } + ), __createActionCreator: >( actionType: string, target: TActionCreator, @@ -279,4 +192,4 @@ export function createSatchelInternal( } // Exclude the private functions from the public API -export const createSatchel: (options?: SatchelOptions) => SatchelInstance = createSatchelInternal; +export const createSatchel: (options?: SatchelOptions) => Satchel = createSatchelInternal; diff --git a/src/index.ts b/src/index.ts index d7d8a66..78641ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,14 +5,15 @@ export { default as ActionCreator } from './interfaces/ActionCreator'; export { default as ActionMessage } from './interfaces/ActionMessage'; export { default as DispatchFunction } from './interfaces/DispatchFunction'; export { default as Middleware } from './interfaces/Middleware'; +export { default as Satchel } from './interfaces/Satchel'; export { default as MutatorFunction } from './interfaces/MutatorFunction'; export { default as OrchestratorFunction } from './interfaces/OrchestratorFunction'; -export { Mutator } from './interfaces/Mutator'; -export { Orchestrator } from './interfaces/Orchestrator'; +export { default as Mutator } from './interfaces/Mutator'; +export { default as Orchestrator } from './interfaces/Orchestrator'; export { default as mutator } from './mutator'; import { default as orchestrator } from './orchestrator'; export { mutatorAction } from './simpleSubscribers'; -export { createSatchel, SatchelInstance } from './createSatchel'; +export { createSatchel } from './createSatchel'; export { useStrict }; // exporting an alias for orchestrator called "flow" diff --git a/src/interfaces/ActionCreator.ts b/src/interfaces/ActionCreator.ts index 000c9e5..4890003 100644 --- a/src/interfaces/ActionCreator.ts +++ b/src/interfaces/ActionCreator.ts @@ -1,4 +1,4 @@ -import ActionMessage from './ActionMessage'; +import type ActionMessage from './ActionMessage'; type ActionCreator = (...args: any[]) => T; export default ActionCreator; diff --git a/src/interfaces/ActionMessage.ts b/src/interfaces/ActionMessage.ts index da19dec..c52d54d 100644 --- a/src/interfaces/ActionMessage.ts +++ b/src/interfaces/ActionMessage.ts @@ -1,6 +1,5 @@ -interface ActionMessage { +type ActionMessage = { type?: string; [key: string]: any; -} - +}; export default ActionMessage; diff --git a/src/interfaces/DispatchFunction.ts b/src/interfaces/DispatchFunction.ts index c022aa4..fdb8f38 100644 --- a/src/interfaces/DispatchFunction.ts +++ b/src/interfaces/DispatchFunction.ts @@ -1,4 +1,4 @@ -import ActionMessage from './ActionMessage'; +import type ActionMessage from './ActionMessage'; type DispatchFunction = (actionMessage: ActionMessage) => void | Promise; export default DispatchFunction; diff --git a/src/interfaces/Middleware.ts b/src/interfaces/Middleware.ts index 0fd4ea3..24df316 100644 --- a/src/interfaces/Middleware.ts +++ b/src/interfaces/Middleware.ts @@ -1,5 +1,5 @@ -import ActionMessage from './ActionMessage'; -import DispatchFunction from './DispatchFunction'; +import type ActionMessage from './ActionMessage'; +import type DispatchFunction from './DispatchFunction'; type Middleware = (next: DispatchFunction, actionMessage: ActionMessage) => void; export default Middleware; diff --git a/src/interfaces/Mutator.ts b/src/interfaces/Mutator.ts index 99d72e6..565b4c1 100644 --- a/src/interfaces/Mutator.ts +++ b/src/interfaces/Mutator.ts @@ -1,9 +1,10 @@ import ActionCreator from './ActionCreator'; -import ActionMessage from './ActionMessage'; -import MutatorFunction from './MutatorFunction'; +import type ActionMessage from './ActionMessage'; +import type MutatorFunction from './MutatorFunction'; -export type Mutator = { +type Mutator = { type: 'mutator'; actionCreator: ActionCreator; target: MutatorFunction; }; +export default Mutator; diff --git a/src/interfaces/Orchestrator.ts b/src/interfaces/Orchestrator.ts index d92316d..bd3a70b 100644 --- a/src/interfaces/Orchestrator.ts +++ b/src/interfaces/Orchestrator.ts @@ -1,9 +1,10 @@ import ActionCreator from './ActionCreator'; -import ActionMessage from './ActionMessage'; -import OrchestratorFunction from './OrchestratorFunction'; +import type ActionMessage from './ActionMessage'; +import type OrchestratorFunction from './OrchestratorFunction'; -export type Orchestrator = { +type Orchestrator = { type: 'orchestrator'; actionCreator: ActionCreator; target: OrchestratorFunction; }; +export default Orchestrator; diff --git a/src/interfaces/OrchestratorFunction.ts b/src/interfaces/OrchestratorFunction.ts index 083820e..4c56749 100644 --- a/src/interfaces/OrchestratorFunction.ts +++ b/src/interfaces/OrchestratorFunction.ts @@ -1,4 +1,4 @@ -import ActionMessage from './ActionMessage'; +import type ActionMessage from './ActionMessage'; type OrchestratorFunction = (actionMessage: T) => void | Promise; export default OrchestratorFunction; diff --git a/src/interfaces/Satchel.ts b/src/interfaces/Satchel.ts new file mode 100644 index 0000000..f6c91c0 --- /dev/null +++ b/src/interfaces/Satchel.ts @@ -0,0 +1,72 @@ +import type { ObservableMap } from 'mobx'; +import type ActionCreator from './ActionCreator'; +import type ActionMessage from './ActionMessage'; +import type Mutator from './Mutator'; +import type MutatorFunction from './MutatorFunction'; +import type Orchestrator from './Orchestrator'; +import type OrchestratorFunction from './OrchestratorFunction'; + +type Satchel = { + /** + * Resolves the target of the subscriber and registers it with the dispatcher. + */ + register( + subscriber: Mutator + ): MutatorFunction; + register( + subscriber: Orchestrator + ): OrchestratorFunction; + + /** + * Dispatches the action message + * @param actionMessage {ActionMessage} The action message to dispatch + * @returns {void} + */ + dispatch: (actionMessage: ActionMessage) => void; + /** + * Decorates a function as an action creator. + * @template T (type parameter) An interface describing the shape of the action message to create. + * @param actionType {string}:A string which identifies the type of the action. + * @param target {((...) => T)=} A function which creates and returns an action message + * @returns {ActionCreator} An action creator + */ + actionCreator: < + T extends ActionMessage = {}, + TActionCreator extends ActionCreator = () => T, + >( + actionType: string, + target?: TActionCreator + ) => TActionCreator; + /** + * Decorates a function as an action creator which also dispatches the action message after creating it. + * + * @template T (type parameter) An interface describing the shape of the action message to create. + * @param actionType {string}:A string which identifies the type of the action. + * @param target {((...) => T)=} A function which creates and returns an action message + * @returns {ActionCreator} An action creator + */ + action: = () => T>( + actionType: string, + target?: TActionCreator + ) => TActionCreator; + /** + * Creates a Satchel store and returns a selector to it. + * + * @template T (type parameter) An interface describing the shape of the store. + * @param name {string} A unique identifier for the store. + * @param initialState {T} The initial state of the store. + * @returns {() => T} A selector to the store. + */ + createStore: (key: string, initialState: T) => () => T; + /** + * Returns Satchel's root store object of the satchel instance. + * @returns {ObservableMap} The root store object + */ + getRootStore: () => ObservableMap; + /** + * Returns whether the action creator has any subscribers. + * @returns {boolean} True if the action creator has subscribers, false otherwise. + */ + hasSubscribers: (actionCreator: ActionCreator) => boolean; +}; +export default Satchel; diff --git a/src/interfaces/SatchelInternal.ts b/src/interfaces/SatchelInternal.ts new file mode 100644 index 0000000..2b4b012 --- /dev/null +++ b/src/interfaces/SatchelInternal.ts @@ -0,0 +1,6 @@ +import type Satchel from './Satchel'; +import type SatchelInternalFunctions from './SatchelInternalFunctions'; +import type SatchelState from './SatchelState'; + +type SatchelInternal = Satchel & SatchelInternalFunctions & SatchelState; +export default SatchelInternal; diff --git a/src/interfaces/SatchelInternalFunctions.ts b/src/interfaces/SatchelInternalFunctions.ts new file mode 100644 index 0000000..165ff11 --- /dev/null +++ b/src/interfaces/SatchelInternalFunctions.ts @@ -0,0 +1,21 @@ +import type { IAction } from 'mobx'; +import type ActionCreator from './ActionCreator'; +import type ActionMessage from './ActionMessage'; +import type DispatchFunction from './DispatchFunction'; +import type Mutator from './Mutator'; + +type SatchelInternalFunctions = { + __createActionId: () => string; + __dispatchWithMiddleware: DispatchFunction; + __finalDispatch: DispatchFunction; + __createStoreAction: (key: string, initialState: any) => void; + __createActionCreator: >( + actionType: string, + target: TActionCreator, + shouldDispatch: boolean + ) => TActionCreator; + __wrapMutatorTarget: ( + mutator: Mutator + ) => ((actionMessage: TAction) => void) & IAction; +}; +export default SatchelInternalFunctions; diff --git a/src/interfaces/SatchelOptions.ts b/src/interfaces/SatchelOptions.ts new file mode 100644 index 0000000..fc082a7 --- /dev/null +++ b/src/interfaces/SatchelOptions.ts @@ -0,0 +1,7 @@ +import type Middleware from './Middleware'; + +type SatchelOptions = { + middleware?: Middleware[]; +}; + +export default SatchelOptions; diff --git a/src/interfaces/SatchelState.ts b/src/interfaces/SatchelState.ts new file mode 100644 index 0000000..5c6d164 --- /dev/null +++ b/src/interfaces/SatchelState.ts @@ -0,0 +1,12 @@ +import type { ObservableMap } from 'mobx'; +import type ActionMessage from './ActionMessage'; +import type SubscriberFunction from './SubscriberFunction'; + +type SatchelState = { + __rootStore: ObservableMap; + __nextActionId: number; + __subscriptions: { [key: string]: SubscriberFunction[] }; + __currentMutator: string | null; +}; + +export default SatchelState; diff --git a/src/interfaces/Subscriber.ts b/src/interfaces/Subscriber.ts index 669eecd..23e4b82 100644 --- a/src/interfaces/Subscriber.ts +++ b/src/interfaces/Subscriber.ts @@ -1,7 +1,9 @@ -import ActionMessage from './ActionMessage'; -import { Mutator } from './Mutator'; -import { Orchestrator } from './Orchestrator'; +import type ActionMessage from './ActionMessage'; +import type Mutator from './Mutator'; +import type Orchestrator from './Orchestrator'; -export type Subscriber = +type Subscriber = | Mutator | Orchestrator; + +export default Subscriber; diff --git a/src/interfaces/SubscriberFunction.ts b/src/interfaces/SubscriberFunction.ts index 1e15ccb..ce5214e 100644 --- a/src/interfaces/SubscriberFunction.ts +++ b/src/interfaces/SubscriberFunction.ts @@ -1,6 +1,6 @@ -import ActionMessage from './ActionMessage'; -import MutatorFunction from './MutatorFunction'; -import OrchestratorFunction from './OrchestratorFunction'; +import type ActionMessage from './ActionMessage'; +import type MutatorFunction from './MutatorFunction'; +import type OrchestratorFunction from './OrchestratorFunction'; type SubscriberFunction = | MutatorFunction diff --git a/src/mutator.ts b/src/mutator.ts index f4b682a..e2bf45f 100644 --- a/src/mutator.ts +++ b/src/mutator.ts @@ -1,7 +1,7 @@ -import ActionCreator from './interfaces/ActionCreator'; -import ActionMessage from './interfaces/ActionMessage'; -import MutatorFunction from './interfaces/MutatorFunction'; -import { Mutator } from './interfaces/Mutator'; +import type ActionCreator from './interfaces/ActionCreator'; +import type ActionMessage from './interfaces/ActionMessage'; +import type MutatorFunction from './interfaces/MutatorFunction'; +import type Mutator from './interfaces/Mutator'; import { setPrivateSubscriberRegistered } from './privatePropertyUtils'; export default function mutator( diff --git a/src/orchestrator.ts b/src/orchestrator.ts index b7ecfa2..553155e 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -1,7 +1,7 @@ -import ActionCreator from './interfaces/ActionCreator'; -import ActionMessage from './interfaces/ActionMessage'; -import OrchestratorFunction from './interfaces/OrchestratorFunction'; -import { Orchestrator } from './interfaces/Orchestrator'; +import type ActionCreator from './interfaces/ActionCreator'; +import type ActionMessage from './interfaces/ActionMessage'; +import type OrchestratorFunction from './interfaces/OrchestratorFunction'; +import type Orchestrator from './interfaces/Orchestrator'; import { setPrivateSubscriberRegistered } from './privatePropertyUtils'; export default function orchestrator( diff --git a/src/privatePropertyUtils.ts b/src/privatePropertyUtils.ts index 46bd41d..4e515ac 100644 --- a/src/privatePropertyUtils.ts +++ b/src/privatePropertyUtils.ts @@ -21,7 +21,3 @@ export const setPrivateSubscriberRegistered = (target: any, isRegistered: boolea export const getPrivateSubscriberRegistered = (target: any): boolean => { return target.__SATCHELJS_SUBSCRIBER_REGISTERED; }; - -export const setPrivateFunction = (property: string, target: any, func: any) => { - target[property] = func; -}; diff --git a/src/simpleSubscribers.ts b/src/simpleSubscribers.ts index 4cd8494..42a8300 100644 --- a/src/simpleSubscribers.ts +++ b/src/simpleSubscribers.ts @@ -1,22 +1,12 @@ -import SimpleAction from './interfaces/SimpleAction'; +import type SimpleAction from './interfaces/SimpleAction'; +import type Satchel from './interfaces/Satchel'; import mutator from './mutator'; -import { SatchelInstance } from './createSatchel'; -import { Mutator } from './interfaces/Mutator'; -import { Orchestrator } from './interfaces/Orchestrator'; -import ActionMessage from './interfaces/ActionMessage'; -type Decorator = ( - actionCreator: () => { - args: IArguments; - }, - callback: (actionMessage: any) => any -) => Mutator | Orchestrator; +type MutatorDecorator = typeof mutator; -export function createSimpleSubscriber( - decorator: Decorator -) { +export function createSimpleSubscriber(decorator: MutatorDecorator) { return function simpleSubscriber any>( - satchelInstance: SatchelInstance, + satchelInstance: Satchel, actionType: string, target: TFunction ): SimpleAction { @@ -31,16 +21,17 @@ export function createSimpleSubscriber; + return simpleActionCreator as any as SimpleAction; }; } diff --git a/yarn.lock b/yarn.lock index c1f27c3..48fa8e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3166,10 +3166,10 @@ prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" -prettier@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" - integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +prettier@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== pretty-format@^27.2.5: version "27.2.5" From 389aa6fd49dbfcac3cfb39998b709baac04f952a Mon Sep 17 00:00:00 2001 From: jareill Date: Wed, 24 Jul 2024 14:16:48 -0700 Subject: [PATCH 07/12] Update node version for prettier --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 644f41a..240791d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: '12.x' + node-version: '14.x' - run: yarn --frozen-lockfile - run: yarn build - run: yarn test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f5f6c2e..49323ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: '12.x' + node-version: '14.x' registry-url: 'https://registry.npmjs.org' - run: yarn - run: yarn build From b9cdc5a6ec06abc2416ce07320ba4471f04c8e2a Mon Sep 17 00:00:00 2001 From: jareill Date: Wed, 24 Jul 2024 14:19:19 -0700 Subject: [PATCH 08/12] Fix test imports --- test/applyMiddlewareTests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/applyMiddlewareTests.ts b/test/applyMiddlewareTests.ts index b8f3978..3ef826f 100644 --- a/test/applyMiddlewareTests.ts +++ b/test/applyMiddlewareTests.ts @@ -1,5 +1,4 @@ import 'jasmine'; -import { createSatchel, createSatchelInternal, SatchelInstance } from '../src/createSatchel'; import { createTestSatchel } from './utils/createTestSatchel'; describe('applyMiddleware', () => { From 09f8cd03067caca1085e5f00f53f0867b426729c Mon Sep 17 00:00:00 2001 From: jareill Date: Thu, 25 Jul 2024 13:56:27 -0700 Subject: [PATCH 09/12] Update middleware tests --- src/createSatchel.ts | 54 +++++++++++++++++++----------------- test/applyMiddlewareTests.ts | 22 +++++++-------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/createSatchel.ts b/src/createSatchel.ts index 913c5ba..ec8c7f3 100644 --- a/src/createSatchel.ts +++ b/src/createSatchel.ts @@ -18,11 +18,17 @@ import type SatchelOptions from './interfaces/SatchelOptions'; import type SatchelInternal from './interfaces/SatchelInternal'; import type Satchel from './interfaces/Satchel'; -export function createSatchelInternal( - options: SatchelOptions = {}, - // This is only used for testing purposes - finalDispatch?: DispatchFunction -): SatchelInternal { +export function createDispatchWithMiddleware( + middleware: Middleware[], + finalDispatch: DispatchFunction +) { + return middleware.reduceRight( + (next: DispatchFunction, m: Middleware) => m.bind(null, next), + finalDispatch + ); +} + +export function createSatchelInternal(options: SatchelOptions = {}): SatchelInternal { const { middleware = [] } = options; const satchel: SatchelInternal = { @@ -98,27 +104,25 @@ export function createSatchelInternal( __createActionId: (): string => { return (satchel.__nextActionId++).toString(); }, - __finalDispatch: - finalDispatch ?? - ((actionMessage) => { - let actionId = getPrivateActionId(actionMessage); - let subscribers = satchel.__subscriptions[actionId]; - - if (subscribers) { - let promises: Promise[] = []; - - for (const subscriber of subscribers) { - let returnValue = subscriber(actionMessage); - if (returnValue) { - promises.push(returnValue); - } - } + __finalDispatch: (actionMessage) => { + let actionId = getPrivateActionId(actionMessage); + let subscribers = satchel.__subscriptions[actionId]; + + if (subscribers) { + let promises: Promise[] = []; - if (promises.length) { - return promises.length == 1 ? promises[0] : Promise.all(promises); + for (const subscriber of subscribers) { + let returnValue = subscriber(actionMessage); + if (returnValue) { + promises.push(returnValue); } } - }), + + if (promises.length) { + return promises.length == 1 ? promises[0] : Promise.all(promises); + } + } + }, __wrapMutatorTarget: ({ actionCreator, target, @@ -183,8 +187,8 @@ export function createSatchelInternal( __dispatchWithMiddleware: undefined as DispatchFunction, }; - satchel.__dispatchWithMiddleware = middleware.reduceRight( - (next: DispatchFunction, m: Middleware) => m.bind(null, next), + satchel.__dispatchWithMiddleware = createDispatchWithMiddleware( + middleware, satchel.__finalDispatch ); diff --git a/test/applyMiddlewareTests.ts b/test/applyMiddlewareTests.ts index 3ef826f..27e443a 100644 --- a/test/applyMiddlewareTests.ts +++ b/test/applyMiddlewareTests.ts @@ -1,4 +1,6 @@ import 'jasmine'; +import { DispatchFunction, Middleware } from '../src'; +import { createDispatchWithMiddleware } from '../src/createSatchel'; import { createTestSatchel } from './utils/createTestSatchel'; describe('applyMiddleware', () => { @@ -34,30 +36,28 @@ describe('applyMiddleware', () => { expect(actualNext).toBe(satchel.__finalDispatch); }); - it('middleware and finalDispatch get called in order', () => { + it('createDispatchWithMiddleware creates a function that calls middleware and finalDispatch in order', () => { // Arrange let sequence: string[] = []; - const middleware = [ - (next: any, actionMessage: any) => { + (next: DispatchFunction, actionMessage: any) => { sequence.push('middleware1'); next(actionMessage); }, - (next: any, actionMessage: any) => { + (next: DispatchFunction, actionMessage: any) => { sequence.push('middleware2'); next(actionMessage); }, ]; - const satchel = createTestSatchel( - { middleware }, - jasmine.createSpy('finalDispatch').and.callFake(() => { - sequence.push('finalDispatch'); - }) - ); + const finalDispatch = (_actionMessage: any) => { + sequence.push('finalDispatch'); + }; + + const testDispatchWithMiddleware = createDispatchWithMiddleware(middleware, finalDispatch); // Act - satchel.__dispatchWithMiddleware({}); + testDispatchWithMiddleware({}); // Assert expect(sequence).toEqual(['middleware1', 'middleware2', 'finalDispatch']); From 2de08fc14321764e68a61cc6aaa6e584df063aea Mon Sep 17 00:00:00 2001 From: jareill Date: Thu, 25 Jul 2024 14:04:28 -0700 Subject: [PATCH 10/12] Add utility functions to get context of a satchel instance --- src/createSatchel.ts | 6 ++++++ src/interfaces/Satchel.ts | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/createSatchel.ts b/src/createSatchel.ts index ec8c7f3..a70b68b 100644 --- a/src/createSatchel.ts +++ b/src/createSatchel.ts @@ -100,6 +100,12 @@ export function createSatchelInternal(options: SatchelOptions = {}): SatchelInte satchel.__createStoreAction(key, initialState); return () => satchel.getRootStore().get(key); }, + getSubscriptions: () => { + return satchel.__subscriptions; + }, + getCurrentMutator: () => { + return satchel.__currentMutator; + }, // Private functions __createActionId: (): string => { return (satchel.__nextActionId++).toString(); diff --git a/src/interfaces/Satchel.ts b/src/interfaces/Satchel.ts index f6c91c0..c43d455 100644 --- a/src/interfaces/Satchel.ts +++ b/src/interfaces/Satchel.ts @@ -5,6 +5,7 @@ import type Mutator from './Mutator'; import type MutatorFunction from './MutatorFunction'; import type Orchestrator from './Orchestrator'; import type OrchestratorFunction from './OrchestratorFunction'; +import SatchelState from './SatchelState'; type Satchel = { /** @@ -68,5 +69,14 @@ type Satchel = { * @returns {boolean} True if the action creator has subscribers, false otherwise. */ hasSubscribers: (actionCreator: ActionCreator) => boolean; + + /** + * Utility function to get the current mutator being executed. + */ + getCurrentMutator: () => SatchelState['__currentMutator']; + /** + * Utility function to get the current subscriptions. + */ + getSubscriptions: () => SatchelState['__subscriptions']; }; export default Satchel; From cdf63517d43544db1a38ca32f62d269fd72dedb6 Mon Sep 17 00:00:00 2001 From: jareill Date: Mon, 29 Jul 2024 12:16:31 -0700 Subject: [PATCH 11/12] Move mutator action to satchel instance --- src/createSatchel.ts | 23 +++++++++++++++++++++++ src/interfaces/Satchel.ts | 7 +++++++ test/applyMiddlewareTests.ts | 4 ++-- test/endToEndTests.ts | 7 +++---- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/createSatchel.ts b/src/createSatchel.ts index a70b68b..f7d08a6 100644 --- a/src/createSatchel.ts +++ b/src/createSatchel.ts @@ -17,6 +17,9 @@ import type Subscriber from './interfaces/Subscriber'; import type SatchelOptions from './interfaces/SatchelOptions'; import type SatchelInternal from './interfaces/SatchelInternal'; import type Satchel from './interfaces/Satchel'; +import mutator from './mutator'; + +type ActionMessageWithArgs = ActionMessage & { args: T }; export function createDispatchWithMiddleware( middleware: Middleware[], @@ -90,6 +93,26 @@ export function createSatchelInternal(options: SatchelOptions = {}): SatchelInte ): TActionCreator => { return satchel.__createActionCreator(actionType, target, true); }, + mutatorAction: ( + actionType: string, + target: (...args: TArgs) => void + ): ((...args: TArgs) => void) => { + const simpleActionCreator = satchel.action(actionType, (...args: TArgs) => { + return { + args, + }; + }); + + const mutatorTarget = (actionMessage: ActionMessageWithArgs): void => { + return target(...actionMessage.args); + }; + + const simpleMutator = mutator(simpleActionCreator, mutatorTarget); + + satchel.register(simpleMutator); + + return simpleActionCreator; + }, getRootStore: (): ObservableMap => { return satchel.__rootStore; }, diff --git a/src/interfaces/Satchel.ts b/src/interfaces/Satchel.ts index c43d455..304ce95 100644 --- a/src/interfaces/Satchel.ts +++ b/src/interfaces/Satchel.ts @@ -6,6 +6,7 @@ import type MutatorFunction from './MutatorFunction'; import type Orchestrator from './Orchestrator'; import type OrchestratorFunction from './OrchestratorFunction'; import SatchelState from './SatchelState'; +import SimpleAction from './SimpleAction'; type Satchel = { /** @@ -50,6 +51,12 @@ type Satchel = { actionType: string, target?: TActionCreator ) => TActionCreator; + + mutatorAction: ( + actionType: string, + target: (...args: TArgs) => void + ) => (...args: TArgs) => void; + /** * Creates a Satchel store and returns a selector to it. * diff --git a/test/applyMiddlewareTests.ts b/test/applyMiddlewareTests.ts index 27e443a..26b9e64 100644 --- a/test/applyMiddlewareTests.ts +++ b/test/applyMiddlewareTests.ts @@ -1,5 +1,5 @@ import 'jasmine'; -import { DispatchFunction, Middleware } from '../src'; +import { DispatchFunction } from '../src'; import { createDispatchWithMiddleware } from '../src/createSatchel'; import { createTestSatchel } from './utils/createTestSatchel'; @@ -50,7 +50,7 @@ describe('applyMiddleware', () => { }, ]; - const finalDispatch = (_actionMessage: any) => { + const finalDispatch = () => { sequence.push('finalDispatch'); }; diff --git a/test/endToEndTests.ts b/test/endToEndTests.ts index 1e9e6af..ee042aa 100644 --- a/test/endToEndTests.ts +++ b/test/endToEndTests.ts @@ -1,6 +1,6 @@ import 'jasmine'; import { autorun } from 'mobx'; -import { mutator, mutatorAction, orchestrator } from '../src/index'; +import { mutator, orchestrator } from '../src/index'; import { createTestSatchel } from './utils/createTestSatchel'; describe('satcheljs', () => { @@ -16,7 +16,7 @@ describe('satcheljs', () => { }); // Create a mutator that subscribes to it - const testMutator = mutator(testAction, function(actionMessage: any) { + const testMutator = mutator(testAction, function (actionMessage: any) { actualValue = actionMessage.value; }); @@ -36,8 +36,7 @@ describe('satcheljs', () => { let arg1Value; let arg2Value; - let testMutatorAction = mutatorAction( - satchel, + let testMutatorAction = satchel.mutatorAction( 'testMutatorAction', function testMutatorAction(arg1: string, arg2: number) { arg1Value = arg1; From bf1bea6efe6205461537051b6490c8bf04d4a0a2 Mon Sep 17 00:00:00 2001 From: jareill Date: Mon, 29 Jul 2024 15:47:40 -0700 Subject: [PATCH 12/12] Mutator action updates --- src/createSatchel.ts | 24 +++++++----- src/index.ts | 1 - src/interfaces/MutatorActionTarget.ts | 6 +++ src/interfaces/Satchel.ts | 21 +++++++--- src/interfaces/SimpleAction.ts | 4 -- src/simpleSubscribers.ts | 38 ------------------- ...scribersTests.ts => mutatorActionTests.ts} | 13 +++---- 7 files changed, 42 insertions(+), 65 deletions(-) create mode 100644 src/interfaces/MutatorActionTarget.ts delete mode 100644 src/interfaces/SimpleAction.ts delete mode 100644 src/simpleSubscribers.ts rename test/{simpleSubscribersTests.ts => mutatorActionTests.ts} (76%) diff --git a/src/createSatchel.ts b/src/createSatchel.ts index f7d08a6..bde27a2 100644 --- a/src/createSatchel.ts +++ b/src/createSatchel.ts @@ -17,6 +17,7 @@ import type Subscriber from './interfaces/Subscriber'; import type SatchelOptions from './interfaces/SatchelOptions'; import type SatchelInternal from './interfaces/SatchelInternal'; import type Satchel from './interfaces/Satchel'; +import type MutatorActionTarget from './interfaces/MutatorActionTarget'; import mutator from './mutator'; type ActionMessageWithArgs = ActionMessage & { args: T }; @@ -93,17 +94,22 @@ export function createSatchelInternal(options: SatchelOptions = {}): SatchelInte ): TActionCreator => { return satchel.__createActionCreator(actionType, target, true); }, - mutatorAction: ( + mutatorAction: >( actionType: string, - target: (...args: TArgs) => void - ): ((...args: TArgs) => void) => { - const simpleActionCreator = satchel.action(actionType, (...args: TArgs) => { - return { - args, - }; - }); + target: MutatorActionTarget + ) => { + const simpleActionCreator = satchel.action( + actionType, + (...args: Parameters) => { + return { + args, + }; + } + ); - const mutatorTarget = (actionMessage: ActionMessageWithArgs): void => { + const mutatorTarget = ( + actionMessage: ActionMessageWithArgs> + ): void => { return target(...actionMessage.args); }; diff --git a/src/index.ts b/src/index.ts index 78641ee..287c7ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,6 @@ export { default as Mutator } from './interfaces/Mutator'; export { default as Orchestrator } from './interfaces/Orchestrator'; export { default as mutator } from './mutator'; import { default as orchestrator } from './orchestrator'; -export { mutatorAction } from './simpleSubscribers'; export { createSatchel } from './createSatchel'; export { useStrict }; diff --git a/src/interfaces/MutatorActionTarget.ts b/src/interfaces/MutatorActionTarget.ts new file mode 100644 index 0000000..ee430bd --- /dev/null +++ b/src/interfaces/MutatorActionTarget.ts @@ -0,0 +1,6 @@ +import ActionCreator from './ActionCreator'; + +type MutatorActionTarget = ActionCreator> = + void extends ReturnType ? F : never; + +export default MutatorActionTarget; diff --git a/src/interfaces/Satchel.ts b/src/interfaces/Satchel.ts index 304ce95..9c8a5c2 100644 --- a/src/interfaces/Satchel.ts +++ b/src/interfaces/Satchel.ts @@ -5,8 +5,8 @@ import type Mutator from './Mutator'; import type MutatorFunction from './MutatorFunction'; import type Orchestrator from './Orchestrator'; import type OrchestratorFunction from './OrchestratorFunction'; -import SatchelState from './SatchelState'; -import SimpleAction from './SimpleAction'; +import type SatchelState from './SatchelState'; +import type MutatorActionTarget from './MutatorActionTarget'; type Satchel = { /** @@ -52,10 +52,21 @@ type Satchel = { target?: TActionCreator ) => TActionCreator; - mutatorAction: ( + /** + * Decorates a function as a mutator action. + * + * - mutatorAction encapsulates action creation, dispatch, and registering the mutator in one simple function call. + * - Use mutatorAction as a convenience when an action only needs to trigger one specific mutator. + * - Because the action creator is not exposed, no other mutators or orchestrators can subscribe to it. If an action needs multiple handlers then it must use the full pattern with action creators and handlers implemented separately. + * + * @param actionType {string}:A string which identifies the type of the action. + * @param target {(...args: TArgs) => void} A function to register as a mutator. + * @returns {(...args: TArgs) => void} The mutator function. + */ + mutatorAction: >( actionType: string, - target: (...args: TArgs) => void - ) => (...args: TArgs) => void; + target: MutatorActionTarget + ) => (...args: Parameters) => void; /** * Creates a Satchel store and returns a selector to it. diff --git a/src/interfaces/SimpleAction.ts b/src/interfaces/SimpleAction.ts deleted file mode 100644 index 29b98f0..0000000 --- a/src/interfaces/SimpleAction.ts +++ /dev/null @@ -1,4 +0,0 @@ -type SimpleAction any> = void extends ReturnType - ? TFunction - : never; -export default SimpleAction; diff --git a/src/simpleSubscribers.ts b/src/simpleSubscribers.ts deleted file mode 100644 index 42a8300..0000000 --- a/src/simpleSubscribers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type SimpleAction from './interfaces/SimpleAction'; -import type Satchel from './interfaces/Satchel'; -import mutator from './mutator'; - -type MutatorDecorator = typeof mutator; - -export function createSimpleSubscriber(decorator: MutatorDecorator) { - return function simpleSubscriber any>( - satchelInstance: Satchel, - actionType: string, - target: TFunction - ): SimpleAction { - // Create the action creator - let simpleActionCreator = satchelInstance.action( - actionType, - function simpleActionCreator() { - return { - args: arguments, - }; - } - ); - - // Create the subscriber - const subscriber = decorator( - simpleActionCreator, - function simpleSubscriberCallback(actionMessage: any) { - return target.apply(null, actionMessage.args); - } - ); - - satchelInstance.register(subscriber); - - // Return a function that dispatches that action - return simpleActionCreator as any as SimpleAction; - }; -} - -export const mutatorAction = createSimpleSubscriber(mutator); diff --git a/test/simpleSubscribersTests.ts b/test/mutatorActionTests.ts similarity index 76% rename from test/simpleSubscribersTests.ts rename to test/mutatorActionTests.ts index cc07884..a789dff 100644 --- a/test/simpleSubscribersTests.ts +++ b/test/mutatorActionTests.ts @@ -1,19 +1,16 @@ import 'jasmine'; -import { createSimpleSubscriber } from '../src/simpleSubscribers'; import { createTestSatchel } from './utils/createTestSatchel'; import * as mutator from '../src/mutator'; -describe('simpleSubscribers', () => { +describe('mutatorAction', () => { let actionCreatorSpy: jasmine.Spy; let decoratorSpy: jasmine.Spy; - let simpleSubscriber: ReturnType; let satchel: ReturnType; beforeEach(() => { satchel = createTestSatchel(); actionCreatorSpy = spyOn(satchel, 'action').and.callThrough(); decoratorSpy = spyOn(mutator, 'default').and.callThrough(); - simpleSubscriber = createSimpleSubscriber(decoratorSpy); }); it('creates and returns a bound action creator', () => { @@ -21,7 +18,7 @@ describe('simpleSubscribers', () => { let actionId = 'testSubscriber'; // Act - let returnValue = simpleSubscriber(satchel, actionId, () => {}); + let returnValue = satchel.mutatorAction(actionId, () => {}); // Assert expect(actionCreatorSpy).toHaveBeenCalled(); @@ -31,7 +28,7 @@ describe('simpleSubscribers', () => { it('includes arguments in the action message', () => { // Act - let returnValue: Function = simpleSubscriber(satchel, 'testSubscriber', () => {}); + let returnValue: Function = satchel.mutatorAction('testSubscriber', () => {}); let createdAction = returnValue(1, 2, 3); // Assert @@ -40,7 +37,7 @@ describe('simpleSubscribers', () => { it('subscribes a callback to the action', () => { // Act - simpleSubscriber(satchel, 'testSubscriber', () => {}); + satchel.mutatorAction('testSubscriber', () => {}); // Assert expect(decoratorSpy).toHaveBeenCalled(); @@ -53,7 +50,7 @@ describe('simpleSubscribers', () => { let actionMessage = { args: [1, 2, 3] }; // Act - simpleSubscriber(satchel, 'testSubscriber', callback); + satchel.mutatorAction('testSubscriber', callback); let decoratorCallback = decoratorSpy.calls.argsFor(0)[1]; decoratorCallback(actionMessage);