lotsa work

This commit is contained in:
Yanick Champoux 2024-08-08 09:28:44 -04:00
parent edb6716c9c
commit 66c2b162db
128 changed files with 3366 additions and 1667 deletions

2
.gitignore vendored
View File

@ -11,3 +11,5 @@ GPUCache/
updux-2.0.0.tgz
pnpm-lock.yaml
updux-5.0.0.tgz
.envrc
.task

11
TODO
View File

@ -1,2 +1,9 @@
- documentation generator (mkdocs + jsdoc-to-markdown)
- createStore
* documentation action + mutation <<<
* createAction
* addMutation
addMutation with signature
.addMutation( actionCreator, (payload,action) => state => newState )
* then check that the mutation is in the new type

View File

@ -20,7 +20,8 @@ tasks:
- git checkout {{.PARENT_BRANCH}}
- git weld -
test: vitest run src docs/*.ts
test: vitest run src
test:dev: vitest src
lint:fix:delta:
@ -65,6 +66,12 @@ tasks:
cmds:
- npx eslint {{.FILES | default "src/**" }}
docs:api:
sources: [src/**.ts]
generates: [docs/src/content/docs/api/**.md]
cmds:
- typedoc
- contrib/api_grooming.pl docs/src/content/docs/api
docs:serve:
cmds:
- mkdocs serve

17
contrib/api_grooming.pl Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env perl
use 5.34.0;
use Path::Tiny;
my $dir = path(shift);
$dir->visit(sub{
my( $path ) = @_;
return unless $path =~ /\.md$/;
$path->edit(sub{
s/.*^\# (.*?)$/---\ntitle: "@{[ $1 =~ s(\\)()gr]}"\n---/sm;
});
},{ recurse => 1});

226
docs-docsidy/README.md Normal file
View File

@ -0,0 +1,226 @@
# 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/).
Right now the best way to understand the whole thing is to go
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
## 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

@ -6,7 +6,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="docsify/themes/buble.css">
<link rel="stylesheet" href="docsify/themes/vue.css">
</head>
<body>
<div id="app"></div>

View File

@ -1,88 +1,12 @@
# Tutorial
This tutorial walks you through the features of `Updux` using the
time-honored example of the implementation of Todo list store.
We'll be using
[@yanick/updeep-remeda](https://www.npmjs.com/package/@yanick/updeep-remeda) to
help with immutability and deep merging,
but that's totally optional. If `updeep` is not your bag,
it can easily be substitued with, say, [immer][],
[remeda][],
[lodash][], or even
plain JavaScript.
## Definition of the state
To begin with, let's define that has nothing but an initial state.
```
--8<- "docs/tutorial-1.test.ts:tut1"
```
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:
```
--8<- "docs/tutorial-1.test.ts:tut2"
```
## Add actions
This is all good, but very static. Let's add some actions!
```
--8<- "docs/tutorial-actions.test.ts:actions1"
```
### Accessing actions
Once an action is defined, its creator is accessible via the `actions` accessor.
This is not yet terribly exciting, but it'll get more interesting once we begin using
subduxes.
```
--8<- "docs/tutorial-actions.test.ts:actions2"
```
### Adding a mutation
Mutations are the reducing functions associated to actions. They
are defined via the `addMutation` method.
```
--8<- "docs/tutorial-actions.test.ts:addMutation"
```
Note that in the mutation we take the liberty of changing the state directly.
Typically, that'd be a big no-no, but we're safe here because updux wraps all mutations in an immer `produce`.
## Effects
In addition to mutations, Updux also provides action-specific middleware, here
called effects.
Effects use the usual Redux middleware signature, plus a few goodies.
The `getState` and `dispatch` functions are augmented with the dux selectors,
and actions, respectively. The selectors and actions are also available
from the api object.
```
--8<- "docs/tutorial-effects.test.ts:effects-1"
```
## Selectors
Selectors can be defined to get data derived from the state.
The `getState` method of a dux store will be augmented
with its selectors, with the first call for the state already
curried for you.
```
--8<- "docs/tutorial-selectors.test.ts:sel1"
```
## Subduxes
@ -96,7 +20,7 @@ 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 dux we have so far:
``` title="todos-monolith.ts"
```title="todos-monolith.ts"
--8<- "docs/tutorial-monolith.test.ts:mono"
```
@ -106,25 +30,25 @@ create upduxes for each of those.
### NextId dux
``` js title="nextId.ts"
```js title="nextId.ts"
--8<- "docs/nextId.ts:dux"
```
### Todo updux
``` title="todo.ts"
```title="todo.ts"
--8<- "docs/todo.ts"
```
### Todos updux
``` title="todos.ts"
```title="todos.ts"
--8<- "docs/todos.ts"
```
### Main store
``` title="todoList.ts"
```title="todoList.ts"
--8<- "docs/todoList.ts"
```

21
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

4
docs/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
docs/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@ -1,226 +1,55 @@
# What's Updux?
# Starlight Starter Kit: Basics
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
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
```
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);
npm create astro@latest -- --template starlight
```
# Description
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
Full documentation can be [found here](https://yanick.github.io/updux/).
Right now the best way to understand the whole thing is to go
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## Exporting upduxes
## 🚀 Project Structure
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:
Inside of your Astro + Starlight project, you'll see the following folders and files:
```
import Updux from 'updux';
const updux = new Updux({ ... });
export default updux;
.
├── public/
├── src/
│ ├── assets/
│ ├── content/
│ │ ├── docs/
│ │ └── config.ts
│ └── env.d.ts
├── astro.config.mjs
├── package.json
└── tsconfig.json
```
Then you can use them as subduxes like this:
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
```
import Updux from 'updux';
import foo from './foo'; // foo is an Updux
import bar from './bar'; // bar is an Updux as well
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
const updux = new Updux({
subduxes: {
foo, bar
}
});
```
Static assets, like favicons, can be placed in the `public/` directory.
Or if you want to use it:
## 🧞 Commands
```
import updux from './myUpdux';
All commands are run from the root of the project, from a terminal:
const {
reducer,
actions: { doTheThing },
createStore,
middleware,
} = updux;
```
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## Mapping a mutation to all values of a state
## 👀 Want to learn more?
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
}
});
```
Check out [Starlights docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).

54
docs/astro.config.mjs Normal file
View File

@ -0,0 +1,54 @@
import { defineConfig, squooshImageService } from 'astro/config';
import starlight from '@astrojs/starlight';
// https://astro.build/config
export default defineConfig({
image: { service: squooshImageService() },
integrations: [
starlight({
title: 'Updux',
pagefind: false,
social: {
github: 'https://github.com/withastro/starlight',
},
sidebar: [
{
label: 'Home',
slug: '',
},
{
label: 'Tutorial',
autogenerate: {
directory: 'tutorial',
}
// items: [
// { label: 'Introduction', slug: 'tutorial/intro'}
// { label: 'Introduction', slug: 'tutorial/intro'}
// ]
},
{
label: 'API',
items: [
{ label: 'updux', slug: 'api/modules' },
{ label: 'defaults', slug: 'api/classes/default' },
]
},
{
label: 'Guide',
autogenerate: {
directory: 'guides',
}
},
// items: [
// // Each item here is one entry in the navigation menu.
// { label: 'Example Guide', slug: 'guides/example' },
// ],
// },
// {
// label: 'Reference',
// autogenerate: { directory: 'reference' },
// },
],
}),
],
});

View File

@ -1,19 +0,0 @@
/// --8<-- [start:dux]
import Updux from '../src/index.js';
import u from '@yanick/updeep-remeda';
const nextIdDux = new Updux({
initialState: 1,
actions: {
incNextId: null,
},
selectors: {
getNextId: state => state
}
});
nextIdDux.addMutation('incNextId', () => id => id + 1)
export default nextIdDux.asDux;
/// --8<-- [end:dux]

19
docs/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "starlight-docs",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.25.3",
"astro": "^4.10.2"
},
"devDependencies": {
"vitest": "^2.0.4"
}
}

177
docs/public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 583 KiB

177
docs/public/updux-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 583 KiB

View File

@ -1,53 +0,0 @@
import { test, expect } from 'vitest';
import u from 'updeep';
import { Updux } from '../src/index.js';
const done = () => (state) => ({...state, done: true});
const todo = new Updux({
initial: { id: 0, done: false },
actions: {
done: null,
doneAll: null,
},
mutations: {
done,
doneAll: done,
},
});
const todos = new Updux({
initial: [],
subduxes: { '*': todo },
actions: { addTodo: null },
mutations: {
addTodo: text => state => [ ...state, { text } ]
}
});
todos.setMutation(
todo.actions.done,
(text,action) => u.map(u.if(u.is('text',text), todo.upreducer(action))),
true // prevents the subduxes mutations to run automatically
);
test( "tutorial", async () => {
const store = todos.createStore();
store.dispatch.addTodo('one');
store.dispatch.addTodo('two');
store.dispatch.addTodo('three');
store.dispatch.done( 'two' );
expect( store.getState()[1].done ).toBeTruthy();
expect( store.getState()[2].done ).toBeFalsy();
store.dispatch.doneAll();
expect( store.getState().map( ({done}) => done ) ).toEqual([
true, true, true
]);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@ -0,0 +1,6 @@
import { defineCollection } from 'astro:content';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
};

View File

@ -0,0 +1 @@
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.

View File

@ -0,0 +1,153 @@
---
title: "Description"
---
Full documentation can be [found here](https://yanick.github.io/updux/).
Right now the best way to understand the whole thing is to go
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
## 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,355 @@
---
title: "Class: default<D>"
---
**`Description`**
main Updux class
## Type parameters
| Name | Type |
| :------ | :------ |
| `D` | extends `DuxConfig` |
## Constructors
### constructor
**new default**\<`D`\>(`duxConfig`): [`default`](default.md)\<`D`\>
#### Type parameters
| Name | Type |
| :------ | :------ |
| `D` | extends `Partial`\<\{ `actions`: `Record`\<`string`, `Function` \| `ActionCreator`\<`string`, `any`[]\>\> ; `initialState`: `any` ; `selectors`: `Record`\<`string`, `Function`\> ; `subduxes`: `Record`\<`string`, Partial\<\{ initialState: any; subduxes: Record\<string, Partial\<...\>\>; actions: Record\<string, Function \| ActionCreator\<string, any[]\>\>; selectors: Record\<...\>; }\>\> }\> |
#### Parameters
| Name | Type |
| :------ | :------ |
| `duxConfig` | `D` |
#### Returns
[`default`](default.md)\<`D`\>
## Accessors
### actions
`get` **actions**(): `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\>
#### Returns
`ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\>
___
### effects
`get` **effects**(): `any`
#### Returns
`any`
___
### initialState
`get` **initialState**(): `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>
#### Returns
`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>
**`Description`**
Initial state of the dux.
___
### reactions
`get` **reactions**(): `any`
#### Returns
`any`
___
### reducer
`get` **reducer**(): `any`
#### Returns
`any`
___
### selectors
`get` **selectors**(): `ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, SUB[key]\> }\>\> : {}\>
#### Returns
`ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, SUB[key]\> }\>\> : {}\>
___
### subduxes
`get` **subduxes**(): `Object`
#### Returns
`Object`
___
### upreducer
`get` **upreducer**(): (`action`: `AnyAction`) => (`state?`: `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>) => `any`
#### Returns
`fn`
▸ (`action`): (`state?`: `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>) => `any`
##### Parameters
| Name | Type |
| :------ | :------ |
| `action` | `AnyAction` |
##### Returns
`fn`
▸ (`state?`): `any`
##### Parameters
| Name | Type |
| :------ | :------ |
| `state?` | `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\> |
##### Returns
`any`
## Methods
### addAction
**addAction**(`action`): `void`
#### Parameters
| Name | Type |
| :------ | :------ |
| `action` | `ActionCreator`\<`any`, `any`, `any`\> |
#### Returns
`void`
___
### addEffect
**addEffect**\<`A`\>(`actionType`, `effect`): [`default`](default.md)\<`D`\>
#### Type parameters
| Name | Type |
| :------ | :------ |
| `A` | extends `string` \| `number` \| `symbol` |
#### Parameters
| Name | Type |
| :------ | :------ |
| `actionType` | `A` |
| `effect` | `EffectMiddleware`\<`D`, `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\>[`A`] extends (...`args`: `any`[]) => `R` ? `R` : `AnyAction`\> |
#### Returns
[`default`](default.md)\<`D`\>
**addEffect**\<`AC`\>(`actionCreator`, `effect`): [`default`](default.md)\<`D`\>
#### Type parameters
| Name | Type |
| :------ | :------ |
| `AC` | extends `ActionCreatorWithPreparedPayload`\<`any`, `any`, `string`, `never`, `never`, `AC`\> |
#### Parameters
| Name | Type |
| :------ | :------ |
| `actionCreator` | `AC` |
| `effect` | `EffectMiddleware`\<`D`, `ReturnType`\<`AC`\>\> |
#### Returns
[`default`](default.md)\<`D`\>
**addEffect**(`guardFunc`, `effect`): [`default`](default.md)\<`D`\>
#### Parameters
| Name | Type |
| :------ | :------ |
| `guardFunc` | (`action`: `AnyAction`) => `boolean` |
| `effect` | `EffectMiddleware`\<`D`, `AnyAction`\> |
#### Returns
[`default`](default.md)\<`D`\>
**addEffect**(`effect`): [`default`](default.md)\<`D`\>
#### Parameters
| Name | Type |
| :------ | :------ |
| `effect` | `EffectMiddleware`\<`D`, `AnyAction`\> |
#### Returns
[`default`](default.md)\<`D`\>
___
### addMutation
**addMutation**\<`A`\>(`actionType`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
#### Type parameters
| Name | Type |
| :------ | :------ |
| `A` | extends `string` \| `number` \| `symbol` |
#### Parameters
| Name | Type |
| :------ | :------ |
| `actionType` | `A` |
| `mutation` | `Mutation`\<`ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\>[`A`] extends (...`args`: `any`[]) => `R` ? `R` : `AnyAction`, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
| `terminal?` | `boolean` |
#### Returns
[`default`](default.md)\<`D`\>
**addMutation**\<`AC`\>(`actionCreator`, `mutation`, `terminal?`): [`default`](default.md)\<`D` & \{ `actions`: `Record`\<`AC` extends \{ `type`: `T` } ? `T` : `never`, `AC`\> }\>
#### Type parameters
| Name | Type |
| :------ | :------ |
| `AC` | extends `ActionCreatorWithPreparedPayload`\<`any`, `any`, `string`, `never`, `never`, `AC`\> |
#### Parameters
| Name | Type |
| :------ | :------ |
| `actionCreator` | `AC` |
| `mutation` | `Mutation`\<`ReturnType`\<`AC`\>, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
| `terminal?` | `boolean` |
#### Returns
[`default`](default.md)\<`D` & \{ `actions`: `Record`\<`AC` extends \{ `type`: `T` } ? `T` : `never`, `AC`\> }\>
**addMutation**\<`A`, `P`, `X`\>(`actionCreator`, `mutation`, `terminal?`): [`default`](default.md)\<`D` & \{ `actions`: `Record`\<`A`, `ActionCreator`\<`A`, `P`, `X`\>\> }\>
#### Type parameters
| Name | Type |
| :------ | :------ |
| `A` | extends `string` |
| `P` | `P` |
| `X` | extends `any`[] |
#### Parameters
| Name | Type |
| :------ | :------ |
| `actionCreator` | `ActionCreator`\<`A`, `P`, `X`\> |
| `mutation` | `Mutation`\<\{ `payload`: `P` ; `type`: `A` }, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
| `terminal?` | `boolean` |
#### Returns
[`default`](default.md)\<`D` & \{ `actions`: `Record`\<`A`, `ActionCreator`\<`A`, `P`, `X`\>\> }\>
**addMutation**(`matcher`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
#### Parameters
| Name | Type |
| :------ | :------ |
| `matcher` | (`action`: `AnyAction`) => `boolean` |
| `mutation` | `Mutation`\<`AnyAction`, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
| `terminal?` | `boolean` |
#### Returns
[`default`](default.md)\<`D`\>
___
### addReaction
**addReaction**(`reaction`): [`default`](default.md)\<`D`\>
#### Parameters
| Name | Type |
| :------ | :------ |
| `reaction` | `DuxReaction`\<`D`\> |
#### Returns
[`default`](default.md)\<`D`\>
___
### createStore
**createStore**(`options?`): `ToolkitStore`\<`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\> & {}, `AnyAction`, `Middlewares`\<`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\>\> & `MiddlewareAPI`\<`Dispatch`\<`AnyAction`\>, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> & \{ `actions`: `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\> ; `dispatch`: `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\> ; `getState`: `CurriedSelectors`\<`ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, (...)[(...)]\> }\>\> : {}\>\> ; `selectors`: `ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, SUB[key]\> }\>\> : {}\> }
#### Parameters
| Name | Type |
| :------ | :------ |
| `options` | `Partial`\<\{ `preloadedState`: `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\> }\> |
#### Returns
`ToolkitStore`\<`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\> & {}, `AnyAction`, `Middlewares`\<`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\>\> & `MiddlewareAPI`\<`Dispatch`\<`AnyAction`\>, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> & \{ `actions`: `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\> ; `dispatch`: `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\> ; `getState`: `CurriedSelectors`\<`ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, (...)[(...)]\> }\>\> : {}\>\> ; `selectors`: `ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, SUB[key]\> }\>\> : {}\> }
___
### setDefaultMutation
**setDefaultMutation**(`mutation`, `terminal?`): `any`
#### Parameters
| Name | Type |
| :------ | :------ |
| `mutation` | `Mutation`\<`any`, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
| `terminal?` | `boolean` |
#### Returns
`any`

View File

@ -0,0 +1,129 @@
---
title: "updux"
---
## Classes
- [default](classes/default.md)
## Functions
### createAction
**createAction**\<`P`, `T`\>(`type`): `PayloadActionCreator`\<`P`, `T`\>
A utility function to create an action creator for the given action type
string. The action creator accepts a single argument, which will be included
in the action object as a field called payload. The action creator function
will also have its toString() overridden so that it returns the action type,
allowing it to be used in reducer logic that is looking for that action type.
#### Type parameters
| Name | Type |
| :------ | :------ |
| `P` | `void` |
| `T` | extends `string` = `string` |
#### Parameters
| Name | Type | Description |
| :------ | :------ | :------ |
| `type` | `T` | The action type to use for created actions. |
#### Returns
`PayloadActionCreator`\<`P`, `T`\>
**createAction**\<`PA`, `T`\>(`type`, `prepareAction`): `PayloadActionCreator`\<`ReturnType`\<`PA`\>[``"payload"``], `T`, `PA`\>
A utility function to create an action creator for the given action type
string. The action creator accepts a single argument, which will be included
in the action object as a field called payload. The action creator function
will also have its toString() overridden so that it returns the action type,
allowing it to be used in reducer logic that is looking for that action type.
#### Type parameters
| Name | Type |
| :------ | :------ |
| `PA` | extends `PrepareAction`\<`any`\> |
| `T` | extends `string` = `string` |
#### Parameters
| Name | Type | Description |
| :------ | :------ | :------ |
| `type` | `T` | The action type to use for created actions. |
| `prepareAction` | `PA` | - |
#### Returns
`PayloadActionCreator`\<`ReturnType`\<`PA`\>[``"payload"``], `T`, `PA`\>
___
### withPayload
**withPayload**\<`P`\>(): (`input`: `P`) => \{ `payload`: `P` }
#### Type parameters
| Name |
| :------ |
| `P` |
#### Returns
`fn`
▸ (`input`): `Object`
##### Parameters
| Name | Type |
| :------ | :------ |
| `input` | `P` |
##### Returns
`Object`
| Name | Type |
| :------ | :------ |
| `payload` | `P` |
**withPayload**\<`P`, `A`\>(`prepare`): (...`input`: `A`) => \{ `payload`: `P` }
#### Type parameters
| Name | Type |
| :------ | :------ |
| `P` | `P` |
| `A` | extends `any`[] |
#### Parameters
| Name | Type |
| :------ | :------ |
| `prepare` | (...`args`: `A`) => `P` |
#### Returns
`fn`
▸ (`...input`): `Object`
##### Parameters
| Name | Type |
| :------ | :------ |
| `...input` | `A` |
##### Returns
`Object`
| Name | Type |
| :------ | :------ |
| `payload` | `P` |

View File

@ -0,0 +1,47 @@
---
title: Introduction
---
Updux is a class that collects together all the stuff pertaining to a Redux
reducer -- actions, middleware, subscriptions, selectors -- in a way that
is as modular and as TypeScript-friendly as possible.
While it has originally been created to play well with [updeep][], it also
work wells with plain JavaScript, and
interfaces very neatly with other immutability/deep merging libraries
like
[Mutative], [immer][], [updeep][],
[remeda][],
[lodash][], etc.
## Updux terminology
<dl>
<dt>Updux</dt>
<dd>Object encapsulating the information pertinent for a Redux reducer.
Named thus because it has been designed to work well with [updeep][],
and follows my spin on
the [Ducks pattern](https://github.com/erikras/ducks-modular-redux).</dd>
<dt>Mutation</dt>
<dd>Reducing function, mostly associated with an action. All mutations of
an updux object are combined to form its reducer.</dd>
<dt>Subdux</dt>
<dd>Updux objects associated with sub-states of a main updux. The main
updux will inherit all of its subduxes actions, mutations, reactions,
etc.</dd>
<dt>Effect</dt>
<dd>A Redux middleware, augmented with a few goodies.</dd>
<dt>Reaction</dt>
<dd>Subscription to a updux. Unlike regular Redux subscriptions, don't
trigger if the state of the updux isn't changed by the reducing.</dd>
</dl>
[updeep]: https://www.npmjs.com/package/@yanick/updeep-remeda
[immer]: https://www.npmjs.com/package/immer
[lodash]: https://www.npmjs.com/package/lodash
[ts-action]: https://www.npmjs.com/package/ts-action
[remeda]: remedajs.com/
[Mutative]: https://mutative.js.org/

View File

@ -0,0 +1,32 @@
---
title: State definition
---
The state of a Updux store is automatically interpolated from the constructor
argument `initialState` (which also provides the reducer's default initial state)..
```js
const myDux = new Updux({
initialState: {
counter: 1,
name: 'foo',
}
});
```
If `initialState` is not provided, it defaults to an empty object.
## Manually providing the store state
Sometimes -- mostly when arrays are involved -- the initial state is not
providing the whole state type. In those cases specifying manually the type of
`initialState` will do the trick.
```js
type Todo = { label: string; done: boolean };
const initialState : Todo[] = [];
const myDux = new Updux({
initialState
});
```

View File

@ -0,0 +1,43 @@
---
title: Selectors
---
Selectors can be defined to get data derived from the state.
The selectors can be accessed as-is via the accessor `selectors`.
More interestingly, the `getState` method of a dux store will be augmented
with its selectors, with the first call for the state already
curried for you.
```js
type Todo = { label: string; id: number; done: boolean };
const dux = new Updux({
initialState: [] as Todo[],
selectors: {
getDone: (state) => state.filter(({ done }) => done),
getById: (state) => (id) => state.find((todo) => todo.id === id),
},
});
const done = dux.selectors.getDone([ ... ]);
const myTodo = dux.selectors.getDone([...])(12);
```
### Store selectors
The store created by an updux will have the updux selectors available from a
`selectors` property. In addition, the store `getState` function will also have
the actions as properties.
```js
const store = dux.createStore();
const done = store.selectors.getDone([ ... ]);
const myTodo = store.selectors.getDone([...])(12);
const done = store.getState.getDone();
const myTodo = store.getState.getDone(12);
```

View File

@ -0,0 +1,96 @@
---
title: Actions
---
Updux actions are just the usual Redux actions. Updux exports two
utility functions: `createAction`, which is a convenience re-export of the
function provided by
[@reduxjs/toolkit](https://redux-toolkit.js.org/), and
`withPayload`, an utility function that makes defining the
action payload just a little more concise.
```js
const foo = createAction('foo', withPayload<string>() );
// equivalent to
const foo = createAction('foo', (payload: string) => ({ payload }) ) );
const createAction('foo', withPayload( (level:number) => ({ level }) );
// equivalent to
const createAction('foo', (level: number) => ({ payload: { level } }) ) );
```
### Adding actions to an Updux object
Actions can be tied to an Updux object both at construction time
```js
const addTodo = createAction('addTodo', withPayload<string>());
const todoDone = createAction('todoDone', withPayload<number>());
const myDux = new Updux({
actions: {
addTodo,
todoDone,
}
});
```
or they will be automatically added when used in methods like `addMutation`.
```js
const myDux = new Updux({})
.addMutation( addTodo, (label) => todos => todos.concat({ label });
```
:::danger
Note the chained invocation of `addMutation`, which is needed if you want
the type of myDux to include the `myAction` action.
```js
// myDux will have the right type for myDux.actions.myAction
myDux = new Updux({})
.addMutation( myAction, (payload) => state => state + payload );
// myDux type will not know about myAction.
myDux = new Updux({});
myDux.addMutation( myAction, (payload) => state => state + payload );
``````
:::
### Accessing Updux actions
Once an action is defined, its creator is accessible via the `actions` accessor.
```js
myDux.actions.addTodo('write tutorial');
// => { type: 'addtodo', payload: 'write tutorial' }
```
### Store actions
The store created by an updux will have the updux actions available from a
`actions` property. In addition, the store `dispatch` function will also have
the actions as properties, which can be used to create the actions and
dispatch them immediately.
```js
const myDux = new Updux({
actions: {
foo: (x: string) => x,
}
});
const store = myDux.createStore();
const a = store.actions.foo('bar'); // { type: 'foo', payload: 'bar' }
store.dispatch.foo('bar');
// equivalent to
// store.dispatch( store.actions.foo('bar') );
```

View File

@ -0,0 +1,86 @@
---
title: Mutations
---
Mutations are what tie actions to a reducing function.
Their signature is a smidge different than the typical
Redux reducing functions:
```js
( actionPayload, action ) => state => newState
```
## Defining a mutation
They are tied to actions in an updux via the method `addMutation`.
```js
todosDux.addMutation(addTodo, (description) => ({ todos, nextId }) => ({
nextId: 1 + nextId,
todos: todos.concat({ description, id: nextId, done: false }),
}));
todosDux.addMutation(todoDone, (id) => ({ todos, nextId }) => ({
nextId: 1 + nextId,
todos: todos.map((todo) => {
if (todo.id !== id) return todo;
return {
...todo,
done: true,
};
}),
}));
```
The first argument can be an action creator (the action will be added to the
actions known to the updux), an action type (must be the type of an action
already known to the updux), a function taking in an action and returning a
boolean.
```
const addTodo = createAction( 'addTodo', withPayload<string>() );
const todoDone = createAction( 'todoDone', withPayload<number>() )
const myDux = new Updux({
actions: {
addTodo
}
});
// valid
myDux.addMutation( addTodo, ... );
// valid
myDux.addMutation( 'addTodo', ... );
// invalid! todoDone is not known to myDux
myDux.addMutation( 'todoDone', ... )
// valid
myDux.addMutation( todoDone, ... )
// valid
myDux.addMutation(
(action) => action.type.startsWidth('todo'),
...
);
```
The first argument can also be omitted to have a mutation that
is applied for all incoming actions.
```js
myDux.addMutation( (payload,action) => state => state+1 );
```
## Default mutation
It's possible to set a mutation that will be triggered if an action doesn't match
any of the other updux mutations (excluding subdux mutations).
```js
myDux.setDefaultMutation( (payload,action) => state => state+1 );
```

View File

@ -0,0 +1,47 @@
---
title: Effects
---
In addition to mutations, Updux also provides action-specific middleware, here
called effects.
Effects use the usual Redux middleware signature, plus a few goodies.
The `getState` and `dispatch` functions are augmented with the dux selectors
and actions, respectively. The selectors and actions are also available
from the api object.
```js
const myEffect = ({ getState, dispatch, actions, selectors })
=> (next) => (action) => {
// ...
};
```
Assigning effects to an updux is done via `addEffect`, and follows the same
pattern as `addMutation`. The filter can be the type of an action,
an action creator, a guard function, or nothing at all.
```js
const myEffect = ({getState,dispatch}) => (next) => (action) => {
next(action);
if( getState.getSpecialThing() )
dispatch.doTheOtherThing();
};
// valid
myDux.addEffect( addTodo, myEffect );
// valid
myDux.addEffect( 'addTodo', myEffect );
// valid
myDux.addEffect(
(action) => action.type.startsWidth('todo'),
myEffect
);
// valid
myDux.addEffect( myEffect );
``````

View File

@ -0,0 +1,28 @@
---
title: Subduxes
---
Subduxes are how Updux goes recursive. They are upduxes (or updux
configurations) themselves and are provided at contruction time.
```js
const bar = ;
const mainDux = new Updux({
subduxes: {
bar: new Updux({
initialState: {
a: 1
}
}),
baz: {
initialState: {
b: 'two'
}
}
}
});
```
All properties of the subduxes will be incorporated in the main updux.

View File

@ -0,0 +1,242 @@
---
title: What's Updux?
description: Updux manual
# template: splash
hero:
# tagline: Congrats on setting up a new Starlight project!
image:
file: ../../../public/updux-logo.svg
# actions:
# - text: Example Guide
# link: /guides/example/
# icon: right-arrow
# variant: primary
# - text: Read the Starlight docs
# link: https://starlight.astro.build
# icon: external
---
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/).
Right now the best way to understand the whole thing is to go
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
## 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,11 @@
---
title: Example Reference
description: A reference page in my new Starlight docs site.
---
Reference pages are ideal for outlining how things work in terse and clear terms.
Less concerned with telling a story or addressing a specific use case, they should give a comprehensive outline of what you're documenting.
## Further reading
- Read [about reference](https://diataxis.fr/reference/) in the Diátaxis framework

View File

@ -0,0 +1,74 @@
---
title: Actions
sidebar:
order: 3
---
import { Code } from '@astrojs/starlight/components';
import snippet from './code/actions.test.ts?raw';
import extractSnippet from './extractSnippet.js';
Having a state is good and all of that, but it's also very static.
So let's introduce some reducing. First step: let's create some actions.
<Code code={extractSnippet('actions1')(snippet)} lang="js" />
`createAction` is actually provided by
[@reduxjs/toolkit](https://redux-toolkit.js.org/), we only re-export it
for convenience. `withPayload` is an utility function that makes defining the
action payload just a little more concise.
```js
createAction('foo', withPayload<string>() );
// equivalent to
createAction('foo', (payload: string) => ({ payload }) ) );
createAction('foo', withPayload( (level:number) => ({ level }) );
// equivalent to
createAction('foo', (level: number) => ({ payload: { level } }) ) );
```
### Adding actions to an Updux object
Actions can be tied to an Updux object both at construction time
```js
const myAction = createAction('myAction');
const myOtherAction = createAction('myOtherAction');
const myDux = new Updux({
actions: {
myAction,
myOtherAction,
}
});
```
or they will be automatically added when used in methods like `addMutation`.
```js
const myDux = new Updux({})
.addMutation( myAction, (payload) => state => state + payload );
```
Note the chained invocation of `addMutation`, which is needed if you want
the type of myDux to include the `myAction` action.
```js
// myDux will have the right type for myDux.actions.myAction
myDux = new Updux({})
.addMutation( myAction, (payload) => state => state + payload );
// myDux type will not know about myAction.
myDux = new Updux({});
myDux.addMutation( myAction, (payload) => state => state + payload );
```
### Accessing Updux actions
Once an action is defined, its creator is accessible via the `actions` accessor.
<Code code={extractSnippet('actions2')(snippet)} lang="js" />

View File

@ -0,0 +1 @@
../../../../../src/tutorial

View File

@ -0,0 +1,14 @@
export default (fence) => (snippet) => {
snippet = snippet.split("\n");
snippet = snippet.slice(snippet.findIndex(l => l.includes(fence)) + 1);
snippet = snippet.slice(0, snippet.findIndex(l => l.includes(fence)));
const indent = Math.min(
...snippet.map(l => l === '' ? 999 : l.match(/^ */)[0].length)
);
if (indent)
snippet = snippet.map(l => l.slice(indent));
return snippet.join("\n");
}

