@inglorious/store 9.5.1 → 9.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/store",
3
- "version": "9.5.1",
3
+ "version": "9.5.3",
4
4
  "description": "A state manager for real-time, collaborative apps, inspired by game development patterns and compatible with Redux.",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -62,6 +62,7 @@
62
62
  "@inglorious/utils": "3.7.3"
63
63
  },
64
64
  "devDependencies": {
65
+ "@reduxjs/toolkit": "^2.11.2",
65
66
  "prettier": "^3.6.2",
66
67
  "vite": "^7.1.3",
67
68
  "vitest": "^1.6.1",
@@ -5,11 +5,14 @@
5
5
  * Inglorious types, enabling gradual migration from RTK to Inglorious Store.
6
6
  */
7
7
 
8
+ import { extend } from "@inglorious/utils/data-structures/objects.js"
9
+
8
10
  import { handleAsync } from "../async"
9
11
 
10
12
  const SEP_LENGTH = 50
11
13
  const INDENT_SPACES = 2
12
14
  const SLICE_PLUS_ACTION = 2
15
+ const THUNK_STAGES = ["pending", "fulfilled", "rejected", "settled"]
13
16
 
14
17
  /**
15
18
  * Converts an RTK createAsyncThunk to Inglorious handleAsync handlers
@@ -50,7 +53,7 @@ const SLICE_PLUS_ACTION = 2
50
53
  *
51
54
  * // Use in type
52
55
  * const todoList = {
53
- * init(entity) { entity.todos = []; entity.status = 'idle' },
56
+ * create(entity) { entity.todos = []; entity.status = 'idle' },
54
57
  * ...todoHandlers
55
58
  * }
56
59
  */
@@ -74,8 +77,16 @@ export function convertAsyncThunk(name, payloadCreator, options = {}) {
74
77
 
75
78
  run: async (payload, api) => {
76
79
  // RTK payloadCreator signature: (arg, thunkAPI)
77
- // We need to adapt it to our simpler signature
78
- return await payloadCreator(payload, { dispatch: api.notify })
80
+ // Use dispatch for action compatibility, with notify fallback in tests/minimal APIs.
81
+ const dispatch = api.dispatch
82
+ ? api.dispatch.bind(api)
83
+ : (action) => {
84
+ if (typeof action === "object" && action !== null) {
85
+ api.notify(action.type, action.payload)
86
+ }
87
+ }
88
+
89
+ return await payloadCreator(payload, { dispatch })
79
90
  },
80
91
 
81
92
  success: onFulfilled
@@ -156,19 +167,88 @@ export function convertAsyncThunk(name, payloadCreator, options = {}) {
156
167
  * })
157
168
  */
158
169
  export function convertSlice(slice, options = {}) {
159
- const { asyncThunks = {}, extraHandlers = {} } = options
170
+ const { asyncThunks = {}, extraHandlers = {}, extraActions = [] } = options
160
171
 
161
172
  const type = {
162
- // Convert initialState to init handler
163
- init(entity) {
173
+ // Convert initialState to create handler.
174
+ // Preserve explicitly preloaded fields from entities (common in tests/SSR hydration).
175
+ create(entity) {
164
176
  const initialState = slice.getInitialState()
165
- Object.assign(entity, initialState)
177
+ Object.assign(entity, extend(initialState, entity))
166
178
  },
167
179
  }
168
180
 
169
181
  // Convert each reducer to an event handler
170
182
  for (const [actionName, reducer] of Object.entries(slice.caseReducers)) {
171
- type[actionName] = (entity, payload) => {
183
+ const thunkConfig = asyncThunks[actionName]
184
+ const isAsyncThunkReducer =
185
+ reducer &&
186
+ typeof reducer === "object" &&
187
+ THUNK_STAGES.some((stage) => typeof reducer[stage] === "function")
188
+
189
+ if (isAsyncThunkReducer) {
190
+ const pending = toLifecycleHandler(
191
+ slice.name,
192
+ actionName,
193
+ "pending",
194
+ reducer.pending,
195
+ )
196
+ const fulfilled = toLifecycleHandler(
197
+ slice.name,
198
+ actionName,
199
+ "fulfilled",
200
+ reducer.fulfilled,
201
+ )
202
+ const rejected = toLifecycleHandler(
203
+ slice.name,
204
+ actionName,
205
+ "rejected",
206
+ reducer.rejected,
207
+ )
208
+ const settled = toLifecycleHandler(
209
+ slice.name,
210
+ actionName,
211
+ "settled",
212
+ reducer.settled,
213
+ )
214
+
215
+ if (thunkConfig?.payloadCreator) {
216
+ const asyncHandlers = convertAsyncThunk(
217
+ actionName,
218
+ thunkConfig.payloadCreator,
219
+ {
220
+ onPending: thunkConfig.onPending || pending,
221
+ onFulfilled: thunkConfig.onFulfilled || fulfilled,
222
+ onRejected: thunkConfig.onRejected || rejected,
223
+ onSettled: thunkConfig.onSettled || settled,
224
+ scope: thunkConfig.scope,
225
+ },
226
+ )
227
+
228
+ Object.assign(type, asyncHandlers)
229
+ } else {
230
+ if (pending) {
231
+ type[`${actionName}Pending`] = pending
232
+ type[`${slice.name}/${actionName}/pending`] = pending
233
+ }
234
+ if (fulfilled) {
235
+ type[`${actionName}Fulfilled`] = fulfilled
236
+ type[`${slice.name}/${actionName}/fulfilled`] = fulfilled
237
+ }
238
+ if (rejected) {
239
+ type[`${actionName}Rejected`] = rejected
240
+ type[`${slice.name}/${actionName}/rejected`] = rejected
241
+ }
242
+ if (settled) {
243
+ type[`${actionName}Settled`] = settled
244
+ type[`${slice.name}/${actionName}/settled`] = settled
245
+ }
246
+ }
247
+
248
+ continue
249
+ }
250
+
251
+ const handler = (entity, payload) => {
172
252
  // Create a mock action object for the RTK reducer
173
253
  const action = { type: `${slice.name}/${actionName}`, payload }
174
254
 
@@ -177,23 +257,42 @@ export function convertSlice(slice, options = {}) {
177
257
  // So we can call the reducer directly on the entity
178
258
  reducer(entity, action)
179
259
  }
260
+
261
+ // Expose both Inglorious-style event names and RTK action-type aliases.
262
+ // This enables direct dispatch of RTK slice actions without extra bridge wiring.
263
+ type[actionName] = handler
264
+ type[`${slice.name}/${actionName}`] = handler
180
265
  }
181
266
 
182
- // Convert async thunks
267
+ // Convert explicitly configured async thunks that are not embedded in slice reducers.
183
268
  for (const [thunkName, thunkHandlers] of Object.entries(asyncThunks)) {
184
- const asyncHandlers = convertAsyncThunk(
185
- thunkName,
186
- thunkHandlers.payloadCreator || (async () => {}),
187
- {
188
- onPending: thunkHandlers.onPending,
189
- onFulfilled: thunkHandlers.onFulfilled,
190
- onRejected: thunkHandlers.onRejected,
191
- onSettled: thunkHandlers.onSettled,
192
- scope: thunkHandlers.scope,
193
- },
194
- )
269
+ if (thunkHandlers?.payloadCreator && !type[thunkName]) {
270
+ const asyncHandlers = convertAsyncThunk(
271
+ thunkName,
272
+ thunkHandlers.payloadCreator,
273
+ {
274
+ onPending: thunkHandlers.onPending,
275
+ onFulfilled: thunkHandlers.onFulfilled,
276
+ onRejected: thunkHandlers.onRejected,
277
+ onSettled: thunkHandlers.onSettled,
278
+ scope: thunkHandlers.scope,
279
+ },
280
+ )
281
+
282
+ Object.assign(type, asyncHandlers)
283
+ }
284
+ }
195
285
 
196
- Object.assign(type, asyncHandlers)
286
+ // Register additional RTK action types (e.g. createAction + extraReducers).
287
+ // RTK does not expose extraReducers through slice.caseReducers, so callers can
288
+ // provide the action creators/types explicitly and we route them through slice.reducer.
289
+ for (const action of extraActions) {
290
+ const actionType = typeof action === "string" ? action : action?.type
291
+ if (typeof actionType !== "string" || type[actionType]) continue
292
+
293
+ type[actionType] = (entity, payload) => {
294
+ applySliceReducer(entity, slice, actionType, payload)
295
+ }
197
296
  }
198
297
 
199
298
  // Add any extra handlers
@@ -324,3 +423,44 @@ export function createRTKCompatDispatch(api, entityId) {
324
423
  }
325
424
  }
326
425
  }
426
+
427
+ function createActionObject(sliceName, actionName, stage, payload) {
428
+ const action = {
429
+ type: `${sliceName}/${actionName}/${stage}`,
430
+ payload,
431
+ meta: { arg: payload },
432
+ }
433
+
434
+ if (stage === "pending") {
435
+ action.payload = undefined
436
+ }
437
+
438
+ if (stage === "rejected") {
439
+ action.error = payload
440
+ }
441
+
442
+ return action
443
+ }
444
+
445
+ function toLifecycleHandler(sliceName, actionName, stage, reducer) {
446
+ if (typeof reducer !== "function") return undefined
447
+
448
+ return (entity, payload) => {
449
+ reducer(entity, createActionObject(sliceName, actionName, stage, payload))
450
+ }
451
+ }
452
+
453
+ function applySliceReducer(entity, slice, actionType, payload) {
454
+ const action = { type: actionType, payload }
455
+ const nextState = slice.reducer(toPlainState(entity), action)
456
+
457
+ for (const key of Object.keys(entity)) {
458
+ delete entity[key]
459
+ }
460
+
461
+ Object.assign(entity, nextState)
462
+ }
463
+
464
+ function toPlainState(entity) {
465
+ return JSON.parse(JSON.stringify(entity))
466
+ }
package/src/store.js CHANGED
@@ -50,6 +50,11 @@ export function createStore({
50
50
  : baseStore
51
51
  const api = createApi(store, store.extras)
52
52
  store._api = api
53
+
54
+ if (updateMode === "auto" && incomingEvents.length) {
55
+ update()
56
+ }
57
+
53
58
  return store
54
59
 
55
60
  /**
@@ -147,6 +147,12 @@ export interface ConvertSliceOptions<TEntity extends BaseEntity = BaseEntity> {
147
147
  string,
148
148
  (entity: TEntity, payload?: any, api?: Api) => void
149
149
  >
150
+
151
+ /**
152
+ * Additional RTK actions to map (useful for createAction + extraReducers).
153
+ * Accepts action creators (with a `type` field) or raw action type strings.
154
+ */
155
+ extraActions?: Array<string | { type: string }>
150
156
  }
151
157
 
152
158
  /**
@@ -156,7 +162,7 @@ export interface InglorisousType<TEntity extends BaseEntity = BaseEntity> {
156
162
  /**
157
163
  * Initialization handler
158
164
  */
159
- init?: (entity: TEntity, payload?: any, api?: Api) => void
165
+ create?: (entity: TEntity, payload?: any, api?: Api) => void
160
166
 
161
167
  /**
162
168
  * Other event handlers