storion 0.2.3 → 0.4.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.
Files changed (91) hide show
  1. package/README.md +771 -561
  2. package/dist/async/async.d.ts +87 -0
  3. package/dist/async/async.d.ts.map +1 -0
  4. package/dist/async/index.d.ts +13 -0
  5. package/dist/async/index.d.ts.map +1 -0
  6. package/dist/async/index.js +451 -0
  7. package/dist/async/types.d.ts +275 -0
  8. package/dist/async/types.d.ts.map +1 -0
  9. package/dist/collection.d.ts +42 -0
  10. package/dist/collection.d.ts.map +1 -0
  11. package/dist/core/container.d.ts +33 -2
  12. package/dist/core/container.d.ts.map +1 -1
  13. package/dist/core/createResolver.d.ts +47 -0
  14. package/dist/core/createResolver.d.ts.map +1 -0
  15. package/dist/core/effect.d.ts.map +1 -1
  16. package/dist/core/equality.d.ts +23 -3
  17. package/dist/core/equality.d.ts.map +1 -1
  18. package/dist/core/fnWrapper.d.ts +54 -0
  19. package/dist/core/fnWrapper.d.ts.map +1 -0
  20. package/dist/core/middleware.d.ts +10 -10
  21. package/dist/core/middleware.d.ts.map +1 -1
  22. package/dist/core/pick.d.ts +6 -6
  23. package/dist/core/pick.d.ts.map +1 -1
  24. package/dist/core/store.d.ts +8 -8
  25. package/dist/core/store.d.ts.map +1 -1
  26. package/dist/core/storeContext.d.ts +63 -0
  27. package/dist/core/storeContext.d.ts.map +1 -0
  28. package/dist/devtools/controller.d.ts +4 -0
  29. package/dist/devtools/controller.d.ts.map +1 -0
  30. package/dist/devtools/index.d.ts +16 -0
  31. package/dist/devtools/index.d.ts.map +1 -0
  32. package/dist/devtools/index.js +239 -0
  33. package/dist/devtools/middleware.d.ts +22 -0
  34. package/dist/devtools/middleware.d.ts.map +1 -0
  35. package/dist/devtools/types.d.ts +116 -0
  36. package/dist/devtools/types.d.ts.map +1 -0
  37. package/dist/devtools-panel/DevtoolsPanel.d.ts +17 -0
  38. package/dist/devtools-panel/DevtoolsPanel.d.ts.map +1 -0
  39. package/dist/devtools-panel/components/CompareModal.d.ts +10 -0
  40. package/dist/devtools-panel/components/CompareModal.d.ts.map +1 -0
  41. package/dist/devtools-panel/components/EventEntry.d.ts +14 -0
  42. package/dist/devtools-panel/components/EventEntry.d.ts.map +1 -0
  43. package/dist/devtools-panel/components/EventFilterBar.d.ts +10 -0
  44. package/dist/devtools-panel/components/EventFilterBar.d.ts.map +1 -0
  45. package/dist/devtools-panel/components/EventsTab.d.ts +15 -0
  46. package/dist/devtools-panel/components/EventsTab.d.ts.map +1 -0
  47. package/dist/devtools-panel/components/ResizeHandle.d.ts +8 -0
  48. package/dist/devtools-panel/components/ResizeHandle.d.ts.map +1 -0
  49. package/dist/devtools-panel/components/StoreEntry.d.ts +13 -0
  50. package/dist/devtools-panel/components/StoreEntry.d.ts.map +1 -0
  51. package/dist/devtools-panel/components/StoresTab.d.ts +12 -0
  52. package/dist/devtools-panel/components/StoresTab.d.ts.map +1 -0
  53. package/dist/devtools-panel/components/TabLayout.d.ts +48 -0
  54. package/dist/devtools-panel/components/TabLayout.d.ts.map +1 -0
  55. package/dist/devtools-panel/components/icons.d.ts +27 -0
  56. package/dist/devtools-panel/components/icons.d.ts.map +1 -0
  57. package/dist/devtools-panel/components/index.d.ts +15 -0
  58. package/dist/devtools-panel/components/index.d.ts.map +1 -0
  59. package/dist/devtools-panel/hooks.d.ts +23 -0
  60. package/dist/devtools-panel/hooks.d.ts.map +1 -0
  61. package/dist/devtools-panel/index.d.ts +25 -0
  62. package/dist/devtools-panel/index.d.ts.map +1 -0
  63. package/dist/devtools-panel/index.js +3326 -0
  64. package/dist/devtools-panel/mount.d.ts +41 -0
  65. package/dist/devtools-panel/mount.d.ts.map +1 -0
  66. package/dist/devtools-panel/styles.d.ts +50 -0
  67. package/dist/devtools-panel/styles.d.ts.map +1 -0
  68. package/dist/devtools-panel/types.d.ts +15 -0
  69. package/dist/devtools-panel/types.d.ts.map +1 -0
  70. package/dist/devtools-panel/utils.d.ts +21 -0
  71. package/dist/devtools-panel/utils.d.ts.map +1 -0
  72. package/dist/index.d.ts +6 -1
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/is.d.ts +69 -0
  75. package/dist/is.d.ts.map +1 -0
  76. package/dist/react/create.d.ts +1 -1
  77. package/dist/react/index.d.ts +1 -0
  78. package/dist/react/index.d.ts.map +1 -1
  79. package/dist/react/index.js +210 -34
  80. package/dist/react/useLocalStore.d.ts.map +1 -1
  81. package/dist/react/useStore.d.ts +2 -2
  82. package/dist/react/useStore.d.ts.map +1 -1
  83. package/dist/react/withStore.d.ts +140 -0
  84. package/dist/react/withStore.d.ts.map +1 -0
  85. package/dist/{index-rLf6DusB.js → store-Yv-9gPVf.js} +543 -742
  86. package/dist/storion.js +809 -9
  87. package/dist/trigger.d.ts +40 -0
  88. package/dist/trigger.d.ts.map +1 -0
  89. package/dist/types.d.ts +538 -71
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +13 -1
package/dist/types.d.ts CHANGED
@@ -1,3 +1,15 @@
1
+ export declare const STORION_TYPE: unique symbol;
2
+ /**
3
+ * Kind identifiers for Storion objects.
4
+ * Used with STORION_SYMBOL for runtime type discrimination.
5
+ */
6
+ export type StorionKind = "store.spec" | "store.action" | "container" | "store" | "focus" | "store.context" | "selector.context" | "async.meta";
7
+ /**
8
+ * Base interface for all Storion objects with runtime type discrimination.
9
+ */
10
+ export interface StorionObject<K extends StorionKind = StorionKind> {
11
+ readonly [STORION_TYPE]: K;
12
+ }
1
13
  /**
2
14
  * Storion - Type-safe reactive state management
3
15
  *
@@ -6,16 +18,14 @@
6
18
  /**
7
19
  * Base constraint for state objects.
8
20
  */
