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 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/package.json b/package.json index f8eff08..7434715 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "satcheljs", - "version": "4.3.1", + "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/actionCreator.ts b/src/actionCreator.ts deleted file mode 100644 index 22fc128..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; -} - -function setPrivateActionId(target: any, actionId: string) { - target.__SATCHELJS_ACTION_ID = actionId; -} - -export function getPrivateActionType(target: any): string { - return target.__SATCHELJS_ACTION_TYPE || 'unknown action'; -} - -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 new file mode 100644 index 0000000..bde27a2 --- /dev/null +++ b/src/createSatchel.ts @@ -0,0 +1,234 @@ +import { observable, ObservableMap, transaction, action as mobxAction } from 'mobx'; +import { + getPrivateActionId, + getPrivateActionType, + setActionType, + setPrivateActionId, + getPrivateSubscriberRegistered, + setPrivateSubscriberRegistered, +} from './privatePropertyUtils'; +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'; +import type MutatorActionTarget from './interfaces/MutatorActionTarget'; +import mutator from './mutator'; + +type ActionMessageWithArgs = ActionMessage & { args: T }; + +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 = { + // 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 actionId = getPrivateActionId(subscriber.actionCreator); + if (!actionId) { + throw new Error(`A ${subscriber.type} can only subscribe to action creators.`); + } + + const wrappedTarget = + subscriber.type == 'mutator' + ? satchel.__wrapMutatorTarget(subscriber) + : subscriber.target; + + if (!satchel.__subscriptions[actionId]) { + satchel.__subscriptions[actionId] = []; + } + + satchel.__subscriptions[actionId].push(wrappedTarget); + // Mark the subscriber as registered + setPrivateSubscriberRegistered(subscriber, true); + + return subscriber.target; + }, + dispatch: (actionMessage: ActionMessage): void => { + if (satchel.__currentMutator) { + throw new Error( + `Mutator (${satchel.__currentMutator}) may not dispatch action (${actionMessage.type})` + ); + } + + 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); + }, + mutatorAction: >( + actionType: string, + target: MutatorActionTarget + ) => { + const simpleActionCreator = satchel.action( + actionType, + (...args: Parameters) => { + 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; + }, + hasSubscribers: (actionCreator: ActionCreator) => { + return !!satchel.__subscriptions[getPrivateActionId(actionCreator)]; + }, + createStore: (key: string, initialState: T): (() => T) => { + 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(); + }, + __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); + } + } + + 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.`); + } + + 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.'); + } + + // Stamp the action message with the type and the private ID + actionMessage.type = actionType; + setPrivateActionId(actionMessage, actionId); + + // Dispatch if necessary + if (shouldDispatch) { + satchel.dispatch(actionMessage); + } + + return actionMessage; + } as TActionCreator; + + // 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, + }; + + satchel.__dispatchWithMiddleware = createDispatchWithMiddleware( + middleware, + satchel.__finalDispatch + ); + + return satchel; +} + +// Exclude the private functions from the public API +export const createSatchel: (options?: SatchelOptions) => Satchel = createSatchelInternal; 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 92d71f7..0000000 --- a/src/dispatcher.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { transaction } from 'mobx'; -import ActionMessage from './interfaces/ActionMessage'; -import Subscriber from './interfaces/Subscriber'; -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 06fa566..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/Subscriber'; -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..287c7ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,16 +5,14 @@ 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 { action, actionCreator } from './actionCreator'; -export { default as applyMiddleware } from './applyMiddleware'; -export { default as createStore } from './createStore'; -export { dispatch } from './dispatcher'; +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 { default as getRootStore } from './getRootStore'; -export { mutatorAction } from './simpleSubscribers'; +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 new file mode 100644 index 0000000..565b4c1 --- /dev/null +++ b/src/interfaces/Mutator.ts @@ -0,0 +1,10 @@ +import ActionCreator from './ActionCreator'; +import type ActionMessage from './ActionMessage'; +import type MutatorFunction from './MutatorFunction'; + +type Mutator = { + type: 'mutator'; + actionCreator: ActionCreator; + target: MutatorFunction; +}; +export default Mutator; 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/Orchestrator.ts b/src/interfaces/Orchestrator.ts new file mode 100644 index 0000000..bd3a70b --- /dev/null +++ b/src/interfaces/Orchestrator.ts @@ -0,0 +1,10 @@ +import ActionCreator from './ActionCreator'; +import type ActionMessage from './ActionMessage'; +import type OrchestratorFunction from './OrchestratorFunction'; + +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..9c8a5c2 --- /dev/null +++ b/src/interfaces/Satchel.ts @@ -0,0 +1,100 @@ +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'; +import type SatchelState from './SatchelState'; +import type MutatorActionTarget from './MutatorActionTarget'; + +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; + + /** + * 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: MutatorActionTarget + ) => (...args: Parameters) => void; + + /** + * 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; + + /** + * 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; 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/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/interfaces/Subscriber.ts b/src/interfaces/Subscriber.ts index 66cfbc3..23e4b82 100644 --- a/src/interfaces/Subscriber.ts +++ b/src/interfaces/Subscriber.ts @@ -1,8 +1,9 @@ -import ActionMessage from './ActionMessage'; -import MutatorFunction from './MutatorFunction'; -import OrchestratorFunction from './OrchestratorFunction'; +import type ActionMessage from './ActionMessage'; +import type Mutator from './Mutator'; +import type Orchestrator from './Orchestrator'; + +type Subscriber = + | Mutator + | Orchestrator; -type Subscriber = - | MutatorFunction - | OrchestratorFunction; export default Subscriber; diff --git a/src/interfaces/SubscriberFunction.ts b/src/interfaces/SubscriberFunction.ts new file mode 100644 index 0000000..ce5214e --- /dev/null +++ b/src/interfaces/SubscriberFunction.ts @@ -0,0 +1,8 @@ +import type ActionMessage from './ActionMessage'; +import type MutatorFunction from './MutatorFunction'; +import type OrchestratorFunction from './OrchestratorFunction'; + +type SubscriberFunction = + | MutatorFunction + | OrchestratorFunction; +export default SubscriberFunction; 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/mutator.ts b/src/mutator.ts index 8a0851f..e2bf45f 100644 --- a/src/mutator.ts +++ b/src/mutator.ts @@ -1,36 +1,20 @@ -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 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( actionCreator: ActionCreator, target: MutatorFunction -): MutatorFunction { - let actionId = getPrivateActionId(actionCreator); - if (!actionId) { - throw new Error('Mutators can only subscribe to action creators.'); - } +): Mutator { + const mutator: Mutator = { + type: 'mutator', + actionCreator, + target, + }; - // 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; - } - }); + setPrivateSubscriberRegistered(mutator, false); - // Subscribe to the action - subscribe(actionId, wrappedTarget); - - return target; + return mutator; } diff --git a/src/orchestrator.ts b/src/orchestrator.ts index 4e42822..553155e 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -1,18 +1,20 @@ -import ActionCreator from './interfaces/ActionCreator'; -import ActionMessage from './interfaces/ActionMessage'; -import OrchestratorFunction from './interfaces/OrchestratorFunction'; -import { getPrivateActionId } from './actionCreator'; -import { subscribe } from './dispatcher'; +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( actionCreator: ActionCreator, target: OrchestratorFunction -) { - let actionId = getPrivateActionId(actionCreator); - if (!actionId) { - throw new Error('Orchestrators can only subscribe to action creators.'); - } +): Orchestrator { + const orchestrator: Orchestrator = { + type: 'orchestrator', + actionCreator, + target, + }; - subscribe(actionId, target); - return target; + setPrivateSubscriberRegistered(orchestrator, false); + + return orchestrator; } 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 deleted file mode 100644 index 48ccb06..0000000 --- a/src/simpleSubscribers.ts +++ /dev/null @@ -1,27 +0,0 @@ -import SimpleAction from './interfaces/SimpleAction'; -import { action } from './actionCreator'; -import mutator from './mutator'; - -export function createSimpleSubscriber(decorator: Function) { - return function simpleSubscriber any>( - actionType: string, - target: TFunction - ): SimpleAction { - // Create the action creator - let simpleActionCreator = action(actionType, function simpleActionCreator() { - return { - args: arguments, - }; - }); - - // Create the subscriber - decorator(simpleActionCreator, function simpleSubscriberCallback(actionMessage: any) { - return target.apply(null, actionMessage.args); - }); - - // Return a function that dispatches that action - return (simpleActionCreator as any) as SimpleAction; - }; -} - -export const mutatorAction = createSimpleSubscriber(mutator); 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..26b9e64 100644 --- a/test/applyMiddlewareTests.ts +++ b/test/applyMiddlewareTests.ts @@ -1,17 +1,16 @@ import 'jasmine'; -import applyMiddleware from '../src/applyMiddleware'; -import * as dispatcher from '../src/dispatcher'; -import { getGlobalContext, __resetGlobalContext } from '../src/globalContext'; +import { DispatchFunction } from '../src'; +import { createDispatchWithMiddleware } from '../src/createSatchel'; +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,47 +18,46 @@ 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', () => { + it('createDispatchWithMiddleware creates a function that calls middleware and finalDispatch in order', () => { // Arrange - __resetGlobalContext(); let sequence: string[] = []; - - spyOn(dispatcher, 'finalDispatch').and.callFake(() => { - sequence.push('finalDispatch'); - }); - - applyMiddleware( - (next: any, actionMessage: any) => { + const middleware = [ + (next: DispatchFunction, actionMessage: any) => { sequence.push('middleware1'); next(actionMessage); }, - (next: any, actionMessage: any) => { + (next: DispatchFunction, actionMessage: any) => { sequence.push('middleware2'); next(actionMessage); - } - ); + }, + ]; + + const finalDispatch = () => { + sequence.push('finalDispatch'); + }; + + const testDispatchWithMiddleware = createDispatchWithMiddleware(middleware, finalDispatch); // Act - getGlobalContext().dispatchWithMiddleware({}); + testDispatchWithMiddleware({}); // 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..77c412d 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'; - -describe('dispatcher', () => { - let mockGlobalContext: any; - - beforeEach(() => { - mockGlobalContext = { - subscriptions: {}, - dispatchWithMiddleware: jasmine.createSpy('dispatchWithMiddleware'), - currentMutator: null, - }; - - spyOn(globalContext, 'getGlobalContext').and.returnValue(mockGlobalContext); - }); +import { createTestSatchel } from './utils/createTestSatchel'; +import * as privateUtils from '../src/privatePropertyUtils'; +describe('dispatch', () => { + /* 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,45 @@ describe('dispatcher', () => { // Assert expect(mockGlobalContext.subscriptions[actionId]).toEqual([callback0, callback1]); }); + */ it('dispatch calls dispatchWithMiddleware', () => { // Arrange let actionMessage = {}; + const satchel = createTestSatchel(); + spyOn(satchel, '__dispatchWithMiddleware'); // 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 +79,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 +107,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..ee042aa 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, 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,17 @@ 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 = satchel.mutatorAction( + 'testMutatorAction', + function testMutatorAction(arg1: string, arg2: number) { + arg1Value = arg1; + arg2Value = arg2; + } + ); // Act testMutatorAction('testValue', 2); @@ -61,13 +54,15 @@ 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, () => { + let testMutator = mutator(modifyStore, () => { store.testProperty = 'newValue'; }); + satchel.register(testMutator); // Act modifyStore(); @@ -78,13 +73,15 @@ 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, () => { + let testOrchestator = orchestrator(modifyStore, () => { store.testProperty = 'newValue'; }); + satchel.register(testOrchestator); // Act / Assert expect(() => { @@ -94,16 +91,19 @@ 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, () => { + const testMutator1 = mutator(modifyStore, () => { store.testProperty++; }); - mutator(modifyStore, () => { + const testMutator2 = mutator(modifyStore, () => { store.testProperty++; }); + satchel.register(testMutator1); + satchel.register(testMutator2); let values: number[] = []; autorun(() => { @@ -122,13 +122,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 +140,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/simpleSubscribersTests.ts b/test/mutatorActionTests.ts similarity index 63% rename from test/simpleSubscribersTests.ts rename to test/mutatorActionTests.ts index b6793a1..a789dff 100644 --- a/test/simpleSubscribersTests.ts +++ b/test/mutatorActionTests.ts @@ -1,18 +1,16 @@ import 'jasmine'; -import { createSimpleSubscriber, mutatorAction } from '../src/simpleSubscribers'; -import { __resetGlobalContext } from '../src/globalContext'; -import * as actionCreator from '../src/actionCreator'; +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: Function; + let satchel: ReturnType; beforeEach(() => { - __resetGlobalContext(); - actionCreatorSpy = spyOn(actionCreator, 'action').and.callThrough(); - decoratorSpy = jasmine.createSpy('decoratorSpy'); - simpleSubscriber = createSimpleSubscriber(decoratorSpy); + satchel = createTestSatchel(); + actionCreatorSpy = spyOn(satchel, 'action').and.callThrough(); + decoratorSpy = spyOn(mutator, 'default').and.callThrough(); }); it('creates and returns a bound action creator', () => { @@ -20,7 +18,7 @@ describe('simpleSubscribers', () => { let actionId = 'testSubscriber'; // Act - let returnValue = simpleSubscriber(actionId, () => {}); + let returnValue = satchel.mutatorAction(actionId, () => {}); // Assert expect(actionCreatorSpy).toHaveBeenCalled(); @@ -30,7 +28,7 @@ describe('simpleSubscribers', () => { it('includes arguments in the action message', () => { // Act - let returnValue: Function = simpleSubscriber('testSubscriber', () => {}); + let returnValue: Function = satchel.mutatorAction('testSubscriber', () => {}); let createdAction = returnValue(1, 2, 3); // Assert @@ -39,7 +37,7 @@ describe('simpleSubscribers', () => { it('subscribes a callback to the action', () => { // Act - simpleSubscriber('testSubscriber', () => {}); + satchel.mutatorAction('testSubscriber', () => {}); // Assert expect(decoratorSpy).toHaveBeenCalled(); @@ -52,7 +50,7 @@ describe('simpleSubscribers', () => { let actionMessage = { args: [1, 2, 3] }; // Act - simpleSubscriber('testSubscriber', callback); + satchel.mutatorAction('testSubscriber', callback); let decoratorCallback = decoratorSpy.calls.argsFor(0)[1]; decoratorCallback(actionMessage); diff --git a/test/mutatorTests.ts b/test/mutatorTests.ts index c811b6e..6f424f9 100644 --- a/test/mutatorTests.ts +++ b/test/mutatorTests.ts @@ -1,50 +1,61 @@ 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'; +import { getPrivateSubscriberRegistered } from '../src/privatePropertyUtils'; describe('mutator', () => { - let mockGlobalContext: any; + it('returns a object describing the mutator', () => { + // Arrange + const callback = () => {}; + const actionId = 'testAction'; + const actionCreator: any = { __SATCHELJS_ACTION_ID: actionId }; - beforeEach(() => { - mockGlobalContext = { currentMutator: null }; - spyOn(globalContext, 'getGlobalContext').and.returnValue(mockGlobalContext); - spyOn(dispatcher, 'subscribe'); - }); + // 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(); }); 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 +63,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 +76,26 @@ 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(); + satchel.register(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 +103,16 @@ 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]; try { - subscribedCallback(); + satchel.register(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..d9316b0 100644 --- a/test/orchestratorTests.ts +++ b/test/orchestratorTests.ts @@ -1,39 +1,61 @@ import 'jasmine'; import orchestrator from '../src/orchestrator'; -import * as dispatcher from '../src/dispatcher'; +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(); }); 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); + expect(getPrivateSubscriberRegistered(testOrchestator)).toBe(true); }); 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/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/utils/createTestSatchel.ts b/test/utils/createTestSatchel.ts new file mode 100644 index 0000000..da5e88a --- /dev/null +++ b/test/utils/createTestSatchel.ts @@ -0,0 +1,4 @@ +import { createSatchelInternal } from '../../src/createSatchel'; + +// Decorator for the internal createSatchel function +export const createTestSatchel = createSatchelInternal; 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"