View File

@ -0,0 +1,20 @@
---
title: State definition
sidebar:
order: 2
---
import { Code } from '@astrojs/starlight/components';
import snippet from './code/initialState.test.ts?raw';
import extractSnippet from './extractSnippet.js';
Let's start slow with an Updux document that has nothing but an initial
state.
<Code code={extractSnippet('tut1')(snippet)} lang="js" />
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:
<Code code={extractSnippet('tut2')(snippet)} lang="js" />

View File

@ -0,0 +1,23 @@
---
title: Introduction
description: Introducing the tutorial
sidebar:
order: 1
---
This tutorial walks you through the features of `Updux` using the
time-honored example of the implementation of Todo list store.
For simplicity sake, we'll write our reducer code using plain JavaScript.
But for real-life applications where we want some help with
immutability and deep merging, Updux plays extremely well with libraries and
tools like [Mutative], [immer][], [updeep][],
[remeda][],
[lodash][], etc.
[updeep]: https://www.npmjs.com/package/@yanick/updeep-remeda
[immer]: https://www.npmjs.com/package/immer
[lodash]: https://www.npmjs.com/package/lodash
[ts-action]: https://www.npmjs.com/package/ts-action
[remeda]: remedajs.com/
[Mutative]: https://mutative.js.org/

View File

