@inglorious/store 9.1.0 → 9.3.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 +269 -0
- package/package.json +5 -1
- package/src/async.js +187 -0
- package/src/migration/rtk.js +326 -0
- package/src/store.js +20 -1
- package/types/async.d.ts +146 -0
- package/types/index.d.ts +1 -0
- package/types/migration/rtk.d.ts +402 -0
- package/types/store.d.ts +13 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { Api, BaseEntity } from "../store"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RTK-compatible thunk API
|
|
5
|
+
* Minimal subset needed for payload creators
|
|
6
|
+
*/
|
|
7
|
+
export interface ThunkAPI {
|
|
8
|
+
dispatch: Api["notify"]
|
|
9
|
+
// Add other thunk API properties as needed
|
|
10
|
+
// getState, extra, requestId, signal, rejectWithValue, fulfillWithValue
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* RTK async thunk payload creator function
|
|
15
|
+
*/
|
|
16
|
+
export type PayloadCreator<TPayload = any, TResult = any> = (
|
|
17
|
+
arg: TPayload,
|
|
18
|
+
thunkAPI: ThunkAPI,
|
|
19
|
+
) => Promise<TResult> | TResult
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for converting an async thunk
|
|
23
|
+
*/
|
|
24
|
+
export interface ConvertAsyncThunkOptions<
|
|
25
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
26
|
+
TPayload = any,
|
|
27
|
+
TResult = any,
|
|
28
|
+
> {
|
|
29
|
+
/**
|
|
30
|
+
* Handler for pending state (RTK: addCase(thunk.pending))
|
|
31
|
+
* Maps to handleAsync 'start'
|
|
32
|
+
*/
|
|
33
|
+
onPending?: (entity: TEntity, payload: TPayload, api: Api) => void
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handler for fulfilled state (RTK: addCase(thunk.fulfilled))
|
|
37
|
+
* Maps to handleAsync 'success'
|
|
38
|
+
*/
|
|
39
|
+
onFulfilled?: (entity: TEntity, result: TResult, api: Api) => void
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Handler for rejected state (RTK: addCase(thunk.rejected))
|
|
43
|
+
* Maps to handleAsync 'error'
|
|
44
|
+
*/
|
|
45
|
+
onRejected?: (entity: TEntity, error: any, api: Api) => void
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handler called after operation completes (success or failure)
|
|
49
|
+
* Maps to handleAsync 'finally'
|
|
50
|
+
*/
|
|
51
|
+
onSettled?: (entity: TEntity, api: Api) => void
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Event scope for lifecycle handlers
|
|
55
|
+
* @default "entity"
|
|
56
|
+
*/
|
|
57
|
+
scope?: "entity" | "type" | "global"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* RTK slice reducer function
|
|
62
|
+
*/
|
|
63
|
+
export type SliceReducer<TState = any, TPayload = any> = (
|
|
64
|
+
state: TState,
|
|
65
|
+
action: { type: string; payload: TPayload },
|
|
66
|
+
) => void
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* RTK slice structure (minimal subset)
|
|
70
|
+
*/
|
|
71
|
+
export interface RTKSlice<TState = any> {
|
|
72
|
+
/**
|
|
73
|
+
* Slice name (used in action types)
|
|
74
|
+
*/
|
|
75
|
+
name: string
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Function that returns initial state
|
|
79
|
+
*/
|
|
80
|
+
getInitialState: () => TState
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Map of case reducers
|
|
84
|
+
*/
|
|
85
|
+
caseReducers: Record<string, SliceReducer<TState, any>>
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Optional: The combined reducer function
|
|
89
|
+
*/
|
|
90
|
+
reducer?: (state: TState, action: any) => TState
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Async thunk configuration for slice conversion
|
|
95
|
+
*/
|
|
96
|
+
export interface AsyncThunkConfig<
|
|
97
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
98
|
+
TPayload = any,
|
|
99
|
+
TResult = any,
|
|
100
|
+
> {
|
|
101
|
+
/**
|
|
102
|
+
* The async operation to perform
|
|
103
|
+
*/
|
|
104
|
+
payloadCreator?: PayloadCreator<TPayload, TResult>
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handler for pending state
|
|
108
|
+
*/
|
|
109
|
+
onPending?: (entity: TEntity, payload: TPayload, api: Api) => void
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Handler for fulfilled state
|
|
113
|
+
*/
|
|
114
|
+
onFulfilled?: (entity: TEntity, result: TResult, api: Api) => void
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handler for rejected state
|
|
118
|
+
*/
|
|
119
|
+
onRejected?: (entity: TEntity, error: any, api: Api) => void
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handler called after completion
|
|
123
|
+
*/
|
|
124
|
+
onSettled?: (entity: TEntity, api: Api) => void
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Event scope
|
|
128
|
+
*/
|
|
129
|
+
scope?: "entity" | "type" | "global"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Options for converting a slice
|
|
134
|
+
*/
|
|
135
|
+
export interface ConvertSliceOptions<TEntity extends BaseEntity = BaseEntity> {
|
|
136
|
+
/**
|
|
137
|
+
* Map of async thunks to convert
|
|
138
|
+
* Key is the thunk name, value is the thunk configuration
|
|
139
|
+
*/
|
|
140
|
+
asyncThunks?: Record<string, AsyncThunkConfig<TEntity, any, any>>
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Additional event handlers not from the slice
|
|
144
|
+
* These will be merged into the resulting type
|
|
145
|
+
*/
|
|
146
|
+
extraHandlers?: Record<
|
|
147
|
+
string,
|
|
148
|
+
(entity: TEntity, payload?: any, api?: Api) => void
|
|
149
|
+
>
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Inglorious type definition (minimal structure)
|
|
154
|
+
*/
|
|
155
|
+
export interface InglorisousType<TEntity extends BaseEntity = BaseEntity> {
|
|
156
|
+
/**
|
|
157
|
+
* Initialization handler
|
|
158
|
+
*/
|
|
159
|
+
init?: (entity: TEntity, payload?: any, api?: Api) => void
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Other event handlers
|
|
163
|
+
*/
|
|
164
|
+
[key: string]:
|
|
165
|
+
| ((entity: TEntity, payload?: any, api?: Api) => void | Promise<void>)
|
|
166
|
+
| undefined
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Converts an RTK createAsyncThunk to Inglorious handleAsync handlers
|
|
171
|
+
*
|
|
172
|
+
* This function adapts RTK's pending/fulfilled/rejected lifecycle to
|
|
173
|
+
* Inglorious's start/run/success/error/finally lifecycle.
|
|
174
|
+
*
|
|
175
|
+
* @template TEntity - The entity type
|
|
176
|
+
* @template TPayload - The payload type
|
|
177
|
+
* @template TResult - The result type returned by the async operation
|
|
178
|
+
*
|
|
179
|
+
* @param name - The thunk name (e.g., 'fetchTodos')
|
|
180
|
+
* @param payloadCreator - The async function that performs the operation
|
|
181
|
+
* @param options - Lifecycle handlers and configuration
|
|
182
|
+
* @returns Inglorious handleAsync event handlers
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* interface User {
|
|
187
|
+
* id: number
|
|
188
|
+
* name: string
|
|
189
|
+
* }
|
|
190
|
+
*
|
|
191
|
+
* interface UserEntity extends BaseEntity {
|
|
192
|
+
* type: 'user'
|
|
193
|
+
* currentUser: User | null
|
|
194
|
+
* loading: boolean
|
|
195
|
+
* error: string | null
|
|
196
|
+
* }
|
|
197
|
+
*
|
|
198
|
+
* const userHandlers = convertAsyncThunk<UserEntity, number, User>(
|
|
199
|
+
* 'fetchUser',
|
|
200
|
+
* async (userId, thunkAPI) => {
|
|
201
|
+
* const response = await fetch(`/api/users/${userId}`)
|
|
202
|
+
* return response.json()
|
|
203
|
+
* },
|
|
204
|
+
* {
|
|
205
|
+
* onPending: (entity) => {
|
|
206
|
+
* entity.loading = true
|
|
207
|
+
* entity.error = null
|
|
208
|
+
* },
|
|
209
|
+
* onFulfilled: (entity, user) => {
|
|
210
|
+
* entity.loading = false
|
|
211
|
+
* entity.currentUser = user
|
|
212
|
+
* },
|
|
213
|
+
* onRejected: (entity, error) => {
|
|
214
|
+
* entity.loading = false
|
|
215
|
+
* entity.error = error.message
|
|
216
|
+
* }
|
|
217
|
+
* }
|
|
218
|
+
* )
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
export function convertAsyncThunk<
|
|
222
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
223
|
+
TPayload = any,
|
|
224
|
+
TResult = any,
|
|
225
|
+
>(
|
|
226
|
+
name: string,
|
|
227
|
+
payloadCreator: PayloadCreator<TPayload, TResult>,
|
|
228
|
+
options?: ConvertAsyncThunkOptions<TEntity, TPayload, TResult>,
|
|
229
|
+
): InglorisousType<TEntity>
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Converts an RTK slice to an Inglorious type
|
|
233
|
+
*
|
|
234
|
+
* This function converts RTK reducers to Inglorious event handlers while
|
|
235
|
+
* preserving the same mutation-based syntax (both use Immer/Mutative).
|
|
236
|
+
* The resulting type can be used directly in an Inglorious store.
|
|
237
|
+
*
|
|
238
|
+
* @template TEntity - The entity type (should match slice state + BaseEntity fields)
|
|
239
|
+
* @template TState - The slice state type
|
|
240
|
+
*
|
|
241
|
+
* @param slice - RTK slice created with createSlice
|
|
242
|
+
* @param options - Async thunks and additional handlers
|
|
243
|
+
* @returns Inglorious type definition
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* interface Todo {
|
|
248
|
+
* id: number
|
|
249
|
+
* text: string
|
|
250
|
+
* completed: boolean
|
|
251
|
+
* }
|
|
252
|
+
*
|
|
253
|
+
* interface TodoState {
|
|
254
|
+
* items: Todo[]
|
|
255
|
+
* filter: 'all' | 'active' | 'completed'
|
|
256
|
+
* status: 'idle' | 'loading' | 'success' | 'error'
|
|
257
|
+
* }
|
|
258
|
+
*
|
|
259
|
+
* interface TodoListEntity extends BaseEntity, TodoState {
|
|
260
|
+
* type: 'todoList'
|
|
261
|
+
* }
|
|
262
|
+
*
|
|
263
|
+
* // RTK slice
|
|
264
|
+
* const todosSlice = createSlice({
|
|
265
|
+
* name: 'todos',
|
|
266
|
+
* initialState: { items: [], filter: 'all', status: 'idle' } as TodoState,
|
|
267
|
+
* reducers: {
|
|
268
|
+
* addTodo: (state, action: PayloadAction<string>) => {
|
|
269
|
+
* state.items.push({
|
|
270
|
+
* id: Date.now(),
|
|
271
|
+
* text: action.payload,
|
|
272
|
+
* completed: false
|
|
273
|
+
* })
|
|
274
|
+
* },
|
|
275
|
+
* toggleTodo: (state, action: PayloadAction<number>) => {
|
|
276
|
+
* const todo = state.items.find(t => t.id === action.payload)
|
|
277
|
+
* if (todo) todo.completed = !todo.completed
|
|
278
|
+
* }
|
|
279
|
+
* }
|
|
280
|
+
* })
|
|
281
|
+
*
|
|
282
|
+
* // Convert to Inglorious
|
|
283
|
+
* const todoList = convertSlice<TodoListEntity, TodoState>(todosSlice, {
|
|
284
|
+
* asyncThunks: {
|
|
285
|
+
* fetchTodos: {
|
|
286
|
+
* payloadCreator: async () => {
|
|
287
|
+
* const response = await fetch('/api/todos')
|
|
288
|
+
* return response.json()
|
|
289
|
+
* },
|
|
290
|
+
* onPending: (entity) => {
|
|
291
|
+
* entity.status = 'loading'
|
|
292
|
+
* },
|
|
293
|
+
* onFulfilled: (entity, todos: Todo[]) => {
|
|
294
|
+
* entity.status = 'success'
|
|
295
|
+
* entity.items = todos
|
|
296
|
+
* },
|
|
297
|
+
* onRejected: (entity, error) => {
|
|
298
|
+
* entity.status = 'error'
|
|
299
|
+
* }
|
|
300
|
+
* }
|
|
301
|
+
* }
|
|
302
|
+
* })
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
export function convertSlice<
|
|
306
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
307
|
+
TState = any,
|
|
308
|
+
>(
|
|
309
|
+
slice: RTKSlice<TState>,
|
|
310
|
+
options?: ConvertSliceOptions<TEntity>,
|
|
311
|
+
): InglorisousType<TEntity>
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Creates a migration guide for a slice
|
|
315
|
+
*
|
|
316
|
+
* Analyzes an RTK slice and generates a readable migration guide showing
|
|
317
|
+
* the equivalent Inglorious code. Useful for documentation and planning.
|
|
318
|
+
*
|
|
319
|
+
* @param slice - RTK slice
|
|
320
|
+
* @returns Migration guide as formatted text
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* const guide = createMigrationGuide(todosSlice)
|
|
325
|
+
* console.log(guide)
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
export function createMigrationGuide(slice: RTKSlice): string
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Creates a compatibility layer for existing RTK code
|
|
332
|
+
*
|
|
333
|
+
* This allows RTK-style dispatch calls to work with Inglorious Store
|
|
334
|
+
* during the migration period. The returned dispatch function translates
|
|
335
|
+
* RTK action objects to Inglorious notify calls.
|
|
336
|
+
*
|
|
337
|
+
* @param api - Inglorious API
|
|
338
|
+
* @param entityId - The entity ID to target
|
|
339
|
+
* @returns A dispatch function compatible with RTK actions
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```typescript
|
|
343
|
+
* const dispatch = createRTKCompatDispatch(api, 'todos')
|
|
344
|
+
*
|
|
345
|
+
* // RTK-style action still works
|
|
346
|
+
* dispatch({
|
|
347
|
+
* type: 'todos/addTodo',
|
|
348
|
+
* payload: 'Buy milk'
|
|
349
|
+
* })
|
|
350
|
+
*
|
|
351
|
+
* // Translates to: api.notify('#todos:addTodo', 'Buy milk')
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
export function createRTKCompatDispatch(
|
|
355
|
+
api: Api,
|
|
356
|
+
entityId: string,
|
|
357
|
+
): (action: { type: string; payload?: any } | Function) => void
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* RTK Action type for type safety
|
|
361
|
+
*/
|
|
362
|
+
export interface RTKAction<TPayload = any> {
|
|
363
|
+
type: string
|
|
364
|
+
payload?: TPayload
|
|
365
|
+
error?: any
|
|
366
|
+
meta?: any
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Helper type to extract state from RTK slice
|
|
371
|
+
*/
|
|
372
|
+
export type SliceState<T extends RTKSlice> = ReturnType<T["getInitialState"]>
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Helper type to create entity from slice state
|
|
376
|
+
*/
|
|
377
|
+
export type EntityFromSlice<
|
|
378
|
+
T extends RTKSlice,
|
|
379
|
+
TType extends string = string,
|
|
380
|
+
> = SliceState<T> & BaseEntity & { type: TType }
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Type-safe wrapper for convertSlice with inferred types
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```typescript
|
|
387
|
+
* const todosSlice = createSlice({ ... })
|
|
388
|
+
*
|
|
389
|
+
* // Type is automatically inferred
|
|
390
|
+
* const todoList = convertSliceTyped(todosSlice, 'todoList', {
|
|
391
|
+
* asyncThunks: { ... }
|
|
392
|
+
* })
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
395
|
+
export function convertSliceTyped<
|
|
396
|
+
TSlice extends RTKSlice,
|
|
397
|
+
TType extends string,
|
|
398
|
+
>(
|
|
399
|
+
slice: TSlice,
|
|
400
|
+
typeName: TType,
|
|
401
|
+
options?: ConvertSliceOptions<EntityFromSlice<TSlice, TType>>,
|
|
402
|
+
): InglorisousType<EntityFromSlice<TSlice, TType>>
|
package/types/store.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ export interface StoreConfig<
|
|
|
53
53
|
entities?: TState
|
|
54
54
|
systems?: System<TState>[]
|
|
55
55
|
middlewares?: Middleware<TEntity, TState>[]
|
|
56
|
+
autoCreateEntities?: boolean
|
|
56
57
|
mode?: "eager" | "batched"
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -145,3 +146,15 @@ export function createApi<
|
|
|
145
146
|
store: Store<TEntity, TState>,
|
|
146
147
|
extras?: Record<string, any>,
|
|
147
148
|
): Api<TEntity, TState>
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Helper to create a set of handlers for an async operation
|
|
152
|
+
*/
|
|
153
|
+
export function handleAsync<
|
|
154
|
+
TEntity extends BaseEntity = BaseEntity,
|
|
155
|
+
TPayload = any,
|
|
156
|
+
TResult = any,
|
|
157
|
+
>(
|
|
158
|
+
type: string,
|
|
159
|
+
handlers: AsyncHandlers<TEntity, TPayload, TResult>,
|
|
160
|
+
): EntityType<TEntity>
|