storion 0.8.2 → 0.9.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/CHANGELOG.md CHANGED
@@ -8,23 +8,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
  ### Added
11
+
11
12
  - `MetaEntry.fields` now supports arrays for applying meta to multiple fields at once
12
13
  ```ts
13
- meta: [notPersisted.for(["password", "token"])]
14
+ meta: [notPersisted.for(["password", "token"])];
14
15
  ```
15
16
  - `MetaQuery.fields(type, predicate?)` method to get field names with a specific meta type
16
17
  ```ts
17
- const sessionFields = ctx.meta.fields(sessionStore); // ['token', 'userId']
18
- const highPriority = ctx.meta.fields(priority, v => v > 5);
18
+ const sessionFields = ctx.meta.fields(sessionStore); // ['token', 'userId']
19
+ const highPriority = ctx.meta.fields(priority, (v) => v > 5);
19
20
  ```
20
- - `persistMiddleware` `fields` option for multi-storage patterns
21
+ - `applyFor` now supports object form to map patterns to different middleware
22
+ ```ts
23
+ applyFor({
24
+ userStore: loggingMiddleware,
25
+ "auth*": [authMiddleware, securityMiddleware],
26
+ "*Cache": cacheMiddleware,
27
+ });
28
+ ```
29
+
30
+ ### Changed
31
+
32
+ - **BREAKING**: `persistMiddleware` API refactored for better encapsulation
33
+
34
+ - New `handler` option replaces `load`/`save` callbacks
35
+ - Handler receives `PersistContext` (extends `StoreMiddlewareContext` with `store` instance)
36
+ - Handler returns `{ load, save }` object (can be sync or async)
37
+ - `onError` signature changed to `(error, operation)` where operation is `"init" | "load" | "save"`
38
+ - Enables encapsulated async initialization (e.g., IndexedDB)
39
+
21
40
  ```ts
22
- // Split fields between session and local storage
41
+ // Before (old API)
23
42
  persistMiddleware({
24
- filter: ({ meta }) => meta.any(sessionStore),
25
- fields: ({ meta }) => meta.fields(sessionStore),
26
- save: (ctx, state) => sessionStorage.setItem(ctx.displayName, JSON.stringify(state)),
27
- })
43
+ load: (ctx) => localStorage.getItem(ctx.displayName),
44
+ save: (ctx, state) =>
45
+ localStorage.setItem(ctx.displayName, JSON.stringify(state)),
46
+ });
47
+
48
+ // After (new API)
49
+ persistMiddleware({
50
+ handler: (ctx) => {
51
+ const key = `app:${ctx.displayName}`;
52
+ return {
53
+ load: () => JSON.parse(localStorage.getItem(key) || "null"),
54
+ save: (state) => localStorage.setItem(key, JSON.stringify(state)),
55
+ };
56
+ },
57
+ });
58
+
59
+ // Async handler (IndexedDB)
60
+ persistMiddleware({
61
+ handler: async (ctx) => {
62
+ const db = await openDB("app-db");
63
+ return {
64
+ load: () => db.get("stores", ctx.displayName),
65
+ save: (state) => db.put("stores", state, ctx.displayName),
66
+ };
67
+ },
68
+ });
28
69
  ```
29
70
 
30
71
  ---
@@ -32,6 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
32
73
  ## [0.8.0] - 2024-12-21
33
74
 
34
75
  ### Added
76
+
35
77
  - **Persist Module** (`storion/persist`)
36
78
  - `persistMiddleware(options)` for automatic state persistence
37
79
  - `notPersisted` meta for excluding stores or fields from persistence
@@ -50,6 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
50
92
  - `store.hydrate(state, { force })` - force option to override dirty properties
51
93
 
52
94
  ### Changed
95
+
53
96
  - `StoreMiddlewareContext` now includes `meta` property for querying store metadata
54
97
  - `FactoryMiddlewareContext` now includes `meta` property for querying factory metadata
55
98
 
@@ -58,6 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
58
101
  ## [0.7.0] - 2024-12-15
59
102
 
60
103
  ### Added
104
+
61
105
  - **DevTools Module** (`storion/devtools`)