@ -0,0 +1,24 @@
---
title: Mutations
sidebar:
order: 4
---
import { Code } from '@astrojs/starlight/components';
import snippet from './code/actions.test.ts?raw';
import extractSnippet from './extractSnippet.js';
Mutations are what tie actions to a reducing function.
## Defining a mutation
The signature of a mutation is just a smidge different than the typical
Redux reducing functions:
```js
( actionPayload, action ) => state => newState
```
They are tied to actions in an updux via the method `addMutation`.
<Code code={extractSnippet('addMutation-1')(snippet)} lang="js" />

2
docs/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

View File

@ -1,28 +0,0 @@
import Updux from '../src/index.js';
import nextIdDux from './nextId';
import todosDux from './todos.js';
const todosListDux = new Updux({
subduxes: {
todos: todosDux,
nextId: nextIdDux,
},
actions: {
addTodo: (description: string) => description,
}
});
todosListDux.addEffect(
'addTodo', ({ getState, dispatch }) => next => action => {
const id = getState.getNextId();
dispatch.incNextId();
next(action);
dispatch.addTodoWithId(action.payload, id);
}
);
export default todosListDux;

View File

@ -1,26 +0,0 @@
import Updux from '../src/index.js';
import u from '@yanick/updeep-remeda';
import todoDux from './todo.js';
const todosDux = new Updux({
initialState: [] as (typeof todoDux.initialState)[],
actions: {
addTodoWithId: (description, id) => ({ description, id }),
todoDone: (id: number) => (id),
},
findSelectors: {
getTodoById: state => id => state.find(u.matches({ id }))
}
});
todosDux.addMutation('addTodoWithId', todo => todos => todos.concat({ ...todo, done: false }));
todosDux.addMutation(
'todoDone', (id, action) => u.map(
u.if(u.matches({ id }), todoDux.upreducer(action))
)
)
export default todosDux.asDux;

