riducers 1.2.4 → 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/src/index.ts CHANGED
@@ -1,228 +1,244 @@
1
- type Id = number | string;
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
- interface ModelObj {
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
- const mapInsertReducer = (state: ModelObj, action: MapAction) => {
37
- if (!state) state = {};
38
- let resp: ModelObj[] = Array.isArray(action.payload) ? action.payload : [action.payload];
39
- let newState = { ...state };
40
- for (let i in resp) {
41
- if (resp[i]?.[action.keyName]) newState[resp[i][action.keyName]] = { ...resp[i] };
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
- const mapDeleteReducer = (state: ModelObj, action: MapAction) => {
47
- if (!state) state = {};
48
- let resp: ModelObj[] = Array.isArray(action.payload) ? action.payload : [action.payload];
49
- let newState = { ...state };
50
- for (let i in resp) {
51
- delete newState[resp[i][action.keyName]];
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 newState;
61
+ return { op, payload };
54
62
  };
55
63
 
56
- const mapReplaceReducer = (state: ModelObj, action: MapAction) => {
57
- let newState = {} as any;
58
- let resp: ModelObj[] = Array.isArray(action.payload) ? action.payload : [action.payload];
59
- for (let i in resp) {
60
- if (resp[i]?.[action.keyName]) newState[resp[i][action.keyName]] = { ...resp[i] };
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 newState;
94
+ return undefined;
63
95
  };
64
96
 
65
- const listInsertReducer = (state: ModelObj[], action: ListAction) => {
66
- if (!state) state = [];
67
- let resp: ModelObj[] = Array.isArray(action.payload) ? action.payload : [action.payload];
68
-
69
- let newState = [...state];
70
- let respMap: Id[] = resp.map((r: any) => r[action.keyName]);
71
- let stateMap: Id[] = newState.map((r: any) => r[action.keyName]);
72
- let toInsert: Id[] = respMap.filter((id: number | string) => stateMap.indexOf(id) < 0);
73
- let toReplace: Id[] = respMap.filter((id: number | string) => stateMap.indexOf(id) >= 0);
74
- for (let i in toReplace) {
75
- let stateIndex = stateMap.indexOf(toReplace[i]);
76
- let respIndex = respMap.indexOf(toReplace[i]);
77
- newState[stateIndex] = { ...resp[respIndex] };
78
- }
79
- if (toInsert.length > 0) {
80
- let inserts = toInsert.map((id: number | string) => resp[respMap.indexOf(id)]);
81
- newState = [...newState, ...inserts];
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
- if (action?.sort) newState.sort(action?.sort);
84
- return newState;
85
- };
86
-
87
- const listSortReducer = (state: ModelObj[], action: ListSortAction) => {
88
- if (!state) state = [];
89
- let newState = [...state];
90
- let sort = action?.sort ? action.sort : keySort(action.keyName);
91
- newState.sort(sort);
92
- return newState;
128
+ return undefined;
93
129
  };
94
130
 
95
- const listDeleteReducer = (state: ModelObj[], action: ListAction) => {
96
- if (!state || !Array.isArray(state)) state = [];
97
- let newState = [...state];
98
- let resp: ModelObj[] = Array.isArray(action.payload) ? action.payload : [action.payload];
99
-
100
- let stateMap: Id[] = newState.map((s: any) => s[action.keyName]);
101
- for (let i in resp) {
102
- let index = stateMap.indexOf(resp[i][action.keyName]);
103
- if (index >= 0) newState.splice(index, 1);
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;
104
151
  }
105
- return newState;
152
+ return undefined;
106
153
  };
107
154
 
108
- const listShiftReducer = (state: ModelObj[]) => {
109
- if (!state || !Array.isArray(state)) state = [];
110
- let newState = [...state];
111
- newState.shift();
112
- return newState;
113
- }
114
-
115
- const listReplaceReducer = (state: ModelObj[], action: ListAction) => {
116
- let resp: ModelObj[] = Array.isArray(action.payload) ? action.payload : [action.payload];
117
- resp = resp.map((r: any) => ({ ...r }));
118
- if (action?.sort) resp.sort(action?.sort);
119
- return resp;
120
- };
121
-
122
- const objInsertReducer = (state: any, action: { payload: any }) => {
123
- return action.payload;
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;
124
175
  };
125
176
 
126
- const objReplaceReducer = (state: any, action: { payload: any }) => {
127
- return action.payload;
128
- };
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";
129
185
 
130
- const objDeleteReducer = () => {
131
- return null;
132
- };
186
+ if (isMapList && !opts?.mapKeyName) {
187
+ throw new Error("mapKeyName is required for stateType: mapList");
188
+ }
133
189
 
134
- interface ReducerOpts {
135
- stateType?: "list" | "map" | "static" | "mapList";
136
- sort?: SortFn;
137
- keyName?: string;
138
- mapKeyName?: string;
139
- initialState?: any;
140
- }
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}/([^\/]+)$`);
141
193
 
142
- const OPS = ["insert", "replace", "delete", "clear", "sort"];
143
- function reducerBuilder(key: string, opts?: ReducerOpts) {
144
- let initialState = opts?.initialState;
145
- let stateType = opts?.stateType ?? "static";
146
- let isList = stateType === "list";
147
- let isMap = stateType === "map";
148
- let isMapList = stateType === "mapList";
149
-
150
- if (isMapList && !opts?.mapKeyName) throw new Error("mapKeyName is required for stateType: mapList");
151
-
152
- if (initialState === undefined) {
153
- if (isList) initialState = [];
154
- else if (isMap) initialState = {};
155
- else initialState = null;
156
- }
157
- return (state: any, action: { type: string; payload?: any; sort?: (a: ModelObj, b: ModelObj) => number }) => {
194
+ return (state: any, action: ReducerAction): any => {
195
+ // Initialize state
158
196
  if (state === undefined) {
159
- if (initialState !== undefined) return initialState;
160
- else if (isList) return [];
161
- else if (isMap) return {};
162
- else if (isMapList) return {};
163
- else return null;
197
+ return initialState !== undefined ? initialState : getDefaultInitialState(stateType);
164
198
  }
165
- let rx = new RegExp(`^${key}/([^\/]+)$`);
166
- let m = action.type.match(rx);
167
- if (!m) return state;
168
199
 
169
- let op = m[1];
200
+ // Match action type
201
+ const match = action.type.match(actionPattern);
202
+ if (!match) return state;
203
+
204
+ let op = match[1];
170
205
  let payload = action.payload;
171
- let keyName = opts?.keyName ?? "id";
172
- let mapKeyName = opts?.mapKeyName;
173
-
174
- if (isMap && !OPS.includes(op)) {
175
- if (payload && !Array.isArray(payload)) {
176
- payload[keyName] = op;
177
- op = "insert";
178
- }
179
- if (!payload) {
180
- payload = {};
181
- payload[keyName] = op;
182
- op = "delete";
183
- }
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;
184
215
  }
185
216
 
186
- let sort = action.sort ?? opts?.sort;
187
- if (op === "insert") {
188
-
189
- if ((isList || isMapList) && payload) return listInsertReducer(state, { payload, sort, keyName, mapKeyName, isMapList });
190
- else if (isMap && payload) return mapInsertReducer(state, { payload, keyName });
191
- return objInsertReducer(state, { payload });
192
-
193
- } else if (op === "replace") {
194
-
195
- if (isList || isMapList) return listReplaceReducer(state, { payload, sort, keyName, mapKeyName, isMapList });
196
- else if (isMap) return mapReplaceReducer(state, { payload, keyName });
197
- return objReplaceReducer(state, { payload });
198
-
199
- } else if (op === "delete") {
200
-
201
- if (isList || isMapList) return listDeleteReducer(state, { payload, keyName, mapKeyName, isMapList });
202
- else if (isMap) return mapDeleteReducer(state, { payload, keyName });
203
- return objDeleteReducer();
204
-
205
- } else if (op === "shift") {
206
- if (isList || isMapList) return listShiftReducer(state);
207
- else {
208
- console.error(`unshift is only valid for stateType: list or mapList`);
209
- }
210
-
211
- } else if (op === "clear") {
212
-
213
- return initialState;
214
-
215
- } else if (op === "sort") {
216
-
217
- if (isList || isMapList) return listSortReducer(state, { sort, keyName, mapKeyName, isMapList });
217
+ // Handle operations based on state type
218
+ let result: any;
218
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);
219
226
  } else {
227
+ result = handleStaticOperation(state, op, payload, initialState);
228
+ }
220
229
 
221
- console.error(`unknown op (${op}) in action type: ${action.type}. Op should be one of: insert, delete, replace, clear, or sort.`);
230
+ if (result !== undefined) {
231
+ return result;
232
+ }
222
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
+ );
223
239
  }
224
-
225
- return state || (isList ? [] : isMap ? {} : null);
240
+
241
+ return state || getDefaultInitialState(stateType);
226
242
  };
227
243
  }
228
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
+ };