62
106
  - `devtoolsMiddleware()` for state inspection
63
107
  - `__revertState` and `__takeSnapshot` injected actions
@@ -71,6 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
71
115
  - `create()` shorthand for single-store apps returning `[instance, useHook, withStore]`
72
116
 
73
117
  ### Changed
118
+
74
119
  - Improved TypeScript inference for store actions
75
120
 
76
121
  ---
@@ -78,6 +123,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
78
123
  ## [0.6.0] - 2024-12-01
79
124
 
80
125
  ### Added
126
+
81
127
  - **Async Module** (`storion/async`)
82
128
  - `async.fresh<T>()` - throws during loading (Suspense-compatible)
83
129
  - `async.stale<T>(initialData)` - returns stale data during loading
@@ -87,6 +133,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
87
133
  - `trigger(action, deps, ...args)` for declarative data fetching in components
88
134
 
89
135
  ### Changed
136
+
90
137
  - Effects now require synchronous functions (use `ctx.safe()` for async)
91
138
 
92
139
  ---
@@ -94,6 +141,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
94
141
  ## [0.5.0] - 2024-11-15
95
142
 
96
143
  ### Added
144
+
97
145
  - **Focus (Lens-like Access)**
98
146
  - `focus(path)` for nested state access
99
147
  - Returns `[getter, setter]` tuple
@@ -111,6 +159,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
111
159
  ## [0.4.0] - 2024-11-01
112
160
 
113
161
  ### Added
162
+
114
163
  - **Middleware System**
115
164
  - `container({ middleware: [...] })` for middleware injection
116
165
  - Middleware receives `MiddlewareContext` with `type`, `next`, `resolver`
@@ -119,6 +168,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
119
168
  - `createValidationMiddleware()` built-in middleware
120
169
 
121
170
  ### Changed
171
+
122
172
  - Container now uses middleware chain pattern
123
173
 
124
174
  ---
@@ -126,6 +176,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
126
176
  ## [0.3.0] - 2024-10-15
127
177
 
128
178
  ### Added
179
+
129
180
  - **Store Lifecycle**
130
181
  - `lifetime: "keepAlive"` (default) - persists until container disposal
131
182
  - `lifetime: "autoDispose"` - disposes when no subscribers
@@ -137,6 +188,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
137
188
  - `store.reset()` to restore initial state
138
189
 
139
190
  ### Changed
191
+
140
192
  - Stores now track dirty state automatically
141
193
 
142
194
  ---
@@ -144,6 +196,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
144
196
  ## [0.2.0] - 2024-10-01
145
197
 
146
198
  ### Added
199
+
147
200
  - **Dependency Injection**
148
201
  - `container()` for managing store instances
149
202
  - `get(factory)` for resolving dependencies
@@ -153,6 +206,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
153
206
  - `useContainer()` hook
154
207
 
155
208
  ### Changed
209
+
156
210
  - Stores are now lazily instantiated via container
157
211
 
158
212
  ---
@@ -160,6 +214,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
160
214
  ## [0.1.0] - 2024-09-15
161
215
 
162
216
  ### Added
217
+
163
218
  - **Core Store**
164
219
  - `store(options)` factory function
165
220
  - `state` - reactive state object
@@ -187,22 +242,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
187
242
  ### Migrating to 0.8.0
188
243
 
189
244
  #### Meta System Changes
245
+
190
246
  If you were using internal meta APIs, update to the new public API:
191
247
 
192
248
  ```ts
193
249
  // Before (internal)
194
- spec.meta // was MetaEntry[]
250
+ spec.meta; // was MetaEntry[]
195
251
 
196
252
  // After (0.8.0)
197
- ctx.meta(persistMeta).store // query store-level
198
- ctx.meta(persistMeta).fields // query field-level
199
- ctx.meta.all(type) // get all values
200
- ctx.meta.any(type1, type2) // check existence
253
+ ctx.meta(persistMeta).store; // query store-level
254
+ ctx.meta(persistMeta).fields; // query field-level
255
+ ctx.meta.all(type); // get all values
256
+ ctx.meta.any(type1, type2); // check existence
201
257
  ```
