Compare commits

..

No commits in common. "66c2b162db2903e81cc1cb034ee248394c66745b" and "7280381ca8286c99340fc0853c1e948db2d61b30" have entirely different histories.

130 changed files with 1817 additions and 4365 deletions

2
.gitignore vendored
View File

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

11
TODO
View File

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

View File

@ -20,8 +20,7 @@ tasks:
- git checkout {{.PARENT_BRANCH}} - git checkout {{.PARENT_BRANCH}}
- git weld - - git weld -
test: vitest run src test: vitest run src docs/*.ts
test:dev: vitest src test:dev: vitest src
lint:fix:delta: lint:fix:delta:
@ -66,20 +65,6 @@ tasks:
cmds: cmds:
- npx eslint {{.FILES | default "src/**" }} - 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
typedoc:
cmds:
- typedoc
docsify-install: docsify-install:
cmds: cmds:
- cp -r node_modules/docsify/lib docs/docsify - cp -r node_modules/docsify/lib docs/docsify

View File

@ -1,17 +0,0 @@
#!/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});

View File

@ -1,226 +0,0 @@
# What's Updux?
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux.
It has a couple of pretty good ideas that removes some of the
boilerplate. Keeping mutations and asynchronous effects close to the
reducer definition? Nice. Automatically infering the
actions from the said mutations and effects? Genius!
But it also enforces a flat hierarchy of reducers -- where
is the fun in that? And I'm also having a strong love for
[Updeep](https://github.com/substantial/updeep), so I want reducer state updates to leverage the heck out of it.
All that to say, say hello to `Updux`. Heavily inspired by `rematch`, but twisted
to work with `updeep` and to fit my peculiar needs. It offers features such as
- Mimic the way VueX has mutations (reducer reactions to specific actions) and
effects (middleware reacting to actions that can be asynchronous and/or
have side-effects), so everything pertaining to a store are all defined
in the space place.
- Automatically gather all actions used by the updux's effects and mutations,
and makes then accessible as attributes to the `dispatch` object of the
store.
- Mutations have a signature that is friendly to Updux and Immer.
- Also, the mutation signature auto-unwrap the payload of the actions for you.
- TypeScript types.
Fair warning: this package is still very new, probably very buggy,
definitively very badly documented, and very subject to changes. Caveat
Maxima Emptor.
# Synopsis
```
import updux from 'updux';
import otherUpdux from './otherUpdux';
const {
initial,
reducer,
actions,
middleware,
createStore,
} = new Updux({
initial: {
counter: 0,
},
subduxes: {
otherUpdux,
},
mutations: {
inc: ( increment = 1 ) => u({counter: s => s + increment })
},
effects: {
'*' => api => next => action => {
console.log( "hey, look, an action zoomed by!", action );
next(action);
};
},
actions: {
customAction: ( someArg ) => ({
type: "custom",
payload: { someProp: someArg }
}),
},
});
const store = createStore();
store.dispatch.inc(3);
```
# Description
Full documentation can be [found here](https://yanick.github.io/updux/).
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

@ -1 +0,0 @@
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

@ -1,228 +0,0 @@
updux / [Exports](modules.md)
# 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

@ -1,435 +0,0 @@
[updux](../README.md) / [Exports](../modules.md) / default
# Class: default\<D\>
## 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`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: `Record`\<`string`, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<string, any\>; }\>\> }\> |
#### Parameters
| Name | Type |
| :------ | :------ |
| `duxConfig` | `D` |
#### Returns
[`default`](default.md)\<`D`\>
## Properties
### #defaultMutation
`Private` **#defaultMutation**: `any`
___
### #effects
`Private` **#effects**: `any`[] = `[]`
___
### #mutations
`Private` **#mutations**: `any`[] = `[]`
___
### #reactions
`Private` **#reactions**: `any`[] = `[]`
___
### duxConfig
`Private` `Readonly` **duxConfig**: `D`
___
### memoBuildActions
**memoBuildActions**: `Moized`\<(`localActions`: {}, `subduxes`: {}) => `any`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`localActions`: {}, `subduxes`: {}) => `any`\> ; `onCacheChange`: `OnCacheOperation`\<(`localActions`: {}, `subduxes`: {}) => `any`\> ; `onCacheHit`: `OnCacheOperation`\<(`localActions`: {}, `subduxes`: {}) => `any`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
___
### memoBuildEffects
**memoBuildEffects**: `Moized`\<(`localEffects`: `any`, `subduxes`: {}) => `any`[], `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`localEffects`: `any`, `subduxes`: {}) => `any`[]\> ; `onCacheChange`: `OnCacheOperation`\<(`localEffects`: `any`, `subduxes`: {}) => `any`[]\> ; `onCacheHit`: `OnCacheOperation`\<(`localEffects`: `any`, `subduxes`: {}) => `any`[]\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
___
### memoBuildReducer
**memoBuildReducer**: `Moized`\<(`initialStateState`: `unknown`, `mutations`: `MutationCase`[], `defaultMutation?`: `Omit`\<`MutationCase`, ``"matcher"``\>, `subduxes`: `Record`\<`string`, `Partial`\<\{ `actions`: `Record`\<`string`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: Record\<string, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<...\>; }\>\> }\>\>) => (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`initialStateState`: `unknown`, `mutations`: `MutationCase`[], `defaultMutation?`: `Omit`\<`MutationCase`, ``"matcher"``\>, `subduxes`: `Record`\<`string`, `Partial`\<\{ `actions`: `Record`\<`string`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: Record\<string, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<...\>; }\>\> }\>\>) => (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`\> ; `onCacheChange`: `OnCacheOperation`\<(`initialStateState`: `unknown`, `mutations`: `MutationCase`[], `defaultMutation?`: `Omit`\<`MutationCase`, ``"matcher"``\>, `subduxes`: `Record`\<`string`, `Partial`\<\{ `actions`: `Record`\<`string`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: Record\<string, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<...\>; }\>\> }\>\>) => (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`\> ; `onCacheHit`: `OnCacheOperation`\<(`initialStateState`: `unknown`, `mutations`: `MutationCase`[], `defaultMutation?`: `Omit`\<`MutationCase`, ``"matcher"``\>, `subduxes`: `Record`\<`string`, `Partial`\<\{ `actions`: `Record`\<`string`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: Record\<string, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<...\>; }\>\> }\>\>) => (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
___
### memoBuildSchema
**memoBuildSchema**: `Moized`\<(`schema`: `any`, `initialState`: `any`, `subduxes`: {}) => `any`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`schema`: `any`, `initialState`: `any`, `subduxes`: {}) => `any`\> ; `onCacheChange`: `OnCacheOperation`\<(`schema`: `any`, `initialState`: `any`, `subduxes`: {}) => `any`\> ; `onCacheHit`: `OnCacheOperation`\<(`schema`: `any`, `initialState`: `any`, `subduxes`: {}) => `any`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
___
### memoBuildSelectors
**memoBuildSelectors**: `Moized`\<(`localSelectors`: {}, `subduxes`: {}) => `object`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`localSelectors`: {}, `subduxes`: {}) => `object`\> ; `onCacheChange`: `OnCacheOperation`\<(`localSelectors`: {}, `subduxes`: {}) => `object`\> ; `onCacheHit`: `OnCacheOperation`\<(`localSelectors`: {}, `subduxes`: {}) => `object`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
___
### memoInitialState
**memoInitialState**: `Moized`\<(`localInitialState`: `any`, `subduxes`: `any`) => `any`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`localInitialState`: `any`, `subduxes`: `any`) => `any`\> ; `onCacheChange`: `OnCacheOperation`\<(`localInitialState`: `any`, `subduxes`: `any`) => `any`\> ; `onCacheHit`: `OnCacheOperation`\<(`localInitialState`: `any`, `subduxes`: `any`) => `any`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
## Accessors
### actions
`get` **actions**(): `DuxActions`\<`D`\>
#### Returns
`DuxActions`\<`D`\>
___
### asDux
`get` **asDux**(): `Object`
#### Returns
`Object`
| Name | Type |
| :------ | :------ |
| `actions` | `DuxActions`\<`D`\> |
| `effects` | `any` |
| `initialState` | `DuxState`\<`D`\> |
| `reactions` | `any`[] |
| `reducer` | (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown` |
| `schema` | `any` |
| `selectors` | `DuxSelectors`\<`D`\> |
| `upreducer` | (`action`: `any`) => (`state`: `any`) => `unknown` |
___
### effects
`get` **effects**(): `any`
#### Returns
`any`
___
### foo
`get` **foo**(): `DuxActions`\<`D`\>
#### Returns
`DuxActions`\<`D`\>
___
### initialState
`get` **initialState**(): `DuxState`\<`D`\>
#### Returns
`DuxState`\<`D`\>
___
### reactions
`get` **reactions**(): `any`[]
#### Returns
`any`[]
___
### reducer
`get` **reducer**(): (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`
#### Returns
`fn`
▸ (`state?`, `action`): `unknown`
##### Parameters
| Name | Type | Default value |
| :------ | :------ | :------ |
| `state` | `unknown` | `initialStateState` |
| `action` | `Action`\<`any`\> | `undefined` |
##### Returns
`unknown`
___
### schema
`get` **schema**(): `any`
#### Returns
`any`
___
### selectors
`get` **selectors**(): `DuxSelectors`\<`D`\>
#### Returns
`DuxSelectors`\<`D`\>
___
### upreducer
`get` **upreducer**(): (`action`: `any`) => (`state`: `any`) => `unknown`
#### Returns
`fn`
▸ (`action`): (`state`: `any`) => `unknown`
##### Parameters
| Name | Type |
| :------ | :------ |
| `action` | `any` |
##### Returns
`fn`
▸ (`state`): `unknown`
##### Parameters
| Name | Type |
| :------ | :------ |
| `state` | `any` |
##### Returns
`unknown`
## Methods
### addDefaultMutation
**addDefaultMutation**(`mutation`, `terminal?`): `any`
#### Parameters
| Name | Type |
| :------ | :------ |
| `mutation` | `Mutation`\<`any`, `DuxState`\<`D`\>\> |
| `terminal?` | `boolean` |
#### Returns
`any`
___
### addEffect
**addEffect**(`actionType`, `effect`): [`default`](default.md)\<`D`\>
#### Parameters
| Name | Type |
| :------ | :------ |
| `actionType` | keyof `D` extends \{ `actions`: `A` } ? \{ [key in string \| number \| symbol]: key extends string ? ExpandedAction\<A[key], key\> : never } : {} \| keyof `UnionToIntersection`\<`D` extends \{ `subduxes`: `S` } ? `DuxActions`\<`S`[keyof `S`]\> : {}\> |
| `effect` | `EffectMiddleware`\<`D`\> |
#### Returns
[`default`](default.md)\<`D`\>
**addEffect**(`actionCreator`, `effect`): [`default`](default.md)\<`D`\>
#### Parameters
| Name | Type |
| :------ | :------ |
| `actionCreator` | `Object` |
| `actionCreator.match` | (`action`: `any`) => `boolean` |
| `effect` | `EffectMiddleware`\<`D`\> |
#### Returns
[`default`](default.md)\<`D`\>
**addEffect**(`guardFunc`, `effect`): [`default`](default.md)\<`D`\>
#### Parameters
| Name | Type |
| :------ | :------ |
| `guardFunc` | (`action`: `AnyAction`) => `boolean` |
| `effect` | `EffectMiddleware`\<`D`\> |
#### Returns
[`default`](default.md)\<`D`\>
**addEffect**(`effect`): [`default`](default.md)\<`D`\>
#### Parameters
| Name | Type |
| :------ | :------ |
| `effect` | `EffectMiddleware`\<`D`\> |
#### Returns
[`default`](default.md)\<`D`\>
___
### addMutation
**addMutation**\<`A`\>(`matcher`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
#### Type parameters
| Name | Type |
| :------ | :------ |
| `A` | extends `string` \| `number` \| `symbol` |
#### Parameters
| Name | Type |
| :------ | :------ |
| `matcher` | `A` |
| `mutation` | `Mutation`\<`DuxActions`\<`D`\>[`A`] extends (...`args`: `any`) => `P` ? `P` : `never`, `DuxState`\<`D`\>\> |
| `terminal?` | `boolean` |
#### Returns
[`default`](default.md)\<`D`\>
**addMutation**\<`A`\>(`matcher`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
#### Type parameters
| Name | Type |
| :------ | :------ |
| `A` | extends `Action`\<`any`, `A`\> |
#### Parameters
| Name | Type |
| :------ | :------ |
| `matcher` | (`action`: `A`) => `boolean` |
| `mutation` | `Mutation`\<`A`, `DuxState`\<`D`\>\> |
| `terminal?` | `boolean` |
#### Returns
[`default`](default.md)\<`D`\>
**addMutation**\<`A`\>(`actionCreator`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
#### Type parameters
| Name | Type |
| :------ | :------ |
| `A` | extends `ActionCreator`\<`any`, `any`[], `A`\> |
#### Parameters
| Name | Type |
| :------ | :------ |
| `actionCreator` | `A` |
| `mutation` | `Mutation`\<`ReturnType`\<`A`\>, `DuxState`\<`D`\>\> |
| `terminal?` | `boolean` |
#### Returns
[`default`](default.md)\<`D`\>
___
### addReaction
**addReaction**(`reaction`): `void`
#### Parameters
| Name | Type |
| :------ | :------ |
| `reaction` | `any` |
#### Returns
`void`
___
### createStore
**createStore**(`options?`): `ToolkitStore`\<`D` extends \{ `initialState`: `INITIAL_STATE` } ? `INITIAL_STATE` : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown` & {}, `AnyAction`, `Middlewares`\<`DuxState`\<`D`\>\>\> & `MiddlewareAPI`\<`Dispatch`\<`AnyAction`\>, `DuxState`\<`D`\>\> & \{ `actions`: `DuxActions`\<`D`\> ; `dispatch`: `DuxActions`\<`D`\> ; `getState`: `CurriedSelectors`\<`DuxSelectors`\<`D`\>\> ; `selectors`: `DuxSelectors`\<`D`\> }
#### Parameters
| Name | Type |
| :------ | :------ |
| `options` | `Partial`\<\{ `buildMiddleware`: (`middleware`: `any`[]) => `any` ; `preloadedState`: `DuxState`\<`D`\> ; `validate`: `boolean` }\> |
#### Returns
`ToolkitStore`\<`D` extends \{ `initialState`: `INITIAL_STATE` } ? `INITIAL_STATE` : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown` & {}, `AnyAction`, `Middlewares`\<`DuxState`\<`D`\>\>\> & `MiddlewareAPI`\<`Dispatch`\<`AnyAction`\>, `DuxState`\<`D`\>\> & \{ `actions`: `DuxActions`\<`D`\> ; `dispatch`: `DuxActions`\<`D`\> ; `getState`: `CurriedSelectors`\<`DuxSelectors`\<`D`\>\> ; `selectors`: `DuxSelectors`\<`D`\> }
___
### toDux
**toDux**(): `Object`
#### Returns
`Object`
| Name | Type |
| :------ | :------ |
| `actions` | `DuxActions`\<`D`\> |
| `effects` | `any` |
| `initialState` | `DuxState`\<`D`\> |
| `reactions` | `any`[] |
| `reducer` | (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown` |
| `schema` | `any` |
| `selectors` | `DuxSelectors`\<`D`\> |
| `upreducer` | (`action`: `any`) => (`state`: `any`) => `unknown` |

View File

@ -1,129 +0,0 @@
[updux](README.md) / Exports
# 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

@ -1,115 +0,0 @@
```
```
--8<- "docs/tutorial-effects.test.ts:effects-1"
```
```
## Subduxes
Now that we have all the building blocks, we can embark on the last and
funkiest part of Updux: its recursive nature.
### Recap: the Todos dux, undivided
Upduxes can be divided into sub-upduxes that deal with the various parts of
the global state. This is better understood by working out an example, so
let's recap on the Todos dux we have so far:
```title="todos-monolith.ts"
--8<- "docs/tutorial-monolith.test.ts:mono"
```
This store has two main components: the `nextId`, and the `todos` collection.
The `todos` collection is itself composed of the individual `todo`s. Let's
create upduxes for each of those.
### NextId dux
```js title="nextId.ts"
--8<- "docs/nextId.ts:dux"
```
### Todo updux
```title="todo.ts"
--8<- "docs/todo.ts"
```
### Todos updux
```title="todos.ts"
--8<- "docs/todos.ts"
```
### Main store
```title="todoList.ts"
--8<- "docs/todoList.ts"
```
Tadah! We had to define the `addTodo` effect at the top level as it needs to
access the `getNextId` selector from `nextId` and the `addTodoWithId`
action from the `todos`.
Note that the `getNextId` selector still gets the
right value; when aggregating subduxes selectors Updux auto-wraps them to
access the right slice of the top object.
## Reactions
Reactions -- aka Redux's subscriptions -- can be added to a updux store via the initial config
or the method `addSubscription`. The signature of a reaction is:
```
(storeApi) => (state, previousState, unsubscribe) => {
...
}
```
Subscriptions registered for an updux and its subduxes are automatically
subscribed to the store when calling `createStore`.
The `state` passed to the subscriptions of the subduxes is the local state.
Also, all subscriptions are wrapped such that they are called only if the
local `state` changed since their last invocation.
Example:
```
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 })
}
});
```
[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/

21
docs/.gitignore vendored
View File

@ -1,21 +0,0 @@
# 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

View File

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

View File

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

View File

@ -1,55 +1,226 @@
# Starlight Starter Kit: Basics # What's Updux?
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 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
``` ```
npm create astro@latest -- --template starlight 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);
``` ```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) # Description
[![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)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 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)
## 🚀 Project Structure ## Exporting upduxes
Inside of your Astro + Starlight project, you'll see the following folders and files: 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';
├── public/
├── src/ const updux = new Updux({ ... });
│ ├── assets/
│ ├── content/ export default updux;
│ │ ├── docs/
│ │ └── config.ts
│ └── env.d.ts
├── astro.config.mjs
├── package.json
└── tsconfig.json
``` ```
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. Then you can use them as subduxes like this:
Images can be added to `src/assets/` and embedded in Markdown with a relative link. ```
import Updux from 'updux';
import foo from './foo'; // foo is an Updux
import bar from './bar'; // bar is an Updux as well
Static assets, like favicons, can be placed in the `public/` directory. const updux = new Updux({
subduxes: {
foo, bar
}
});
```
## 🧞 Commands Or if you want to use it:
All commands are run from the root of the project, from a terminal: ```
import updux from './myUpdux';
| Command | Action | const {
| :------------------------ | :----------------------------------------------- | reducer,
| `npm install` | Installs dependencies | actions: { doTheThing },
| `npm run dev` | Starts local dev server at `localhost:4321` | createStore,
| `npm run build` | Build your production site to `./dist/` | middleware,
| `npm run preview` | Preview your build locally, before deploying | } = updux;
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | ```
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more? ## Mapping a mutation to all values of a state
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). Say you have a `todos` state that is an array of `todo` sub-states. It's easy
enough to have the main reducer maps away all items to the sub-reducer:
```
const todo = new Updux({
mutations: {
review: () => u({ reviewed: true}),
done: () => u({done: true}),
},
});
const todos = new Updux({ initial: [] });
todos.addMutation(
todo.actions.review,
(_,action) => state => state.map( todo.upreducer(action) )
);
todos.addMutation(
todo.actions.done,
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
);
```
But `updeep` can iterate through all the items of an array (or the values of
an object) via the special key `*`. So the todos updux above could also be
written:
```
const todo = new Updux({
mutations: {
review: () => u({ reviewed: true}),
done: () => u({done: true}),
},
});
const todos = new Updux({
subduxes: { '*': todo },
});
todos.addMutation(
todo.actions.done,
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
true
);
```
The advantages being that the actions/mutations/effects of the subdux will be
imported by the root updux as usual, and all actions that aren't being
overridden by a sink mutation will trickle down automatically.
## Usage with Immer
While Updux was created with Updeep in mind, it also plays very
well with [Immer](https://immerjs.github.io/immer/docs/introduction).
For example, taking this basic updux:
```
import Updux from 'updux';
const updux = new Updux({
initial: { counter: 0 },
mutations: {
add: (inc=1) => state => { counter: counter + inc }
}
});
```
Converting it to Immer would look like:
```
import Updux from 'updux';
import { produce } from 'Immer';
const updux = new Updux({
initial: { counter: 0 },
mutations: {
add: (inc=1) => produce( draft => draft.counter += inc ) }
}
});
```
But since typing `produce` over and over is no fun, `groomMutations`
can be used to wrap all mutations with it:
```
import Updux from 'updux';
import { produce } from 'Immer';
const updux = new Updux({
initial: { counter: 0 },
groomMutations: mutation => (...args) => produce( mutation(...args) ),
mutations: {
add: (inc=1) => draft => draft.counter += inc
}
});
```

View File

@ -1,54 +0,0 @@
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

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

19
docs/nextId.ts Normal file
View File

@ -0,0 +1,19 @@
/// [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;
/// [dux]

View File

@ -1,19 +0,0 @@
{
"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"
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 583 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 583 KiB

53
docs/recipes.test.js Normal file
View File

@ -0,0 +1,53 @@
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.

Before

Width:  |  Height:  |  Size: 96 KiB

View File

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

View File

@ -1 +0,0 @@
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

@ -1,153 +0,0 @@
---
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

@ -1,355 +0,0 @@
---
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

@ -1,129 +0,0 @@
---
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

@ -1,47 +0,0 @@
---
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,32 +0,0 @@
---
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

@ -1,43 +0,0 @@
---
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

@ -1,96 +0,0 @@
---
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

@ -1,86 +0,0 @@
---
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

@ -1,47 +0,0 @@
---
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

@ -1,28 +0,0 @@
---
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

@ -1,242 +0,0 @@
---
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

@ -1,11 +0,0 @@
---
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

@ -1,74 +0,0 @@
---
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

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

View File

@ -1,14 +0,0 @@
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

@ -1,20 +0,0 @@
---
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

@ -1,23 +0,0 @@
---
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

@ -1,24 +0,0 @@
---
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
View File

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

View File

@ -1,10 +1,10 @@
import Updux from '../index.js'; import Updux from '../src/index.js';
import u from '@yanick/updeep-remeda'; import u from '@yanick/updeep-remeda';
export default new Updux({ const todoDux = new Updux({
initialState: { initialState: {
id: 0, id: 0,
description: '', description: "",
done: false, done: false,
}, },
actions: { actions: {
@ -12,7 +12,9 @@ export default new Updux({
}, },
selectors: { selectors: {
desc: ({ description }) => description, desc: ({ description }) => description,
}, }
}) });
.addMutation('todoDone', () => u({ done: true }));
todoDux.addMutation('todoDone', () => u({ done: true }));
export default todoDux.asDux;

28
docs/todoList.ts Normal file
View File

@ -0,0 +1,28 @@
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;

26
docs/todos.ts Normal file
View File

@ -0,0 +1,26 @@
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;

View File

@ -1,10 +0,0 @@
{
"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/*"]
}
}
}

19
docs/tutorial-1.test.js Normal file
View File

@ -0,0 +1,19 @@
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: []
});
});

25
docs/tutorial-1.test.ts Normal file
View File

@ -0,0 +1,25 @@
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

@ -0,0 +1,56 @@
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,6 +1,6 @@
import { test, expect } from 'vitest'; import { test, expect } from 'vitest';
/// --8<-- [start:actions1] /// [actions1]
import Updux, { createAction, withPayload } from 'updux'; import Updux, { createAction, withPayload } from 'updux';
type TodoId = number; type TodoId = number;
@ -8,13 +8,6 @@ type TodoId = number;
const addTodo = createAction('addTodo', withPayload<string>()); const addTodo = createAction('addTodo', withPayload<string>());
const todoDone = createAction('todoDone', withPayload<TodoId>()); const todoDone = createAction('todoDone', withPayload<TodoId>());
/// --8<-- [end:actions1]
test('createAction', () => {
expect(addTodo).toBeTypeOf('function');
expect(todoDone).toBeTypeOf('function');
});
const todosDux = new Updux({ const todosDux = new Updux({
initialState: { initialState: {
nextId: 1, nextId: 1,
@ -23,39 +16,28 @@ const todosDux = new Updux({
actions: { actions: {
addTodo, addTodo,
todoDone, todoDone,
}, }
}); });
/// [actions1]
/// --8<-- [start:actions2] /// [actions2]
todosDux.actions.addTodo('write tutorial'); todosDux.actions.addTodo('write tutorial');
// { type: 'addTodo', payload: 'write tutorial' } // { type: 'addTodo', payload: 'write tutorial' }
/// --8<-- [end:actions2] /// [actions2]
test('basic', () => { test("basic", () => {
expect(todosDux.actions.addTodo('write tutorial')).toEqual({ expect(todosDux.actions.addTodo('write tutorial')).toEqual({
type: 'addTodo', type: 'addTodo', payload: 'write tutorial'
payload: 'write tutorial', })
});
}); });
/// --8<-- [start:addMutation-1] /// [addMutation]
todosDux.addMutation(addTodo, (description) => ({ todos, nextId }) => {
todosDux.addMutation(addTodo, (description) => ({ todos, nextId }) => ({ return {
nextId: 1 + nextId, todos: todos.concat({ description, id: nextId, done: false }),
todos: todos.concat({ description, id: nextId, done: false }), nextId: 1 + nextId,
})); }
});
todosDux.addMutation(todoDone, (id) => ({ todos, nextId }) => ({
nextId: 1 + nextId,
todos: todos.map((todo) => {
if (todo.id !== id) return todo;
return {
...todo,
done: true,
};
}),
}));
/// --8<-- [end:addMutation-1]
const store = todosDux.createStore(); const store = todosDux.createStore();
@ -73,9 +55,9 @@ const state = store.getState();
// ] // ]
// } // }
/// --8<-- [end:addMutation] /// [addMutation]
test('addMutation', () => { test("addMutation", () => {
expect(state).toEqual({ expect(state).toEqual({
nextId: 2, nextId: 2,
todos: [ todos: [
@ -83,7 +65,7 @@ test('addMutation', () => {
description: 'write tutorial', description: 'write tutorial',
done: false, done: false,
id: 1, id: 1,
}, }
], ]
}); });
}); });

View File

@ -0,0 +1,37 @@
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,23 +1,21 @@
import { test, expect } from 'vitest'; import { test, expect } from 'vitest';
/// --8<-- [start:effects-1] //process.env.UPDEEP_MODE = "dangerously_never_freeze";
/// [effects-1]
import u from '@yanick/updeep-remeda'; import u from '@yanick/updeep-remeda';
import * as R from 'remeda'; import * as R from 'remeda';
import Updux, { createAction, withPayload } from 'updux'; import Updux, { createAction, withPayload } from '../src/index.js';
// we want to decouple the increment of next_id and the creation of // we want to decouple the increment of next_id and the creation of
// a new todo. So let's use a new version of the action 'addTodo'. // a new todo. So let's use a new version of the action 'addTodo'.
type TodoId = number; type TodoId = number;
const addTodoWithId = createAction( const addTodoWithId = createAction('addTodoWithId', withPayload<{
'addTodoWithId', id: TodoId, description: string
withPayload<{ }>());
id: TodoId;
description: string;
}>(),
);
const incNextId = createAction('incNextId'); const incNextId = createAction('incNextId');
const addTodo = createAction('addTodo', withPayload<string>()); const addTodo = createAction('addTodo', withPayload<string>());
@ -30,37 +28,36 @@ const todosDux = new Updux({
}, },
}); });
todosDux.addMutation(addTodoWithId, (todo) => todosDux.addMutation(addTodoWithId, (todo) => u({
u({ todos: R.concat([u(todo, { done: false })])
todos: R.concat([u(todo, { done: false })]), }));
}),
todosDux.addMutation(
incNextId, () => u({ nextId: id => id + 1 })
); );
todosDux.addMutation(incNextId, () => u({ nextId: (id) => id + 1 })); todosDux.addEffect("addTodo", ({ getState, dispatch }) => next => action => {
const id = getState.nextId();
todosDux.addEffect( dispatch.incNextId();
'addTodo',
({ getState, dispatch }) =>
(next) =>
(action) => {
const id = getState.nextId();
dispatch.incNextId(); next(action);
next(action); dispatch.addTodoWithId(
{ id, description: action.payload }
);
}
dispatch.addTodoWithId({ id, description: action.payload }); )
},
);
const store = todosDux.createStore(); const store = todosDux.createStore();
store.dispatch.addTodo('write tutorial'); store.dispatch.addTodo('write tutorial');
/// --8<-- [end:effects-1] /// [effects-1]
test('basic', () => { test('basic', () => {
expect(store.getState()).toMatchObject({ expect(store.getState()).toMatchObject({
nextId: 2, nextId: 2,
todos: [{ id: 1, description: 'write tutorial', done: false }], todos: [{ id: 1, description: 'write tutorial', done: false }]
}); })
}); })

View File

@ -0,0 +1,42 @@
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,8 +1,8 @@
import { test, expect } from 'vitest'; import { test, expect } from 'vitest';
/// --8<-- [start:mono] /// [mono]
import Updux from '../index.js'; import Updux from '../src/index.js';
import u from '@yanick/updeep-remeda'; import u from '@yanick/updeep-remeda';
type TodoId = number; type TodoId = number;
@ -14,30 +14,33 @@ const todosDux = new Updux({
}, },
actions: { actions: {
addTodo: (description: string) => description, addTodo: (description: string) => description,
addTodoWithId: (description: string, id: TodoId) => ({ addTodoWithId: (description: string, id: TodoId) => ({ description, id, done: false }),
description,
id,
done: false,
}),
todoDone: (id: TodoId) => id, todoDone: (id: TodoId) => id,
incNextId: () => { }, incNextId: null,
}, },
selectors: { selectors: {
getTodoById: getTodoById: ({ todos }) => id => todos.find(u.matches({ id })),
({ todos }) =>
(id) =>
todos.find(u.matches({ id })),
getNextId: ({ nextId }) => nextId, getNextId: ({ nextId }) => nextId,
}, },
}) });
.addMutation('addTodoWithId', (todo) =>
u.updateIn('todos', (todos) => [...todos, todo]), todosDux.addMutation(
todosDux.actions.addTodoWithId, todo =>
u.updateIn('todos', todos => [...todos, todo]),
);
todosDux.addMutation(
todosDux.actions.incNextId,
() => u({ nextId: x => x + 1 }));
todosDux.addMutation(
todosDux.actions.todoDone, (id) => u.updateIn('todos',
u.map(u.if(u.matches({ id }), { done: true }))
) )
.addMutation('incNextId', () => u({ nextId: (x) => x + 1 })) );
.addMutation('todoDone', (id) =>
u.updateIn('todos', u.map(u.if(u.matches({ id }), { done: true }))), todosDux.addEffect(
) todosDux.actions.addTodo, ({ getState, dispatch }) => next => action => {
.addEffect('addTodo', ({ getState, dispatch }) => (next) => (action) => {
const id = getState.getNextId(); const id = getState.getNextId();
dispatch.incNextId(); dispatch.incNextId();
@ -45,10 +48,10 @@ const todosDux = new Updux({
next(action); next(action);
dispatch.addTodoWithId(action.payload, id); dispatch.addTodoWithId(action.payload, id);
}); }
);
/// [mono]
/// --8<-- [end:mono]
test('basic', () => { test('basic', () => {
const store = todosDux.createStore(); const store = todosDux.createStore();
@ -59,8 +62,8 @@ test('basic', () => {
nextId: 3, nextId: 3,
todos: [ todos: [
{ id: 1, description: 'write tutorial', done: false }, { id: 1, description: 'write tutorial', done: false },
{ id: 2, description: 'have fun', done: false }, { id: 2, description: 'have fun', done: false }
], ]
}); });
store.dispatch.todoDone(1); store.dispatch.todoDone(1);
@ -70,6 +73,6 @@ test('basic', () => {
todos: [ todos: [
{ id: 1, description: 'write tutorial', done: true }, { id: 1, description: 'write tutorial', done: true },
{ id: 2, description: 'have fun', done: false }, { id: 2, description: 'have fun', done: false },
], ]
}); });
}); })

View File

@ -0,0 +1,39 @@
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

@ -0,0 +1,27 @@
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,44 +1,42 @@
import { test, expect } from 'vitest'; import { test, expect } from 'vitest';
/// --8<-- [start:sel1] /// [sel1]
import Updux from 'updux'; import Updux from 'updux';
const dux = new Updux({ const dux = new Updux({
initialState: [] as { id: number; done: boolean }[], initialState: [] as { id: number, done: boolean }[],
selectors: { selectors: {
getDone: (state) => state.filter(({ done }) => done), getDone: (state) => state.filter(({ done }) => done),
getById: (state) => (id) => state.find((todo) => todo.id === id), getById: (state) => (id) => state.find(todo => todo.id === id),
}, }
}); });
const state = [ const state = [{ id: 1, done: true }, { id: 2, done: false }];
{ id: 1, done: true },
{ id: 2, done: false },
];
dux.selectors.getDone(state); // = [ { id: 1, done: true } ] dux.selectors.getDone(state); // = [ { id: 1, done: true } ]
dux.selectors.getById(state)(2); // = { id: 2, done: false } dux.selectors.getById(state)(2); // = { id: 2, done: false }
const store = dux.createStore({ preloadedState: state }); const store = dux.createStore({ preloadedState: state });
store.selectors.getDone(state); // = [ { id: 1, done: true } ] store.selectors.getDone(state); // = [ { id: 1, done: true } ]
store.selectors.getById(state)(2); // = { id: 2, done: false } store.selectors.getById(state)(2); // = { id: 2, done: false }
store.getState.getDone(); // = [ { id: 1, done: true } ] store.getState.getDone(); // = [ { id: 1, done: true } ]
store.getState.getById(2); // = { id: 2, done: false } store.getState.getById(2); // = { id: 2, done: false }
/// --8<-- [end:sel1] /// [sel1]
test('selectors', () => { test('selectors', () => {
expect(dux.selectors.getDone(state)).toMatchObject([{ id: 1, done: true }]); expect(dux.selectors.getDone(state)).toMatchObject([{ id: 1, done: true }]);
expect(dux.selectors.getById(state)(2)).toMatchObject({ id: 2 }); expect(dux.selectors.getById(state)(2)).toMatchObject({ id: 2 });
expect(store.selectors.getDone(state)).toMatchObject([ expect(store.selectors.getDone(state)).toMatchObject([{ id: 1, done: true }]);
{ id: 1, done: true },
]);
expect(store.selectors.getById(state)(2)).toMatchObject({ id: 2 }); expect(store.selectors.getById(state)(2)).toMatchObject({ id: 2 });
expect(store.getState()).toMatchObject([{ id: 1 }, { id: 2 }]); expect(store.getState()).toMatchObject([
{ id: 1 }, { id: 2 }
]);
expect(store.getState.getDone()).toMatchObject([{ id: 1, done: true }]); expect(store.getState.getDone()).toMatchObject([{ id: 1, done: true }]);
expect(store.getState.getById(2)).toMatchObject({ id: 2 }); expect(store.getState.getById(2)).toMatchObject({ id: 2 });
}); });

View File

@ -0,0 +1,91 @@
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);
});

168
docs/tutorial.md Normal file
View File

@ -0,0 +1,168 @@
# 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.
[filename](tutorial-1.test.ts ':include :type=code :fragment=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:
[filename](tutorial-1.test.ts ':include :type=code :fragment=tut2')
## Add actions
This is all good, but very static. Let's add some actions!
[filename](tutorial-actions.test.ts ':include :type=code :fragment=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.
[filename](tutorial-actions.test.ts ':include :type=code :fragment=actions2')
### Adding a mutation
Mutations are the reducing functions associated to actions. They
are defined via the `addMutation` method.
[filename](tutorial-actions.test.ts ':include :type=code :fragment=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.
[filename](tutorial-effects.test.ts ':include :type=code :fragment=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.
[filename](tutorial-selectors.test.ts ':include :type=code :fragment=sel1')
## Subduxes
Now that we have all the building blocks, we can embark on the last and
funkiest part of Updux: its recursive nature.
### Recap: the Todos dux, undivided
Upduxes can be divided into sub-upduxes that deal with the various parts of
the global state. This is better understood by working out an example, so
let's recap on the Todos dux we have so far:
[filename](tutorial-monolith.test.ts ':include :type=code :fragment=mono')
This store has two main components: the `nextId`, and the `todos` collection.
The `todos` collection is itself composed of the individual `todo`s. Let's
create upduxes for each of those.
### NextId dux
[filename](nextId.ts ':include :type=code :fragment=dux')
### Todo updux
[filename](todo.ts ':include :type=code')
### Todos updux
[filename](todos.ts ':include :type=code')
### Main store
[filename](todoList.ts ':include :type=code')
Tadah! We had to define the `addTodo` effect at the top level as it needs to
access the `getNextId` selector from `nextId` and the `addTodoWithId`
action from the `todos`.
Note that the `getNextId` selector still gets the
right value; when aggregating subduxes selectors Updux auto-wraps them to
access the right slice of the top object.
## Reactions
Reactions -- aka Redux's subscriptions -- can be added to a updux store via the initial config
or the method `addSubscription`. The signature of a reaction is:
```
(storeApi) => (state, previousState, unsubscribe) => {
...
}
```
Subscriptions registered for an updux and its subduxes are automatically
subscribed to the store when calling `createStore`.
The `state` passed to the subscriptions of the subduxes is the local state.
Also, all subscriptions are wrapped such that they are called only if the
local `state` changed since their last invocation.
Example:
```
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 })
}
});
```
[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/

26
docs/tutorial.test.js Normal file
View File

@ -0,0 +1,26 @@
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'
})
});

View File

@ -1,8 +0,0 @@
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

@ -1,47 +0,0 @@
---
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,16 +0,0 @@
site_name: Updux
theme:
name: material
markdown_extensions:
- pymdownx.snippets
- pymdownx.inlinehilite
- pymdownx.superfences
- pymdownx.highlight:
default_lang: js
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
nav:
- Home: index.md
- Tutorial: tutorial.md
- API: api/modules.md

View File

@ -1,62 +1,59 @@
{ {
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@mobily/ts-belt": "^3.13.1", "@yanick/updeep-remeda": "^2.2.0",
"@yanick/updeep-remeda": "^2.3.1", "ajv": "^8.12.0",
"ajv": "^8.12.0", "expect-type": "^0.16.0",
"expect-type": "^0.16.0", "immer": "^9.0.15",
"immer": "^9.0.15", "json-schema-shorthand": "^2.0.0",
"json-schema-shorthand": "^2.0.0", "json-schema-to-ts": "^2.9.2",
"json-schema-to-ts": "^2.9.2", "memoize-one": "^6.0.0",
"memoize-one": "^6.0.0", "moize": "^6.1.6",
"moize": "^6.1.6", "redux": "^4.2.0",
"redux": "^4.2.0", "remeda": "^1.0.1",
"remeda": "^1.0.1", "updeep": "^1.2.1"
"updeep": "^1.2.1" },
}, "license": "MIT",
"license": "MIT", "module": "dist/index.js",
"module": "dist/index.js", "types": "./dist/index.d.ts",
"types": "./dist/index.d.ts", "exports": {
"exports": { ".": {
".": { "import": "./dist/index.js"
"import": "./dist/index.js"
}
},
"name": "updux",
"description": "Updeep-friendly Redux helper framework",
"scripts": {
"docsify:serve": "docsify serve docs"
},
"version": "5.1.0",
"repository": {
"type": "git",
"url": "git+https://github.com/yanick/updux.git"
},
"keywords": [
"redux",
"updeep"
],
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca)",
"bugs": {
"url": "https://github.com/yanick/updux/issues"
},
"homepage": "https://github.com/yanick/updux#readme",
"devDependencies": {
"@reduxjs/toolkit": "==2.0.0-alpha.5 ",
"@vitest/browser": "^0.23.1",
"@vitest/ui": "^2.0.5",
"docsify": "^4.13.1",
"eslint": "^8.22.0",
"eslint-plugin-no-only-tests": "^3.0.0",
"eslint-plugin-todo-plz": "^1.2.1",
"jsdoc-to-markdown": "^7.1.1",
"prettier": "^2.7.1",
"redux-toolkit": "^1.1.2",
"tsdoc-markdown": "^0.0.4",
"typedoc": "^0.25.9",
"typedoc-plugin-markdown": "^3.17.1",
"typescript": "^4.9.5",
"vite": "^4.2.1",
"vitest": "2.0.4"
} }
},
"name": "updux",
"description": "Updeep-friendly Redux helper framework",
"scripts": {
"docsify:serve": "docsify serve docs"
},
"version": "5.1.0",
"repository": {
"type": "git",
"url": "git+https://github.com/yanick/updux.git"
},
"keywords": [
"redux",
"updeep"
],
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca)",
"bugs": {
"url": "https://github.com/yanick/updux/issues"
},
"homepage": "https://github.com/yanick/updux#readme",
"devDependencies": {
"@reduxjs/toolkit": "==2.0.0-alpha.5 ",
"@vitest/browser": "^0.23.1",
"@vitest/ui": "^0.23.1",
"docsify": "^4.13.1",
"eslint": "^8.22.0",
"eslint-plugin-no-only-tests": "^3.0.0",
"eslint-plugin-todo-plz": "^1.2.1",
"jsdoc-to-markdown": "^7.1.1",
"prettier": "^2.7.1",
"redux-toolkit": "^1.1.2",
"tsdoc-markdown": "^0.0.4",
"typescript": "^4.9.5",
"vite": "^4.2.1",
"vitest": "0.23.1"
}
} }

View File

@ -0,0 +1,13 @@
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,115 +1,198 @@
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 { import {
Action,
ActionCreator,
AnyAction,
configureStore, configureStore,
EnhancedStore, EnhancedStore,
Action,
PayloadAction,
AnyAction,
ActionCreatorWithPreparedPayload,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
import { import { produce } from 'immer';
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 { buildReducer } from './reducer.js';
import { buildSelectors } from './selectors.js'; import { buildInitialState } from './initialState.js';
import { buildEffectsMiddleware, buildEffects, EffectMiddleware, augmentMiddlewareApi } from './effects.js'; import { buildSelectors, DuxSelectors } from './selectors.js';
import { augmentGetState, augmentDispatch } from './createStore.js'; import { AugmentedMiddlewareAPI, augmentGetState } from './createStore.js';
import { buildReactions } from './reactions.js'; import {
augmentMiddlewareApi,
buildEffects,
buildEffectsMiddleware,
EffectMiddleware,
} from './effects.js';
import buildSchema from './schema.js';
import Ajv from 'ajv';
type CreateStoreOptions<D> = Partial<{ export type Mutation<A = AnyAction, S = any> = (
preloadedState: DuxState<D>; payload: A extends {
}>; payload: infer P;
}
? P
: undefined,
action: A,
) => (state: S) => S | void;
interface ActionCreator<T extends string, P, A extends Array<any>> {
type: T; function buildValidateMiddleware(schema) {
match: ( // @ts-ignore
action: Action<unknown>, const ajv = new Ajv();
) => action is PayloadAction<P, T, never, never>; const validate = ajv.compile(schema);
(...args: A): PayloadAction<P, T, never, never>; 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));
}
} }
const moize = (func) => baseMoize(func, { maxSize: 1 });
/**
* @description main Updux class
*/
export default class Updux<D extends DuxConfig> { export default class Updux<D extends DuxConfig> {
/** @internal */ #mutations = [];
#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) { constructor(private readonly duxConfig: D) {
if (duxConfig.subduxes) // just to warn at creation time if config has issues
this.#subduxes = D.map(duxConfig.subduxes, (s) => this.actions;
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> { get initialState(): DuxState<D> {
return this.#memoInitialState( return this.memoInitialState(
this.duxConfig.initialState, this.duxConfig.initialState,
this.#subduxes, this.duxConfig.subduxes,
); );
} }
get subduxes() { get actions(): DuxActions<D> {
return this.#subduxes; 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 actions(): DuxActions<D> { get foo(): DuxActions<D> {
return this.#actions as any; 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;
} }
createStore( createStore(
options: CreateStoreOptions<D> = {}, options: Partial<{
preloadedState: DuxState<D>;
validate: boolean;
buildMiddleware: (middleware: any[]) => any
}> = {},
): EnhancedStore<DuxState<D>> & AugmentedMiddlewareAPI<D> { ): EnhancedStore<DuxState<D>> & AugmentedMiddlewareAPI<D> {
const preloadedState = options.preloadedState; const preloadedState = options.preloadedState;
@ -121,19 +204,32 @@ export default class Updux<D extends DuxConfig> {
let middleware = [effects]; let middleware = [effects];
const store: any = configureStore({ if (options.validate) {
preloadedState, middleware.unshift(
buildValidateMiddleware(this.schema)
)
}
if (options.buildMiddleware)
middleware = options.buildMiddleware(middleware);
const store = configureStore({
reducer: this.reducer, reducer: this.reducer,
preloadedState,
middleware, middleware,
}); });
store.dispatch = augmentDispatch(store.dispatch, this.actions); 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.getState = augmentGetState(store.getState, this.selectors); store.getState = augmentGetState(store.getState, this.selectors);
store.actions = this.actions;
store.selectors = this.selectors;
for (const reaction of this.reactions) { for (const reaction of this.reactions) {
let unsub; let unsub;
const r = reaction(store); const r = reaction(store);
@ -141,110 +237,32 @@ export default class Updux<D extends DuxConfig> {
unsub = store.subscribe(() => r(unsub)); unsub = store.subscribe(() => r(unsub));
} }
(store as any).actions = this.actions;
(store as any).selectors = this.selectors;
return store as any; 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>
// >
// >;
} }
addAction(action: ActionCreator<any, any, any>) { #effects = [];
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}`);
}
addMutation<A extends keyof DuxActions<D>>( addEffect(
actionType: A, actionType: keyof DuxActions<D>,
mutation: Mutation< effect: EffectMiddleware<D>,
DuxActions<D>[A] extends (...args: any[]) => infer R ? R : AnyAction,
DuxState<D>
>,
terminal?: boolean,
): Updux<D>; ): Updux<D>;
addMutation<AC extends ActionCreatorWithPreparedPayload<any, any, string, never, never>>( addEffect(
actionCreator: AC, actionCreator: { match: (action: any) => boolean },
mutation: Mutation<ReturnType<AC>, DuxState<D>>, effect: EffectMiddleware<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>; ): Updux<D>;
addEffect( addEffect(
guardFunc: (action: AnyAction) => boolean, guardFunc: (action: AnyAction) => boolean,
@ -280,45 +298,53 @@ export default class Updux<D extends DuxConfig> {
}; };
} }
this.#effects = this.#effects.concat(effect); this.#effects = [...this.#effects, effect];
return this as any; return this;
} }
get effects(): any { get effects(): any {
return this.#memoBuildEffects(this.#effects, this.#subduxes); return this.memoBuildEffects(this.#effects, this.duxConfig.subduxes);
}
get upreducer() {
return (action: AnyAction) => (state?: DuxState<D>) => this.reducer(state, action);
} }
#reactions = [];
addReaction( addReaction(
reaction: DuxReaction<D> reaction// :DuxReaction<D>
) { ) {
let previous: any; let previous: any;
const memoized = (api: any) => { const memoized = (api: any) => {
api = augmentMiddlewareApi(api, this.actions, this.selectors); api = augmentMiddlewareApi(api, this.actions, this.selectors);
const r = reaction(api); const r = reaction(api);
return (unsub: () => void) => {
const rMemoized = (localState, unsub) => { const state = api.getState();
if (state === previous) return;
let p = previous; let p = previous;
previous = localState; previous = state;
r(localState, p, unsub); r(state, p, unsub);
}; };
return (unsub: () => void) => rMemoized(api.getState(), unsub);
}; };
this.#reactions = [
this.#reactions = ...this.#reactions, memoized
this.#reactions.concat(memoized as any); ]
return this;
} }
get reactions() { get reactions() {
return this.#memoBuildReactions(this.#reactions, this.#subduxes); 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,
),
),
),
];
} }
} }

View File

@ -1,94 +0,0 @@
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,6 +1,11 @@
import { ActionCreatorWithPreparedPayload } from '@reduxjs/toolkit'; import Updux from './Updux.js';
import { buildActions, createAction, withPayload } from './actions.js'; import { createAction, withPayload, expandAction } from './actions.js';
import Updux from './index.js'; import type { ExpandedAction } from './actions.js';
import { expectTypeOf } from 'expect-type';
import {
ActionCreatorWithoutPayload,
ActionCreatorWithPreparedPayload,
} from '@reduxjs/toolkit';
test('basic action', () => { test('basic action', () => {
const foo = createAction( const foo = createAction(
@ -23,91 +28,187 @@ test('basic action', () => {
}>(); }>();
}); });
test('withPayload, just the type', () => { test('Updux config accepts actions', () => {
const foo = createAction('foo', withPayload<string>()); const foo = new Updux({
expect(foo('bar')).toEqual({
type: 'foo',
payload: 'bar',
});
expectTypeOf(foo).parameters.toMatchTypeOf<[string]>();
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',
});
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('Updux interactions', () => {
const dux = new Updux({
initialState: { a: 3 },
actions: { actions: {
add: (x: number) => x, one: createAction(
withNull: null, 'one',
withPayload((x) => ({ x })),
),
two: createAction(
'two',
withPayload((x) => x),
),
},
});
expect(Object.keys(foo.actions)).toHaveLength(2);
expect(foo.actions.one).toBeTypeOf('function');
expect(foo.actions.one('potato')).toEqual({
type: 'one',
payload: {
x: 'potato',
},
});
expectTypeOf(foo.actions.one).toMatchTypeOf<
ActionCreatorWithPreparedPayload<any, any, any, any>
>();
});
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({
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,
}, },
subduxes: { subduxes: {
subdux1: new Updux({ beta: {
actions: { actions: {
fromSubdux: (x: string) => x baz,
} },
}), },
subdux2: { // to check if we can deal with empty actions
actions: { gamma: {},
ohmy: () => { } },
}
}
}
}); });
test('actions getter', () => { expect(foo.actions).toHaveProperty('bar');
expect(dux.actions.add).toBeTruthy(); expect(foo.actions).toHaveProperty('baz');
});
expectTypeOf(dux.actions.withNull).toMatchTypeOf< expect(foo.actions.bar(2)).toHaveProperty('type', 'bar');
ActionCreatorWithPreparedPayload<any, null, 'withNull'>>(); expect(foo.actions.baz()).toHaveProperty('type', 'baz');
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,8 +1,54 @@
import { createAction } from '@reduxjs/toolkit'; import {
import { D } from '@mobily/ts-belt'; ActionCreator,
import { DuxActions, DuxConfig, Subduxes } from './types.js'; ActionCreatorWithoutPayload,
createAction,
} from '@reduxjs/toolkit';
export { createAction } from '@reduxjs/toolkit'; 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>(): (input: P) => { payload: P };
export function withPayload<P, A extends any[]>( export function withPayload<P, A extends any[]>(
@ -14,17 +60,20 @@ export function withPayload(prepare = (input) => input) {
}); });
} }
export function buildActions<L extends DuxConfig['actions']>( export function expandAction(prepare, actionType?: string) {
localActions: L, if (typeof prepare === 'function' && prepare.type) return prepare;
): DuxActions<{ actions: L }>;
export function buildActions<L extends DuxActions<any>, S extends Subduxes>( if (typeof prepare === 'function') {
localActions: L, return createAction(actionType, withPayload(prepare));
subduxes: S, }
): DuxActions<{ actions: L; subduxes: S }>;
if (actionType) {
return createAction(actionType);
}
}
export function buildActions(localActions = {}, subduxes = {}) { export function buildActions(localActions = {}, subduxes = {}) {
localActions = D.mapWithKey(localActions, (key, value) => localActions = R.mapValues(localActions, expandAction);
expandAction(value, String(key)),
);
let actions: Record<string, string> = {}; let actions: Record<string, string> = {};
@ -41,27 +90,18 @@ export function buildActions(localActions = {}, subduxes = {}) {
} }
actions[a] = slice; actions[a] = slice;
} }
}
for (const a in localActions) { for (const a in localActions) {
if (actions[a]) { if (actions[a]) {
throw new Error( throw new Error(
`action '${a}' defined both locally and in subdux '${actions[a]}'`, `action '${a}' defined both locally and in subdux '${actions[a]}'`,
); );
}
} }
} }
return [ return R.mergeAll([
localActions, localActions,
...D.values(subduxes).map((s: any) => s.actions ?? {}), ...Object.values(subduxes).map(R.pathOr<any, any>(['actions'], {})),
].reduce(D.merge); ]) as any;
}
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,30 +1,67 @@
import { createAction } from '@reduxjs/toolkit';
import { expectTypeOf } from 'expect-type';
import { test, expect } from 'vitest'; import { test, expect } from 'vitest';
import Updux, { createAction, withPayload } from './index.js'; import Updux from './Updux.js';
const incr = createAction('incr', withPayload<number>());
const dux = new Updux({ const dux = new Updux({
initialState: 1, initialState: 'a',
// selectors: { actions: {
// double: (x: string) => x + x, action1: 0,
// }, },
}).addMutation(incr, (i, action) => state => { selectors: {
expectTypeOf(i).toEqualTypeOf<number>(); double: (x: string) => x + x,
expectTypeOf(state).toEqualTypeOf<number>(); },
expectTypeOf(action).toEqualTypeOf<{ type: 'incr', payload: number; }>();
return state + i
}); });
suite.only('store dispatch actions', () => { 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(); const store = dux.createStore();
test('dispatch actions', () => { expect(store.getState()).toEqual('a');
expect(store.dispatch.incr).toBeTypeOf('function');
expectTypeOf(store.dispatch.incr).toMatchTypeOf<Function>(); 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('reducer does something', () => { subdux1.addMutation(incr, () => (state) => state + 1);
store.dispatch.incr(7);
expect(store.getState()).toEqual(8); 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 });
}); });

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