@bquery/bquery 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/README.md +501 -323
  2. package/dist/batch-4LAvfLE7.js +13 -0
  3. package/dist/batch-4LAvfLE7.js.map +1 -0
  4. package/dist/component/component.d.ts +69 -0
  5. package/dist/component/component.d.ts.map +1 -0
  6. package/dist/component/html.d.ts +35 -0
  7. package/dist/component/html.d.ts.map +1 -0
  8. package/dist/component/index.d.ts +3 -126
  9. package/dist/component/index.d.ts.map +1 -1
  10. package/dist/component/props.d.ts +18 -0
  11. package/dist/component/props.d.ts.map +1 -0
  12. package/dist/component/types.d.ts +77 -0
  13. package/dist/component/types.d.ts.map +1 -0
  14. package/dist/component.es.mjs +90 -59
  15. package/dist/component.es.mjs.map +1 -1
  16. package/dist/core/collection.d.ts +36 -0
  17. package/dist/core/collection.d.ts.map +1 -1
  18. package/dist/core/dom.d.ts +6 -0
  19. package/dist/core/dom.d.ts.map +1 -0
  20. package/dist/core/element.d.ts +8 -0
  21. package/dist/core/element.d.ts.map +1 -1
  22. package/dist/core/index.d.ts +1 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/utils/array.d.ts +74 -0
  25. package/dist/core/utils/array.d.ts.map +1 -0
  26. package/dist/core/utils/function.d.ts +70 -0
  27. package/dist/core/utils/function.d.ts.map +1 -0
  28. package/dist/core/utils/index.d.ts +70 -0
  29. package/dist/core/utils/index.d.ts.map +1 -0
  30. package/dist/core/utils/misc.d.ts +63 -0
  31. package/dist/core/utils/misc.d.ts.map +1 -0
  32. package/dist/core/utils/number.d.ts +65 -0
  33. package/dist/core/utils/number.d.ts.map +1 -0
  34. package/dist/core/utils/object.d.ts +133 -0
  35. package/dist/core/utils/object.d.ts.map +1 -0
  36. package/dist/core/utils/string.d.ts +80 -0
  37. package/dist/core/utils/string.d.ts.map +1 -0
  38. package/dist/core/utils/type-guards.d.ts +79 -0
  39. package/dist/core/utils/type-guards.d.ts.map +1 -0
  40. package/dist/core-COenAZjD.js +145 -0
  41. package/dist/core-COenAZjD.js.map +1 -0
  42. package/dist/core.es.mjs +411 -448
  43. package/dist/core.es.mjs.map +1 -1
  44. package/dist/full.d.ts +8 -2
  45. package/dist/full.d.ts.map +1 -1
  46. package/dist/full.es.mjs +86 -40
  47. package/dist/full.es.mjs.map +1 -1
  48. package/dist/full.iife.js +6 -1
  49. package/dist/full.iife.js.map +1 -1
  50. package/dist/full.umd.js +6 -1
  51. package/dist/full.umd.js.map +1 -1
  52. package/dist/index.d.ts +3 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.es.mjs +137 -44
  55. package/dist/index.es.mjs.map +1 -1
  56. package/dist/motion/animate.d.ts +25 -0
  57. package/dist/motion/animate.d.ts.map +1 -0
  58. package/dist/motion/easing.d.ts +30 -0
  59. package/dist/motion/easing.d.ts.map +1 -0
  60. package/dist/motion/flip.d.ts +55 -0
  61. package/dist/motion/flip.d.ts.map +1 -0
  62. package/dist/motion/index.d.ts +11 -138
  63. package/dist/motion/index.d.ts.map +1 -1
  64. package/dist/motion/keyframes.d.ts +21 -0
  65. package/dist/motion/keyframes.d.ts.map +1 -0
  66. package/dist/motion/reduced-motion.d.ts +12 -0
  67. package/dist/motion/reduced-motion.d.ts.map +1 -0
  68. package/dist/motion/scroll.d.ts +15 -0
  69. package/dist/motion/scroll.d.ts.map +1 -0
  70. package/dist/motion/spring.d.ts +42 -0
  71. package/dist/motion/spring.d.ts.map +1 -0
  72. package/dist/motion/stagger.d.ts +22 -0
  73. package/dist/motion/stagger.d.ts.map +1 -0
  74. package/dist/motion/timeline.d.ts +21 -0
  75. package/dist/motion/timeline.d.ts.map +1 -0
  76. package/dist/motion/transition.d.ts +22 -0
  77. package/dist/motion/transition.d.ts.map +1 -0
  78. package/dist/motion/types.d.ts +182 -0
  79. package/dist/motion/types.d.ts.map +1 -0
  80. package/dist/motion.es.mjs +320 -61
  81. package/dist/motion.es.mjs.map +1 -1
  82. package/dist/persisted-Dz_ryNuC.js +278 -0
  83. package/dist/persisted-Dz_ryNuC.js.map +1 -0
  84. package/dist/reactive/batch.d.ts +13 -0
  85. package/dist/reactive/batch.d.ts.map +1 -0
  86. package/dist/reactive/computed.d.ts +50 -0
  87. package/dist/reactive/computed.d.ts.map +1 -0
  88. package/dist/reactive/core.d.ts +60 -0
  89. package/dist/reactive/core.d.ts.map +1 -0
  90. package/dist/reactive/effect.d.ts +15 -0
  91. package/dist/reactive/effect.d.ts.map +1 -0
  92. package/dist/reactive/index.d.ts +2 -2
  93. package/dist/reactive/index.d.ts.map +1 -1
  94. package/dist/reactive/internals.d.ts +36 -0
  95. package/dist/reactive/internals.d.ts.map +1 -0
  96. package/dist/reactive/linked.d.ts +36 -0
  97. package/dist/reactive/linked.d.ts.map +1 -0
  98. package/dist/reactive/persisted.d.ts +14 -0
  99. package/dist/reactive/persisted.d.ts.map +1 -0
  100. package/dist/reactive/readonly.d.ts +26 -0
  101. package/dist/reactive/readonly.d.ts.map +1 -0
  102. package/dist/reactive/signal.d.ts +13 -305
  103. package/dist/reactive/signal.d.ts.map +1 -1
  104. package/dist/reactive/type-guards.d.ts +20 -0
  105. package/dist/reactive/type-guards.d.ts.map +1 -0
  106. package/dist/reactive/untrack.d.ts +29 -0
  107. package/dist/reactive/untrack.d.ts.map +1 -0
  108. package/dist/reactive/watch.d.ts +42 -0
  109. package/dist/reactive/watch.d.ts.map +1 -0
  110. package/dist/reactive.es.mjs +30 -154
  111. package/dist/reactive.es.mjs.map +1 -1
  112. package/dist/router/index.d.ts +41 -0
  113. package/dist/router/index.d.ts.map +1 -0
  114. package/dist/router/links.d.ts +44 -0
  115. package/dist/router/links.d.ts.map +1 -0
  116. package/dist/router/match.d.ts +20 -0
  117. package/dist/router/match.d.ts.map +1 -0
  118. package/dist/router/navigation.d.ts +45 -0
  119. package/dist/router/navigation.d.ts.map +1 -0
  120. package/dist/router/query.d.ts +16 -0
  121. package/dist/router/query.d.ts.map +1 -0
  122. package/dist/router/router.d.ts +34 -0
  123. package/dist/router/router.d.ts.map +1 -0
  124. package/dist/router/state.d.ts +27 -0
  125. package/dist/router/state.d.ts.map +1 -0
  126. package/dist/router/types.d.ts +88 -0
  127. package/dist/router/types.d.ts.map +1 -0
  128. package/dist/router/utils.d.ts +65 -0
  129. package/dist/router/utils.d.ts.map +1 -0
  130. package/dist/router.es.mjs +202 -0
  131. package/dist/router.es.mjs.map +1 -0
  132. package/dist/sanitize-1FBEPAFH.js +272 -0
  133. package/dist/sanitize-1FBEPAFH.js.map +1 -0
  134. package/dist/security/constants.d.ts +42 -0
  135. package/dist/security/constants.d.ts.map +1 -0
  136. package/dist/security/csp.d.ts +24 -0
  137. package/dist/security/csp.d.ts.map +1 -0
  138. package/dist/security/index.d.ts +4 -2
  139. package/dist/security/index.d.ts.map +1 -1
  140. package/dist/security/sanitize-core.d.ts +13 -0
  141. package/dist/security/sanitize-core.d.ts.map +1 -0
  142. package/dist/security/sanitize.d.ts +5 -57
  143. package/dist/security/sanitize.d.ts.map +1 -1
  144. package/dist/security/trusted-types.d.ts +25 -0
  145. package/dist/security/trusted-types.d.ts.map +1 -0
  146. package/dist/security/types.d.ts +36 -0
  147. package/dist/security/types.d.ts.map +1 -0
  148. package/dist/security.es.mjs +50 -277
  149. package/dist/security.es.mjs.map +1 -1
  150. package/dist/store/create-store.d.ts +15 -0
  151. package/dist/store/create-store.d.ts.map +1 -0
  152. package/dist/store/define-store.d.ts +28 -0
  153. package/dist/store/define-store.d.ts.map +1 -0
  154. package/dist/store/devtools.d.ts +22 -0
  155. package/dist/store/devtools.d.ts.map +1 -0
  156. package/dist/store/index.d.ts +12 -0
  157. package/dist/store/index.d.ts.map +1 -0
  158. package/dist/store/mapping.d.ts +28 -0
  159. package/dist/store/mapping.d.ts.map +1 -0
  160. package/dist/store/persisted.d.ts +13 -0
  161. package/dist/store/persisted.d.ts.map +1 -0
  162. package/dist/store/plugins.d.ts +13 -0
  163. package/dist/store/plugins.d.ts.map +1 -0
  164. package/dist/store/registry.d.ts +28 -0
  165. package/dist/store/registry.d.ts.map +1 -0
  166. package/dist/store/types.d.ts +71 -0
  167. package/dist/store/types.d.ts.map +1 -0
  168. package/dist/store/utils.d.ts +28 -0
  169. package/dist/store/utils.d.ts.map +1 -0
  170. package/dist/store/watch.d.ts +23 -0
  171. package/dist/store/watch.d.ts.map +1 -0
  172. package/dist/store.es.mjs +27 -0
  173. package/dist/store.es.mjs.map +1 -0
  174. package/dist/type-guards-DRma3-Kc.js +16 -0
  175. package/dist/type-guards-DRma3-Kc.js.map +1 -0
  176. package/dist/untrack-BuEQKH7_.js +6 -0
  177. package/dist/untrack-BuEQKH7_.js.map +1 -0
  178. package/dist/view/directives/bind.d.ts +7 -0
  179. package/dist/view/directives/bind.d.ts.map +1 -0
  180. package/dist/view/directives/class.d.ts +8 -0
  181. package/dist/view/directives/class.d.ts.map +1 -0
  182. package/dist/view/directives/for.d.ts +23 -0
  183. package/dist/view/directives/for.d.ts.map +1 -0
  184. package/dist/view/directives/html.d.ts +7 -0
  185. package/dist/view/directives/html.d.ts.map +1 -0
  186. package/dist/view/directives/if.d.ts +7 -0
  187. package/dist/view/directives/if.d.ts.map +1 -0
  188. package/dist/view/directives/index.d.ts +12 -0
  189. package/dist/view/directives/index.d.ts.map +1 -0
  190. package/dist/view/directives/model.d.ts +7 -0
  191. package/dist/view/directives/model.d.ts.map +1 -0
  192. package/dist/view/directives/on.d.ts +7 -0
  193. package/dist/view/directives/on.d.ts.map +1 -0
  194. package/dist/view/directives/ref.d.ts +7 -0
  195. package/dist/view/directives/ref.d.ts.map +1 -0
  196. package/dist/view/directives/show.d.ts +7 -0
  197. package/dist/view/directives/show.d.ts.map +1 -0
  198. package/dist/view/directives/style.d.ts +7 -0
  199. package/dist/view/directives/style.d.ts.map +1 -0
  200. package/dist/view/directives/text.d.ts +7 -0
  201. package/dist/view/directives/text.d.ts.map +1 -0
  202. package/dist/view/evaluate.d.ts +43 -0
  203. package/dist/view/evaluate.d.ts.map +1 -0
  204. package/dist/view/index.d.ts +111 -0
  205. package/dist/view/index.d.ts.map +1 -0
  206. package/dist/view/mount.d.ts +69 -0
  207. package/dist/view/mount.d.ts.map +1 -0
  208. package/dist/view/process.d.ts +26 -0
  209. package/dist/view/process.d.ts.map +1 -0
  210. package/dist/view/types.d.ts +36 -0
  211. package/dist/view/types.d.ts.map +1 -0
  212. package/dist/view.es.mjs +426 -0
  213. package/dist/view.es.mjs.map +1 -0
  214. package/dist/watch-CXyaBC_9.js +58 -0
  215. package/dist/watch-CXyaBC_9.js.map +1 -0
  216. package/package.json +26 -14
  217. package/src/component/component.ts +289 -0
  218. package/src/component/html.ts +53 -0
  219. package/src/component/index.ts +40 -414
  220. package/src/component/props.ts +116 -0
  221. package/src/component/types.ts +85 -0
  222. package/src/core/collection.ts +588 -454
  223. package/src/core/dom.ts +38 -0
  224. package/src/core/element.ts +746 -740
  225. package/src/core/index.ts +43 -0
  226. package/src/core/utils/array.ts +102 -0
  227. package/src/core/utils/function.ts +110 -0
  228. package/src/core/utils/index.ts +83 -0
  229. package/src/core/utils/misc.ts +82 -0
  230. package/src/core/utils/number.ts +78 -0
  231. package/src/core/utils/object.ts +206 -0
  232. package/src/core/utils/string.ts +112 -0
  233. package/src/core/utils/type-guards.ts +112 -0
  234. package/src/full.ts +187 -106
  235. package/src/index.ts +36 -27
  236. package/src/motion/animate.ts +113 -0
  237. package/src/motion/easing.ts +40 -0
  238. package/src/motion/flip.ts +176 -0
  239. package/src/motion/index.ts +41 -358
  240. package/src/motion/keyframes.ts +46 -0
  241. package/src/motion/reduced-motion.ts +17 -0
  242. package/src/motion/scroll.ts +57 -0
  243. package/src/motion/spring.ts +150 -0
  244. package/src/motion/stagger.ts +43 -0
  245. package/src/motion/timeline.ts +246 -0
  246. package/src/motion/transition.ts +51 -0
  247. package/src/motion/types.ts +198 -0
  248. package/src/reactive/batch.ts +22 -0
  249. package/src/reactive/computed.ts +92 -0
  250. package/src/reactive/core.ts +93 -0
  251. package/src/reactive/effect.ts +43 -0
  252. package/src/reactive/index.ts +23 -22
  253. package/src/reactive/internals.ts +105 -0
  254. package/src/reactive/linked.ts +56 -0
  255. package/src/reactive/persisted.ts +74 -0
  256. package/src/reactive/readonly.ts +35 -0
  257. package/src/reactive/signal.ts +20 -506
  258. package/src/reactive/type-guards.ts +22 -0
  259. package/src/reactive/untrack.ts +31 -0
  260. package/src/reactive/watch.ts +73 -0
  261. package/src/router/index.ts +41 -0
  262. package/src/router/links.ts +130 -0
  263. package/src/router/match.ts +106 -0
  264. package/src/router/navigation.ts +71 -0
  265. package/src/router/query.ts +35 -0
  266. package/src/router/router.ts +211 -0
  267. package/src/router/state.ts +46 -0
  268. package/src/router/types.ts +93 -0
  269. package/src/router/utils.ts +116 -0
  270. package/src/security/constants.ts +209 -0
  271. package/src/security/csp.ts +77 -0
  272. package/src/security/index.ts +4 -12
  273. package/src/security/sanitize-core.ts +343 -0
  274. package/src/security/sanitize.ts +66 -625
  275. package/src/security/trusted-types.ts +69 -0
  276. package/src/security/types.ts +40 -0
  277. package/src/store/create-store.ts +329 -0
  278. package/src/store/define-store.ts +48 -0
  279. package/src/store/devtools.ts +45 -0
  280. package/src/store/index.ts +22 -0
  281. package/src/store/mapping.ts +73 -0
  282. package/src/store/persisted.ts +61 -0
  283. package/src/store/plugins.ts +32 -0
  284. package/src/store/registry.ts +51 -0
  285. package/src/store/types.ts +94 -0
  286. package/src/store/utils.ts +141 -0
  287. package/src/store/watch.ts +52 -0
  288. package/src/view/directives/bind.ts +23 -0
  289. package/src/view/directives/class.ts +70 -0
  290. package/src/view/directives/for.ts +275 -0
  291. package/src/view/directives/html.ts +19 -0
  292. package/src/view/directives/if.ts +30 -0
  293. package/src/view/directives/index.ts +11 -0
  294. package/src/view/directives/model.ts +56 -0
  295. package/src/view/directives/on.ts +41 -0
  296. package/src/view/directives/ref.ts +41 -0
  297. package/src/view/directives/show.ts +26 -0
  298. package/src/view/directives/style.ts +47 -0
  299. package/src/view/directives/text.ts +15 -0
  300. package/src/view/evaluate.ts +274 -0
  301. package/src/view/index.ts +112 -0
  302. package/src/view/mount.ts +200 -0
  303. package/src/view/process.ts +92 -0
  304. package/src/view/types.ts +44 -0
  305. package/dist/core/utils.d.ts +0 -313
  306. package/dist/core/utils.d.ts.map +0 -1
  307. package/src/core/utils.ts +0 -444