9
- export interface StateBase {
10
- [key: string]: unknown;
11
- }
21
+ export type StateBase = object | Record<string, unknown>;
12
22
  /**
13
23
  * Base constraint for actions.
14
24
  */
15
25
  export interface ActionsBase {
16
26
  [key: string]: (...args: any[]) => any;
17
27
  }
18
- export type EqualityShorthand = "strict" | "shallow" | "deep";
28
+ export type EqualityShorthand = "strict" | "shallow" | "shallow2" | "shallow3" | "deep";
19
29
  /**
20
30
  * Equality strategies for change detection.
21
31
  */
@@ -26,6 +36,120 @@ export type Equality<T = unknown> = EqualityShorthand | ((a: T, b: T) => boolean
26
36
  export type EqualityMap<T> = {
27
37
  [K in keyof T]?: Equality<T[K]>;
28
38
  };
39
+ /**
40
+ * Extracts nested object paths as dot-notation strings.
41
+ * Stops at arrays (no index support).
42
+ *
43
+ * @example
44
+ * type State = { profile: { name: string; address: { city: string } } };
45
+ * type P = StatePath<State>; // "profile" | "profile.name" | "profile.address" | "profile.address.city"
46
+ */
47
+ export type StatePath<T, Prefix extends string = ""> = T extends object ? T extends unknown[] ? never : {
48
+ [K in keyof T & string]: NonNullable<T[K]> extends object ? NonNullable<T[K]> extends unknown[] ? `${Prefix}${K}` : `${Prefix}${K}` | StatePath<NonNullable<T[K]>, `${Prefix}${K}.`> : `${Prefix}${K}`;
49
+ }[keyof T & string] : never;
50
+ /**
51
+ * Gets the type at a nested path.
52
+ *
53
+ * @example
54
+ * type State = { profile: { address: { city: string } } };
55
+ * type City = PathValue<State, "profile.address.city">; // string
56
+ */
57
+ export type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<NonNullable<T[K]>, Rest> : never : P extends keyof T ? T[P] : never;
58
+ /**
59
+ * Non-nullable type utility.
60
+ */
61
+ export type NonNullish<TValue, TFlag extends true | false = true> = TFlag extends true ? Exclude<TValue, undefined | null> : TValue;
62
+ /**
63
+ * Focus change event payload.
64
+ */
65
+ export interface FocusChangeEvent<T> {
66
+ next: T;
67
+ prev: T;
68
+ }
69
+ /**
70
+ * Focus options for configuring getter/setter behavior.
71
+ *
72
+ * @example
73
+ * // With fallback for nullable values
74
+ * const focus = ctx.focus("profile", {
75
+ * fallback: () => ({ name: "Guest" })
76
+ * });
77
+ *
78
+ * @example
79
+ * // With custom equality for change detection
80
+ * const focus = ctx.focus("items", {
81
+ * equality: "shallow"
82
+ * });
83
+ */
84
+ export interface FocusOptions<T> {
85
+ /**
86
+ * Fallback factory for when the focused value is nullish.
87
+ * Applied to both getter (returns fallback) and setter (reducer receives fallback).
88
+ */
89
+ fallback?: () => NonNullish<T>;
90
+ /**
91
+ * Equality strategy for change detection in on() listener.
92
+ * Defaults to strict equality (===).
93
+ */
94
+ equality?: Equality<T>;
95
+ }
96
+ /**
97
+ * Focus tuple: [getter, setter] with an on() method for subscribing to changes.
98
+ *
99
+ * @example
100
+ * const [getName, setName] = ctx.focus("profile.name");
101
+ *
102
+ * // Get current value
103
+ * const name = getName();
104
+ *
105
+ * // Set value directly
106
+ * setName("Jane");
107
+ *
108
+ * // Set value with reducer (returns new value)
109
+ * setName(prev => prev.toUpperCase());
110
+ *
111
+ * // Set value with produce (immer-style, mutate draft)
112
+ * setName(draft => { draft.nested = "value"; });
113
+ *
114
+ * // Listen to changes
115
+ * const unsubscribe = ctx.focus("profile.name").on(({ next, prev }) => {
116
+ * console.log(`Name changed from ${prev} to ${next}`);
117
+ * });
118
+ */
119
+ export type Focus<TValue> = [
120
+ /** Get the current value at the focused path */
121
+ getter: () => TValue,
122
+ /**
123
+ * Set the value at the focused path.
124
+ * - Direct value: `set(newValue)`
125
+ * - Reducer: `set(prev => newValue)` - returns new value
126
+ * - Produce: `set(draft => { draft.x = y })` - immer-style mutation, no return
127
+ */
128
+ setter: (valueOrReducerOrProduce: TValue | ((prev: TValue) => TValue | void)) => void
129
+ ] & {
130
+ readonly [STORION_TYPE]: "focus";
131
+ /**
132
+ * Subscribe to changes at the focused path.
133
+ * Uses the configured equality to determine if value has changed.
134
+ *
135
+ * @param listener - Called with { next, prev } when value changes
136
+ * @returns Unsubscribe function
137
+ */
138
+ on(listener: (event: FocusChangeEvent<TValue>) => void): VoidFunction;
139
+ /**
140
+ * Create a new Focus relative to the current path.
141
+ *
142
+ * @param relativePath - Dot-notation path relative to current focus
143
+ * @param options - Focus options for the new focus
144
+ * @returns A new Focus at the combined path
145
+ *
146
+ * @example
147
+ * const userFocus = focus("user");
148
+ * const addressFocus = userFocus.to("address");
149
+ * const cityFocus = userFocus.to("address.city");
150
+ */
151
+ to<TChild>(relativePath: string, options?: FocusOptions<TChild>): Focus<TChild>;
152
+ };
29
153
  /**
30
154
  * Controls when a store instance is automatically disposed.
31
155
  *
@@ -96,14 +220,32 @@ export type ReactiveActions<TActions extends ActionsBase> = {
96
220
  [K in keyof TActions]: ReactiveAction<TActions, K>;
97
221
  };
98
222
  /**
99
- * Store specification (definition).
223
+ * Store specification (definition) that is also a factory function.
100
224
  * A spec describes HOW to create a store instance - it holds no state.
225
+ *
226
+ * Can be called directly as a factory: `spec(resolver) => instance`
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * const counterSpec = store({ count: state(0) });
231
+ *
232
+ * // Use with container (recommended)
233
+ * const instance = container.get(counterSpec);
234
+ *
235
+ * // Or call directly as factory
236
+ * const instance = counterSpec(resolver);
237
+ * ```
101
238
  */
102
- export interface StoreSpec<TState extends StateBase = StateBase, TActions extends ActionsBase = ActionsBase> {
103
- /** Store name for debugging */
104
- readonly name: string;
239
+ export interface StoreSpec<TState extends StateBase = StateBase, TActions extends ActionsBase = ActionsBase> extends StorionObject<"store.spec"> {
240
+ /** Store display name for debugging (renamed from 'name' since that's a reserved function property) */
241
+ readonly displayName: string;
105
242
  /** Store options (state, setup, lifetime, etc.) */
106
243
  readonly options: StoreOptions<TState, TActions>;
244
+ /**
245
+ * Factory function - creates a new store instance.
246
+ * Called by container/resolver internally.
247
+ */
248
+ (resolver: Resolver): StoreInstance<TState, TActions>;
107
249
  }
108
250
  /**
109
251
  * A reusable mixin for store setup.
@@ -129,10 +271,61 @@ export type StoreMixin<TState extends StateBase, TResult, TArgs extends unknown[
129
271
  * }, 0);
130
272
  */
131
273
  export type SelectorMixin<TResult, TArgs extends unknown[] = []> = (context: SelectorContext, ...args: TArgs) => TResult;
274
+ /**
275
+ * Tuple returned by get() with both array destructuring and named properties.
276
+ *
277
+ * @example
278
+ * // Array destructuring
279
+ * const [state, actions] = get(counterSpec);
280
+ *
281
+ * // Named properties
282
+ * const tuple = get(counterSpec);
283
+ * tuple.state.count;
284
+ * tuple.actions.increment();
285
+ */
286
+ export type StoreTuple<S extends StateBase, A extends ActionsBase> = readonly [
287
+ Readonly<S>,
288
+ A
289
+ ] & {
290
+ readonly state: Readonly<S>;
291
+ readonly actions: A;
292
+ };
293
+ /**
294
+ * Update function with action creator.
295
+ *
296
+ * Can be called directly to update state, or use `.action()` to create
297
+ * action functions that wrap updates.
298
+ */
299
+ export interface StoreUpdate<TState extends StateBase> {
300
+ /**
301
+ * Update state using Immer-style updater function.
302
+ */
303
+ (updater: (draft: TState) => void): void;
304
+ /**
305
+ * Update state with partial object (shallow merge).
306
+ */
307
+ (partial: Partial<TState>): void;
308
+ /**
309
+ * Create an action function that wraps an updater.
310
+ * Throws error if the updater returns a PromiseLike (async not supported).
311
+ *
312
+ * @example
313
+ * // No arguments
314
+ * increment: update.action(draft => {
315
+ * draft.count++;
316
+ * }),
317
+ *
318
+ * // With arguments
319
+ * addItem: update.action((draft, name: string, price: number) => {
320
+ * draft.items.push({ name, price });
321
+ * }),
322
+ */
323
+ action<TArgs extends unknown[]>(updater: (draft: TState, ...args: TArgs) => void): (...args: TArgs) => void;
324
+ }
132
325
  /**
133
326
  * Context provided to the setup() function.
134
327
  */
135
- export interface StoreContext<TState extends StateBase> {
328
+ export interface StoreContext<TState extends StateBase = StateBase> extends StorionObject<"store.context"> {
136
329
  /**
137
330
  * Mutable reactive state proxy.
138
331
  * Writes trigger subscriber notifications.
@@ -141,30 +334,93 @@ export interface StoreContext<TState extends StateBase> {
141
334
  readonly state: TState;
142
335
  /**
143
336
  * Get another store's state and actions.
144
- * Returns [readonlyState, actions] tuple.
337
+ * Returns tuple with both array destructuring and named properties.
145
338
  * Creates dependency - store is created if not exists.
146
339
  *
147
- * Note: Returns limited tuple, not full StoreInstance.
148
- * You cannot access id, subscribe(), or dispose().
340
+ * @example
341
+ * // Array destructuring
342
+ * const [state, actions] = get(counterSpec);
343
+ *
344
+ * // Named properties
345
+ * const tuple = get(counterSpec);
346
+ * tuple.state.count;
149
347
  */
150
- resolve<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>): readonly [Readonly<S>, A];
348
+ get<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>): StoreTuple<S, A>;
151
349
  /**
152
- * Update state using Immer-style updater function.
350
+ * Get a service or factory instance.
351
+ * Creates and caches the instance using the factory function.
352
+ * Returns the instance directly (not a tuple).
353
+ *
354
+ * @example
355
+ * const db = get(indexedDBService);
356
+ * await db.users.getAll();
357
+ */
358
+ get<T>(factory: (...args: any[]) => T): T;
359
+ /**
360
+ * Create a child store instance that is automatically disposed
361
+ * when the parent store is disposed.
362
+ *
363
+ * Unlike `get()`, this returns the full StoreInstance with access to
364
+ * id, subscribe(), dispose(), etc.
365
+ *
366
+ * Use this when you need a store with the same lifecycle as the parent,
367
+ * or when you need full instance access.
368
+ *
369
+ * @example
370
+ * setup: (ctx) => {
371
+ * // Create a child store - disposed when parent disposes
372
+ * const childInstance = ctx.create(childSpec);
373
+ *
374
+ * return {
375
+ * getChildState: () => childInstance.state,
376
+ * disposeChild: () => childInstance.dispose(),
377
+ * };
378
+ * }
379
+ */
380
+ create<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>): StoreInstance<S, A>;
381
+ /**
382
+ * Create a service or factory instance that is automatically disposed
383
+ * when the parent store is disposed (if the instance has a dispose method).
384
+ *
385
+ * Unlike `get()` which caches instances, `create()` always creates fresh instances.
153
386
  *
154
387
  * @example
388
+ * setup: (ctx) => {
389
+ * // Create a fresh service instance - disposed with parent
390
+ * const db = ctx.create(indexedDBService);
391
+ *
392
+ * return {
393
+ * clearData: () => db.clearAll(),
394
+ * };
395
+ * }
396
+ */
397
+ create<T>(factory: (...args: any[]) => T): T;
398
+ /**
399
+ * Update state using Immer-style updater function or partial object.
400
+ *
401
+ * Also provides `.action()` to create action functions that wrap updates.
402
+ *
403
+ * @example
404
+ * // Direct update with updater function
155
405
  * update(draft => {
156
406
  * draft.items.push({ id: 1, name: 'New Item' });
157
407
  * draft.count++;
158
408
  * });
159
- */
160
- update(updater: (draft: TState) => void): void;
161
- /**
162
- * Update state with partial object (shallow merge).
163
409
  *
164
- * @example
410
+ * // Direct update with partial object
165
411
  * update({ count: 10, name: 'Updated' });
412
+ *
413
+ * // Create action with update.action()
414
+ * increment: update.action(draft => {
415
+ * draft.count++;
416
+ * }),
417
+ *
418
+ * // Action with arguments
419
+ * addItem: update.action((draft, name: string) => {
420
+ * draft.items.push({ id: Date.now(), name });
421
+ * }),
166
422
  */
167
- update(partial: Partial<TState>): void;
423
+ update: StoreUpdate<TState>;
168
424
  /**
169
425
  * Check if state has been modified since setup completed.
170
426
  *
@@ -183,15 +439,46 @@ export interface StoreContext<TState extends StateBase> {
183
439
  */
184
440
  reset(): void;
185
441
  /**
186
- * Use a mixin to compose reusable logic.
442
+ * Apply a mixin to compose reusable logic.
187
443
  * Mixins receive the same context and can return actions or values.
188
444
  * Only callable during setup phase.
189
445
  *
190
446
  * @example
191
- * const counter = ctx.use(counterMixin);
192
- * const multiplier = ctx.use(multiplyMixin, 2);
447
+ * const counter = ctx.mixin(counterMixin);
448
+ * const multiplier = ctx.mixin(multiplyMixin, 2);
449
+ */
450
+ mixin<TResult, TArgs extends unknown[]>(mixin: StoreMixin<TState, TResult, TArgs>, ...args: TArgs): TResult;
451
+ /**
452
+ * Create a lens-like accessor for a nested state path.
453
+ * Returns a [getter, setter] tuple with an on() method for subscribing to changes.
454
+ *
455
+ * @example
456
+ * // Basic usage - get/set value
457
+ * const [getName, setName] = focus("profile.name");
458
+ * const name = getName();
459
+ * setName("Jane");
460
+ *
461
+ * // With reducer
462
+ * setName(prev => prev.toUpperCase());
463
+ *
464
+ * // With fallback for nullable values
465
+ * const [getProfile, setProfile] = focus("profile", {
466
+ * fallback: () => ({ name: "Guest" })
467
+ * });
468
+ *
469
+ * // Subscribe to changes
470
+ * const unsubscribe = focus("profile.name").on(({ next, prev }) => {
471
+ * console.log(`Changed from ${prev} to ${next}`);
472
+ * });
473
+ *
474
+ * // Can be returned as actions
475
+ * return { nameFocus: focus("name"), profileFocus: focus("profile") };
193
476
  */
194
- use<TResult, TArgs extends unknown[]>(mixin: StoreMixin<TState, TResult, TArgs>, ...args: TArgs): TResult;
477
+ focus<P extends StatePath<TState>>(path: P): Focus<PathValue<TState, P>>;
478
+ focus<P extends StatePath<TState>>(path: P, options: FocusOptions<PathValue<TState, P>> & {
479
+ fallback: () => NonNullish<PathValue<TState, P>>;
480
+ }): Focus<NonNullish<PathValue<TState, P>>>;
481
+ focus<P extends StatePath<TState>>(path: P, options: FocusOptions<PathValue<TState, P>>): Focus<PathValue<TState, P>>;
195
482
  }