202
258
 
203
259
  ### Migrating to 0.6.0
204
260
 
205
261
  #### Async Effects
262
+
206
263
  Effects must now be synchronous:
207
264
 
208
265
  ```ts
@@ -231,4 +288,3 @@ effect((ctx) => {
231
288
  [0.3.0]: https://github.com/linq2js/storion/compare/v0.2.0...v0.3.0
232
289
  [0.2.0]: https://github.com/linq2js/storion/compare/v0.1.0...v0.2.0
233
290
  [0.1.0]: https://github.com/linq2js/storion/releases/tag/v0.1.0
234
-
@@ -2,6 +2,8 @@ import { Middleware, MiddlewareContext, StoreMiddleware } from '../types';
2
2
 
3
3
  /** Pattern type for matching displayName */
4
4
  export type SpecPattern = string | RegExp;
5
+ /** Mapping of patterns to middleware */
6
+ export type MiddlewareMap = Record<string, StoreMiddleware | StoreMiddleware[]>;
5
7
  /**
6
8
  * Conditionally apply middleware based on a predicate or pattern(s).
7
9
  *
@@ -13,6 +15,9 @@ export type SpecPattern = string | RegExp;
13
15
  * @param patterns - Pattern or array of patterns to match displayName
14
16
  * @param middleware - Middleware or array of middleware to apply
15
17
  *
18
+ * @overload Apply different middleware for different patterns (object form)
19
+ * @param middlewareMap - Object mapping patterns to middleware
20
+ *
16
21
  * Pattern types:
17
22
  * - `"userStore"` - exact match
18
23
  * - `"user*"` - startsWith
@@ -44,10 +49,18 @@ export type SpecPattern = string | RegExp;
44
49
  *
45
50
  * // Multiple middleware
46
51
  * applyFor("counterStore", [loggingMiddleware, devtoolsMiddleware]);
52
+ *
53
+ * // Object form - map patterns to middleware
54
+ * applyFor({
55
+ * "userStore": loggingMiddleware,
56
+ * "auth*": [authMiddleware, securityMiddleware],
57
+ * "*Cache": cacheMiddleware,
58
+ * });
47
59
  * ```
48
60
  */
49
61
  export declare function applyFor(predicate: (ctx: MiddlewareContext) => boolean, middleware: Middleware | Middleware[]): Middleware;
50
62
  export declare function applyFor(patterns: SpecPattern | SpecPattern[], middleware: StoreMiddleware | StoreMiddleware[]): Middleware;