10
docs/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */,
"paths": {
"updux": ["../src/index.ts"],
"@tutorial/*": [ "../src/tutorial/*"]
}
}
}

View File

@ -1,19 +0,0 @@
import { test, expect } from 'vitest';
/// [tut1]
import Updux from 'updux';
const todosDux = new Updux({
initialState: {
nextId: 1,
todos: [],
}
});
/// [tut1]
/// [tut2]
const store = todosDux.createStore();
store.getState(); // { nextId: 1, todos: [] }
/// [tut2]
test("basic", () => {
expect(store.getState()).toEqual({
nextId: 1, todos: []
});
});

View File

@ -1,25 +0,0 @@
import { test, expect } from 'vitest';
/// --8<-- [start:tut1]
import Updux from 'updux';
const todosDux = new Updux({
initialState: {
nextId: 1,
todos: [],
}
});
/// ---8<-- [end:tut1]
/// ---8<-- [start:tut2]
const store = todosDux.createStore();
store.getState(); // { nextId: 1, todos: [] }
/// ---8<-- [end:tut2]
test("basic", () => {
expect(store.getState()).toEqual({
nextId: 1, todos: []
})
});

View File

@ -1,56 +0,0 @@
import { test, expect } from 'vitest';
/// [actions1]
import Updux, { createAction, withPayload } from 'updux';
const addTodo = createAction('addTodo', withPayload());
const todoDone = createAction('todoDone', withPayload());
const todosDux = new Updux({
initialState: {
nextId: 1,
todos: [],
},
actions: {
addTodo,
todoDone,
}
});
/// [actions1]
/// [actions2]
todosDux.actions.addTodo('write tutorial');
// { type: 'addTodo', payload: 'write tutorial' }
/// [actions2]
test("basic", () => {
expect(todosDux.actions.addTodo('write tutorial')).toEqual({
type: 'addTodo', payload: 'write tutorial'
});
});
/// [addMutation]
todosDux.addMutation(addTodo, (description) => (state) => {
state.todos.unshift({ description, id: state.nextId, done: false });
state.nextId++;
});
const store = todosDux.createStore();
store.dispatch.addTodo('write tutorial');
const state = store.getState();
// {
// nextId: 2,
// todos: [
// {
// description: 'write tutorial',
// done: false,
// id: 1,
// }
// ]
// }
/// [addMutation]
test("addMutation", () => {
expect(state).toEqual({
nextId: 2,
todos: [
{
description: 'write tutorial',
done: false,
id: 1,
}
]
});
});

View File

@ -1,37 +0,0 @@
import { test, expect } from 'vitest';
process.env.UPDEEP_MODE = "dangerously_never_freeze";
// [effects-1]
import u from '@yanick/updeep-remeda';
import * as R from 'remeda';
import Updux, { createAction, withPayload } from 'updux';
const addTodoWithId = createAction('addTodoWithId', withPayload());
const incNextId = createAction('incNextId');
const addTodo = createAction('addTodo', withPayload());
const todosDux = new Updux({
initialState: { nextId: 1, todos: [] },
actions: { addTodo, incNextId, addTodoWithId },
selectors: {
nextId: ({ nextId }) => nextId,
},
});
todosDux.addMutation(addTodoWithId, (todo) => state => state
// u({
// todos: R.concat([u(todo, { done: false })])
// })
);
todosDux.addMutation(incNextId, () => state => state); //u({ nextId: id => id + 1 }));
// todosDux.addEffect(addTodo, ({ getState, dispatch }) => next => action => {
// const id = getState.nextId();
// dispatch.incNextId();
// next(action);
// dispatch.addTodoWithId({ id, description: action.payload });
// });
const store = todosDux.createStore();
store.dispatch.addTodo('write tutorial');
// [effects-1]
test('basic', () => {
expect(store.getState()).toMatchObject({
nextId: 2,
todos: [{ id: 1, description: 'write tutorial', done: false }]
});
});

View File

@ -1,42 +0,0 @@
import { expectTypeOf } from 'expect-type';
import { test, expect } from 'vitest';
import todoListDux from './todoList.js';
test("basic", () => {
const store = todoListDux.createStore();
store.dispatch.addTodo('write tutorial');
store.dispatch.addTodo('test code snippets');
store.dispatch.todoDone(2);
const s = store.getState();
expectTypeOf(s).toMatchTypeOf<{
nextId: number
}>();
expect(store.getState()).toMatchObject({
todos: [
{ id: 1, done: false },
{ id: 2, done: true }
]
});
expect(todoListDux.schema).toMatchObject({
type: 'object',
properties: {
nextId: { type: 'number', default: 1 },
todos: {
default: [],
type: 'array',
}
},
default: {
nextId: 1,
todos: [],
},
});
});

View File

@ -1,39 +0,0 @@
import { test, expect } from 'vitest';
import u from 'updeep';
import { Updux } from '../src/index.js';
const todos = new Updux({
initial: [],
actions: {
setNbrTodos: null,
addTodo: null,
},
mutations: {
addTodo: todo => todos => [ ...todos, todo ],
},
reactions: [
({dispatch}) => todos => dispatch.setNbrTodos(todos.length)
],
});
const myDux = new Updux({
initial: {
nbrTodos: 0
},
subduxes: {
todos,
},
mutations: {
setNbrTodos: nbrTodos => u({ nbrTodos })
}
});
test( "basic tests", async () => {
const store = myDux.createStore();
store.dispatch.addTodo('one');
store.dispatch.addTodo('two');
expect(store.getState().nbrTodos).toEqual(2);
});

View File

@ -1,27 +0,0 @@
import { test, expect } from 'vitest';
import { Updux } from '../src/index.js';
test( 'selectors', () => {
const dux = new Updux({
initial: { a: 1, b: 2 },
selectors: {
getA: ({a}) => a,
getBPlus: ({b}) => addition => b + addition,
},
subduxes: {
subbie: new Updux({
initial: { d: 3 },
selectors: {
getD: ({d}) => d
}
})
}
})
const store = dux.createStore();
expect( store.getState.getA() ).toEqual(1);
expect( store.getState.getBPlus(7) ).toEqual(9);
expect( store.getState.getD() ).toEqual(3);
} );

View File

