diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..c22a301 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,32 @@ +// @format + +// https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb + +const _ = require('lodash'); + +const ts_nope = [ + 'no-explicit-any', + 'explicit-function-return-type', + 'no-object-literal-type-assertion', + 'camelcase', + 'member-delimiter-style', + 'prefer-interface', + 'indent', +].map(r => '@typescript-eslint/' + r); + +module.exports = { + parser: '@typescript-eslint/parser', // Specifies the ESLint parser + extends: [ + 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin + ], + plugins: ['import'], + parserOptions: { + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: 'module', // Allows for the use of imports + }, + rules: { + ..._.fromPairs(ts_nope.map(r => [r, 'off'])), + // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs + // e.g. "@typescript-eslint/explicit-function-return-type": "off", + }, +}; diff --git a/.gitignore b/.gitignore index 0855c7c..bf384e5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ tsconfig.tsbuildinfo dist package-lock.json yarn.lock +.nyc_output/ +pnpm-debug.log +pnpm-lock.yaml +yarn-error.log +GPUCache/ diff --git a/Promake b/Promake new file mode 100755 index 0000000..0cf1176 --- /dev/null +++ b/Promake @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +const Promake = require('promake'); +const glob = require('glob').sync; + +const { + task, cli, rule, exec +} = new Promake(); + + + +const compile = task( 'compile', rule( 'tsconfig.tsbuildinfo', glob('src/**.ts'), + async () => { + return exec( 'tsc' ); + }) ); + +const docs = task( 'docs', [ rule( + glob('docs/4-API/*.md'), + [ ...glob('./src/**.ts'), compile ], async() => { + await exec( "typedoc src" ); + } +)]); + +const sidebar = task( + 'sidebar', + rule('./docs/_sidebar.md', [...glob('./docs/4-API/**.md'), docs], async () => { + return exec('docsify-auto-sidebar -d docs'); + }) +); + + +cli(); diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 0000000..24197d3 --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,369 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "./dist/index.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [ ], + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true, + + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/etc/" + */ + "reportFolder": "/temp/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + "reportTempFolder": "/temp/" + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true, + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true, + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + // "untrimmedFilePath": "/dist/.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning", + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + }, + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning", + // "addToApiReportFile": false + }, + + // "ae-extra-release-tag": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning", + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } + +} diff --git a/docs/API/README.md b/docs/API/README.md new file mode 100644 index 0000000..9222c85 --- /dev/null +++ b/docs/API/README.md @@ -0,0 +1,228 @@ +[updux - v1.2.0](README.md) › [Globals](globals.md) + +# updux - v1.2.0 + +# What's Updux? + +So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered +[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux. + +It has a couple of pretty good ideas that removes some of the +boilerplate. Keeping mutations and asynchronous effects close to the +reducer definition? Nice. Automatically infering the +actions from the said mutations and effects? Genius! + +But it also enforces a flat hierarchy of reducers -- where +is the fun in that? And I'm also having a strong love for +[Updeep](https://github.com/substantial/updeep), so I want reducer state updates to leverage the heck out of it. + +All that to say, say hello to `Updux`. Heavily inspired by `rematch`, but twisted +to work with `updeep` and to fit my peculiar needs. It offers features such as + +* Mimic the way VueX has mutations (reducer reactions to specific actions) and + effects (middleware reacting to actions that can be asynchronous and/or + have side-effects), so everything pertaining to a store are all defined + in the space place. +* Automatically gather all actions used by the updux's effects and mutations, + and makes then accessible as attributes to the `dispatch` object of the + store. +* Mutations have a signature that is friendly to Updux and Immer. +* Also, the mutation signature auto-unwrap the payload of the actions for you. +* TypeScript types. + +Fair warning: this package is still very new, probably very buggy, +definitively very badly documented, and very subject to changes. Caveat +Maxima Emptor. + +# Synopsis + +``` +import updux from 'updux'; + +import otherUpdux from './otherUpdux'; + +const { + initial, + reducer, + actions, + middleware, + createStore, +} = new Updux({ + initial: { + counter: 0, + }, + subduxes: { + otherUpdux, + }, + mutations: { + inc: ( increment = 1 ) => u({counter: s => s + increment }) + }, + effects: { + '*' => api => next => action => { + console.log( "hey, look, an action zoomed by!", action ); + next(action); + }; + }, + actions: { + customAction: ( someArg ) => ({ + type: "custom", + payload: { someProp: someArg } + }), + }, + +}); + +const store = createStore(); + +store.dispatch.inc(3); +``` + +# Description + +Full documentation can be [found here](https://yanick.github.io/updux/docs/). + +## Exporting upduxes + +If you are creating upduxes that will be used as subduxes +by other upduxes, or as +[ducks](https://github.com/erikras/ducks-modular-redux)-like containers, I +recommend that you export the Updux instance as the default export: + +``` +import Updux from 'updux'; + +const updux = new Updux({ ... }); + +export default updux; +``` + +Then you can use them as subduxes like this: + +``` +import Updux from 'updux'; +import foo from './foo'; // foo is an Updux +import bar from './bar'; // bar is an Updux as well + +const updux = new Updux({ + subduxes: { + foo, bar + } +}); +``` + +Or if you want to use it: + +``` +import updux from './myUpdux'; + +const { + reducer, + actions: { doTheThing }, + createStore, + middleware, +} = updux; +``` + +## Mapping a mutation to all values of a state + +Say you have a `todos` state that is an array of `todo` sub-states. It's easy +enough to have the main reducer maps away all items to the sub-reducer: + +``` +const todo = new Updux({ + mutations: { + review: () => u({ reviewed: true}), + done: () => u({done: true}), + }, +}); + +const todos = new Updux({ initial: [] }); + +todos.addMutation( + todo.actions.review, + (_,action) => state => state.map( todo.upreducer(action) ) +); +todos.addMutation( + todo.actions.done, + (id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))), +); + +``` + +But `updeep` can iterate through all the items of an array (or the values of +an object) via the special key `*`. So the todos updux above could also be +written: + +``` +const todo = new Updux({ + mutations: { + review: () => u({ reviewed: true}), + done: () => u({done: true}), + }, +}); + +const todos = new Updux({ + subduxes: { '*': todo }, +}); + +todos.addMutation( + todo.actions.done, + (id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))), + true +); +``` + +The advantages being that the actions/mutations/effects of the subdux will be +imported by the root updux as usual, and all actions that aren't being +overridden by a sink mutation will trickle down automatically. + +## Usage with Immer + +While Updux was created with Updeep in mind, it also plays very +well with [Immer](https://immerjs.github.io/immer/docs/introduction). + +For example, taking this basic updux: + +``` +import Updux from 'updux'; + +const updux = new Updux({ + initial: { counter: 0 }, + mutations: { + add: (inc=1) => state => { counter: counter + inc } + } +}); + +``` + +Converting it to Immer would look like: + +``` +import Updux from 'updux'; +import { produce } from 'Immer'; + +const updux = new Updux({ + initial: { counter: 0 }, + mutations: { + add: (inc=1) => produce( draft => draft.counter += inc ) } + } +}); + +``` + +But since typing `produce` over and over is no fun, `groomMutations` +can be used to wrap all mutations with it: + +``` +import Updux from 'updux'; +import { produce } from 'Immer'; + +const updux = new Updux({ + initial: { counter: 0 }, + groomMutations: mutation => (...args) => produce( mutation(...args) ), + mutations: { + add: (inc=1) => draft => draft.counter += inc + } +}); + +``` diff --git a/docs/API/classes/updux.md b/docs/API/classes/updux.md new file mode 100644 index 0000000..5a7a2a1 --- /dev/null +++ b/docs/API/classes/updux.md @@ -0,0 +1,491 @@ +[updux - v1.2.0](../README.md) › [Globals](../globals.md) › [Updux](updux.md) + +# Class: Updux <**S, A, X, C**> + +## Type parameters + +▪ **S** + +▪ **A** + +▪ **X** + +▪ **C**: *[UpduxConfig](../globals.md#upduxconfig)* + +## Hierarchy + +* **Updux** + +## Index + +### Constructors + +* [constructor](updux.md#constructor) + +### Properties + +* [coduxes](updux.md#coduxes) +* [groomMutations](updux.md#groommutations) +* [subduxes](updux.md#subduxes) + +### Accessors + +* [_middlewareEntries](updux.md#_middlewareentries) +* [actions](updux.md#actions) +* [asDux](updux.md#asdux) +* [createStore](updux.md#createstore) +* [initial](updux.md#initial) +* [middleware](updux.md#middleware) +* [mutations](updux.md#mutations) +* [reducer](updux.md#reducer) +* [selectors](updux.md#selectors) +* [subduxUpreducer](updux.md#subduxupreducer) +* [upreducer](updux.md#upreducer) + +### Methods + +* [addAction](updux.md#addaction) +* [addEffect](updux.md#addeffect) +* [addMutation](updux.md#addmutation) +* [addSelector](updux.md#addselector) + +## Constructors + +### constructor + +\+ **new Updux**(`config`: C): *[Updux](updux.md)* + +**Parameters:** + +Name | Type | Default | Description | +------ | ------ | ------ | ------ | +`config` | C | {} as C | an [UpduxConfig](../globals.md#upduxconfig) plain object | + +**Returns:** *[Updux](updux.md)* + +## Properties + +### coduxes + +• **coduxes**: *[Dux](../globals.md#dux)[]* + +___ + +### groomMutations + +• **groomMutations**: *function* + +#### Type declaration: + +▸ (`mutation`: [Mutation](../globals.md#mutation)‹S›): *[Mutation](../globals.md#mutation)‹S›* + +**Parameters:** + +Name | Type | +------ | ------ | +`mutation` | [Mutation](../globals.md#mutation)‹S› | + +___ + +### subduxes + +• **subduxes**: *[Dictionary](../globals.md#dictionary)‹[Dux](../globals.md#dux)›* + +## Accessors + +### _middlewareEntries + +• **get _middlewareEntries**(): *any[]* + +**Returns:** *any[]* + +___ + +### actions + +• **get actions**(): *[DuxActions](../globals.md#duxactions)‹A, C›* + +Action creators for all actions defined or used in the actions, mutations, effects and subduxes +of the updux config. + +Non-custom action creators defined in `actions` have the signature `(payload={},meta={}) => ({type, +payload,meta})` (with the extra sugar that if `meta` or `payload` are not +specified, that key won't be present in the produced action). + +The same action creator can be included +in multiple subduxes. However, if two different creators +are included for the same action, an error will be thrown. + +**`example`** + +``` +const actions = updux.actions; +``` + +**Returns:** *[DuxActions](../globals.md#duxactions)‹A, C›* + +___ + +### asDux + +• **get asDux**(): *object* + +Returns a ducks-like +plain object holding the reducer from the Updux object and all +its trimmings. + +**`example`** + +``` +const { + createStore, + upreducer, + subduxes, + coduxes, + middleware, + actions, + reducer, + mutations, + initial, + selectors, +} = myUpdux.asDux; +``` + +**Returns:** *object* + +* **actions**: = this.actions + +* **coduxes**: *object[]* = this.coduxes + +* **createStore**(): *function* + + * (`initial?`: S, `injectEnhancer?`: Function): *Store‹S› & object* + +* **initial**: = this.initial + +* **middleware**(): *function* + + * (`api`: UpduxMiddlewareAPI‹S, X›): *function* + + * (`next`: Function): *function* + + * (`action`: A): *any* + +* **mutations**(): *object* + +* **reducer**(): *function* + + * (`state`: S | undefined, `action`: [Action](../globals.md#action)): *S* + +* **selectors**: = this.selectors + +* **subduxes**(): *object* + +* **upreducer**(): *function* + + * (`action`: [Action](../globals.md#action)): *function* + + * (`state`: S): *S* + +___ + +### createStore + +• **get createStore**(): *function* + +Returns a `createStore` function that takes two argument: +`initial` and `injectEnhancer`. `initial` is a custom +initial state for the store, and `injectEnhancer` is a function +taking in the middleware built by the updux object and allowing +you to wrap it in any enhancer you want. + +**`example`** + +``` +const createStore = updux.createStore; + +const store = createStore(initial); +``` + +**Returns:** *function* + +▸ (`initial?`: S, `injectEnhancer?`: Function): *Store‹S› & object* + +**Parameters:** + +Name | Type | +------ | ------ | +`initial?` | S | +`injectEnhancer?` | Function | + +___ + +### initial + +• **get initial**(): *AggDuxState‹S, C›* + +**Returns:** *AggDuxState‹S, C›* + +___ + +### middleware + +• **get middleware**(): *[UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C››* + +Array of middlewares aggregating all the effects defined in the +updux and its subduxes. Effects of the updux itself are +done before the subduxes effects. +Note that `getState` will always return the state of the +local updux. + +**`example`** + +``` +const middleware = updux.middleware; +``` + +**Returns:** *[UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C››* + +___ + +### mutations + +• **get mutations**(): *[Dictionary](../globals.md#dictionary)‹[Mutation](../globals.md#mutation)‹S››* + +Merge of the updux and subduxes mutations. If an action triggers +mutations in both the main updux and its subduxes, the subduxes +mutations will be performed first. + +**Returns:** *[Dictionary](../globals.md#dictionary)‹[Mutation](../globals.md#mutation)‹S››* + +___ + +### reducer + +• **get reducer**(): *function* + +A Redux reducer generated using the computed initial state and +mutations. + +**Returns:** *function* + +▸ (`state`: S | undefined, `action`: [Action](../globals.md#action)): *S* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | undefined | +`action` | [Action](../globals.md#action) | + +___ + +### selectors + +• **get selectors**(): *[DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›* + +A dictionary of the updux's selectors. Subduxes' +selectors are included as well (with the mapping to the +sub-state already taken care of you). + +**Returns:** *[DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›* + +___ + +### subduxUpreducer + +• **get subduxUpreducer**(): *function* + +Returns the upreducer made of the merge of all sudbuxes reducers, without +the local mutations. Useful, for example, for sink mutations. + +**`example`** + +``` +import todo from './todo'; // updux for a single todo +import Updux from 'updux'; +import u from 'updeep'; + +const todos = new Updux({ initial: [], subduxes: { '*': todo } }); +todos.addMutation( + todo.actions.done, + ({todo_id},action) => u.map( u.if( u.is('id',todo_id) ), todos.subduxUpreducer(action) ) + true +); +``` + +**Returns:** *function* + +▸ (`action`: [Action](../globals.md#action)): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`action` | [Action](../globals.md#action) | + +▸ (`state`: S): *S* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | + +___ + +### upreducer + +• **get upreducer**(): *[Upreducer](../globals.md#upreducer)‹S›* + +**Returns:** *[Upreducer](../globals.md#upreducer)‹S›* + +## Methods + +### addAction + +▸ **addAction**(`theaction`: string, `transform?`: any): *ActionCreator‹string, any›* + +Adds an action to the updux. It can take an already defined action +creator, or any arguments that can be passed to `actionCreator`. + +**`example`** +``` + const action = updux.addAction( name, ...creatorArgs ); + const action = updux.addAction( otherActionCreator ); +``` + +**`example`** +``` +import {actionCreator, Updux} from 'updux'; + +const updux = new Updux(); + +const foo = updux.addAction('foo'); +const bar = updux.addAction( 'bar', (x) => ({stuff: x+1}) ); + +const baz = actionCreator( 'baz' ); + +foo({ a: 1}); // => { type: 'foo', payload: { a: 1 } } +bar(2); // => { type: 'bar', payload: { stuff: 3 } } +baz(); // => { type: 'baz', payload: undefined } +``` + +**Parameters:** + +Name | Type | +------ | ------ | +`theaction` | string | +`transform?` | any | + +**Returns:** *ActionCreator‹string, any›* + +▸ **addAction**(`theaction`: string | ActionCreator‹any›, `transform?`: undefined): *ActionCreator‹string, any›* + +**Parameters:** + +Name | Type | +------ | ------ | +`theaction` | string | ActionCreator‹any› | +`transform?` | undefined | + +**Returns:** *ActionCreator‹string, any›* + +___ + +### addEffect + +▸ **addEffect**<**AC**>(`creator`: AC, `middleware`: [UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›, ReturnType‹AC››, `isGenerator?`: undefined | false | true): *any* + +**Type parameters:** + +▪ **AC**: *ActionCreator* + +**Parameters:** + +Name | Type | +------ | ------ | +`creator` | AC | +`middleware` | [UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›, ReturnType‹AC›› | +`isGenerator?` | undefined | false | true | + +**Returns:** *any* + +▸ **addEffect**(`creator`: string, `middleware`: [UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C››, `isGenerator?`: undefined | false | true): *any* + +**Parameters:** + +Name | Type | +------ | ------ | +`creator` | string | +`middleware` | [UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›› | +`isGenerator?` | undefined | false | true | + +**Returns:** *any* + +___ + +### addMutation + +▸ **addMutation**<**A**>(`creator`: A, `mutation`: [Mutation](../globals.md#mutation)‹S, ActionType‹A››, `isSink?`: undefined | false | true): *any* + +Adds a mutation and its associated action to the updux. + +**`remarks`** + +If a local mutation was already associated to the action, +it will be replaced by the new one. + +**`example`** + +```js +updux.addMutation( + action('ADD', payload() ), + inc => state => state + in +); +``` + +**Type parameters:** + +▪ **A**: *ActionCreator* + +**Parameters:** + +Name | Type | Description | +------ | ------ | ------ | +`creator` | A | - | +`mutation` | [Mutation](../globals.md#mutation)‹S, ActionType‹A›› | - | +`isSink?` | undefined | false | true | If `true`, disables the subduxes mutations for this action. To conditionally run the subduxes mutations, check out [subduxUpreducer](updux.md#subduxupreducer). Defaults to `false`. | + +**Returns:** *any* + +▸ **addMutation**<**A**>(`creator`: string, `mutation`: [Mutation](../globals.md#mutation)‹S, any›, `isSink?`: undefined | false | true): *any* + +**Type parameters:** + +▪ **A**: *ActionCreator* + +**Parameters:** + +Name | Type | +------ | ------ | +`creator` | string | +`mutation` | [Mutation](../globals.md#mutation)‹S, any› | +`isSink?` | undefined | false | true | + +**Returns:** *any* + +___ + +### addSelector + +▸ **addSelector**(`name`: string, `selector`: [Selector](../globals.md#selector)): *void* + +**Parameters:** + +Name | Type | +------ | ------ | +`name` | string | +`selector` | [Selector](../globals.md#selector) | + +**Returns:** *void* diff --git a/docs/API/globals.md b/docs/API/globals.md new file mode 100644 index 0000000..cd61ed5 --- /dev/null +++ b/docs/API/globals.md @@ -0,0 +1,980 @@ +[updux - v1.2.0](README.md) › [Globals](globals.md) + +# updux - v1.2.0 + +## Index + +### Classes + +* [Updux](classes/updux.md) + +### Type aliases + +* [Action](globals.md#action) +* [ActionPair](globals.md#actionpair) +* [ActionPayloadGenerator](globals.md#actionpayloadgenerator) +* [ActionsOf](globals.md#actionsof) +* [CoduxesOf](globals.md#coduxesof) +* [Dictionary](globals.md#dictionary) +* [Dux](globals.md#dux) +* [DuxActions](globals.md#duxactions) +* [DuxActionsCoduxes](globals.md#duxactionscoduxes) +* [DuxActionsSubduxes](globals.md#duxactionssubduxes) +* [DuxSelectors](globals.md#duxselectors) +* [DuxState](globals.md#duxstate) +* [DuxStateCoduxes](globals.md#duxstatecoduxes) +* [DuxStateGlobSub](globals.md#duxstateglobsub) +* [DuxStateSubduxes](globals.md#duxstatesubduxes) +* [Effect](globals.md#effect) +* [GenericActions](globals.md#genericactions) +* [ItemsOf](globals.md#itemsof) +* [LocalDuxState](globals.md#localduxstate) +* [MaybePayload](globals.md#maybepayload) +* [MaybeReturnType](globals.md#maybereturntype) +* [Merge](globals.md#merge) +* [Mutation](globals.md#mutation) +* [MutationEntry](globals.md#mutationentry) +* [MwGen](globals.md#mwgen) +* [Next](globals.md#next) +* [RebaseSelector](globals.md#rebaseselector) +* [Selector](globals.md#selector) +* [SelectorsOf](globals.md#selectorsof) +* [StateOf](globals.md#stateof) +* [StoreWithDispatchActions](globals.md#storewithdispatchactions) +* [SubMutations](globals.md#submutations) +* [Submws](globals.md#submws) +* [UnionToIntersection](globals.md#uniontointersection) +* [UpduxActions](globals.md#upduxactions) +* [UpduxConfig](globals.md#upduxconfig) +* [UpduxLocalActions](globals.md#upduxlocalactions) +* [UpduxMiddleware](globals.md#upduxmiddleware) +* [Upreducer](globals.md#upreducer) + +### Variables + +* [subEffects](globals.md#const-subeffects) +* [updux](globals.md#const-updux) + +### Functions + +* [MiddlewareFor](globals.md#const-middlewarefor) +* [buildActions](globals.md#buildactions) +* [buildCreateStore](globals.md#buildcreatestore) +* [buildInitial](globals.md#buildinitial) +* [buildMiddleware](globals.md#buildmiddleware) +* [buildMutations](globals.md#buildmutations) +* [buildSelectors](globals.md#buildselectors) +* [buildUpreducer](globals.md#buildupreducer) +* [coduxes](globals.md#const-coduxes) +* [composeMutations](globals.md#const-composemutations) +* [composeMw](globals.md#const-composemw) +* [dux](globals.md#const-dux) +* [effectToMw](globals.md#const-effecttomw) +* [sliceMw](globals.md#slicemw) +* [subMiddleware](globals.md#const-submiddleware) +* [subSelectors](globals.md#subselectors) + +## Type aliases + +### Action + +Ƭ **Action**: *object & [MaybePayload](globals.md#maybepayload)‹P›* + +___ + +### ActionPair + +Ƭ **ActionPair**: *[string, ActionCreator]* + +___ + +### ActionPayloadGenerator + +Ƭ **ActionPayloadGenerator**: *function* + +#### Type declaration: + +▸ (...`args`: any[]): *any* + +**Parameters:** + +Name | Type | +------ | ------ | +`...args` | any[] | + +___ + +### ActionsOf + +Ƭ **ActionsOf**: *U extends Updux ? U["actions"] : object* + +___ + +### CoduxesOf + +Ƭ **CoduxesOf**: *U extends Updux ? S : []* + +___ + +### Dictionary + +Ƭ **Dictionary**: *object* + +#### Type declaration: + +* \[ **key**: *string*\]: T + +___ + +### Dux + +Ƭ **Dux**: *object* + +#### Type declaration: + +* **actions**: *A* + +* **coduxes**: *[Dux](globals.md#dux)[]* + +* **initial**: *AggDuxState‹S, C›* + +* **subduxes**: *[Dictionary](globals.md#dictionary)‹[Dux](globals.md#dux)›* + +___ + +### DuxActions + +Ƭ **DuxActions**: + +___ + +### DuxActionsCoduxes + +Ƭ **DuxActionsCoduxes**: *C extends Array ? UnionToIntersection> : object* + +___ + +### DuxActionsSubduxes + +Ƭ **DuxActionsSubduxes**: *C extends object ? ActionsOf : unknown* + +___ + +### DuxSelectors + +Ƭ **DuxSelectors**: *unknown extends X ? object : X* + +___ + +### DuxState + +Ƭ **DuxState**: *D extends object ? S : unknown* + +___ + +### DuxStateCoduxes + +Ƭ **DuxStateCoduxes**: *C extends Array ? UnionToIntersection> : unknown* + +___ + +### DuxStateGlobSub + +Ƭ **DuxStateGlobSub**: *S extends object ? StateOf : unknown* + +___ + +### DuxStateSubduxes + +Ƭ **DuxStateSubduxes**: *C extends object ? object : C extends object ? object : unknown* + +___ + +### Effect + +Ƭ **Effect**: *[string, [UpduxMiddleware](globals.md#upduxmiddleware) | [MwGen](globals.md#mwgen), undefined | false | true]* + +___ + +### GenericActions + +Ƭ **GenericActions**: *[Dictionary](globals.md#dictionary)‹ActionCreator‹string, function››* + +___ + +### ItemsOf + +Ƭ **ItemsOf**: *C extends object ? C[keyof C] : unknown* + +___ + +### LocalDuxState + +Ƭ **LocalDuxState**: *S extends never[] ? unknown[] : S* + +___ + +### MaybePayload + +Ƭ **MaybePayload**: *P extends object | string | boolean | number ? object : object* + +___ + +### MaybeReturnType + +Ƭ **MaybeReturnType**: *X extends function ? ReturnType : unknown* + +___ + +### Merge + +Ƭ **Merge**: *[UnionToIntersection](globals.md#uniontointersection)‹T[keyof T]›* + +___ + +### Mutation + +Ƭ **Mutation**: *function* + +#### Type declaration: + +▸ (`payload`: A["payload"], `action`: A): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`payload` | A["payload"] | +`action` | A | + +▸ (`state`: S): *S* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | + +___ + +### MutationEntry + +Ƭ **MutationEntry**: *[ActionCreator | string, [Mutation](globals.md#mutation)‹any, [Action](globals.md#action)‹string, any››, undefined | false | true]* + +___ + +### MwGen + +Ƭ **MwGen**: *function* + +#### Type declaration: + +▸ (): *[UpduxMiddleware](globals.md#upduxmiddleware)* + +___ + +### Next + +Ƭ **Next**: *function* + +#### Type declaration: + +▸ (`action`: [Action](globals.md#action)): *any* + +**Parameters:** + +Name | Type | +------ | ------ | +`action` | [Action](globals.md#action) | + +___ + +### RebaseSelector + +Ƭ **RebaseSelector**: *object* + +#### Type declaration: + +___ + +### Selector + +Ƭ **Selector**: *function* + +#### Type declaration: + +▸ (`state`: S): *unknown* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | + +___ + +### SelectorsOf + +Ƭ **SelectorsOf**: *C extends object ? S : unknown* + +___ + +### StateOf + +Ƭ **StateOf**: *D extends object ? I : unknown* + +___ + +### StoreWithDispatchActions + +Ƭ **StoreWithDispatchActions**: *Store‹S› & object* + +___ + +### SubMutations + +Ƭ **SubMutations**: *object* + +#### Type declaration: + +* \[ **slice**: *string*\]: [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation)› + +___ + +### Submws + +Ƭ **Submws**: *[Dictionary](globals.md#dictionary)‹[UpduxMiddleware](globals.md#upduxmiddleware)›* + +___ + +### UnionToIntersection + +Ƭ **UnionToIntersection**: *U extends any ? function : never extends function ? I : never* + +___ + +### UpduxActions + +Ƭ **UpduxActions**: *U extends Updux ? UnionToIntersection | ActionsOf[keyof CoduxesOf]>> : object* + +___ + +### UpduxConfig + +Ƭ **UpduxConfig**: *Partial‹object›* + +Configuration object given to Updux's constructor. + +#### arguments + +##### initial + +Default initial state of the reducer. If applicable, is merged with +the subduxes initial states, with the parent having precedence. + +If not provided, defaults to an empty object. + +##### actions + +[Actions](/concepts/Actions) used by the updux. + +```js +import { dux } from 'updux'; +import { action, payload } from 'ts-action'; + +const bar = action('BAR', payload()); +const foo = action('FOO'); + +const myDux = dux({ + actions: { + bar + }, + mutations: [ + [ foo, () => state => state ] + ] +}); + +myDux.actions.foo({ x: 1, y: 2 }); // => { type: foo, x:1, y:2 } +myDux.actions.bar(2); // => { type: bar, payload: 2 } +``` + +New actions used directly in mutations and effects will be added to the +dux actions -- that is, they will be accessible via `dux.actions` -- but will +not appear as part of its Typescript type. + +##### selectors + +Dictionary of selectors for the current updux. The updux also +inherit its subduxes' selectors. + +The selectors are available via the class' getter. + +##### mutations + + mutations: [ + [ action, mutation, isSink ], + ... + ] + +or + + mutations: { + action: mutation, + ... + } + +List of mutations for assign to the dux. If you want Typescript goodness, you +probably want to use `addMutation()` instead. + +In its generic array-of-array form, +each mutation tuple contains: the action, the mutation, +and boolean indicating if this is a sink mutation. + +The action can be an action creator function or a string. If it's a string, it's considered to be the +action type and a generic `action( actionName, payload() )` creator will be +generated for it. If an action is not already defined in the `actions` +parameter, it'll be automatically added. + +The pseudo-action type `*` can be used to match any action not explicitly matched by other mutations. + +```js +const todosUpdux = updux({ + mutations: { + add: todo => state => [ ...state, todo ], + done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ), + '*' (payload,action) => state => { + console.warn( "unexpected action ", action.type ); + return state; + }, + } +}); +``` + +The signature of the mutations is `(payload,action) => state => newState`. +It is designed to play well with `Updeep` (and [Immer](https://immerjs.github.io/immer/docs/introduction)). This way, instead of doing + +```js + mutation: { + renameTodo: newName => state => { ...state, name: newName } + } +``` + +we can do + +```js + mutation: { + renameTodo: newName => u({ name: newName }) + } +``` + +The final argument is the optional boolean `isSink`. If it is true, it'll +prevent subduxes' mutations on the same action. It defaults to `false`. + +The object version of the argument can be used as a shortcut when all actions +are strings. In that case, `isSink` is `false` for all mutations. + +##### groomMutations + +Function that can be provided to alter all local mutations of the updux +(the mutations of subduxes are left untouched). + +Can be used, for example, for Immer integration: + +```js +import Updux from 'updux'; +import { produce } from 'Immer'; + +const updux = new Updux({ + initial: { counter: 0 }, + groomMutations: mutation => (...args) => produce( mutation(...args) ), + mutations: { + add: (inc=1) => draft => draft.counter += inc + } +}); +``` + +Or perhaps for debugging: + +```js +import Updux from 'updux'; + +const updux = new Updux({ + initial: { counter: 0 }, + groomMutations: mutation => (...args) => state => { + console.log( "got action ", args[1] ); + return mutation(...args)(state); + } +}); +``` +##### subduxes + +Object mapping slices of the state to sub-upduxes. In addition to creating +sub-reducers for those slices, it'll make the parend updux inherit all the +actions and middleware from its subduxes. + +For example, if in plain Redux you would do + +```js +import { combineReducers } from 'redux'; +import todosReducer from './todos'; +import statisticsReducer from './statistics'; + +const rootReducer = combineReducers({ + todos: todosReducer, + stats: statisticsReducer, +}); +``` + +then with Updux you'd do + +```js +import { updux } from 'updux'; +import todos from './todos'; +import statistics from './statistics'; + +const rootUpdux = updux({ + subduxes: { + todos, + statistics + } +}); +``` + +##### effects + +Array of arrays or plain object defining asynchronous actions and side-effects triggered by actions. +The effects themselves are Redux middleware, with the `dispatch` +property of the first argument augmented with all the available actions. + +``` +updux({ + effects: { + fetch: ({dispatch}) => next => async (action) => { + next(action); + + let result = await fetch(action.payload.url).then( result => result.json() ); + dispatch.fetchSuccess(result); + } + } +}); +``` + +**`example`** + +``` +import Updux from 'updux'; +import { actions, payload } from 'ts-action'; +import u from 'updeep'; + +const todoUpdux = new Updux({ + initial: { + done: false, + note: "", + }, + actions: { + finish: action('FINISH', payload()), + edit: action('EDIT', payload()), + }, + mutations: [ + [ edit, note => u({note}) ] + ], + selectors: { + getNote: state => state.note + }, + groomMutations: mutation => transform(mutation), + subduxes: { + foo + }, + effects: { + finish: () => next => action => { + console.log( "Woo! one more bites the dust" ); + } + } +}) +``` + +___ + +### UpduxLocalActions + +Ƭ **UpduxLocalActions**: *S extends Updux ? object : S extends Updux ? A : object* + +___ + +### UpduxMiddleware + +Ƭ **UpduxMiddleware**: *function* + +#### Type declaration: + +▸ (`api`: UpduxMiddlewareAPI‹S, X›): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`api` | UpduxMiddlewareAPI‹S, X› | + +▸ (`next`: Function): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`next` | Function | + +▸ (`action`: A): *any* + +**Parameters:** + +Name | Type | +------ | ------ | +`action` | A | + +___ + +### Upreducer + +Ƭ **Upreducer**: *function* + +#### Type declaration: + +▸ (`action`: [Action](globals.md#action)): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`action` | [Action](globals.md#action) | + +▸ (`state`: S): *S* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | + +## Variables + +### `Const` subEffects + +• **subEffects**: *[Effect](globals.md#effect)* = [ '*', subMiddleware ] as any + +___ + +### `Const` updux + +• **updux**: *[Updux](classes/updux.md)‹unknown, null, unknown, object›* = new Updux({ + subduxes: { + foo: dux({ initial: "banana" }) + } +}) + +## Functions + +### `Const` MiddlewareFor + +▸ **MiddlewareFor**(`type`: any, `mw`: Middleware): *Middleware* + +**Parameters:** + +Name | Type | +------ | ------ | +`type` | any | +`mw` | Middleware | + +**Returns:** *Middleware* + +___ + +### buildActions + +▸ **buildActions**(`actions`: [ActionPair](globals.md#actionpair)[]): *[Dictionary](globals.md#dictionary)‹ActionCreator‹string, function››* + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`actions` | [ActionPair](globals.md#actionpair)[] | [] | + +**Returns:** *[Dictionary](globals.md#dictionary)‹ActionCreator‹string, function››* + +___ + +### buildCreateStore + +▸ **buildCreateStore**<**S**, **A**>(`reducer`: Reducer‹S›, `middleware`: Middleware, `actions`: A): *function* + +**Type parameters:** + +▪ **S** + +▪ **A** + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`reducer` | Reducer‹S› | - | +`middleware` | Middleware | - | +`actions` | A | {} as A | + +**Returns:** *function* + +▸ (`initial?`: S, `injectEnhancer?`: Function): *Store‹S› & object* + +**Parameters:** + +Name | Type | +------ | ------ | +`initial?` | S | +`injectEnhancer?` | Function | + +___ + +### buildInitial + +▸ **buildInitial**(`initial`: any, `coduxes`: any, `subduxes`: any): *any* + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`initial` | any | - | +`coduxes` | any | [] | +`subduxes` | any | {} | + +**Returns:** *any* + +___ + +### buildMiddleware + +▸ **buildMiddleware**<**S**>(`local`: [UpduxMiddleware](globals.md#upduxmiddleware)[], `co`: [UpduxMiddleware](globals.md#upduxmiddleware)[], `sub`: [Submws](globals.md#submws)): *[UpduxMiddleware](globals.md#upduxmiddleware)‹S›* + +**Type parameters:** + +▪ **S** + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`local` | [UpduxMiddleware](globals.md#upduxmiddleware)[] | [] | +`co` | [UpduxMiddleware](globals.md#upduxmiddleware)[] | [] | +`sub` | [Submws](globals.md#submws) | {} | + +**Returns:** *[UpduxMiddleware](globals.md#upduxmiddleware)‹S›* + +___ + +### buildMutations + +▸ **buildMutations**(`mutations`: [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation) | [[Mutation](globals.md#mutation), boolean | undefined]›, `subduxes`: object, `coduxes`: [Upreducer](globals.md#upreducer)[]): *object* + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`mutations` | [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation) | [[Mutation](globals.md#mutation), boolean | undefined]› | {} | +`subduxes` | object | {} | +`coduxes` | [Upreducer](globals.md#upreducer)[] | [] | + +**Returns:** *object* + +___ + +### buildSelectors + +▸ **buildSelectors**(`localSelectors`: [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›, `coduxes`: [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›[], `subduxes`: [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›): *object* + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`localSelectors` | [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)› | {} | +`coduxes` | [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›[] | [] | +`subduxes` | [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)› | {} | + +**Returns:** *object* + +___ + +### buildUpreducer + +▸ **buildUpreducer**<**S**>(`initial`: S, `mutations`: [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation)‹S››): *[Upreducer](globals.md#upreducer)‹S›* + +**Type parameters:** + +▪ **S** + +**Parameters:** + +Name | Type | +------ | ------ | +`initial` | S | +`mutations` | [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation)‹S›› | + +**Returns:** *[Upreducer](globals.md#upreducer)‹S›* + +___ + +### `Const` coduxes + +▸ **coduxes**<**C**, **U**>(...`coduxes`: U): *object* + +**Type parameters:** + +▪ **C**: *[Dux](globals.md#dux)* + +▪ **U**: *[C]* + +**Parameters:** + +Name | Type | +------ | ------ | +`...coduxes` | U | + +**Returns:** *object* + +* **coduxes**: *U* + +___ + +### `Const` composeMutations + +▸ **composeMutations**(`mutations`: [Mutation](globals.md#mutation)[]): *function | (Anonymous function)* + +**Parameters:** + +Name | Type | +------ | ------ | +`mutations` | [Mutation](globals.md#mutation)[] | + +**Returns:** *function | (Anonymous function)* + +___ + +### `Const` composeMw + +▸ **composeMw**(`mws`: [UpduxMiddleware](globals.md#upduxmiddleware)[]): *(Anonymous function)* + +**Parameters:** + +Name | Type | +------ | ------ | +`mws` | [UpduxMiddleware](globals.md#upduxmiddleware)[] | + +**Returns:** *(Anonymous function)* + +___ + +### `Const` dux + +▸ **dux**<**S**, **A**, **X**, **C**>(`config`: C): *object* + +**Type parameters:** + +▪ **S** + +▪ **A** + +▪ **X** + +▪ **C**: *[UpduxConfig](globals.md#upduxconfig)* + +**Parameters:** + +Name | Type | +------ | ------ | +`config` | C | + +**Returns:** *object* + +* **actions**: = this.actions + +* **coduxes**: *object[]* = this.coduxes + +* **createStore**(): *function* + + * (`initial?`: S, `injectEnhancer?`: Function): *Store‹S› & object* + +* **initial**: = this.initial + +* **middleware**(): *function* + + * (`api`: UpduxMiddlewareAPI‹S, X›): *function* + + * (`next`: Function): *function* + + * (`action`: A): *any* + +* **mutations**(): *object* + +* **reducer**(): *function* + + * (`state`: S | undefined, `action`: [Action](globals.md#action)): *S* + +* **selectors**: = this.selectors + +* **subduxes**(): *object* + +* **upreducer**(): *function* + + * (`action`: [Action](globals.md#action)): *function* + + * (`state`: S): *S* + +___ + +### `Const` effectToMw + +▸ **effectToMw**(`effect`: [Effect](globals.md#effect), `actions`: [Dictionary](globals.md#dictionary)‹ActionCreator›, `selectors`: [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›): *subMiddleware | augmented* + +**Parameters:** + +Name | Type | +------ | ------ | +`effect` | [Effect](globals.md#effect) | +`actions` | [Dictionary](globals.md#dictionary)‹ActionCreator› | +`selectors` | [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)› | + +**Returns:** *subMiddleware | augmented* + +___ + +### sliceMw + +▸ **sliceMw**(`slice`: string, `mw`: [UpduxMiddleware](globals.md#upduxmiddleware)): *[UpduxMiddleware](globals.md#upduxmiddleware)* + +**Parameters:** + +Name | Type | +------ | ------ | +`slice` | string | +`mw` | [UpduxMiddleware](globals.md#upduxmiddleware) | + +**Returns:** *[UpduxMiddleware](globals.md#upduxmiddleware)* + +___ + +### `Const` subMiddleware + +▸ **subMiddleware**(): *(Anonymous function)* + +**Returns:** *(Anonymous function)* + +___ + +### subSelectors + +▸ **subSelectors**(`__namedParameters`: [string, Function]): *[string, [Selector](globals.md#selector)][]* + +**Parameters:** + +Name | Type | +------ | ------ | +`__namedParameters` | [string, Function] | + +**Returns:** *[string, [Selector](globals.md#selector)][]* diff --git a/docs/README.md b/docs/README.md index bc1f7a3..b845fc6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,11 +1,12 @@ # What's Updux? -So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered -[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux. +So, I'm a fan of [Redux](https://redux.js.org). -It has a couple of pretty good ideas that removes some of the -boilerplate. Keeping mutations and asynchronous effects close to the +As I was looking into tools to help cut on its boilerplate, +I came across [rematch](https://rematch.github.io/rematch). +It has a few pretty darn good ideas. +Keeping mutations and asynchronous effects close to the reducer definition? Nice. Automatically infering the actions from the said mutations and effects? Genius! @@ -13,7 +14,7 @@ But it also enforces a flat hierarchy of reducers -- where is the fun in that? And I'm also having a strong love for [Updeep](https://github.com/substantial/updeep), so I want reducer state updates to leverage the heck out of it. -All that to say, say hello to `Updux`. Heavily inspired by `rematch`, but twisted +Hence: `Updux`. Heavily inspired by `rematch`, but twisted to work with `updeep` and to fit my peculiar needs. It offers features such as * Mimic the way VueX has mutations (reducer reactions to specific actions) and @@ -24,55 +25,49 @@ to work with `updeep` and to fit my peculiar needs. It offers features such as and makes then accessible as attributes to the `dispatch` object of the store. * Mutations have a signature that is friendly to Updux and Immer. -* Also, the mutation signature auto-unwrap the payload of the actions for you. +* Mutations auto-unwrapping the payload of actions for you. * TypeScript types. +* Leverage [ts-action](https://www.npmjs.com/package/ts-action) for action + creation. - -Fair warning: this package is still very new, probably very buggy, -definitively very badly documented, and very subject to changes. Caveat -Maxima Emptor. +**Fair warning**: this package is still very new, likely to go through +big changes before I find the perfect balance between ease of use and sanity. +Caveat Emptor. # Synopsis ``` -import updux from 'updux'; +import Updux from 'updux'; +import { action, payload } from 'ts-action'; -import otherUpdux from './otherUpdux'; +import otherDux from './otherUpdux'; -const { - initial, - reducer, - actions, - middleware, - createStore, -} = new Updux({ +const inc = action( 'INC', payload() ); + +const updux = new Updux({ initial: { counter: 0, }, - subduxes: { - otherUpdux, - }, - mutations: { - inc: ( increment = 1 ) => u({counter: s => s + increment }) - }, - effects: { - '*' => api => next => action => { - console.log( "hey, look, an action zoomed by!", action ); - next(action); - }; - }, actions: { - customAction: ( someArg ) => ({ - type: "custom", - payload: { someProp: someArg } - }), + inc }, - + subduxes: { + otherDux, + } }); -const store = createStore(); +updux.addMutation( inc, increment => u({counter: s => s + increment })); -store.dispatch.inc(3); +updux.addEffect( '*', api => next => action => { + console.log( "hey, look, an action zoomed by!", action ); + next(action); +} ); + +const myDux = updux.asDux; + +const store = myDux.createStore(); + +store.dispatch( myDux.actions.inc(3) ); ``` # Description @@ -85,23 +80,22 @@ types can be found over [here](https://yanick.github.io/updux/docs/classes/updux If you are creating upduxes that will be used as subduxes by other upduxes, or as [ducks](https://github.com/erikras/ducks-modular-redux)-like containers, I -recommend that you export the Updux instance as the default export: +recommend that you export the "compiled" (as in, no more editable and with all its properties resolved) output of the Updux instance via its `asDux()` getter: ``` import Updux from 'updux'; const updux = new Updux({ ... }); -export default updux; +export default updux.asDux; ``` - Then you can use them as subduxes like this: ``` import Updux from 'updux'; -import foo from './foo'; // foo is an Updux -import bar from './bar'; // bar is an Updux as well +import foo from './foo'; // foo is a dux +import bar from './bar'; // bar is a dux as well const updux = new Updux({ subduxes: { @@ -109,125 +103,3 @@ const updux = new Updux({ } }); ``` - -Or if you want to use it: - -``` -import updux from './myUpdux'; - -const { - reducer, - actions: { doTheThing }, - createStore, - middleware, -} = updux; -``` - -## Mapping a mutation to all values of a state - -Say you have a `todos` state that is an array of `todo` sub-states. It's easy -enough to have the main reducer maps away all items to the sub-reducer: - -``` -const todo = new Updux({ - mutations: { - review: () => u({ reviewed: true}), - done: () => u({done: true}), - }, -}); - -const todos = new Updux({ initial: [] }); - -todos.addMutation( - todo.actions.review, - (_,action) => state => state.map( todo.upreducer(action) ) -); -todos.addMutation( - todo.actions.done, - (id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))), -); - -``` - -But `updeep` can iterate through all the items of an array (or the values of -an object) via the special key `*`. So the todos updux above could also be -written: - -``` -const todo = new Updux({ - mutations: { - review: () => u({ reviewed: true}), - done: () => u({done: true}), - }, -}); - -const todos = new Updux({ - subduxes: { '*': todo }, -}); - -todos.addMutation( - todo.actions.done, - (id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))), - true -); -``` - -The advantages being that the actions/mutations/effects of the subdux will be -imported by the root updux as usual, and all actions that aren't being -overridden by a sink mutation will trickle down automatically. - -## Usage with Immer - -While Updux was created with Updeep in mind, it also plays very -well with [Immer](https://immerjs.github.io/immer/docs/introduction). - -For example, taking this basic updux: - -``` -import Updux from 'updux'; - -const updux = new Updux({ - initial: { counter: 0 }, - mutations: { - add: (inc=1) => state => { counter: counter + inc } - } -}); - -``` - -Converting it to Immer would look like: - - -``` -import Updux from 'updux'; -import { produce } from 'Immer'; - -const updux = new Updux({ - initial: { counter: 0 }, - mutations: { - add: (inc=1) => produce( draft => draft.counter += inc ) } - } -}); - -``` - -But since typing `produce` over and over is no fun, `groomMutations` -can be used to wrap all mutations with it: - - -``` -import Updux from 'updux'; -import { produce } from 'Immer'; - -const updux = new Updux({ - initial: { counter: 0 }, - groomMutations: mutation => (...args) => produce( mutation(...args) ), - mutations: { - add: (inc=1) => draft => draft.counter += inc - } -}); - -``` - - - diff --git a/docs/_sidebar.md b/docs/_sidebar.md index dfdd5c5..d741133 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,6 +1,7 @@ - - -* [Home](/) -* [Concepts](concepts.md) -* API Reference - * [Updux](updux.md) + - [README](/README.md) + - [Tutorial](/tutorial.md) + - [Concepts](/concepts.md) + - [Recipes](/recipes.md) + - API + - [Top-level exports](/exports.md) + - [main index](/API/globals.md) diff --git a/docs/concepts.md b/docs/concepts.md index 75607f5..c33dcea 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -3,7 +3,7 @@ ## actions Updux internally uses the package `ts-action` to create action creator -functions. Even if you don't use typescript, I recommend that you use it, +functions. Even if you don't use Typescript, I recommend that you use it, as it does what it does very well. But if you don't want to, no big deal. Updux will recognize a function as an action creator if it has a `type` property. So a homegrown creator could be as simple as: @@ -14,10 +14,9 @@ function action(type) { } ``` - ## effects -Updux effects are a superset of redux middleware. I kept that format, and the +Updux effects are redux middlewares. I kept that format, and the use of `next` mostly because I wanted to give myself a way to alter actions before they hit the reducer, something that `redux-saga` and `rematch` don't allow. @@ -25,21 +24,5 @@ actions before they hit the reducer, something that `redux-saga` and An effect has the signature ```js -const effect = ({ getState, dispatch, getRootState, selectors}) - => next => action => { ... } -``` - -The first argument is like the usual redux middlewareApi, except -for the availability of selectors and of the root updux's state. - -Also, the function `dispatch` is augmented to be able to be called -with the allowed actions as props. For example, assuming that the action -`complete_todo` has been declared somewhere, then it's possible to do: - -```js -updux.addEffect( 'todo_bankrupcy', - ({ getState, dispatch }) => next => action => { - getState.forEach( todo => dispatch.complete_todo( todo.id ) ); - } -) +const effect = ({ getState, dispatch }) => next => action => { ... } ``` diff --git a/docs/exports.md b/docs/exports.md new file mode 100644 index 0000000..7d3aab3 --- /dev/null +++ b/docs/exports.md @@ -0,0 +1,12 @@ +# Top-level exports + +## Default export + +* [Updux](/API/classes/updux) + +## Exports + +* [Updux](/API/classes/updux) +* [dux](/API/globals?id=const-dux) +* [coduxes](/API/globals?id=const-coduxes) +* [subEffects](/API/globals?id=const-subeffects) diff --git a/docs/index.html b/docs/index.html index 05cfb55..dcfdc00 100644 --- a/docs/index.html +++ b/docs/index.html @@ -16,6 +16,7 @@ repo: 'https://github.com/yanick/updux', loadSidebar: true, subMaxLevel: 4, + relativePath: true, } diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 0000000..20aec25 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,107 @@ +# Recipes + +## Mapping a mutation to all values of a state + +Say you have a `todos` state that is an array of `todo` sub-states. It's easy +enough to have the main reducer maps away all items to the sub-reducer: + +``` +const todo = new Updux({ + actions: { + review: action('REVIEW'), + done: action('DONE',payload()), + }, + mutations: { + review: () => u({reviewed: true}), + done: () => u({done: true}), + }, +}); + +const todos = new Updux({ initial: [] }); + +todos.addMutation( + todo.actions.review, + (_,action) => state => state.map( todo.upreducer(action) ) +); +todos.addMutation( + todo.actions.done, + (id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))), +); + +``` + +But `updeep` can iterate through all the items of an array (or the values of +an object) via the special key `*`. So the todos updux above can be +rewritten as: + +``` +const todos = new Updux({ + subduxes: { '*': todo }, +}); + +todos.addMutation( + todo.actions.done, + (id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))), + true +); +``` + +The advantages being that the actions/mutations/effects of the subdux will be +imported by the root updux as usual, and all actions not +overridden by a sink mutation will trickle down automatically. + +## Usage with Immer + +While Updux was created with Updeep in mind, it also plays very +well with [Immer](https://immerjs.github.io/immer/docs/introduction). + +For example, taking this basic updux: + +``` +import Updux from 'updux'; + +const updux = new Updux({ + initial: { counter: 0 }, + mutations: { + add: (inc=1) => state => { counter: counter + inc } + } +}); + +``` + +Converting it to Immer would look like: + + +``` +import Updux from 'updux'; +import { produce } from 'Immer'; + +const updux = new Updux({ + initial: { counter: 0 }, + mutations: { + add: (inc=1) => produce( draft => draft.counter += inc ) } + } +}); + +``` + +But since typing `produce` over and over is no fun, `groomMutations` +can be used to wrap all mutations with it: + + +``` +import Updux from 'updux'; +import { produce } from 'Immer'; + +const updux = new Updux({ + initial: { counter: 0 }, + groomMutations: mutation => (...args) => produce( mutation(...args) ), + mutations: { + add: (inc=1) => draft => draft.counter += inc + } +}); + +``` + + + diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..75801f6 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,596 @@ +# Tutorial + +This tutorial walks you through the features of `Updux` using the +time-honored example of the implementation of Todo list store. + +This tutorial assumes that our project is written in TypeScript, and +that we are using [updeep](https://www.npmjs.com/package/updeep) to +help with immutability and deep merging and [ts-action][] to manage our +actions. This is the recommended setup, but +none of those two architecture +decisions are mandatory; Updux is equally usable in a pure-JavaScript setting, +and `updeep` can easily be substitued by, say, [immer][], [lodash][], or even +just plain JavaScript. Eventually, I plan to write a version of this tutorial +with all those different configurations. + +Also, the code used here is also available in the project repository, in the +`src/tutorial` directory. + +## Definition of the state + +First thing first: let's define the type of our store: + +``` +type Todo = { + id: number; + description: string; + done: boolean; +}; + +type TodoStore = { + next_id: number; + todos: Todo[]; +}; +``` + +With that, let's create our very first Updux: + +``` +import Updux from 'updux'; + +const todosUpdux = new Updux({ + initial: { + next_id: 1, + todos: [], + } as TodoStore +}); +``` + +Note that we explicitly cast the initial state as `as TodoStore`. This lets +Updux know what is the store's state. + +This being said, congrats! You have written your first Updux object. It +doesn't do a lot, but you can already create a store out of it, and its +initial state will be automatically set: + +``` +const store = todosUpdux.createStore(); + +console.log(store.getState()); +// { next_id: 1, todos: [] } +``` + +## Add actions + +This is all good, but a little static. Let's add actions! + +``` +import { action, payload } from 'ts-action'; + +const add_todo = action('add_todo', payload() ); +const todo_done = action('todo_done', payload() ); +``` + +Now, there is a lot of ways to add actions to a Updux object. + +It can be defined when the object is created: + +``` +const todosUpdux = new Updux({ + actions: { + add_todo, + todo_done, + } +}); +``` + +It can be done via the method `addAction`: + +``` +todosUpdux.addAction(add_todo); +``` + +Or it can be directly used in the definition of a mutation or effect, and will +be automatically added to the Updux. + +``` +todosUpdux.addMutation( add_todo, todoMutation ); +``` + +For TypeScript projects I recommend declaring the actions as part of the +configuration passed to the constructors, as it makes them accessible to the class +at compile-time, and allow Updux to auto-add them to its aggregated `actions` type. + + +``` +const todosUpdux = new Updux({ + actions: { + add_todo, + } +}); + +todosUpdux.addAction(todo_done); + +// only `add_todo` is visible to the type +type MyActions = typeof todosUpdux.actions; +// { add_todo: Function } + +// but both actions are accessible at runtime +const myAction = ( todosUpdux.actions as any).todo_done(1); +``` + +### Accessing actions + +Once an action is defined, its creator is accessible via the `actions` accessor. + +``` +console.log( todosUpdux.actions.add_todo('write tutorial') ); +// { type: 'add_todo', payload: 'write tutorial' } +``` + +### What is an action? + +In this tutorial we use `ts-action` for all the work, but under the hood Updux defines actions via +their creators, which are expected to be: + +1. Functions, +2. returning a plain object of the format `{ type: string; payload?: unknown }`. +3. with an additional property `type`, which is also the action type. + +For example, this is a perfectly cromulent action: + +``` +const add_todo = description => ({ type: 'add_todo', payload: description}); +add_todo.type = 'add_todo'; +``` + +## Mutations + +Actions that don't do anything are not fun. The transformations typically +done by a Redux's reducer are called 'mutations' in Updux. A mutation is a +function with the following signature: + +``` +( payload, action ) => state => { + // ... stuff done here + return new_state; +} +``` + +The inversion and chaining of parameters from the usual Redux reducer's +signature is there to work with `updeep`'s curried nature. The expansion of +the usual `action` into `(payload, action)` is present because in most cases +`payload` is what we're interested in. So why not make it easily available? + +### Adding a mutation + +As for the actions, a mutation can be defined as part of the Updux +init arguments: + +``` +const add_todo_mutation = description => ({next_id: id, todos}) => { + return { + next_id: 1 + id, + todos: [...todos, { description, id, done: false }] + } + +}; + +const todosUpdux = new Updux({ + actions: { add_todo }, + mutations: [ + [ add_todo, add_todo_mutation ] + ] +}); +``` + +or via the method `addMutation`: + +``` +todos.addMutation( add_todo, description => ({next_id: id, todos}) => { + return { + next_id: 1 + id, + todos: [...todos, { description, id, done: false }] + } +}); +``` + +This time around, if the project is using TypeScript then the addition of +mutations via `addMutation` is encouraged, as the method signature +has visibility of the types of the action and state. + +### Leftover mutation + +A mutation with the special action `*` will match any action that haven't been +explicitly dealt with with any other defined mutation. + +``` +todosUpdux.addMutation( '*', (payload,action) => state => { + console.log("hey, action has no mutation! ", action.type); +}); +``` + + +## Effects + +In addition of mutations, Updux also provide action-specific middleware, here +called effects. + +Effects use the usual Redux middleware signature: + +``` +import u from 'updeep'; + +// we want to decouple the increment of next_id and the creation of +// a new todo. So let's use a new version of the action 'add_todo'. + +const add_todo_with_id = action('add_todo_with_id', payload<{description: string; id?: number}>() ); +const inc_next_id = action('inc_next_id'); + +const populate_next_id = ({ getState, dispatch }) => next => action => { + const { next_id: id } = getState(); + + dispatch(inc_next_id()); + next(action); + dispatch( add_todo_with_id({ description: action.payload, id }) ); +} +``` + +And just like mutations, they can be defined as part of the init +configuration, or after via the method `addEffect`: + +``` +const todosUpdux = new Updux({ + actions: { add_todo, inc_next_id }, + effects: [ + [ add_todo, populate_next_id ] + ] +}) +``` + +or + +``` +const todosUpdux = new Updux({ + actions: { add_todo, inc_next_id }, +}); + +todosUpdux.addEffect( add_todo, populate_next_id ); +``` + +As for the mutations, for TypeScript projects +the use of `addEffect` is prefered, as the method gives visibility of the +action and state types. + +### Catch-all effect + +It is possible to have an effect match all actions via the special `*` token. + +``` +todosUpdux.addEffect('*', () => next => action => { + console.log( 'seeing action fly by:', action ); + next(action); +}); +``` + +## Selectors + +Selectors can be defined to get data derived from the state. + +### Adding selectors + +From now you should know the drill: selectors can be defined at construction +time or via `addSelector`. + +``` +import fp from 'lodash/fp'; + +const getTodoById = ({todos}) => id => fp.find({id},todos); + +const todosUpdux = new Updux({ + selectors: { + getTodoById + } +}) +``` + +or + +``` +todosUpdux.addSelector('getTodoById', ({todos}) => id => fp.find({id},todos)); +``` + +Here the declaration as part of the constructor configuration is prefered. +Whereas the `addSelector` will provides the state's type as part of its +signature, declaring the selectors via the constructors will make them visible +via the type of the accessors `selectors`. + +### Accessing selectors + +Selectors are available via the accessor `selectors`. + +``` +const store = todosUpdux.createStore(); + +console.log( + todosUpdux.selectors.getTodoById( store.getState() )(1) +); +``` + +## Subduxes + +Now that we have all the building blocks, we can embark on the last, and best, +part of Updux: its recursive nature. + +### Recap: the Todos updux, undivided + +Upduxes can be divided into sub-upduxes that deal with the various parts of +the global state. This is better understood by working out an example, so +let's recap on the Todos Updux we have so far: + +``` +import Updux from 'updux'; +import { action, payload } from 'ts-action'; +import u from 'updeep'; +import fp from 'lodash/fp'; + +type Todo = { + id: number; + description: string; + done: boolean; +}; + +type TodoStore = { + next_id: number; + todos: Todo[]; +}; + +const add_todo = action('add_todo', payload() ); +const add_todo_with_id = action('add_todo_with_id', + payload<{ description: string; id: number }>() ); +const todo_done = action('todo_done', payload() ); +const inc_next_id = action('inc_next_id'); + +const todosUpdux = new Updux({ + initial: { + next_id: 1, + todos: [], + } as TodoStore, + actions: { + add_todo, + add_todo_with_id, + todo_done, + inc_next_id, + }, + selectors: { + getTodoById: ({todos}) => id => fp.find({id},todos) + } +}); + +todosUpdux.addMutation( add_todo_with_id, payload => + u.updateIn( 'todos', todos => [ ...todos, { ...payload, done: false }] ) +); + +todosUpdux.addMutation( inc_next_id, () => u({ next_id: i => i + 1 }) ); + +todosUpdux.addMutation( todo_done, id => u.updateIn( + 'todos', u.map( u.if( fp.matches({id}), todo => u({done: true}, todo) ) ) +) ); + +todosUpdux.addEffect( add_todo, ({ getState, dispatch }) => next => action => { + const { next_id: id } = getState(); + + dispatch(inc_next_id()); + + next(u.updateIn('payload', {id}, action)) +}); + +``` + +This store has two main components: the `next_id`, and the `todos` collection. +The `todos` collection is itself composed of the individual `todo`s. So let's +create upduxes for each of those. + +### Next_id updux + +``` +// dux/next_id.ts + +import Updux from 'updux'; +import { action, payload } from 'ts-action'; +import u from 'updeep'; +import fp from 'lodash/fp'; + +const inc_next_id = action('inc_next_id'); + +const updux = new Updux({ + initial: 1, + actions: { + inc_next_id, + }, + selectors: { + getNextId: state => state + } +}); + +updux.addMutation( inc_next_id, () => fp.add(1) ); + +export default updux.asDux; + +``` + +Notice that here we didn't have to specify what is the type of `initial`; +TypeScript figures by itself that it's a number. + +Also, note that we're exporting the output of the accessor `asDux` instead of +the updux object itself. See the upcoming section 'Exporting upduxes' for the rationale. + +### Todo updux + +``` +// dux/todos/todo/index.ts + +import Updux from 'updux'; +import { action, payload } from 'ts-action'; +import u from 'updeep'; +import fp from 'lodash/fp'; + +type Todo = { + id: number; + description: string; + done: boolean; +}; + +const todo_done = action('todo_done', payload() ); + +const updux = new Updux({ + initial: { + next_id: 0, + description: "", + done: false, + } as Todo, + actions: { + todo_done + } +}); + +updux.addMutation( todo_done, id => u.if( fp.matches({id}), { done: true }) ); + +export default updux.asDux; + +``` + +### Todos updux + +``` +// dux/todos/index.ts + +import Updux, { DuxState } from 'updux'; +import { action, payload } from 'ts-action'; +import u from 'updeep'; +import fp from 'lodash/fp'; + +import todo from './todo'; + +type TodoState = DuxState; + +const add_todo_with_id = action('add_todo_with_id', + payload<{ description: string; id: number }>() +); + +const updux = new Updux({ + initial: [] as Todo[], + subduxes: { + '*': todo.upreducer + }, + actions: { + add_todo_with_id, + }, + selectors: { + getTodoById: state => id => fp.find({id},state) + } +}); + +todosUpdux.addMutation( add_todo_with_id, payload => + todos => [ ...todos, { ...payload, done: false }] +); + +export default updux.asDux; +``` + +Note the special '*' subdux key used here. This +allows the updux to map every item present in its +state to a `todo` updux. See [this recipe](/recipes?id=mapping-a-mutation-to-all-values-of-a-state) for details. +We could also have written the updux as: + +``` +const updux = new Updux({ + initial: [] as Todo[], + actions: { + add_todo_with_id, + }, + selectors: { + getTodoById: state => id => fp.find({id},state) + }, + mutations: { + '*': (payload,action) => state => u.map( todo.reducer(state, action) ) + } +}); +``` + +Note how we are using the `upreducer` accessor in the first case (which yields +a reducer for the dux using the signature `(payload,action) => state => +new_state`) and `reducer` in the second case (which yield an equivalent +reducer using the classic signature `(state,action) => new_state`). + + +### Main store + +``` +// dux/index.ts + +import Updux from 'updux'; + +import todos from './todos'; +import next_id from './next_id'; + +const add_todo = action('add_todo', payload() ); + +const updux = new Updux({ + subduxes: { + next_id, + todos, + }, + actions: { + add_todo + } +}); + +todos.addEffect( add_todo, ({ getState, dispatch }) => next => action => { + const id = updux.selectors.getNextId( getState() ); + + dispatch(updux.actions.inc_next_id()); + + next(action); + + dispatch( updux.actions.add_todo_with_id({ description: action.payload, id }) ); +}); + +export default updux.asDux; + +``` + +Tadah! We had to define the `add_todo` effect at the top level as it needs to +access the `getNextId` selector from `next_id` and the `add_todo_with_id` +action from the `todos`. + +Note that the `getNextId` selector still get the +rigth value; when aggregating subduxes selectors Updux auto-wrap them to +access the right slice of the top object. I.e., the `getNextId` selector +at the main level is actually defined as: + +``` +const getNextId = state => next_id.selectors.getNextId(state.next_id); +``` + +## Exporting upduxes + +As a general rule, don't directly export your upduxes, but rather use the accessor `asDux`. + +``` +const updux = new Updux({ ... }); + +... + +export default updux.asDux; +``` + +`asDux` returns an immutable copy of the attributes of the updux. Exporting +this instead of the updux itself prevents unexpected modifications done +outside of the updux declaration file. More importantly, the output of +`asDux` has more precise typing, which in result results in better typing of +parent upduxes using the dux as one of its subduxes. + +[immer]: https://www.npmjs.com/package/immer +[lodash]: https://www.npmjs.com/package/lodash +[ts-action]: https://www.npmjs.com/package/ts-action diff --git a/docs/typedoc/README.md b/docs/typedoc/README.md new file mode 100644 index 0000000..9222c85 --- /dev/null +++ b/docs/typedoc/README.md @@ -0,0 +1,228 @@ +[updux - v1.2.0](README.md) › [Globals](globals.md) + +# updux - v1.2.0 + +# What's Updux? + +So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered +[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux. + +It has a couple of pretty good ideas that removes some of the +boilerplate. Keeping mutations and asynchronous effects close to the +reducer definition? Nice. Automatically infering the +actions from the said mutations and effects? Genius! + +But it also enforces a flat hierarchy of reducers -- where +is the fun in that? And I'm also having a strong love for +[Updeep](https://github.com/substantial/updeep), so I want reducer state updates to leverage the heck out of it. + +All that to say, say hello to `Updux`. Heavily inspired by `rematch`, but twisted +to work with `updeep` and to fit my peculiar needs. It offers features such as + +* Mimic the way VueX has mutations (reducer reactions to specific actions) and + effects (middleware reacting to actions that can be asynchronous and/or + have side-effects), so everything pertaining to a store are all defined + in the space place. +* Automatically gather all actions used by the updux's effects and mutations, + and makes then accessible as attributes to the `dispatch` object of the + store. +* Mutations have a signature that is friendly to Updux and Immer. +* Also, the mutation signature auto-unwrap the payload of the actions for you. +* TypeScript types. + +Fair warning: this package is still very new, probably very buggy, +definitively very badly documented, and very subject to changes. Caveat +Maxima Emptor. + +# Synopsis + +``` +import updux from 'updux'; + +import otherUpdux from './otherUpdux'; + +const { + initial, + reducer, + actions, + middleware, + createStore, +} = new Updux({ + initial: { + counter: 0, + }, + subduxes: { + otherUpdux, + }, + mutations: { + inc: ( increment = 1 ) => u({counter: s => s + increment }) + }, + effects: { + '*' => api => next => action => { + console.log( "hey, look, an action zoomed by!", action ); + next(action); + }; + }, + actions: { + customAction: ( someArg ) => ({ + type: "custom", + payload: { someProp: someArg } + }), + }, + +}); + +const store = createStore(); + +store.dispatch.inc(3); +``` + +# Description + +Full documentation can be [found here](https://yanick.github.io/updux/docs/). + +## Exporting upduxes + +If you are creating upduxes that will be used as subduxes +by other upduxes, or as +[ducks](https://github.com/erikras/ducks-modular-redux)-like containers, I +recommend that you export the Updux instance as the default export: + +``` +import Updux from 'updux'; + +const updux = new Updux({ ... }); + +export default updux; +``` + +Then you can use them as subduxes like this: + +``` +import Updux from 'updux'; +import foo from './foo'; // foo is an Updux +import bar from './bar'; // bar is an Updux as well + +const updux = new Updux({ + subduxes: { + foo, bar + } +}); +``` + +Or if you want to use it: + +``` +import updux from './myUpdux'; + +const { + reducer, + actions: { doTheThing }, + createStore, + middleware, +} = updux; +``` + +## Mapping a mutation to all values of a state + +Say you have a `todos` state that is an array of `todo` sub-states. It's easy +enough to have the main reducer maps away all items to the sub-reducer: + +``` +const todo = new Updux({ + mutations: { + review: () => u({ reviewed: true}), + done: () => u({done: true}), + }, +}); + +const todos = new Updux({ initial: [] }); + +todos.addMutation( + todo.actions.review, + (_,action) => state => state.map( todo.upreducer(action) ) +); +todos.addMutation( + todo.actions.done, + (id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))), +); + +``` + +But `updeep` can iterate through all the items of an array (or the values of +an object) via the special key `*`. So the todos updux above could also be +written: + +``` +const todo = new Updux({ + mutations: { + review: () => u({ reviewed: true}), + done: () => u({done: true}), + }, +}); + +const todos = new Updux({ + subduxes: { '*': todo }, +}); + +todos.addMutation( + todo.actions.done, + (id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))), + true +); +``` + +The advantages being that the actions/mutations/effects of the subdux will be +imported by the root updux as usual, and all actions that aren't being +overridden by a sink mutation will trickle down automatically. + +## Usage with Immer + +While Updux was created with Updeep in mind, it also plays very +well with [Immer](https://immerjs.github.io/immer/docs/introduction). + +For example, taking this basic updux: + +``` +import Updux from 'updux'; + +const updux = new Updux({ + initial: { counter: 0 }, + mutations: { + add: (inc=1) => state => { counter: counter + inc } + } +}); + +``` + +Converting it to Immer would look like: + +``` +import Updux from 'updux'; +import { produce } from 'Immer'; + +const updux = new Updux({ + initial: { counter: 0 }, + mutations: { + add: (inc=1) => produce( draft => draft.counter += inc ) } + } +}); + +``` + +But since typing `produce` over and over is no fun, `groomMutations` +can be used to wrap all mutations with it: + +``` +import Updux from 'updux'; +import { produce } from 'Immer'; + +const updux = new Updux({ + initial: { counter: 0 }, + groomMutations: mutation => (...args) => produce( mutation(...args) ), + mutations: { + add: (inc=1) => draft => draft.counter += inc + } +}); + +``` diff --git a/docs/typedoc/classes/updux.md b/docs/typedoc/classes/updux.md new file mode 100644 index 0000000..5a7a2a1 --- /dev/null +++ b/docs/typedoc/classes/updux.md @@ -0,0 +1,491 @@ +[updux - v1.2.0](../README.md) › [Globals](../globals.md) › [Updux](updux.md) + +# Class: Updux <**S, A, X, C**> + +## Type parameters + +▪ **S** + +▪ **A** + +▪ **X** + +▪ **C**: *[UpduxConfig](../globals.md#upduxconfig)* + +## Hierarchy + +* **Updux** + +## Index + +### Constructors + +* [constructor](updux.md#constructor) + +### Properties + +* [coduxes](updux.md#coduxes) +* [groomMutations](updux.md#groommutations) +* [subduxes](updux.md#subduxes) + +### Accessors + +* [_middlewareEntries](updux.md#_middlewareentries) +* [actions](updux.md#actions) +* [asDux](updux.md#asdux) +* [createStore](updux.md#createstore) +* [initial](updux.md#initial) +* [middleware](updux.md#middleware) +* [mutations](updux.md#mutations) +* [reducer](updux.md#reducer) +* [selectors](updux.md#selectors) +* [subduxUpreducer](updux.md#subduxupreducer) +* [upreducer](updux.md#upreducer) + +### Methods + +* [addAction](updux.md#addaction) +* [addEffect](updux.md#addeffect) +* [addMutation](updux.md#addmutation) +* [addSelector](updux.md#addselector) + +## Constructors + +### constructor + +\+ **new Updux**(`config`: C): *[Updux](updux.md)* + +**Parameters:** + +Name | Type | Default | Description | +------ | ------ | ------ | ------ | +`config` | C | {} as C | an [UpduxConfig](../globals.md#upduxconfig) plain object | + +**Returns:** *[Updux](updux.md)* + +## Properties + +### coduxes + +• **coduxes**: *[Dux](../globals.md#dux)[]* + +___ + +### groomMutations + +• **groomMutations**: *function* + +#### Type declaration: + +▸ (`mutation`: [Mutation](../globals.md#mutation)‹S›): *[Mutation](../globals.md#mutation)‹S›* + +**Parameters:** + +Name | Type | +------ | ------ | +`mutation` | [Mutation](../globals.md#mutation)‹S› | + +___ + +### subduxes + +• **subduxes**: *[Dictionary](../globals.md#dictionary)‹[Dux](../globals.md#dux)›* + +## Accessors + +### _middlewareEntries + +• **get _middlewareEntries**(): *any[]* + +**Returns:** *any[]* + +___ + +### actions + +• **get actions**(): *[DuxActions](../globals.md#duxactions)‹A, C›* + +Action creators for all actions defined or used in the actions, mutations, effects and subduxes +of the updux config. + +Non-custom action creators defined in `actions` have the signature `(payload={},meta={}) => ({type, +payload,meta})` (with the extra sugar that if `meta` or `payload` are not +specified, that key won't be present in the produced action). + +The same action creator can be included +in multiple subduxes. However, if two different creators +are included for the same action, an error will be thrown. + +**`example`** + +``` +const actions = updux.actions; +``` + +**Returns:** *[DuxActions](../globals.md#duxactions)‹A, C›* + +___ + +### asDux + +• **get asDux**(): *object* + +Returns a ducks-like +plain object holding the reducer from the Updux object and all +its trimmings. + +**`example`** + +``` +const { + createStore, + upreducer, + subduxes, + coduxes, + middleware, + actions, + reducer, + mutations, + initial, + selectors, +} = myUpdux.asDux; +``` + +**Returns:** *object* + +* **actions**: = this.actions + +* **coduxes**: *object[]* = this.coduxes + +* **createStore**(): *function* + + * (`initial?`: S, `injectEnhancer?`: Function): *Store‹S› & object* + +* **initial**: = this.initial + +* **middleware**(): *function* + + * (`api`: UpduxMiddlewareAPI‹S, X›): *function* + + * (`next`: Function): *function* + + * (`action`: A): *any* + +* **mutations**(): *object* + +* **reducer**(): *function* + + * (`state`: S | undefined, `action`: [Action](../globals.md#action)): *S* + +* **selectors**: = this.selectors + +* **subduxes**(): *object* + +* **upreducer**(): *function* + + * (`action`: [Action](../globals.md#action)): *function* + + * (`state`: S): *S* + +___ + +### createStore + +• **get createStore**(): *function* + +Returns a `createStore` function that takes two argument: +`initial` and `injectEnhancer`. `initial` is a custom +initial state for the store, and `injectEnhancer` is a function +taking in the middleware built by the updux object and allowing +you to wrap it in any enhancer you want. + +**`example`** + +``` +const createStore = updux.createStore; + +const store = createStore(initial); +``` + +**Returns:** *function* + +▸ (`initial?`: S, `injectEnhancer?`: Function): *Store‹S› & object* + +**Parameters:** + +Name | Type | +------ | ------ | +`initial?` | S | +`injectEnhancer?` | Function | + +___ + +### initial + +• **get initial**(): *AggDuxState‹S, C›* + +**Returns:** *AggDuxState‹S, C›* + +___ + +### middleware + +• **get middleware**(): *[UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C››* + +Array of middlewares aggregating all the effects defined in the +updux and its subduxes. Effects of the updux itself are +done before the subduxes effects. +Note that `getState` will always return the state of the +local updux. + +**`example`** + +``` +const middleware = updux.middleware; +``` + +**Returns:** *[UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C››* + +___ + +### mutations + +• **get mutations**(): *[Dictionary](../globals.md#dictionary)‹[Mutation](../globals.md#mutation)‹S››* + +Merge of the updux and subduxes mutations. If an action triggers +mutations in both the main updux and its subduxes, the subduxes +mutations will be performed first. + +**Returns:** *[Dictionary](../globals.md#dictionary)‹[Mutation](../globals.md#mutation)‹S››* + +___ + +### reducer + +• **get reducer**(): *function* + +A Redux reducer generated using the computed initial state and +mutations. + +**Returns:** *function* + +▸ (`state`: S | undefined, `action`: [Action](../globals.md#action)): *S* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | undefined | +`action` | [Action](../globals.md#action) | + +___ + +### selectors + +• **get selectors**(): *[DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›* + +A dictionary of the updux's selectors. Subduxes' +selectors are included as well (with the mapping to the +sub-state already taken care of you). + +**Returns:** *[DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›* + +___ + +### subduxUpreducer + +• **get subduxUpreducer**(): *function* + +Returns the upreducer made of the merge of all sudbuxes reducers, without +the local mutations. Useful, for example, for sink mutations. + +**`example`** + +``` +import todo from './todo'; // updux for a single todo +import Updux from 'updux'; +import u from 'updeep'; + +const todos = new Updux({ initial: [], subduxes: { '*': todo } }); +todos.addMutation( + todo.actions.done, + ({todo_id},action) => u.map( u.if( u.is('id',todo_id) ), todos.subduxUpreducer(action) ) + true +); +``` + +**Returns:** *function* + +▸ (`action`: [Action](../globals.md#action)): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`action` | [Action](../globals.md#action) | + +▸ (`state`: S): *S* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | + +___ + +### upreducer + +• **get upreducer**(): *[Upreducer](../globals.md#upreducer)‹S›* + +**Returns:** *[Upreducer](../globals.md#upreducer)‹S›* + +## Methods + +### addAction + +▸ **addAction**(`theaction`: string, `transform?`: any): *ActionCreator‹string, any›* + +Adds an action to the updux. It can take an already defined action +creator, or any arguments that can be passed to `actionCreator`. + +**`example`** +``` + const action = updux.addAction( name, ...creatorArgs ); + const action = updux.addAction( otherActionCreator ); +``` + +**`example`** +``` +import {actionCreator, Updux} from 'updux'; + +const updux = new Updux(); + +const foo = updux.addAction('foo'); +const bar = updux.addAction( 'bar', (x) => ({stuff: x+1}) ); + +const baz = actionCreator( 'baz' ); + +foo({ a: 1}); // => { type: 'foo', payload: { a: 1 } } +bar(2); // => { type: 'bar', payload: { stuff: 3 } } +baz(); // => { type: 'baz', payload: undefined } +``` + +**Parameters:** + +Name | Type | +------ | ------ | +`theaction` | string | +`transform?` | any | + +**Returns:** *ActionCreator‹string, any›* + +▸ **addAction**(`theaction`: string | ActionCreator‹any›, `transform?`: undefined): *ActionCreator‹string, any›* + +**Parameters:** + +Name | Type | +------ | ------ | +`theaction` | string | ActionCreator‹any› | +`transform?` | undefined | + +**Returns:** *ActionCreator‹string, any›* + +___ + +### addEffect + +▸ **addEffect**<**AC**>(`creator`: AC, `middleware`: [UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›, ReturnType‹AC››, `isGenerator?`: undefined | false | true): *any* + +**Type parameters:** + +▪ **AC**: *ActionCreator* + +**Parameters:** + +Name | Type | +------ | ------ | +`creator` | AC | +`middleware` | [UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›, ReturnType‹AC›› | +`isGenerator?` | undefined | false | true | + +**Returns:** *any* + +▸ **addEffect**(`creator`: string, `middleware`: [UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C››, `isGenerator?`: undefined | false | true): *any* + +**Parameters:** + +Name | Type | +------ | ------ | +`creator` | string | +`middleware` | [UpduxMiddleware](../globals.md#upduxmiddleware)‹AggDuxState‹S, C›, [DuxSelectors](../globals.md#duxselectors)‹AggDuxState‹S, C›, X, C›› | +`isGenerator?` | undefined | false | true | + +**Returns:** *any* + +___ + +### addMutation + +▸ **addMutation**<**A**>(`creator`: A, `mutation`: [Mutation](../globals.md#mutation)‹S, ActionType‹A››, `isSink?`: undefined | false | true): *any* + +Adds a mutation and its associated action to the updux. + +**`remarks`** + +If a local mutation was already associated to the action, +it will be replaced by the new one. + +**`example`** + +```js +updux.addMutation( + action('ADD', payload() ), + inc => state => state + in +); +``` + +**Type parameters:** + +▪ **A**: *ActionCreator* + +**Parameters:** + +Name | Type | Description | +------ | ------ | ------ | +`creator` | A | - | +`mutation` | [Mutation](../globals.md#mutation)‹S, ActionType‹A›› | - | +`isSink?` | undefined | false | true | If `true`, disables the subduxes mutations for this action. To conditionally run the subduxes mutations, check out [subduxUpreducer](updux.md#subduxupreducer). Defaults to `false`. | + +**Returns:** *any* + +▸ **addMutation**<**A**>(`creator`: string, `mutation`: [Mutation](../globals.md#mutation)‹S, any›, `isSink?`: undefined | false | true): *any* + +**Type parameters:** + +▪ **A**: *ActionCreator* + +**Parameters:** + +Name | Type | +------ | ------ | +`creator` | string | +`mutation` | [Mutation](../globals.md#mutation)‹S, any› | +`isSink?` | undefined | false | true | + +**Returns:** *any* + +___ + +### addSelector + +▸ **addSelector**(`name`: string, `selector`: [Selector](../globals.md#selector)): *void* + +**Parameters:** + +Name | Type | +------ | ------ | +`name` | string | +`selector` | [Selector](../globals.md#selector) | + +**Returns:** *void* diff --git a/docs/typedoc/globals.md b/docs/typedoc/globals.md new file mode 100644 index 0000000..cd61ed5 --- /dev/null +++ b/docs/typedoc/globals.md @@ -0,0 +1,980 @@ +[updux - v1.2.0](README.md) › [Globals](globals.md) + +# updux - v1.2.0 + +## Index + +### Classes + +* [Updux](classes/updux.md) + +### Type aliases + +* [Action](globals.md#action) +* [ActionPair](globals.md#actionpair) +* [ActionPayloadGenerator](globals.md#actionpayloadgenerator) +* [ActionsOf](globals.md#actionsof) +* [CoduxesOf](globals.md#coduxesof) +* [Dictionary](globals.md#dictionary) +* [Dux](globals.md#dux) +* [DuxActions](globals.md#duxactions) +* [DuxActionsCoduxes](globals.md#duxactionscoduxes) +* [DuxActionsSubduxes](globals.md#duxactionssubduxes) +* [DuxSelectors](globals.md#duxselectors) +* [DuxState](globals.md#duxstate) +* [DuxStateCoduxes](globals.md#duxstatecoduxes) +* [DuxStateGlobSub](globals.md#duxstateglobsub) +* [DuxStateSubduxes](globals.md#duxstatesubduxes) +* [Effect](globals.md#effect) +* [GenericActions](globals.md#genericactions) +* [ItemsOf](globals.md#itemsof) +* [LocalDuxState](globals.md#localduxstate) +* [MaybePayload](globals.md#maybepayload) +* [MaybeReturnType](globals.md#maybereturntype) +* [Merge](globals.md#merge) +* [Mutation](globals.md#mutation) +* [MutationEntry](globals.md#mutationentry) +* [MwGen](globals.md#mwgen) +* [Next](globals.md#next) +* [RebaseSelector](globals.md#rebaseselector) +* [Selector](globals.md#selector) +* [SelectorsOf](globals.md#selectorsof) +* [StateOf](globals.md#stateof) +* [StoreWithDispatchActions](globals.md#storewithdispatchactions) +* [SubMutations](globals.md#submutations) +* [Submws](globals.md#submws) +* [UnionToIntersection](globals.md#uniontointersection) +* [UpduxActions](globals.md#upduxactions) +* [UpduxConfig](globals.md#upduxconfig) +* [UpduxLocalActions](globals.md#upduxlocalactions) +* [UpduxMiddleware](globals.md#upduxmiddleware) +* [Upreducer](globals.md#upreducer) + +### Variables + +* [subEffects](globals.md#const-subeffects) +* [updux](globals.md#const-updux) + +### Functions + +* [MiddlewareFor](globals.md#const-middlewarefor) +* [buildActions](globals.md#buildactions) +* [buildCreateStore](globals.md#buildcreatestore) +* [buildInitial](globals.md#buildinitial) +* [buildMiddleware](globals.md#buildmiddleware) +* [buildMutations](globals.md#buildmutations) +* [buildSelectors](globals.md#buildselectors) +* [buildUpreducer](globals.md#buildupreducer) +* [coduxes](globals.md#const-coduxes) +* [composeMutations](globals.md#const-composemutations) +* [composeMw](globals.md#const-composemw) +* [dux](globals.md#const-dux) +* [effectToMw](globals.md#const-effecttomw) +* [sliceMw](globals.md#slicemw) +* [subMiddleware](globals.md#const-submiddleware) +* [subSelectors](globals.md#subselectors) + +## Type aliases + +### Action + +Ƭ **Action**: *object & [MaybePayload](globals.md#maybepayload)‹P›* + +___ + +### ActionPair + +Ƭ **ActionPair**: *[string, ActionCreator]* + +___ + +### ActionPayloadGenerator + +Ƭ **ActionPayloadGenerator**: *function* + +#### Type declaration: + +▸ (...`args`: any[]): *any* + +**Parameters:** + +Name | Type | +------ | ------ | +`...args` | any[] | + +___ + +### ActionsOf + +Ƭ **ActionsOf**: *U extends Updux ? U["actions"] : object* + +___ + +### CoduxesOf + +Ƭ **CoduxesOf**: *U extends Updux ? S : []* + +___ + +### Dictionary + +Ƭ **Dictionary**: *object* + +#### Type declaration: + +* \[ **key**: *string*\]: T + +___ + +### Dux + +Ƭ **Dux**: *object* + +#### Type declaration: + +* **actions**: *A* + +* **coduxes**: *[Dux](globals.md#dux)[]* + +* **initial**: *AggDuxState‹S, C›* + +* **subduxes**: *[Dictionary](globals.md#dictionary)‹[Dux](globals.md#dux)›* + +___ + +### DuxActions + +Ƭ **DuxActions**: + +___ + +### DuxActionsCoduxes + +Ƭ **DuxActionsCoduxes**: *C extends Array ? UnionToIntersection> : object* + +___ + +### DuxActionsSubduxes + +Ƭ **DuxActionsSubduxes**: *C extends object ? ActionsOf : unknown* + +___ + +### DuxSelectors + +Ƭ **DuxSelectors**: *unknown extends X ? object : X* + +___ + +### DuxState + +Ƭ **DuxState**: *D extends object ? S : unknown* + +___ + +### DuxStateCoduxes + +Ƭ **DuxStateCoduxes**: *C extends Array ? UnionToIntersection> : unknown* + +___ + +### DuxStateGlobSub + +Ƭ **DuxStateGlobSub**: *S extends object ? StateOf : unknown* + +___ + +### DuxStateSubduxes + +Ƭ **DuxStateSubduxes**: *C extends object ? object : C extends object ? object : unknown* + +___ + +### Effect + +Ƭ **Effect**: *[string, [UpduxMiddleware](globals.md#upduxmiddleware) | [MwGen](globals.md#mwgen), undefined | false | true]* + +___ + +### GenericActions + +Ƭ **GenericActions**: *[Dictionary](globals.md#dictionary)‹ActionCreator‹string, function››* + +___ + +### ItemsOf + +Ƭ **ItemsOf**: *C extends object ? C[keyof C] : unknown* + +___ + +### LocalDuxState + +Ƭ **LocalDuxState**: *S extends never[] ? unknown[] : S* + +___ + +### MaybePayload + +Ƭ **MaybePayload**: *P extends object | string | boolean | number ? object : object* + +___ + +### MaybeReturnType + +Ƭ **MaybeReturnType**: *X extends function ? ReturnType : unknown* + +___ + +### Merge + +Ƭ **Merge**: *[UnionToIntersection](globals.md#uniontointersection)‹T[keyof T]›* + +___ + +### Mutation + +Ƭ **Mutation**: *function* + +#### Type declaration: + +▸ (`payload`: A["payload"], `action`: A): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`payload` | A["payload"] | +`action` | A | + +▸ (`state`: S): *S* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | + +___ + +### MutationEntry + +Ƭ **MutationEntry**: *[ActionCreator | string, [Mutation](globals.md#mutation)‹any, [Action](globals.md#action)‹string, any››, undefined | false | true]* + +___ + +### MwGen + +Ƭ **MwGen**: *function* + +#### Type declaration: + +▸ (): *[UpduxMiddleware](globals.md#upduxmiddleware)* + +___ + +### Next + +Ƭ **Next**: *function* + +#### Type declaration: + +▸ (`action`: [Action](globals.md#action)): *any* + +**Parameters:** + +Name | Type | +------ | ------ | +`action` | [Action](globals.md#action) | + +___ + +### RebaseSelector + +Ƭ **RebaseSelector**: *object* + +#### Type declaration: + +___ + +### Selector + +Ƭ **Selector**: *function* + +#### Type declaration: + +▸ (`state`: S): *unknown* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | + +___ + +### SelectorsOf + +Ƭ **SelectorsOf**: *C extends object ? S : unknown* + +___ + +### StateOf + +Ƭ **StateOf**: *D extends object ? I : unknown* + +___ + +### StoreWithDispatchActions + +Ƭ **StoreWithDispatchActions**: *Store‹S› & object* + +___ + +### SubMutations + +Ƭ **SubMutations**: *object* + +#### Type declaration: + +* \[ **slice**: *string*\]: [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation)› + +___ + +### Submws + +Ƭ **Submws**: *[Dictionary](globals.md#dictionary)‹[UpduxMiddleware](globals.md#upduxmiddleware)›* + +___ + +### UnionToIntersection + +Ƭ **UnionToIntersection**: *U extends any ? function : never extends function ? I : never* + +___ + +### UpduxActions + +Ƭ **UpduxActions**: *U extends Updux ? UnionToIntersection | ActionsOf[keyof CoduxesOf]>> : object* + +___ + +### UpduxConfig + +Ƭ **UpduxConfig**: *Partial‹object›* + +Configuration object given to Updux's constructor. + +#### arguments + +##### initial + +Default initial state of the reducer. If applicable, is merged with +the subduxes initial states, with the parent having precedence. + +If not provided, defaults to an empty object. + +##### actions + +[Actions](/concepts/Actions) used by the updux. + +```js +import { dux } from 'updux'; +import { action, payload } from 'ts-action'; + +const bar = action('BAR', payload()); +const foo = action('FOO'); + +const myDux = dux({ + actions: { + bar + }, + mutations: [ + [ foo, () => state => state ] + ] +}); + +myDux.actions.foo({ x: 1, y: 2 }); // => { type: foo, x:1, y:2 } +myDux.actions.bar(2); // => { type: bar, payload: 2 } +``` + +New actions used directly in mutations and effects will be added to the +dux actions -- that is, they will be accessible via `dux.actions` -- but will +not appear as part of its Typescript type. + +##### selectors + +Dictionary of selectors for the current updux. The updux also +inherit its subduxes' selectors. + +The selectors are available via the class' getter. + +##### mutations + + mutations: [ + [ action, mutation, isSink ], + ... + ] + +or + + mutations: { + action: mutation, + ... + } + +List of mutations for assign to the dux. If you want Typescript goodness, you +probably want to use `addMutation()` instead. + +In its generic array-of-array form, +each mutation tuple contains: the action, the mutation, +and boolean indicating if this is a sink mutation. + +The action can be an action creator function or a string. If it's a string, it's considered to be the +action type and a generic `action( actionName, payload() )` creator will be +generated for it. If an action is not already defined in the `actions` +parameter, it'll be automatically added. + +The pseudo-action type `*` can be used to match any action not explicitly matched by other mutations. + +```js +const todosUpdux = updux({ + mutations: { + add: todo => state => [ ...state, todo ], + done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ), + '*' (payload,action) => state => { + console.warn( "unexpected action ", action.type ); + return state; + }, + } +}); +``` + +The signature of the mutations is `(payload,action) => state => newState`. +It is designed to play well with `Updeep` (and [Immer](https://immerjs.github.io/immer/docs/introduction)). This way, instead of doing + +```js + mutation: { + renameTodo: newName => state => { ...state, name: newName } + } +``` + +we can do + +```js + mutation: { + renameTodo: newName => u({ name: newName }) + } +``` + +The final argument is the optional boolean `isSink`. If it is true, it'll +prevent subduxes' mutations on the same action. It defaults to `false`. + +The object version of the argument can be used as a shortcut when all actions +are strings. In that case, `isSink` is `false` for all mutations. + +##### groomMutations + +Function that can be provided to alter all local mutations of the updux +(the mutations of subduxes are left untouched). + +Can be used, for example, for Immer integration: + +```js +import Updux from 'updux'; +import { produce } from 'Immer'; + +const updux = new Updux({ + initial: { counter: 0 }, + groomMutations: mutation => (...args) => produce( mutation(...args) ), + mutations: { + add: (inc=1) => draft => draft.counter += inc + } +}); +``` + +Or perhaps for debugging: + +```js +import Updux from 'updux'; + +const updux = new Updux({ + initial: { counter: 0 }, + groomMutations: mutation => (...args) => state => { + console.log( "got action ", args[1] ); + return mutation(...args)(state); + } +}); +``` +##### subduxes + +Object mapping slices of the state to sub-upduxes. In addition to creating +sub-reducers for those slices, it'll make the parend updux inherit all the +actions and middleware from its subduxes. + +For example, if in plain Redux you would do + +```js +import { combineReducers } from 'redux'; +import todosReducer from './todos'; +import statisticsReducer from './statistics'; + +const rootReducer = combineReducers({ + todos: todosReducer, + stats: statisticsReducer, +}); +``` + +then with Updux you'd do + +```js +import { updux } from 'updux'; +import todos from './todos'; +import statistics from './statistics'; + +const rootUpdux = updux({ + subduxes: { + todos, + statistics + } +}); +``` + +##### effects + +Array of arrays or plain object defining asynchronous actions and side-effects triggered by actions. +The effects themselves are Redux middleware, with the `dispatch` +property of the first argument augmented with all the available actions. + +``` +updux({ + effects: { + fetch: ({dispatch}) => next => async (action) => { + next(action); + + let result = await fetch(action.payload.url).then( result => result.json() ); + dispatch.fetchSuccess(result); + } + } +}); +``` + +**`example`** + +``` +import Updux from 'updux'; +import { actions, payload } from 'ts-action'; +import u from 'updeep'; + +const todoUpdux = new Updux({ + initial: { + done: false, + note: "", + }, + actions: { + finish: action('FINISH', payload()), + edit: action('EDIT', payload()), + }, + mutations: [ + [ edit, note => u({note}) ] + ], + selectors: { + getNote: state => state.note + }, + groomMutations: mutation => transform(mutation), + subduxes: { + foo + }, + effects: { + finish: () => next => action => { + console.log( "Woo! one more bites the dust" ); + } + } +}) +``` + +___ + +### UpduxLocalActions + +Ƭ **UpduxLocalActions**: *S extends Updux ? object : S extends Updux ? A : object* + +___ + +### UpduxMiddleware + +Ƭ **UpduxMiddleware**: *function* + +#### Type declaration: + +▸ (`api`: UpduxMiddlewareAPI‹S, X›): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`api` | UpduxMiddlewareAPI‹S, X› | + +▸ (`next`: Function): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`next` | Function | + +▸ (`action`: A): *any* + +**Parameters:** + +Name | Type | +------ | ------ | +`action` | A | + +___ + +### Upreducer + +Ƭ **Upreducer**: *function* + +#### Type declaration: + +▸ (`action`: [Action](globals.md#action)): *function* + +**Parameters:** + +Name | Type | +------ | ------ | +`action` | [Action](globals.md#action) | + +▸ (`state`: S): *S* + +**Parameters:** + +Name | Type | +------ | ------ | +`state` | S | + +## Variables + +### `Const` subEffects + +• **subEffects**: *[Effect](globals.md#effect)* = [ '*', subMiddleware ] as any + +___ + +### `Const` updux + +• **updux**: *[Updux](classes/updux.md)‹unknown, null, unknown, object›* = new Updux({ + subduxes: { + foo: dux({ initial: "banana" }) + } +}) + +## Functions + +### `Const` MiddlewareFor + +▸ **MiddlewareFor**(`type`: any, `mw`: Middleware): *Middleware* + +**Parameters:** + +Name | Type | +------ | ------ | +`type` | any | +`mw` | Middleware | + +**Returns:** *Middleware* + +___ + +### buildActions + +▸ **buildActions**(`actions`: [ActionPair](globals.md#actionpair)[]): *[Dictionary](globals.md#dictionary)‹ActionCreator‹string, function››* + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`actions` | [ActionPair](globals.md#actionpair)[] | [] | + +**Returns:** *[Dictionary](globals.md#dictionary)‹ActionCreator‹string, function››* + +___ + +### buildCreateStore + +▸ **buildCreateStore**<**S**, **A**>(`reducer`: Reducer‹S›, `middleware`: Middleware, `actions`: A): *function* + +**Type parameters:** + +▪ **S** + +▪ **A** + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`reducer` | Reducer‹S› | - | +`middleware` | Middleware | - | +`actions` | A | {} as A | + +**Returns:** *function* + +▸ (`initial?`: S, `injectEnhancer?`: Function): *Store‹S› & object* + +**Parameters:** + +Name | Type | +------ | ------ | +`initial?` | S | +`injectEnhancer?` | Function | + +___ + +### buildInitial + +▸ **buildInitial**(`initial`: any, `coduxes`: any, `subduxes`: any): *any* + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`initial` | any | - | +`coduxes` | any | [] | +`subduxes` | any | {} | + +**Returns:** *any* + +___ + +### buildMiddleware + +▸ **buildMiddleware**<**S**>(`local`: [UpduxMiddleware](globals.md#upduxmiddleware)[], `co`: [UpduxMiddleware](globals.md#upduxmiddleware)[], `sub`: [Submws](globals.md#submws)): *[UpduxMiddleware](globals.md#upduxmiddleware)‹S›* + +**Type parameters:** + +▪ **S** + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`local` | [UpduxMiddleware](globals.md#upduxmiddleware)[] | [] | +`co` | [UpduxMiddleware](globals.md#upduxmiddleware)[] | [] | +`sub` | [Submws](globals.md#submws) | {} | + +**Returns:** *[UpduxMiddleware](globals.md#upduxmiddleware)‹S›* + +___ + +### buildMutations + +▸ **buildMutations**(`mutations`: [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation) | [[Mutation](globals.md#mutation), boolean | undefined]›, `subduxes`: object, `coduxes`: [Upreducer](globals.md#upreducer)[]): *object* + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`mutations` | [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation) | [[Mutation](globals.md#mutation), boolean | undefined]› | {} | +`subduxes` | object | {} | +`coduxes` | [Upreducer](globals.md#upreducer)[] | [] | + +**Returns:** *object* + +___ + +### buildSelectors + +▸ **buildSelectors**(`localSelectors`: [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›, `coduxes`: [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›[], `subduxes`: [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›): *object* + +**Parameters:** + +Name | Type | Default | +------ | ------ | ------ | +`localSelectors` | [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)› | {} | +`coduxes` | [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›[] | [] | +`subduxes` | [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)› | {} | + +**Returns:** *object* + +___ + +### buildUpreducer + +▸ **buildUpreducer**<**S**>(`initial`: S, `mutations`: [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation)‹S››): *[Upreducer](globals.md#upreducer)‹S›* + +**Type parameters:** + +▪ **S** + +**Parameters:** + +Name | Type | +------ | ------ | +`initial` | S | +`mutations` | [Dictionary](globals.md#dictionary)‹[Mutation](globals.md#mutation)‹S›› | + +**Returns:** *[Upreducer](globals.md#upreducer)‹S›* + +___ + +### `Const` coduxes + +▸ **coduxes**<**C**, **U**>(...`coduxes`: U): *object* + +**Type parameters:** + +▪ **C**: *[Dux](globals.md#dux)* + +▪ **U**: *[C]* + +**Parameters:** + +Name | Type | +------ | ------ | +`...coduxes` | U | + +**Returns:** *object* + +* **coduxes**: *U* + +___ + +### `Const` composeMutations + +▸ **composeMutations**(`mutations`: [Mutation](globals.md#mutation)[]): *function | (Anonymous function)* + +**Parameters:** + +Name | Type | +------ | ------ | +`mutations` | [Mutation](globals.md#mutation)[] | + +**Returns:** *function | (Anonymous function)* + +___ + +### `Const` composeMw + +▸ **composeMw**(`mws`: [UpduxMiddleware](globals.md#upduxmiddleware)[]): *(Anonymous function)* + +**Parameters:** + +Name | Type | +------ | ------ | +`mws` | [UpduxMiddleware](globals.md#upduxmiddleware)[] | + +**Returns:** *(Anonymous function)* + +___ + +### `Const` dux + +▸ **dux**<**S**, **A**, **X**, **C**>(`config`: C): *object* + +**Type parameters:** + +▪ **S** + +▪ **A** + +▪ **X** + +▪ **C**: *[UpduxConfig](globals.md#upduxconfig)* + +**Parameters:** + +Name | Type | +------ | ------ | +`config` | C | + +**Returns:** *object* + +* **actions**: = this.actions + +* **coduxes**: *object[]* = this.coduxes + +* **createStore**(): *function* + + * (`initial?`: S, `injectEnhancer?`: Function): *Store‹S› & object* + +* **initial**: = this.initial + +* **middleware**(): *function* + + * (`api`: UpduxMiddlewareAPI‹S, X›): *function* + + * (`next`: Function): *function* + + * (`action`: A): *any* + +* **mutations**(): *object* + +* **reducer**(): *function* + + * (`state`: S | undefined, `action`: [Action](globals.md#action)): *S* + +* **selectors**: = this.selectors + +* **subduxes**(): *object* + +* **upreducer**(): *function* + + * (`action`: [Action](globals.md#action)): *function* + + * (`state`: S): *S* + +___ + +### `Const` effectToMw + +▸ **effectToMw**(`effect`: [Effect](globals.md#effect), `actions`: [Dictionary](globals.md#dictionary)‹ActionCreator›, `selectors`: [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)›): *subMiddleware | augmented* + +**Parameters:** + +Name | Type | +------ | ------ | +`effect` | [Effect](globals.md#effect) | +`actions` | [Dictionary](globals.md#dictionary)‹ActionCreator› | +`selectors` | [Dictionary](globals.md#dictionary)‹[Selector](globals.md#selector)› | + +**Returns:** *subMiddleware | augmented* + +___ + +### sliceMw + +▸ **sliceMw**(`slice`: string, `mw`: [UpduxMiddleware](globals.md#upduxmiddleware)): *[UpduxMiddleware](globals.md#upduxmiddleware)* + +**Parameters:** + +Name | Type | +------ | ------ | +`slice` | string | +`mw` | [UpduxMiddleware](globals.md#upduxmiddleware) | + +**Returns:** *[UpduxMiddleware](globals.md#upduxmiddleware)* + +___ + +### `Const` subMiddleware + +▸ **subMiddleware**(): *(Anonymous function)* + +**Returns:** *(Anonymous function)* + +___ + +### subSelectors + +▸ **subSelectors**(`__namedParameters`: [string, Function]): *[string, [Selector](globals.md#selector)][]* + +**Parameters:** + +Name | Type | +------ | ------ | +`__namedParameters` | [string, Function] | + +**Returns:** *[string, [Selector](globals.md#selector)][]* diff --git a/docs/updux.md b/docs/updux.md deleted file mode 100644 index debb225..0000000 --- a/docs/updux.md +++ /dev/null @@ -1,359 +0,0 @@ -# Updux - -`Updux` is a way to minimize and simplify the boilerplate associated with the -creation of a `Redux` store. It takes a shorthand configuration -object, and generates the appropriate reducer, actions, middleware, etc. -In true `Redux`-like fashion, upduxes can be made of sub-upduxes (`subduxes` for short) for different slices of the root state. - -## Constructor - - const updux = new Updux({ ...buildArgs }) - -### arguments - -#### initial - -Default initial state of the reducer. If applicable, is merged with -the subduxes initial states, with the parent having precedence. - -If not provided, defaults to an empty object. - -#### actions - -Generic action creations are automatically created from the mutations and effects, but you can -also define custom action creator here. The object's values are function that -transform the arguments of the creator to the action's payload. - -```js -const { actions } = updux({ - actions: { - bar: (x,y) => ({x,y}) - }, - mutations: { - foo: () => state => state, - } -}); - -actions.foo({ x: 1, y: 2 }); // => { type: foo, payload: { x:1, y:2 } } -actions.bar(1,2); // => { type: bar, payload: { x:1, y:2 } } - - -#### selectors - -Dictionary of selectors for the current updux. The updux also -inherit its dubduxes' selectors. - -The selectors are available via the class' getter and, for -middlewares, the middlewareApi. - -```js -const todoUpdux = new Updux({ - selectors: { - done: state => state.filter( ({done}) => done ), - byId: state => targetId => state.find( ({id}) => id === targetId ), - } -} -``` - -#### mutations - -Object mapping actions to the associated state mutation. - -For example, in `Redux` you'd do - -```js -function todosReducer(state=[],action) { - - switch(action.type) { - case 'ADD': return [ ...state, action.payload ]; - - case 'DONE': return state.map( todo => todo.id === action.payload - ? { ...todo, done: true } : todo ) - - default: return state; - } -} -``` - -With Updux: - -```js -const todosUpdux = updux({ - mutations: { - add: todo => state => [ ...state, todo ], - done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ) - } -}); -``` - -The signature of the mutations is `(payload,action) => state => newState`. -It is designed to play well with `Updeep` (and [Immer](https://immerjs.github.io/immer/docs/introduction)). This way, instead of doing - -```js - mutation: { - renameTodo: newName => state => { ...state, name: newName } - } -``` - -we can do - -```js - mutation: { - renameTodo: newName => u({ name: newName }) - } -``` - -Also, the special key `*` can be used to match any -action not explicitly matched by other mutations. - -```js -const todosUpdux = updux({ - mutations: { - add: todo => state => [ ...state, todo ], - done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ), - '*' (payload,action) => state => { - console.warn( "unexpected action ", action.type ); - return state; - }, - } -}); -``` - - -#### groomMutations - - -Function that can be provided to alter all local mutations of the updux -(the mutations of subduxes are left untouched). - -Can be used, for example, for Immer integration: - -```js -import Updux from 'updux'; -import { produce } from 'Immer'; - -const updux = new Updux({ - initial: { counter: 0 }, - groomMutations: mutation => (...args) => produce( mutation(...args) ), - mutations: { - add: (inc=1) => draft => draft.counter += inc - } -}); -``` - -Or perhaps for debugging: - -```js -import Updux from 'updux'; - -const updux = new Updux({ - initial: { counter: 0 }, - groomMutations: mutation => (...args) => state => { - console.log( "got action ", args[1] ); - return mutation(...args)(state); - } -}); -``` - -#### subduxes - -Object mapping slices of the state to sub-upduxes. In addition to creating -sub-reducers for those slices, it'll make the parend updux inherit all the -actions and middleware from its subduxes. - -For example, if in plain Redux you would do - -```js -import { combineReducers } from 'redux'; -import todosReducer from './todos'; -import statisticsReducer from './statistics'; - -const rootReducer = combineReducers({ - todos: todosReducer, - stats: statisticsReducer, -}); -``` - -then with Updux you'd do - -```js -import { updux } from 'updux'; -import todos from './todos'; -import statistics from './statistics'; - -const rootUpdux = updux({ - subduxes: { - todos, - statistics - } -}); -``` - -#### effects - -Plain object defining asynchronous actions and side-effects triggered by actions. -The effects themselves are Redux middleware, with the `dispatch` -property of the first argument augmented with all the available actions. - -``` -updux({ - effects: { - fetch: ({dispatch}) => next => async (action) => { - next(action); - - let result = await fetch(action.payload.url).then( result => result.json() ); - dispatch.fetchSuccess(result); - } - } -}); -``` - - -#### middleware - -## Getters - -### actions - -Action creators for all actions defined or used in the actions, mutations, effects and subduxes -of the updux config. - -Non-custom action creators defined in `actions` have the signature `(payload={},meta={}) => ({type, -payload,meta})` (with the extra sugar that if `meta` or `payload` are not -specified, the key is not present in the produced action). - -If the same action appears in multiple locations, the precedence order -determining which one will prevail is - - actions generated from mutations/effects < non-custom subduxes actions < - custom subduxes actions < custom actions - -### middleware - - const middleware = updux.middleware; - -Array of middlewares aggregating all the effects defined in the -updux and its subduxes. Effects of the updux itself are -done before the subduxes effects. -Note that `getState` will always return the state of the -local updux. The function `getRootState` is provided -alongside `getState` to get the root state. - - -#### reducer - -A Redux reducer generated using the computed initial state and -mutations. - - -#### mutations - -Merge of the updux and subduxes mutations. If an action triggers -mutations in both the main updux and its subduxes, the subduxes -mutations will be performed first. - -#### subduxUpreducer - - -Returns the upreducer made of the merge of all sudbuxes reducers, without -the local mutations. Useful, for example, for sink mutations. - -```js -import todo from './todo'; // updux for a single todo -import Updux from 'updux'; -import u from 'updeep'; - -const todos = new Updux({ initial: [], subduxes: { '*': todo } }); -todos.addMutation( - todo.actions.done, - ({todo_id},action) => u.map( u.if( u.is('id',todo_id) ), todos.subduxUpreducer(action) ) - true -); -``` - - -#### createStore - - -Same as doing - -```js -import { createStore, applyMiddleware } from 'redux'; - -const { initial, reducer, middleware, actions } = updox(...); - -const store = createStore( initial, reducer, applyMiddleware(middleware) ); - -for ( let type in actions ) { - store.dispatch[type] = (...args) => { - store.dispatch(actions[type](...args)) - }; -} -``` - -So that later on you can do - -```js -store.dispatch.addTodo(...); - -// still work -store.dispatch( actions.addTodo(...) ); -``` - -## Methods - -### asDux - - -Returns a [ducks](https://github.com/erikras/ducks-modular-redux)-like -plain object holding the reducer from the Updux object and all -its trimmings. - - -### addMutation - -Adds a mutation and its associated action to the updux. -If a local mutation was already associated to the action, -it will be replaced by the new one. -@param isSink -If `true`, disables the subduxes mutations for this action. To -conditionally run the subduxes mutations, check out [[subduxUpreducer]]. - -```js -updux.addMutation( add, inc => state => state + inc ); -``` - -### addAction - -```js -const action = updux.addAction( name, ...creatorArgs ); -const action = updux.addAction( otherActionCreator ); -``` - -Adds an action to the updux. It can take an already defined action creator, -or any arguments that can be passed to `actionCreator`. - -```js -import {actionCreator, Updux} from 'updux'; - -const updux = new Updux(); - -const foo = updux.addAction('foo'); -const bar = updux.addAction( 'bar', (x) => ({stuff: x+1}) ); - -const baz = actionCreator( 'baz' ); - -foo({ a: 1}); // => { type: 'foo', payload: { a: 1 } } -bar(2); // => { type: 'bar', payload: { stuff: 3 } } -baz(); // => { type: 'baz', payload: undefined } - -``` - -### selectors - -Returns a dictionary of the -updux's selectors. Subduxes' selectors -are included as well (with the mapping to the sub-state already -taken care of you). - - diff --git a/package.json b/package.json index 26cbf24..ccca393 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,67 @@ { - "dependencies": { - "lodash": "^4.17.15", - "redux": "^4.0.4", - "ts-action": "^11.0.0", - "updeep": "^1.2.0" - }, - "devDependencies": { - "@babel/cli": "^7.6.4", - "@babel/core": "^7.6.4", - "@babel/preset-env": "^7.6.3", - "@types/jest": "^24.0.19", - "@types/lodash": "^4.14.144", - "babel-jest": "^24.9.0", - "docsify": "^4.10.2", - "docsify-cli": "^4.4.0", - "jest": "^24.9.0", - "ts-jest": "^24.1.0", - "tsd": "^0.10.0", - "typescript": "^3.6.4" - }, - "license": "MIT", - "main": "dist/index.js", - "name": "updux", - "description": "Updeep-friendly Redux helper framework", - "scripts": { - "docsify:serve": "docsify serve docs", - "build": "tsc", - "test": "jest" - }, - "version": "1.2.0", - "repository": { - "type": "git", - "url": "git+https://github.com/yanick/updux.git" - }, - "keywords": [ - "redux", - "updeep" - ], - "author": "Yanick Champoux (http://techblog.babyl.ca)", - "bugs": { - "url": "https://github.com/yanick/updux/issues" - }, - "homepage": "https://github.com/yanick/updux#readme", - "types": "./dist/index.d.ts", - "prettier": { - "tabWidth": 4, - "singleQuote": true, - "trailingComma": "es5" - } + "dependencies": { + "lodash": "^4.17.15", + "redux": "^4.0.5", + "ts-action": "^11.0.0", + "ts-node": "^8.6.2", + "updeep": "^1.2.0" + }, + "devDependencies": { + "tap": "^14.10.6", + "typedoc": "0.17.7", + "typedoc-plugin-markdown": "^2.2.17", + "sinon": "^9.0.1", + "promake": "^3.1.3", + "dtslint": "^3.3.0", + "glob": "^7.1.6", + "@types/sinon": "^7.5.2", + "docsify-tools": "^1.0.20", + "@babel/cli": "^7.8.4", + "@babel/core": "^7.8.7", + "@babel/preset-env": "^7.8.7", + "@types/jest": "^25.1.4", + "@types/lodash": "^4.14.149", + "@typescript-eslint/eslint-plugin": "^2.23.0", + "@typescript-eslint/parser": "^2.23.0", + "babel-jest": "^25.1.0", + "docsify": "^4.11.2", + "docsify-cli": "^4.4.0", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-prettier": "^3.1.2", + "jest": "^25.1.0", + "ts-jest": "^25.2.1", + "tsd": "^0.11.0", + "typescript": "^3.8.3" + }, + "license": "MIT", + "main": "dist/index.js", + "name": "updux", + "description": "Updeep-friendly Redux helper framework", + "scripts": { + "docsify:serve": "docsify serve docs", + "build": "tsc", + "test": "tap src/**test.ts" + }, + "version": "1.2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/yanick/updux.git" + }, + "keywords": [ + "redux", + "updeep" + ], + "author": "Yanick Champoux (http://techblog.babyl.ca)", + "bugs": { + "url": "https://github.com/yanick/updux/issues" + }, + "homepage": "https://github.com/yanick/updux#readme", + "types": "./dist/index.d.ts", + "prettier": { + "tabWidth": 4, + "singleQuote": true, + "trailingComma": "es5" + } } diff --git a/src/actions-params.test.ts b/src/actions-params.test.ts new file mode 100644 index 0000000..f998dcc --- /dev/null +++ b/src/actions-params.test.ts @@ -0,0 +1,121 @@ +import Updux, { dux, coduxes } from '.'; +import { action, payload } from 'ts-action'; +import { test } from 'tap'; +import { expectAssignable } from 'tsd'; +import { DuxActionsCoduxes } from './types'; + +const foo_actions = { + aaa: action('aaa', payload()), +}; + +const fooDux = dux({ + actions: foo_actions, +}); + +const bar_actions = { + bbb: action('bbb', (x: string) => ({ payload: '1' + x })), +}; + +test('actions are present', t => { + const barDux = dux({ + subduxes: { foo: fooDux }, + actions: bar_actions, + }); + + const result = Object.keys(barDux.actions); + result.sort(); + t.same(result, ['aaa', 'bbb']); + t.end(); +}); + +const a = action('a'); +const b = action('b'); + +test('typing', t => { + t.test('nothing at all', t => { + const foo = new Updux(); + t.same(foo.actions, {}); + t.end(); + }); + + t.test('with two coduxes', t => { + const myDux = new Updux({ + ...coduxes(dux({ actions: { a } }), dux({ actions: { b } })), + }); + + t.ok(myDux.actions.a); + t.end(); + }); + + t.test('empty dux', t => { + const empty = dux({}); + + expectAssignable(empty.actions); + + t.same(empty.actions, {}, 'no actions there'); + + t.end(); + }); + + t.test('coduxes actions', t => { + const typeOf = (x: C) => (x as any) as DuxActionsCoduxes; + + expectAssignable<{ a: any }>(typeOf([dux({ actions: { a } })])); + expectAssignable<{}>(typeOf([dux({})])); + + expectAssignable<{ a: any; b: any }>( + typeOf([dux({ actions: { a } }), dux({ actions: { b } })]) + ); + + const co = coduxes(dux({ actions: { a } }), dux({})).coduxes; + + expectAssignable<{ a: any }>(typeOf(co)); + + t.end(); + }); + + t.test('with empty coduxes', t => { + const emptyDux = dux({}); + + const myDux = dux({ + ...coduxes(dux({ actions: { a } }), emptyDux), + }); + + t.ok(myDux.actions.a); + t.end(); + }); + + t.test('with three coduxes', t => { + const emptyDux = new Updux(); + emptyDux.actions; + + const dux = new Updux({ + coduxes: [ + emptyDux, + new Updux({ actions: { a } }), + new Updux({ actions: { b } }), + ], + }); + + t.ok(dux.actions.b); + t.end(); + }); + + t.test('with grandchild', t => { + const dux = new Updux({ + subduxes: { + bar: new Updux({ + subduxes: { + baz: new Updux({ + actions: { a }, + }), + }, + }), + }, + }); + + t.ok(dux.actions.a); + t.end(); + }); + t.end(); +}); diff --git a/src/actions.test.ts b/src/actions.test.ts index 20fec44..92e893d 100644 --- a/src/actions.test.ts +++ b/src/actions.test.ts @@ -1,58 +1,78 @@ import { action, payload } from 'ts-action'; -import u from 'updeep'; -import Updux from '.'; +import Updux, { dux } from '.'; +import { test } from 'tap'; +import { expectAssignable } from 'tsd'; -const noopEffect = () => () => () => {}; +const noopEffect = () => () => () => null; -test('actions defined in effects and mutations, multi-level', () => { - const bar = action('bar',(payload,meta) => ({payload,meta}) ); - const foo = action('foo',(limit:number) => ({payload:{ limit} }) ); +test( + 'actions defined in effects and mutations, multi-level', + { autoend: true }, + t => { + const bar = action('bar', (payload, meta) => ({ payload, meta })); + const foo = action('foo', (limit: number) => ({ + payload: { limit }, + })); - const {actions} = new Updux({ - effects: [ [ foo, noopEffect ] ], - mutations: [ [ bar, () => () => null ] ], - subduxes: { - mysub: { - effects: {baz: noopEffect}, - mutations: {quux: () => () => null}, - actions: { - foo - }, - }, - myothersub: { - effects: [ [foo, noopEffect] ], - }, - }, - }); + const { actions }: any = dux({ + effects: [[foo, noopEffect] as any], + mutations: [[bar, () => () => null]], + subduxes: { + mysub: dux({ + effects: { baz: noopEffect }, + mutations: { quux: () => () => null }, + actions: { + foo, + }, + }), + myothersub: dux({ + effects: [[foo, noopEffect]], + }), + }, + }); - const types = Object.keys(actions); - types.sort(); + const types = Object.keys(actions); + types.sort(); - expect(types).toEqual(['bar', 'baz', 'foo', 'quux']); + t.match(types, ['bar', 'baz', 'foo', 'quux']); - expect(actions.bar()).toEqual({type: 'bar'}); - expect(actions.bar('xxx')).toEqual({type: 'bar', payload: 'xxx'}); - expect(actions.bar(undefined, 'yyy')).toEqual({type: 'bar', payload: undefined, meta: 'yyy'}); + t.match(actions.bar(), { type: 'bar' }); + t.match(actions.bar('xxx'), { type: 'bar', payload: 'xxx' }); + t.match(actions.bar(undefined, 'yyy'), { + type: 'bar', + payload: undefined, + meta: 'yyy', + }); - expect(actions.foo(12)).toEqual({type: 'foo', payload: {limit: 12}}); + t.same(actions.foo(12), { type: 'foo', payload: { limit: 12 } }); + } +); + +test('different calls to addAction', t => { + const updux = new Updux(); + + updux.addAction(action('foo', payload())); + t.match(updux.actions.foo('yo'), { + type: 'foo', + payload: 'yo', + }); + + updux.addAction('baz', x => ({ x })); + t.match(updux.actions.baz(3), { + type: 'baz', + payload: { x: 3 }, + }); + + t.end(); }); -describe('different calls to addAction', () => { - const updux = new Updux(); +test('types', t => { + const {actions} = dux({ actions: { + foo: action('foo') + }}); - test('string', () => { - updux.addAction( action('foo', payload() )); - expect(updux.actions.foo('yo')).toMatchObject({ - type: 'foo', - payload: 'yo', - }); - }); + expectAssignable( actions ); - test('actionCreator inlined', () => { - updux.addAction( 'baz', (x) => ({payload: {x}})); - expect(updux.actions.baz(3)).toMatchObject({ - type: 'baz', payload: { x: 3 } - }); - }); + t.end(); }); diff --git a/src/addMutations.test.ts b/src/addMutations.test.ts index 70f95e3..a956167 100644 --- a/src/addMutations.test.ts +++ b/src/addMutations.test.ts @@ -1,4 +1,5 @@ import { action } from 'ts-action'; +import tap from 'tap'; import Updux from "./updux"; @@ -6,17 +7,19 @@ type MyState = { sum: number; }; -test("added mutation is present", () => { +tap.test("added mutation is present", t => { const updux = new Updux({ initial: { sum: 0 } }); const add = action("add", (n: number) => ({ payload: { n } })); - updux.addMutation(add, ({ n }, action) => ({ sum }) => ({ sum: sum + n })); + updux.addMutation(add, ({ n }) => ({ sum }) => ({ sum: sum + n })); const store = updux.createStore(); - store.dispatch.add(3); + store.dispatch(add(3)); - expect(store.getState()).toEqual({ sum: 3 }); + t.same(store.getState(), {sum: 3}); + + t.end(); }); diff --git a/src/buildActions/index.ts b/src/buildActions/index.ts index 0961f56..5327e1b 100644 --- a/src/buildActions/index.ts +++ b/src/buildActions/index.ts @@ -1,12 +1,15 @@ import fp from 'lodash/fp'; import { - ActionCreator, + ActionCreator +} from 'ts-action'; + +import { Dictionary, } from '../types'; type ActionPair = [string, ActionCreator]; -function buildActions(actions: ActionPair[] = []): Dictionary { +function buildActions(actions: ActionPair[] = []): Dictionary {type: string} >>{ // priority => generics => generic subs => craft subs => creators const [crafted, generic] = fp.partition(([type, f]) => !f._genericAction)( diff --git a/src/buildCreateStore/index.ts b/src/buildCreateStore/index.ts index 981fac3..1d9ce7a 100644 --- a/src/buildCreateStore/index.ts +++ b/src/buildCreateStore/index.ts @@ -3,29 +3,28 @@ import { applyMiddleware, Middleware, Reducer, - PreloadedState + PreloadedState, + Store, } from 'redux'; -import { ActionCreator, Dictionary } from '../types'; -function buildCreateStore( +function buildCreateStore( reducer: Reducer, - initial: PreloadedState, middleware: Middleware, - actions: Dictionary, -) { - return () => { + actions: A = {} as A, +): (initial?: S, injectEnhancer?: Function) => Store & { actions: A } { + return function createStore(initial?: S, injectEnhancer?: Function ): Store & { actions: A } { + + let enhancer = injectEnhancer ? injectEnhancer(middleware) : applyMiddleware(middleware); + const store = reduxCreateStore( reducer, - initial, - applyMiddleware(middleware), + initial as PreloadedState, + enhancer ); - for (let a in actions) { - ( store.dispatch as any)[a] = (...args: any[]) => { - store.dispatch(actions[a](...args)); - }; - } - return store; + (store as any).actions = actions; + + return store as any; }; } diff --git a/src/buildCreateStore/test.ts b/src/buildCreateStore/test.ts new file mode 100644 index 0000000..fdd5a50 --- /dev/null +++ b/src/buildCreateStore/test.ts @@ -0,0 +1,28 @@ +import tap from 'tap'; +import { expectType } from 'tsd'; + +import buildCreateStore from '.'; +import { action, ActionCreator } from 'ts-action'; +import {Reducer} from 'redux'; + +const foo = action('foo'); +const bar = action('bar'); + +type State = { + x: number, + y: string +} + +const store = buildCreateStore( + ((state: State|undefined) => state ?? {x: 1} ) as Reducer, + () => () => () => {return}, + { foo, bar } +)(); + +expectType( store.getState() ); +expectType( store.getState().x ); + +expectType<{ foo: ActionCreator }>( store.actions ); + +tap.pass(); + diff --git a/src/buildInitial/index.ts b/src/buildInitial/index.ts index a32431d..1c38393 100644 --- a/src/buildInitial/index.ts +++ b/src/buildInitial/index.ts @@ -1,13 +1,16 @@ import fp from 'lodash/fp'; -import { Dictionary } from '../types'; +import u from 'updeep'; -function buildInitial( initial: S, subduxes?: Dictionary ): S; -function buildInitial( initial?: Partial, subduxes?: Partial ): S extends object ? S : never; -function buildInitial( - initial : any = {}, - subduxes : any = {} , -) { - return fp.isPlainObject(initial) ? fp.mergeAll([subduxes, initial]) : initial; +function buildInitial(initial: any, coduxes: any = [], subduxes: any = {}) { + if (!fp.isPlainObject(initial)) return initial; + + return fp.flow( + [ + u(fp.omit(['*'], subduxes)), + coduxes.map(i => u(i)), + u(initial), + ].flat() + )({}); } export default buildInitial; diff --git a/src/buildMiddleware/index.ts b/src/buildMiddleware/index.ts index 8421869..dc76f8c 100644 --- a/src/buildMiddleware/index.ts +++ b/src/buildMiddleware/index.ts @@ -1,61 +1,89 @@ -import fp from "lodash/fp"; +import fp from 'lodash/fp'; +import { ActionCreator } from 'ts-action'; -import { Middleware, MiddlewareAPI, Dispatch } from "redux"; +import { Middleware, MiddlewareAPI, Dispatch } from 'redux'; import { - Dictionary, - ActionCreator, - Action, - UpduxDispatch, - UpduxMiddleware, - UpduxMiddlewareAPI, - EffectEntry -} from "../types"; -import Updux from ".."; + Dictionary, + Action, + UpduxMiddleware, + UpduxMiddlewareAPI, + Selector, +} from '../types'; +import Updux from '..'; const MiddlewareFor = ( - type: any, - mw: Middleware + type: any, + mw: Middleware ): Middleware => api => next => action => { - if (!["*", "^", "$"].includes(type) && action.type !== type) - return next(action); + if (!type.includes('*') && action.type !== type) return next(action); - return mw(api)(next)(action); + return mw(api)(next)(action); }; type Next = (action: Action) => any; -function sliceMw(slice: string, mw: Middleware, updux: Updux): Middleware { - return api => { - const getSliceState = - slice.length > 0 ? () => fp.get(slice, api.getState()) : api.getState; - const getRootState = (api as any).getRootState || api.getState; - return mw({ ...api, getState: getSliceState, getRootState, - selectors: updux.selectors } as any); - }; +function sliceMw(slice: string, mw: UpduxMiddleware): UpduxMiddleware { + return api => { + const getSliceState = () => fp.get(slice, api.getState()); + return mw({ ...api, getState: getSliceState } as any); + }; } -function buildMiddleware( - middlewareEntries: any[] = [], - actions: Dictionary = {} -): UpduxMiddleware { - let mws = middlewareEntries - .map(([updux, slice, actionType, mw, isGen]: any) => - isGen ? [updux, slice, actionType, mw()] : [updux, slice, actionType, mw] - ) - .map(([updux, slice, actionType, mw]) => - MiddlewareFor(actionType, sliceMw(slice, mw, updux)) - ); +type Submws = Dictionary; - return (api: UpduxMiddlewareAPI) => { - for (let type in actions) { - const ac = actions[type]; - api.dispatch[type] = (...args: any[]) => api.dispatch(ac(...args)); - } +type MwGen = () => UpduxMiddleware; +export type Effect = [string, UpduxMiddleware|MwGen, boolean? ]; - return (original_next: Next) => { - return mws.reduceRight((next, mw) => mw(api)(next), original_next); +export const subMiddleware = () => next => action => next(action); +export const subEffects : Effect = [ '*', subMiddleware ] as any; + +export const effectToMw = ( + effect: Effect, + actions: Dictionary, + selectors: Dictionary, +) => { + let [type, mw, gen]: any = effect; + + if ( mw === subMiddleware ) return subMiddleware; + + if (gen) mw = mw(); + + const augmented = api => mw({ ...api, actions, selectors }); + + if (type === '*') return augmented; + + return api => next => action => { + if (action.type !== type) return next(action); + + return augmented(api)(next)(action); }; - }; +}; + +const composeMw = (mws: UpduxMiddleware[]) => ( + api: UpduxMiddlewareAPI +) => (original_next: Next) => + mws.reduceRight((next, mw) => mw(api)(next), original_next); + +export function buildMiddleware( + local: UpduxMiddleware[] = [], + co: UpduxMiddleware[] = [], + sub: Submws = {} +): UpduxMiddleware { + let inner = [ + ...co, + ...Object.entries(sub).map(([slice, mw]) => sliceMw(slice, mw)), + ]; + + let found = false; + let mws = local.flatMap(e => { + if (e !== subMiddleware) return e; + found = true; + return inner; + }); + + if (!found) mws = [...mws, ...inner]; + + return composeMw(mws) as UpduxMiddleware; } export default buildMiddleware; diff --git a/src/buildMiddleware/test.ts b/src/buildMiddleware/test.ts new file mode 100644 index 0000000..fa1502e --- /dev/null +++ b/src/buildMiddleware/test.ts @@ -0,0 +1,67 @@ +import { buildMiddleware, subMiddleware } from '.'; +import tap from 'tap'; +import sinon from 'sinon'; + +const myMiddleware = (tag: string) => api => next => action => { + next({ + ...action, + payload: [...action.payload, tag], + }); +}; + +const local1 = myMiddleware('local1'); +const local2 = myMiddleware('local2'); +const co = myMiddleware('co'); +const sub = myMiddleware('sub'); + +tap.test('basic', t => { + const next = sinon.fake(); + + const mw = buildMiddleware([local1, local2], [co], { sub }); + + mw({} as any)(next)({ type: 'foo', payload: [] }); + + t.match(next.firstCall.args, [ + { + type: 'foo', + payload: ['local1', 'local2', 'co', 'sub'], + }, + ]); + + t.end(); +}); + +tap.test('inner in the middle', t => { + const next = sinon.fake(); + + const mw = buildMiddleware([local1, subMiddleware, local2], [co], { + sub, + }); + + mw({} as any)(next)({ type: 'foo', payload: [] }); + + t.match(next.firstCall.lastArg, { + type: 'foo', + payload: ['local1', 'co', 'sub', 'local2'], + }); + + t.end(); +}); + +tap.test('sub-mw get their store sliced', t => { + const next = sinon.fake(); + + const sub = ({ getState }) => next => action => next(getState()); + + const mw = buildMiddleware([], [], { sub }); + + mw({ + getState() { + return { foo: 1, sub: 2 }; + }, + } as any)(next)({ type: 'noop' }); + + t.same( next.firstCall.lastArg, 2 ); + + t.end(); +}); diff --git a/src/buildMutations/index.ts b/src/buildMutations/index.ts index 41265b8..bdea378 100644 --- a/src/buildMutations/index.ts +++ b/src/buildMutations/index.ts @@ -1,71 +1,57 @@ -import fp from "lodash/fp"; -import u from "updeep"; -import { Mutation, Action, Dictionary, MutationEntry } from "../types"; +import fp from 'lodash/fp'; +import u from 'updeep'; +import { + Mutation, + Action, + Dictionary, + MutationEntry, + Upreducer, +} from '../types'; +import Updux from '..'; -const composeMutations = (mutations: Mutation[]) => - mutations.reduce((m1, m2) => (payload: any = null, action: Action) => state => - m2(payload, action)(m1(payload, action)(state)) - ); +const composeMutations = (mutations: Mutation[]) => { + if (mutations.length == 0) return () => state => state; + + return mutations.reduce( + (m1, m2) => (payload: any = null, action: Action) => state => + m2(payload, action)(m1(payload, action)(state)) + ); +}; type SubMutations = { - [slice: string]: Dictionary; + [slice: string]: Dictionary; }; function buildMutations( - mutations: Dictionary = {}, - subduxes = {} + mutations: Dictionary = {}, + subduxes = {}, + coduxes: Upreducer[] = [] ) { - // we have to differentiate the subduxes with '*' than those - // without, as the root '*' is not the same as any sub-'*' + const submuts = Object.entries(subduxes).map( + ([slice, upreducer]: [string, any]) => + ( + ((payload, action: Action) => + (u.updateIn as any)(slice, upreducer(action))) + ) + ); - const actions = fp.uniq( - Object.keys(mutations).concat( - ...Object.values(subduxes).map(({ mutations = {} }: any) => - Object.keys(mutations) - ) - ) - ); + const comuts = coduxes.map(c => (payload, action: Action) => c(action)); - let mergedMutations: Dictionary = {}; + const subreducer = composeMutations([...submuts, ...comuts]); - let [globby, nonGlobby] = fp.partition( - ([_, { mutations = {} }]: any) => mutations["*"], - Object.entries(subduxes) - ); + let merged = {}; - globby = fp.flow([ - fp.fromPairs, - fp.mapValues(({ reducer }) => (_: any, action: Action) => (state: any) => - reducer(state, action) - ) - ])(globby); - - const globbyMutation = (payload: any, action: Action) => - u(fp.mapValues((mut: any) => mut(payload, action))(globby)); - - actions.forEach(action => { - mergedMutations[action] = [globbyMutation]; - }); - - nonGlobby.forEach(([slice, { mutations = {}, reducer = {} }]: any[]) => { Object.entries(mutations).forEach(([type, mutation]) => { - const localized = (payload = null, action: Action) => { - return u.updateIn(slice)((mutation as Mutation)(payload, action)); - }; + const [m, sink] = Array.isArray(mutation) + ? mutation + : [mutation, false]; - mergedMutations[type].push(localized); + merged[type] = sink ? m : composeMutations([subreducer, m]); }); - }); - Object.entries(mutations).forEach(([type, mutation]) => { - if (Array.isArray(mutation)) { - if (mutation[1]) { - mergedMutations[type] = [mutation[0]]; - } else mergedMutations[type].push(mutation[0]); - } else mergedMutations[type].push(mutation); - }); + if (!merged['*']) merged['*'] = subreducer; - return fp.mapValues(composeMutations)(mergedMutations); + return merged; } export default buildMutations; diff --git a/src/buildSelectors/index.ts b/src/buildSelectors/index.ts index 217202b..1c8ce3a 100644 --- a/src/buildSelectors/index.ts +++ b/src/buildSelectors/index.ts @@ -2,24 +2,25 @@ import fp from 'lodash/fp'; import Updux from '..'; import { Dictionary, Selector } from '../types'; -function subSelectors([slice, subdux]: [string, Updux]): [string, Selector][] { - const selectors = subdux.selectors; +function subSelectors([slice, selectors]: [string, Function]): [string, Selector][] { if (!selectors) return []; return Object.entries( fp.mapValues(selector => (state: any) => (selector as any)(state[slice]) - )(selectors) + )(selectors as any) ); } export default function buildSelectors( localSelectors: Dictionary = {}, - subduxes: Dictionary = {} + coduxes: Dictionary[] = [], + subduxes: Dictionary = {} ) { return Object.fromEntries( [ Object.entries(subduxes).flatMap(subSelectors), + Object.entries(coduxes), Object.entries(localSelectors), ].flat() ); diff --git a/src/coduxes.test.ts b/src/coduxes.test.ts new file mode 100644 index 0000000..39e04ed --- /dev/null +++ b/src/coduxes.test.ts @@ -0,0 +1,36 @@ +import Updux from '.'; +import { action } from 'ts-action'; +import { Effect } from './buildMiddleware'; +import tap from 'tap'; +import sinon from 'sinon'; + +const myEffect: Effect = [ + '*', + () => next => action => next({ ...action, hello: true }), +]; + +const codux = new Updux({ + actions: { + foo: action('foo'), + }, + effects: [myEffect], +}); + +const dux = new Updux({ + coduxes: [codux], +}); + +tap.test('actions', t => { + t.ok(dux.actions.foo); + t.end(); +}); + +tap.test('effects', t => { + const next = sinon.fake(); + + dux.middleware({} as any)(next)({ type: 'foo' }); + + t.same(next.lastCall.lastArg, { type: 'foo', hello: true }); + + t.end(); +}); diff --git a/src/duxstate.test.ts b/src/duxstate.test.ts new file mode 100644 index 0000000..63e2c90 --- /dev/null +++ b/src/duxstate.test.ts @@ -0,0 +1,14 @@ +import tap from 'tap'; +import { expectType } from 'tsd'; + +import { dux, DuxState } from '.'; + +const myDux = dux({ + initial: { a: 1, b: "potato" } +}); + +type State = DuxState; + +expectType({ a: 12, b: "something" }); + +tap.pass(); diff --git a/src/effects-action.test.ts b/src/effects-action.test.ts new file mode 100644 index 0000000..5ee5362 --- /dev/null +++ b/src/effects-action.test.ts @@ -0,0 +1,14 @@ +import tap from 'tap'; +import Updux from '.'; +import { action, payload } from 'ts-action'; +import { expectType } from 'tsd'; + +const dux = new Updux({ }); + +const myAction = action('mine',payload() ); + +dux.addEffect( myAction, () => () => action => { + expectType<{payload: string; type: "mine"}>(action); +}); + +tap.pass("pure type checking"); diff --git a/src/index.ts b/src/index.ts index 126c1e2..e5c4888 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,22 @@ import Updux from "./updux"; +import { UpduxConfig, Dux, Dictionary, Selector, Mutation, AggDuxState, Action, + Upreducer, UpduxMiddleware, DuxActions, DuxSelectors } from "./types"; + +import {Creator} from 'ts-action'; +import { AnyAction, Store } from 'redux'; export { default as Updux } from "./updux"; -export { UpduxConfig } from "./types"; +export { UpduxConfig, DuxState } from "./types"; +export { subEffects } from './buildMiddleware'; +export * from './types'; export default Updux; + +export const coduxes = (...coduxes: U): { coduxes: U } => ({ + coduxes }); + +export const dux = (config: C ) => { +/// : Dux => { + return ( new Updux(config) ).asDux; +} + diff --git a/src/initial.test.ts b/src/initial.test.ts new file mode 100644 index 0000000..4a04e85 --- /dev/null +++ b/src/initial.test.ts @@ -0,0 +1,15 @@ +import { dux } from '.'; +import tap from 'tap'; + +const foo = dux({ + initial: { root: 'abc' }, + coduxes: [ + dux({ initial: { co: 'works' } }), + dux({ initial: { co2: 'works' } }), + ], + subduxes: { + bar: dux({ initial: 123 }), + }, +}); + +tap.same(foo.initial, { root: 'abc', co: 'works', co2: 'works', bar: 123 }); diff --git a/src/mappedUpdux.test.ts b/src/mappedUpdux.test.ts index ee5d2fc..274a313 100644 --- a/src/mappedUpdux.test.ts +++ b/src/mappedUpdux.test.ts @@ -1,28 +1,31 @@ import Updux from './updux'; import u from 'updeep'; +import tap from 'tap'; -const todo = new Updux({ +const todo: any = new Updux({ mutations: { - review: () => u({ reviewed: true}), - done: () => u({done: true}), + review: () => u({ reviewed: true }), + done: () => u({ done: true }), }, }); -const todos = new Updux({ +const todos: any = new Updux({ subduxes: { '*': todo }, }); todos.addMutation( - todo.actions.done, (id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))), true + todo.actions.done, + (id, action) => u.map(u.if(u.is('id', id), todo.upreducer(action))), + true ); -test( '* for mapping works', () => { +tap.test('* for mapping works', async t => { const reducer = todos.reducer; - let state = [ { id: 0 }, {id: 1 } ]; - state = reducer( state, todos.actions.review() ); - state = reducer( state, todos.actions.done(1) ); + let state = [{ id: 0 }, { id: 1 }]; + state = reducer(state, todos.actions.review()); + state = reducer(state, todos.actions.done(1)); - expect(state).toEqual([ + t.same(state, [ { id: 0, reviewed: true }, { id: 1, reviewed: true, done: true }, ]); diff --git a/src/middleware.test.ts b/src/middleware.test.ts index 9f725dc..5dc73ab 100644 --- a/src/middleware.test.ts +++ b/src/middleware.test.ts @@ -1,36 +1,38 @@ import u from 'updeep'; -import { action, payload } from 'ts-action'; +import { action } from 'ts-action'; +import tap from 'tap'; +import sinon from 'sinon'; -import Updux from '.'; +import Updux, { dux, subEffects } from '.'; import mwUpdux from './middleware_aux'; -test('simple effect', () => { - const tracer = jest.fn(); +tap.test('simple effect', async t => { + const tracer = sinon.fake(); const store = new Updux({ effects: { - foo: (api: any) => (next: any) => (action: any) => { + foo: () => (next: any) => (action: any) => { tracer(); next(action); }, }, }).createStore(); - expect(tracer).not.toHaveBeenCalled(); + t.ok(!tracer.called); store.dispatch({ type: 'bar' }); - expect(tracer).not.toHaveBeenCalled(); + t.ok(!tracer.called); - store.dispatch.foo(); + store.dispatch( (store as any).actions.foo() ); - expect(tracer).toHaveBeenCalled(); + t.ok(tracer.called); }); -test('effect and sub-effect', () => { - const tracer = jest.fn(); +tap.test('effect and sub-effect', async t => { + const tracer = sinon.fake(); - const tracerEffect = (signature: string) => (api: any) => (next: any) => ( + const tracerEffect = (signature: string) => () => (next: any) => ( action: any ) => { tracer(signature); @@ -42,87 +44,86 @@ test('effect and sub-effect', () => { foo: tracerEffect('root'), }, subduxes: { - zzz: { + zzz: dux({ effects: { foo: tracerEffect('child'), }, - }, + }), }, }).createStore(); - expect(tracer).not.toHaveBeenCalled(); + t.ok(!tracer.called); store.dispatch({ type: 'bar' }); - expect(tracer).not.toHaveBeenCalled(); + t.ok(!tracer.called); - store.dispatch.foo(); + store.dispatch( (store.actions as any).foo() ); - expect(tracer).toHaveBeenNthCalledWith(1, 'root'); - expect(tracer).toHaveBeenNthCalledWith(2, 'child'); + t.is( tracer.firstCall.lastArg, 'root' ); + t.is( tracer.secondCall.lastArg, 'child' ); }); -describe('"*" effect', () => { - test('from the constructor', () => { - const tracer = jest.fn(); +tap.test('"*" effect', async t => { + t.test('from the constructor', async t => { + const tracer = sinon.fake(); const store = new Updux({ effects: { - '*': api => next => action => { + '*': () => next => action => { tracer(); next(action); }, }, }).createStore(); - expect(tracer).not.toHaveBeenCalled(); + t.ok(!tracer.called); store.dispatch({ type: 'bar' }); + t.ok(tracer.called); - expect(tracer).toHaveBeenCalled(); }); - test('from addEffect', () => { - const tracer = jest.fn(); + t.test('from addEffect', async t => { + const tracer = sinon.fake(); const updux = new Updux({}); - updux.addEffect('*', api => next => action => { + updux.addEffect('*', () => next => action => { tracer(); next(action); }); - expect(tracer).not.toHaveBeenCalled(); + t.ok(!tracer.called); updux.createStore().dispatch({ type: 'bar' }); - expect(tracer).toHaveBeenCalled(); + t.ok(tracer.called); }); - test('action can be modified', () => { + t.test('action can be modified', async t => { const mw = mwUpdux.middleware; - const next = jest.fn(); + const next = sinon.fake(); mw({dispatch:{}} as any)(next as any)({type: 'bar'}); - expect(next).toHaveBeenCalled(); - - expect(next.mock.calls[0][0]).toMatchObject({meta: 'gotcha'}); + t.ok(next.called); + t.match( next.firstCall.args[0], {meta: 'gotcha' } ); }); }); -test('async effect', async () => { +tap.test('async effect', async t => { function timeout(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } - const tracer = jest.fn(); + const tracer = sinon.fake(); const store = new Updux({ effects: { - foo: api => next => async action => { + foo: () => next => async action => { next(action); await timeout(1000); tracer(); @@ -130,28 +131,26 @@ test('async effect', async () => { }, }).createStore(); - expect(tracer).not.toHaveBeenCalled(); + t.ok(!tracer.called); - store.dispatch.foo(); + store.dispatch( (store.actions as any).foo() ); - expect(tracer).not.toHaveBeenCalled(); + t.ok(!tracer.called); await timeout(1000); - expect(tracer).toHaveBeenCalled(); + t.ok(tracer.called); }); -test('getState is local', () => { +tap.test('getState is local', async t => { let childState; let rootState; - let rootFromChild; const child = new Updux({ initial: { alpha: 12 }, effects: { - doIt: ({ getState, getRootState }) => next => action => { + doIt: ({ getState }) => next => action => { childState = getState(); - rootFromChild = getRootState(); next(action); }, }, @@ -159,7 +158,7 @@ test('getState is local', () => { const root = new Updux({ initial: { beta: 24 }, - subduxes: { child }, + subduxes: { child: child.asDux }, effects: { doIt: ({ getState }) => next => action => { rootState = getState(); @@ -169,17 +168,13 @@ test('getState is local', () => { }); const store = root.createStore(); - store.dispatch.doIt(); + store.dispatch( (store.actions as any).doIt() ); - expect(rootState).toEqual({ beta: 24, child: { alpha: 12 } }); - expect(rootFromChild).toEqual({ beta: 24, child: { alpha: 12 } }); - expect(childState).toEqual({ alpha: 12 }); + t.match(rootState,{ beta: 24, child: { alpha: 12 } }); + t.match(childState,{ alpha: 12 }); }); -test('middleware as map', () => { - let childState; - let rootState; - let rootFromChild; +tap.test('middleware as map', async t => { const doIt = action('doIt', () => ({payload: ''})); @@ -200,12 +195,13 @@ test('middleware as map', () => { ], }); + const root = new Updux({ initial: { message: '' }, - subduxes: { child }, + subduxes: { child: child.asDux }, effects: [ [ - '^', + '*', () => next => action => { next( u({ payload: (p: string) => p + 'Pre' }, action) as any @@ -231,8 +227,9 @@ test('middleware as map', () => { ); }, ], + subEffects, [ - '$', + '*', () => next => action => { next( u({ payload: (p: string) => p + 'End' }, action) as any @@ -244,12 +241,13 @@ test('middleware as map', () => { }); const store = root.createStore(); - store.dispatch.doIt(''); + const actions: any = store.actions; + store.dispatch( actions.doIt('') ); - expect(store.getState()).toEqual({ message: 'PreRootAfterChildEnd' }); + t.same(store.getState(),{ message: 'PreRootAfterChildEnd' }); }); -test('generator', () => { +tap.test('generator', async t => { const updux = new Updux({ initial: 0, mutations: [['doIt', payload => () => payload]], @@ -267,13 +265,14 @@ test('generator', () => { }); const store1 = updux.createStore(); - store1.dispatch.doIt(); - expect(store1.getState()).toEqual(1); - store1.dispatch.doIt(); - expect(store1.getState()).toEqual(2); - updux.actions; + store1.dispatch( (store1 as any).actions.doIt() ); + + t.is(store1.getState(),1); + store1.dispatch( (store1 as any).actions.doIt() ); + + t.is(store1.getState(),2); const store2 = updux.createStore(); - store2.dispatch.doIt(); - expect(store2.getState()).toEqual(1); + store2.dispatch( (store2 as any).actions.doIt() ); + t.is(store2.getState(),1); }); diff --git a/src/middleware_aux.ts b/src/middleware_aux.ts index 47ab313..3fadbc3 100644 --- a/src/middleware_aux.ts +++ b/src/middleware_aux.ts @@ -1,12 +1,12 @@ -import Updux from '.'; +import Updux, {dux} from '.'; const updux = new Updux({ subduxes: { - foo: { initial: "banana" } + foo: dux({ initial: "banana" }) } }); -updux.addEffect('*', api => next => action => { +updux.addEffect('*', () => next => action => { next({...action, meta: "gotcha" }); }); diff --git a/src/mutations.test.ts b/src/mutations.test.ts index 3764f9e..2c57e9c 100644 --- a/src/mutations.test.ts +++ b/src/mutations.test.ts @@ -1,27 +1,88 @@ -import { action } from 'ts-action'; +import { action, empty } from 'ts-action'; +import Updux, { dux } from '.'; +import tap from 'tap'; -import Updux from "./updux"; +import u from 'updeep'; -describe("as array of arrays", () => { - const doIt = action("doIt"); +tap.test('as array of arrays', async t => { + const doIt = action('doIt'); - const updux = new Updux({ - initial: "", - mutations: [ - [doIt, () => () => "bingo"], - ["thisToo", () => () => "straight type"] - ] - }); + const updux = new Updux({ + initial: '', + actions: { doIt }, + mutations: [ + [doIt, () => () => 'bingo'], + ['thisToo', () => () => 'straight type'], + ], + }); - const store = updux.createStore(); + const store = updux.createStore(); - test("doIt", () => { - store.dispatch.doIt(); - expect(store.getState()).toEqual("bingo"); - }); + t.test('doIt', async t => { + store.dispatch( store.actions.doIt() ); + t.is(store.getState(),'bingo'); + }); - test("straight type", () => { - store.dispatch.thisToo(); - expect(store.getState()).toEqual("straight type"); - }); + t.test('straight type', async t => { + store.dispatch( (store.actions as any).thisToo() ); + t.is(store.getState(),'straight type'); + }); +}); + +tap.test('override', async t => { + const d = new Updux({ + initial: { alpha: [] }, + mutations: { + '*': (payload, action) => state => ({ + ...state, + alpha: [...state.alpha, action.type], + }), + }, + subduxes: { + subbie: dux({ + initial: 0, + mutations: { + foo: () => state => state + 1, + }, + }), + }, + }); + + const store = d.createStore(); + store.dispatch({ type: 'foo' }); + store.dispatch({ type: 'bar' }); + + t.pass(); +}); + +tap.test('coduxes and subduxes', async t => { + const foo = action('foo',empty()); + + const d = new Updux({ + initial: { + x: '', + }, + actions: { + foo + }, + mutations: [[foo, () => (u.updateIn as any)('x', x => x + 'm')]], + subduxes: { + x: dux({ + mutations: [[foo, () => x => x + 's']], + }), + }, + coduxes: [ + dux({ + mutations: [[foo, () => (u.updateIn as any)('x', x => x + 'c')]], + }), + ], + }); + + const store = d.createStore(); + + store.dispatch(d.actions.foo()); + + t.same(store.getState(),{ + x: 'scm', + }); }); diff --git a/src/selectors.test.ts b/src/selectors.test.ts index 29feabe..73ec7e9 100644 --- a/src/selectors.test.ts +++ b/src/selectors.test.ts @@ -1,16 +1,18 @@ -import Updux from '.'; +import { test } from 'tap'; +import Updux, { dux, coduxes, DuxState } from '.'; +import { expectType } from 'tsd'; -test('basic selectors', () => { - const updux = new Updux({ +test('basic selectors', async t => { + const updux = dux({ subduxes: { - bogeys: { + bogeys: dux({ selectors: { bogey: (bogeys: any) => (id: string) => bogeys[id], }, - }, + }), }, selectors: { - bogeys: ({ bogeys }: any) => bogeys, + bogeys: ({ bogeys }) => bogeys, }, }); @@ -21,19 +23,19 @@ test('basic selectors', () => { }, }; - expect(updux.selectors.bogeys(state)).toEqual({ foo: 1, bar: 2 }); - expect((updux.selectors.bogey(state) as any)('foo')).toEqual(1); + t.same(updux.selectors.bogeys(state), { foo: 1, bar: 2 }); + t.equal(updux.selectors.bogey(state)('foo'), 1); }); -test('available in the middleware', () => { - const updux = new Updux({ +test('available in the middleware', async t => { + const updux = dux({ subduxes: { - bogeys: { + bogeys: dux({ initial: { enkidu: 'foo' }, selectors: { bogey: (bogeys: any) => (id: string) => bogeys[id], }, - }, + }), }, effects: { doIt: ({ selectors: { bogey }, getState }) => next => action => { @@ -49,7 +51,85 @@ test('available in the middleware', () => { }); const store = updux.createStore(); - store.dispatch.doIt(); + store.dispatch(updux.actions.doIt()); - expect(store.getState()).toMatchObject({ payload: 'foo' }); + t.match(store.getState(), { payload: 'foo' }); +}); + +test('selector typescript', async t => { + const bar = dux({ + initial: { baz: 1 } as { baz: number }, + selectors: { + getBaz: (state: { baz: number }) => state.baz, + getStringBaz: state => `${state.baz}`, + getMultBaz: state => (mult: number) => state.baz * mult, + }, + }); + + expectType<{ + getBaz: Function; + getStringBaz: Function; + }>(bar.selectors); + + t.same(bar.selectors.getBaz(bar.initial), 1); + t.same(bar.selectors.getMultBaz({ baz: 3 })(2), 6); + + test('subduxes', async t => { + const foo = dux({ + subduxes: { bar }, + ...coduxes( dux({}) ), + selectors: { + getRoot: () => 'root' + } + }); + + expectType<{ + ({ bar: { baz: number } }): number; + }>(foo.selectors.getBaz); + + t.same(foo.selectors.getBaz(foo.initial), 1); + t.same(foo.selectors.getMultBaz({ bar: { baz: 3 } })(2), 6); + + t.ok( foo.selectors.getRoot ); + }); + + test('no root selector', async t => { + const foo = dux({ + subduxes: { + quux: dux({}), + bar: dux({ + selectors: { + getBaz: () => 'baz' + } + }) + } + }); + + t.ok(foo.selectors.getBaz); + }); +}); + +test('selector in mw', async () => { + const myDux = new Updux( + { + initial: { stuff: 12 }, + subduxes: { + bar: dux({ + initial: 'potato', + selectors: { getBar: () => 'meh' } + }) + }, + selectors: { + // TODO here we should auto-populate the state + getStuff: (state: {stuff: number}) => state.stuff + } + } + ); + + myDux.addEffect( '*', ({ + selectors, getState + }) => () => () => { + expectType>( getState() ); + expectType<(...args:any[]) => number>(selectors.getStuff); + }); }); diff --git a/src/sink.test.ts b/src/sink.test.ts index 0d8a942..91310a5 100644 --- a/src/sink.test.ts +++ b/src/sink.test.ts @@ -1,56 +1,57 @@ -import Updux from './updux'; +import Updux, { dux } from '.'; +import tap from 'tap'; +import { action } from 'ts-action'; -const foo = new Updux({ - initial: 0, - mutations: { - doIt: () => (state: number) => { - return state + 1; +const foo = dux({ + initial: 0, + actions: { + doIt: action('doIt'), + doTheThing: action('doTheThing'), }, - doTheThing: () => (state: number) => { - return state + 3; + mutations: { + doIt: () => (state: number) => { + return state + 1; + }, + doTheThing: () => (state: number) => { + return state + 3; + }, }, - }, }); -const bar = new Updux<{foo: number}>({ - subduxes: {foo}, +const bar: any = new Updux<{ foo: number }>({ + subduxes: { foo }, }); bar.addMutation( - foo.actions.doTheThing, - (_, action) => state => { - return { - ...state, - baz: bar.subduxUpreducer(action)(state), - }; - }, - true, + foo.actions.doTheThing, + (_, action) => state => { + return { + ...state, + baz: foo.upreducer(action)(state.foo), + }; + }, + true ); bar.addMutation( - foo.actions.doIt, - () => (state: any) => ({...state, bar: 'yay'}), - true, + foo.actions.doIt, + () => (state: any) => ({ ...state, bar: 'yay' }), + true ); -test('initial', () => { - expect(bar.initial).toEqual({foo: 0}); +tap.same(bar.initial, { foo: 0 }); + +tap.test('foo alone', t => { + t.is(foo.reducer(undefined, foo.actions.doIt()), 1); + t.end(); }); -test('foo alone', () => { - expect(foo.reducer(undefined, foo.actions.doIt())).toEqual(1); -}); +tap.test('sink mutations', t => { + t.same( + bar.reducer(undefined, bar.actions.doIt()), { + foo: 0, + bar: 'yay', + }); -test('sink mutations', () => { - expect(bar.reducer(undefined, bar.actions.doIt())).toEqual({ - foo: 0, - bar: 'yay', - }); -}); - -test('sink mutation and subduxUpreducer', () => { - expect(bar.reducer(undefined, bar.actions.doTheThing())).toEqual({ - foo: 0, - baz: {foo: 3}, - }); + t.end(); }); diff --git a/src/splat.test.ts b/src/splat.test.ts index c2543d2..3c2ac7c 100644 --- a/src/splat.test.ts +++ b/src/splat.test.ts @@ -1,63 +1,113 @@ -import Updux from '.'; +import tap from 'tap'; +import Updux, {dux} from '.'; import u from 'updeep'; +import { expectType } from 'tsd'; -const tracer = (chr:string) => u({ tracer: (s='') => s + chr }); +const tracer = (chr: string) => u({ tracer: (s = '') => s + chr }); -test( 'mutations, simple', () => { - const dux = new Updux({ +tap.test('mutations, simple', t => { + const dux : any = new Updux({ mutations: { foo: () => tracer('a'), '*': () => tracer('b'), }, }); - const store = dux.createStore(); + const store = dux.createStore(); - expect(store.getState()).toEqual({ tracer: 'b'}); + t.same(store.getState(),{ tracer: 'b' }); - store.dispatch.foo(); + store.dispatch( store.actions.foo() ); - expect(store.getState()).toEqual({ tracer: 'ba', }); + t.same(store.getState(),{ tracer: 'ba' }); store.dispatch({ type: 'bar' }); - expect(store.getState()).toEqual({ tracer: 'bab', }); + t.same(store.getState(),{ tracer: 'bab' }); + + t.end(); }); -test( 'with subduxes', () => { - const dux = new Updux({ +tap.test('with subduxes', t => { + const d = dux({ mutations: { foo: () => tracer('a'), '*': () => tracer('b'), - bar: () => ({bar}:any) => ({ bar, tracer: bar.tracer }) + bar: () => ({ bar }: any) => ({ bar, tracer: bar.tracer }), }, subduxes: { - bar: { + bar: dux({ mutations: { foo: () => tracer('d'), - '*': () => tracer('e'), + '*': () => tracer('e'), }, - }, + }), }, }); - const store = dux.createStore(); + const store = d.createStore(); - expect(store.getState()).toEqual({ + t.same(store.getState(),{ tracer: 'b', - bar: { tracer: 'e' } }); + bar: { tracer: 'e' }, + }); - store.dispatch.foo(); + store.dispatch( (store.actions as any).foo() ); - expect(store.getState()).toEqual({ + t.same(store.getState(),{ tracer: 'ba', - bar: { tracer: 'ed' } }); + bar: { tracer: 'ed' }, + }); - store.dispatch({type: 'bar'}); + store.dispatch({ type: 'bar' }); - expect(store.getState()).toEqual({ + t.same(store.getState(),{ tracer: 'ede', - bar: { tracer: 'ede' } }); + bar: { tracer: 'ede' }, + }); + t.end(); +}); + +tap.test( 'splat and state', async t => { + + const inner = dux({ initial: 3 }); + inner.initial; + + const arrayDux = dux({ + initial: [], + subduxes: { + '*': inner + } + }); + + expectType(arrayDux.initial) + t.same(arrayDux.initial,[]); + + const objDux = dux({ + initial: {}, + subduxes: { + '*': dux({initial: 3}) + } + }); + + expectType<{ + [key: string]: number + }>(objDux.initial); + + t.same(objDux.initial,{}); + +}); + +tap.test( 'multi-splat', async t => { + + dux({ + subduxes: { + foo: dux({ mutations: { '*': () => s => s }}), + bar: dux({ mutations: { '*': () => s => s }}), + } + }); + + t.pass(); }); diff --git a/src/test.ts b/src/test.ts index fef9a28..b351d6d 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,23 +1,27 @@ -import Updux from '.'; +import Updux, {dux} from '.'; +import u from 'updeep'; +import { action, payload } from 'ts-action'; +import tap from 'tap'; -test('actions from mutations', () => { +tap.test('actions from mutations', async t => { const { actions: {foo, bar}, - } = new Updux({ + } : any = new Updux({ mutations: { foo: () => (x:any) => x, }, }); - expect(foo()).toEqual({type: 'foo'}); - - expect(foo(true)).toEqual({type: 'foo', payload: true}); + t.match( foo(), { type: 'foo' } ); + t.same( foo(true), {type: 'foo', payload: true}); }); -test('reducer', () => { - const {actions, reducer} = new Updux({ +tap.test('reducer', async t => { + const inc = action('inc'); + const {actions, reducer} = dux({ initial: {counter: 1}, + actions: { inc }, mutations: { inc: () => ({counter}:{counter:number}) => ({counter: counter + 1}), }, @@ -25,24 +29,28 @@ test('reducer', () => { let state = reducer(undefined, {type:'noop'}); - expect(state).toEqual({counter: 1}); + t.same(state,{counter: 1}); state = reducer(state, actions.inc()); - expect(state).toEqual({counter: 2}); + t.same(state,{counter: 2}); }); -test( 'sub reducers', () => { - const foo = new Updux({ +tap.test( 'sub reducers', async t => { + const doAll = action('doAll', payload() ); + + const foo = dux({ initial: 1, + actions: { doAll }, mutations: { doFoo: () => (x:number) => x + 1, doAll: () => (x:number) => x + 10, }, }); - const bar = new Updux({ + const bar = dux({ initial: 'a', + actions: { doAll }, mutations: { doBar: () => (x:string) => x + 'a', doAll: () => (x:string) => x + 'b', @@ -55,34 +63,34 @@ test( 'sub reducers', () => { } }); - expect(initial).toEqual({ foo: 1, bar: 'a' }); + t.same(initial,{ foo: 1, bar: 'a' }); - expect(Object.keys(actions)).toHaveLength(3); + t.is(Object.keys(actions).length,3); let state = reducer(undefined,{type:'noop'}); - expect(state).toEqual({ foo: 1, bar: 'a' }); + t.same(state,{ foo: 1, bar: 'a' }); - state = reducer(state, actions.doFoo() ); + state = reducer(state, (actions as any).doFoo() ); - expect(state).toEqual({ foo: 2, bar: 'a' }); + t.same(state,{ foo: 2, bar: 'a' }); - state = reducer(state, actions.doBar() ); + state = reducer(state, (actions as any).doBar() ); - expect(state).toEqual({ foo: 2, bar: 'aa' }); + t.same(state,{ foo: 2, bar: 'aa' }); - state = reducer(state, actions.doAll() ); + state = reducer(state, (actions as any).doAll() ); - expect(state).toEqual({ foo: 12, bar: 'aab' }); + t.same(state,{ foo: 12, bar: 'aab' }); }); -test('precedence between root and sub-reducers', () => { +tap.test('precedence between root and sub-reducers', async t => { const { initial, reducer, actions, - } = new Updux({ + } = dux({ initial: { foo: { bar: 4 }, }, @@ -95,7 +103,7 @@ test('precedence between root and sub-reducers', () => { } }, subduxes: { - foo: { + foo: dux({ initial: { bar: 2, quux: 3, @@ -103,15 +111,16 @@ test('precedence between root and sub-reducers', () => { mutations: { inc: () => (state:any) => ({...state, bar: state.bar + 1 }) }, - }, + }), } }); - expect(initial).toEqual({ + // quick fix until https://github.com/facebook/jest/issues/9531 + t.same(initial,{ foo: { bar: 4, quux: 3 } }); - expect( reducer(undefined,actions.inc() ) ).toEqual({ + t.same( reducer(undefined,(actions as any).inc() ) ,{ foo: { bar: 5, quux: 3 }, surprise: 5 }); @@ -121,67 +130,73 @@ function timeout(ms:number) { return new Promise(resolve => setTimeout(resolve, ms)); } -test( 'middleware', async () => { +tap.test( 'middleware', async t => { const { middleware, - createStore - } = new Updux({ - initial: "", + createStore, + actions, + } = dux({ + initial: { result: [] }, mutations: { - inc: (addition:number) => (state:number) => state + addition, - doEeet: () => (state:number) => { - return state + 'Z'; - }, + inc: (addition:number) => u({ result: r => [ ...r, addition ]}), + doEeet: () => u({ result: r => [ ...r, 'Z' ]}), }, effects: { doEeet: api => next => async action => { - api.dispatch.inc('a'); + api.dispatch( api.actions.inc('a') ); next(action); await timeout(1000); - api.dispatch.inc('c'); + api.dispatch( api.actions.inc('c') ); } }, subduxes: { - foo: { + foo: dux({ effects: { doEeet: (api:any) => ( next:any ) => ( action: any ) => { api.dispatch({ type: 'inc', payload: 'b'}); next(action); } } - }, + }), } }); - const store = createStore(); + const store :any = createStore(); - store.dispatch.doEeet(); + store.dispatch( (actions as any).doEeet() ); - expect(store.getState()).toEqual( 'abZ' ); + t.is(store.getState().result.join(''),'abZ' ); await timeout(1000); - expect(store.getState()).toEqual( 'abZc' ); + t.is(store.getState().result.join(''), 'abZc' ); }); -test( "subduxes and mutations", () => { - const foo = new Updux({ mutations: { +tap.test( "subduxes and mutations", async t => { + const quux = action('quux'); + + const foo = dux({ + actions: { quux }, + mutations: { quux: () => () => 'x', blart: () => () => 'a', }}); - const bar = new Updux({ mutations: { + const bar = dux({ + actions: { quux }, + mutations: { quux: () => () => 'y' }}); - const baz = new Updux({ + const baz = dux({ + actions: { quux }, mutations: { quux: () => (state:any) => ({...state, "baz": "z" }) }, subduxes: { foo, bar } }); - let state = baz.reducer(undefined, baz.actions.quux() ); + let state = baz.reducer(undefined, (baz.actions as any).quux() ); - expect(state).toEqual({ + t.same(state,{ foo: "x", bar: "y", baz: "z", diff --git a/src/tutorial/todos.test.ts b/src/tutorial/todos.test.ts new file mode 100644 index 0000000..02aad86 --- /dev/null +++ b/src/tutorial/todos.test.ts @@ -0,0 +1,34 @@ +import tap from 'tap'; + +import Updux from '..'; +import {action, payload} from 'ts-action'; + +type Todo = { + id: number; + description: string; + done: boolean; +}; + +type TodoStore = { + next_id: number; + todos: Todo[]; +}; + +const todosUpdux = new Updux({ + initial: { + next_id: 1, + todos: [], + } as TodoStore +}); + + +tap.test('initial state', async t => { + const store = todosUpdux.createStore(); + + t.like(store.getState(), { + next_id: 1, + todos: [], + }, 'initial state' + ); +}); + diff --git a/src/types.ts b/src/types.ts index 98e2bcf..086e927 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,209 +1,362 @@ -import { Dispatch, Middleware } from "redux"; +import { ActionCreator } from 'ts-action'; type MaybePayload

