going toward v2

typescript
Yanick Champoux 2020-06-02 16:00:48 -04:00
parent e3c5aad399
commit f0653442f3
50 changed files with 6577 additions and 1401 deletions

32
.eslintrc.js Normal file
View File

@ -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",
},
};

5
.gitignore vendored
View File

@ -4,3 +4,8 @@ tsconfig.tsbuildinfo
dist
package-lock.json
yarn.lock
.nyc_output/
pnpm-debug.log
pnpm-lock.yaml
yarn-error.log
GPUCache/

32
Promake Executable file
View File

@ -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();

369
api-extractor.json Normal file
View File

@ -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 "<projectFolder>" 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 "<lookup>", 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: <lookup>
* DEFAULT VALUE: "<lookup>"
*/
// "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 "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
*/
"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 "<projectFolder>".
*
* Note: This setting will be ignored if "overrideTsconfig" is used.
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/tsconfig.json"
*/
// "tsconfigFilePath": "<projectFolder>/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: <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<unscopedPackageName>.api.md"
*/
// "reportFileName": "<unscopedPackageName>.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 "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/etc/"
*/
"reportFolder": "<projectFolder>/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 "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/"
*/
"reportTempFolder": "<projectFolder>/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 "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
*/
// "apiJsonFilePath": "<projectFolder>/temp/<unscopedPackageName>.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 "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/dist/<unscopedPackageName>.d.ts"
*/
// "untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>.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 "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-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 "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: ""
*/
// "publicTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-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 "<projectFolder>".
*
* The default value is "<lookup>", 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: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<lookup>"
*/
// "tsdocMetadataFilePath": "<projectFolder>/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
// },
//
// . . .
}
}
}

228
docs/API/README.md Normal file
View File

