@vizejs/composable 0.345.0 → 0.347.7

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 (42) hide show
  1. package/dist/abort-signal.d.mts +96 -0
  2. package/dist/abort-signal.mjs +203 -0
  3. package/dist/async-resource.d.mts +97 -0
  4. package/dist/async-resource.mjs +99 -0
  5. package/dist/capability-BNpkvSy5.d.mts +93 -0
  6. package/dist/capability.d.mts +2 -0
  7. package/dist/capability.mjs +37 -0
  8. package/dist/catalog-CuWhR-0q.d.mts +506 -0
  9. package/dist/catalog-DOXTbk4r.mjs +529 -0
  10. package/dist/catalog.d.mts +2 -0
  11. package/dist/catalog.mjs +2 -0
  12. package/dist/disposal-scope.d.mts +93 -0
  13. package/dist/disposal-scope.mjs +119 -0
  14. package/dist/event-listener.d.mts +85 -0
  15. package/dist/event-listener.mjs +55 -0
  16. package/dist/index.d.mts +19 -786
  17. package/dist/index.mjs +19 -751
  18. package/dist/locale.d.mts +65 -0
  19. package/dist/locale.mjs +72 -0
  20. package/dist/media-query.d.mts +56 -0
  21. package/dist/media-query.mjs +56 -0
  22. package/dist/retry-async.d.mts +91 -0
  23. package/dist/retry-async.mjs +136 -0
  24. package/dist/retry-delay.d.mts +73 -0
  25. package/dist/retry-delay.mjs +50 -0
  26. package/dist/scope.d.mts +18 -0
  27. package/dist/scope.mjs +23 -0
  28. package/dist/timeout-scheduler.d.mts +18 -0
  29. package/dist/timeout-scheduler.mjs +1 -0
  30. package/dist/use-counter.d.mts +91 -0
  31. package/dist/use-counter.mjs +67 -0
  32. package/dist/use-debounced.d.mts +87 -0
  33. package/dist/use-debounced.mjs +98 -0
  34. package/dist/use-history.d.mts +105 -0
  35. package/dist/use-history.mjs +137 -0
  36. package/dist/use-previous.d.mts +43 -0
  37. package/dist/use-previous.mjs +11 -0
  38. package/dist/use-throttled.d.mts +107 -0
  39. package/dist/use-throttled.mjs +124 -0
  40. package/dist/use-toggle.d.mts +45 -0
  41. package/dist/use-toggle.mjs +34 -0
  42. package/package.json +91 -1