@ -1,91 +0,0 @@
import { test, expect } from 'vitest';
import u from 'updeep';
import R from 'remeda';
import { Updux } from '../src/index.js';
const nextIdDux = new Updux({
initial: 1,
actions: {
incrementNextId: null,
},
selectors: {
getNextId: state => state
},
mutations: {
incrementNextId: () => state => state + 1,
}
});
const matches = conditions => target => Object.entries(conditions).every(
([key,value]) => typeof value === 'function' ? value(target[key]) : target[key] === value
);
const todoDux = new Updux({
initial: {
id: 0,
description: "",
done: false,
},
actions: {
todoDone: null,
},
mutations: {
todoDone: id => u.if( matches({id}), { done: true })
},
selectors: {
desc: R.prop('description'),
}
});
const todosDux = new Updux({
initial: [],
subduxes: {
'*': todoDux
},
actions: {
addTodoWithId: (description, id) => ({description, id} )
},
findSelectors: {
getTodoById: state => id => state.find(matches({id}))
},
mutations: {
addTodoWithId: todo => todos => [...todos, todo]
}
});
const mainDux = new Updux({
subduxes: {
nextId: nextIdDux,
todos: todosDux,
},
actions: {
addTodo: null
},
effects: {
addTodo: ({ getState, dispatch }) => next => action => {
const id = getState.getNextId();
dispatch.incrementNextId()
next(action);
dispatch.addTodoWithId( action.payload, id );
}
}
});
const store = mainDux.createStore();
test( "basic tests", () => {
const myDesc = 'do the thing';
store.dispatch.addTodo(myDesc);
expect( store.getState.getTodoById(1).desc() ).toEqual(myDesc);
});

View File

@ -1,26 +0,0 @@
import { test, expect } from 'vitest';
import { Updux } from '../src/index.js';
test( "basic checks", async () => {
const todosDux = new Updux({
initial: {
next_id: 1,
todos: [],
},
actions: {
addTodo: null,
todoDone: null,
}
});
const store = todosDux.createStore();
expect(store.getState()).toEqual({ next_id: 1, todos: [] });
expect(store.actions.addTodo("learn updux")).toMatchObject({
type: 'addTodo', payload: 'learn updux'
})
});

8
guides/+page.js Normal file
View File

@ -0,0 +1,8 @@
import { redirect } from '@sveltejs/kit';
export const prerender = true;
/** @type {import('./$types').PageLoad} */
export function load() {
throw redirect(307, '/docs/first-category/first-page');
}

View File