@@ -0,0 +1,74 @@
1
+ /**
2
+ * LocalStorage-backed signals.
3
+ */
4
+
5
+ import { signal, Signal } from './core';
6
+ import { effect } from './effect';
7
+
8
+ /**
9
+ * Creates a signal that persists to localStorage.
10
+ *
11
+ * @template T - The type of the signal value
12
+ * @param key - The localStorage key
13
+ * @param initialValue - The initial value if not found in storage
14
+ * @returns A Signal that syncs with localStorage (falls back to in-memory if unavailable)
15
+ */
16
+ export const persistedSignal = <T>(key: string, initialValue: T): Signal<T> => {
17
+ // Check if localStorage is available and accessible
18
+ let hasLocalStorage = false;
19
+ let storage: Storage | null = null;
20
+
21
+ try {
22
+ // In Safari private mode, accessing localStorage can throw SecurityError
23
+ storage = globalThis.localStorage;
24
+ if (storage) {
25
+ // Test actual access to ensure it's not just present but usable
26
+ // Use a randomized test key to avoid overwriting real user data
27
+ const testKey = `__bquery_test_${Math.random().toString(36).slice(2, 9)}__`;
28
+ const testValue = '__test__';
29
+ try {
30
+ storage.setItem(testKey, testValue);
31
+ storage.getItem(testKey);
32
+ hasLocalStorage = true;
33
+ } finally {
34
+ // Ensure we don't leave any test data behind
35
+ try {
36
+ storage.removeItem(testKey);
37
+ } catch {
38
+ // Ignore cleanup errors (e.g., storage becoming unavailable)
39
+ }
40
+ }
41
+ }
42
+ } catch {
43
+ // localStorage unavailable or access denied (Safari private mode, sandboxed iframes, etc.)
44
+ hasLocalStorage = false;
45
+ }
46
+
47
+ let stored: T = initialValue;
48
+
49
+ if (hasLocalStorage && storage) {
50
+ try {
51
+ const raw = storage.getItem(key);
52
+ if (raw !== null) {
53
+ stored = JSON.parse(raw) as T;
54
+ }
55
+ } catch {
56
+ // Use initial value on parse error or access denial
57
+ }
58
+ }
59
+
60
+ const sig = signal(stored);
61
+
62
+ // Only set up persistence effect if localStorage is available
63
+ if (hasLocalStorage && storage) {
64
+ effect(() => {
65
+ try {
66
+ storage!.setItem(key, JSON.stringify(sig.value));
67
+ } catch {
68
+ // Ignore storage errors (quota exceeded, sandboxed iframes, etc.)
69
+ }
70
+ });
71
+ }
72
+
73
+ return sig;
74
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Read-only signal wrappers.
3
+ */
4
+
5
+ import type { Signal } from './core';
6
+
7
+ /**
8
+ * A readonly wrapper around a signal that prevents writes.
9
+ * Provides read-only access to a signal's value while maintaining reactivity.
10
+ *
11
+ * @template T - The type of the wrapped value
12
+ */
13
+ export interface ReadonlySignal<T> {
14
+ /** Gets the current value with dependency tracking. */
15
+ readonly value: T;
16
+ /** Gets the current value without dependency tracking. */
17
+ peek(): T;
18
+ }
19
+
20
+ /**
21
+ * Creates a read-only view of a signal.
22
+ * Useful for exposing reactive state without allowing modifications.
23
+ *
24
+ * @template T - The type of the signal value
25
+ * @param sig - The signal to wrap
26
+ * @returns A readonly signal wrapper
27
+ */
28
+ export const readonly = <T>(sig: Signal<T>): ReadonlySignal<T> => ({
29
+ get value(): T {
30
+ return sig.value;
31
+ },
32
+ peek(): T {
33
+ return sig.peek();
34
+ },
35
+ });
@@ -1,506 +1,20 @@
1
- /**
2
- * Reactive primitives inspired by fine-grained reactivity.
3
- *
4
- * This module provides a minimal but powerful reactive system:
5
- * - Signal: A reactive value that notifies subscribers when changed
6
- * - Computed: A derived value that automatically updates when dependencies change
7
- * - Effect: A side effect that re-runs when its dependencies change
8
- * - Batch: Group multiple updates to prevent intermediate re-renders
9
- *
10
- * @module bquery/reactive
11
- *
12
- * @example
13
- * ```ts
14
- * const count = signal(0);
15
- * const doubled = computed(() => count.value * 2);
16
- *
17
- * effect(() => {
18
- * console.log(`Count: ${count.value}, Doubled: ${doubled.value}`);
19
- * });
20
- *
21
- * batch(() => {
22
- * count.value = 1;
23
- * count.value = 2;
24
- * });
25
- * // Logs: "Count: 2, Doubled: 4" (only once due to batching)
26
- * ```
27
- */
28
-
29
- /**
30
- * Observer function type used internally for tracking reactivity.
31
- */
32
- export type Observer = () => void;
33
-
34
- /**
35
- * Cleanup function returned by effects for disposal.
36
- */
37
- export type CleanupFn = () => void;
38
-
39
- // Internal state for tracking the current observer context
40
- const observerStack: Observer[] = [];
41
- let batchDepth = 0;
42
- const pendingObservers = new Set<Observer>();
43
-
44
- // Flag to disable tracking temporarily (for untrack)
45
- let trackingEnabled = true;
46
-
47
- /**
48
- * Tracks dependencies during a function execution.
49
- * Uses direct push/pop for O(1) operations instead of array copying.
50
- * @internal
51
- */
52
- const track = <T>(observer: Observer, fn: () => T): T => {
53
- observerStack.push(observer);
54
- try {
55
- return fn();
56
- } finally {
57
- observerStack.pop();
58
- }
59
- };
60
-
61
- /**
62
- * Schedules an observer to run, respecting batch mode.
63
- * @internal
64
- */
65
- const scheduleObserver = (observer: Observer) => {
66
- if (batchDepth > 0) {
67
- pendingObservers.add(observer);
68
- return;
69
- }
70
- observer();
71
- };
72
-
73
- /**
74
- * Flushes all pending observers after a batch completes.
75
- * @internal
76
- */
77
- const flushObservers = () => {
78
- for (const observer of Array.from(pendingObservers)) {
79
- pendingObservers.delete(observer);
80
- observer();
81
- }
82
- };
83
-
84
- /**
85
- * A reactive value container that notifies subscribers on change.
86
- *
87
- * Signals are the foundational primitive of the reactive system.
88
- * Reading a signal's value inside an effect or computed automatically
89
- * establishes a reactive dependency.
90
- *
91
- * @template T - The type of the stored value
92
- *
93
- * @example
94
- * ```ts
95
- * const name = signal('World');
96
- * console.log(name.value); // 'World'
97
- *
98
- * name.value = 'bQuery';
99
- * console.log(name.value); // 'bQuery'
100
- * ```
101
- */
102
- export class Signal<T> {
103
- private subscribers = new Set<Observer>();
104
-
105
- /**
106
- * Creates a new signal with an initial value.
107
- * @param _value - The initial value
108
- */
109
- constructor(private _value: T) {}
110
-
111
- /**
112
- * Gets the current value and tracks the read if inside an observer.
113
- * Respects the global tracking state (disabled during untrack calls).
114
- */
115
- get value(): T {
116
- if (trackingEnabled) {
117
- const current = observerStack[observerStack.length - 1];
118
- if (current) {
119
- this.subscribers.add(current);
120
- }
121
- }
122
- return this._value;
123
- }
124
-
125
- /**
126
- * Sets a new value and notifies all subscribers if the value changed.
127
- * Uses Object.is for equality comparison.
128
- */
129
- set value(next: T) {
130
- if (Object.is(this._value, next)) return;
131
- this._value = next;
132
- for (const subscriber of this.subscribers) {
133
- scheduleObserver(subscriber);
134
- }
135
- }
136
-
137
- /**
138
- * Reads the current value without tracking.
139
- * Useful when you need the value but don't want to create a dependency.
140
- *
141
- * @returns The current value
142
- */
143
- peek(): T {
144
- return this._value;
145
- }
146
-
147
- /**
148
- * Updates the value using a function.
149
- * Useful for updates based on the current value.
150
- *
151
- * @param updater - Function that receives current value and returns new value
152
- */
153
- update(updater: (current: T) => T): void {
154
- this.value = updater(this._value);
155
- }
156
- }
157
-
158
- /**
159
- * A computed value that derives from other reactive sources.
160
- *
161
- * Computed values are lazily evaluated and cached. They only
162
- * recompute when their dependencies change.
163
- *
164
- * @template T - The type of the computed value
165
- *
166
- * @example
167
- * ```ts
168
- * const price = signal(100);
169
- * const quantity = signal(2);
170
- * const total = computed(() => price.value * quantity.value);
171
- *
172
- * console.log(total.value); // 200
173
- * price.value = 150;
174
- * console.log(total.value); // 300
175
- * ```
176
- */
177
- export class Computed<T> {
178
- private cachedValue!: T;
179
- private dirty = true;
180
- private subscribers = new Set<Observer>();
181
- private readonly markDirty = () => {
182
- this.dirty = true;
183
- for (const subscriber of this.subscribers) {
184
- scheduleObserver(subscriber);
185
- }
186
- };
187
-
188
- /**
189
- * Creates a new computed value.
190
- * @param compute - Function that computes the value
191
- */
192
- constructor(private readonly compute: () => T) {}
193
-
194
- /**
195
- * Gets the computed value, recomputing if dependencies changed.
196
- */
197
- get value(): T {
198
- const current = observerStack[observerStack.length - 1];
199
- if (current) {
200
- this.subscribers.add(current);
201
- }
202
- if (this.dirty) {
203
- this.dirty = false;
204
- this.cachedValue = track(this.markDirty, this.compute);
205
- }
206
- return this.cachedValue;
207
- }
208
- }
209
-
210
- /**
211
- * Creates a new reactive signal.
212
- *
213
- * @template T - The type of the signal value
214
- * @param value - The initial value
215
- * @returns A new Signal instance
216
- *
217
- * @example
218
- * ```ts
219
- * const count = signal(0);
220
- * count.value++; // Triggers subscribers
221
- * ```
222
- */
223
- export const signal = <T>(value: T): Signal<T> => new Signal(value);
224
-
225
- /**
226
- * Creates a new computed value.
227
- *
228
- * @template T - The type of the computed value
229
- * @param fn - Function that computes the value from reactive sources
230
- * @returns A new Computed instance
231
- *
232
- * @example
233
- * ```ts
234
- * const doubled = computed(() => count.value * 2);
235
- * ```
236
- */
237
- export const computed = <T>(fn: () => T): Computed<T> => new Computed(fn);
238
-
239
- /**
240
- * Creates a side effect that automatically re-runs when dependencies change.
241
- *
242
- * The effect runs immediately upon creation and then re-runs whenever
243
- * any signal or computed value read inside it changes.
244
- *
245
- * @param fn - The effect function to run
246
- * @returns A cleanup function to stop the effect
247
- *
248
- * @example
249
- * ```ts
250
- * const count = signal(0);
251
- *
252
- * const cleanup = effect(() => {
253
- * document.title = `Count: ${count.value}`;
254
- * });
255
- *
256
- * // Later, to stop the effect:
257
- * cleanup();
258
- * ```
259
- */
260
- export const effect = (fn: () => void | CleanupFn): CleanupFn => {
261
- let cleanupFn: CleanupFn | void;
262
- let isDisposed = false;
263
-
264
- const observer: Observer = () => {
265
- if (isDisposed) return;
266
-
267
- // Run previous cleanup if exists
268
- if (cleanupFn) {
269
- cleanupFn();
270
- }
271
-
272
- // Run effect and capture cleanup
273
- cleanupFn = track(observer, fn);
274
- };
275
-
276
- observer();
277
-
278
- return () => {
279
- isDisposed = true;
280
- if (cleanupFn) {
281
- cleanupFn();
282
- }
283
- };
284
- };
285
-
286
- /**
287
- * Batches multiple signal updates into a single notification cycle.
288
- *
289
- * Updates made inside the batch function are deferred until the batch
290
- * completes, preventing intermediate re-renders and improving performance.
291
- *
292
- * @param fn - Function containing multiple signal updates
293
- *
294
- * @example
295
- * ```ts
296
- * batch(() => {
297
- * firstName.value = 'John';
298
- * lastName.value = 'Doe';
299
- * age.value = 30;
300
- * });
301
- * // Effects only run once with all three updates
302
- * ```
303
- */
304
- export const batch = (fn: () => void): void => {
305
- batchDepth += 1;
306
- try {
307
- fn();
308
- } finally {
309
- batchDepth -= 1;
310
- if (batchDepth === 0) {
311
- flushObservers();
312
- }
313
- }
314
- };
315
-
316
- /**
317
- * Creates a signal that persists to localStorage.
318
- *
319
- * @template T - The type of the signal value
320
- * @param key - The localStorage key
321
- * @param initialValue - The initial value if not found in storage
322
- * @returns A Signal that syncs with localStorage
323
- *
324
- * @example
325
- * ```ts
326
- * const theme = persistedSignal('theme', 'light');
327
- * theme.value = 'dark'; // Automatically saved to localStorage
328
- * ```
329
- */
330
- export const persistedSignal = <T>(key: string, initialValue: T): Signal<T> => {
331
- let stored: T = initialValue;
332
-
333
- try {
334
- const raw = localStorage.getItem(key);
335
- if (raw !== null) {
336
- stored = JSON.parse(raw) as T;
337
- }
338
- } catch {
339
- // Use initial value on parse error
340
- }
341
-
342
- const sig = signal(stored);
343
-
344
- // Create an effect to persist changes
345
- effect(() => {
346
- try {
347
- localStorage.setItem(key, JSON.stringify(sig.value));
348
- } catch {
349
- // Ignore storage errors
350
- }
351
- });
352
-
353
- return sig;
354
- };
355
-
356
- // ============================================================================
357
- // Extended Reactive Utilities
358
- // ============================================================================
359
-
360
- /**
361
- * A readonly wrapper around a signal that prevents writes.
362
- * Provides read-only access to a signal's value while maintaining reactivity.
363
- *
364
- * @template T - The type of the wrapped value
365
- */
366
- export interface ReadonlySignal<T> {
367
- /** Gets the current value with dependency tracking. */
368
- readonly value: T;
369
- /** Gets the current value without dependency tracking. */
370
- peek(): T;
371
- }
372
-
373
- /**
374
- * Creates a read-only view of a signal.
375
- * Useful for exposing reactive state without allowing modifications.
376
- *
377
- * @template T - The type of the signal value
378
- * @param sig - The signal to wrap
379
- * @returns A readonly signal wrapper
380
- *
381
- * @example
382
- * ```ts
383
- * const _count = signal(0);
384
- * const count = readonly(_count); // Expose read-only version
385
- *
386
- * console.log(count.value); // 0
387
- * count.value = 1; // TypeScript error: Cannot assign to 'value'
388
- * ```
389
- */
390
- export const readonly = <T>(sig: Signal<T>): ReadonlySignal<T> => ({
391
- get value(): T {
392
- return sig.value;
393
- },
394
- peek(): T {
395
- return sig.peek();
396
- },
397
- });
398
-
399
- /**
400
- * Watches a signal or computed value and calls a callback with old and new values.
401
- * Unlike effect, watch provides access to the previous value.
402
- *
403
- * @template T - The type of the watched value
404
- * @param source - The signal or computed to watch
405
- * @param callback - Function called with (newValue, oldValue) on changes
406
- * @param options - Watch options
407
- * @returns A cleanup function to stop watching
408
- *
409
- * @example
410
- * ```ts
411
- * const count = signal(0);
412
- *
413
- * const cleanup = watch(count, (newVal, oldVal) => {
414
- * console.log(`Changed from ${oldVal} to ${newVal}`);
415
- * });
416
- *
417
- * count.value = 5; // Logs: "Changed from 0 to 5"
418
- * cleanup();
419
- * ```
420
- */
421
- export const watch = <T>(
422
- source: Signal<T> | Computed<T>,
423
- callback: (newValue: T, oldValue: T | undefined) => void,
424
- options: { immediate?: boolean } = {}
425
- ): CleanupFn => {
426
- let oldValue: T | undefined;
427
- let isFirst = true;
428
-
429
- return effect(() => {
430
- const newValue = source.value;
431
-
432
- if (isFirst) {
433
- isFirst = false;
434
- oldValue = newValue;
435
- if (options.immediate) {
436
- callback(newValue, undefined);
437
- }
438
- return;
439
- }
440
-
441
- callback(newValue, oldValue);
442
- oldValue = newValue;
443
- });
444
- };
445
-
446
- /**
447
- * Executes a function without tracking any signal dependencies.
448
- * Useful when reading a signal value without creating a reactive dependency.
449
- *
450
- * @template T - The return type of the function
451
- * @param fn - The function to execute without tracking
452
- * @returns The result of the function
453
- *
454
- * @example
455
- * ```ts
456
- * const count = signal(0);
457
- *
458
- * effect(() => {
459
- * // This creates a dependency
460
- * console.log('Tracked:', count.value);
461
- *
462
- * // This does NOT create a dependency
463
- * const untracked = untrack(() => otherSignal.value);
464
- * });
465
- * ```
466
- */
467
- export const untrack = <T>(fn: () => T): T => {
468
- const prevTracking = trackingEnabled;
469
- trackingEnabled = false;
470
- try {
471
- return fn();
472
- } finally {
473
- trackingEnabled = prevTracking;
474
- }
475
- };
476
-
477
- /**
478
- * Type guard to check if a value is a Signal instance.
479
- *
480
- * @param value - The value to check
481
- * @returns True if the value is a Signal
482
- *
483
- * @example
484
- * ```ts
485
- * const count = signal(0);
486
- * const num = 42;
487
- *
488
- * isSignal(count); // true
489
- * isSignal(num); // false
490
- * ```
491
- */
492
- export const isSignal = (value: unknown): value is Signal<unknown> => value instanceof Signal;
493
-
494
- /**
495
- * Type guard to check if a value is a Computed instance.
496
- *
497
- * @param value - The value to check
498
- * @returns True if the value is a Computed
499
- *
500
- * @example
501
- * ```ts
502
- * const doubled = computed(() => count.value * 2);
503
- * isComputed(doubled); // true
504
- * ```
505
- */
506
- export const isComputed = (value: unknown): value is Computed<unknown> => value instanceof Computed;
1
+ /**
2
+ * Reactive primitives inspired by fine-grained reactivity.
3
+ *
4
+ * @module bquery/reactive
5
+ */
6
+
7
+ export { batch } from './batch';
8
+ export { Computed, computed } from './computed';
9
+ export { Signal, signal } from './core';
10
+ export { effect } from './effect';
11
+ export { linkedSignal } from './linked';
12
+ export { persistedSignal } from './persisted';
13
+ export { readonly } from './readonly';
14
+ export { isComputed, isSignal } from './type-guards';
15
+ export { untrack } from './untrack';
16
+ export { watch } from './watch';
17
+
18
+ export type { CleanupFn, Observer } from './internals';
19
+ export type { LinkedSignal } from './linked';
20
+ export type { ReadonlySignal } from './readonly';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Type guards for reactive primitives.
3
+ */
4
+
5
+ import { Computed } from './computed';
6
+ import { Signal } from './core';
7
+
8
+ /**
9
+ * Type guard to check if a value is a Signal instance.
10
+ *
11
+ * @param value - The value to check
12
+ * @returns True if the value is a Signal
13
+ */
14
+ export const isSignal = (value: unknown): value is Signal<unknown> => value instanceof Signal;
15
+
16
+ /**
17
+ * Type guard to check if a value is a Computed instance.
18
+ *
19
+ * @param value - The value to check
20
+ * @returns True if the value is a Computed
21
+ */
22
+ export const isComputed = (value: unknown): value is Computed<unknown> => value instanceof Computed;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Dependency tracking control helpers.
3
+ */
4
+
5
+ import { withoutCurrentObserver } from './internals';
6
+
7
+ /**
8
+ * Executes a function without tracking any signal dependencies.
9
+ * Useful when reading a signal value without creating a reactive dependency.
10
+ *
11
+ * This implementation temporarily hides the current observer rather than
12
+ * disabling tracking globally. This ensures that nested reactive internals
13
+ * (e.g., computed recomputation triggered during untrack) can still properly
14
+ * track their own dependencies.
15
+ *
16
+ * @template T - The return type of the function
17
+ * @param fn - The function to execute without tracking
18
+ * @returns The result of the function
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const count = signal(0);
23
+ * effect(() => {
24
+ * // This read creates a dependency
25
+ * console.log(count.value);
26
+ * // This read does not create a dependency
27
+ * const snapshot = untrack(() => count.value);
28
+ * });
29
+ * ```
30
+ */
31
+ export const untrack = <T>(fn: () => T): T => withoutCurrentObserver(fn);