63
+ export declare function applyFor(middlewareMap: MiddlewareMap): Middleware;
51
64
  /**
52
65
  * Apply middleware to all except those matching predicate or pattern(s).
53
66
  *
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/core/middleware.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAEV,UAAU,EACV,iBAAiB,EACjB,eAAe,EAChB,MAAM,UAAU,CAAC;AAElB,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AA8F1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,QAAQ,CACtB,SAAS,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,EAC9C,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,GACpC,UAAU,CAAC;AACd,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,WAAW,GAAG,WAAW,EAAE,EACrC,UAAU,EAAE,eAAe,GAAG,eAAe,EAAE,GAC9C,UAAU,CAAC;AA4Bd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,EAC9C,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,GACpC,UAAU,CAAC;AACd,wBAAgB,WAAW,CACzB,QAAQ,EAAE,WAAW,GAAG,WAAW,EAAE,EACrC,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,GACpC,UAAU,CAAC;AAmBd;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CACvB,eAAe,EAAE,eAAe,GAAG,eAAe,EAAE,GACnD,UAAU,CAWZ"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/core/middleware.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAEV,UAAU,EACV,iBAAiB,EACjB,eAAe,EAEhB,MAAM,UAAU,CAAC;AAElB,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AA8F1C,wCAAwC;AACxC,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,EAAE,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,QAAQ,CACtB,SAAS,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,EAC9C,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,GACpC,UAAU,CAAC;AACd,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,WAAW,GAAG,WAAW,EAAE,EACrC,UAAU,EAAE,eAAe,GAAG,eAAe,EAAE,GAC9C,UAAU,CAAC;AACd,wBAAgB,QAAQ,CAAC,aAAa,EAAE,aAAa,GAAG,UAAU,CAAC;AAwDnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,EAC9C,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,GACpC,UAAU,CAAC;AACd,wBAAgB,WAAW,CACzB,QAAQ,EAAE,WAAW,GAAG,WAAW,EAAE,EACrC,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,GACpC,UAAU,CAAC;AAmBd;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CACvB,eAAe,EAAE,eAAe,GAAG,eAAe,EAAE,GACnD,UAAU,CAWZ"}
@@ -2868,8 +2868,23 @@ function compose(...middlewares) {
2868
2868
  return executeNext();
2869
2869
  };
2870
2870
  }
2871
- function applyFor(predicateOrPatterns, middleware) {
2872
- const predicate = typeof predicateOrPatterns === "function" ? predicateOrPatterns : patternsToPredicate(predicateOrPatterns);
2871
+ function applyFor(predicateOrPatternsOrMap, middleware) {
2872
+ if (typeof predicateOrPatternsOrMap === "object" && !Array.isArray(predicateOrPatternsOrMap) && !(predicateOrPatternsOrMap instanceof RegExp)) {
2873
+ const entries = Object.entries(predicateOrPatternsOrMap);
2874
+ const pairs = entries.map(([pattern, mw]) => ({
2875
+ predicate: patternsToPredicate(pattern),
2876
+ middleware: Array.isArray(mw) ? compose(...mw) : mw
2877
+ }));
2878
+ return (ctx) => {
2879
+ for (const { predicate: predicate2, middleware: middleware2 } of pairs) {
2880
+ if (predicate2(ctx)) {
2881
+ return middleware2(ctx);
2882
+ }
2883
+ }
2884
+ return ctx.next();
2885
+ };
2886
+ }
2887
+ const predicate = typeof predicateOrPatternsOrMap === "function" ? predicateOrPatternsOrMap : patternsToPredicate(predicateOrPatternsOrMap);
2873
2888
  const composedMiddleware = Array.isArray(middleware) ? compose(...middleware) : middleware;
2874
2889
  return (ctx) => {
2875
2890
  if (predicate(ctx)) {
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { batch, untrack } from './core/tracking';
11
11
  export { createResolver } from './core/createResolver';
12
12
  export { pick } from './core/pick';
13
13
  export { effect, type EffectFn, type EffectContext, type EffectOptions, type EffectErrorStrategy, type EffectErrorContext, type EffectRetryConfig, } from './core/effect';
14
- export { applyFor, applyExcept, forStores, type SpecPattern, } from './core/middleware';
14
+ export { applyFor, applyExcept, forStores, type SpecPattern, type MiddlewareMap, } from './core/middleware';
15
15
  export { equality, shallowEqual, deepEqual, strictEqual, } from './core/equality';
16
16
  export { trigger, type TriggerOptions } from './trigger';
17
17
  export { wrapFn as wrapFn, unwrapFn, isWrappedFn } from './core/fnWrapper';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,YAAY,EAEjB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAEf,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACb,KAAK,eAAe,EAEpB,KAAK,QAAQ,EACb,KAAK,QAAQ,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,EAAE,EACF,SAAS,EACT,OAAO,EACP,MAAM,EACN,WAAW,EACX,OAAO,EACP,OAAO,EACP,QAAQ,EACR,cAAc,EACd,iBAAiB,GAClB,MAAM,MAAM,CAAC;AAGd,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,OAAO,EACL,MAAM,EACN,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,KAAK,WAAW,GACjB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,WAAW,GACZ,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAGzD,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG3E,OAAO,EACL,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,YAAY,EAEjB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAEf,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACb,KAAK,eAAe,EAEpB,KAAK,QAAQ,EACb,KAAK,QAAQ,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,EAAE,EACF,SAAS,EACT,OAAO,EACP,MAAM,EACN,WAAW,EACX,OAAO,EACP,OAAO,EACP,QAAQ,EACR,cAAc,EACd,iBAAiB,GAClB,MAAM,MAAM,CAAC;AAGd,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAGjD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,OAAO,EACL,MAAM,EACN,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,KAAK,WAAW,EAChB,KAAK,aAAa,GACnB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,WAAW,GACZ,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAGzD,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG3E,OAAO,EACL,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC"}
@@ -2,7 +2,7 @@ import { m as meta } from "../meta-40r-AZfe.js";
2
2
  import { i as isPromiseLike } from "../isPromiseLike-bFkfHAbm.js";
3
3
  const notPersisted = meta();
4
4
  function persistMiddleware(options) {
5
- const { filter, fields, load, save, onError, force = false } = options;
5
+ const { filter, fields, handler, onError, force = false } = options;
6
6
  return (context) => {
7
7
  const { next, meta: meta2 } = context;
8
8
  const instance = next();
@@ -38,39 +38,61 @@ function persistMiddleware(options) {
38
38
  }
39
39
  return filtered;
40
40
  };
41
- const hydrateWithState = (state) => {
42
- if (state != null) {
41
+ const persistContext = {
42
+ ...context,
43
+ store: instance
44
+ };
45
+ const setupPersistence = (persistHandler) => {
46
+ const { load, save } = persistHandler;
47
+ const hydrateWithState = (state) => {
48
+ if (state != null) {
49
+ try {
50
+ instance.hydrate(filterState(state), { force });
51
+ } catch (error) {
52
+ onError == null ? void 0 : onError(error, "load");
53
+ }
54
+ }
55
+ };
56
+ if (load) {
43
57
  try {
44
- instance.hydrate(filterState(state), { force });
58
+ const loadResult = load();
59
+ if (loadResult) {
60
+ if (isPromiseLike(loadResult)) {
61
+ loadResult.then(
62
+ (state) => hydrateWithState(state),
63
+ (error) => onError == null ? void 0 : onError(error, "load")
64
+ );
65
+ } else {
66
+ hydrateWithState(loadResult);
67
+ }
68
+ }
45
69
  } catch (error) {
46
- onError == null ? void 0 : onError(context, error, "load");
70
+ onError == null ? void 0 : onError(error, "load");
47
71
  }
48
72
  }
73
+ if (save) {
74
+ instance.subscribe(() => {
75
+ try {
76
+ const state = instance.dehydrate();
77
+ save(filterState(state));
78
+ } catch (error) {
79
+ onError == null ? void 0 : onError(error, "save");
80
+ }
81
+ });
82
+ }
49
83
  };
50
84
  try {
51
- const loadResult = load == null ? void 0 : load(context);
52
- if (loadResult) {
53
- if (isPromiseLike(loadResult)) {
54
- loadResult.then(
55
- (state) => hydrateWithState(state),
56
- (error) => onError == null ? void 0 : onError(context, error, "load")
57
- );
58
- } else {
59
- hydrateWithState(loadResult);
60
- }
85
+ const handlerResult = handler(persistContext);
86
+ if (isPromiseLike(handlerResult)) {
87
+ handlerResult.then(
88
+ (persistHandler) => setupPersistence(persistHandler),
89
+ (error) => onError == null ? void 0 : onError(error, "init")
90
+ );
91
+ } else {
92
+ setupPersistence(handlerResult);
61
93
  }
62
94
  } catch (error) {
63
- onError == null ? void 0 : onError(context, error, "load");
64
- }
65
- if (save) {
66
- instance.subscribe(() => {
67
- try {
68
- const state = instance.dehydrate();
69
- save(context, filterState(state));
70
- } catch (error) {
71
- onError == null ? void 0 : onError(context, error, "save");
72
- }
73
- });
95
+ onError == null ? void 0 : onError(error, "init");
74
96
  }
75
97
  return instance;
76
98
  };
@@ -1,4 +1,4 @@
1
- import { StoreMiddleware, StoreMiddlewareContext } from '../types';
1
+ import { StoreMiddleware, StoreMiddlewareContext, StoreInstance } from '../types';
2
2
 
3
3
  /**
4
4
  * Mark stores or fields as not persisted.
@@ -27,8 +27,8 @@ import { StoreMiddleware, StoreMiddlewareContext } from '../types';
27
27
  * state: { name: '', password: '', token: '' },
28
28
  * setup: () => ({}),
29
29
  * meta: [
30
- * notPersisted('password'),
31
- * notPersisted('token'),
30
+ * notPersisted.for('password'),
31
+ * notPersisted.for('token'),
32
32
  * ],
33
33
  * });
34
34
  * ```
@@ -38,6 +38,33 @@ export declare const notPersisted: import('..').MetaType<any, [], true>;
38
38
  * Result from load function - can be sync or async
39
39
  */
