@bquery/bquery 1.8.2 → 1.10.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/README.md +255 -27
- package/dist/{a11y-DVBCy09c.js → a11y-DG2i4iZN.js} +3 -3
- package/dist/{a11y-DVBCy09c.js.map → a11y-DG2i4iZN.js.map} +1 -1
- package/dist/a11y.es.mjs +1 -1
- package/dist/{component-L3-JfOFz.js → component-DRotf1hl.js} +19 -18
- package/dist/{component-L3-JfOFz.js.map → component-DRotf1hl.js.map} +1 -1
- package/dist/component.es.mjs +1 -1
- package/dist/concurrency/errors.d.ts +29 -0
- package/dist/concurrency/errors.d.ts.map +1 -0
- package/dist/concurrency/high-level.d.ts +85 -0
- package/dist/concurrency/high-level.d.ts.map +1 -0
- package/dist/concurrency/index.d.ts +19 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/internal.d.ts +26 -0
- package/dist/concurrency/internal.d.ts.map +1 -0
- package/dist/concurrency/pipeline.d.ts +30 -0
- package/dist/concurrency/pipeline.d.ts.map +1 -0
- package/dist/concurrency/pool.d.ts +48 -0
- package/dist/concurrency/pool.d.ts.map +1 -0
- package/dist/concurrency/reactive.d.ts +107 -0
- package/dist/concurrency/reactive.d.ts.map +1 -0
- package/dist/concurrency/rpc.d.ts +46 -0
- package/dist/concurrency/rpc.d.ts.map +1 -0
- package/dist/concurrency/support.d.ts +23 -0
- package/dist/concurrency/support.d.ts.map +1 -0
- package/dist/concurrency/task.d.ts +31 -0
- package/dist/concurrency/task.d.ts.map +1 -0
- package/dist/concurrency/types.d.ts +343 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency-BU1wPEsZ.js +826 -0
- package/dist/concurrency-BU1wPEsZ.js.map +1 -0
- package/dist/concurrency.es.mjs +29 -0
- package/dist/{constraints-D5RHQLmP.js → constraints-CqjhmpZC.js} +1 -1
- package/dist/{constraints-D5RHQLmP.js.map → constraints-CqjhmpZC.js.map} +1 -1
- package/dist/core-CongXJuo.js +87 -0
- package/dist/core-CongXJuo.js.map +1 -0
- package/dist/{custom-directives-Dr4C5lVV.js → custom-directives-BjFzFhuf.js} +1 -1
- package/dist/{custom-directives-Dr4C5lVV.js.map → custom-directives-BjFzFhuf.js.map} +1 -1
- package/dist/{devtools-BhB2iDPT.js → devtools-C5FExMwv.js} +2 -2
- package/dist/{devtools-BhB2iDPT.js.map → devtools-C5FExMwv.js.map} +1 -1
- package/dist/devtools.es.mjs +1 -1
- package/dist/{dnd-NwZBYh4l.js → dnd-BAqzPlSo.js} +1 -1
- package/dist/{dnd-NwZBYh4l.js.map → dnd-BAqzPlSo.js.map} +1 -1
- package/dist/dnd.es.mjs +1 -1
- package/dist/effect-Cc51IH91.js +87 -0
- package/dist/effect-Cc51IH91.js.map +1 -0
- package/dist/{env-CTdvLaH2.js → env-PvwYHnJq.js} +1 -1
- package/dist/{env-CTdvLaH2.js.map → env-PvwYHnJq.js.map} +1 -1
- package/dist/{forms-UcRHsYxC.js → forms-Dx1Scvh0.js} +30 -29
- package/dist/{forms-UcRHsYxC.js.map → forms-Dx1Scvh0.js.map} +1 -1
- package/dist/forms.es.mjs +1 -1
- package/dist/full.d.ts +6 -4
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +240 -206
- package/dist/full.iife.js +117 -33
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +117 -33
- package/dist/full.umd.js.map +1 -1
- package/dist/{i18n-kuF6Ekj6.js → i18n-Cazyk9RD.js} +3 -3
- package/dist/{i18n-kuF6Ekj6.js.map → i18n-Cazyk9RD.js.map} +1 -1
- package/dist/i18n.es.mjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.mjs +274 -240
- package/dist/media/index.d.ts +10 -3
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/observers.d.ts +99 -0
- package/dist/media/observers.d.ts.map +1 -0
- package/dist/media/types.d.ts +125 -0
- package/dist/media/types.d.ts.map +1 -1
- package/dist/media-dAKIGPk3.js +514 -0
- package/dist/media-dAKIGPk3.js.map +1 -0
- package/dist/media.es.mjs +12 -9
- package/dist/{motion-BJsAuULb.js → motion-BBMso9Ir.js} +1 -1
- package/dist/{motion-BJsAuULb.js.map → motion-BBMso9Ir.js.map} +1 -1
- package/dist/motion.es.mjs +1 -1
- package/dist/mount-C8O2vXkQ.js +450 -0
- package/dist/mount-C8O2vXkQ.js.map +1 -0
- package/dist/{platform-Dw2gE3zI.js → platform-BPHIXbw8.js} +17 -16
- package/dist/{platform-Dw2gE3zI.js.map → platform-BPHIXbw8.js.map} +1 -1
- package/dist/platform.es.mjs +1 -1
- package/dist/{plugin-C2WuC8SF.js → plugin-DjTqWg-P.js} +2 -2
- package/dist/{plugin-C2WuC8SF.js.map → plugin-DjTqWg-P.js.map} +1 -1
- package/dist/plugin.es.mjs +1 -1
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/signal.d.ts +2 -1
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/watch.d.ts +49 -0
- package/dist/reactive/watch.d.ts.map +1 -1
- package/dist/reactive/websocket.d.ts +6 -3
- package/dist/reactive/websocket.d.ts.map +1 -1
- package/dist/reactive-BAd2hfl8.js +1171 -0
- package/dist/reactive-BAd2hfl8.js.map +1 -0
- package/dist/reactive.es.mjs +41 -37
- package/dist/readonly-C0ZwS1Tf.js +35 -0
- package/dist/readonly-C0ZwS1Tf.js.map +1 -0
- package/dist/{registry-B08iilIh.js → registry-Cr6VH8CR.js} +1 -1
- package/dist/{registry-B08iilIh.js.map → registry-Cr6VH8CR.js.map} +1 -1
- package/dist/{router-CQikC9Ed.js → router-CCepRMpC.js} +29 -28
- package/dist/{router-CQikC9Ed.js.map → router-CCepRMpC.js.map} +1 -1
- package/dist/router.es.mjs +1 -1
- package/dist/{ssr-_dAcGdzu.js → ssr-D-1IPcfw.js} +4 -4
- package/dist/{ssr-_dAcGdzu.js.map → ssr-D-1IPcfw.js.map} +1 -1
- package/dist/ssr.es.mjs +1 -1
- package/dist/{store-Cb3gPRve.js → store-CjmEeX9-.js} +6 -6
- package/dist/{store-Cb3gPRve.js.map → store-CjmEeX9-.js.map} +1 -1
- package/dist/store.es.mjs +2 -2
- package/dist/{testing-C5Sjfsna.js → testing-TdfaL7VE.js} +8 -8
- package/dist/{testing-C5Sjfsna.js.map → testing-TdfaL7VE.js.map} +1 -1
- package/dist/testing.es.mjs +1 -1
- package/dist/{untrack-D0fnO5k2.js → untrack-bjWDNdyE.js} +11 -10
- package/dist/{untrack-D0fnO5k2.js.map → untrack-bjWDNdyE.js.map} +1 -1
- package/dist/view/directives/aria.d.ts +7 -0
- package/dist/view/directives/aria.d.ts.map +1 -0
- package/dist/view/directives/error.d.ts +7 -0
- package/dist/view/directives/error.d.ts.map +1 -0
- package/dist/view/directives/index.d.ts +2 -0
- package/dist/view/directives/index.d.ts.map +1 -1
- package/dist/view/mount.d.ts.map +1 -1
- package/dist/view/process.d.ts +2 -0
- package/dist/view/process.d.ts.map +1 -1
- package/dist/view.es.mjs +12 -11
- package/package.json +18 -14
- package/src/concurrency/errors.ts +57 -0
- package/src/concurrency/high-level.ts +387 -0
- package/src/concurrency/index.ts +63 -0
- package/src/concurrency/internal.ts +100 -0
- package/src/concurrency/pipeline.ts +133 -0
- package/src/concurrency/pool.ts +450 -0
- package/src/concurrency/reactive.ts +339 -0
- package/src/concurrency/rpc.ts +380 -0
- package/src/concurrency/support.ts +44 -0
- package/src/concurrency/task.ts +318 -0
- package/src/concurrency/types.ts +431 -0
- package/src/full.ts +77 -0
- package/src/index.ts +3 -0
- package/src/media/index.ts +20 -2
- package/src/media/observers.ts +418 -0
- package/src/media/types.ts +136 -0
- package/src/reactive/index.ts +3 -0
- package/src/reactive/signal.ts +2 -1
- package/src/reactive/watch.ts +138 -0
- package/src/reactive/websocket.ts +31 -8
- package/src/view/directives/aria.ts +72 -0
- package/src/view/directives/error.ts +56 -0
- package/src/view/directives/index.ts +2 -0
- package/src/view/mount.ts +4 -0
- package/src/view/process.ts +6 -0
- package/dist/core-DdtZHzsS.js +0 -168
- package/dist/core-DdtZHzsS.js.map +0 -1
- package/dist/media-i-fB5WxI.js +0 -340
- package/dist/media-i-fB5WxI.js.map +0 -1
- package/dist/mount-B4Y8bk8Z.js +0 -403
- package/dist/mount-B4Y8bk8Z.js.map +0 -1
- package/dist/reactive-DwkhUJfP.js +0 -1148
- package/dist/reactive-DwkhUJfP.js.map +0 -1
package/src/reactive/watch.ts
CHANGED
|
@@ -6,7 +6,9 @@ import type { Computed } from './computed';
|
|
|
6
6
|
import type { Signal } from './core';
|
|
7
7
|
import type { CleanupFn } from './internals';
|
|
8
8
|
|
|
9
|
+
import { debounce, throttle } from '../core/utils/function';
|
|
9
10
|
import { effect } from './effect';
|
|
11
|
+
import { getCurrentScope, onScopeDispose } from './scope';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Options for the watch function.
|
|
@@ -71,3 +73,139 @@ export const watch = <T>(
|
|
|
71
73
|
}
|
|
72
74
|
});
|
|
73
75
|
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Watches a signal or computed value and debounces callback delivery.
|
|
79
|
+
* Rapid changes are collapsed into a single callback using the latest value
|
|
80
|
+
* and the first old value observed within the debounce window.
|
|
81
|
+
*
|
|
82
|
+
* @template T - The type of the watched value
|
|
83
|
+
* @param source - The signal or computed to watch
|
|
84
|
+
* @param callback - Function called with the debounced (newValue, oldValue)
|
|
85
|
+
* @param delayMs - Debounce delay in milliseconds
|
|
86
|
+
* @param options - Watch options
|
|
87
|
+
* @returns A cleanup function to stop watching and cancel pending callbacks
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const query = signal('');
|
|
92
|
+
* const stop = watchDebounce(query, (newQuery) => {
|
|
93
|
+
* console.log('Search for', newQuery);
|
|
94
|
+
* }, 250);
|
|
95
|
+
*
|
|
96
|
+
* query.value = 'b';
|
|
97
|
+
* query.value = 'bq';
|
|
98
|
+
* query.value = 'bqu'; // Only this value is delivered after 250ms
|
|
99
|
+
*
|
|
100
|
+
* stop();
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export const watchDebounce = <T>(
|
|
104
|
+
source: Signal<T> | Computed<T>,
|
|
105
|
+
callback: (newValue: T, oldValue: T | undefined) => void,
|
|
106
|
+
delayMs: number,
|
|
107
|
+
options: WatchOptions<T> = {}
|
|
108
|
+
): CleanupFn => {
|
|
109
|
+
const { immediate = false, equals = Object.is } = options;
|
|
110
|
+
const normalizedDelayMs = Number.isFinite(delayMs) ? Math.max(0, delayMs) : 0;
|
|
111
|
+
let hasPending = false;
|
|
112
|
+
let pendingNewValue!: T;
|
|
113
|
+
let pendingOldValue: T | undefined;
|
|
114
|
+
const cancelPending = (): void => {
|
|
115
|
+
notify.cancel();
|
|
116
|
+
hasPending = false;
|
|
117
|
+
pendingOldValue = undefined;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const notify = debounce(() => {
|
|
121
|
+
if (!hasPending) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
callback(pendingNewValue, pendingOldValue);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('bQuery reactive: Error in watchDebounce callback', error);
|
|
129
|
+
}
|
|
130
|
+
hasPending = false;
|
|
131
|
+
pendingOldValue = undefined;
|
|
132
|
+
}, normalizedDelayMs);
|
|
133
|
+
|
|
134
|
+
if (immediate) {
|
|
135
|
+
callback(source.peek(), undefined);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const cleanup = watch(
|
|
139
|
+
source,
|
|
140
|
+
(newValue, oldValue) => {
|
|
141
|
+
if (!hasPending) {
|
|
142
|
+
pendingOldValue = oldValue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pendingNewValue = newValue;
|
|
146
|
+
hasPending = true;
|
|
147
|
+
notify();
|
|
148
|
+
},
|
|
149
|
+
{ equals }
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (getCurrentScope()) {
|
|
153
|
+
onScopeDispose(cancelPending);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return () => {
|
|
157
|
+
cleanup();
|
|
158
|
+
cancelPending();
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Watches a signal or computed value and throttles callback delivery.
|
|
164
|
+
* Changes are delivered at most once per interval.
|
|
165
|
+
*
|
|
166
|
+
* @template T - The type of the watched value
|
|
167
|
+
* @param source - The signal or computed to watch
|
|
168
|
+
* @param callback - Function called with throttled (newValue, oldValue) updates
|
|
169
|
+
* @param intervalMs - Minimum interval between callback runs in milliseconds
|
|
170
|
+
* @param options - Watch options
|
|
171
|
+
* @returns A cleanup function to stop watching and reset the throttle window
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* const scrollY = signal(0);
|
|
176
|
+
* const stop = watchThrottle(scrollY, (nextY) => {
|
|
177
|
+
* console.log('Scroll position', nextY);
|
|
178
|
+
* }, 100);
|
|
179
|
+
*
|
|
180
|
+
* stop();
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export const watchThrottle = <T>(
|
|
184
|
+
source: Signal<T> | Computed<T>,
|
|
185
|
+
callback: (newValue: T, oldValue: T | undefined) => void,
|
|
186
|
+
intervalMs: number,
|
|
187
|
+
options: WatchOptions<T> = {}
|
|
188
|
+
): CleanupFn => {
|
|
189
|
+
const { immediate = false, equals = Object.is } = options;
|
|
190
|
+
const normalizedIntervalMs = Number.isFinite(intervalMs) ? Math.max(0, intervalMs) : 0;
|
|
191
|
+
const notify = throttle((newValue: T, oldValue: T | undefined) => {
|
|
192
|
+
callback(newValue, oldValue);
|
|
193
|
+
}, normalizedIntervalMs);
|
|
194
|
+
|
|
195
|
+
if (immediate) {
|
|
196
|
+
notify(source.peek(), undefined);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const cleanup = watch(
|
|
200
|
+
source,
|
|
201
|
+
(newValue, oldValue) => {
|
|
202
|
+
notify(newValue, oldValue);
|
|
203
|
+
},
|
|
204
|
+
{ equals }
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
return () => {
|
|
208
|
+
cleanup();
|
|
209
|
+
notify.cancel();
|
|
210
|
+
};
|
|
211
|
+
};
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
import { computed } from './computed';
|
|
9
9
|
import { Signal, signal } from './core';
|
|
10
10
|
|
|
11
|
+
/** @internal */
|
|
12
|
+
type WebSocketSendData = string | Blob | ArrayBufferLike | ArrayBufferView;
|
|
13
|
+
|
|
11
14
|
// ---------------------------------------------------------------------------
|
|
12
15
|
// Types
|
|
13
16
|
// ---------------------------------------------------------------------------
|
|
@@ -40,7 +43,7 @@ export type EventSourceReconnectConfig = Pick<
|
|
|
40
43
|
/** Configuration for keep-alive heartbeats. */
|
|
41
44
|
export interface WebSocketHeartbeatConfig {
|
|
42
45
|
/** Outgoing ping message (default: `'ping'`). */
|
|
43
|
-
message?:
|
|
46
|
+
message?: WebSocketSendData;
|
|
44
47
|
/** Interval in ms between heartbeat pings (default: 30 000). */
|
|
45
48
|
interval?: number;
|
|
46
49
|
/** Time in ms to wait for a pong before assuming the connection is dead (default: 10 000). */
|
|
@@ -52,7 +55,7 @@ export interface WebSocketHeartbeatConfig {
|
|
|
52
55
|
/** Serializer/deserializer for typed messaging. */
|
|
53
56
|
export interface WebSocketSerializer<TSend = unknown, TReceive = unknown> {
|
|
54
57
|
/** Serialize a value before sending over the wire. Default: `JSON.stringify`. */
|
|
55
|
-
serialize?: (data: TSend) =>
|
|
58
|
+
serialize?: (data: TSend) => WebSocketSendData;
|
|
56
59
|
/** Deserialize an incoming message. Default: `JSON.parse`. */
|
|
57
60
|
deserialize?: (event: MessageEvent) => TReceive;
|
|
58
61
|
}
|
|
@@ -115,7 +118,7 @@ export interface UseWebSocketReturn<TSend = unknown, TReceive = unknown> {
|
|
|
115
118
|
* Uses the same queuing behavior as {@link send}: data is queued when the
|
|
116
119
|
* socket is not `OPEN` and flushed once a connection is (re)established.
|
|
117
120
|
*/
|
|
118
|
-
sendRaw: (data:
|
|
121
|
+
sendRaw: (data: WebSocketSendData) => void;
|
|
119
122
|
/** Manually open / reconnect the WebSocket. */
|
|
120
123
|
open: () => void;
|
|
121
124
|
/** Gracefully close the connection. */
|
|
@@ -162,6 +165,26 @@ const computeDelay = (attempt: number, config: WebSocketReconnectConfig): number
|
|
|
162
165
|
return Math.min(base * factor ** attempt, max);
|
|
163
166
|
};
|
|
164
167
|
|
|
168
|
+
/** @internal */
|
|
169
|
+
const sendSocketData = (socket: WebSocket, data: WebSocketSendData): void => {
|
|
170
|
+
if (typeof data === 'string' || data instanceof Blob || data instanceof ArrayBuffer) {
|
|
171
|
+
socket.send(data);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (ArrayBuffer.isView(data)) {
|
|
176
|
+
socket.send(data as BufferSource);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (typeof SharedArrayBuffer !== 'undefined' && data instanceof SharedArrayBuffer) {
|
|
181
|
+
socket.send(data as unknown as BufferSource);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
throw new TypeError('Unsupported WebSocket payload type.');
|
|
186
|
+
};
|
|
187
|
+
|
|
165
188
|
// ---------------------------------------------------------------------------
|
|
166
189
|
// useWebSocket
|
|
167
190
|
// ---------------------------------------------------------------------------
|
|
@@ -249,7 +272,7 @@ export const useWebSocket = <TSend = string, TReceive = string>(
|
|
|
249
272
|
let internalReconnectCount = 0;
|
|
250
273
|
let isAutoReconnecting = false;
|
|
251
274
|
let pingSentAt = 0;
|
|
252
|
-
const sendQueue:
|
|
275
|
+
const sendQueue: WebSocketSendData[] = [];
|
|
253
276
|
|
|
254
277
|
const reconnectConfig = resolveReconnect(options.autoReconnect);
|
|
255
278
|
const heartbeatConfig = resolveHeartbeat(options.heartbeat);
|
|
@@ -265,7 +288,7 @@ export const useWebSocket = <TSend = string, TReceive = string>(
|
|
|
265
288
|
heartbeatTimer = setInterval(() => {
|
|
266
289
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
267
290
|
pingSentAt = Date.now();
|
|
268
|
-
ws
|
|
291
|
+
sendSocketData(ws, pingMsg);
|
|
269
292
|
if (pongTimer !== undefined) {
|
|
270
293
|
clearTimeout(pongTimer);
|
|
271
294
|
}
|
|
@@ -341,7 +364,7 @@ export const useWebSocket = <TSend = string, TReceive = string>(
|
|
|
341
364
|
if (ws.readyState !== WebSocket.OPEN) {
|
|
342
365
|
break;
|
|
343
366
|
}
|
|
344
|
-
ws
|
|
367
|
+
sendSocketData(ws, sendQueue[index]);
|
|
345
368
|
}
|
|
346
369
|
|
|
347
370
|
if (index > 0) {
|
|
@@ -460,10 +483,10 @@ export const useWebSocket = <TSend = string, TReceive = string>(
|
|
|
460
483
|
sendRaw(serialized);
|
|
461
484
|
};
|
|
462
485
|
|
|
463
|
-
const sendRaw = (raw:
|
|
486
|
+
const sendRaw = (raw: WebSocketSendData): void => {
|
|
464
487
|
if (disposed) return;
|
|
465
488
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
466
|
-
ws
|
|
489
|
+
sendSocketData(ws, raw);
|
|
467
490
|
} else {
|
|
468
491
|
sendQueue.push(raw);
|
|
469
492
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { effect } from '../../reactive/index';
|
|
2
|
+
import { isPrototypePollutionKey } from '../../core/utils/object';
|
|
3
|
+
import { evaluate, parseObjectExpression } from '../evaluate';
|
|
4
|
+
import type { DirectiveHandler } from '../types';
|
|
5
|
+
|
|
6
|
+
const toKebabCase = (value: string): string => value.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
7
|
+
|
|
8
|
+
const normalizeAriaAttribute = (name: string): string => {
|
|
9
|
+
const trimmed = name.trim();
|
|
10
|
+
const lower = trimmed.toLowerCase();
|
|
11
|
+
|
|
12
|
+
if (lower.startsWith('aria-')) {
|
|
13
|
+
return lower;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const withoutPrefix = /^aria[A-Z]/.test(trimmed) ? trimmed.slice(4) : trimmed;
|
|
17
|
+
return `aria-${toKebabCase(withoutPrefix).replace(/^-/, '')}`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const shouldRemoveAttribute = (value: unknown): boolean => value == null || value === '';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handles bq-aria directive - reactive ARIA attribute binding.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export const handleAria: DirectiveHandler = (el, expression, context, cleanups) => {
|
|
27
|
+
let appliedAttributes: Set<string> = new Set();
|
|
28
|
+
|
|
29
|
+
const cleanup = effect(() => {
|
|
30
|
+
const newAttributes = new Set<string>();
|
|
31
|
+
const ariaValues = Object.create(null) as Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
if (expression.trimStart().startsWith('{')) {
|
|
34
|
+
const ariaMap = parseObjectExpression(expression);
|
|
35
|
+
for (const [attrName, valueExpr] of Object.entries(ariaMap)) {
|
|
36
|
+
ariaValues[attrName] = evaluate(valueExpr, context);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
const result = evaluate<Record<string, unknown>>(expression, context);
|
|
40
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
41
|
+
for (const [attrName, value] of Object.entries(result)) {
|
|
42
|
+
if (isPrototypePollutionKey(attrName)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
ariaValues[attrName] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [attrName, value] of Object.entries(ariaValues)) {
|
|
52
|
+
const normalizedName = normalizeAriaAttribute(attrName);
|
|
53
|
+
if (shouldRemoveAttribute(value)) {
|
|
54
|
+
el.removeAttribute(normalizedName);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
el.setAttribute(normalizedName, value === true ? 'true' : String(value));
|
|
59
|
+
newAttributes.add(normalizedName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const attrName of appliedAttributes) {
|
|
63
|
+
if (!newAttributes.has(attrName)) {
|
|
64
|
+
el.removeAttribute(attrName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
appliedAttributes = newAttributes;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
cleanups.push(cleanup);
|
|
72
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { effect, isComputed, isSignal } from '../../reactive/index';
|
|
2
|
+
import { evaluate } from '../evaluate';
|
|
3
|
+
import type { DirectiveHandler } from '../types';
|
|
4
|
+
|
|
5
|
+
type ErrorSource = unknown;
|
|
6
|
+
|
|
7
|
+
const getErrorMessage = (source: ErrorSource): string => {
|
|
8
|
+
if (isSignal(source) || isComputed(source)) {
|
|
9
|
+
return String(source.value ?? '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (source && typeof source === 'object' && 'error' in source) {
|
|
13
|
+
const errorValue = source.error;
|
|
14
|
+
if (isSignal(errorValue) || isComputed(errorValue)) {
|
|
15
|
+
return String(errorValue.value ?? '');
|
|
16
|
+
}
|
|
17
|
+
return String(errorValue ?? '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return String(source ?? '');
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Handles bq-error directive - renders error messages and toggles visibility.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export const handleError: DirectiveHandler = (el, expression, context, cleanups) => {
|
|
28
|
+
const htmlEl = el as HTMLElement;
|
|
29
|
+
const shouldManageAriaHidden = !htmlEl.hasAttribute('aria-hidden');
|
|
30
|
+
|
|
31
|
+
if (!htmlEl.hasAttribute('role')) {
|
|
32
|
+
htmlEl.setAttribute('role', 'alert');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!htmlEl.hasAttribute('aria-live')) {
|
|
36
|
+
htmlEl.setAttribute('aria-live', 'assertive');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const cleanup = effect(() => {
|
|
40
|
+
const source = evaluate(expression, context);
|
|
41
|
+
const message = getErrorMessage(source).trim();
|
|
42
|
+
const hasMessage = message.length > 0;
|
|
43
|
+
|
|
44
|
+
htmlEl.textContent = message;
|
|
45
|
+
htmlEl.hidden = !hasMessage;
|
|
46
|
+
if (shouldManageAriaHidden) {
|
|
47
|
+
if (hasMessage) {
|
|
48
|
+
htmlEl.removeAttribute('aria-hidden');
|
|
49
|
+
} else {
|
|
50
|
+
htmlEl.setAttribute('aria-hidden', 'true');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
cleanups.push(cleanup);
|
|
56
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export { handleAria } from './aria';
|
|
1
2
|
export { handleBind } from './bind';
|
|
2
3
|
export { handleClass } from './class';
|
|
4
|
+
export { handleError } from './error';
|
|
3
5
|
export { createForHandler } from './for';
|
|
4
6
|
export { handleHtml } from './html';
|
|
5
7
|
export { handleIf } from './if';
|
package/src/view/mount.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { CleanupFn } from '../reactive/index';
|
|
2
2
|
import {
|
|
3
3
|
createForHandler,
|
|
4
|
+
handleAria,
|
|
4
5
|
handleBind,
|
|
5
6
|
handleClass,
|
|
7
|
+
handleError,
|
|
6
8
|
handleHtml,
|
|
7
9
|
handleIf,
|
|
8
10
|
handleModel,
|
|
@@ -80,6 +82,8 @@ export const mount = (
|
|
|
80
82
|
|
|
81
83
|
const handlers: DirectiveHandlers = {
|
|
82
84
|
text: handleText,
|
|
85
|
+
error: handleError,
|
|
86
|
+
aria: handleAria,
|
|
83
87
|
html: handleHtml(sanitize),
|
|
84
88
|
if: handleIf,
|
|
85
89
|
show: handleShow,
|
package/src/view/process.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { BindingContext, DirectiveHandler } from './types';
|
|
|
5
5
|
|
|
6
6
|
export type DirectiveHandlers = {
|
|
7
7
|
text: DirectiveHandler;
|
|
8
|
+
error: DirectiveHandler;
|
|
9
|
+
aria: DirectiveHandler;
|
|
8
10
|
html: DirectiveHandler;
|
|
9
11
|
if: DirectiveHandler;
|
|
10
12
|
show: DirectiveHandler;
|
|
@@ -46,6 +48,10 @@ export const processElement = (
|
|
|
46
48
|
// Handle other directives
|
|
47
49
|
if (directive === 'text') {
|
|
48
50
|
handlers.text(el, value, context, cleanups);
|
|
51
|
+
} else if (directive === 'error') {
|
|
52
|
+
handlers.error(el, value, context, cleanups);
|
|
53
|
+
} else if (directive === 'aria') {
|
|
54
|
+
handlers.aria(el, value, context, cleanups);
|
|
49
55
|
} else if (directive === 'html') {
|
|
50
56
|
handlers.html(el, value, context, cleanups);
|
|
51
57
|
} else if (directive === 'if') {
|
package/dist/core-DdtZHzsS.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
var t = [], o = 0, p = /* @__PURE__ */ new Set(), n = /* @__PURE__ */ new WeakMap(), y = (e, r) => {
|
|
2
|
-
t.push(e);
|
|
3
|
-
try {
|
|
4
|
-
return r();
|
|
5
|
-
} finally {
|
|
6
|
-
t.pop();
|
|
7
|
-
}
|
|
8
|
-
}, g = () => t[t.length - 1], A = (e) => {
|
|
9
|
-
t.push(void 0);
|
|
10
|
-
try {
|
|
11
|
-
return e();
|
|
12
|
-
} finally {
|
|
13
|
-
t.pop();
|
|
14
|
-
}
|
|
15
|
-
}, S = (e) => {
|
|
16
|
-
if (o > 0) {
|
|
17
|
-
p.add(e);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
e();
|
|
21
|
-
}, _ = () => {
|
|
22
|
-
for (const e of Array.from(p)) {
|
|
23
|
-
p.delete(e);
|
|
24
|
-
try {
|
|
25
|
-
e();
|
|
26
|
-
} catch (r) {
|
|
27
|
-
console.error("bQuery reactive: Error in observer during batch flush", r);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}, O = () => {
|
|
31
|
-
o += 1;
|
|
32
|
-
}, C = () => {
|
|
33
|
-
o <= 0 || (o -= 1, o === 0 && _());
|
|
34
|
-
}, w = (e, r) => {
|
|
35
|
-
let s = n.get(e);
|
|
36
|
-
s || (s = /* @__PURE__ */ new Set(), n.set(e, s)), s.add(r);
|
|
37
|
-
}, D = (e, r) => {
|
|
38
|
-
const s = n.get(e);
|
|
39
|
-
s && s.delete(r);
|
|
40
|
-
}, d = (e) => {
|
|
41
|
-
const r = n.get(e);
|
|
42
|
-
if (r) {
|
|
43
|
-
for (const s of r) s.unsubscribe(e);
|
|
44
|
-
r.clear();
|
|
45
|
-
}
|
|
46
|
-
}, c = [], f = (e) => typeof e == "object" && e !== null && "_addDisposer" in e, m = (e) => (typeof e == "object" || typeof e == "function") && e !== null && typeof e.then == "function", E = (e) => {
|
|
47
|
-
const r = typeof e.constructor == "function" ? e.constructor.name : void 0;
|
|
48
|
-
return typeof e == "function" && (Symbol.toStringTag in e && e[Symbol.toStringTag] === "AsyncFunction" || r === "AsyncFunction");
|
|
49
|
-
}, a = () => {
|
|
50
|
-
for (let e = c.length - 1; e >= 0; e--) if (c[e].active) return c[e];
|
|
51
|
-
}, k = class {
|
|
52
|
-
constructor() {
|
|
53
|
-
this.disposers = [], this._active = !0;
|
|
54
|
-
}
|
|
55
|
-
get active() {
|
|
56
|
-
return this._active;
|
|
57
|
-
}
|
|
58
|
-
_addDisposer(e) {
|
|
59
|
-
this._active && this.disposers.push(e);
|
|
60
|
-
}
|
|
61
|
-
run(e) {
|
|
62
|
-
if (!this._active) throw new Error("bQuery reactive: Cannot run in a stopped effectScope");
|
|
63
|
-
if (E(e)) throw new Error("bQuery reactive: effectScope.run() only supports synchronous callbacks");
|
|
64
|
-
c.push(this);
|
|
65
|
-
try {
|
|
66
|
-
const r = e();
|
|
67
|
-
if (m(r))
|
|
68
|
-
throw this.stop(), new Error("bQuery reactive: effectScope.run() only supports synchronous callbacks");
|
|
69
|
-
return r;
|
|
70
|
-
} finally {
|
|
71
|
-
c.pop();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
stop() {
|
|
75
|
-
if (this._active) {
|
|
76
|
-
this._active = !1;
|
|
77
|
-
for (let e = this.disposers.length - 1; e >= 0; e--) try {
|
|
78
|
-
this.disposers[e]();
|
|
79
|
-
} catch (r) {
|
|
80
|
-
console.error("bQuery reactive: Error in scope cleanup", r);
|
|
81
|
-
}
|
|
82
|
-
this.disposers.length = 0;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}, F = () => {
|
|
86
|
-
const e = new k(), r = a();
|
|
87
|
-
return f(r) && r._addDisposer(() => e.stop()), e;
|
|
88
|
-
}, j = () => a(), B = (e) => {
|
|
89
|
-
const r = a();
|
|
90
|
-
if (!r || !r.active || !f(r)) throw new Error("bQuery reactive: onScopeDispose() must be called inside an active effectScope");
|
|
91
|
-
r._addDisposer(e);
|
|
92
|
-
}, T = (e) => {
|
|
93
|
-
let r, s = !1;
|
|
94
|
-
const h = a(), l = () => {
|
|
95
|
-
if (r) {
|
|
96
|
-
try {
|
|
97
|
-
r();
|
|
98
|
-
} catch (u) {
|
|
99
|
-
console.error("bQuery reactive: Error in effect cleanup", u);
|
|
100
|
-
}
|
|
101
|
-
r = void 0;
|
|
102
|
-
}
|
|
103
|
-
}, v = () => {
|
|
104
|
-
l(), d(i);
|
|
105
|
-
}, b = () => {
|
|
106
|
-
s || (s = !0, v());
|
|
107
|
-
};
|
|
108
|
-
f(h) && h._addDisposer(b);
|
|
109
|
-
const i = () => {
|
|
110
|
-
if (!s) {
|
|
111
|
-
l(), d(i);
|
|
112
|
-
try {
|
|
113
|
-
r = y(i, e);
|
|
114
|
-
} catch (u) {
|
|
115
|
-
console.error("bQuery reactive: Error in effect", u);
|
|
116
|
-
}
|
|
117
|
-
s && v();
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
return i(), b;
|
|
121
|
-
}, Q = class {
|
|
122
|
-
constructor(e) {
|
|
123
|
-
this._value = e, this.subscribers = /* @__PURE__ */ new Set();
|
|
124
|
-
}
|
|
125
|
-
get value() {
|
|
126
|
-
const e = g();
|
|
127
|
-
return e && (this.subscribers.add(e), w(e, this)), this._value;
|
|
128
|
-
}
|
|
129
|
-
set value(e) {
|
|
130
|
-
if (Object.is(this._value, e)) return;
|
|
131
|
-
this._value = e;
|
|
132
|
-
const r = Array.from(this.subscribers);
|
|
133
|
-
for (const s of r) S(s);
|
|
134
|
-
}
|
|
135
|
-
peek() {
|
|
136
|
-
return this._value;
|
|
137
|
-
}
|
|
138
|
-
update(e) {
|
|
139
|
-
this.value = e(this._value);
|
|
140
|
-
}
|
|
141
|
-
dispose() {
|
|
142
|
-
for (const e of this.subscribers) D(e, this);
|
|
143
|
-
this.subscribers.clear();
|
|
144
|
-
}
|
|
145
|
-
unsubscribe(e) {
|
|
146
|
-
this.subscribers.delete(e);
|
|
147
|
-
}
|
|
148
|
-
}, I = (e) => new Q(e);
|
|
149
|
-
export {
|
|
150
|
-
a,
|
|
151
|
-
B as c,
|
|
152
|
-
C as d,
|
|
153
|
-
g as f,
|
|
154
|
-
A as g,
|
|
155
|
-
y as h,
|
|
156
|
-
F as i,
|
|
157
|
-
O as l,
|
|
158
|
-
S as m,
|
|
159
|
-
I as n,
|
|
160
|
-
j as o,
|
|
161
|
-
w as p,
|
|
162
|
-
T as r,
|
|
163
|
-
f as s,
|
|
164
|
-
Q as t,
|
|
165
|
-
d as u
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
//# sourceMappingURL=core-DdtZHzsS.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"core-DdtZHzsS.js","names":[],"sources":["../src/reactive/internals.ts","../src/reactive/scope.ts","../src/reactive/effect.ts","../src/reactive/core.ts"],"sourcesContent":["/**\n * Internal reactive plumbing shared across primitives.\n * @internal\n */\n\nexport type Observer = () => void;\nexport type CleanupFn = () => void;\n\n/**\n * Interface for reactive sources (Signals, Computed) that can unsubscribe observers.\n * @internal\n */\nexport interface ReactiveSource {\n unsubscribe(observer: Observer): void;\n}\n\nconst observerStack: Observer[] = [];\nlet batchDepth = 0;\nconst pendingObservers = new Set<Observer>();\n\n// Track dependencies for each observer to enable cleanup\nconst observerDependencies = new WeakMap<Observer, Set<ReactiveSource>>();\n\nexport const track = <T>(observer: Observer, fn: () => T): T => {\n observerStack.push(observer);\n try {\n return fn();\n } finally {\n observerStack.pop();\n }\n};\n\nexport const getCurrentObserver = (): Observer | undefined =>\n observerStack[observerStack.length - 1];\n\n/**\n * Executes a function without exposing the current observer to dependencies.\n * Unlike disabling tracking globally, this still allows nested reactive internals\n * (e.g., computed recomputation) to track their own dependencies.\n * @internal\n */\nexport const withoutCurrentObserver = <T>(fn: () => T): T => {\n // Push undefined to temporarily \"hide\" the current observer\n // This way, Signal.value reads won't link to the previous observer,\n // but nested track() calls (e.g., computed recompute) still work normally.\n observerStack.push(undefined as unknown as Observer);\n try {\n return fn();\n } finally {\n observerStack.pop();\n }\n};\n\nexport const scheduleObserver = (observer: Observer): void => {\n if (batchDepth > 0) {\n pendingObservers.add(observer);\n return;\n }\n observer();\n};\n\nconst flushObservers = (): void => {\n for (const observer of Array.from(pendingObservers)) {\n pendingObservers.delete(observer);\n try {\n observer();\n } catch (error) {\n console.error('bQuery reactive: Error in observer during batch flush', error);\n }\n }\n};\n\nexport const beginBatch = (): void => {\n batchDepth += 1;\n};\n\nexport const endBatch = (): void => {\n if (batchDepth <= 0) return;\n batchDepth -= 1;\n if (batchDepth === 0) {\n flushObservers();\n }\n};\n\n/**\n * Registers a dependency between an observer and a reactive source.\n * @internal\n */\nexport const registerDependency = (observer: Observer, source: ReactiveSource): void => {\n let deps = observerDependencies.get(observer);\n if (!deps) {\n deps = new Set();\n observerDependencies.set(observer, deps);\n }\n deps.add(source);\n};\n\n/**\n * Removes a specific source from an observer's dependency set.\n * Used when a source (e.g. Signal) is disposed to prevent stale references.\n * @internal\n */\nexport const removeDependency = (observer: Observer, source: ReactiveSource): void => {\n const deps = observerDependencies.get(observer);\n if (deps) {\n deps.delete(source);\n }\n};\n\n/**\n * Clears all dependencies for an observer, unsubscribing from all sources.\n * @internal\n */\nexport const clearDependencies = (observer: Observer): void => {\n const deps = observerDependencies.get(observer);\n if (deps) {\n for (const source of deps) {\n source.unsubscribe(observer);\n }\n deps.clear();\n }\n};\n","/**\n * Reactive effect scopes for grouped disposal.\n *\n * An `EffectScope` collects all effects, computed values, and watches created\n * inside its `run()` callback so they can be disposed together with a single\n * `stop()` call. Scopes nest — an inner scope is collected by its parent.\n *\n * @module bquery/reactive\n */\n\nimport type { CleanupFn } from './internals';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * A scope that collects reactive resources for grouped disposal.\n *\n * @example\n * ```ts\n * import { effectScope, signal, effect, computed } from '@bquery/bquery/reactive';\n *\n * const scope = effectScope();\n *\n * scope.run(() => {\n * const count = signal(0);\n * effect(() => console.log(count.value));\n * const doubled = computed(() => count.value * 2);\n * });\n *\n * scope.stop(); // All effects and computed values disposed\n * ```\n */\nexport interface EffectScope {\n /** Whether the scope has not yet been stopped. */\n readonly active: boolean;\n\n /**\n * Executes `fn` inside this scope, collecting any reactive resources\n * (effects, computed values, watches, nested scopes) created during the call.\n *\n * `run()` is synchronous-only. Do not pass an async function or a function\n * that returns a Promise — resources created after an `await` cannot be\n * collected reliably.\n *\n * @template T - Return type of the provided function\n * @param fn - Function to run inside the scope\n * @returns The return value of `fn`\n * @throws {Error} If the scope has already been stopped\n */\n run<T>(fn: () => T): T;\n\n /**\n * Disposes all collected resources and marks the scope as inactive.\n * Calling `stop()` on an already-stopped scope is a safe no-op.\n */\n stop(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Internal scope stack\n// ---------------------------------------------------------------------------\n\n/** @internal */\ninterface ScopeInternal extends EffectScope {\n /** @internal – Register a cleanup callback to run when the scope stops. */\n _addDisposer(fn: CleanupFn): void;\n}\n\nconst scopeStack: ScopeInternal[] = [];\n\n/** @internal */\nexport const hasScopeDisposer = (\n scope: EffectScope | undefined\n): scope is EffectScope & { _addDisposer(fn: CleanupFn): void } =>\n typeof scope === 'object' && scope !== null && '_addDisposer' in scope;\n\nconst isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n (typeof value === 'object' || typeof value === 'function') &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function';\n\n/**\n * Best-effort detection for native async functions so `run()` can reject them\n * before invocation. Transpiled async functions may not preserve this shape, so\n * promise-like return values are still checked after execution as a fallback.\n * @internal\n */\nconst isAsyncFunction = (value: unknown): value is (...args: never[]) => Promise<unknown> => {\n const constructorName =\n typeof (value as { constructor?: unknown }).constructor === 'function'\n ? (value as { constructor: { name?: unknown } }).constructor.name\n : undefined;\n\n return (\n typeof value === 'function' &&\n ((Symbol.toStringTag in value &&\n (value as { [Symbol.toStringTag]?: unknown })[Symbol.toStringTag] === 'AsyncFunction') ||\n constructorName === 'AsyncFunction')\n );\n};\n\n/**\n * Returns the currently active scope, or `undefined` if none.\n * @internal\n */\nexport const getActiveScope = (): EffectScope | undefined => {\n for (let i = scopeStack.length - 1; i >= 0; i--) {\n if (scopeStack[i].active) {\n return scopeStack[i];\n }\n }\n\n return undefined;\n};\n\n// ---------------------------------------------------------------------------\n// EffectScope implementation\n// ---------------------------------------------------------------------------\n\nclass EffectScopeImpl implements ScopeInternal {\n private disposers: CleanupFn[] = [];\n private _active = true;\n\n get active(): boolean {\n return this._active;\n }\n\n /** @internal */\n _addDisposer(fn: CleanupFn): void {\n if (this._active) {\n this.disposers.push(fn);\n }\n }\n\n run<T>(fn: () => T): T {\n if (!this._active) {\n throw new Error('bQuery reactive: Cannot run in a stopped effectScope');\n }\n if (isAsyncFunction(fn)) {\n throw new Error('bQuery reactive: effectScope.run() only supports synchronous callbacks');\n }\n\n scopeStack.push(this);\n try {\n const result = fn();\n if (isPromiseLike(result)) {\n this.stop();\n throw new Error('bQuery reactive: effectScope.run() only supports synchronous callbacks');\n }\n return result;\n } finally {\n scopeStack.pop();\n }\n }\n\n stop(): void {\n if (!this._active) return;\n this._active = false;\n\n // Dispose in reverse order (LIFO) to mirror creation order\n for (let i = this.disposers.length - 1; i >= 0; i--) {\n try {\n this.disposers[i]();\n } catch (error) {\n console.error('bQuery reactive: Error in scope cleanup', error);\n }\n }\n this.disposers.length = 0;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a new effect scope for grouped disposal of reactive resources.\n *\n * All `effect()`, `computed()`, `watch()`, and nested `effectScope()` calls\n * made inside `scope.run(fn)` are automatically collected. Calling\n * `scope.stop()` disposes them all at once.\n *\n * `run()` is synchronous-only. Create the scope outside async flows when\n * needed, but keep the callback itself synchronous so cleanup registration\n * stays deterministic.\n *\n * @returns A new {@link EffectScope}\n *\n * @example\n * ```ts\n * import { effectScope, signal, effect, onScopeDispose } from '@bquery/bquery/reactive';\n *\n * const scope = effectScope();\n *\n * scope.run(() => {\n * const count = signal(0);\n *\n * effect(() => console.log(count.value));\n *\n * onScopeDispose(() => {\n * console.log('Custom cleanup');\n * });\n * });\n *\n * scope.stop(); // logs \"Custom cleanup\", all effects stopped\n * ```\n */\nexport const effectScope = (): EffectScope => {\n const scope = new EffectScopeImpl();\n\n // If created inside another scope, auto-collect as a nested scope\n const parent = getActiveScope();\n if (hasScopeDisposer(parent)) {\n parent._addDisposer(() => scope.stop());\n }\n\n return scope;\n};\n\n/**\n * Returns the currently active {@link EffectScope}, or `undefined` if\n * code is not running inside any scope's `run()` callback.\n *\n * @returns The active scope, or `undefined`\n *\n * @example\n * ```ts\n * import { effectScope, getCurrentScope } from '@bquery/bquery/reactive';\n *\n * const scope = effectScope();\n * scope.run(() => {\n * console.log(getCurrentScope() !== undefined); // true\n * });\n *\n * console.log(getCurrentScope()); // undefined\n * ```\n */\nexport const getCurrentScope = (): EffectScope | undefined => getActiveScope();\n\n/**\n * Registers a cleanup callback on the currently active scope.\n *\n * The callback runs when the scope is stopped. This is useful for\n * registering arbitrary cleanup (e.g. event listeners, timers)\n * alongside effects and computed values.\n *\n * @param fn - Cleanup function to run when the scope stops\n * @throws {Error} If called outside an active scope\n *\n * @example\n * ```ts\n * import { effectScope, onScopeDispose } from '@bquery/bquery/reactive';\n *\n * const scope = effectScope();\n *\n * scope.run(() => {\n * const controller = new AbortController();\n * fetch('/api/data', { signal: controller.signal });\n *\n * onScopeDispose(() => controller.abort());\n * });\n *\n * scope.stop(); // abort() is called\n * ```\n */\nexport const onScopeDispose = (fn: CleanupFn): void => {\n const scope = getActiveScope();\n if (!scope || !scope.active || !hasScopeDisposer(scope)) {\n throw new Error(\n 'bQuery reactive: onScopeDispose() must be called inside an active effectScope'\n );\n }\n scope._addDisposer(fn);\n};\n","/**\n * Reactive effects.\n */\n\nimport { CleanupFn, Observer, track, clearDependencies } from './internals';\nimport { getActiveScope, hasScopeDisposer } from './scope';\n\n/**\n * Creates a side effect that automatically re-runs when dependencies change.\n *\n * The effect runs immediately upon creation and then re-runs whenever\n * any signal or computed value read inside it changes.\n *\n * If created inside an {@link effectScope}, the effect is automatically\n * collected and will be disposed when the scope stops.\n *\n * @param fn - The effect function to run\n * @returns A cleanup function to stop the effect\n */\nexport const effect = (fn: () => void | CleanupFn): CleanupFn => {\n let cleanupFn: CleanupFn | void;\n let isDisposed = false;\n const scope = getActiveScope();\n\n const runCleanup = (): void => {\n if (cleanupFn) {\n try {\n cleanupFn();\n } catch (error) {\n console.error('bQuery reactive: Error in effect cleanup', error);\n }\n cleanupFn = undefined;\n }\n };\n\n const clearEffectState = (): void => {\n runCleanup();\n // Clean up all dependencies when effect is disposed\n clearDependencies(observer);\n };\n\n const dispose: CleanupFn = () => {\n if (isDisposed) {\n return;\n }\n\n isDisposed = true;\n clearEffectState();\n };\n\n if (hasScopeDisposer(scope)) {\n scope._addDisposer(dispose);\n }\n\n const observer: Observer = () => {\n if (isDisposed) return;\n\n runCleanup();\n\n // Clear old dependencies before running to avoid stale subscriptions\n clearDependencies(observer);\n\n try {\n cleanupFn = track(observer, fn);\n } catch (error) {\n console.error('bQuery reactive: Error in effect', error);\n }\n\n if (isDisposed) {\n clearEffectState();\n }\n };\n\n observer();\n\n return dispose;\n};\n","/**\n * Core reactive signals.\n */\n\nimport {\n getCurrentObserver,\n registerDependency,\n removeDependency,\n scheduleObserver,\n type ReactiveSource,\n} from './internals';\n\n/**\n * A reactive value container that notifies subscribers on change.\n *\n * Signals are the foundational primitive of the reactive system.\n * Reading a signal's value inside an effect or computed automatically\n * establishes a reactive dependency.\n *\n * @template T - The type of the stored value\n */\nexport class Signal<T> implements ReactiveSource {\n private subscribers = new Set<() => void>();\n\n /**\n * Creates a new signal with an initial value.\n * @param _value - The initial value\n */\n constructor(private _value: T) {}\n\n /**\n * Gets the current value and tracks the read if inside an observer.\n * During untrack calls, getCurrentObserver returns undefined, preventing dependency tracking.\n */\n get value(): T {\n const current = getCurrentObserver();\n if (current) {\n this.subscribers.add(current);\n registerDependency(current, this);\n }\n return this._value;\n }\n\n /**\n * Sets a new value and notifies all subscribers if the value changed.\n * Uses Object.is for equality comparison.\n */\n set value(next: T) {\n if (Object.is(this._value, next)) return;\n this._value = next;\n // Create snapshot to avoid issues with subscribers modifying the set during iteration\n const subscribersSnapshot = Array.from(this.subscribers);\n for (const subscriber of subscribersSnapshot) {\n scheduleObserver(subscriber);\n }\n }\n\n /**\n * Reads the current value without tracking.\n * Useful when you need the value but don't want to create a dependency.\n *\n * @returns The current value\n */\n peek(): T {\n return this._value;\n }\n\n /**\n * Updates the value using a function.\n * Useful for updates based on the current value.\n *\n * @param updater - Function that receives current value and returns new value\n */\n update(updater: (current: T) => T): void {\n this.value = updater(this._value);\n }\n\n /**\n * Removes all subscribers from this signal.\n * Use this when a signal is no longer needed to prevent memory leaks.\n *\n * @example\n * ```ts\n * const count = signal(0);\n * effect(() => console.log(count.value));\n * count.dispose(); // All subscribers removed\n * ```\n */\n dispose(): void {\n // Remove this signal from each subscriber's dependency set\n // so the observer no longer holds a strong reference to it\n for (const subscriber of this.subscribers) {\n removeDependency(subscriber, this);\n }\n this.subscribers.clear();\n }\n\n /**\n * Removes an observer from this signal's subscriber set.\n * @internal\n */\n unsubscribe(observer: () => void): void {\n this.subscribers.delete(observer);\n }\n}\n\n/**\n * Creates a new reactive signal.\n *\n * @template T - The type of the signal value\n * @param value - The initial value\n * @returns A new Signal instance\n */\nexport const signal = <T>(value: T): Signal<T> => new Signal(value);\n"],"mappings":"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"}
|