riducers 1.2.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +313 -19
- package/package.json +8 -8
- package/src/helpers.ts +27 -0
- package/src/index.ts +208 -205
- package/src/listReducers.ts +86 -0
- package/src/mapListReducers.ts +193 -0
- package/src/mapReducers.ts +41 -0
- package/src/objReducers.ts +11 -0
- package/src/types.ts +55 -0
package/README.md
CHANGED
|
@@ -1,34 +1,328 @@
|
|
|
1
|
-
#
|
|
1
|
+
# riducers
|
|
2
2
|
|
|
3
|
-
Plug
|
|
3
|
+
Plug-and-play Redux reducers for entity-based state management. Supports four state types optimized for different use cases.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install riducers
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## State Types
|
|
12
|
+
|
|
13
|
+
| Type | State Shape | Best For |
|
|
14
|
+
|------|-------------|----------|
|
|
15
|
+
| `list` | `ModelObj[]` | Ordered collections |
|
|
16
|
+
| `map` | `{ [id]: ModelObj }` | Fast lookups by ID |
|
|
17
|
+
| `mapList` | `{ [key]: ModelObj[] }` | Grouped/categorized data |
|
|
18
|
+
| `static` | `ModelObj \| null` | Single objects (auth, settings) |
|
|
8
19
|
|
|
9
|
-
|
|
20
|
+
## Quick Start
|
|
10
21
|
|
|
11
|
-
```
|
|
22
|
+
```typescript
|
|
12
23
|
import { configureStore } from '@reduxjs/toolkit'
|
|
13
24
|
import { reducerBuilder } from 'riducers'
|
|
14
|
-
import { combineReducers } from 'redux'
|
|
15
25
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
const store = configureStore({
|
|
27
|
+
reducer: {
|
|
28
|
+
users: reducerBuilder('users', { stateType: 'list' }),
|
|
29
|
+
usersById: reducerBuilder('usersById', { stateType: 'map' }),
|
|
30
|
+
tasksByProject: reducerBuilder('tasks', { stateType: 'mapList', mapKeyName: 'projectId' }),
|
|
31
|
+
currentUser: reducerBuilder('currentUser'),
|
|
21
32
|
}
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 1. List Reducer (`stateType: 'list'`)
|
|
39
|
+
|
|
40
|
+
Stores entities as an ordered array. Supports: `insert`, `unshift`, `replace`, `delete`, `shift`, `sort`, `clear`.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const usersReducer = reducerBuilder('users', { stateType: 'list' })
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Actions & State Transitions
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Initial state
|
|
50
|
+
state = []
|
|
51
|
+
|
|
52
|
+
// insert - append items
|
|
53
|
+
dispatch({ type: 'users/insert', payload: { id: 1, name: 'Alice' } })
|
|
54
|
+
state = [{ id: 1, name: 'Alice' }]
|
|
55
|
+
|
|
56
|
+
dispatch({ type: 'users/insert', payload: [{ id: 2, name: 'Bob' }, { id: 3, name: 'Carol' }] })
|
|
57
|
+
state = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Carol' }]
|
|
58
|
+
|
|
59
|
+
// unshift - prepend item
|
|
60
|
+
dispatch({ type: 'users/unshift', payload: { id: 0, name: 'Zoe' } })
|
|
61
|
+
state = [{ id: 0, name: 'Zoe' }, { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Carol' }]
|
|
62
|
+
|
|
63
|
+
// delete - remove by id
|
|
64
|
+
dispatch({ type: 'users/delete', payload: { id: 2 } })
|
|
65
|
+
state = [{ id: 0, name: 'Zoe' }, { id: 1, name: 'Alice' }, { id: 3, name: 'Carol' }]
|
|
66
|
+
|
|
67
|
+
// shift - remove first item
|
|
68
|
+
dispatch({ type: 'users/shift' })
|
|
69
|
+
state = [{ id: 1, name: 'Alice' }, { id: 3, name: 'Carol' }]
|
|
70
|
+
|
|
71
|
+
// sort - reorder (default: ascending by keyName)
|
|
72
|
+
dispatch({ type: 'users/sort' })
|
|
73
|
+
state = [{ id: 1, name: 'Alice' }, { id: 3, name: 'Carol' }]
|
|
74
|
+
|
|
75
|
+
// sort with custom comparator
|
|
76
|
+
dispatch({ type: 'users/sort', sort: (a, b) => b.id - a.id })
|
|
77
|
+
state = [{ id: 3, name: 'Carol' }, { id: 1, name: 'Alice' }]
|
|
78
|
+
|
|
79
|
+
// replace - replace entire state
|
|
80
|
+
dispatch({ type: 'users/replace', payload: [{ id: 10, name: 'New' }] })
|
|
81
|
+
state = [{ id: 10, name: 'New' }]
|
|
82
|
+
|
|
83
|
+
// clear - reset to initial state
|
|
84
|
+
dispatch({ type: 'users/clear' })
|
|
85
|
+
state = []
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### With Auto-Sort
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const sortedReducer = reducerBuilder('users', {
|
|
92
|
+
stateType: 'list',
|
|
93
|
+
sort: (a, b) => a.name.localeCompare(b.name)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
dispatch({ type: 'users/insert', payload: [{ id: 1, name: 'Zoe' }, { id: 2, name: 'Alice' }] })
|
|
97
|
+
state = [{ id: 2, name: 'Alice' }, { id: 1, name: 'Zoe' }] // auto-sorted
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 2. Map Reducer (`stateType: 'map'`)
|
|
103
|
+
|
|
104
|
+
Stores entities keyed by ID for O(1) lookups. Supports: `insert`, `replace`, `delete`, `clear`.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const usersReducer = reducerBuilder('users', { stateType: 'map' })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Actions & State Transitions
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Initial state
|
|
114
|
+
state = {}
|
|
115
|
+
|
|
116
|
+
// insert - add/update items
|
|
117
|
+
dispatch({ type: 'users/insert', payload: { id: 1, name: 'Alice' } })
|
|
118
|
+
state = { 1: { id: 1, name: 'Alice' } }
|
|
119
|
+
|
|
120
|
+
dispatch({ type: 'users/insert', payload: [{ id: 2, name: 'Bob' }, { id: 3, name: 'Carol' }] })
|
|
121
|
+
state = {
|
|
122
|
+
1: { id: 1, name: 'Alice' },
|
|
123
|
+
2: { id: 2, name: 'Bob' },
|
|
124
|
+
3: { id: 3, name: 'Carol' }
|
|
22
125
|
}
|
|
23
126
|
|
|
24
|
-
|
|
127
|
+
// insert updates existing item
|
|
128
|
+
dispatch({ type: 'users/insert', payload: { id: 1, name: 'Alice Updated' } })
|
|
129
|
+
state = {
|
|
130
|
+
1: { id: 1, name: 'Alice Updated' },
|
|
131
|
+
2: { id: 2, name: 'Bob' },
|
|
132
|
+
3: { id: 3, name: 'Carol' }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// delete - remove by id
|
|
136
|
+
dispatch({ type: 'users/delete', payload: { id: 2 } })
|
|
137
|
+
state = {
|
|
138
|
+
1: { id: 1, name: 'Alice Updated' },
|
|
139
|
+
3: { id: 3, name: 'Carol' }
|
|
140
|
+
}
|
|
25
141
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
store.dispatch('user/sort', {})
|
|
30
|
-
store.dispatch('user/clear', {})
|
|
142
|
+
// replace - replace entire state
|
|
143
|
+
dispatch({ type: 'users/replace', payload: [{ id: 10, name: 'New' }] })
|
|
144
|
+
state = { 10: { id: 10, name: 'New' } }
|
|
31
145
|
|
|
146
|
+
// clear - reset to initial state
|
|
147
|
+
dispatch({ type: 'users/clear' })
|
|
148
|
+
state = {}
|
|
32
149
|
```
|
|
33
150
|
|
|
34
|
-
|
|
151
|
+
### Shorthand Syntax
|
|
152
|
+
|
|
153
|
+
Map reducers support using the key directly as the action type:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Insert/update using key as action type
|
|
157
|
+
dispatch({ type: 'users/123', payload: { name: 'Alice' } })
|
|
158
|
+
state = { 123: { id: 123, name: 'Alice' } }
|
|
159
|
+
|
|
160
|
+
// Delete by dispatch with no payload
|
|
161
|
+
dispatch({ type: 'users/123', payload: null })
|
|
162
|
+
state = {}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 3. MapList Reducer (`stateType: 'mapList'`)
|
|
168
|
+
|
|
169
|
+
Groups entities into keyed arrays—ideal for categorized data. Requires `mapKeyName`. Supports: `insert`, `unshift`, `replace`, `delete`, `shift`, `sort`, `clear`.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const tasksReducer = reducerBuilder('tasks', {
|
|
173
|
+
stateType: 'mapList',
|
|
174
|
+
keyName: 'id',
|
|
175
|
+
mapKeyName: 'projectId',
|
|
176
|
+
initialState: {}
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Actions & State Transitions
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// Initial state
|
|
184
|
+
state = {}
|
|
185
|
+
|
|
186
|
+
// insert - items grouped by mapKeyName
|
|
187
|
+
dispatch({ type: 'tasks/insert', payload: { id: 1, projectId: 'a', title: 'Task 1' } })
|
|
188
|
+
state = {
|
|
189
|
+
a: [{ id: 1, projectId: 'a', title: 'Task 1' }]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
dispatch({ type: 'tasks/insert', payload: [
|
|
193
|
+
{ id: 2, projectId: 'a', title: 'Task 2' },
|
|
194
|
+
{ id: 3, projectId: 'b', title: 'Task 3' }
|
|
195
|
+
]})
|
|
196
|
+
state = {
|
|
197
|
+
a: [{ id: 1, projectId: 'a', title: 'Task 1' }, { id: 2, projectId: 'a', title: 'Task 2' }],
|
|
198
|
+
b: [{ id: 3, projectId: 'b', title: 'Task 3' }]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// delete - items removed across all groups, empty groups removed
|
|
202
|
+
dispatch({ type: 'tasks/delete', payload: { id: 3 } })
|
|
203
|
+
state = {
|
|
204
|
+
a: [{ id: 1, projectId: 'a', title: 'Task 1' }, { id: 2, projectId: 'a', title: 'Task 2' }]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// sort - sorts within each group
|
|
208
|
+
dispatch({ type: 'tasks/sort', sort: (a, b) => b.id - a.id })
|
|
209
|
+
state = {
|
|
210
|
+
a: [{ id: 2, projectId: 'a', title: 'Task 2' }, { id: 1, projectId: 'a', title: 'Task 1' }]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// shift - removes first item from each group
|
|
214
|
+
dispatch({ type: 'tasks/shift' })
|
|
215
|
+
state = {
|
|
216
|
+
a: [{ id: 1, projectId: 'a', title: 'Task 1' }]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// replace - replace entire state
|
|
220
|
+
dispatch({ type: 'tasks/replace', payload: [{ id: 10, projectId: 'x', title: 'New' }] })
|
|
221
|
+
state = {
|
|
222
|
+
x: [{ id: 10, projectId: 'x', title: 'New' }]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// clear - reset to initial state
|
|
226
|
+
dispatch({ type: 'tasks/clear' })
|
|
227
|
+
state = {}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 4. Static Reducer (default)
|
|
233
|
+
|
|
234
|
+
Stores a single object or null. Supports: `insert`, `replace`, `delete`, `clear`.
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
const authReducer = reducerBuilder('auth')
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Actions & State Transitions
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// Initial state
|
|
244
|
+
state = null
|
|
245
|
+
|
|
246
|
+
// insert - set the object
|
|
247
|
+
dispatch({ type: 'auth/insert', payload: { userId: 1, token: 'abc123' } })
|
|
248
|
+
state = { userId: 1, token: 'abc123' }
|
|
249
|
+
|
|
250
|
+
// replace - replace entire object
|
|
251
|
+
dispatch({ type: 'auth/replace', payload: { userId: 2, token: 'xyz789' } })
|
|
252
|
+
state = { userId: 2, token: 'xyz789' }
|
|
253
|
+
|
|
254
|
+
// delete - set to null
|
|
255
|
+
dispatch({ type: 'auth/delete' })
|
|
256
|
+
state = null
|
|
257
|
+
|
|
258
|
+
// clear - reset to initial state
|
|
259
|
+
dispatch({ type: 'auth/clear' })
|
|
260
|
+
state = null
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Options
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
interface ReducerOpts {
|
|
269
|
+
stateType?: 'list' | 'map' | 'static' | 'mapList' // default: 'static'
|
|
270
|
+
keyName?: string // ID field name, default: 'id'
|
|
271
|
+
mapKeyName?: string // Required for mapList - key to group by
|
|
272
|
+
sort?: SortFn // Auto-sort on insert
|
|
273
|
+
initialState?: any // Custom initial state
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Full Store Example
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import { configureStore } from '@reduxjs/toolkit'
|
|
281
|
+
import { reducerBuilder } from 'riducers'
|
|
282
|
+
|
|
283
|
+
const store = configureStore({
|
|
284
|
+
reducer: {
|
|
285
|
+
// Ordered list of posts
|
|
286
|
+
posts: reducerBuilder('posts', { stateType: 'list' }),
|
|
287
|
+
|
|
288
|
+
// Users indexed by ID for quick lookup
|
|
289
|
+
users: reducerBuilder('users', { stateType: 'map' }),
|
|
290
|
+
|
|
291
|
+
// Comments grouped by postId
|
|
292
|
+
comments: reducerBuilder('comments', {
|
|
293
|
+
stateType: 'mapList',
|
|
294
|
+
mapKeyName: 'postId',
|
|
295
|
+
initialState: {}
|
|
296
|
+
}),
|
|
297
|
+
|
|
298
|
+
// Current authenticated user
|
|
299
|
+
auth: reducerBuilder('auth'),
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// After some dispatches, state might look like:
|
|
304
|
+
// {
|
|
305
|
+
// posts: [
|
|
306
|
+
// { id: 1, title: 'Hello World' },
|
|
307
|
+
// { id: 2, title: 'Redux Tips' }
|
|
308
|
+
// ],
|
|
309
|
+
// users: {
|
|
310
|
+
// 1: { id: 1, name: 'Alice' },
|
|
311
|
+
// 2: { id: 2, name: 'Bob' }
|
|
312
|
+
// },
|
|
313
|
+
// comments: {
|
|
314
|
+
// 1: [
|
|
315
|
+
// { id: 101, postId: 1, text: 'Great post!' },
|
|
316
|
+
// { id: 102, postId: 1, text: 'Thanks!' }
|
|
317
|
+
// ],
|
|
318
|
+
// 2: [
|
|
319
|
+
// { id: 201, postId: 2, text: 'Very helpful' }
|
|
320
|
+
// ]
|
|
321
|
+
// },
|
|
322
|
+
// auth: { userId: 1, token: 'abc123' }
|
|
323
|
+
// }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riducers",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Plug-and-play Redux reducers for entity-based state management. Supports list, map, mapList, and static state types with built-in insert, delete, replace, sort, and clear actions.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "esm/index.js",
|
|
7
7
|
"files": [
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"author": "Yusuf Bhabhrawala",
|
|
27
27
|
"license": "ISC",
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@types/jest": "^
|
|
30
|
-
"jest": "^
|
|
31
|
-
"rimraf": "^
|
|
32
|
-
"ts-jest": "^29.
|
|
33
|
-
"typescript": "^
|
|
29
|
+
"@types/jest": "^30.0.0",
|
|
30
|
+
"jest": "^30.2.0",
|
|
31
|
+
"rimraf": "^6.1.3",
|
|
32
|
+
"ts-jest": "^29.4.6",
|
|
33
|
+
"typescript": "^5.9.3"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"redux": "^
|
|
36
|
+
"redux": "^5.0.1"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ModelObj, SortFn } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a sort function that sorts by a specific key
|
|
5
|
+
*/
|
|
6
|
+
export const keySort = (key: string): SortFn => (a: ModelObj, b: ModelObj) => {
|
|
7
|
+
if (typeof a[key] === "number" && typeof b[key] === "number") {
|
|
8
|
+
return a[key] - b[key];
|
|
9
|
+
} else if (typeof a[key] === "string" && typeof b[key] === "string") {
|
|
10
|
+
return a[key].localeCompare(b[key]);
|
|
11
|
+
}
|
|
12
|
+
return 0;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes payload to always be an array
|
|
17
|
+
*/
|
|
18
|
+
export const normalizePayload = <T>(payload: T | T[]): T[] => {
|
|
19
|
+
return Array.isArray(payload) ? payload : [payload];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initializes state with default value if undefined/null
|
|
24
|
+
*/
|
|
25
|
+
export const initializeState = <T>(state: T | undefined | null, defaultValue: T): T => {
|
|
26
|
+
return state ?? defaultValue;
|
|
27
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,241 +1,244 @@
|
|
|
1
|
-
|
|
1
|
+
import { ReducerOpts, ReducerAction, ModelObj, SortFn } from "./types";
|
|
2
|
+
import { mapInsertReducer, mapDeleteReducer, mapReplaceReducer } from "./mapReducers";
|
|
3
|
+
import {
|
|
4
|
+
listInsertReducer,
|
|
5
|
+
listUnshiftReducer,
|
|
6
|
+
listSortReducer,
|
|
7
|
+
listDeleteReducer,
|
|
8
|
+
listShiftReducer,
|
|
9
|
+
listReplaceReducer
|
|
10
|
+
} from "./listReducers";
|
|
11
|
+
import {
|
|
12
|
+
mapListInsertReducer,
|
|
13
|
+
mapListDeleteReducer,
|
|
14
|
+
mapListReplaceReducer,
|
|
15
|
+
mapListSortReducer,
|
|
16
|
+
mapListShiftReducer,
|
|
17
|
+
mapListUnshiftReducer,
|
|
18
|
+
MapListState
|
|
19
|
+
} from "./mapListReducers";
|
|
20
|
+
import { objInsertReducer, objReplaceReducer, objDeleteReducer } from "./objReducers";
|
|
21
|
+
|
|
22
|
+
// Re-export types for external usage
|
|
23
|
+
export * from "./types";
|
|
24
|
+
export { keySort } from "./helpers";
|
|
2
25
|
|
|
3
|
-
|
|
4
|
-
[key: string]: any;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
type SortFn = (a: ModelObj, b: ModelObj) => number;
|
|
8
|
-
|
|
9
|
-
interface ListAction {
|
|
10
|
-
payload: ModelObj | ModelObj[];
|
|
11
|
-
sort?: SortFn;
|
|
12
|
-
keyName: string;
|
|
13
|
-
mapKeyName?: string;
|
|
14
|
-
isMapList: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface MapAction {
|
|
18
|
-
payload: ModelObj | ModelObj[];
|
|
19
|
-
sort?: SortFn;
|
|
20
|
-
keyName: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface ListSortAction {
|
|
24
|
-
sort?: SortFn;
|
|
25
|
-
keyName: string;
|
|
26
|
-
mapKeyName?: string;
|
|
27
|
-
isMapList: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const keySort = (key: string) => (a: ModelObj, b: ModelObj) => {
|
|
31
|
-
if (typeof a[key] === "number" && typeof b[key] === "number") return a[key] - b[key];
|
|
32
|
-
else if (typeof a[key] === "string" && typeof b[key] === "string") return a[key].localeCompare(b[key]);
|
|
33
|
-
return 0;
|
|
34
|
-
};
|
|
26
|
+
const OPS = ["insert", "replace", "delete", "clear", "sort"];
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Gets the default initial state based on state type
|
|
30
|
+
*/
|
|
31
|
+
const getDefaultInitialState = (stateType: string): any => {
|
|
32
|
+
switch (stateType) {
|
|
33
|
+
case "list":
|
|
34
|
+
return [];
|
|
35
|
+
case "map":
|
|
36
|
+
return {};
|
|
37
|
+
default:
|
|
38
|
+
return null;
|
|
42
39
|
}
|
|
43
|
-
return newState;
|
|
44
40
|
};
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Handles map-specific operation normalization
|
|
44
|
+
* When using map type, allows using the key as the operation (e.g., "users/123" for insert/delete)
|
|
45
|
+
*/
|
|
46
|
+
const normalizeMapOperation = (
|
|
47
|
+
op: string,
|
|
48
|
+
payload: any,
|
|
49
|
+
keyName: string
|
|
50
|
+
): { op: string; payload: any } => {
|
|
51
|
+
if (!OPS.includes(op)) {
|
|
52
|
+
if (payload && !Array.isArray(payload)) {
|
|
53
|
+
payload[keyName] = op;
|
|
54
|
+
return { op: "insert", payload };
|
|
55
|
+
}
|
|
56
|
+
if (!payload) {
|
|
57
|
+
const newPayload: ModelObj = { [keyName]: op };
|
|
58
|
+
return { op: "delete", payload: newPayload };
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
|
-
return
|
|
61
|
+
return { op, payload };
|
|
54
62
|
};
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Handles list operations (insert, replace, delete, shift, sort)
|
|
66
|
+
*/
|
|
67
|
+
const handleListOperation = (
|
|
68
|
+
state: ModelObj[],
|
|
69
|
+
op: string,
|
|
70
|
+
payload: any,
|
|
71
|
+
sort: SortFn | undefined,
|
|
72
|
+
keyName: string,
|
|
73
|
+
initialState: any
|
|
74
|
+
): ModelObj[] | undefined => {
|
|
75
|
+
const isMapList = false;
|
|
76
|
+
switch (op) {
|
|
77
|
+
case "insert":
|
|
78
|
+
if (payload) return listInsertReducer(state, { payload, sort, keyName, isMapList });
|
|
79
|
+
break;
|
|
80
|
+
case "unshift":
|
|
81
|
+
if (payload) return listUnshiftReducer(state, { payload, sort, keyName, isMapList });
|
|
82
|
+
break;
|
|
83
|
+
case "replace":
|
|
84
|
+
return listReplaceReducer(state, { payload, sort, keyName, isMapList });
|
|
85
|
+
case "delete":
|
|
86
|
+
return listDeleteReducer(state, { payload, keyName, isMapList });
|
|
87
|
+
case "shift":
|
|
88
|
+
return listShiftReducer(state);
|
|
89
|
+
case "clear":
|
|
90
|
+
return initialState;
|
|
91
|
+
case "sort":
|
|
92
|
+
return listSortReducer(state, { sort, keyName, isMapList });
|
|
61
93
|
}
|
|
62
|
-
return
|
|
94
|
+
return undefined;
|
|
63
95
|
};
|
|
64
96
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Handles mapList operations (insert, replace, delete, shift, sort, unshift)
|
|
99
|
+
* mapList is a hybrid: { [mapKeyValue]: ModelObj[] }
|
|
100
|
+
*/
|
|
101
|
+
const handleMapListOperation = (
|
|
102
|
+
state: MapListState,
|
|
103
|
+
op: string,
|
|
104
|
+
payload: any,
|
|
105
|
+
sort: SortFn | undefined,
|
|
106
|
+
keyName: string,
|
|
107
|
+
mapKeyName: string,
|
|
108
|
+
initialState: any
|
|
109
|
+
): MapListState | undefined => {
|
|
110
|
+
switch (op) {
|
|
111
|
+
case "insert":
|
|
112
|
+
if (payload) return mapListInsertReducer(state, { payload, sort, keyName, mapKeyName });
|
|
113
|
+
break;
|
|
114
|
+
case "unshift":
|
|
115
|
+
if (payload) return mapListUnshiftReducer(state, { payload, sort, keyName, mapKeyName });
|
|
116
|
+
break;
|
|
117
|
+
case "replace":
|
|
118
|
+
return mapListReplaceReducer(state, { payload, sort, keyName, mapKeyName });
|
|
119
|
+
case "delete":
|
|
120
|
+
return mapListDeleteReducer(state, { payload, keyName, mapKeyName });
|
|
121
|
+
case "shift":
|
|
122
|
+
return mapListShiftReducer(state);
|
|
123
|
+
case "clear":
|
|
124
|
+
return initialState;
|
|
125
|
+
case "sort":
|
|
126
|
+
return mapListSortReducer(state, { sort, keyName, mapKeyName });
|
|
82
127
|
}
|
|
83
|
-
|
|
84
|
-
return newState;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const listUnshiftReducer = (state: ModelObj[], action: ListAction) => {
|
|
88
|
-
if (!state) state = [];
|
|
89
|
-
let resp: ModelObj[] = Array.isArray(action.payload) ? action.payload : [action.payload];
|
|
90
|
-
|
|
91
|
-
let newState = [...resp, ...state];
|
|
92
|
-
return newState;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const listSortReducer = (state: ModelObj[], action: ListSortAction) => {
|
|
96
|
-
if (!state) state = [];
|
|
97
|
-
let newState = [...state];
|
|
98
|
-
let sort = action?.sort ? action.sort : keySort(action.keyName);
|
|
99
|
-
newState.sort(sort);
|
|
100
|
-
return newState;
|
|
128
|
+
return undefined;
|
|
101
129
|
};
|
|
102
130
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Handles map operations (insert, replace, delete, clear)
|
|
133
|
+
*/
|
|
134
|
+
const handleMapOperation = (
|
|
135
|
+
state: ModelObj,
|
|
136
|
+
op: string,
|
|
137
|
+
payload: any,
|
|
138
|
+
keyName: string,
|
|
139
|
+
initialState: any
|
|
140
|
+
): ModelObj | null | undefined => {
|
|
141
|
+
switch (op) {
|
|
142
|
+
case "insert":
|
|
143
|
+
if (payload) return mapInsertReducer(state, { payload, keyName });
|
|
144
|
+
break;
|
|
145
|
+
case "replace":
|
|
146
|
+
return mapReplaceReducer(state, { payload, keyName });
|
|
147
|
+
case "delete":
|
|
148
|
+
return mapDeleteReducer(state, { payload, keyName });
|
|
149
|
+
case "clear":
|
|
150
|
+
return initialState;
|
|
112
151
|
}
|
|
113
|
-
return
|
|
152
|
+
return undefined;
|
|
114
153
|
};
|
|
115
154
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Handles static/object operations (insert, replace, delete, clear)
|
|
157
|
+
*/
|
|
158
|
+
const handleStaticOperation = (
|
|
159
|
+
state: any,
|
|
160
|
+
op: string,
|
|
161
|
+
payload: any,
|
|
162
|
+
initialState: any
|
|
163
|
+
): any | undefined => {
|
|
164
|
+
switch (op) {
|
|
165
|
+
case "insert":
|
|
166
|
+
return objInsertReducer(state, { payload });
|
|
167
|
+
case "replace":
|
|
168
|
+
return objReplaceReducer(state, { payload });
|
|
169
|
+
case "delete":
|
|
170
|
+
return objDeleteReducer();
|
|
171
|
+
case "clear":
|
|
172
|
+
return initialState;
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
132
175
|
};
|
|
133
176
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Builds a reducer function for managing state with various operations
|
|
179
|
+
*/
|
|
180
|
+
function reducerBuilder(key: string, opts?: ReducerOpts) {
|
|
181
|
+
const stateType = opts?.stateType ?? "static";
|
|
182
|
+
const isList = stateType === "list";
|
|
183
|
+
const isMap = stateType === "map";
|
|
184
|
+
const isMapList = stateType === "mapList";
|
|
137
185
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
186
|
+
if (isMapList && !opts?.mapKeyName) {
|
|
187
|
+
throw new Error("mapKeyName is required for stateType: mapList");
|
|
188
|
+
}
|
|
141
189
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
keyName?: string;
|
|
146
|
-
mapKeyName?: string;
|
|
147
|
-
initialState?: any;
|
|
148
|
-
}
|
|
190
|
+
// Use provided initialState if defined, otherwise get default based on state type
|
|
191
|
+
const initialState = opts?.initialState !== undefined ? opts.initialState : getDefaultInitialState(stateType);
|
|
192
|
+
const actionPattern = new RegExp(`^${key}/([^\/]+)$`);
|
|
149
193
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
let initialState = opts?.initialState;
|
|
153
|
-
let stateType = opts?.stateType ?? "static";
|
|
154
|
-
let isList = stateType === "list";
|
|
155
|
-
let isMap = stateType === "map";
|
|
156
|
-
let isMapList = stateType === "mapList";
|
|
157
|
-
|
|
158
|
-
if (isMapList && !opts?.mapKeyName) throw new Error("mapKeyName is required for stateType: mapList");
|
|
159
|
-
|
|
160
|
-
if (initialState === undefined) {
|
|
161
|
-
if (isList) initialState = [];
|
|
162
|
-
else if (isMap) initialState = {};
|
|
163
|
-
else initialState = null;
|
|
164
|
-
}
|
|
165
|
-
return (state: any, action: { type: string; payload?: any; sort?: (a: ModelObj, b: ModelObj) => number }) => {
|
|
194
|
+
return (state: any, action: ReducerAction): any => {
|
|
195
|
+
// Initialize state
|
|
166
196
|
if (state === undefined) {
|
|
167
|
-
|
|
168
|
-
else if (isList) return [];
|
|
169
|
-
else if (isMap) return {};
|
|
170
|
-
else if (isMapList) return {};
|
|
171
|
-
else return null;
|
|
197
|
+
return initialState !== undefined ? initialState : getDefaultInitialState(stateType);
|
|
172
198
|
}
|
|
173
|
-
let rx = new RegExp(`^${key}/([^\/]+)$`);
|
|
174
|
-
let m = action.type.match(rx);
|
|
175
|
-
if (!m) return state;
|
|
176
199
|
|
|
177
|
-
|
|
200
|
+
// Match action type
|
|
201
|
+
const match = action.type.match(actionPattern);
|
|
202
|
+
if (!match) return state;
|
|
203
|
+
|
|
204
|
+
let op = match[1];
|
|
178
205
|
let payload = action.payload;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
payload = {};
|
|
189
|
-
payload[keyName] = op;
|
|
190
|
-
op = "delete";
|
|
191
|
-
}
|
|
206
|
+
const keyName = opts?.keyName ?? "id";
|
|
207
|
+
const mapKeyName = opts?.mapKeyName;
|
|
208
|
+
const sort = action.sort ?? opts?.sort;
|
|
209
|
+
|
|
210
|
+
// Handle map-specific operation normalization
|
|
211
|
+
if (isMap) {
|
|
212
|
+
const normalized = normalizeMapOperation(op, payload, keyName);
|
|
213
|
+
op = normalized.op;
|
|
214
|
+
payload = normalized.payload;
|
|
192
215
|
}
|
|
193
216
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if ((isList || isMapList) && payload) return listInsertReducer(state, { payload, sort, keyName, mapKeyName, isMapList });
|
|
198
|
-
else if (isMap && payload) return mapInsertReducer(state, { payload, keyName });
|
|
199
|
-
return objInsertReducer(state, { payload });
|
|
200
|
-
|
|
201
|
-
} else if (op === 'unshift') {
|
|
202
|
-
if (isList && payload) return listUnshiftReducer(state, { payload, sort, keyName, mapKeyName, isMapList });
|
|
203
|
-
else {
|
|
204
|
-
console.error(`unshift is only valid for stateType: list or mapList`);
|
|
205
|
-
}
|
|
206
|
-
} else if (op === "replace") {
|
|
207
|
-
|
|
208
|
-
if (isList || isMapList) return listReplaceReducer(state, { payload, sort, keyName, mapKeyName, isMapList });
|
|
209
|
-
else if (isMap) return mapReplaceReducer(state, { payload, keyName });
|
|
210
|
-
return objReplaceReducer(state, { payload });
|
|
211
|
-
|
|
212
|
-
} else if (op === "delete") {
|
|
213
|
-
|
|
214
|
-
if (isList || isMapList) return listDeleteReducer(state, { payload, keyName, mapKeyName, isMapList });
|
|
215
|
-
else if (isMap) return mapDeleteReducer(state, { payload, keyName });
|
|
216
|
-
return objDeleteReducer();
|
|
217
|
-
|
|
218
|
-
} else if (op === "shift") {
|
|
219
|
-
if (isList || isMapList) return listShiftReducer(state);
|
|
220
|
-
else {
|
|
221
|
-
console.error(`unshift is only valid for stateType: list or mapList`);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
} else if (op === "clear") {
|
|
225
|
-
|
|
226
|
-
return initialState;
|
|
227
|
-
|
|
228
|
-
} else if (op === "sort") {
|
|
229
|
-
|
|
230
|
-
if (isList || isMapList) return listSortReducer(state, { sort, keyName, mapKeyName, isMapList });
|
|
217
|
+
// Handle operations based on state type
|
|
218
|
+
let result: any;
|
|
231
219
|
|
|
220
|
+
if (isList) {
|
|
221
|
+
result = handleListOperation(state, op, payload, sort, keyName, initialState);
|
|
222
|
+
} else if (isMapList) {
|
|
223
|
+
result = handleMapListOperation(state, op, payload, sort, keyName, mapKeyName!, initialState);
|
|
224
|
+
} else if (isMap) {
|
|
225
|
+
result = handleMapOperation(state, op, payload, keyName, initialState);
|
|
232
226
|
} else {
|
|
227
|
+
result = handleStaticOperation(state, op, payload, initialState);
|
|
228
|
+
}
|
|
233
229
|
|
|
234
|
-
|
|
230
|
+
if (result !== undefined) {
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
235
233
|
|
|
234
|
+
// Unknown operation
|
|
235
|
+
if (!["insert", "unshift", "replace", "delete", "shift", "clear", "sort"].includes(op)) {
|
|
236
|
+
console.error(
|
|
237
|
+
`unknown op (${op}) in action type: ${action.type}. Op should be one of: insert, delete, replace, clear, or sort.`
|
|
238
|
+
);
|
|
236
239
|
}
|
|
237
|
-
|
|
238
|
-
return state || (
|
|
240
|
+
|
|
241
|
+
return state || getDefaultInitialState(stateType);
|
|
239
242
|
};
|
|
240
243
|
}
|
|
241
244
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ModelObj, Id, ListAction, ListSortAction } from "./types";
|
|
2
|
+
import { normalizePayload, initializeState, keySort } from "./helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Helper to get key mappings from items
|
|
6
|
+
*/
|
|
7
|
+
const getKeyMap = (items: ModelObj[], keyName: string): Id[] => {
|
|
8
|
+
return items.map((item: ModelObj) => item[keyName]);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper to find items to insert (not in state) and items to replace (in state)
|
|
13
|
+
*/
|
|
14
|
+
const partitionItems = (respMap: Id[], stateMap: Id[]) => {
|
|
15
|
+
const toInsert = respMap.filter((id: Id) => stateMap.indexOf(id) < 0);
|
|
16
|
+
const toReplace = respMap.filter((id: Id) => stateMap.indexOf(id) >= 0);
|
|
17
|
+
return { toInsert, toReplace };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const listInsertReducer = (state: ModelObj[], action: ListAction): ModelObj[] => {
|
|
21
|
+
const currentState = initializeState(state, []);
|
|
22
|
+
const items = normalizePayload(action.payload);
|
|
23
|
+
|
|
24
|
+
let newState = [...currentState];
|
|
25
|
+
const respMap = getKeyMap(items, action.keyName);
|
|
26
|
+
const stateMap = getKeyMap(newState, action.keyName);
|
|
27
|
+
const { toInsert, toReplace } = partitionItems(respMap, stateMap);
|
|
28
|
+
|
|
29
|
+
// Update existing items
|
|
30
|
+
for (const id of toReplace) {
|
|
31
|
+
const stateIndex = stateMap.indexOf(id);
|
|
32
|
+
const respIndex = respMap.indexOf(id);
|
|
33
|
+
newState[stateIndex] = { ...items[respIndex] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Add new items
|
|
37
|
+
if (toInsert.length > 0) {
|
|
38
|
+
const inserts = toInsert.map((id: Id) => items[respMap.indexOf(id)]);
|
|
39
|
+
newState = [...newState, ...inserts];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (action?.sort) {
|
|
43
|
+
newState.sort(action.sort);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return newState;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const listUnshiftReducer = (state: ModelObj[], action: ListAction): ModelObj[] => {
|
|
50
|
+
const currentState = initializeState(state, []);
|
|
51
|
+
const items = normalizePayload(action.payload);
|
|
52
|
+
return [...items, ...currentState];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const listSortReducer = (state: ModelObj[], action: ListSortAction): ModelObj[] => {
|
|
56
|
+
const currentState = initializeState(state, []);
|
|
57
|
+
const newState = [...currentState];
|
|
58
|
+
const sort = action?.sort ?? keySort(action.keyName);
|
|
59
|
+
newState.sort(sort);
|
|
60
|
+
return newState;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const listDeleteReducer = (state: ModelObj[], action: ListAction): ModelObj[] => {
|
|
64
|
+
const currentState = !state || !Array.isArray(state) ? [] : state;
|
|
65
|
+
const items = normalizePayload(action.payload);
|
|
66
|
+
const deleteKeys = new Set<Id>(getKeyMap(items, action.keyName));
|
|
67
|
+
return currentState.filter((item: ModelObj) => !deleteKeys.has(item[action.keyName]));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const listShiftReducer = (state: ModelObj[]): ModelObj[] => {
|
|
71
|
+
const currentState = !state || !Array.isArray(state) ? [] : state;
|
|
72
|
+
const newState = [...currentState];
|
|
73
|
+
newState.shift();
|
|
74
|
+
return newState;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const listReplaceReducer = (state: ModelObj[], action: ListAction): ModelObj[] => {
|
|
78
|
+
const items = normalizePayload(action.payload);
|
|
79
|
+
const result = items.map((item: ModelObj) => ({ ...item }));
|
|
80
|
+
|
|
81
|
+
if (action?.sort) {
|
|
82
|
+
result.sort(action.sort);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ModelObj, Id, SortFn, MapListAction, MapListSortAction } from "./types";
|
|
2
|
+
import { normalizePayload, initializeState, keySort } from "./helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* State structure for mapList: { [mapKeyValue]: ModelObj[] }
|
|
6
|
+
*/
|
|
7
|
+
export type MapListState = { [key: string]: ModelObj[] };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Helper to get key value from an item
|
|
11
|
+
*/
|
|
12
|
+
const getKeyValue = (item: ModelObj, keyName: string): Id => item[keyName];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper to get map key value from an item
|
|
16
|
+
*/
|
|
17
|
+
const getMapKeyValue = (item: ModelObj, mapKeyName: string): string => String(item[mapKeyName]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper to insert/update an item in a list by keyName
|
|
21
|
+
*/
|
|
22
|
+
const upsertInList = (
|
|
23
|
+
list: ModelObj[],
|
|
24
|
+
item: ModelObj,
|
|
25
|
+
keyName: string,
|
|
26
|
+
sort?: SortFn
|
|
27
|
+
): ModelObj[] => {
|
|
28
|
+
const itemKey = getKeyValue(item, keyName);
|
|
29
|
+
const existingIndex = list.findIndex((i) => getKeyValue(i, keyName) === itemKey);
|
|
30
|
+
|
|
31
|
+
let newList: ModelObj[];
|
|
32
|
+
if (existingIndex >= 0) {
|
|
33
|
+
// Update existing item
|
|
34
|
+
newList = [...list];
|
|
35
|
+
newList[existingIndex] = { ...item };
|
|
36
|
+
} else {
|
|
37
|
+
// Add new item
|
|
38
|
+
newList = [...list, { ...item }];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (sort) {
|
|
42
|
+
newList.sort(sort);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return newList;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Helper to delete items from a list by keyName
|
|
50
|
+
*/
|
|
51
|
+
const deleteFromList = (
|
|
52
|
+
list: ModelObj[],
|
|
53
|
+
keysToDelete: Set<Id>,
|
|
54
|
+
keyName: string
|
|
55
|
+
): ModelObj[] => {
|
|
56
|
+
return list.filter((item) => !keysToDelete.has(getKeyValue(item, keyName)));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Insert items into the mapList structure
|
|
61
|
+
* Groups items by mapKeyName, each group is a list managed by keyName
|
|
62
|
+
*/
|
|
63
|
+
export const mapListInsertReducer = (
|
|
64
|
+
state: MapListState,
|
|
65
|
+
action: MapListAction
|
|
66
|
+
): MapListState => {
|
|
67
|
+
const currentState = initializeState(state, {});
|
|
68
|
+
const items = normalizePayload(action.payload);
|
|
69
|
+
const newState = { ...currentState };
|
|
70
|
+
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
if (!item?.[action.keyName] || !item?.[action.mapKeyName]) continue;
|
|
73
|
+
|
|
74
|
+
const mapKey = getMapKeyValue(item, action.mapKeyName);
|
|
75
|
+
const existingList = newState[mapKey] || [];
|
|
76
|
+
newState[mapKey] = upsertInList(existingList, item, action.keyName, action.sort);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return newState;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Delete items from the mapList structure
|
|
84
|
+
* Removes items by keyName, removes empty map keys
|
|
85
|
+
*/
|
|
86
|
+
export const mapListDeleteReducer = (
|
|
87
|
+
state: MapListState,
|
|
88
|
+
action: MapListAction
|
|
89
|
+
): MapListState => {
|
|
90
|
+
const currentState = initializeState(state, {});
|
|
91
|
+
const items = normalizePayload(action.payload);
|
|
92
|
+
const keysToDelete = new Set<Id>(items.map((item) => getKeyValue(item, action.keyName)));
|
|
93
|
+
|
|
94
|
+
const newState: MapListState = {};
|
|
95
|
+
|
|
96
|
+
for (const [mapKey, list] of Object.entries(currentState)) {
|
|
97
|
+
const filteredList = deleteFromList(list, keysToDelete, action.keyName);
|
|
98
|
+
if (filteredList.length > 0) {
|
|
99
|
+
newState[mapKey] = filteredList;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return newState;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Replace entire mapList state with new items
|
|
108
|
+
*/
|
|
109
|
+
export const mapListReplaceReducer = (
|
|
110
|
+
state: MapListState,
|
|
111
|
+
action: MapListAction
|
|
112
|
+
): MapListState => {
|
|
113
|
+
const items = normalizePayload(action.payload);
|
|
114
|
+
const newState: MapListState = {};
|
|
115
|
+
|
|
116
|
+
for (const item of items) {
|
|
117
|
+
if (!item?.[action.keyName] || !item?.[action.mapKeyName]) continue;
|
|
118
|
+
|
|
119
|
+
const mapKey = getMapKeyValue(item, action.mapKeyName);
|
|
120
|
+
const existingList = newState[mapKey] || [];
|
|
121
|
+
newState[mapKey] = [...existingList, { ...item }];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Sort each list if sort function provided
|
|
125
|
+
if (action.sort) {
|
|
126
|
+
for (const mapKey of Object.keys(newState)) {
|
|
127
|
+
newState[mapKey].sort(action.sort);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return newState;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sort all lists within the mapList structure
|
|
136
|
+
*/
|
|
137
|
+
export const mapListSortReducer = (
|
|
138
|
+
state: MapListState,
|
|
139
|
+
action: MapListSortAction
|
|
140
|
+
): MapListState => {
|
|
141
|
+
const currentState = initializeState(state, {});
|
|
142
|
+
const sort = action.sort ?? keySort(action.keyName);
|
|
143
|
+
const newState: MapListState = {};
|
|
144
|
+
|
|
145
|
+
for (const [mapKey, list] of Object.entries(currentState)) {
|
|
146
|
+
const sortedList = [...list];
|
|
147
|
+
sortedList.sort(sort);
|
|
148
|
+
newState[mapKey] = sortedList;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return newState;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Shift (remove first item) from all lists in mapList
|
|
156
|
+
* Removes empty map keys after shift
|
|
157
|
+
*/
|
|
158
|
+
export const mapListShiftReducer = (state: MapListState): MapListState => {
|
|
159
|
+
const currentState = initializeState(state, {});
|
|
160
|
+
const newState: MapListState = {};
|
|
161
|
+
|
|
162
|
+
for (const [mapKey, list] of Object.entries(currentState)) {
|
|
163
|
+
const shiftedList = [...list];
|
|
164
|
+
shiftedList.shift();
|
|
165
|
+
if (shiftedList.length > 0) {
|
|
166
|
+
newState[mapKey] = shiftedList;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return newState;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Unshift (add to beginning) items into mapList
|
|
175
|
+
*/
|
|
176
|
+
export const mapListUnshiftReducer = (
|
|
177
|
+
state: MapListState,
|
|
178
|
+
action: MapListAction
|
|
179
|
+
): MapListState => {
|
|
180
|
+
const currentState = initializeState(state, {});
|
|
181
|
+
const items = normalizePayload(action.payload);
|
|
182
|
+
const newState = { ...currentState };
|
|
183
|
+
|
|
184
|
+
for (const item of items) {
|
|
185
|
+
if (!item?.[action.keyName] || !item?.[action.mapKeyName]) continue;
|
|
186
|
+
|
|
187
|
+
const mapKey = getMapKeyValue(item, action.mapKeyName);
|
|
188
|
+
const existingList = newState[mapKey] || [];
|
|
189
|
+
newState[mapKey] = [{ ...item }, ...existingList];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return newState;
|
|
193
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ModelObj, MapAction } from "./types";
|
|
2
|
+
import { normalizePayload, initializeState } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const mapInsertReducer = (state: ModelObj, action: MapAction): ModelObj => {
|
|
5
|
+
const currentState = initializeState(state, {});
|
|
6
|
+
const items = normalizePayload(action.payload);
|
|
7
|
+
const newState = { ...currentState };
|
|
8
|
+
|
|
9
|
+
for (const item of items) {
|
|
10
|
+
if (item?.[action.keyName]) {
|
|
11
|
+
newState[item[action.keyName]] = { ...item };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return newState;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const mapDeleteReducer = (state: ModelObj, action: MapAction): ModelObj => {
|
|
19
|
+
const currentState = initializeState(state, {});
|
|
20
|
+
const items = normalizePayload(action.payload);
|
|
21
|
+
const newState = { ...currentState };
|
|
22
|
+
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
delete newState[item[action.keyName]];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return newState;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const mapReplaceReducer = (state: ModelObj, action: MapAction): ModelObj => {
|
|
31
|
+
const items = normalizePayload(action.payload);
|
|
32
|
+
const newState: ModelObj = {};
|
|
33
|
+
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
if (item?.[action.keyName]) {
|
|
36
|
+
newState[item[action.keyName]] = { ...item };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return newState;
|
|
41
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const objInsertReducer = <T>(state: T, action: { payload: T }): T => {
|
|
2
|
+
return action.payload;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const objReplaceReducer = <T>(state: T, action: { payload: T }): T => {
|
|
6
|
+
return action.payload;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const objDeleteReducer = (): null => {
|
|
10
|
+
return null;
|
|
11
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type Id = number | string;
|
|
2
|
+
|
|
3
|
+
export interface ModelObj {
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type SortFn = (a: ModelObj, b: ModelObj) => number;
|
|
8
|
+
|
|
9
|
+
export interface ListAction {
|
|
10
|
+
payload: ModelObj | ModelObj[];
|
|
11
|
+
sort?: SortFn;
|
|
12
|
+
keyName: string;
|
|
13
|
+
mapKeyName?: string;
|
|
14
|
+
isMapList: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MapAction {
|
|
18
|
+
payload: ModelObj | ModelObj[];
|
|
19
|
+
sort?: SortFn;
|
|
20
|
+
keyName: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ListSortAction {
|
|
24
|
+
sort?: SortFn;
|
|
25
|
+
keyName: string;
|
|
26
|
+
mapKeyName?: string;
|
|
27
|
+
isMapList: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MapListAction {
|
|
31
|
+
payload: ModelObj | ModelObj[];
|
|
32
|
+
sort?: SortFn;
|
|
33
|
+
keyName: string;
|
|
34
|
+
mapKeyName: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MapListSortAction {
|
|
38
|
+
sort?: SortFn;
|
|
39
|
+
keyName: string;
|
|
40
|
+
mapKeyName: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ReducerOpts {
|
|
44
|
+
stateType?: "list" | "map" | "static" | "mapList";
|
|
45
|
+
sort?: SortFn;
|
|
46
|
+
keyName?: string;
|
|
47
|
+
mapKeyName?: string;
|
|
48
|
+
initialState?: any;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ReducerAction {
|
|
52
|
+
type: string;
|
|
53
|
+
payload?: any;
|
|
54
|
+
sort?: SortFn;
|
|
55
|
+
}
|