196
483
  /**
197
484
  * Options for defining a store.
@@ -277,7 +564,7 @@ export interface StoreOptions<TState extends StateBase, TActions extends Actions
277
564
  * Created and managed by the container.
278
565
  * Provides access to state, actions, and lifecycle management.
279
566
  */
280
- export interface StoreInstance<TState extends StateBase = StateBase, TActions extends ActionsBase = ActionsBase> {
567
+ export interface StoreInstance<TState extends StateBase = StateBase, TActions extends ActionsBase = ActionsBase> extends StorionObject<"store"> {
281
568
  /** Unique identifier for this instance */
282
569
  readonly id: string;
283
570
  /** The store specification that created this instance */
@@ -286,7 +573,7 @@ export interface StoreInstance<TState extends StateBase = StateBase, TActions ex
286
573
  readonly state: Readonly<TState>;
287
574
  /** Bound actions with reactive last() method */
288
575
  readonly actions: ReactiveActions<TActions>;
289
- /** Store instances this store depends on (via resolve() in setup) */
576
+ /** Store instances this store depends on (via get() in setup) */
290
577
  readonly deps: readonly StoreInstance<any, any>[];
291
578
  /**
292
579
  * Subscribe to state changes.
@@ -380,79 +667,214 @@ export interface StoreInstance<TState extends StateBase = StateBase, TActions ex
380
667
  _subscribeInternal<K extends keyof TState>(propKey: K | string, listener: () => void): VoidFunction;
381
668
  }
382
669
  /**
383
- * Middleware function for intercepting store creation.
670
+ * Container options.
671
+ */
672
+ export interface ContainerOptions {
673
+ /** Auto dispose options for all stores */
674
+ autoDispose?: AutoDisposeOptions;
675
+ /** Middleware chain for intercepting store creation */
676
+ middleware?: StoreMiddleware[];
677
+ }
678
+ /**
679
+ * Factory function that creates an instance using the resolver.
680
+ * The factory itself is used as the cache key (by reference).
384
681
  *
385
- * Middleware can:
386
- * - Modify the spec before creation
387
- * - Call next() to get the instance
388
- * - Wrap or modify the instance after creation
682
+ * StoreSpec is a specialized factory that returns StoreInstance.
683
+ */
684
+ export type Factory<T = any> = (resolver: Resolver) => T;
685
+ /**
686
+ * Context passed to generic middleware functions (for any factory).
687
+ */
688
+ export interface MiddlewareContext<T = any> {
689
+ /** The factory being invoked (StoreSpec or plain factory) */
690
+ readonly factory: Factory<T>;
691
+ /** The resolver instance */
692
+ readonly resolver: Resolver;
693
+ /** Call the next middleware or the factory itself */
694
+ readonly next: () => T;
695
+ /**
696
+ * Display name for debugging.
697
+ * - For stores: spec.displayName
698
+ * - For factories: factory.displayName ?? factory.name ?? undefined
699
+ */
700
+ readonly displayName: string | undefined;
701
+ }
702
+ /**
703
+ * Context passed to store middleware functions.
704
+ * Unlike MiddlewareContext, `spec` is always present.
705
+ */
706
+ export interface StoreMiddlewareContext<S extends StateBase = StateBase, A extends ActionsBase = ActionsBase> extends MiddlewareContext<StoreInstance<S, A>> {
707
+ /** The store spec being invoked */
708
+ readonly spec: StoreSpec<S, A>;
709
+ /** Store display name (always present for stores) */
710
+ readonly displayName: string;
711
+ }
712
+ /**
713
+ * Generic middleware function for intercepting any factory creation.
714
+ * Use for resolver middleware that handles both stores and plain factories.
389
715
  *
390
716
  * @example
391
- * // Logging middleware
392
- * const loggingMiddleware: StoreMiddleware = (spec, next) => {
393
- * console.log(`Creating store: ${spec.name}`);
394
- * const instance = next(spec);
395
- * console.log(`Created store: ${instance.id}`);
396
- * return instance;
717
+ * ```ts
718
+ * const loggingMiddleware: Middleware = (ctx) => {
719
+ * if (ctx.displayName) {
720
+ * console.log("Creating:", ctx.displayName);
721
+ * }
722
+ * return ctx.next();
397
723
  * };
724
+ * ```
725
+ */
726
+ export type Middleware = <T>(ctx: MiddlewareContext<T>) => T;
727
+ /**
728
+ * Store-specific middleware function.
729
+ * Use for container middleware where you always work with stores.
730
+ * The context always has `spec` available.
398
731
  *
399
732
  * @example
400
- * // Wrap actions with error boundary
401
- * const errorBoundaryMiddleware: StoreMiddleware = (spec, next) => {
402
- * const instance = next(spec);
403
- * // Wrap actions here if needed
733
+ * ```ts
734
+ * const loggingMiddleware: StoreMiddleware = (ctx) => {
735
+ * console.log(`Creating store: ${ctx.displayName}`);
736
+ * const instance = ctx.next();
737
+ * console.log(`Created: ${instance.id}`);
404
738
  * return instance;
405
739
  * };
740
+ * ```
406
741
  */
407
- export type StoreMiddleware = <S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>, next: (spec: StoreSpec<S, A>) => StoreInstance<S, A>) => StoreInstance<S, A>;
742
+ export type StoreMiddleware = <S extends StateBase = StateBase, A extends ActionsBase = ActionsBase>(ctx: StoreMiddlewareContext<S, A>) => StoreInstance<S, A>;
408
743
  /**
409
- * Container options.
744
+ * Options for creating a resolver.
410
745
  */
411
- export interface ContainerOptions {
412
- /** Default lifetime for stores */
413
- defaultLifetime?: Lifetime;
414
- /** Default equality for stores */
415
- defaultEquality?: Equality;
416
- /** Middleware chain for intercepting store creation */
417
- middleware?: StoreMiddleware[];
746
+ export interface ResolverOptions {
747
+ /** Middleware to apply to factory creation */
748
+ middleware?: Middleware[];
749
+ /** Parent resolver for hierarchical lookup */
750
+ parent?: Resolver;
418
751
  }
419
752
  /**
420
- * Resolver interface to get store instances.
421
- * StoreContainer implements this interface.
753
+ * Resolver interface for factory-based dependency injection.
754
+ * Provides caching, override support, and scoped resolvers.
755
+ *
756
+ * StoreContainer extends this with store-specific lifecycle methods.
422
757
  */
423
- export interface StoreResolver {
758
+ export interface Resolver {
424
759
  /**
425
- * Get a store instance by spec.
426
- * First call creates instance, subsequent calls return cached.
760
+ * Get or create a cached instance from a factory.
761
+ * Returns the same instance on subsequent calls.
427
762
  */
428
- get<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>): StoreInstance<S, A>;
763
+ get<T>(factory: Factory<T>): T;
429
764
  /**
430
- * Get a store instance by its unique ID.
765
+ * Create a fresh instance from a factory (bypasses cache).
431
766
  */
432
- get(id: string): StoreInstance<any, any> | undefined;
767
+ create<T>(factory: Factory<T>): T;
433
768
  /**
434
- * Check if a store instance exists.
769
+ * Override a factory with a custom implementation.
770
+ * Useful for testing or environment-specific behavior.
771
+ * Clears the cached instance if one exists.
435
772
  */
436
- has(spec: StoreSpec<any, any>): boolean;
773
+ set<T>(factory: Factory<T>, override: Factory<T>): void;
774
+ /**
775
+ * Check if a factory has a cached instance.
776
+ */
777
+ has(factory: Factory): boolean;
778
+ /**
779
+ * Get a cached instance if it exists, otherwise return undefined.
780
+ * Does NOT create the instance if not cached.
781
+ */
782
+ tryGet<T>(factory: Factory<T>): T | undefined;
783
+ /**
784
+ * Delete a cached instance.
785
+ * If the instance has a `dispose()` method, it will be called.
786
+ * Returns true if an instance was deleted.
787
+ */
788
+ delete(factory: Factory): boolean;
789
+ /**
790
+ * Clear all cached instances.
791
+ * Calls `dispose()` on each instance that has the method.
792
+ */
793
+ clear(): void;
794
+ /**
795
+ * Create a child resolver that inherits from this resolver.
796
+ * Child can override factories without affecting parent.
797
+ */
798
+ scope(options?: ResolverOptions): Resolver;
437
799
  }
800
+ export type AutoDisposeOptions = {
801
+ /** Grace period in ms before disposing the store (default: 100) */
802
+ gracePeriodMs?: number;
803
+ };
438
804
  /**
439
- * Store container - manages store instances.
805
+ * Store container - manages store instances with resolver pattern.
440
806
  *
441
807
  * The container is responsible for:
442
808
  * - Creating instances on first access (lazy)
443
809
  * - Caching instances (singleton per spec)
444
810
  * - Resolving dependencies between stores
445
811
  * - Managing lifetime and disposal
812
+ * - Supporting DI via set() overrides
813
+ * - Scoped containers via scope()
814
+ *
815
+ * Note: StoreContainer has store-specific method signatures.
816
+ * For generic factory DI, use createResolver() instead.
446
817
  */
447
- export interface StoreContainer extends StoreResolver {
818
+ export interface StoreContainer extends StorionObject<"container"> {
819
+ /**
820
+ * Get a store instance by spec (cached).
821
+ * First call creates instance, subsequent calls return cached.
822
+ */
823
+ get<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>): StoreInstance<S, A>;
824
+ /**
825
+ * Get a service or factory instance (cached).
826
+ * Returns the instance directly (not a StoreInstance).
827
+ *
828
+ * @example
829
+ * const db = container.get(indexedDBService);
830
+ * await db.users.getAll();
831
+ */
832
+ get<T>(factory: Factory<T>): T;
833
+ /**
834
+ * Get a store instance by its unique ID.
835
+ */
836
+ get(id: string): StoreInstance<any, any> | undefined;
837
+ /**
838
+ * Create a fresh store instance (bypasses cache).
839
+ * Useful for child stores or temporary instances.
840
+ */
841
+ create<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>): StoreInstance<S, A>;
842
+ /**
843
+ * Create a fresh factory instance (bypasses cache).
844
+ * Returns the instance directly.
845
+ */
846
+ create<T>(factory: Factory<T>): T;
847
+ /**
848
+ * Override a spec with a custom implementation.
849
+ * Useful for testing or environment-specific behavior.
850
+ */
851
+ set<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>, override: StoreSpec<S, A>): void;
448
852
  /**
449
- * Dispose all cached instances.
853
+ * Check if a store instance is cached.
854
+ */
855
+ has(spec: StoreSpec<any, any>): boolean;
856
+ /**
857
+ * Get cached instance if exists, otherwise undefined.
858
+ * Does NOT create the instance.
859
+ */
860
+ tryGet<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>): StoreInstance<S, A> | undefined;
861
+ /**
862
+ * Remove a cached instance.
863
+ */
864
+ delete(spec: StoreSpec<any, any>): boolean;
865
+ /**
866
+ * Clear all cached instances (disposes them).
450
867
  */
