@bquery/bquery 1.7.0 → 1.8.2

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 (262) hide show
  1. package/README.md +760 -716
  2. package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
  3. package/dist/a11y-DVBCy09c.js.map +1 -0
  4. package/dist/a11y.es.mjs +1 -1
  5. package/dist/component/library.d.ts.map +1 -1
  6. package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
  7. package/dist/component-L3-JfOFz.js.map +1 -0
  8. package/dist/component.es.mjs +1 -1
  9. package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
  10. package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
  11. package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
  12. package/dist/constraints-D5RHQLmP.js.map +1 -0
  13. package/dist/core/collection.d.ts +86 -0
  14. package/dist/core/collection.d.ts.map +1 -1
  15. package/dist/core/element.d.ts +28 -0
  16. package/dist/core/element.d.ts.map +1 -1
  17. package/dist/core/shared.d.ts +6 -0
  18. package/dist/core/shared.d.ts.map +1 -1
  19. package/dist/core-DdtZHzsS.js +168 -0
  20. package/dist/core-DdtZHzsS.js.map +1 -0
  21. package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
  22. package/dist/core-EMYSLzaT.js.map +1 -0
  23. package/dist/core.es.mjs +48 -47
  24. package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
  25. package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
  26. package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
  27. package/dist/devtools-BhB2iDPT.js.map +1 -0
  28. package/dist/devtools.es.mjs +1 -1
  29. package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
  30. package/dist/dnd-NwZBYh4l.js.map +1 -0
  31. package/dist/dnd.es.mjs +1 -1
  32. package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
  33. package/dist/env-CTdvLaH2.js.map +1 -0
  34. package/dist/forms/create-form.d.ts.map +1 -1
  35. package/dist/forms/index.d.ts +3 -2
  36. package/dist/forms/index.d.ts.map +1 -1
  37. package/dist/forms/types.d.ts +46 -0
  38. package/dist/forms/types.d.ts.map +1 -1
  39. package/dist/forms/use-field.d.ts +34 -0
  40. package/dist/forms/use-field.d.ts.map +1 -0
  41. package/dist/forms/validators.d.ts +25 -0
  42. package/dist/forms/validators.d.ts.map +1 -1
  43. package/dist/forms-UcRHsYxC.js +227 -0
  44. package/dist/forms-UcRHsYxC.js.map +1 -0
  45. package/dist/forms.es.mjs +14 -12
  46. package/dist/full.d.ts +17 -26
  47. package/dist/full.d.ts.map +1 -1
  48. package/dist/full.es.mjs +206 -181
  49. package/dist/full.iife.js +33 -33
  50. package/dist/full.iife.js.map +1 -1
  51. package/dist/full.umd.js +33 -33
  52. package/dist/full.umd.js.map +1 -1
  53. package/dist/function-Cybd57JV.js +33 -0
  54. package/dist/function-Cybd57JV.js.map +1 -0
  55. package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
  56. package/dist/i18n-kuF6Ekj6.js.map +1 -0
  57. package/dist/i18n.es.mjs +1 -1
  58. package/dist/index.es.mjs +251 -228
  59. package/dist/media/breakpoints.d.ts.map +1 -1
  60. package/dist/media/types.d.ts +2 -2
  61. package/dist/media/types.d.ts.map +1 -1
  62. package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
  63. package/dist/media-i-fB5WxI.js.map +1 -0
  64. package/dist/media.es.mjs +1 -1
  65. package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
  66. package/dist/motion-BJsAuULb.js.map +1 -0
  67. package/dist/motion.es.mjs +1 -1
  68. package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
  69. package/dist/mount-B4Y8bk8Z.js.map +1 -0
  70. package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
  71. package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
  72. package/dist/platform.es.mjs +2 -2
  73. package/dist/plugin/registry.d.ts.map +1 -1
  74. package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
  75. package/dist/plugin-C2WuC8SF.js.map +1 -0
  76. package/dist/plugin.es.mjs +1 -1
  77. package/dist/reactive/async-data.d.ts +28 -3
  78. package/dist/reactive/async-data.d.ts.map +1 -1
  79. package/dist/reactive/computed.d.ts +3 -0
  80. package/dist/reactive/computed.d.ts.map +1 -1
  81. package/dist/reactive/effect.d.ts +3 -0
  82. package/dist/reactive/effect.d.ts.map +1 -1
  83. package/dist/reactive/http.d.ts +194 -0
  84. package/dist/reactive/http.d.ts.map +1 -0
  85. package/dist/reactive/index.d.ts +2 -2
  86. package/dist/reactive/index.d.ts.map +1 -1
  87. package/dist/reactive/pagination.d.ts +126 -0
  88. package/dist/reactive/pagination.d.ts.map +1 -0
  89. package/dist/reactive/polling.d.ts +55 -0
  90. package/dist/reactive/polling.d.ts.map +1 -0
  91. package/dist/reactive/readonly.d.ts +20 -1
  92. package/dist/reactive/readonly.d.ts.map +1 -1
  93. package/dist/reactive/rest.d.ts +293 -0
  94. package/dist/reactive/rest.d.ts.map +1 -0
  95. package/dist/reactive/scope.d.ts +140 -0
  96. package/dist/reactive/scope.d.ts.map +1 -0
  97. package/dist/reactive/signal.d.ts +16 -2
  98. package/dist/reactive/signal.d.ts.map +1 -1
  99. package/dist/reactive/to-value.d.ts +57 -0
  100. package/dist/reactive/to-value.d.ts.map +1 -0
  101. package/dist/reactive/websocket.d.ts +285 -0
  102. package/dist/reactive/websocket.d.ts.map +1 -0
  103. package/dist/reactive-DwkhUJfP.js +1148 -0
  104. package/dist/reactive-DwkhUJfP.js.map +1 -0
  105. package/dist/reactive.es.mjs +38 -19
  106. package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
  107. package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
  108. package/dist/router/constraints.d.ts.map +1 -1
  109. package/dist/router/index.d.ts +1 -1
  110. package/dist/router/index.d.ts.map +1 -1
  111. package/dist/router/router.d.ts.map +1 -1
  112. package/dist/router/state.d.ts +25 -2
  113. package/dist/router/state.d.ts.map +1 -1
  114. package/dist/router-CQikC9Ed.js +492 -0
  115. package/dist/router-CQikC9Ed.js.map +1 -0
  116. package/dist/router.es.mjs +9 -8
  117. package/dist/ssr/hydrate.d.ts.map +1 -1
  118. package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
  119. package/dist/ssr-_dAcGdzu.js.map +1 -0
  120. package/dist/ssr.es.mjs +1 -1
  121. package/dist/store/persisted.d.ts.map +1 -1
  122. package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
  123. package/dist/store-Cb3gPRve.js.map +1 -0
  124. package/dist/store.es.mjs +2 -2
  125. package/dist/storybook.es.mjs.map +1 -1
  126. package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
  127. package/dist/testing-C5Sjfsna.js.map +1 -0
  128. package/dist/testing.es.mjs +1 -1
  129. package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
  130. package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
  131. package/dist/untrack-D0fnO5k2.js +36 -0
  132. package/dist/untrack-D0fnO5k2.js.map +1 -0
  133. package/dist/view/custom-directives.d.ts.map +1 -1
  134. package/dist/view.es.mjs +4 -4
  135. package/package.json +178 -177
  136. package/src/a11y/announce.ts +131 -131
  137. package/src/a11y/audit.ts +314 -314
  138. package/src/a11y/index.ts +68 -68
  139. package/src/a11y/media-preferences.ts +255 -255
  140. package/src/a11y/roving-tab-index.ts +164 -164
  141. package/src/a11y/skip-link.ts +255 -255
  142. package/src/a11y/trap-focus.ts +184 -184
  143. package/src/a11y/types.ts +183 -183
  144. package/src/component/component.ts +599 -599
  145. package/src/component/html.ts +153 -153
  146. package/src/component/index.ts +52 -52
  147. package/src/component/library.ts +540 -542
  148. package/src/component/scope.ts +212 -212
  149. package/src/component/types.ts +310 -310
  150. package/src/core/collection.ts +876 -707
  151. package/src/core/element.ts +1015 -981
  152. package/src/core/env.ts +60 -60
  153. package/src/core/index.ts +49 -49
  154. package/src/core/shared.ts +77 -62
  155. package/src/core/utils/index.ts +148 -148
  156. package/src/devtools/devtools.ts +410 -410
  157. package/src/devtools/index.ts +48 -48
  158. package/src/devtools/types.ts +104 -104
  159. package/src/dnd/draggable.ts +296 -296
  160. package/src/dnd/droppable.ts +228 -228
  161. package/src/dnd/index.ts +62 -62
  162. package/src/dnd/sortable.ts +307 -307
  163. package/src/dnd/types.ts +293 -293
  164. package/src/forms/create-form.ts +320 -278
  165. package/src/forms/index.ts +70 -65
  166. package/src/forms/types.ts +203 -154
  167. package/src/forms/use-field.ts +231 -0
  168. package/src/forms/validators.ts +294 -265
  169. package/src/full.ts +554 -480
  170. package/src/i18n/formatting.ts +67 -67
  171. package/src/i18n/i18n.ts +200 -200
  172. package/src/i18n/index.ts +67 -67
  173. package/src/i18n/translate.ts +182 -182
  174. package/src/i18n/types.ts +171 -171
  175. package/src/index.ts +108 -108
  176. package/src/media/battery.ts +116 -116
  177. package/src/media/breakpoints.ts +129 -131
  178. package/src/media/clipboard.ts +80 -80
  179. package/src/media/device-sensors.ts +158 -158
  180. package/src/media/geolocation.ts +119 -119
  181. package/src/media/index.ts +76 -76
  182. package/src/media/media-query.ts +92 -92
  183. package/src/media/network.ts +115 -115
  184. package/src/media/types.ts +177 -177
  185. package/src/media/viewport.ts +84 -84
  186. package/src/motion/index.ts +57 -57
  187. package/src/motion/morph.ts +151 -151
  188. package/src/motion/parallax.ts +120 -120
  189. package/src/motion/reduced-motion.ts +66 -66
  190. package/src/motion/types.ts +271 -271
  191. package/src/motion/typewriter.ts +164 -164
  192. package/src/plugin/index.ts +37 -37
  193. package/src/plugin/registry.ts +284 -269
  194. package/src/plugin/types.ts +137 -137
  195. package/src/reactive/async-data.ts +250 -29
  196. package/src/reactive/computed.ts +144 -130
  197. package/src/reactive/effect.ts +29 -6
  198. package/src/reactive/http.ts +790 -0
  199. package/src/reactive/index.ts +60 -0
  200. package/src/reactive/pagination.ts +317 -0
  201. package/src/reactive/polling.ts +179 -0
  202. package/src/reactive/readonly.ts +52 -8
  203. package/src/reactive/rest.ts +859 -0
  204. package/src/reactive/scope.ts +276 -0
  205. package/src/reactive/signal.ts +61 -1
  206. package/src/reactive/to-value.ts +71 -0
  207. package/src/reactive/websocket.ts +849 -0
  208. package/src/router/bq-link.ts +279 -279
  209. package/src/router/constraints.ts +204 -201
  210. package/src/router/index.ts +49 -49
  211. package/src/router/match.ts +312 -312
  212. package/src/router/path-pattern.ts +52 -52
  213. package/src/router/query.ts +38 -38
  214. package/src/router/router.ts +421 -402
  215. package/src/router/state.ts +51 -3
  216. package/src/router/types.ts +139 -139
  217. package/src/router/use-route.ts +68 -68
  218. package/src/router/utils.ts +157 -157
  219. package/src/security/index.ts +12 -12
  220. package/src/ssr/hydrate.ts +84 -82
  221. package/src/ssr/index.ts +70 -70
  222. package/src/ssr/render.ts +508 -508
  223. package/src/ssr/serialize.ts +296 -296
  224. package/src/ssr/types.ts +81 -81
  225. package/src/store/create-store.ts +467 -467
  226. package/src/store/index.ts +27 -27
  227. package/src/store/persisted.ts +245 -249
  228. package/src/store/types.ts +247 -247
  229. package/src/store/utils.ts +135 -135
  230. package/src/storybook/index.ts +480 -480
  231. package/src/testing/index.ts +42 -42
  232. package/src/testing/testing.ts +593 -593
  233. package/src/testing/types.ts +170 -170
  234. package/src/view/custom-directives.ts +28 -30
  235. package/src/view/evaluate.ts +292 -292
  236. package/src/view/process.ts +108 -108
  237. package/dist/a11y-C5QOVvRn.js.map +0 -1
  238. package/dist/component-CuuTijA6.js.map +0 -1
  239. package/dist/constraints-3lV9yyBw.js.map +0 -1
  240. package/dist/core-Cjl7GUu8.js.map +0 -1
  241. package/dist/core-DnlyjbF2.js +0 -112
  242. package/dist/core-DnlyjbF2.js.map +0 -1
  243. package/dist/custom-directives-7wAShnnd.js.map +0 -1
  244. package/dist/devtools-D2fQLhDN.js.map +0 -1
  245. package/dist/dnd-B8EgyzaI.js.map +0 -1
  246. package/dist/env-NeVmr4Gf.js.map +0 -1
  247. package/dist/forms-C3yovgH9.js +0 -141
  248. package/dist/forms-C3yovgH9.js.map +0 -1
  249. package/dist/i18n-BnnhTFOS.js.map +0 -1
  250. package/dist/media-Di2Ta22s.js.map +0 -1
  251. package/dist/motion-qPj_TYGv.js.map +0 -1
  252. package/dist/mount-SM07RUa6.js.map +0 -1
  253. package/dist/plugin-cPoOHFLY.js.map +0 -1
  254. package/dist/reactive-Cfv0RK6x.js +0 -233
  255. package/dist/reactive-Cfv0RK6x.js.map +0 -1
  256. package/dist/router-BrthaP_z.js +0 -473
  257. package/dist/router-BrthaP_z.js.map +0 -1
  258. package/dist/ssr-B2qd_WBB.js.map +0 -1
  259. package/dist/store-DWpyH6p5.js.map +0 -1
  260. package/dist/testing-CsqjNUyy.js.map +0 -1
  261. package/dist/untrack-DJVQQ2WM.js +0 -33
  262. package/dist/untrack-DJVQQ2WM.js.map +0 -1
