Skip to content

Commit

Permalink
Add tests and major refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
tommasoberlose committed May 29, 2024
1 parent 22900d2 commit fba1ee3
Show file tree
Hide file tree
Showing 19 changed files with 1,148 additions and 243 deletions.
28 changes: 28 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const globals = require("globals");

module.exports = {
root: true,
plugins: [
"@typescript-eslint/eslint-plugin",
"eslint-plugin-tsdoc"
],
extends: [
'plugin:@typescript-eslint/recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: __dirname,
ecmaVersion: 2018,
sourceType: "module"
},
rules: {
"tsdoc/syntax": "warn",
'prettier/prettier': [
'error',
{
'no-inline-styles': false,
},
],
},
};
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
arrowParens: 'avoid',
bracketSameLine: true,
bracketSpacing: true,
singleQuote: true,
trailingComma: 'all',
};
10 changes: 10 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";


export default [
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
];
16 changes: 16 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
testMatch: [
'(/test/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
'**/?(*.)+(spec|test).[tj]s?(x)',
],
transform: {
'^.+\\.[t|j]sx?$': 'babel-jest',
},
preset: 'ts-jest',
testEnvironment: 'node',
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json',
},
},
};
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.35",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down Expand Up @@ -45,12 +45,18 @@
}
],
"devDependencies": {
"@eslint/js": "^9.1.1",
"@size-limit/preset-small-lib": "^11.1.2",
"eslint": "^9.1.1",
"eslint-plugin-tsdoc": "^0.2.17",
"globals": "^15.1.0",
"husky": "^9.0.11",
"prettier": "3.2.5",
"size-limit": "^11.1.2",
"tsdx": "^0.14.1",
"tslib": "^2.6.2",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"typescript-eslint": "^7.8.0"
},
"dependencies": {
"@reduxjs/toolkit": "^2.2.3"
Expand Down
159 changes: 116 additions & 43 deletions src/extraReducersBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,132 @@
import { ActionReducerMapBuilder, CaseReducer, Action } from "@reduxjs/toolkit";

export class Builder<
SliceState,
PersistedSliceState extends SliceState & {
sliceStorageLastUpdateAt?: number;
},
> implements ActionReducerMapBuilder<PersistedSliceState>
import { ActionReducerMapBuilder, CaseReducer, Action, Draft } from "@reduxjs/toolkit";

/**
* Extension of a reducer that invokes a callback every time
* this specific reducer is called.
*
* @internal
*/
const persistReducer = <SliceState>(r: CaseReducer<SliceState, Action>, onStateUpdate: () => void) => (s: Draft<SliceState>, a: Action): void | SliceState | Draft<SliceState> => {
const ns = r(s, a);
onStateUpdate();
if (ns) return ns;
};

/**
* A builder for an action <-> reducer map updating the attribute
* sliceStorageLastUpdateAt whenever the state is changed.
*
* @param builder The builder of the extraReducers
* @param onStateUpdate The callback to invoke when the state is changed
*
* @public
*/
export class Builder<SliceState> implements ActionReducerMapBuilder<SliceState>
{
name: string;
builder: ActionReducerMapBuilder<PersistedSliceState>;
builder: ActionReducerMapBuilder<SliceState>;
onStateUpdate: () => void;
constructor(
name: string,
builder: ActionReducerMapBuilder<PersistedSliceState>,
builder: ActionReducerMapBuilder<SliceState>,
onStateUpdate: () => void,
) {
this.name = name;
this.builder = builder;
this.onStateUpdate = onStateUpdate;
}

addDefaultCase(r: CaseReducer<PersistedSliceState, Action>) {
this.builder.addDefaultCase((s, a) => {
const ns = r(s, a);
if (ns) {
ns.sliceStorageLastUpdateAt = new Date().getMilliseconds();
} else {
s.sliceStorageLastUpdateAt = new Date().getMilliseconds();
}
return ns;
});
/**
* Adds a "default case" reducer that is executed if no case reducer and no matcher
* reducer was executed for this action.
* @param reducer - The fallback "default case" reducer function.
*
* @example
```ts
import { createReducer } from '@reduxjs/toolkit'
const initialState = { otherActions: 0 }
const reducer = createReducer(initialState, builder => {
builder
// .addCase(...)
// .addMatcher(...)
.addDefaultCase((state, action) => {
state.otherActions++
})
})
```
*/
addDefaultCase(r: CaseReducer<SliceState, Action>) {
this.builder.addDefaultCase(persistReducer(r, this.onStateUpdate));
return this;
}

addMatcher(p: any, r: CaseReducer<PersistedSliceState, any>) {
this.builder.addMatcher(p, (s, a) => {
const ns = r(s, a);
if (ns) {
ns.sliceStorageLastUpdateAt = new Date().getMilliseconds();
} else {
s.sliceStorageLastUpdateAt = new Date().getMilliseconds();
}
return ns;
});
/**
* Allows you to match your incoming actions against your own filter function instead of only the `action.type` property.
* @remarks
* If multiple matcher reducers match, all of them will be executed in the order
* they were defined in - even if a case reducer already matched.
* All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`.
* @param matcher - A matcher function. In TypeScript, this should be a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
* function
* @param reducer - The actual case reducer function.
*
* @example
```ts
import {
createAction,
createReducer,
AsyncThunk,
UnknownAction,
} from "@reduxjs/toolkit";
type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>;
type PendingAction = ReturnType<GenericAsyncThunk["pending"]>;
type RejectedAction = ReturnType<GenericAsyncThunk["rejected"]>;
type FulfilledAction = ReturnType<GenericAsyncThunk["fulfilled"]>;
const initialState: Record<string, string> = {};
const resetAction = createAction("reset-tracked-loading-state");
function isPendingAction(action: UnknownAction): action is PendingAction {
return typeof action.type === "string" && action.type.endsWith("/pending");
}
const reducer = createReducer(initialState, (builder) => {
builder
.addCase(resetAction, () => initialState)
// matcher can be defined outside as a type predicate function
.addMatcher(isPendingAction, (state, action) => {
state[action.meta.requestId] = "pending";
})
.addMatcher(
// matcher can be defined inline as a type predicate function
(action): action is RejectedAction => action.type.endsWith("/rejected"),
(state, action) => {
state[action.meta.requestId] = "rejected";
}
)
// matcher can just return boolean and the matcher can receive a generic argument
.addMatcher<FulfilledAction>(
(action) => action.type.endsWith("/fulfilled"),
(state, action) => {
state[action.meta.requestId] = "fulfilled";
}
);
});
```
*/
addMatcher(p: any, r: CaseReducer<SliceState, any>) {
this.builder.addMatcher(p, persistReducer(r, this.onStateUpdate));
return this;
}

addCase(ac: any, r: CaseReducer<PersistedSliceState, any>) {
this.builder.addCase(ac, (s, a) => {
const ns = r(s, a);
if (ns) {
ns.sliceStorageLastUpdateAt = new Date().getMilliseconds();
} else {
s.sliceStorageLastUpdateAt = new Date().getMilliseconds();
}
return ns;
});
/**
* Adds a case reducer to handle a single exact action type.
* @remarks
* All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`.
* @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type.
* @param reducer - The actual case reducer function.
*/
addCase(ac: string, r: CaseReducer<SliceState, any>) {
this.builder.addCase(ac, persistReducer(r, this.onStateUpdate));
return this;
}
}
Loading

0 comments on commit fba1ee3

Please sign in to comment.