451
868
  clear(): void;
452
869
  /**
453
870
  * Dispose a specific store instance.
454
871
  */
455
872
  dispose(spec: StoreSpec<any, any>): boolean;
873
+ /**
874
+ * Create a child container that inherits from this one.
875
+ * Child can override specs without affecting parent.
876
+ */
877
+ scope(options?: ContainerOptions): StoreContainer;
456
878
  /**
457
879
  * Subscribe to store creation events.
458
880
  */
@@ -468,25 +890,70 @@ export interface StoreContainer extends StoreResolver {
468
890
  * Provides access to stores within a selector function.
469
891
  * Returns tuple [readonlyState, actions] with tracking proxy.
470
892
  */
471
- export interface SelectorContext {
893
+ export interface SelectorContext extends StorionObject<"selector.context"> {
472
894
  /**
473
895
  * Get a store's state and actions.
474
896
  *
475
- * Returns a tracking proxy that records property access
476
- * for fine-grained re-render optimization.
897
+ * Returns tuple with both array destructuring and named properties.
898
+ * Includes tracking proxy for fine-grained re-render optimization.
899
+ *
900
+ * @example
901
+ * // Array destructuring
902
+ * const [state, actions] = get(counterSpec);
903
+ *
904
+ * // Named properties
905
+ * const tuple = get(counterSpec);
906
+ * tuple.state.count;
907
+ * tuple.actions.increment();
477
908
  *
478
909
  * @param spec - Store specification
479
- * @returns Tuple of [trackingState, actions]
910
+ * @returns Tuple of [trackingState, actions] with .state and .actions props
911
+ */
912
+ get<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>): StoreTuple<S, A>;
913
+ /**
914
+ * Get a service or factory instance.
915
+ * Creates and caches the instance using the factory function.
916
+ * Returns the instance directly (not a tuple).
917
+ *
918
+ * @example
919
+ * const db = get(indexedDBService);
920
+ * await db.users.getAll();
480
921
  */
481
- resolve<S extends StateBase, A extends ActionsBase>(spec: StoreSpec<S, A>): readonly [Readonly<S>, A];
922
+ get<T>(factory: (...args: any[]) => T): T;
482
923
  /**
483
- * Use a mixin to compose reusable selector logic.
924
+ * Apply a mixin to compose reusable selector logic.
484
925
  * Mixins receive the same context and can return computed values.
485
926
  *
486
927
  * @example
487
- * const total = ctx.use(sumMixin, [store1, store2]);
928
+ * const total = ctx.mixin(sumMixin, [store1, store2]);
929
+ */
930
+ mixin<TResult, TArgs extends unknown[]>(mixin: SelectorMixin<TResult, TArgs>, ...args: TArgs): TResult;
931
+ /**
932
+ * Unique identifier for this selector context (per component instance).
933
+ * Useful for scoping operations with trigger().
934
+ *
935
+ * @example
936
+ * const { data } = useStore(({ get, id }) => {
937
+ * const store = get(dataStore);
938
+ * // Each component instance gets unique scope
939
+ * trigger(id, store.actions.dispatch, [params], params);
940
+ * return { data: store.state.data };
941
+ * });
942
+ */
943
+ readonly id: object;
944
+ /**
945
+ * Run a callback once when the component mounts.
946
+ * The callback is executed immediately during render (before paint).
947
+ *
948
+ * @example
949
+ * useStore(({ get, once }) => {
950
+ * const mutation = get(submitStore);
951
+ * // Reset mutation state on mount
952
+ * once(() => mutation.actions.reset());
953
+ * return mutation;
954
+ * });
488
955
  */
489
- use<TResult, TArgs extends unknown[]>(mixin: SelectorMixin<TResult, TArgs>, ...args: TArgs): TResult;
956
+ once(callback: () => void): void;
490
957
  }
491
958
  /**
492
959
  * Selector function type.