package/dist/index.mjs CHANGED
@@ -1,751 +1,19 @@
1
- import { computed, getCurrentScope, onScopeDispose, readonly, ref, shallowRef, toValue, watch, watchEffect } from "vue";
2
- //#region src/scope.ts
3
- /**
4
- * Register cleanup in the active reactive scope when one exists.
5
- *
6
- * This is the shared lifecycle primitive of the package: composables hand
7
- * their teardown here so owned resources are released when the surrounding
8
- * scope stops.
9
- *
10
- * Never throws and is safe during server rendering; no browser globals are
11
- * read. When no scope is active the cleanup is not registered and disposal
12
- * ownership stays with the caller.
13
- *
14
- * @param cleanup Cleanup invoked exactly once when the scope is disposed.
15
- * @returns Whether the cleanup was registered.
16
- */
17
- function tryOnScopeDispose(cleanup) {
18
- if (!getCurrentScope()) return false;
19
- onScopeDispose(cleanup);
20
- return true;
21
- }
22
- //#endregion
23
- //#region src/async-resource.ts
24
- /**
25
- * Create a scoped, abortable asynchronous resource with latest-result-wins
26
- * state. Every execution returns a discriminated result, so cancellation,
27
- * supersession, loader failure, and successful `undefined` data stay distinct.
28
- *
29
- * When created inside an active reactive scope (and `scope` is enabled), the
30
- * active execution is aborted when that scope stops; outside a scope,
31
- * cancellation ownership stays with the caller. The execute promise never
32
- * rejects — synchronous and asynchronous loader failures both settle into
33
- * the `"error"` result. Safe during server rendering: no browser globals are
34
- * read and abort reasons use the runtime-native `DOMException`.
35
- *
36
- * @param loader Asynchronous loader receiving the abort context first.
37
- * @param options Data retention, supersession, and scope behavior.
38
- * @default options {}
39
- * @returns Reactive state and controls for the loader.
40
- */
41
- function useAsyncResource(loader, options = {}) {
42
- const data = shallowRef(options.initialData);
43
- const error = shallowRef(void 0);
44
- const status = shallowRef("idle");
45
- const pending = computed(() => status.value === "pending");
46
- let generation = 0;
47
- let active;
48
- const cancel = (reason = createAbortReason("The execution was cancelled.")) => {
49
- if (active === void 0) return false;
50
- generation += 1;
51
- active.controller.abort(reason);
52
- active = void 0;
53
- status.value = "cancelled";
54
- return true;
55
- };
56
- const execute = async (...arguments_) => {
57
- if ((options.cancelPrevious ?? true) && active !== void 0) {
58
- active.superseded = true;
59
- active.controller.abort(createAbortReason("A newer execution started."));
60
- }
61
- const record = {
62
- generation: ++generation,
63
- controller: new AbortController(),
64
- superseded: false
65
- };
66
- active = record;
67
- error.value = void 0;
68
- status.value = "pending";
69
- if (!(options.keepData ?? true)) data.value = void 0;
70
- try {
71
- const result = await loader({ signal: record.controller.signal }, ...arguments_);
72
- if (record.generation !== generation) return executionAfterInvalidation(record);
73
- data.value = result;
74
- status.value = "success";
75
- return {
76
- status: "success",
77
- data: result
78
- };
79
- } catch (cause) {
80
- if (record.generation !== generation || record.controller.signal.aborted) return executionAfterInvalidation(record);
81
- error.value = cause;
82
- status.value = "error";
83
- return {
84
- status: "error",
85
- error: cause
86
- };
87
- } finally {
88
- if (active === record) active = void 0;
89
- }
90
- };
91
- const reset = () => {
92
- cancel(createAbortReason("The resource was reset."));
93
- data.value = options.initialData;
94
- error.value = void 0;
95
- status.value = "idle";
96
- };
97
- if (options.scope ?? true) tryOnScopeDispose(() => cancel(createAbortReason("The reactive scope was disposed.")));
98
- return {
99
- data,
100
- error,
101
- status,
102
- pending,
103
- execute,
104
- cancel,
105
- reset
106
- };
107
- }
108
- function executionAfterInvalidation(execution) {
109
- if (execution.superseded || !execution.controller.signal.aborted) return { status: "superseded" };
110
- return {
111
- status: "cancelled",
112
- reason: execution.controller.signal.reason
113
- };
114
- }
115
- function createAbortReason(message) {
116
- return new DOMException(message, "AbortError");
117
- }
118
- //#endregion
119
- //#region src/event-listener.ts
120
- function useEventListener(target, event, listener, options = {}) {
121
- const { capture = false, once = false, passive = false, signal, immediate = true, flush = "pre" } = options;
122
- const isListening = ref(false);
123
- const eventOptions = {
124
- capture,
125
- passive,
126
- ...signal ? { signal } : {}
127
- };
128
- let disposed = false;
129
- let stopWatch;
130
- const stop = () => {
131
- stopWatch?.stop();
132
- stopWatch = void 0;
133
- isListening.value = false;
134
- };
135
- const start = () => {
136
- if (disposed || stopWatch || signal?.aborted) return false;
137
- stopWatch = watch(() => toValue(target), (next, _previous, onCleanup) => {
138
- isListening.value = false;
139
- if (!next || signal?.aborted) return;
140
- const invoke = (nativeEvent) => {
141
- if (once) stop();
142
- listener(nativeEvent);
143
- };
144
- const onAbort = () => stop();
145
- next.addEventListener(event, invoke, eventOptions);
146
- signal?.addEventListener("abort", onAbort, { once: true });
147
- isListening.value = true;
148
- onCleanup(() => {
149
- next.removeEventListener(event, invoke, capture);
150
- signal?.removeEventListener("abort", onAbort);
151
- isListening.value = false;
152
- });
153
- }, {
154
- flush,
155
- immediate: true
156
- });
157
- return true;
158
- };
159
- tryOnScopeDispose(() => {
160
- disposed = true;
161
- stop();
162
- });
163
- if (immediate) start();
164
- return {
165
- isListening: readonly(isListening),
166
- start,
167
- stop
168
- };
169
- }
170
- //#endregion
171
- //#region src/locale.ts
172
- const FORMATTER_CACHE_LIMIT = 32;
173
- /**
174
- * Create reactive locale metadata and platform-native formatter factories.
175
- *
176
- * Equivalent formatter options reuse instances. The bounded cache follows the
177
- * active locale automatically and prevents repeated constructor overhead in
178
- * reactive render paths.
179
- *
180
- * Detection is lazy and guarded: the default detector reads
181
- * `navigator.language` behind a `typeof` check at call time, so importing and
182
- * calling this during server rendering is safe, and runtimes without a
183
- * `navigator` resolve to the fallback locale. The composable owns no timers
184
- * or listeners, so no scope cleanup is required.
185
- *
186
- * @param source Reactive locale source. Empty values defer to the detector.
187
- * @param options Detection and fallback behavior.
188
- * @default options {}
189
- * @throws `RangeError` on first read of the reactive values when the winning
190
- * candidate is not a structurally valid locale identifier.
191
- * @returns Reactive locale metadata and cached formatter factories.
192
- */
193
- function useLocale(source, options = {}) {
194
- const locale = computed(() => {
195
- const candidate = (source === void 0 ? void 0 : toValue(source)) ?? (options.detect ?? detectBrowserLocale)() ?? options.fallback ?? "en";
196
- return candidate instanceof Intl.Locale ? candidate.toString() : new Intl.Locale(candidate).toString();
197
- });
198
- const details = computed(() => new Intl.Locale(locale.value));
199
- const direction = computed(() => details.value.getTextInfo().direction ?? "ltr");
200
- const number = createFormatterCache((activeLocale, formatOptions) => new Intl.NumberFormat(activeLocale, formatOptions));
201
- const dateTime = createFormatterCache((activeLocale, formatOptions) => new Intl.DateTimeFormat(activeLocale, formatOptions));
202
- const list = createFormatterCache((activeLocale, formatOptions) => new Intl.ListFormat(activeLocale, formatOptions));
203
- const relativeTime = createFormatterCache((activeLocale, formatOptions) => new Intl.RelativeTimeFormat(activeLocale, formatOptions));
204
- return {
205
- locale,
206
- details,
207
- direction,
208
- number: (formatOptions) => number(locale.value, formatOptions),
209
- dateTime: (formatOptions) => dateTime(locale.value, formatOptions),
210
- list: (formatOptions) => list(locale.value, formatOptions),
211
- relativeTime: (formatOptions) => relativeTime(locale.value, formatOptions)
212
- };
213
- }
214
- function detectBrowserLocale() {
215
- return typeof navigator === "undefined" ? void 0 : navigator.language;
216
- }
217
- function createFormatterCache(create) {
218
- const cache = /* @__PURE__ */ new Map();
219
- return (locale, options) => {
220
- const key = `${locale}\u0000${serializeOptions(options)}`;
221
- const cached = cache.get(key);
222
- if (cached !== void 0) {
223
- cache.delete(key);
224
- cache.set(key, cached);
225
- return cached;
226
- }
227
- const formatter = create(locale, options);
228
- if (cache.size >= FORMATTER_CACHE_LIMIT) {
229
- const oldest = cache.keys().next().value;
230
- if (oldest !== void 0) cache.delete(oldest);
231
- }
232
- cache.set(key, formatter);
233
- return formatter;
234
- };
235
- }
236
- function serializeOptions(options) {
237
- if (options === void 0) return "";
238
- return JSON.stringify(Object.entries(options).sort(([left], [right]) => left.localeCompare(right)));
239
- }
240
- //#endregion
241
- //#region src/media-query.ts
242
- /**
243
- * Evaluate a reactive media query without requiring browser globals.
244
- *
245
- * During server rendering (or whenever no capability host resolves) the ref
246
- * holds the configured server value and no subscription is created. The
247
- * change subscription follows the reactive query and host: each
248
- * re-evaluation removes the previous listener, and the final listener is
249
- * removed when the owning reactive scope stops. Call inside an active scope
250
- * so the subscription is released. A host whose matcher throws propagates
251
- * the error to the active effect run; the browser default never throws.
252
- *
253
- * @param query Reactive media-query source.
254
- * @param options Runtime capability and server-rendered fallback.
255
- * @default options {}
256
- * @returns Readonly ref that is `true` while the query matches.
257
- */
258
- function useMediaQuery(query, options = {}) {
259
- const matches = ref(options.ssrValue ?? false);
260
- watchEffect((onCleanup) => {
261
- const host = options.host === void 0 ? browserMediaQueryHost() : toValue(options.host);
262
- if (!host) {
263
- matches.value = options.ssrValue ?? false;
264
- return;
265
- }
266
- const media = host.matchMedia(toValue(query));
267
- const update = () => {
268
- matches.value = media.matches;
269
- };
270
- update();
271
- media.addEventListener("change", update);
272
- onCleanup(() => media.removeEventListener("change", update));
273
- });
274
- return readonly(matches);
275
- }
276
- /**
277
- * Return the reactive user motion preference.
278
- *
279
- * Shares {@link useMediaQuery} semantics: during server rendering the
280
- * preference is `"no-preference"` unless `ssrValue` is `true`, and the
281
- * underlying subscription is removed when the owning reactive scope stops.
282
- *
283
- * @param options Runtime capability and server-rendered fallback.
284
- * @default options {}
285
- * @returns Computed preference for `(prefers-reduced-motion: reduce)`.
286
- */
287
- function useReducedMotion(options = {}) {
288
- const reduced = useMediaQuery("(prefers-reduced-motion: reduce)", options);
289
- return computed(() => reduced.value ? "reduce" : "no-preference");
290
- }
291
- function browserMediaQueryHost() {
292
- return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window : void 0;
293
- }
294
- //#endregion
295
- //#region src/use-counter.ts
296
- /**
297
- * Create a clamped counter whose every transition stays inside `[min, max]`.
298
- *
299
- * All operations clamp instead of failing, including the initial value, so
300
- * the count is inside the bounds at every observable moment. Only `NaN` is
301
- * rejected — silently corrupting the count is never an option. Purely
302
- * synchronous state: safe during server rendering (no browser globals, no
303
- * timers) and nothing to dispose, so it works inside and outside reactive
304
- * scopes alike. Bounds are fixed at creation and not reactive.
305
- *
306
- * @example
307
- * ```ts
308
- * const { count, increment, atMax } = useCounter(9, { min: 0, max: 10 });
309
- * increment(); // 10
310
- * increment(); // 10 (clamped)
311
- * atMax.value; // true
312
- * ```
313
- *
314
- * @param initial Count before any operation, clamped into the bounds.
315
- * @default initial 0
316
- * @param options Inclusive bounds for every value the counter takes.
317
- * @default options {}
318
- * @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_RANGE` when a
319
- * bound is `NaN` or `min` exceeds `max`.
320
- * @throws `RangeError` tagged `VIZE_COMPOSE_COUNTER_INVALID_VALUE` when an
321
- * initial value, operand, or arithmetic result is `NaN` (for example
322
- * incrementing `-Infinity` by `Infinity`); the count is left unchanged.
323
- * @returns Reactive count, bound flags, and mutation controls.
324
- */
325
- function useCounter(initial = 0, options = {}) {
326
- const min = requireBound(options.min ?? Number.NEGATIVE_INFINITY, "min");
327
- const max = requireBound(options.max ?? Number.POSITIVE_INFINITY, "max");
328
- if (min > max) throw new RangeError(`[VIZE_COMPOSE_COUNTER_INVALID_RANGE] min must not exceed max; received min ${String(min)} and max ${String(max)}`);
329
- const clamp = (value) => Math.min(max, Math.max(min, value));
330
- const count = shallowRef(clamp(requireValue(initial)));
331
- let baseline = count.value;
332
- const setClamped = (next) => {
333
- count.value = clamp(requireValue(next));
334
- return count.value;
335
- };
336
- const reset = (value) => {
337
- const applied = setClamped(value ?? baseline);
338
- if (value !== void 0) baseline = applied;
339
- return applied;
340
- };
341
- return {
342
- count,
343
- atMin: computed(() => count.value === min),
344
- atMax: computed(() => count.value === max),
345
- increment: (delta = 1) => setClamped(count.value + requireValue(delta)),
346
- decrement: (delta = 1) => setClamped(count.value - requireValue(delta)),
347
- set: setClamped,
348
- reset
349
- };
350
- }
351
- function requireBound(value, label) {
352
- if (Number.isNaN(value)) throw new RangeError(`[VIZE_COMPOSE_COUNTER_INVALID_RANGE] ${label} must not be NaN`);
353
- return value;
354
- }
355
- function requireValue(value) {
356
- if (Number.isNaN(value)) throw new RangeError("[VIZE_COMPOSE_COUNTER_INVALID_VALUE] the value is NaN; counter state was left unchanged");
357
- return value;
358
- }
359
- //#endregion
360
- //#region src/use-debounced.ts
361
- const defaultScheduler$1 = {
362
- setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
363
- clearTimeout: (handle) => {
364
- globalThis.clearTimeout(handle);
365
- }
366
- };
367
- /**
368
- * Create a readonly debounced view of a reactive source.
369
- *
370
- * The view starts at the current source value. Each source change (observed
371
- * with `flush: "sync"`, so every synchronous write counts) restarts a
372
- * single-shot timer of `waitMs` milliseconds; when it fires, the view takes
373
- * the source value current at that moment. `waitMs` is reactive and is read
374
- * when a timer is scheduled; changing it does not restart an already-pending
375
- * timer. A wait of `0` still defers to the next scheduler tick.
376
- *
377
- * Server rendering is explicit: without a browser `window` (and with
378
- * {@link UseDebouncedOptions.runOnServer} disabled) no timer ever starts and
379
- * the view mirrors the source synchronously, so server-rendered output shows
380
- * current values and nothing leaks. `pending` stays `false` and the controls
381
- * report `false` in that mode.
382
- *
383
- * Cleanup rule: the watcher and any pending timer are released when the
384
- * owning reactive scope stops; call inside an active scope. Outside one, the
385
- * watcher lives as long as the source and `cancel()` only clears the pending
386
- * timer.
387
- *
388
- * @example
389
- * ```ts
390
- * const query = shallowRef("");
391
- * const { debounced, flush } = useDebounced(query, 300);
392
- * query.value = "vize"; // debounced.value still "" for 300ms
393
- * flush(); // debounced.value === "vize" immediately
394
- * ```
395
- *
396
- * @param source Reactive source to debounce.
397
- * @param waitMs Reactive delay in milliseconds; must be finite and at least
398
- * zero. Fractions are truncated.
399
- * @param options Runtime scheduling overrides.
400
- * @default options {}
401
- * @throws `RangeError` tagged `VIZE_COMPOSE_DEBOUNCE_INVALID_WAIT` when the
402
- * resolved wait is not finite or is negative, both synchronously at creation
403
- * (even in mirror mode) and again for every scheduled delay.
404
- * @returns Readonly debounced view, pending flag, and cancel/flush controls.
405
- */
406
- function useDebounced(source, waitMs, options = {}) {
407
- const scheduler = options.scheduler ?? defaultScheduler$1;
408
- const debounced = shallowRef(toValue(source));
409
- const pending = shallowRef(false);
410
- let handle;
411
- resolveWaitMs$1(toValue(waitMs));
412
- const apply = () => {
413
- handle = void 0;
414
- pending.value = false;
415
- debounced.value = toValue(source);
416
- };
417
- const cancel = () => {
418
- if (!pending.value) return false;
419
- scheduler.clearTimeout(handle);
420
- handle = void 0;
421
- pending.value = false;
422
- return true;
423
- };
424
- const flush = () => {
425
- if (!pending.value) return false;
426
- scheduler.clearTimeout(handle);
427
- apply();
428
- return true;
429
- };
430
- watch(() => toValue(source), (next) => {
431
- if (typeof window === "undefined" && !(options.runOnServer ?? false)) {
432
- debounced.value = next;
433
- return;
434
- }
435
- const delayMs = resolveWaitMs$1(toValue(waitMs));
436
- if (pending.value) scheduler.clearTimeout(handle);
437
- pending.value = true;
438
- handle = scheduler.setTimeout(apply, delayMs);
439
- }, { flush: "sync" });
440
- tryOnScopeDispose(() => {
441
- cancel();
442
- });
443
- return {
444
- debounced,
445
- pending,
446
- cancel,
447
- flush
448
- };
449
- }
450
- function resolveWaitMs$1(value) {
451
- if (!Number.isFinite(value) || value < 0) throw new RangeError(`[VIZE_COMPOSE_DEBOUNCE_INVALID_WAIT] waitMs must be finite and at least zero; received ${String(value)}`);
452
- return Math.trunc(value);
453
- }
454
- //#endregion
455
- //#region src/use-history.ts
456
- /**
457
- * Record bounded undo/redo history over the writes of a ref.
458
- *
459
- * Recording is shallow and identity-based, matching Vue's own change
460
- * detection: assignments to `source.value` are recorded (observed with
461
- * `flush: "sync"`, so every synchronous write counts), writes that are
462
- * `Object.is`-equal to the current value are not changes, and in-place
463
- * mutations of object values are invisible — pair mutable values with
464
- * {@link UseHistoryOptions.clone} and reassign. Undoing and redoing restore
465
- * values through `clone` as well, so snapshots never share identity with the
466
- * live value unless the default identity clone is kept. When a
467
- * user-provided `clone` throws, the failed operation leaves history
468
- * unchanged and the error propagates.
469
- *
470
- * Safe during server rendering: no browser globals are read and no timers
471
- * start. Cleanup rule: when the owning reactive scope stops, recording stops
472
- * and every retained snapshot is released, so `undo`/`redo` return `false`
473
- * afterwards; call inside an active scope, or the watcher lives as long as
474
- * the source.
475
- *
476
- * @example
477
- * ```ts
478
- * const text = shallowRef("");
479
- * const { undo, redo, batch } = useHistory(text);
480
- * text.value = "a";
481
- * batch(() => {
482
- * text.value = "ab";
483
- * text.value = "abc";
484
- * });
485
- * undo(); // text.value === "a" (the batch is one step)
486
- * redo(); // text.value === "abc"
487
- * ```
488
- *
489
- * @param source Ref whose writes are recorded.
490
- * @param options Retention bound and snapshot cloning.
491
- * @default options {}
492
- * @throws `RangeError` tagged `VIZE_COMPOSE_HISTORY_INVALID_CAPACITY` when
493
- * the capacity is not an integer greater than zero.
494
- * @throws `Error` tagged `VIZE_COMPOSE_HISTORY_IN_BATCH` when `undo`,
495
- * `redo`, or `clear` is called inside {@link HistoryControls.batch}, where
496
- * stack movement would corrupt the pending group.
497
- * @returns Reactive undo/redo state and controls.
498
- */
499
- function useHistory(source, options = {}) {
500
- const capacity = options.capacity ?? 100;
501
- if (!Number.isInteger(capacity) || capacity < 1) throw new RangeError(`[VIZE_COMPOSE_HISTORY_INVALID_CAPACITY] capacity must be an integer greater than zero; received ${String(capacity)}`);
502
- const clone = options.clone ?? ((value) => value);
503
- const undoStack = shallowRef([]);
504
- const redoStack = shallowRef([]);
505
- let restoring = false;
506
- let batchDepth = 0;
507
- let activeBatch;
508
- const pushUndo = (entry) => {
509
- const next = [...undoStack.value, entry];
510
- undoStack.value = next.length > capacity ? next.slice(next.length - capacity) : next;
511
- redoStack.value = [];
512
- };
513
- const writeSilently = (value) => {
514
- restoring = true;
515
- try {
516
- source.value = value;
517
- } finally {
518
- restoring = false;
519
- }
520
- };
521
- const requireOutsideBatch = (operation) => {
522
- if (batchDepth > 0) throw new Error(`[VIZE_COMPOSE_HISTORY_IN_BATCH] ${operation}() is not available inside batch()`);
523
- };
524
- const undo = () => {
525
- requireOutsideBatch("undo");
526
- const entry = undoStack.value.at(-1);
527
- if (entry === void 0) return false;
528
- const restored = clone(entry.value);
529
- const recorded = { value: clone(source.value) };
530
- undoStack.value = undoStack.value.slice(0, -1);
531
- redoStack.value = [...redoStack.value, recorded];
532
- writeSilently(restored);
533
- return true;
534
- };
535
- const redo = () => {
536
- requireOutsideBatch("redo");
537
- const entry = redoStack.value.at(-1);
538
- if (entry === void 0) return false;
539
- const restored = clone(entry.value);
540
- const recorded = { value: clone(source.value) };
541
- redoStack.value = redoStack.value.slice(0, -1);
542
- undoStack.value = [...undoStack.value, recorded];
543
- writeSilently(restored);
544
- return true;
545
- };
546
- const batch = (run) => {
547
- if (batchDepth === 0) activeBatch = {
548
- raw: source.value,
549
- entry: { value: clone(source.value) }
550
- };
551
- batchDepth += 1;
552
- try {
553
- return run();
554
- } finally {
555
- batchDepth -= 1;
556
- if (batchDepth === 0 && activeBatch !== void 0) {
557
- const finished = activeBatch;
558
- activeBatch = void 0;
559
- if (!Object.is(finished.raw, source.value)) pushUndo(finished.entry);
560
- }
561
- }
562
- };
563
- const clear = () => {
564
- requireOutsideBatch("clear");
565
- undoStack.value = [];
566
- redoStack.value = [];
567
- };
568
- const handle = watch(source, (_next, replaced) => {
569
- if (restoring || batchDepth > 0) return;
570
- pushUndo({ value: clone(replaced) });
571
- }, { flush: "sync" });
572
- tryOnScopeDispose(() => {
573
- handle.stop();
574
- undoStack.value = [];
575
- redoStack.value = [];
576
- });
577
- return {
578
- canUndo: computed(() => undoStack.value.length > 0),
579
- canRedo: computed(() => redoStack.value.length > 0),
580
- undoCount: computed(() => undoStack.value.length),
581
- redoCount: computed(() => redoStack.value.length),
582
- undo,
583
- redo,
584
- batch,
585
- clear
586
- };
587
- }
588
- //#endregion
589
- //#region src/use-previous.ts
590
- function usePrevious(source, ...initial) {
591
- const previous = shallowRef(initial.length === 1 ? initial[0] : void 0);
592
- watch(() => toValue(source), (_next, replaced) => {
593
- previous.value = replaced;
594
- }, { flush: "sync" });
595
- return previous;
596
- }
597
- //#endregion
598
- //#region src/use-throttled.ts
599
- const defaultScheduler = {
600
- setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
601
- clearTimeout: (handle) => {
602
- globalThis.clearTimeout(handle);
603
- }
604
- };
605
- /**
606
- * Create a readonly throttled view of a reactive source.
607
- *
608
- * Changes are observed with `flush: "sync"`, so every synchronous write
609
- * counts. Outside a cooldown window, a change applies immediately when
610
- * {@link UseThrottledOptions.leading} is enabled (otherwise it waits as a
611
- * trailing update) and opens a window of `waitMs` milliseconds. Changes
612
- * inside a window are collected as the trailing candidate; when the window
613
- * ends with a candidate waiting, the source value current at that moment is
614
- * applied and the next window opens back to back, keeping applications
615
- * spaced by `waitMs`. A window that ends without a candidate closes silently.
616
- * `waitMs` is reactive and is read each time a window opens; changing it
617
- * never disturbs an already-open window. A wait of `0` still defers trailing
618
- * updates to the next scheduler tick.
619
- *
620
- * Server rendering is explicit: without a browser `window` (and with
621
- * {@link UseThrottledOptions.runOnServer} disabled) no timer ever starts and
622
- * the view mirrors the source synchronously, so server-rendered output shows
623
- * current values and nothing leaks. `pending` stays `false` and the controls
624
- * report `false` in that mode.
625
- *
626
- * Cleanup rule: the watcher and any open window timer are released when the
627
- * owning reactive scope stops; call inside an active scope. Outside one, the
628
- * watcher lives as long as the source and `cancel()` only clears the window.
629
- *
630
- * @example
631
- * ```ts
632
- * const scrollY = shallowRef(0);
633
- * const { throttled } = useThrottled(scrollY, 100);
634
- * scrollY.value = 40; // applied immediately (leading edge)
635
- * scrollY.value = 80; // applied when the 100ms window ends
636
- * ```
637
- *
638
- * @param source Reactive source to throttle.
639
- * @param waitMs Reactive cooldown in milliseconds; must be finite and at
640
- * least zero. Fractions are truncated.
641
- * @param options Edge policy and runtime scheduling overrides.
642
- * @default options {}
643
- * @throws `RangeError` tagged `VIZE_COMPOSE_THROTTLE_INVALID_WAIT` when the
644
- * resolved wait is not finite or is negative, both synchronously at creation
645
- * (even in mirror mode) and again each time a window opens.
646
- * @throws `TypeError` tagged `VIZE_COMPOSE_THROTTLE_INVALID_EDGES` when both
647
- * `leading` and `trailing` are disabled, because updates could then never
648
- * propagate.
649
- * @returns Readonly throttled view, pending flag, and cancel/flush controls.
650
- */
651
- function useThrottled(source, waitMs, options = {}) {
652
- const leading = options.leading ?? true;
653
- const trailing = options.trailing ?? true;
654
- if (!leading && !trailing) throw new TypeError("[VIZE_COMPOSE_THROTTLE_INVALID_EDGES] at least one of leading or trailing must be enabled");
655
- const scheduler = options.scheduler ?? defaultScheduler;
656
- const throttled = shallowRef(toValue(source));
657
- const pending = shallowRef(false);
658
- let windowHandle;
659
- let windowOpen = false;
660
- resolveWaitMs(toValue(waitMs));
661
- const openWindow = () => {
662
- windowOpen = true;
663
- windowHandle = scheduler.setTimeout(() => {
664
- windowHandle = void 0;
665
- if (!pending.value) {
666
- windowOpen = false;
667
- return;
668
- }
669
- pending.value = false;
670
- throttled.value = toValue(source);
671
- openWindow();
672
- }, resolveWaitMs(toValue(waitMs)));
673
- };
674
- const closeWindow = () => {
675
- if (windowOpen) scheduler.clearTimeout(windowHandle);
676
- windowHandle = void 0;
677
- windowOpen = false;
678
- pending.value = false;
679
- };
680
- const cancel = () => {
681
- const hadTrailing = pending.value;
682
- closeWindow();
683
- return hadTrailing;
684
- };
685
- const flush = () => {
686
- if (!pending.value) return false;
687
- closeWindow();
688
- throttled.value = toValue(source);
689
- return true;
690
- };
691
- watch(() => toValue(source), (next) => {
692
- if (typeof window === "undefined" && !(options.runOnServer ?? false)) {
693
- throttled.value = next;
694
- return;
695
- }
696
- if (windowOpen) {
697
- if (trailing) pending.value = true;
698
- return;
699
- }
700
- if (leading) throttled.value = next;
701
- else pending.value = true;
702
- openWindow();
703
- }, { flush: "sync" });
704
- tryOnScopeDispose(() => {
705
- cancel();
706
- });
707
- return {
708
- throttled,
709
- pending,
710
- cancel,
711
- flush
712
- };
713
- }
714
- function resolveWaitMs(value) {
715
- if (!Number.isFinite(value) || value < 0) throw new RangeError(`[VIZE_COMPOSE_THROTTLE_INVALID_WAIT] waitMs must be finite and at least zero; received ${String(value)}`);
716
- return Math.trunc(value);
717
- }
718
- //#endregion
719
- //#region src/use-toggle.ts
720
- /**
721
- * Create owned boolean state with an inverting control.
722
- *
723
- * Purely synchronous state: safe during server rendering (no browser
724
- * globals, no timers) and nothing to dispose, so it works inside and
725
- * outside reactive scopes alike.
726
- *
727
- * @example
728
- * ```ts
729
- * const { state: open, toggle } = useToggle();
730
- * toggle(); // true
731
- * toggle(false); // false
732
- * open.value; // false
733
- * ```
734
- *
735
- * @param initial State before the first toggle.
736
- * @default initial false
737
- * @returns The writable state and its toggle control.
738
- */
739
- function useToggle(initial = false) {
740
- const state = shallowRef(initial);
741
- const toggle = (force) => {
742
- state.value = force ?? !state.value;
743
- return state.value;
744
- };
745
- return {
746
- state,
747
- toggle
748
- };
749
- }
750
- //#endregion
751
- export { tryOnScopeDispose, useAsyncResource, useCounter, useDebounced, useEventListener, useHistory, useLocale, useMediaQuery, usePrevious, useReducedMotion, useThrottled, useToggle };
1
+ import { anyAbortSignal, deadlineAbortSignal, timeoutAbortSignal } from "./abort-signal.mjs";
2
+ import { tryOnScopeDispose } from "./scope.mjs";
3
+ import { useAsyncResource } from "./async-resource.mjs";
4
+ import { availableCapability, isCapabilityAvailable, isCapabilityUnavailable, unavailableCapability } from "./capability.mjs";
5
+ import { t as COMPOSABLE_CATALOG } from "./catalog-DOXTbk4r.mjs";
6
+ import { DISPOSAL_ERROR_CODE, DisposalError, createDisposalScope } from "./disposal-scope.mjs";
7
+ import { useEventListener } from "./event-listener.mjs";
8
+ import { useLocale } from "./locale.mjs";
9
+ import { useMediaQuery, useReducedMotion } from "./media-query.mjs";
10
+ import { calculateRetryDelay } from "./retry-delay.mjs";
11
+ import { retryAsync } from "./retry-async.mjs";
12
+ import "./timeout-scheduler.mjs";
13
+ import { useCounter } from "./use-counter.mjs";
14
+ import { useDebounced } from "./use-debounced.mjs";
15
+ import { useHistory } from "./use-history.mjs";
16
+ import { usePrevious } from "./use-previous.mjs";
17
+ import { useThrottled } from "./use-throttled.mjs";
18
+ import { useToggle } from "./use-toggle.mjs";
19
+ export { COMPOSABLE_CATALOG, DISPOSAL_ERROR_CODE, DisposalError, anyAbortSignal, availableCapability, calculateRetryDelay, createDisposalScope, deadlineAbortSignal, isCapabilityAvailable, isCapabilityUnavailable, retryAsync, timeoutAbortSignal, tryOnScopeDispose, unavailableCapability, useAsyncResource, useCounter, useDebounced, useEventListener, useHistory, useLocale, useMediaQuery, usePrevious, useReducedMotion, useThrottled, useToggle };