@ -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
}
});
```

491
docs/API/classes/updux.md Normal file
View File

@ -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 <a href="https://github.com/erikras/ducks-modular-redux">ducks</a>-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): *StoreS & object*
* **initial**: = this.initial
* **middleware**(): *function*
* (`api`: UpduxMiddlewareAPIS, 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): *StoreS & object*
**Parameters:**
Name | Type |
------ | ------ |
`initial?` | S |
`injectEnhancer?` | Function |
___
### initial
**get initial**(): *AggDuxStateS, C*
**Returns:** *AggDuxStateS, C*
___
### middleware
**get middleware**(): *[UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, 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)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, 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 &#124; undefined |
`action` | [Action](../globals.md#action) |
___
### selectors
**get selectors**(): *[DuxSelectors](../globals.md#duxselectors)AggDuxStateS, 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)AggDuxStateS, 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): *ActionCreatorstring, 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:** *ActionCreatorstring, any*
**addAction**(`theaction`: string | ActionCreatorany, `transform?`: undefined): *ActionCreatorstring, any*
**Parameters:**
Name | Type |
------ | ------ |
`theaction` | string &#124; ActionCreatorany |
`transform?` | undefined |
**Returns:** *ActionCreatorstring, any*
___
### addEffect
**addEffect**<**AC**>(`creator`: AC, `middleware`: [UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, C, X, C, ReturnTypeAC, `isGenerator?`: undefined | false | true): *any*
**Type parameters:**
**AC**: *ActionCreator*
**Parameters:**
Name | Type |
------ | ------ |
`creator` | AC |
`middleware` | [UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, C, X, C, ReturnTypeAC |
`isGenerator?` | undefined &#124; false &#124; true |
**Returns:** *any*
**addEffect**(`creator`: string, `middleware`: [UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, C, X, C, `isGenerator?`: undefined | false | true): *any*
**Parameters:**
Name | Type |
------ | ------ |
`creator` | string |
`middleware` | [UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, C, X, C |
`isGenerator?` | undefined &#124; false &#124; true |
**Returns:** *any*
___
### addMutation
**addMutation**<**A**>(`creator`: A, `mutation`: [Mutation](../globals.md#mutation)S, ActionTypeA, `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<int>() ),
inc => state => state + in
);
```
**Type parameters:**
**A**: *ActionCreator*
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`creator` | A | - |
`mutation` | [Mutation](../globals.md#mutation)S, ActionTypeA | - |
`isSink?` | undefined &#124; false &#124; 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 &#124; false &#124; 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*

980
docs/API/globals.md Normal file
View File

@ -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<any, any, any, infer S> ? S : []*
___
### Dictionary
Ƭ **Dictionary**: *object*
#### Type declaration:
* \[ **key**: *string*\]: T
___
### Dux
Ƭ **Dux**: *object*
#### Type declaration:
* **actions**: *A*
* **coduxes**: *[Dux](globals.md#dux)[]*
* **initial**: *AggDuxStateS, C*
* **subduxes**: *[Dictionary](globals.md#dictionary)[Dux](globals.md#dux)*
___
### DuxActions
Ƭ **DuxActions**:
___
### DuxActionsCoduxes
Ƭ **DuxActionsCoduxes**: *C extends Array<infer I> ? UnionToIntersection<ActionsOf<I>> : object*
___
### DuxActionsSubduxes
Ƭ **DuxActionsSubduxes**: *C extends object ? ActionsOf<C[keyof C]> : unknown*
___
### DuxSelectors
Ƭ **DuxSelectors**: *unknown extends X ? object : X*
___
### DuxState
Ƭ **DuxState**: *D extends object ? S : unknown*
___
### DuxStateCoduxes
Ƭ **DuxStateCoduxes**: *C extends Array<infer U> ? UnionToIntersection<StateOf<U>> : unknown*
___
### DuxStateGlobSub
Ƭ **DuxStateGlobSub**: *S extends object ? StateOf<I> : 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)ActionCreatorstring, 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<X> : 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**: *StoreS & 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<UpduxLocalActions<U> | ActionsOf<CoduxesOf<U>[keyof CoduxesOf<U>]>> : object*
___
### UpduxConfig
Ƭ **UpduxConfig**: *Partialobject*
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<int>());
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<any, null> ? object : S extends Updux<any, infer A> ? A : object*
___
### UpduxMiddleware
Ƭ **UpduxMiddleware**: *function*
#### Type declaration:
▸ (`api`: UpduxMiddlewareAPIS, X): *function*
**Parameters:**
Name | Type |
------ | ------ |
`api` | UpduxMiddlewareAPIS, 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)ActionCreatorstring, function*
**Parameters:**
Name | Type | Default |
------ | ------ | ------ |
`actions` | [ActionPair](globals.md#actionpair)[] | [] |
**Returns:** *[Dictionary](globals.md#dictionary)ActionCreatorstring, function*
___
### buildCreateStore
**buildCreateStore**<**S**, **A**>(`reducer`: ReducerS, `middleware`: Middleware, `actions`: A): *function*
**Type parameters:**
▪ **S**
▪ **A**
**Parameters:**
Name | Type | Default |
------ | ------ | ------ |
`reducer` | ReducerS | - |
`middleware` | Middleware | - |
`actions` | A | {} as A |
**Returns:** *function*
▸ (`initial?`: S, `injectEnhancer?`: Function): *StoreS & 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) &#124; [[Mutation](globals.md#mutation), boolean &#124; 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): *StoreS & object*
* **initial**: = this.initial
* **middleware**(): *function*
* (`api`: UpduxMiddlewareAPIS, 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)][]*

View File

@ -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<int>() );
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
}
});
```

View File

@ -1,6 +1,7 @@
<!-- docs/_sidebar.md -->
* [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)

View File

@ -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 => { ... }
```

12
docs/exports.md Normal file
View File

@ -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)

View File

@ -16,6 +16,7 @@
repo: 'https://github.com/yanick/updux',
loadSidebar: true,
subMaxLevel: 4,
relativePath: true,
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>

107
docs/recipes.md Normal file
View File

@ -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<int>()),
},
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
}
});
```

596
docs/tutorial.md Normal file
View File

@ -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<string>() );
const todo_done = action('todo_done', payload<number>() );
```
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<string>() );
const add_todo_with_id = action('add_todo_with_id',
payload<{ description: string; id: number }>() );
const todo_done = action('todo_done', payload<number>() );
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<number>() );
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<typeof todo>;
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<string>() );
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

228
docs/typedoc/README.md Normal file
View File

@ -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
}
});
```

View File