= P extends object | string | boolean | number - ? { - payload: P; - } - : { payload?: P }; + ? { + payload: P; + } + : { payload?: P }; export type Action = { - type: T; + type: T; } & MaybePayload

; export type Dictionary = { [key: string]: T }; export type Mutation = ( - payload: A["payload"], - action: A + payload: A['payload'], + action: A ) => (state: S) => S; export type ActionPayloadGenerator = (...args: any[]) => any; export type MutationEntry = [ - ActionCreator | string, - Mutation>, - boolean? + ActionCreator | string, + Mutation>, + boolean? ]; -export type ActionCreator = { - type: T; - _genericAction?: boolean; -} & ((...args: any[]) => Action); +export type GenericActions = Dictionary< + ActionCreator { type: string }> +>; -export type UpduxDispatch = Dispatch & Dictionary; +export type UnionToIntersection = (U extends any + ? (k: U) => void + : never) extends (k: infer I) => void + ? I + : never; + +export type StateOf = D extends { initial: infer I } ? I : unknown; + +export type DuxStateCoduxes = C extends Array ? UnionToIntersection>: unknown +export type DuxStateSubduxes = + C extends { '*': infer I } ? { + [ key: string ]: StateOf, + [ index: number ]: StateOf, +} : + C extends object ? { [ K in keyof C]: StateOf}: unknown; + +type DuxStateGlobSub = S extends { '*': infer I } ? StateOf : unknown; + +type LocalDuxState = S extends never[] ? unknown[] : S; + +/** @ignore */ +type AggDuxState2 = ( + L extends never[] ? Array> : L & DuxStateSubduxes ) & DuxStateCoduxes; + +/** @ignore */ +export type AggDuxState = unknown extends O ? + AggDuxState2 : O + + +type SelectorsOf = C extends { selectors: infer S } ? S : unknown; + +/** @ignore */ +export type DuxSelectorsSubduxes = C extends object ? UnionToIntersection> : unknown; + +/** @ignore */ +export type DuxSelectorsCoduxes = C extends Array ? UnionToIntersection> : unknown; + +type MaybeReturnType = X extends (...args: any) => any ? ReturnType : unknown; + +type RebaseSelector = { + [ K in keyof X]: (state: S) => MaybeReturnType< X[K] > +} + +type ActionsOf = C extends { actions: infer A } ? A : {}; + +type DuxActionsSubduxes = C extends object ? ActionsOf : unknown; +export type DuxActionsCoduxes = C extends Array ? UnionToIntersection> : {}; + +type ItemsOf = C extends object? C[keyof C] : unknown + +export type DuxActions = A extends object ? A: ( + UnionToIntersection|ItemsOf>> + ); + +export type DuxSelectors = unknown extends X ? ( + RebaseSelector & + DuxSelectorsSubduxes > +): X + +export type Dux< + S = unknown, + A = unknown, + X = unknown, + C = unknown, +> = { + subduxes: Dictionary, + coduxes: Dux[], + initial: AggDuxState, + actions: A, +} /** - * Configuration object given to Updux's constructor. - * @typeparam S Type of the Updux's state. Defaults to `any`. - */ -export type UpduxConfig = { - /** - * The default initial state of the reducer. Can be anything your - * heart desires. - */ - initial?: S; +* Configuration object given to Updux's constructor. +* +* #### arguments +* +* ##### initial +* +* Default initial state of the reducer. If applicable, is merged with +* the subduxes initial states, with the parent having precedence. +* +* If not provided, defaults to an empty object. +* +* ##### actions +* +* [Actions](/concepts/Actions) used by the updux. +* +* ```js +* import { dux } from 'updux'; +* import { action, payload } from 'ts-action'; +* +* const bar = action('BAR', payload()); +* const foo = action('FOO'); +* +* const myDux = dux({ +* actions: { +* bar +* }, +* mutations: [ +* [ foo, () => state => state ] +* ] +* }); +* +* myDux.actions.foo({ x: 1, y: 2 }); // => { type: foo, x:1, y:2 } +* myDux.actions.bar(2); // => { type: bar, payload: 2 } +* ``` +* +* New actions used directly in mutations and effects will be added to the +* dux actions -- that is, they will be accessible via `dux.actions` -- but will +* not appear as part of its Typescript type. +* +* ##### selectors +* +* Dictionary of selectors for the current updux. The updux also +* inherit its subduxes' selectors. +* +* The selectors are available via the class' getter. +* +* ##### mutations +* +* mutations: [ +* [ action, mutation, isSink ], +* ... +* ] +* +* or +* +* mutations: { +* action: mutation, +* ... +* } +* +* List of mutations for assign to the dux. If you want Typescript goodness, you +* probably want to use `addMutation()` instead. +* +* In its generic array-of-array form, +* each mutation tuple contains: the action, the mutation, +* and boolean indicating if this is a sink mutation. +* +* The action can be an action creator function or a string. If it's a string, it's considered to be the +* action type and a generic `action( actionName, payload() )` creator will be +* generated for it. If an action is not already defined in the `actions` +* parameter, it'll be automatically added. +* +* The pseudo-action type `*` can be used to match any action not explicitly matched by other mutations. +* +* ```js +* const todosUpdux = updux({ +* mutations: { +* add: todo => state => [ ...state, todo ], +* done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ), +* '*' (payload,action) => state => { +* console.warn( "unexpected action ", action.type ); +* return state; +* }, +* } +* }); +* ``` +* +* The signature of the mutations is `(payload,action) => state => newState`. +* It is designed to play well with `Updeep` (and [Immer](https://immerjs.github.io/immer/docs/introduction)). This way, instead of doing +* +* ```js +* mutation: { +* renameTodo: newName => state => { ...state, name: newName } +* } +* ``` +* +* we can do +* +* ```js +* mutation: { +* renameTodo: newName => u({ name: newName }) +* } +* ``` +* +* The final argument is the optional boolean `isSink`. If it is true, it'll +* prevent subduxes' mutations on the same action. It defaults to `false`. +* +* The object version of the argument can be used as a shortcut when all actions +* are strings. In that case, `isSink` is `false` for all mutations. +* +* ##### groomMutations +* +* Function that can be provided to alter all local mutations of the updux +* (the mutations of subduxes are left untouched). +* +* Can be used, for example, for Immer integration: +* +* ```js +* import Updux from 'updux'; +* import { produce } from 'Immer'; +* +* const updux = new Updux({ +* initial: { counter: 0 }, +* groomMutations: mutation => (...args) => produce( mutation(...args) ), +* mutations: { +* add: (inc=1) => draft => draft.counter += inc +* } +* }); +* ``` +* +* Or perhaps for debugging: +* +* ```js +* import Updux from 'updux'; +* +* const updux = new Updux({ +* initial: { counter: 0 }, +* groomMutations: mutation => (...args) => state => { +* console.log( "got action ", args[1] ); +* return mutation(...args)(state); +* } +* }); +* ``` +* ##### subduxes +* +* Object mapping slices of the state to sub-upduxes. In addition to creating +* sub-reducers for those slices, it'll make the parend updux inherit all the +* actions and middleware from its subduxes. +* +* For example, if in plain Redux you would do +* +* ```js +* import { combineReducers } from 'redux'; +* import todosReducer from './todos'; +* import statisticsReducer from './statistics'; +* +* const rootReducer = combineReducers({ +* todos: todosReducer, +* stats: statisticsReducer, +* }); +* ``` +* +* then with Updux you'd do +* +* ```js +* import { updux } from 'updux'; +* import todos from './todos'; +* import statistics from './statistics'; +* +* const rootUpdux = updux({ +* subduxes: { +* todos, +* statistics +* } +* }); +* ``` +* +* ##### effects +* +* Array of arrays or plain object defining asynchronous actions and side-effects triggered by actions. +* The effects themselves are Redux middleware, with the `dispatch` +* property of the first argument augmented with all the available actions. +* +* ``` +* updux({ +* effects: { +* fetch: ({dispatch}) => next => async (action) => { +* next(action); +* +* let result = await fetch(action.payload.url).then( result => result.json() ); +* dispatch.fetchSuccess(result); +* } +* } +* }); +* ``` +* +* @example +* +* ``` +* import Updux from 'updux'; +* import { actions, payload } from 'ts-action'; +* import u from 'updeep'; +* +* const todoUpdux = new Updux({ +* initial: { +* done: false, +* note: "", +* }, +* actions: { +* finish: action('FINISH', payload()), +* edit: action('EDIT', payload()), +* }, +* mutations: [ +* [ edit, note => u({note}) ] +* ], +* selectors: { +* getNote: state => state.note +* }, +* groomMutations: mutation => transform(mutation), +* subduxes: { +* foo +* }, +* effects: { +* finish: () => next => action => { +* console.log( "Woo! one more bites the dust" ); +* } +* } +* }) +* ``` +*/ +export type UpduxConfig = Partial<{ + initial: unknown, /** foo */ + subduxes: Dictionary, + coduxes: Dux[], + actions: Dictionary, + selectors: Dictionary, + mutations: any, + groomMutations: (m: Mutation) => Mutation, + effects: any, +}>; - /** - * Object mapping slices of the state to sub-upduxes. - * - * For example, if in plain Redux you would do - * - * ``` - * import { combineReducers } from 'redux'; - * import todosReducer from './todos'; - * import statisticsReducer from './statistics'; - * - * const rootReducer = combineReducers({ - * todos: todosReducer, - * stats: statisticsReducer, - * }); - * ``` - * - * then with Updux you'd do - * - * ``` - * import { updux } from 'updux'; - * import todos from './todos'; - * import statistics from './statistics'; - * - * const rootUpdux = updux({ - * subduxes: { - * todos, statistics - * } - * }); - */ - subduxes?: {}; - - /** - * Generic action creations are automatically created from the mutations and effects, but you can - * also define custom action creator here. The object's values are function that - * transform the arguments of the creator to the action's payload. - * - * ``` - * const { actions } = updox({ - * mutations: { - * foo: () => state => state, - * } - * actions: { - * bar: (x,y) => ({x,y}) - * } - * }); - * - * actions.foo({ x: 1, y: 2 }); // => { type: foo, payload: { x:1, y:2 } } - * actions.bar(1,2); // => { type: bar, payload: { x:1, y:2 } } - * ``` - */ - - actions?: { - [type: string]: ActionCreator; - }; - - selectors?: Dictionary; - - /** - * Object mapping actions to the associated state mutation. - * - * For example, in `Redux` you'd do - * - * ``` - * function todosReducer(state=[],action) { - * - * switch(action.type) { - * case 'ADD': return [ ...state, action.payload ]; - * - * case 'DONE': return state.map( todo => todo.id === action.payload - * ? { ...todo, done: true } : todo ) - * - * default: return state; - * } - * } - * ``` - * - * With Updux: - * - * ``` - * const todosUpdux = updux({ - * mutations: { - * add: todo => state => [ ...state, todo ], - * done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ) - * } - * }); - * ``` - * - * The signature of the mutations is `(payload,action) => state => newState`. - * It is designed to play well with `Updeep` (and [Immer](https://immerjs.github.io/immer/docs/introduction)). This way, instead of doing - * - * ``` - * mutation: { - * renameTodo: newName => state => { ...state, name: newName } - * } - * ``` - * - * we can do - * - * ``` - * mutation: { - * renameTodo: newName => u({ name: newName }) - * } - * ``` - * - * Also, the special key `*` can be used to match any - * action not explicitly matched by other mutations. - * - * ``` - * const todosUpdux = updux({ - * mutations: { - * add: todo => state => [ ...state, todo ], - * done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ), - * '*' (payload,action) => state => { - * console.warn( "unexpected action ", action.type ); - * return state; - * }, - * } - * }); - */ - mutations?: { [actionType: string]: Mutation } | MutationEntry[]; - - groomMutations?: (m: Mutation) => Mutation; - - /** - * Plain object defining asynchronous actions and side-effects triggered by actions. - * The effects themselves are Redux middleware, expect with the `dispatch` - * property of the first argument augmented with all the available actions. - * - * ``` - * updux({ - * effects: { - * fetch: ({dispatch}) => next => async (action) => { - * next(action); - * - * let result = await fetch(action.payload.url).then( result => result.json() ); - * dispatch.fetchSuccess(result); - * } - * } - * }); - * ``` - * - */ - effects?: Dictionary> | EffectEntry[]; -}; - -export type EffectEntry = [ - ActionCreator | string, - UpduxMiddleware, - boolean? -]; export type Upreducer = (action: Action) => (state: S) => S; -export interface UpduxMiddlewareAPI { - dispatch: UpduxDispatch; - getState(): any; - getRootState(): S; - selectors: Dictionary; +/** @ignore */ +export interface UpduxMiddlewareAPI> { + dispatch: Function; + getState(): S; + selectors: X; + actions: Dictionary; } -export type UpduxMiddleware = ( - api: UpduxMiddlewareAPI -) => (next: UpduxDispatch) => (action: Action) => any; +export type UpduxMiddleware,A = Action> = ( + api: UpduxMiddlewareAPI +) => (next: Function) => (action: A) => any; -export type Selector = (state:S) => any; +export type Selector = (state: S) => unknown; + +export type DuxState = D extends { initial: infer S } ? S : unknown; diff --git a/src/updux.ts b/src/updux.ts index 5e3cfa8..383ad6e 100644 --- a/src/updux.ts +++ b/src/updux.ts @@ -1,259 +1,522 @@ -import fp from "lodash/fp"; -import u from "updeep"; -import { action, payload } from 'ts-action'; +import fp from 'lodash/fp'; +import { action, payload, ActionCreator, ActionType } from 'ts-action'; +import {AnyAction} from 'redux'; -import buildActions from "./buildActions"; -import buildInitial from "./buildInitial"; -import buildMutations from "./buildMutations"; +import buildInitial from './buildInitial'; +import buildMutations from './buildMutations'; -import buildCreateStore from "./buildCreateStore"; -import buildMiddleware from "./buildMiddleware"; -import buildUpreducer from "./buildUpreducer"; +import buildCreateStore from './buildCreateStore'; +import buildMiddleware, { effectToMw, Effect } from './buildMiddleware'; +import buildUpreducer from './buildUpreducer'; import { - UpduxConfig, - Dictionary, - Action, - ActionCreator, - Mutation, - Upreducer, - UpduxDispatch, - UpduxMiddleware, - MutationEntry, - EffectEntry, - Selector -} from "./types"; + UpduxConfig, + Dictionary, + Action, + Mutation, + Upreducer, + UpduxMiddleware, + Selector, + Dux, + UnionToIntersection, + AggDuxState, + DuxSelectors, + DuxActions, +} from './types'; -import { Middleware, Store, PreloadedState } from "redux"; -import buildSelectors from "./buildSelectors"; +import { Store, PreloadedState } from 'redux'; +import buildSelectors from './buildSelectors'; + +type Merge = UnionToIntersection; + +type ActionsOf = U extends Updux ? U['actions'] : {}; + +//| ActionsOf[keyof SubduxesOf]> +export type UpduxActions = U extends Updux + ? UnionToIntersection< + UpduxLocalActions | ActionsOf[keyof CoduxesOf]> + > + : {}; + +export type UpduxLocalActions = S extends Updux + ? {} + : S extends Updux + ? A + : {}; +export type CoduxesOf = U extends Updux ? S : []; type StoreWithDispatchActions< - S = any, - Actions = { [action: string]: (...args: any) => Action } + S = any, + Actions = { [action: string]: (...args: any) => Action } > = Store & { - dispatch: { [type in keyof Actions]: (...args: any) => void }; + dispatch: { [type in keyof Actions]: (...args: any) => void }; }; -export type Dux = Pick< - Updux, - | "subduxes" - | "actions" - | "initial" - | "mutations" - | "reducer" - | "middleware" - | "createStore" - | "upreducer" ->; +/** + * @public + * `Updux` is a way to minimize and simplify the boilerplate associated with the + * creation of a `Redux` store. It takes a shorthand configuration + * object, and generates the appropriate reducer, actions, middleware, etc. + * In true `Redux`-like fashion, upduxes can be made of sub-upduxes (`subduxes` for short) for different slices of the root state. + */ +export class Updux< + S = unknown, + A = null, + X = unknown, + C extends UpduxConfig = {} + > { + subduxes: Dictionary; + coduxes: Dux[]; -export class Updux { - subduxes: Dictionary = {}; + private localSelectors: Dictionary = {}; - private local_selectors: Dictionary> = {}; + private localInitial: unknown; - initial: S; + groomMutations: (mutation: Mutation) => Mutation; - groomMutations: (mutation: Mutation) => Mutation; + private localEffects: Effect[] = []; + private localActions: Dictionary = {}; - private localEffects: EffectEntry[] = []; + private localMutations: Dictionary< + Mutation | [Mutation, boolean | undefined] + > = {}; - private localActions: Dictionary = {}; + get initial(): AggDuxState { + return buildInitial( + this.localInitial, + this.coduxes.map(({ initial }) => initial), + fp.mapValues('initial', this.subduxes) + ) as any; + } - private localMutations: Dictionary< - Mutation | [Mutation, boolean | undefined] - > = {}; + /** + * @param config an [[UpduxConfig]] plain object + * + */ + constructor(config: C = {} as C) { + this.localInitial = config.initial ?? {}; + this.localSelectors = config.selectors ?? {}; + this.coduxes = config.coduxes ?? []; + this.subduxes = config.subduxes ?? {}; - constructor(config: UpduxConfig = {}) { - this.groomMutations = config.groomMutations || ((x: Mutation) => x); + Object.entries(config.actions ?? {}).forEach((args) => + (this.addAction as any)(...args) + ); - const selectors = fp.getOr( {}, 'selectors', config ) as Dictionary; - Object.entries(selectors).forEach( ([name,sel]: [string,Function]) => this.addSelector(name,sel as Selector) ); + this.coduxes.forEach((c: any) => + Object.entries(c.actions).forEach((args) => + (this.addAction as any)(...args) + ) + ); + Object.values(this.subduxes).forEach((c: any) => { + Object.entries(c.actions).forEach((args) => { + (this.addAction as any)(...args); + }); + }); - Object.entries( fp.mapValues((value: UpduxConfig | Updux) => - fp.isPlainObject(value) ? new Updux(value as any) : value - )(fp.getOr({}, "subduxes", config))).forEach( - ([slice,sub]) => this.subduxes[slice] = sub as any - ); + this.groomMutations = + config.groomMutations ?? ((x: Mutation) => x); - const actions = fp.getOr({}, "actions", config); - Object.entries(actions).forEach(([type, p]: [string, any]): any => - this.addAction( - (p as any).type ? p : action(type, p) - ) - ); + let effects = config.effects ?? []; - let effects = fp.getOr([], "effects", config); - if (!Array.isArray(effects)) { - effects = Object.entries(effects); - } - effects.forEach(effect => this.addEffect(...effect)); + if (!Array.isArray(effects)) { + effects = (Object.entries(effects) as unknown) as Effect[]; + } + effects.forEach((effect) => (this.addEffect as any)(...effect)); - this.initial = buildInitial( - config.initial, - fp.mapValues(({ initial }) => initial)(this.subduxes) - ); + let mutations = config.mutations ?? []; - let mutations = fp.getOr([], "mutations", config); - if (!Array.isArray(mutations)) { - mutations = fp.toPairs(mutations); - } + if (!Array.isArray(mutations)) { + mutations = fp.toPairs(mutations); + } - mutations.forEach(args => (this.addMutation as any)(...args)); - } + mutations.forEach((args) => (this.addMutation as any)(...args)); - get middleware(): UpduxMiddleware { - return buildMiddleware(this._middlewareEntries, this.actions); - } + /* - get actions(): Dictionary { - return buildActions([ - ...(Object.entries(this.localActions) as any), - ...(fp.flatten( - Object.values(this.subduxes).map(({ actions }: Updux) => - Object.entries(actions) - ) - ) as any), - , - ]); - } + Object.entries(selectors).forEach(([name, sel]: [string, Function]) => + this.addSelector(name, sel as Selector) + ); - get upreducer(): Upreducer { - return buildUpreducer(this.initial, this.mutations); - } + Object.entries( + fp.mapValues((value: UpduxConfig | Updux) => + fp.isPlainObject(value) ? new Updux(value as any) : value + )(fp.getOr({}, 'subduxes', config)) + ).forEach(([slice, sub]) => (this.subduxes[slice] = sub as any)); - get reducer(): (state: S | undefined, action: Action) => S { - return (state, action) => this.upreducer(action)(state as S); - } + const actions = fp.getOr({}, 'actions', config); + Object.entries(actions as any).forEach(([type, p]: [string, any]): any => + this.addAction((p as any).type ? p : action(type, p)) + ); - get mutations(): Dictionary> { - return buildMutations(this.localMutations, this.subduxes); - } + */ + } - get subduxUpreducer() { - return buildUpreducer(this.initial, buildMutations({}, this.subduxes)); - } + /** + * Array of middlewares aggregating all the effects defined in the + * updux and its subduxes. Effects of the updux itself are + * done before the subduxes effects. + * Note that `getState` will always return the state of the + * local updux. + * + * @example + * + * ``` + * const middleware = updux.middleware; + * ``` + */ + get middleware(): UpduxMiddleware< + AggDuxState, + DuxSelectors, X, C> + > { + const selectors = this.selectors; + const actions = this.actions; + return buildMiddleware( + this.localEffects.map((effect) => + effectToMw(effect, actions as any, selectors as any) + ), + (this.coduxes as any).map(fp.get('middleware')) as any, + fp.mapValues('middleware', this.subduxes) + ) as any; + } - get createStore(): () => StoreWithDispatchActions { - const actions = this.actions; + /** + * Action creators for all actions defined or used in the actions, mutations, effects and subduxes + * of the updux config. + * + * Non-custom action creators defined in `actions` have the signature `(payload={},meta={}) => ({type, + * payload,meta})` (with the extra sugar that if `meta` or `payload` are not + * specified, that key won't be present in the produced action). + * + * The same action creator can be included + * in multiple subduxes. However, if two different creators + * are included for the same action, an error will be thrown. + * + * @example + * + * ``` + * const actions = updux.actions; + * ``` + */ + get actions(): DuxActions { + // UpduxActions> { + return this.localActions as any; + } - return buildCreateStore( - this.reducer, - this.initial as PreloadedState, - this.middleware as any, - actions - ) as () => StoreWithDispatchActions; - } + get upreducer(): Upreducer { + return buildUpreducer( + this.initial, + this.mutations as any + ) as any; + } - get asDux(): Dux { - return { - createStore: this.createStore, - upreducer: this.upreducer, - subduxes: this.subduxes, - middleware: this.middleware, - actions: this.actions, - reducer: this.reducer, - mutations: this.mutations, - initial: this.initial - }; - } + /** + * A Redux reducer generated using the computed initial state and + * mutations. + */ + get reducer(): (state: S | undefined, action: Action) => S { + return (state, action) => this.upreducer(action)(state as S); + } - addMutation( - creator: A, - mutation: Mutation infer R ? R : never>, - isSink?: boolean - ) - addMutation( - creator: string, - mutation: Mutation, - isSink?: boolean - ) - addMutation( - creator, - mutation, - isSink - ) - { - let c = this.addAction(creator); + /** + * Merge of the updux and subduxes mutations. If an action triggers + * mutations in both the main updux and its subduxes, the subduxes + * mutations will be performed first. + */ + get mutations(): Dictionary> { + return buildMutations( + this.localMutations, + fp.mapValues('upreducer', this.subduxes as any), + fp.map('upreducer', this.coduxes as any) + ); + } - this.localMutations[c.type] = [ - this.groomMutations(mutation as any) as Mutation, - isSink - ]; - } + /** + * Returns the upreducer made of the merge of all sudbuxes reducers, without + * the local mutations. Useful, for example, for sink mutations. + * + * @example + * + * ``` + * import todo from './todo'; // updux for a single todo + * import Updux from 'updux'; + * import u from 'updeep'; + * + * const todos = new Updux({ initial: [], subduxes: { '*': todo } }); + * todos.addMutation( + * todo.actions.done, + * ({todo_id},action) => u.map( u.if( u.is('id',todo_id) ), todos.subduxUpreducer(action) ) + * true + * ); + * ``` + * + * + * + */ + get subduxUpreducer() { + return buildUpreducer( + this.initial, + buildMutations({}, this.subduxes) + ); + } - addEffect( - creator: ActionCreator | string, - middleware: UpduxMiddleware, - isGenerator: boolean = false - ) { - const c = this.addAction(creator); - this.localEffects.push([c.type, middleware, isGenerator]); - } + /** + * Returns a `createStore` function that takes two argument: + * `initial` and `injectEnhancer`. `initial` is a custom + * initial state for the store, and `injectEnhancer` is a function + * taking in the middleware built by the updux object and allowing + * you to wrap it in any enhancer you want. + * + * @example + * + * ``` + * const createStore = updux.createStore; + * + * const store = createStore(initial); + * ``` + * + * + * + */ + get createStore() { + return buildCreateStore, DuxActions>( + this.reducer as any, + this.middleware as any, + this.actions + ); + } - // can be - //addAction( actionCreator ) - // addAction( 'foo', transform ) - addAction(theaction: string, transform?: any): ActionCreator - addAction(theaction: string|ActionCreator, transform?: never): ActionCreator - addAction(theaction: any,transform:any) { - if (typeof theaction === "string") { - if(transform !== undefined ) { - theaction = action(theaction,transform); - } - else { - theaction = this.actions[theaction] || action(theaction,payload()) - } - } + /** + * Returns a ducks-like + * plain object holding the reducer from the Updux object and all + * its trimmings. + * + * @example + * + * ``` + * const { + * createStore, + * upreducer, + * subduxes, + * coduxes, + * middleware, + * actions, + * reducer, + * mutations, + * initial, + * selectors, + * } = myUpdux.asDux; + * ``` + * + * + * + * + */ + get asDux() { + return { + createStore: this.createStore, + upreducer: this.upreducer, + subduxes: this.subduxes, + coduxes: this.coduxes, + middleware: this.middleware, + actions: this.actions, + reducer: this.reducer, + mutations: this.mutations, + initial: this.initial, + selectors: this.selectors, + }; + } - const already = this.actions[theaction.type]; - if( already ) { - if ( already !== theaction ) { - throw new Error(`action ${theaction.type} already exists`) - } - return already; - } + /** + * Adds a mutation and its associated action to the updux. + * + * @param isSink - If `true`, disables the subduxes mutations for this action. To + * conditionally run the subduxes mutations, check out [[subduxUpreducer]]. Defaults to `false`. + * + * @remarks + * + * If a local mutation was already associated to the action, + * it will be replaced by the new one. + * + * + * @example + * + * ```js + * updux.addMutation( + * action('ADD', payload() ), + * inc => state => state + in + * ); + * ``` + */ + addMutation( + creator: A, + mutation: Mutation>, + isSink?: boolean + ); + addMutation( + creator: string, + mutation: Mutation, + isSink?: boolean + ); + addMutation( + creator, + mutation, + isSink + ) { + const c = this.addAction(creator); - return this.localActions[theaction.type] = theaction; - } + this.localMutations[c.type] = [ + this.groomMutations(mutation as any) as Mutation, + isSink, + ]; + } - get _middlewareEntries() { - const groupByOrder = (mws: any) => - fp.groupBy( - ([a,b, actionType]: any) => - ["^", "$"].includes(actionType) ? actionType : "middle", - mws - ); + addEffect( + creator: AC, + middleware: UpduxMiddleware< + AggDuxState, + DuxSelectors, X, C>, + ReturnType + >, + isGenerator?: boolean + ); + addEffect( + creator: string, + middleware: UpduxMiddleware< + AggDuxState, + DuxSelectors, X, C> + >, + isGenerator?: boolean + ); + addEffect(creator, middleware, isGenerator = false) { + const c = this.addAction(creator); + this.localEffects.push([c.type, middleware, isGenerator] as any); + } - let subs = fp.flow([ - fp.toPairs, - fp.map(([slice, updux]) => - updux._middlewareEntries.map(([u, ps, ...args]: any) => [u,[slice, ...ps], ...args]) - ), - fp.flatten, - groupByOrder - ])(this.subduxes); + // can be + //addAction( actionCreator ) + // addAction( 'foo', transform ) + /** + * Adds an action to the updux. It can take an already defined action + * creator, or any arguments that can be passed to `actionCreator`. + * @example + * ``` + * const action = updux.addAction( name, ...creatorArgs ); + * const action = updux.addAction( otherActionCreator ); + * ``` + * @example + * ``` + * import {actionCreator, Updux} from 'updux'; + * + * const updux = new Updux(); + * + * const foo = updux.addAction('foo'); + * const bar = updux.addAction( 'bar', (x) => ({stuff: x+1}) ); + * + * const baz = actionCreator( 'baz' ); + * + * foo({ a: 1}); // => { type: 'foo', payload: { a: 1 } } + * bar(2); // => { type: 'bar', payload: { stuff: 3 } } + * baz(); // => { type: 'baz', payload: undefined } + * ``` + */ + addAction( + theaction: string, + transform?: any + ): ActionCreator; + addAction( + theaction: string | ActionCreator, + transform?: never + ): ActionCreator; + addAction(actionIn: any, transform: any) { + let name: string; + let creator: ActionCreator; - let local = groupByOrder(this.localEffects.map(x => [this,[], ...x])); + if (typeof actionIn === 'string') { + name = actionIn; - return fp.flatten( - [ - local["^"], - subs["^"], - local.middle, - subs.middle, - subs["$"], - local["$"] - ].filter(x => x) - ); - } + if (transform) { + creator = transform.type + ? transform + : action(name, (...args: any) => ({ + payload: transform(...args), + })); + } else { + creator = + this.localActions[name] ?? action(name, payload()); + } + } else { + name = actionIn.type; + creator = actionIn; + } - addSelector( name: string, selector: Selector) { - this.local_selectors[name] = selector; - } + const already = this.localActions[name]; - get selectors() { - return buildSelectors(this.local_selectors, this.subduxes); - } -} + if (!already) + return ((this.localActions as any)[name] = creator) as any; + + if (already !== creator && already.type !== '*') { + throw new Error(`action ${name} already exists`); + } + + return already; + } + + get _middlewareEntries() { + const groupByOrder = (mws: any) => + fp.groupBy( + ([, , actionType]: any) => + ['^', '$'].includes(actionType) + ? actionType + : 'middle', + mws + ); + + const subs = fp.flow([ + fp.toPairs, + fp.map(([slice, updux]) => + updux._middlewareEntries.map(([u, ps, ...args]: any) => [ + u, + [slice, ...ps], + ...args, + ]) + ), + fp.flatten, + groupByOrder, + ])(this.subduxes); + + const local = groupByOrder( + this.localEffects.map((x) => [this, [], ...x]) + ); + + return fp.flatten( + [ + local['^'], + subs['^'], + local.middle, + subs.middle, + subs['$'], + local['$'], + ].filter((x) => x) + ); + } + + addSelector(name: string, selector: Selector) { + this.localSelectors[name] = selector; + } + + /** +A dictionary of the updux's selectors. Subduxes' +selectors are included as well (with the mapping to the +sub-state already taken care of you). + */ + get selectors(): DuxSelectors, X, C> { + return buildSelectors( + this.localSelectors, + fp.map('selectors', this.coduxes), + fp.mapValues('selectors', this.subduxes) + ) as any; + } + } export default Updux; diff --git a/src/updux/createStore.test.ts b/src/updux/createStore.test.ts new file mode 100644 index 0000000..f8d8ae2 --- /dev/null +++ b/src/updux/createStore.test.ts @@ -0,0 +1,22 @@ +import tap from 'tap'; +import { expectType } from 'tsd'; + +import { dux } from '..'; + +import { action } from 'ts-action'; + +const d = dux({ + initial: 123, + actions: { + foo: action('foo'), + }, +}); + +expectType( d.createStore().getState() ); + +tap.equal(d.createStore().getState(), 123, 'default initial state'); + +tap.equal(d.createStore(456).getState(), 456, 'given initial state'); + +expectType<{ foo: Function }>( d.createStore().actions ); +tap.pass('we have actions'); diff --git a/tools/gen_sidebar.pl b/tools/gen_sidebar.pl new file mode 100755 index 0000000..a6149c5 --- /dev/null +++ b/tools/gen_sidebar.pl @@ -0,0 +1,75 @@ +#!/usr/bin/env perl + +use 5.30.0; +use warnings; +use experimental qw/ + signatures + postderef +/; + + +use Path::Tiny; +use Path::Tiny::Glob pathglob => { all => 1}; +use File::Serialize; +use List::AllUtils qw/ before_incl /; +use List::UtilsBy qw/ partition_by /; + +my $api = deserialize_file './temp/updux.api.json'; + +my @lines = generate_index([$api]); + +my $sidebar = path('docs/_sidebar.md'); + +$sidebar->spew( + ( before_incl { /!-- API/ } $sidebar->lines ), + map { "$_\n" } @lines +); + +sub generate_index($entries,$indent = 2) { + my @lines; + + my %sections = partition_by { + $_->{kind} + } @$entries; + + + for my $type ( sort keys %sections ) { + my $entries = $sections{$type}; + + if ( $type eq 'Constructor' ) { + push @lines, sprintf "%s- [%s](%s)", + " " x $indent, 'Constructor', ref2link(@$entries); + next; + } + + push @lines, join '', " " x $indent, '- ', $type unless $type eq 'EntryPoint'; + + for my $entry ( @$entries ) { + my $i = $indent; + + if( $entry->{name} ){ + my $link = ref2link($entry); + + push @lines, sprintf "%s- [%s](%s)", + " " x (++$i), $entry->{name}, $link; + } + + my $members = $entry->{members} or next; + + push @lines, generate_index($members, $i+1); + } + } + + return @lines; +} + +sub ref2link($entry) { + return "/4-API/". + lc( $entry->{canonicalReference} =~ s/!|#/./gr + =~ s/:constructor/._constructor_/r + =~ s/:\w+//r + =~ s/\)//r + =~ s/\((\d+)/'_' . ($1-1)/er + =~ s/_0//r ). '.md' + ; +} diff --git a/tsconfig.json b/tsconfig.json index f45ba03..9df74d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ //"composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - "removeComments": true, /* Do not emit comments to output. */ + "removeComments": false, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ diff --git a/typedoc.json b/typedoc.json index 57328f4..60cff4c 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,9 +1,10 @@ { - "theme": "default", - "tsBuildInfoFile": true, - "out": "docs", - "mode": "file", + "theme": "markdown", + "out": "docs/API", + "mode": "modules", "excludePrivate": true, - "excludeNotExported": false, + "excludeNotExported": true, + "hideSources": true, + "includeVersion": true, "exclude": [ "src/**/*test.ts" ] }