@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.
@@ -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>