@@ -1,27 +1,27 @@
1
- /**
2
- * Store module exports.
3
- */
4
-
5
- export type {
6
- ActionContext,
7
- Actions,
8
- Getters,
9
- OnActionCallback,
10
- PersistedStoreOptions,
11
- StateFactory,
12
- StorageBackend,
13
- Store,
14
- StoreDefinition,
15
- StorePatch,
16
- StorePlugin,
17
- StoreSerializer,
18
- StoreSubscriber,
19
- } from './types';
20
-
21
- export { createStore } from './create-store';
22
- export { defineStore } from './define-store';
23
- export { mapActions, mapGetters, mapState } from './mapping';
24
- export { createPersistedStore } from './persisted';
25
- export { registerPlugin } from './plugins';
26
- export { destroyStore, getStore, listStores } from './registry';
27
- export { watchStore } from './watch';
1
+ /**
2
+ * Store module exports.
3
+ */
4
+
5
+ export type {
6
+ ActionContext,
7
+ Actions,
8
+ Getters,
9
+ OnActionCallback,
10
+ PersistedStoreOptions,
11
+ StateFactory,
12
+ StorageBackend,
13
+ Store,
14
+ StoreDefinition,
15
+ StorePatch,
16
+ StorePlugin,
17
+ StoreSerializer,
18
+ StoreSubscriber,
19
+ } from './types';
20
+
21
+ export { createStore } from './create-store';
22
+ export { defineStore } from './define-store';
23
+ export { mapActions, mapGetters, mapState } from './mapping';
24
+ export { createPersistedStore } from './persisted';
25
+ export { registerPlugin } from './plugins';
26
+ export { destroyStore, getStore, listStores } from './registry';
27
+ export { watchStore } from './watch';
@@ -1,249 +1,245 @@
1
- /**
2
- * Store persistence helpers.
3
- */
4
-
5
- import { isPrototypePollutionKey } from '../core/utils/object';
6
- import { createStore } from './create-store';
7
- import { isDev } from './utils';
8
- import type { PersistedStoreOptions, StorageBackend, Store, StoreDefinition } from './types';
9
-
10
- /** @internal Version key suffix */
11
- const VERSION_SUFFIX = '__version';
12
-
13
- /** @internal Default JSON serializer */
14
- const defaultSerializer = {
15
- serialize: (state: unknown) => JSON.stringify(state),
16
- deserialize: (raw: string) => JSON.parse(raw) as unknown,
17
- };
18
-
19
- /** @internal Check whether a value can be merged into store state. */
20
- const isPersistedStateObject = (value: unknown): value is Record<string, unknown> => {
21
- if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
22
-
23
- const prototype = Object.getPrototypeOf(value);
24
- return prototype === null || Object.getPrototypeOf(prototype) === null;
25
- };
26
-
27
- /**
28
- * Applies persisted state onto the default state while ignoring dangerous
29
- * prototype-pollution keys such as `__proto__`, `constructor`, and `prototype`.
30
- *
31
- * @internal
32
- */
33
- const mergePersistedState = <S extends Record<string, unknown>>(
34
- defaultState: S,
35
- persisted: Record<string, unknown>
36
- ): S => {
37
- const merged = { ...defaultState };
38
- for (const [key, value] of Object.entries(persisted)) {
39
- if (isPrototypePollutionKey(key)) continue;
40
- if (!Object.prototype.hasOwnProperty.call(defaultState, key)) continue;
41
- merged[key as keyof S] = value as S[keyof S];
42
- }
43
- return merged;
44
- };
45
-
46
- /** @internal Resolve the default storage backend safely. */
47
- const getDefaultStorage = (): StorageBackend | undefined => {
48
- try {
49
- return globalThis.localStorage;
50
- } catch {
51
- return undefined;
52
- }
53
- };
54
-
55
- /**
56
- * Creates a store with automatic persistence.
57
- *
58
- * Supports configurable storage backends, custom serializers, and schema
59
- * versioning with migration functions. All options are optional and
60
- * backward-compatible with the simple `(definition, storageKey?)` signature.
61
- *
62
- * @param definition - Store definition
63
- * @param options - Persistence options or a plain string storage key for backward compatibility
64
- * @returns The reactive store instance
65
- *
66
- * @example Basic usage (localStorage + JSON)
67
- * ```ts
68
- * const store = createPersistedStore({
69
- * id: 'settings',
70
- * state: () => ({ theme: 'dark' }),
71
- * });
72
- * ```
73
- *
74
- * @example With sessionStorage and custom key
75
- * ```ts
76
- * const store = createPersistedStore(
77
- * { id: 'session', state: () => ({ token: '' }) },
78
- * { key: 'my-session', storage: sessionStorage },
79
- * );
80
- * ```
81
- *
82
- * @example With versioning and migration
83
- * ```ts
84
- * const store = createPersistedStore(
85
- * { id: 'app', state: () => ({ name: '', theme: 'auto' }) },
86
- * {
87
- * version: 2,
88
- * migrate: (old, v) => {
89
- * if (v < 2) return { ...old, theme: 'auto' };
90
- * return old;
91
- * },
92
- * },
93
- * );
94
- * ```
95
- */
96
- export const createPersistedStore = <
97
- S extends Record<string, unknown>,
98
- G extends Record<string, unknown> = Record<string, never>,
99
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- actions may declare specific parameter types
100
- A extends Record<string, (...args: any[]) => any> = Record<string, never>,
101
- >(
102
- definition: StoreDefinition<S, G, A>,
103
- options?: PersistedStoreOptions | string
104
- ): Store<S, G, A> => {
105
- // Normalize options — a plain string is treated as the storage key for backward compatibility
106
- const opts: PersistedStoreOptions =
107
- typeof options === 'string' ? { key: options } : (options ?? {});
108
-
109
- const key = opts.key ?? `bquery-store-${definition.id}`;
110
- const storage = opts.storage ?? getDefaultStorage();
111
- const serializer = opts.serializer ?? defaultSerializer;
112
- const version = opts.version;
113
- const migrate = opts.migrate;
114
- const versionKey = key + VERSION_SUFFIX;
115
- let shouldPersistInitialVersion = storage !== undefined && version !== undefined;
116
- let pendingVersionWrite = false;
117
- let canRetryPendingVersionAfterCreate = false;
118
-
119
- const tryPersistVersion = (warningMessage?: string): boolean => {
120
- if (!storage || version === undefined) return false;
121
-
122
- try {
123
- storage.setItem(versionKey, String(version));
124
- return true;
125
- } catch (error) {
126
- if (
127
- warningMessage &&
128
- isDev() &&
129
- typeof console !== 'undefined' &&
130
- typeof console.warn === 'function'
131
- ) {
132
- console.warn(warningMessage, error);
133
- }
134
- return false;
135
- }
136
- };
137
-
138
- const originalStateFactory = definition.state;
139
-
140
- const wrappedDefinition: StoreDefinition<S, G, A> = {
141
- ...definition,
142
- state: () => {
143
- const defaultState = originalStateFactory();
144
-
145
- if (!storage) return defaultState;
146
-
147
- try {
148
- const saved = storage.getItem(key);
149
- if (!saved) return defaultState;
150
-
151
- const deserialized = serializer.deserialize(saved);
152
- if (!isPersistedStateObject(deserialized)) {
153
- return defaultState;
154
- }
155
-
156
- let persisted = deserialized;
157
-
158
- // Handle versioning & migration
159
- if (version !== undefined && migrate) {
160
- const rawVersion = storage.getItem(versionKey);
161
- const parsedVersion = rawVersion !== null ? Number(rawVersion) : 0;
162
- const oldVersion = Number.isFinite(parsedVersion) ? parsedVersion : 0;
163
-
164
- if (oldVersion !== version) {
165
- shouldPersistInitialVersion = false;
166
- pendingVersionWrite = true;
167
- const migrated = migrate(persisted, oldVersion);
168
- if (!isPersistedStateObject(migrated)) {
169
- return defaultState;
170
- }
171
- persisted = migrated;
172
-
173
- let migratedStatePersisted = false;
174
- // Save the migrated state and version immediately when possible.
175
- // If the state write fails, never advance the version key.
176
- try {
177
- storage.setItem(key, serializer.serialize(persisted));
178
- migratedStatePersisted = true;
179
- canRetryPendingVersionAfterCreate = true;
180
- } catch (e) {
181
- // Migration will re-run on next load, but state is still usable
182
- if (
183
- isDev() &&
184
- typeof console !== 'undefined' &&
185
- typeof console.warn === 'function'
186
- ) {
187
- console.warn(
188
- `[bQuery store "${definition.id}"] Failed to persist migrated state:`,
189
- e
190
- );
191
- }
192
- }
193
-
194
- if (
195
- migratedStatePersisted &&
196
- tryPersistVersion(
197
- `[bQuery store "${definition.id}"] Failed to persist migrated version:`
198
- )
199
- ) {
200
- pendingVersionWrite = false;
201
- }
202
- } else {
203
- shouldPersistInitialVersion = false;
204
- }
205
- }
206
-
207
- return mergePersistedState(defaultState, persisted);
208
- } catch {
209
- // Ignore parse errors
210
- return defaultState;
211
- }
212
- },
213
- };
214
-
215
- const store = createStore(wrappedDefinition);
216
-
217
- // Persist the version number on first creation
218
- if (shouldPersistInitialVersion && storage) {
219
- tryPersistVersion();
220
- } else if (
221
- pendingVersionWrite &&
222
- canRetryPendingVersionAfterCreate &&
223
- tryPersistVersion(
224
- `[bQuery store "${definition.id}"] Failed to persist migrated version after store creation:`
225
- )
226
- ) {
227
- pendingVersionWrite = false;
228
- }
229
-
230
- // Subscribe to save changes
231
- store.$subscribe((state) => {
232
- if (!storage) return;
233
- try {
234
- storage.setItem(key, serializer.serialize(state));
235
- if (
236
- pendingVersionWrite &&
237
- tryPersistVersion(
238
- `[bQuery store "${definition.id}"] Failed to persist migrated version after a successful state write:`
239
- )
240
- ) {
241
- pendingVersionWrite = false;
242
- }
243
- } catch {
244
- // Ignore quota errors
245
- }
246
- });
247
-
248
- return store;
249
- };
1
+ /**
2
+ * Store persistence helpers.
3
+ */
4
+
5
+ import { isPrototypePollutionKey } from '../core/utils/object';
6
+ import { createStore } from './create-store';
7
+ import { isDev } from './utils';
8
+ import type { PersistedStoreOptions, StorageBackend, Store, StoreDefinition } from './types';
9
+
10
+ /** @internal Version key suffix */
11
+ const VERSION_SUFFIX = '__version';
12
+
13
+ /** @internal Default JSON serializer */
14
+ const defaultSerializer = {
15
+ serialize: (state: unknown) => JSON.stringify(state),
16
+ deserialize: (raw: string) => JSON.parse(raw) as unknown,
17
+ };
18
+
19
+ /** @internal Check whether a value can be merged into store state. */
20
+ const isPersistedStateObject = (value: unknown): value is Record<string, unknown> => {
21
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
22
+
23
+ const prototype = Object.getPrototypeOf(value);
24
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
25
+ };
26
+
27
+ /**
28
+ * Applies persisted state onto the default state while ignoring dangerous
29
+ * prototype-pollution keys such as `__proto__`, `constructor`, and `prototype`.
30
+ *
31
+ * @internal
32
+ */
33
+ const mergePersistedState = <S extends Record<string, unknown>>(
34
+ defaultState: S,
35
+ persisted: Record<string, unknown>
36
+ ): S => {
37
+ const merged = { ...defaultState };
38
+ for (const [key, value] of Object.entries(persisted)) {
39
+ if (isPrototypePollutionKey(key)) continue;
40
+ if (!Object.prototype.hasOwnProperty.call(defaultState, key)) continue;
41
+ merged[key as keyof S] = value as S[keyof S];
42
+ }
43
+ return merged;
44
+ };
45
+
46
+ /** @internal Resolve the default storage backend safely. */
47
+ const getDefaultStorage = (): StorageBackend | undefined => {
48
+ try {
49
+ return globalThis.localStorage;
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ };
54
+
55
+ /**
56
+ * Creates a store with automatic persistence.
57
+ *
58
+ * Supports configurable storage backends, custom serializers, and schema
59
+ * versioning with migration functions. All options are optional and
60
+ * backward-compatible with the simple `(definition, storageKey?)` signature.
61
+ *
62
+ * @param definition - Store definition
63
+ * @param options - Persistence options or a plain string storage key for backward compatibility
64
+ * @returns The reactive store instance
65
+ *
66
+ * @example Basic usage (localStorage + JSON)
67
+ * ```ts
68
+ * const store = createPersistedStore({
69
+ * id: 'settings',
70
+ * state: () => ({ theme: 'dark' }),
71
+ * });
72
+ * ```
73
+ *
74
+ * @example With sessionStorage and custom key
75
+ * ```ts
76
+ * const store = createPersistedStore(
77
+ * { id: 'session', state: () => ({ token: '' }) },
78
+ * { key: 'my-session', storage: sessionStorage },
79
+ * );
80
+ * ```
81
+ *
82
+ * @example With versioning and migration
83
+ * ```ts
84
+ * const store = createPersistedStore(
85
+ * { id: 'app', state: () => ({ name: '', theme: 'auto' }) },
86
+ * {
87
+ * version: 2,
88
+ * migrate: (old, v) => {
89
+ * if (v < 2) return { ...old, theme: 'auto' };
90
+ * return old;
91
+ * },
92
+ * },
93
+ * );
94
+ * ```
95
+ */
96
+ export const createPersistedStore = <
97
+ S extends Record<string, unknown>,
98
+ G extends Record<string, unknown> = Record<string, never>,
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- actions may declare specific parameter types
100
+ A extends Record<string, (...args: any[]) => any> = Record<string, never>,
101
+ >(
102
+ definition: StoreDefinition<S, G, A>,
103
+ options?: PersistedStoreOptions | string
104
+ ): Store<S, G, A> => {
105
+ // Normalize options — a plain string is treated as the storage key for backward compatibility
106
+ const opts: PersistedStoreOptions =
107
+ typeof options === 'string' ? { key: options } : (options ?? {});
108
+
109
+ const key = opts.key ?? `bquery-store-${definition.id}`;
110
+ const storage = opts.storage ?? getDefaultStorage();
111
+ const serializer = opts.serializer ?? defaultSerializer;
112
+ const version = opts.version;
113
+ const migrate = opts.migrate;
114
+ const versionKey = key + VERSION_SUFFIX;
115
+ let shouldPersistInitialVersion = storage !== undefined && version !== undefined;
116
+ let pendingVersionWrite = false;
117
+ let canRetryPendingVersionAfterCreate = false;
118
+
119
+ const tryPersistVersion = (warningMessage?: string): boolean => {
120
+ if (!storage || version === undefined) return false;
121
+
122
+ try {
123
+ storage.setItem(versionKey, String(version));
124
+ return true;
125
+ } catch (error) {
126
+ if (
127
+ warningMessage &&
128
+ isDev() &&
129
+ typeof console !== 'undefined' &&
130
+ typeof console.warn === 'function'
131
+ ) {
132
+ console.warn(warningMessage, error);
133
+ }
134
+ return false;
135
+ }
136
+ };
137
+
138
+ const originalStateFactory = definition.state;
139
+
140
+ const wrappedDefinition: StoreDefinition<S, G, A> = {
141
+ ...definition,
142
+ state: () => {
143
+ const defaultState = originalStateFactory();
144
+
145
+ if (!storage) return defaultState;
146
+
147
+ try {
148
+ const saved = storage.getItem(key);
149
+ if (!saved) return defaultState;
150
+
151
+ const deserialized = serializer.deserialize(saved);
152
+ if (!isPersistedStateObject(deserialized)) {
153
+ return defaultState;
154
+ }
155
+
156
+ let persisted = deserialized;
157
+
158
+ // Handle versioning & migration
159
+ if (version !== undefined && migrate) {
160
+ const rawVersion = storage.getItem(versionKey);
161
+ const parsedVersion = rawVersion !== null ? Number(rawVersion) : 0;
162
+ const oldVersion = Number.isFinite(parsedVersion) ? parsedVersion : 0;
163
+
164
+ if (oldVersion !== version) {
165
+ shouldPersistInitialVersion = false;
166
+ pendingVersionWrite = true;
167
+ const migrated = migrate(persisted, oldVersion);
168
+ if (!isPersistedStateObject(migrated)) {
169
+ return defaultState;
170
+ }
171
+ persisted = migrated;
172
+
173
+ let migratedStatePersisted = false;
174
+ // Save the migrated state and version immediately when possible.
175
+ // If the state write fails, never advance the version key.
176
+ try {
177
+ storage.setItem(key, serializer.serialize(persisted));
178
+ migratedStatePersisted = true;
179
+ canRetryPendingVersionAfterCreate = true;
180
+ } catch (e) {
181
+ // Migration will re-run on next load, but state is still usable
182
+ if (isDev() && typeof console !== 'undefined' && typeof console.warn === 'function') {
183
+ console.warn(
184
+ `[bQuery store "${definition.id}"] Failed to persist migrated state:`,
185
+ e
186
+ );
187
+ }
188
+ }
189
+
190
+ if (
191
+ migratedStatePersisted &&
192
+ tryPersistVersion(
193
+ `[bQuery store "${definition.id}"] Failed to persist migrated version:`
194
+ )
195
+ ) {
196
+ pendingVersionWrite = false;
197
+ }
198
+ } else {
199
+ shouldPersistInitialVersion = false;
200
+ }
201
+ }
202
+
203
+ return mergePersistedState(defaultState, persisted);
204
+ } catch {
205
+ // Ignore parse errors
206
+ return defaultState;
207
+ }
208
+ },
209
+ };
210
+
211
+ const store = createStore(wrappedDefinition);
212
+
213
+ // Persist the version number on first creation
214
+ if (shouldPersistInitialVersion && storage) {
215
+ tryPersistVersion();
216
+ } else if (
217
+ pendingVersionWrite &&
218
+ canRetryPendingVersionAfterCreate &&
219
+ tryPersistVersion(
220
+ `[bQuery store "${definition.id}"] Failed to persist migrated version after store creation:`
221
+ )
222
+ ) {
223
+ pendingVersionWrite = false;
224
+ }
225
+
226
+ // Subscribe to save changes
227
+ store.$subscribe((state) => {
228
+ if (!storage) return;
229
+ try {
230
+ storage.setItem(key, serializer.serialize(state));
231
+ if (
232
+ pendingVersionWrite &&
233
+ tryPersistVersion(
234
+ `[bQuery store "${definition.id}"] Failed to persist migrated version after a successful state write:`
235
+ )
236
+ ) {
237
+ pendingVersionWrite = false;
238
+ }
239
+ } catch {
240
+ // Ignore quota errors
241
+ }
242
+ });
243
+
244
+ return store;
245
+ };