@ -0,0 +1,47 @@
---
title: Introduction
---
Updux is a class that collects together all the stuff pertaining to a Redux
reducer -- actions, middleware, subscriptions, selectors -- in a way that
is as modular and as TypeScript-friendly as possible.
While it has originally been created to play well with [updeep][], it also
work wells with plain JavaScript, and
interfaces very neatly with other immutability/deep merging libraries
like
[Mutative], [immer][], [updeep][],
[remeda][],
[lodash][], etc.
## Updux terminology
<dl>
<dt>Updux</dt>
<dd>Object encapsulating the information pertinent for a Redux reducer.
Named thus because it has been designed to work well with [updeep][],
and follows my spin on
the [Ducks pattern](https://github.com/erikras/ducks-modular-redux).</dd>
<dt>Mutation</dt>
<dd>Reducing function, mostly associated with an action. All mutations of
an updux object are combined to form its reducer.</dd>
<dt>Subdux</dt>
<dd>Updux objects associated with sub-states of a main updux. The main
updux will inherit all of its subduxes actions, mutations, reactions,
etc.</dd>
<dt>Effect</dt>
<dd>A Redux middleware, augmented with a few goodies.</dd>
<dt>Reaction</dt>
<dd>Subscription to a updux. Unlike regular Redux subscriptions, don't
trigger if the state of the updux isn't changed by the reducing.</dd>
</dl>
[updeep]: https://www.npmjs.com/package/@yanick/updeep-remeda
[immer]: https://www.npmjs.com/package/immer
[lodash]: https://www.npmjs.com/package/lodash
[ts-action]: https://www.npmjs.com/package/ts-action
[remeda]: remedajs.com/
[Mutative]: https://mutative.js.org/

View File

@ -1,7 +1,8 @@
{
"type": "module",
"dependencies": {
"@yanick/updeep-remeda": "^2.2.0",
"@mobily/ts-belt": "^3.13.1",
"@yanick/updeep-remeda": "^2.3.1",
"ajv": "^8.12.0",
"expect-type": "^0.16.0",
"immer": "^9.0.15",
@ -43,7 +44,7 @@
"devDependencies": {
"@reduxjs/toolkit": "==2.0.0-alpha.5 ",
"@vitest/browser": "^0.23.1",
"@vitest/ui": "^0.23.1",
"@vitest/ui": "^2.0.5",
"docsify": "^4.13.1",
"eslint": "^8.22.0",
"eslint-plugin-no-only-tests": "^3.0.0",
@ -56,6 +57,6 @@
"typedoc-plugin-markdown": "^3.17.1",
"typescript": "^4.9.5",
"vite": "^4.2.1",
"vitest": "0.23.1"
"vitest": "2.0.4"
}
}

View File

@ -1,13 +0,0 @@
import { test, expect } from 'vitest';
import Updux from './Updux.js';
test('subdux idempotency', () => {
const foo = new Updux({
subduxes: {
a: new Updux({ initialState: 2 }),
},
});
let fooState = foo.reducer(undefined, { type: 'noop' });
expect(foo.reducer(fooState, { type: 'noop' })).toBe(fooState);
});

View File

@ -1,198 +1,115 @@
import type { DuxConfig, DuxState } from './types.js';
import u from '@yanick/updeep-remeda';
import moize from 'moize/mjs/index.mjs';
import * as R from 'remeda';
import { expandAction, buildActions, DuxActions } from './actions.js';
import {
Action,
ActionCreator,
AnyAction,
configureStore,
EnhancedStore,
Action,
PayloadAction,
AnyAction,
ActionCreatorWithPreparedPayload,
} from '@reduxjs/toolkit';
import { produce } from 'immer';
import { buildReducer } from './reducer.js';
import { buildInitialState } from './initialState.js';
import { buildSelectors, DuxSelectors } from './selectors.js';
import { AugmentedMiddlewareAPI, augmentGetState } from './createStore.js';
import {
augmentMiddlewareApi,
buildEffects,
buildEffectsMiddleware,
EffectMiddleware,
} from './effects.js';
import buildSchema from './schema.js';
import Ajv from 'ajv';
AugmentedMiddlewareAPI,
DuxActions,
DuxConfig,
DuxReaction,
DuxSelectors,
DuxState,
Mutation,
} from './types.js';
import baseMoize from 'moize/mjs/index.mjs';
import { buildInitialState } from './initialState.js';
import { buildActions } from './actions.js';
import { D } from '@mobily/ts-belt';
import { buildReducer } from './reducer.js';
import { buildSelectors } from './selectors.js';
import { buildEffectsMiddleware, buildEffects, EffectMiddleware, augmentMiddlewareApi } from './effects.js';
import { augmentGetState, augmentDispatch } from './createStore.js';
import { buildReactions } from './reactions.js';
export type Mutation<A = AnyAction, S = any> = (
payload: A extends {
payload: infer P;
}
? P
: undefined,
action: A,
) => (state: S) => S | void;
type CreateStoreOptions<D> = Partial<{
preloadedState: DuxState<D>;
}>;
function buildValidateMiddleware(schema) {
// @ts-ignore
const ajv = new Ajv();
const validate = ajv.compile(schema);
return (api) => next => action => {
next(action);
const valid = validate(api.getState());
if (!valid) throw new Error("validation failed after action " + JSON.stringify(action) + "\n" + JSON.stringify(validate.errors));
}
interface ActionCreator<T extends string, P, A extends Array<any>> {
type: T;
match: (
action: Action<unknown>,
) => action is PayloadAction<P, T, never, never>;
(...args: A): PayloadAction<P, T, never, never>;
}
const moize = (func) => baseMoize(func, { maxSize: 1 });
/**
* @description main Updux class
*/
export default class Updux<D extends DuxConfig> {
#mutations = [];
/** @internal */
#subduxes: {};
/** @internal */
#memoInitialState = moize(buildInitialState);
/** @internal */
#memoBuildReducer = moize(buildReducer);
/** @internal */
#memoBuildSelectors = moize(buildSelectors);
/** @internal */
#memoBuildEffects = moize(buildEffects);
/** @internal */
#memoBuildReactions = moize(buildReactions);
/** @internal */
#actions: Record<string, ActionCreator<string, any, any>> = {};
/** @internal */
#defaultMutation: {
terminal: boolean;
mutation: Mutation<any, DuxState<D>>;
};
/** @internal */
#effects = [];
/** @internal */
#reactions: DuxReaction<D>[] = [];
/** @internal */
#mutations: any[] = [];
constructor(private readonly duxConfig: D) {
// just to warn at creation time if config has issues
this.actions;
if (duxConfig.subduxes)
this.#subduxes = D.map(duxConfig.subduxes, (s) =>
s instanceof Updux ? s : new Updux(s),
);
this.#actions = buildActions(duxConfig.actions, this.#subduxes) as any;
}
memoInitialState = moize(buildInitialState, {
maxSize: 1,
});
memoBuildActions = moize(buildActions, {
maxSize: 1,
});
memoBuildReducer = moize(buildReducer, { maxSize: 1 });
memoBuildSelectors = moize(buildSelectors, { maxSize: 1 });
memoBuildEffects = moize(buildEffects, { maxSize: 1 });
memoBuildSchema = moize(buildSchema, { maxSize: 1 });
get schema() {
return this.memoBuildSchema(
this.duxConfig.schema,
this.initialState,
this.duxConfig.subduxes,
)
}
/**
* @description Initial state of the dux.
*/
get initialState(): DuxState<D> {
return this.memoInitialState(
return this.#memoInitialState(
this.duxConfig.initialState,
this.duxConfig.subduxes,
this.#subduxes,
);
}
get subduxes() {
return this.#subduxes;
}
get actions(): DuxActions<D> {
return this.memoBuildActions(
this.duxConfig.actions,
this.duxConfig.subduxes,
);
}
toDux() {
return {
initialState: this.initialState,
actions: this.actions,
reducer: this.reducer,
effects: this.effects,
reactions: this.reactions,
selectors: this.selectors,
upreducer: this.upreducer,
schema: this.schema,
};
}
get asDux() {
return this.toDux();
}
get foo(): DuxActions<D> {
return true as any;
}
get upreducer() {
const reducer = this.reducer;
return action => state => reducer(state, action);
}
// TODO be smarter with the guard?
addMutation<A extends keyof DuxActions<D>>(
matcher: A,
mutation: Mutation<DuxActions<D>[A] extends (...args: any) => infer P ? P : never, DuxState<D>>,
terminal?: boolean,
): Updux<D>;
addMutation<A extends Action<any>>(
matcher: (action: A) => boolean,
mutation: Mutation<A, DuxState<D>>,
terminal?: boolean,
): Updux<D>;
addMutation<A extends ActionCreator<any>>(
actionCreator: A,
mutation: Mutation<ReturnType<A>, DuxState<D>>,
terminal?: boolean,
): Updux<D>;
addMutation(matcher, mutation, terminal = false) {
if (typeof matcher === 'string') {
if (!this.actions[matcher]) {
throw new Error(`action ${matcher} is unknown`);
}
matcher = this.actions[matcher];
}
if (typeof matcher === 'function' && matcher.match) {
// matcher, matcher man...
matcher = matcher.match;
}
//const immerMutation = (...args) => produce(mutation(...args));
this.#mutations = [
...this.#mutations,
{
terminal,
matcher,
mutation,
},
];
return this;
}
#defaultMutation;
addDefaultMutation(
mutation: Mutation<any, DuxState<D>>,
terminal?: boolean,
);
addDefaultMutation(mutation, terminal = false) {
this.#defaultMutation = { terminal, mutation };
}
get reducer() {
return this.memoBuildReducer(
this.initialState,
this.#mutations,
this.#defaultMutation,
this.duxConfig.subduxes,
);
}
get selectors(): DuxSelectors<D> {
return this.memoBuildSelectors(
this.duxConfig.selectors,
this.duxConfig.subduxes,
) as any;
return this.#actions as any;
}
createStore(
options: Partial<{
preloadedState: DuxState<D>;
validate: boolean;
buildMiddleware: (middleware: any[]) => any
}> = {},
options: CreateStoreOptions<D> = {},
): EnhancedStore<DuxState<D>> & AugmentedMiddlewareAPI<D> {
const preloadedState = options.preloadedState;
@ -204,32 +121,19 @@ export default class Updux<D extends DuxConfig> {
let middleware = [effects];
if (options.validate) {
middleware.unshift(
buildValidateMiddleware(this.schema)
)
}
if (options.buildMiddleware)
middleware = options.buildMiddleware(middleware);
const store = configureStore({
reducer: this.reducer,
const store: any = configureStore({
preloadedState,
reducer: this.reducer,
middleware,
});
const dispatch: any = store.dispatch;
for (const a in this.actions) {
dispatch[a] = (...args) => {
const action = (this.actions as any)[a](...args);
dispatch(action);
return action;
};
}
store.dispatch = augmentDispatch(store.dispatch, this.actions);
store.getState = augmentGetState(store.getState, this.selectors);
store.actions = this.actions;
store.selectors = this.selectors;
for (const reaction of this.reactions) {
let unsub;
const r = reaction(store);
@ -237,32 +141,110 @@ export default class Updux<D extends DuxConfig> {
unsub = store.subscribe(() => r(unsub));
}
(store as any).actions = this.actions;
(store as any).selectors = this.selectors;
return store as any;
// return store as ToolkitStore<AggregateState<T_LocalState, T_Subduxes>> &
// AugmentedMiddlewareAPI<
// AggregateState<T_LocalState, T_Subduxes>,
// AggregateActions<ResolveActions<T_LocalActions>, T_Subduxes>,
// AggregateSelectors<
// T_LocalSelectors,
// T_Subduxes,
// AggregateState<T_LocalState, T_Subduxes>
// >
// >;
}
#effects = [];
addAction(action: ActionCreator<any, any, any>) {
const { type } = action as any;
if (!this.#actions[type]) {
this.#actions = {
...this.#actions,
[type]: action,
};
return;
}
if (this.#actions[type] !== action)
throw new Error(`redefining action ${type}`);
}
addEffect(
actionType: keyof DuxActions<D>,
effect: EffectMiddleware<D>,
addMutation<A extends keyof DuxActions<D>>(
actionType: A,
mutation: Mutation<
DuxActions<D>[A] extends (...args: any[]) => infer R ? R : AnyAction,
DuxState<D>
>,
terminal?: boolean,
): Updux<D>;
addEffect(
actionCreator: { match: (action: any) => boolean },
effect: EffectMiddleware<D>,
addMutation<AC extends ActionCreatorWithPreparedPayload<any, any, string, never, never>>(
actionCreator: AC,
mutation: Mutation<ReturnType<AC>, DuxState<D>>,
terminal?: boolean
): Updux<D & {
actions: Record<
AC extends { type: infer T } ? T : never
, AC
>
}
>;
addMutation<A extends string, P, X extends Array<any>>(
actionCreator: ActionCreator<A, P, X>,
mutation: Mutation<ReturnType<ActionCreator<A, P, X>>, DuxState<D>>,
terminal?: boolean,
): Updux<D & {
actions: Record<A, ActionCreator<A, P, X>>;
}
>;
addMutation(
matcher: (action: AnyAction) => boolean,
mutation: Mutation<AnyAction, DuxState<D>>,
terminal?: boolean,
): Updux<D>;
addMutation(actionCreator, mutation, terminal = false) {
if (typeof actionCreator === 'string') {
if (!this.actions[actionCreator]) throw new Error(`action ${actionCreator} not found`);
actionCreator = this.actions[actionCreator];
}
else {
this.addAction(actionCreator);
}
this.#mutations = this.#mutations.concat({
terminal,
matcher: (actionCreator as any).match ?? actionCreator,
mutation,
});
return this as any;
}
setDefaultMutation(
mutation: Mutation<any, DuxState<D>>,
terminal?: boolean,
);
setDefaultMutation(mutation, terminal = false) {
this.#defaultMutation = { terminal, mutation };
return this;
}
get reducer() {
return this.#memoBuildReducer(
this.initialState,
this.#mutations,
this.#defaultMutation,
this.#subduxes,
);
}
get selectors(): DuxSelectors<D> {
return this.#memoBuildSelectors(
this.duxConfig.selectors,
this.#subduxes,
) as any;
}
addEffect<A extends keyof DuxActions<D>>(
actionType: A,
effect: EffectMiddleware<D,
DuxActions<D>[A] extends (...args: any[]) => infer R ? R : AnyAction
>,
): Updux<D>;
addEffect<AC extends ActionCreatorWithPreparedPayload<any, any, string, never, never>>(
actionCreator: AC,
effect: EffectMiddleware<D, ReturnType<AC>>,
): Updux<D>;
addEffect(
guardFunc: (action: AnyAction) => boolean,
@ -298,53 +280,45 @@ export default class Updux<D extends DuxConfig> {
};
}
this.#effects = [...this.#effects, effect];
this.#effects = this.#effects.concat(effect);
return this;
return this as any;
}
get effects(): any {
return this.memoBuildEffects(this.#effects, this.duxConfig.subduxes);
return this.#memoBuildEffects(this.#effects, this.#subduxes);
}
get upreducer() {
return (action: AnyAction) => (state?: DuxState<D>) => this.reducer(state, action);
}
#reactions = [];
addReaction(
reaction// :DuxReaction<D>
reaction: DuxReaction<D>
) {
let previous: any;
const memoized = (api: any) => {
api = augmentMiddlewareApi(api, this.actions, this.selectors);
const r = reaction(api);
return (unsub: () => void) => {
const state = api.getState();
if (state === previous) return;
const rMemoized = (localState, unsub) => {
let p = previous;
previous = state;
r(state, p, unsub);
previous = localState;
r(localState, p, unsub);
};
return (unsub: () => void) => rMemoized(api.getState(), unsub);
};
this.#reactions = [
...this.#reactions, memoized
]
this.#reactions =
this.#reactions.concat(memoized as any);
return this;
}
get reactions() {
return [
...this.#reactions,
...(Object.entries(this.duxConfig.subduxes ?? {}) as any).flatMap(
([slice, { reactions }]) =>
reactions.map(
(r) => (api, unsub) =>
r(
{
...api,
getState: () => api.getState()[slice],
},
unsub,
),
),
),
];
return this.#memoBuildReactions(this.#reactions, this.#subduxes);
}
}

94
src/Updux.ts.2024-07-29 Normal file
View File

@ -0,0 +1,94 @@
import type { DuxConfig, DuxState } from './types.js';
import u from '@yanick/updeep-remeda';
import moize from 'moize/mjs/index.mjs';
import * as R from 'remeda';
import { expandAction, buildActions, DuxActions } from './actions.js';
import {
Action,
ActionCreator,
AnyAction,
configureStore,
EnhancedStore,
} from '@reduxjs/toolkit';
import { produce } from 'immer';
import { buildReducer } from './reducer.js';
import { buildInitialState } from './initialState.js';
import { buildSelectors, DuxSelectors } from './selectors.js';
import { AugmentedMiddlewareAPI, augmentGetState } from './createStore.js';
import {
augmentMiddlewareApi,
buildEffects,
buildEffectsMiddleware,
EffectMiddleware,
} from './effects.js';
import buildSchema from './schema.js';
import Ajv from 'ajv';
function buildValidateMiddleware(schema) {
// @ts-ignore
const ajv = new Ajv();
const validate = ajv.compile(schema);
return (api) => next => action => {
next(action);
const valid = validate(api.getState());
if (!valid) throw new Error("validation failed after action " + JSON.stringify(action) + "\n" + JSON.stringify(validate.errors));
}
}
export default class Updux<D extends DuxConfig> {
#mutations = [];
get schema() {
return this.memoBuildSchema(
this.duxConfig.schema,
this.initialState,
this.duxConfig.subduxes,
)
}
}
createStore(
options: Partial<{
preloadedState: DuxState<D>;
validate: boolean;
buildMiddleware: (middleware: any[]) => any
}> = {},
): EnhancedStore<DuxState<D>> & AugmentedMiddlewareAPI<D> {
const preloadedState = options.preloadedState;
if (options.validate) {
middleware.unshift(
buildValidateMiddleware(this.schema)
)
}
if (options.buildMiddleware)
middleware = options.buildMiddleware(middleware);
const store = configureStore({
reducer: this.reducer,
preloadedState,
middleware,
});
return store as any;
// return store as ToolkitStore<AggregateState<T_LocalState, T_Subduxes>> &
// AugmentedMiddlewareAPI<
// AggregateState<T_LocalState, T_Subduxes>,
// AggregateActions<ResolveActions<T_LocalActions>, T_Subduxes>,
// AggregateSelectors<
// T_LocalSelectors,
// T_Subduxes,
// AggregateState<T_LocalState, T_Subduxes>
// >
// >;
}
}

View File