@ -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 <a href="https://github.com/erikras/ducks-modular-redux">ducks</a>-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): *StoreS & object*
* **initial**: = this.initial
* **middleware**(): *function*
* (`api`: UpduxMiddlewareAPIS, 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): *StoreS & object*
**Parameters:**
Name | Type |
------ | ------ |
`initial?` | S |
`injectEnhancer?` | Function |
___
### initial
**get initial**(): *AggDuxStateS, C*
**Returns:** *AggDuxStateS, C*
___
### middleware
**get middleware**(): *[UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, 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)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, 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 &#124; undefined |
`action` | [Action](../globals.md#action) |
___
### selectors
**get selectors**(): *[DuxSelectors](../globals.md#duxselectors)AggDuxStateS, 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)AggDuxStateS, 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): *ActionCreatorstring, 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:** *ActionCreatorstring, any*
**addAction**(`theaction`: string | ActionCreatorany, `transform?`: undefined): *ActionCreatorstring, any*
**Parameters:**
Name | Type |
------ | ------ |
`theaction` | string &#124; ActionCreatorany |
`transform?` | undefined |
**Returns:** *ActionCreatorstring, any*
___
### addEffect
**addEffect**<**AC**>(`creator`: AC, `middleware`: [UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, C, X, C, ReturnTypeAC, `isGenerator?`: undefined | false | true): *any*
**Type parameters:**
**AC**: *ActionCreator*
**Parameters:**
Name | Type |
------ | ------ |
`creator` | AC |
`middleware` | [UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, C, X, C, ReturnTypeAC |
`isGenerator?` | undefined &#124; false &#124; true |
**Returns:** *any*
**addEffect**(`creator`: string, `middleware`: [UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, C, X, C, `isGenerator?`: undefined | false | true): *any*
**Parameters:**
Name | Type |
------ | ------ |
`creator` | string |
`middleware` | [UpduxMiddleware](../globals.md#upduxmiddleware)AggDuxStateS, C, [DuxSelectors](../globals.md#duxselectors)AggDuxStateS, C, X, C |
`isGenerator?` | undefined &#124; false &#124; true |
**Returns:** *any*
___
### addMutation
**addMutation**<**A**>(`creator`: A, `mutation`: [Mutation](../globals.md#mutation)S, ActionTypeA, `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<int>() ),
inc => state => state + in
);
```
**Type parameters:**
**A**: *ActionCreator*
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`creator` | A | - |
`mutation` | [Mutation](../globals.md#mutation)S, ActionTypeA | - |
`isSink?` | undefined &#124; false &#124; 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 &#124; false &#124; 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*

980
docs/typedoc/globals.md Normal file
View File

@ -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<any, any, any, infer S> ? S : []*
___
### Dictionary
Ƭ **Dictionary**: *object*
#### Type declaration:
* \[ **key**: *string*\]: T
___
### Dux
Ƭ **Dux**: *object*
#### Type declaration:
* **actions**: *A*
* **coduxes**: *[Dux](globals.md#dux)[]*
* **initial**: *AggDuxStateS, C*
* **subduxes**: *[Dictionary](globals.md#dictionary)[Dux](globals.md#dux)*
___
### DuxActions
Ƭ **DuxActions**:
___
### DuxActionsCoduxes
Ƭ **DuxActionsCoduxes**: *C extends Array<infer I> ? UnionToIntersection<ActionsOf<I>> : object*
___
### DuxActionsSubduxes
Ƭ **DuxActionsSubduxes**: *C extends object ? ActionsOf<C[keyof C]> : unknown*
___
### DuxSelectors
Ƭ **DuxSelectors**: *unknown extends X ? object : X*
___
### DuxState
Ƭ **DuxState**: *D extends object ? S : unknown*
___
### DuxStateCoduxes
Ƭ **DuxStateCoduxes**: *C extends Array<infer U> ? UnionToIntersection<StateOf<U>> : unknown*
___
### DuxStateGlobSub
Ƭ **DuxStateGlobSub**: *S extends object ? StateOf<I> : 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)ActionCreatorstring, 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<X> : 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**: *StoreS & 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<UpduxLocalActions<U> | ActionsOf<CoduxesOf<U>[keyof CoduxesOf<U>]>> : object*
___
### UpduxConfig
Ƭ **UpduxConfig**: *Partialobject*
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<int>());
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<any, null> ? object : S extends Updux<any, infer A> ? A : object*
___
### UpduxMiddleware
Ƭ **UpduxMiddleware**: *function*
#### Type declaration:
▸ (`api`: UpduxMiddlewareAPIS, X): *function*
**Parameters:**
Name | Type |
------ | ------ |
`api` | UpduxMiddlewareAPIS, 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)ActionCreatorstring, function*
**Parameters:**
Name | Type | Default |
------ | ------ | ------ |
`actions` | [ActionPair](globals.md#actionpair)[] | [] |
**Returns:** *[Dictionary](globals.md#dictionary)ActionCreatorstring, function*
___
### buildCreateStore
**buildCreateStore**<**S**, **A**>(`reducer`: ReducerS, `middleware`: Middleware, `actions`: A): *function*
**Type parameters:**
▪ **S**
▪ **A**
**Parameters:**
Name | Type | Default |
------ | ------ | ------ |
`reducer` | ReducerS | - |
`middleware` | Middleware | - |
`actions` | A | {} as A |
**Returns:** *function*
▸ (`initial?`: S, `injectEnhancer?`: Function): *StoreS & 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) &#124; [[Mutation](globals.md#mutation), boolean &#124; 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): *StoreS & object*
* **initial**: = this.initial
* **middleware**(): *function*
* (`api`: UpduxMiddlewareAPIS, 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)][]*

View File

@ -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).

View File

@ -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 <yanick@babyl.ca> (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 <yanick@babyl.ca> (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"
}
}

121
src/actions-params.test.ts Normal file
View File

@ -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<number>()),
};
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<object>(empty.actions);
t.same(empty.actions, {}, 'no actions there');
t.end();
});
t.test('coduxes actions', t => {
const typeOf = <C>(x: C) => (x as any) as DuxActionsCoduxes<C>;
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();
});

View File

@ -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<any,any>();
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<object>( actions );
test('actionCreator inlined', () => {
updux.addAction( 'baz', (x) => ({payload: {x}}));
expect(updux.actions.baz(3)).toMatchObject({
type: 'baz', payload: { x: 3 }
});
});
t.end();
});

View File

@ -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<MyState>({
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();
});

View File

@ -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<ActionCreator> {
function buildActions(actions: ActionPair[] = []): Dictionary<ActionCreator<string,(...args: any) => {type: string} >>{
// priority => generics => generic subs => craft subs => creators
const [crafted, generic] = fp.partition(([type, f]) => !f._genericAction)(

View File

@ -3,29 +3,28 @@ import {
applyMiddleware,
Middleware,
Reducer,
PreloadedState
PreloadedState,
Store,
} from 'redux';
import { ActionCreator, Dictionary } from '../types';
function buildCreateStore<S>(
function buildCreateStore<S,A = {}>(
reducer: Reducer<S>,
initial: PreloadedState<S>,
middleware: Middleware,
actions: Dictionary<ActionCreator>,
) {
return () => {
actions: A = {} as A,
): (initial?: S, injectEnhancer?: Function) => Store<S> & { actions: A } {
return function createStore(initial?: S, injectEnhancer?: Function ): Store<S> & { actions: A } {
let enhancer = injectEnhancer ? injectEnhancer(middleware) : applyMiddleware(middleware);
const store = reduxCreateStore(
reducer,
initial,
applyMiddleware(middleware),
initial as PreloadedState<S>,
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;
};
}

View File

@ -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<State>,
() => () => () => {return},
{ foo, bar }
)();
expectType<State>( store.getState() );
expectType<number>( store.getState().x );
expectType<{ foo: ActionCreator }>( store.actions );
tap.pass();

View File

@ -1,13 +1,16 @@
import fp from 'lodash/fp';
import { Dictionary } from '../types';
import u from 'updeep';
function buildInitial<S extends number|string|boolean>( initial: S, subduxes?: Dictionary<undefined> ): S;
function buildInitial<S extends object>( initial?: Partial<S>, subduxes?: Partial<S> ): 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;

View File

@ -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<S = any>(
middlewareEntries: any[] = [],
actions: Dictionary<ActionCreator> = {}
): UpduxMiddleware<S> {
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<UpduxMiddleware>;
return (api: UpduxMiddlewareAPI<S>) => {
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<ActionCreator>,
selectors: Dictionary<Selector>,
) => {
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<any>
) => (original_next: Next) =>
mws.reduceRight((next, mw) => mw(api)(next), original_next);
export function buildMiddleware<S = unknown>(
local: UpduxMiddleware[] = [],
co: UpduxMiddleware[] = [],
sub: Submws = {}
): UpduxMiddleware<S> {
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<S>;
}
export default buildMiddleware;

View File

@ -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();
});

View File

@ -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<Mutation>;
[slice: string]: Dictionary<Mutation>;
};
function buildMutations(
mutations: Dictionary<Mutation | [Mutation, boolean | undefined]> = {},
subduxes = {}
mutations: Dictionary<Mutation | [Mutation, boolean | undefined]> = {},
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]) =>
<Mutation>(
((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<Mutation[]> = {};
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;

View File

@ -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<Selector> = {},
subduxes: Dictionary<Updux> = {}
coduxes: Dictionary<Selector>[] = [],
subduxes: Dictionary<Selector> = {}
) {
return Object.fromEntries(
[
Object.entries(subduxes).flatMap(subSelectors),
Object.entries(coduxes),
Object.entries(localSelectors),
].flat()
);

36
src/coduxes.test.ts Normal file
View File

@ -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();
});

14
src/duxstate.test.ts Normal file
View File

@ -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<typeof myDux>;
expectType<State>({ a: 12, b: "something" });
tap.pass();

View File

@ -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<string>() );
dux.addEffect( myAction, () => () => action => {
expectType<{payload: string; type: "mine"}>(action);
});
tap.pass("pure type checking");

View File

@ -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 = <C extends Dux, U extends [C,...C[]]>(...coduxes: U): { coduxes: U } => ({
coduxes });
export const dux = <S=unknown,A=unknown,X=unknown,C extends UpduxConfig= {}>(config: C ) => {
/// : Dux<S,A,X,C> => {
return ( new Updux<S,A,X,C>(config) ).asDux;
}

15
src/initial.test.ts Normal file
View File

@ -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 });

View File

@ -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<any>({
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 },
]);

View File

@ -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);
});

View File

@ -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" });
});

View File

@ -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<any>({
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',
});
});

View File

@ -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<any, any>({
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<DuxState<typeof myDux>>( getState() );
expectType<(...args:any[]) => number>(selectors.getStuff);
});
});

View File

@ -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<number>({
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();
});

View File

@ -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<number[]>(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();
});

View File

@ -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<number>() );
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",

View File

@ -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'
);
});

View File

@ -1,209 +1,362 @@
import { Dispatch, Middleware } from "redux";
import { ActionCreator } from 'ts-action';
type MaybePayload<P> = P extends object | string | boolean | number
? {
payload: P;
}
: { payload?: P };
? {
payload: P;
}
: { payload?: P };
export type Action<T extends string = string, P = any> = {
type: T;
type: T;
} & MaybePayload<P>;
export type Dictionary<T> = { [key: string]: T };
export type Mutation<S = any, A extends Action = Action> = (
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<any, Action<string, any>>,
boolean?
ActionCreator | string,
Mutation<any, Action<string, any>>,
boolean?
];
export type ActionCreator<T extends string = string, P = any> = {
type: T;
_genericAction?: boolean;
} & ((...args: any[]) => Action<T, P>);
export type GenericActions = Dictionary<
ActionCreator<string, (...args: any) => { type: string }>
>;
export type UpduxDispatch = Dispatch & Dictionary<Function>;
export type UnionToIntersection<U> = (U extends any
? (k: U) => void
: never) extends (k: infer I) => void
? I
: never;
export type StateOf<D> = D extends { initial: infer I } ? I : unknown;
export type DuxStateCoduxes<C> = C extends Array<infer U> ? UnionToIntersection<StateOf<U>>: unknown
export type DuxStateSubduxes<C> =
C extends { '*': infer I } ? {
[ key: string ]: StateOf<I>,
[ index: number ]: StateOf<I>,
} :
C extends object ? { [ K in keyof C]: StateOf<C[K]>}: unknown;
type DuxStateGlobSub<S> = S extends { '*': infer I } ? StateOf<I> : unknown;
type LocalDuxState<S> = S extends never[] ? unknown[] : S;
/** @ignore */
type AggDuxState2<L,S,C> = (
L extends never[] ? Array<DuxStateGlobSub<S>> : L & DuxStateSubduxes<S> ) & DuxStateCoduxes<C>;
/** @ignore */
export type AggDuxState<O,S extends UpduxConfig> = unknown extends O ?
AggDuxState2<S['initial'],S['subduxes'],S['coduxes']> : O
type SelectorsOf<C> = C extends { selectors: infer S } ? S : unknown;
/** @ignore */
export type DuxSelectorsSubduxes<C> = C extends object ? UnionToIntersection<SelectorsOf<C[keyof C]>> : unknown;
/** @ignore */
export type DuxSelectorsCoduxes<C> = C extends Array<infer U> ? UnionToIntersection<SelectorsOf<U>> : unknown;
type MaybeReturnType<X> = X extends (...args: any) => any ? ReturnType<X> : unknown;
type RebaseSelector<S,X> = {
[ K in keyof X]: (state: S) => MaybeReturnType< X[K] >
}
type ActionsOf<C> = C extends { actions: infer A } ? A : {};
type DuxActionsSubduxes<C> = C extends object ? ActionsOf<C[keyof C]> : unknown;
export type DuxActionsCoduxes<C> = C extends Array<infer I> ? UnionToIntersection<ActionsOf<I>> : {};
type ItemsOf<C> = C extends object? C[keyof C] : unknown
export type DuxActions<A,C extends UpduxConfig> = A extends object ? A: (
UnionToIntersection<ActionsOf<C|ItemsOf<C['subduxes']>|ItemsOf<C['coduxes']>>>
);
export type DuxSelectors<S,X,C extends UpduxConfig> = unknown extends X ? (
RebaseSelector<S,
C['selectors'] & DuxSelectorsCoduxes<C['coduxes']> &
DuxSelectorsSubduxes<C['subduxes']> >
): X
export type Dux<
S = unknown,
A = unknown,
X = unknown,
C = unknown,
> = {
subduxes: Dictionary<Dux>,
coduxes: Dux[],
initial: AggDuxState<S,C>,
actions: A,
}
/**
* Configuration object given to Updux's constructor.
* @typeparam S Type of the Updux's state. Defaults to `any`.
*/
export type UpduxConfig<S = any> = {
/**
* 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<int>());
* 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<Dux>,
coduxes: Dux[],
actions: Dictionary<ActionCreator>,
selectors: Dictionary<Selector>,
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<Selector>;
/**
* 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<S> } | MutationEntry[];
groomMutations?: (m: Mutation<S>) => Mutation<S>;
/**
* 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<UpduxMiddleware<S>> | EffectEntry<S>[];
};
export type EffectEntry<S> = [
ActionCreator | string,
UpduxMiddleware<S>,
boolean?
];
export type Upreducer<S = any> = (action: Action) => (state: S) => S;
export interface UpduxMiddlewareAPI<S> {
dispatch: UpduxDispatch;
getState(): any;
getRootState(): S;
selectors: Dictionary<Selector>;
/** @ignore */
export interface UpduxMiddlewareAPI<S=any,X = Dictionary<Selector>> {
dispatch: Function;
getState(): S;
selectors: X;
actions: Dictionary<ActionCreator>;
}
export type UpduxMiddleware<S = any> = (
api: UpduxMiddlewareAPI<S>
) => (next: UpduxDispatch) => (action: Action) => any;
export type UpduxMiddleware<S=any,X = Dictionary<Selector>,A = Action> = (
api: UpduxMiddlewareAPI<S,X>
) => (next: Function) => (action: A) => any;
export type Selector<S = any> = (state:S) => any;
export type Selector<S = any> = (state: S) => unknown;
export type DuxState<D> = D extends { initial: infer S } ? S : unknown;

View File

@ -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<T> = UnionToIntersection<T[keyof T]>;
type ActionsOf<U> = U extends Updux ? U['actions'] : {};
//| ActionsOf<SubduxesOf<U>[keyof SubduxesOf<U>]>
export type UpduxActions<U> = U extends Updux
? UnionToIntersection<
UpduxLocalActions<U> | ActionsOf<CoduxesOf<U>[keyof CoduxesOf<U>]>
>
: {};
export type UpduxLocalActions<S> = S extends Updux<any, null>
? {}
: S extends Updux<any, infer A>
? A
: {};
export type CoduxesOf<U> = U extends Updux<any, any, any, infer S> ? S : [];
type StoreWithDispatchActions<
S = any,
Actions = { [action: string]: (...args: any) => Action }
S = any,
Actions = { [action: string]: (...args: any) => Action }
> = Store<S> & {
dispatch: { [type in keyof Actions]: (...args: any) => void };
dispatch: { [type in keyof Actions]: (...args: any) => void };
};
export type Dux<S> = Pick<
Updux<S>,
| "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<Dux>;
coduxes: Dux[];
export class Updux<S = any> {
subduxes: Dictionary<Updux> = {};
private localSelectors: Dictionary<Selector> = {};
private local_selectors: Dictionary<Selector<S>> = {};
private localInitial: unknown;
initial: S;
groomMutations: (mutation: Mutation<S>) => Mutation<S>;
groomMutations: (mutation: Mutation<S>) => Mutation<S>;
private localEffects: Effect[] = [];
private localActions: Dictionary<ActionCreator> = {};
private localEffects: EffectEntry<S>[] = [];
private localMutations: Dictionary<
Mutation<S> | [Mutation<S>, boolean | undefined]
> = {};
private localActions: Dictionary<ActionCreator> = {};
get initial(): AggDuxState<S, C> {
return buildInitial(
this.localInitial,
this.coduxes.map(({ initial }) => initial),
fp.mapValues('initial', this.subduxes)
) as any;
}
private localMutations: Dictionary<
Mutation<S> | [Mutation<S>, 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<S>) => x);
Object.entries(config.actions ?? {}).forEach((args) =>
(this.addAction as any)(...args)
);
const selectors = fp.getOr( {}, 'selectors', config ) as Dictionary<Selector>;
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<S>) => 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<any>(
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<S> {
return buildMiddleware(this._middlewareEntries, this.actions);
}
/*
get actions(): Dictionary<ActionCreator> {
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<S> {
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<Mutation<S>> {
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<S, C>,
DuxSelectors<AggDuxState<S, C>, 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<S> {
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<A, C> {
// UpduxActions<Updux<S,A,SUB,CO>> {
return this.localActions as any;
}
return buildCreateStore<S>(
this.reducer,
this.initial as PreloadedState<S>,
this.middleware as any,
actions
) as () => StoreWithDispatchActions<S, typeof actions>;
}
get upreducer(): Upreducer<S> {
return buildUpreducer(
this.initial,
this.mutations as any
) as any;
}
get asDux(): Dux<S> {
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<A extends ActionCreator=any>(
creator: A,
mutation: Mutation<S, A extends (...args: any[]) => infer R ? R : never>,
isSink?: boolean
)
addMutation<A extends ActionCreator=any>(
creator: string,
mutation: Mutation<S, any>,
isSink?: boolean
)
addMutation<A extends ActionCreator=any>(
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<Mutation<S>> {
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<S>,
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<S>,
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<AggDuxState<S, C>, DuxActions<A, C>>(
this.reducer as any,
this.middleware as any,
this.actions
);
}
// can be
//addAction( actionCreator )
// addAction( 'foo', transform )
addAction(theaction: string, transform?: any): ActionCreator<string,any>
addAction(theaction: string|ActionCreator<any>, transform?: never): ActionCreator<string,any>
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 <a href="https://github.com/erikras/ducks-modular-redux">ducks</a>-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<int>() ),
* inc => state => state + in
* );
* ```
*/
addMutation<A extends ActionCreator>(
creator: A,
mutation: Mutation<S, ActionType<A>>,
isSink?: boolean
);
addMutation<A extends ActionCreator = any>(
creator: string,
mutation: Mutation<S, any>,
isSink?: boolean
);
addMutation<A extends ActionCreator = any>(
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<S>,
isSink,
];
}
get _middlewareEntries() {
const groupByOrder = (mws: any) =>
fp.groupBy(
([a,b, actionType]: any) =>
["^", "$"].includes(actionType) ? actionType : "middle",
mws
);
addEffect<AC extends ActionCreator>(
creator: AC,
middleware: UpduxMiddleware<
AggDuxState<S, C>,
DuxSelectors<AggDuxState<S, C>, X, C>,
ReturnType<AC>
>,
isGenerator?: boolean
);
addEffect(
creator: string,
middleware: UpduxMiddleware<
AggDuxState<S, C>,
DuxSelectors<AggDuxState<S, C>, 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<string, any>;
addAction(
theaction: string | ActionCreator<any>,
transform?: never
): ActionCreator<string, any>;
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<AggDuxState<S, C>, X, C> {
return buildSelectors(
this.localSelectors,
fp.map('selectors', this.coduxes),
fp.mapValues('selectors', this.subduxes)
) as any;
}
}
export default Updux;

View File

@ -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<number>( 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');

75
tools/gen_sidebar.pl Executable file
View File

@ -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'
;
}

View File

@ -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'. */

View File

@ -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" ]
}