40
40
  export type PersistLoadResult = Record<string, unknown> | null | undefined | Promise<Record<string, unknown> | null | undefined>;
41
+ /**
42
+ * Context passed to the handler function.
43
+ * Extends StoreMiddlewareContext with the created store instance.
44
+ */
45
+ export interface PersistContext extends StoreMiddlewareContext {
46
+ /** The store instance being persisted */
47
+ store: StoreInstance;
48
+ }
49
+ /**
50
+ * Handler returned by the handler function.
51
+ * Contains the load and save operations for a specific store.
52
+ */
53
+ export interface PersistHandler {
54
+ /**
55
+ * Load persisted state for the store.
56
+ * Can return sync or async result.
57
+ *
58
+ * @returns The persisted state, null/undefined if not found, or a Promise
59
+ */
60
+ load?: () => PersistLoadResult;
61
+ /**
62
+ * Save state to persistent storage.
63
+ *
64
+ * @param state - The dehydrated state to save
65
+ */
66
+ save?: (state: Record<string, unknown>) => void;
67
+ }
41
68
  /**
42
69
  * Options for persist middleware
43
70
  */
@@ -59,28 +86,43 @@ export interface PersistOptions {
59
86
  */
60
87
  fields?: (context: StoreMiddlewareContext) => string[];
61
88
  /**
62
- * Load persisted state for a store.
63
- * Can return sync or async result.
89
+ * Handler factory that creates load/save operations for each store.
90
+ * Receives context with store instance, returns handler with load/save.
91
+ * Can be sync or async (e.g., for IndexedDB initialization).
64
92
  *
65
- * @param context - The middleware context
66
- * @returns The persisted state, null/undefined if not found, or a Promise
67
- */
68
- load?: (context: StoreMiddlewareContext) => PersistLoadResult;
69
- /**
70
- * Save state to persistent storage.
93
+ * @param context - The persist context with store instance
94
+ * @returns Handler with load/save operations, or Promise of handler
71
95
  *
72
- * @param context - The middleware context
73
- * @param state - The dehydrated state to save
96
+ * @example Sync handler (localStorage)
97
+ * ```ts
98
+ * handler: (ctx) => {
99
+ * const key = `app:${ctx.displayName}`;
100
+ * return {
101
+ * load: () => JSON.parse(localStorage.getItem(key) || 'null'),
102
+ * save: (state) => localStorage.setItem(key, JSON.stringify(state)),
103
+ * };
104
+ * }
105
+ * ```
106
+ *
107
+ * @example Async handler (IndexedDB)
108
+ * ```ts
109
+ * handler: async (ctx) => {
110
+ * const db = await openDB('app-db');
111
+ * return {
112
+ * load: () => db.get('stores', ctx.displayName),
113
+ * save: (state) => db.put('stores', state, ctx.displayName),
114
+ * };
115
+ * }
116
+ * ```
74
117
  */
75
- save?: (context: StoreMiddlewareContext, state: Record<string, unknown>) => void;
118
+ handler: (context: PersistContext) => PersistHandler | PromiseLike<PersistHandler>;
76
119
  /**
77
- * Called when an error occurs during load or save.
120
+ * Called when an error occurs during init, load, or save.
78
121
  *
79
- * @param context - The middleware context
80
122
  * @param error - The error that occurred
81
- * @param operation - Whether the error occurred during 'load' or 'save'
123
+ * @param operation - Whether the error occurred during 'init', 'load', or 'save'
82
124
  */
83
- onError?: (context: StoreMiddlewareContext, error: unknown, operation: "load" | "save") => void;
125
+ onError?: (error: unknown, operation: "init" | "load" | "save") => void;
84
126
  /**
85
127
  * Force hydration to overwrite dirty (modified) state properties.
86
128
  *
@@ -96,59 +138,74 @@ export interface PersistOptions {
96
138
  /**
97
139
  * Creates a persist middleware that automatically saves and restores store state.
98
140
  *
99
- * @example
141
+ * @example localStorage (sync handler)
100
142
  * ```ts
101
- * import { container } from "storion";
143
+ * import { container, forStores } from "storion";
102
144
  * import { persistMiddleware } from "storion/persist";
103
145
  *
104
146
  * const app = container({
105
- * middleware: [persistMiddleware({
106
- * load: (ctx) => {
107
- * const key = `storion:${ctx.spec.displayName}`;
108
- * const data = localStorage.getItem(key);
109
- * return data ? JSON.parse(data) : null;
110
- * },
111
- * save: (ctx, state) => {
112
- * const key = `storion:${ctx.spec.displayName}`;
113
- * localStorage.setItem(key, JSON.stringify(state));
114
- * },
115
- * onError: (ctx, error, op) => {
116
- * console.error(`Persist ${op} error for ${ctx.spec.displayName}:`, error);
117
- * },
118
- * })],
147
+ * middleware: forStores([
148
+ * persistMiddleware({
149
+ * handler: (ctx) => {
150
+ * const key = `app:${ctx.displayName}`;
151
+ * return {
152
+ * load: () => JSON.parse(localStorage.getItem(key) || 'null'),
153
+ * save: (state) => localStorage.setItem(key, JSON.stringify(state)),
154
+ * };
155
+ * },
156
+ * onError: (error, op) => console.error(`Persist ${op} failed:`, error),
157
+ * }),
158
+ * ]),
119
159
  * });
120
160
  * ```
121
161
  *
122
- * @example Async load (e.g., IndexedDB)
162
+ * @example IndexedDB (async handler)
123
163
  * ```ts
124
164
  * persistMiddleware({
125
- * load: async (ctx) => {
126
- * const db = await openDB();
127
- * return db.get('stores', ctx.spec.displayName);
128
- * },
129
- * save: (ctx, state) => {
130
- * openDB().then(db => db.put('stores', state, ctx.spec.displayName));
165
+ * handler: async (ctx) => {
166
+ * const db = await openDB('app-db', 1, {
167
+ * upgrade(db) { db.createObjectStore('stores'); },
168
+ * });
169
+ * return {
170
+ * load: () => db.get('stores', ctx.displayName),
171
+ * save: (state) => db.put('stores', state, ctx.displayName),
172
+ * };
131
173
  * },
132
174
  * });
133
175
  * ```
134
176
  *
135
- * @example Using meta in callbacks
177
+ * @example With shared debounce
136
178
  * ```ts
137
- * import { meta } from "storion";
179
+ * persistMiddleware({
180
+ * handler: (ctx) => {
181
+ * const key = `app:${ctx.displayName}`;
182
+ * const debouncedSave = debounce(
183
+ * (s) => localStorage.setItem(key, JSON.stringify(s)),
184
+ * 300
185
+ * );
186
+ * return {
187
+ * load: () => JSON.parse(localStorage.getItem(key) || 'null'),
188
+ * save: debouncedSave,
189
+ * };
190
+ * },
191
+ * });
192
+ * ```
138
193
  *
139
- * const persistKey = meta<string>();
194
+ * @example Multi-storage with meta
195
+ * ```ts
196
+ * const sessionStore = meta();
197
+ * const localStore = meta();
140
198
  *
199
+ * // Session storage middleware
141
200
  * persistMiddleware({
142
- * load: (ctx) => {
143
- * // Use custom key from meta, fallback to displayName
144
- * const customKey = ctx.meta(persistKey).store;
145
- * const key = customKey ?? ctx.spec.displayName;
146
- * return JSON.parse(localStorage.getItem(key) || 'null');
147
- * },
148
- * save: (ctx, state) => {
149
- * const customKey = ctx.meta(persistKey).store;
150
- * const key = customKey ?? ctx.spec.displayName;
151
- * localStorage.setItem(key, JSON.stringify(state));
201
+ * filter: ({ meta }) => meta.any(sessionStore),
202
+ * fields: ({ meta }) => meta.fields(sessionStore),
203
+ * handler: (ctx) => {
204
+ * const key = `session:${ctx.displayName}`;
205
+ * return {
206
+ * load: () => JSON.parse(sessionStorage.getItem(key) || 'null'),
207
+ * save: (state) => sessionStorage.setItem(key, JSON.stringify(state)),
208
+ * };
152
209
  * },
153
210
  * });
154
211
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/persist/persist.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAIxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,YAAY,sCAAS,CAAC;AAEnC;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACvB,IAAI,GACJ,SAAS,GACT,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC;IAEtD;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,MAAM,EAAE,CAAC;IAEvD;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,iBAAiB,CAAC;IAE9D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CACL,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,IAAI,CAAC;IAEV;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CACR,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,GAAG,MAAM,KACvB,IAAI,CAAC;IAEV;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,eAAe,CAyH1E"}
1
+ {"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/persist/persist.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,sBAAsB,EACtB,aAAa,EACd,MAAM,UAAU,CAAC;AAIlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,YAAY,sCAAS,CAAC;AAEnC;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACvB,IAAI,GACJ,SAAS,GACT,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAExD;;;GAGG;AACH,MAAM,WAAW,cAAe,SAAQ,sBAAsB;IAC5D,yCAAyC;IACzC,KAAK,EAAE,aAAa,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,iBAAiB,CAAC;IAE/B;;;;OAIG;IACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACjD;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC;IAEtD;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,MAAM,EAAE,CAAC;IAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,OAAO,EAAE,CACP,OAAO,EAAE,cAAc,KACpB,cAAc,GAAG,WAAW,CAAC,cAAc,CAAC,CAAC;IAElD;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAExE;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,eAAe,CAkJ1E"}
@@ -2,8 +2,8 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { memo, useRef, useMemo, createElement, createContext, useContext, useReducer, useEffect, useLayoutEffect, useState, forwardRef } from "react";
5
- import { c as container, i as isSpec, S as STORION_TYPE, r as resolveEquality, s as store } from "../index-BsdNVe8q.js";
6
- import { n, m, l, u, q, o, g, a, h, d, f, k, e, j, b, z, p, t, v, w, y, A, x } from "../index-BsdNVe8q.js";
5
+ import { c as container, i as isSpec, S as STORION_TYPE, r as resolveEquality, s as store } from "../index-C8B6Mo8r.js";
6
+ import { n, m, l, u, q, o, g, a, h, d, f, k, e, j, b, z, p, t, v, w, y, A, x } from "../index-C8B6Mo8r.js";
7
7
  import { P as ProviderMissingError, L as LocalStoreDependencyError, w as withHooks, A as AsyncFunctionError } from "../effect-C6h0PDDI.js";
8
8
  import { E, H, I, c, a as a2, d as d2, S, b as b2, e as e2, u as u2 } from "../effect-C6h0PDDI.js";
9
9
  import { jsx } from "react/jsx-runtime";
package/dist/storion.js CHANGED
@@ -1,4 +1,4 @@
1
- import { S, n, m, c, l, u, q, o, g, a, h, d, f, k, i, e, j, b, z, p, t, s, v, w, y, A, x } from "./index-BsdNVe8q.js";
1
+ import { S, n, m, c, l, u, q, o, g, a, h, d, f, k, i, e, j, b, z, p, t, s, v, w, y, A, x } from "./index-C8B6Mo8r.js";
2
2
  import { A as A2, E, H, I, c as c2, L, P, a as a2, d as d2, S as S2, b as b2, e as e2, u as u2 } from "./effect-C6h0PDDI.js";
3
3
  import { m as m2 } from "./meta-40r-AZfe.js";
4
4
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storion",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "Reactive stores for modern apps. Type-safe. Auto-tracked. Effortlessly composable",
5
5
  "type": "module",
6
6
  "main": "./dist/storion.js",