@ -1,11 +1,6 @@
import Updux from './Updux.js';
import { createAction, withPayload, expandAction } from './actions.js';
import type { ExpandedAction } from './actions.js';
import { expectTypeOf } from 'expect-type';
import {
ActionCreatorWithoutPayload,
ActionCreatorWithPreparedPayload,
} from '@reduxjs/toolkit';
import { ActionCreatorWithPreparedPayload } from '@reduxjs/toolkit';
import { buildActions, createAction, withPayload } from './actions.js';
import Updux from './index.js';
test('basic action', () => {
const foo = createAction(
@ -28,187 +23,91 @@ test('basic action', () => {
}>();
});
test('Updux config accepts actions', () => {
const foo = new Updux({
actions: {
one: createAction(
'one',
withPayload((x) => ({ x })),
),
two: createAction(
'two',
withPayload((x) => x),
),
},
test('withPayload, just the type', () => {
const foo = createAction('foo', withPayload<string>());
expect(foo('bar')).toEqual({
type: 'foo',
payload: 'bar',
});
expect(Object.keys(foo.actions)).toHaveLength(2);
expectTypeOf(foo).parameters.toMatchTypeOf<[string]>();
expect(foo.actions.one).toBeTypeOf('function');
expect(foo.actions.one('potato')).toEqual({
expectTypeOf(foo).returns.toMatchTypeOf<{
type: 'foo';
payload: string;
}>();
});
test('buildActions', () => {
const actions = buildActions(
{
one: createAction('one'), two: (x: string) => x,
withoutValue: null,
},
{ a: { actions: buildActions({ three: () => 3 }) } },
);
expect(actions.one()).toEqual({
type: 'one',
payload: {
x: 'potato',
},
});
expectTypeOf(foo.actions.one).toMatchTypeOf<
ActionCreatorWithPreparedPayload<any, any, any, any>
>();
expectTypeOf(actions.one()).toMatchTypeOf<{
type: 'one';
}>();
expect(actions.two('potato')).toEqual({ type: 'two', payload: 'potato' });
expectTypeOf(actions.two('potato')).toMatchTypeOf<{
type: 'two';
payload: string;
}>();
expect(actions.three()).toEqual({ type: 'three', payload: 3 });
expectTypeOf(actions.three()).toMatchTypeOf<{
type: 'three';
payload: number;
}>();
expect(actions.withoutValue()).toEqual({ type: 'withoutValue' });
expectTypeOf(actions.withoutValue()).toMatchTypeOf<{
type: 'withoutValue';
}>();
});
describe('expandAction', () => {
test('as-is', () => {
const result = expandAction(
createAction('foo', withPayload<boolean>()),
);
expectTypeOf(result).toMatchTypeOf<
(input: boolean) => { type: 'foo'; payload: boolean }
>();
expect(result(true)).toMatchObject({
type: 'foo',
payload: true,
});
});
test('0', () => {
const result = expandAction(0, 'foo');
expectTypeOf(result).toMatchTypeOf<() => { type: 'foo' }>();
expect(result()).toMatchObject({
type: 'foo',
});
});
test('function', () => {
const result = expandAction((size: number) => ({ size }), 'foo');
expectTypeOf(result).toMatchTypeOf<
() => { type: 'foo'; payload: { size: number } }
>();
expect(result(12)).toMatchObject({
type: 'foo',
payload: {
size: 12,
},
});
});
});
test('action types', () => {
const action1 = createAction('a1', withPayload<number>());
expectTypeOf(
true as any as ExpandedAction<typeof action1, 'a1'>,
).toMatchTypeOf<
ActionCreatorWithPreparedPayload<
[input: number],
number,
'a1',
never,
never
>
>();
const action2 = (input: boolean) => input;
let typed2: ExpandedAction<typeof action2, 'a2'>;
expectTypeOf(typed2).toMatchTypeOf<
ActionCreatorWithPreparedPayload<
[input: boolean],
boolean,
'a2',
never,
never
>
>();
let typed3: ExpandedAction<boolean, 'a3'>;
expectTypeOf(typed3).toMatchTypeOf<ActionCreatorWithoutPayload<'a3'>>();
});
test('action definition shortcut', () => {
const foo = new Updux({
describe('Updux interactions', () => {
const dux = new Updux({
initialState: { a: 3 },
actions: {
foo: 0,
bar: (x: number) => ({ x }),
baz: createAction('baz', withPayload<boolean>()),
},
});
expect(foo.actions.foo()).toEqual({ type: 'foo', payload: undefined });
expect(foo.actions.baz(false)).toEqual({
type: 'baz',
payload: false,
});
expect(foo.actions.bar(2)).toEqual({
type: 'bar',
payload: { x: 2 },
});
expectTypeOf(foo.actions.foo).toMatchTypeOf<ActionCreatorWithoutPayload<'foo'>>();
});
test('subduxes actions', () => {
const bar = createAction<number>('bar');
const baz = createAction('baz');
const foo = new Updux({
actions: {
bar,
add: (x: number) => x,
withNull: null,
},
subduxes: {
beta: {
subdux1: new Updux({
actions: {
baz,
},
},
// to check if we can deal with empty actions
gamma: {},
},
fromSubdux: (x: string) => x
}
}),
subdux2: {
actions: {
ohmy: () => { }
}
}
}
});
expect(foo.actions).toHaveProperty('bar');
expect(foo.actions).toHaveProperty('baz');
test('actions getter', () => {
expect(dux.actions.add).toBeTruthy();
});
expect(foo.actions.bar(2)).toHaveProperty('type', 'bar');
expect(foo.actions.baz()).toHaveProperty('type', 'baz');
expectTypeOf(dux.actions.withNull).toMatchTypeOf<
ActionCreatorWithPreparedPayload<any, null, 'withNull'>>();
expectTypeOf(dux.actions).toMatchTypeOf<Record<string, any>>();
expectTypeOf(dux.actions?.add).not.toEqualTypeOf<any>();
expect(dux.actions.fromSubdux('payload')).toEqual({ type: 'fromSubdux', payload: 'payload' });
expectTypeOf(foo.actions.baz).toMatchTypeOf<
ActionCreatorWithoutPayload<'baz'>
>();
});
test('throw if double action', () => {
expect(
() =>
new Updux({
actions: {
foo: createAction('foo'),
},
subduxes: {
beta: {
actions: {
foo: createAction('foo'),
},
},
},
}),
).toThrow(/action 'foo' defined both locally and in subdux 'beta'/);
expect(
() =>
new Updux({
subduxes: {
gamma: {
actions: {
foo: createAction('foo'),
},
},
beta: {
actions: {
foo: createAction('foo'),
},
},
},
}),
).toThrow(/action 'foo' defined both in subduxes 'gamma' and 'beta'/);
});

View File

@ -1,54 +1,8 @@
import {
ActionCreator,
ActionCreatorWithoutPayload,
createAction,
} from '@reduxjs/toolkit';
import { createAction } from '@reduxjs/toolkit';
import { D } from '@mobily/ts-belt';
import { DuxActions, DuxConfig, Subduxes } from './types.js';
export { createAction } from '@reduxjs/toolkit';
import { ActionCreatorWithPreparedPayload } from '@reduxjs/toolkit';
import type {
FromSchema,
SubduxesState,
DuxSchema,
DuxConfig,
UnionToIntersection,
} from './types.js';
import * as R from 'remeda';
export type DuxActions<D> = (D extends { actions: infer A }
? {
[key in keyof A]: key extends string
? ExpandedAction<A[key], key>
: never;
}
: {}) &
UnionToIntersection<
D extends { subduxes: infer S } ? DuxActions<S[keyof S]> : {}
>;
export type ExpandedAction<P, T extends string> = P extends (...a: any[]) => {
type: string;
}
? P
: P extends (...a: any[]) => any
? ActionCreatorWithPreparedPayload<
Parameters<P>,
ReturnType<P>,
T,
never,
never
>
: ActionCreatorWithoutPayload<T>;
export type DuxState<D> = (D extends { schema: Record<string, any> }
? FromSchema<DuxSchema<D>>
: D extends { initialState: infer INITIAL_STATE }
? INITIAL_STATE
: {}) &
(D extends { subduxes: Record<string, DuxConfig> }
? SubduxesState<D>
: unknown);
export function withPayload<P>(): (input: P) => { payload: P };
export function withPayload<P, A extends any[]>(
@ -60,20 +14,17 @@ export function withPayload(prepare = (input) => input) {
});
}
export function expandAction(prepare, actionType?: string) {
if (typeof prepare === 'function' && prepare.type) return prepare;
if (typeof prepare === 'function') {
return createAction(actionType, withPayload(prepare));
}
if (actionType) {
return createAction(actionType);
}
}
export function buildActions<L extends DuxConfig['actions']>(
localActions: L,
): DuxActions<{ actions: L }>;
export function buildActions<L extends DuxActions<any>, S extends Subduxes>(
localActions: L,
subduxes: S,
): DuxActions<{ actions: L; subduxes: S }>;
export function buildActions(localActions = {}, subduxes = {}) {
localActions = R.mapValues(localActions, expandAction);
localActions = D.mapWithKey(localActions, (key, value) =>
expandAction(value, String(key)),
);
let actions: Record<string, string> = {};
@ -90,18 +41,27 @@ export function buildActions(localActions = {}, subduxes = {}) {
}
actions[a] = slice;
}
}
for (const a in localActions) {
if (actions[a]) {
throw new Error(
`action '${a}' defined both locally and in subdux '${actions[a]}'`,
);
for (const a in localActions) {
if (actions[a]) {
throw new Error(
`action '${a}' defined both locally and in subdux '${actions[a]}'`,
);
}
}
}
return R.mergeAll([
return [
localActions,
...Object.values(subduxes).map(R.pathOr<any, any>(['actions'], {})),
]) as any;
...D.values(subduxes).map((s: any) => s.actions ?? {}),
].reduce(D.merge);
}
export function expandAction(prepare, actionType?: string) {
if (typeof prepare === 'function' && prepare.type) return prepare;
if (typeof prepare === 'function')
return createAction(actionType, withPayload(prepare));
if (actionType) return createAction(actionType);
}

View File

@ -1,67 +1,30 @@
import { createAction } from '@reduxjs/toolkit';
import { expectTypeOf } from 'expect-type';
import { test, expect } from 'vitest';
import Updux from './Updux.js';
import Updux, { createAction, withPayload } from './index.js';
const incr = createAction('incr', withPayload<number>());
const dux = new Updux({
initialState: 'a',
actions: {
action1: 0,
},
selectors: {
double: (x: string) => x + x,
},
initialState: 1,
// selectors: {
// double: (x: string) => x + x,
// },
}).addMutation(incr, (i, action) => state => {
expectTypeOf(i).toEqualTypeOf<number>();
expectTypeOf(state).toEqualTypeOf<number>();
expectTypeOf(action).toEqualTypeOf<{ type: 'incr', payload: number; }>();
return state + i
});
dux.addMutation(dux.actions.action1, () => (state) => 'mutation1');
test('createStore', () => {
expect(dux.createStore().getState()).toEqual('a');
expect(dux.createStore({ preloadedState: 'b' }).getState()).toEqual('b');
});
test('augmentGetState', () => {
suite.only('store dispatch actions', () => {
const store = dux.createStore();
expect(store.getState()).toEqual('a');
expectTypeOf(store.getState()).toMatchTypeOf<string>();
expectTypeOf(store.getState.double()).toMatchTypeOf<string>();
expect(store.getState.double()).toEqual('aa');
expect(store.actions.action1).toBeTypeOf('function');
expect(store.dispatch.action1()).toMatchObject({ type: 'action1' });
expect(store.getState.double()).toEqual('mutation1mutation1');
// selectors
expect(store.selectors.double).toBeTypeOf('function');
});
test('mutations of subduxes', () => {
const incr = createAction('incr');
const subdux1 = new Updux({
actions: { incr },
initialState: 0,
test('dispatch actions', () => {
expect(store.dispatch.incr).toBeTypeOf('function');
expectTypeOf(store.dispatch.incr).toMatchTypeOf<Function>();
});
subdux1.addMutation(incr, () => (state) => state + 1);
const dux = new Updux({
subduxes: {
subdux1: subdux1.asDux,
},
test('reducer does something', () => {
store.dispatch.incr(7);
expect(store.getState()).toEqual(8);
});
const store = dux.createStore();
expect(store.getState()).toMatchObject({ subdux1: 0 });
store.dispatch.incr();
expect(store.getState()).toMatchObject({ subdux1: 1 });
store.dispatch.incr();
expect(store.getState()).toMatchObject({ subdux1: 2 });
});

View File

@ -0,0 +1,66 @@
import { createAction } from '@reduxjs/toolkit';
import { expectTypeOf } from 'expect-type';
import Updux from './Updux.js';
const dux = new Updux({
initialState: 'a',
actions: {
action1: 0,
},
selectors: {
double: (x: string) => x + x,
},
});
dux.addMutation(dux.actions.action1, () => (state) => 'mutation1');
test('createStore', () => {
expect(dux.createStore().getState()).toEqual('a');
expect(dux.createStore({ preloadedState: 'b' }).getState()).toEqual('b');
});
test('augmentGetState', () => {
const store = dux.createStore();
expect(store.getState()).toEqual('a');
expectTypeOf(store.getState()).toMatchTypeOf<string>();
expectTypeOf(store.getState.double()).toMatchTypeOf<string>();
expect(store.getState.double()).toEqual('aa');
expect(store.actions.action1).toBeTypeOf('function');
expect(store.dispatch.action1()).toMatchObject({ type: 'action1' });
expect(store.getState.double()).toEqual('mutation1mutation1');
// selectors
expect(store.selectors.double).toBeTypeOf('function');
});
test('mutations of subduxes', () => {
const incr = createAction('incr');
const subdux1 = new Updux({
actions: { incr },
initialState: 0,
});
subdux1.addMutation(incr, () => (state) => state + 1);
const dux = new Updux({
subduxes: {
subdux1: subdux1.asDux,
},
});
const store = dux.createStore();
expect(store.getState()).toMatchObject({ subdux1: 0 });
store.dispatch.incr();
expect(store.getState()).toMatchObject({ subdux1: 1 });
store.dispatch.incr();
expect(store.getState()).toMatchObject({ subdux1: 2 });
});

View File

@ -1,24 +1,3 @@
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
import { DuxActions } from './actions.js';
import { DuxSelectors } from './selectors.js';
import { DuxState } from './types.js';
type XSel<R> = R extends Function ? R : () => R;
type CurriedSelector<S> = S extends (...args: any) => infer R ? XSel<R> : never;
type CurriedSelectors<S> = {
[key in keyof S]: CurriedSelector<S[key]>;
};
export type AugmentedMiddlewareAPI<D> = MiddlewareAPI<
Dispatch<AnyAction>,
DuxState<D>
> & {
dispatch: DuxActions<D>;
getState: CurriedSelectors<DuxSelectors<D>>;
actions: DuxActions<D>;
selectors: DuxSelectors<D>;
};
export function augmentGetState(originalGetState, selectors) {
const getState = () => originalGetState();
@ -31,3 +10,14 @@ export function augmentGetState(originalGetState, selectors) {
}
return getState;
}
export function augmentDispatch(dispatch, actions) {
for (const a in actions) {
dispatch[a] = (...args) => {
const action = actions[a](...args);
dispatch(action);
return action;
};
}
return dispatch;
}

View File

@ -2,12 +2,41 @@ import { test, expect } from 'vitest';
import { expectTypeOf } from 'expect-type';
import Updux from './Updux.js';
import { buildEffectsMiddleware } from './effects.js';
import { createAction } from './index.js';
import { createAction, withPayload } from './index.js';
import { AnyAction, Dispatch } from '@reduxjs/toolkit';
import { AugmentedMiddlewareAPI } from './types.js';
test('addEffect', () => {
const dux = new Updux({});
test('addEffect signatures', () => {
const someAction = createAction('someAction', withPayload<number>());
const dux = new Updux({
actions: {
someAction,
}
});
dux.addEffect((api) => (next) => (action) => {});
dux.addEffect((api) => (next) => (action) => {
expectTypeOf(action).toMatchTypeOf<AnyAction>();
expectTypeOf(next).toMatchTypeOf<Dispatch<AnyAction>>();
expectTypeOf(api).toMatchTypeOf<AugmentedMiddlewareAPI<{}>>();
});
dux.addEffect('someAction', (api) => (next) => (action) => {
expectTypeOf(action).toMatchTypeOf<{ type: 'someAction' }>();
expectTypeOf(next).toMatchTypeOf<Dispatch<AnyAction>>();
expectTypeOf(api).toMatchTypeOf<AugmentedMiddlewareAPI<{}>>();
});
dux.addEffect(someAction, (api) => (next) => (action) => {
expectTypeOf(action).toMatchTypeOf<{ type: 'someAction' }>();
expectTypeOf(next).toMatchTypeOf<Dispatch<AnyAction>>();
expectTypeOf(api).toMatchTypeOf<AugmentedMiddlewareAPI<{}>>();
});
dux.addEffect((action) => action?.payload === 3, (api) => (next) => (action) => {
expectTypeOf(action).toMatchTypeOf<AnyAction>();
expectTypeOf(next).toMatchTypeOf<Dispatch<AnyAction>>();
expectTypeOf(api).toMatchTypeOf<AugmentedMiddlewareAPI<{}>>();
});
});
test('buildEffectsMiddleware', () => {
@ -48,7 +77,7 @@ test('buildEffectsMiddleware', () => {
expect(seen).toEqual(0);
const dispatch = vi.fn();
mw({ getState: () => 'the state', dispatch })(() => {})({
mw({ getState: () => 'the state', dispatch })(() => { })({
type: 'noop',
});
expect(seen).toEqual(1);
@ -61,7 +90,7 @@ test('basic', () => {
loaded: true,
},
actions: {
foo: 0,
foo: null,
},
});
@ -87,7 +116,7 @@ test('basic', () => {
test('subdux', () => {
const bar = new Updux({
initialState: 'bar state',
actions: { foo: 0 },
actions: { foo: null },
});
let seen = 0;
@ -144,8 +173,8 @@ test('addEffect with actionCreator', () => {
test('addEffect with function', () => {
const dux = new Updux({
actions: {
foo: () => {},
bar: () => {},
foo: () => { },
bar: () => { },
},
});

View File

@ -1,15 +1,16 @@
import { AnyAction } from '@reduxjs/toolkit';
import { Dispatch } from '@reduxjs/toolkit';
import { MiddlewareAPI } from '@reduxjs/toolkit';
import { AugmentedMiddlewareAPI, augmentGetState } from './createStore.js';
import { augmentGetState } from './createStore.js';
import { AugmentedMiddlewareAPI } from './types.js';
//const composeMw = (mws) => (api) => (originalNext) =>
// mws.reduceRight((next, mw) => mw(api)(next), originalNext);
export interface EffectMiddleware<D> {
export interface EffectMiddleware<D, A = AnyAction> {
(api: AugmentedMiddlewareAPI<D>): (
next: Dispatch<AnyAction>,
) => (action: AnyAction) => any;
) => (action: A) => any;
}
export function buildEffects(localEffects, subduxes = {}) {

View File

@ -1,5 +1,6 @@
export { withPayload } from './actions.js';
import Updux from './Updux.js';
export { withPayload } from './actions.js';
export { createAction } from '@reduxjs/toolkit';
export default Updux;

View File

@ -1,4 +1,3 @@
import { expectTypeOf } from 'expect-type';
import { buildInitialState } from './initialState.js';
import Updux from './Updux.js';
@ -14,10 +13,12 @@ test('default', () => {
test('number', () => {
const dux = new Updux({ initialState: 3 });
expect(dux.initialState).toBeTypeOf('number');
expect(dux.initialState).toEqual(3);
expectTypeOf(dux.initialState).toEqualTypeOf<number>();
type X = typeof dux.initialState;
const f = { initialState: dux.initialState };
});
test('single dux', () => {
@ -31,12 +32,12 @@ test('single dux', () => {
test('no initialState for subdux', () => {
const subduxes = {
bar: new Updux({}).toDux(),
baz: new Updux({ initialState: 'potato' }).toDux(),
bar: new Updux({}),
baz: new Updux({ initialState: 'potato' }),
};
const dux = new Updux({
subduxes,
}).toDux();
});
expectTypeOf(dux.initialState).toEqualTypeOf<{
bar: {};
@ -69,7 +70,7 @@ const bar = new Updux({ initialState: 123 });
const foo = new Updux({
initialState: { root: 'abc' },
subduxes: {
bar: bar.asDux,
bar
},
});

View File

@ -1,5 +1,5 @@
import u from '@yanick/updeep-remeda';
import * as R from 'remeda';
import { D } from '@mobily/ts-belt';
export function buildInitialState(localInitialState, subduxes) {
let state = localInitialState ?? {};
@ -9,7 +9,7 @@ export function buildInitialState(localInitialState, subduxes) {
throw new Error('root initial state is not an object');
}
state = u(state, R.mapValues(subduxes, R.prop('initialState')));
state = u(state, D.map(subduxes, D.prop('initialState')));
}
return state;

Some files were not shown because too many files have changed in this diff Show More