@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
@@ -0,0 +1,65 @@
1
+ import { ComputedRef, MaybeRefOrGetter } from "vue";
2
+
3
+ //#region src/locale.d.ts
4
+ /** Text flow reported by the internationalization runtime. */
5
+ type TextDirection = "ltr" | "rtl";
6
+ /** Locale selection options for {@link useLocale}. */
7
+ interface UseLocaleOptions {
8
+ /**
9
+ * Locale detector used when the reactive source has no value.
10
+ *
11
+ * @default navigator.language when available; otherwise undefined
12
+ */
13
+ readonly detect?: () => Intl.Locale | string | null | undefined;
14
+ /**
15
+ * Locale used when neither the source nor detector provides one.
16
+ *
17
+ * @default "en"
18
+ */
19
+ readonly fallback?: Intl.Locale | string;
20
+ }
21
+ /**
22
+ * Reactive locale metadata and cached formatter factories.
23
+ *
24
+ * Formatter factories propagate `TypeError` and `RangeError` from the
25
+ * platform `Intl` constructors when the supplied options are invalid.
26
+ */
27
+ interface LocaleControls {
28
+ /** Canonical Unicode locale identifier. */
29
+ readonly locale: ComputedRef<string>;
30
+ /** Parsed locale details supplied by the internationalization runtime. */
31
+ readonly details: ComputedRef<Intl.Locale>;
32
+ /** Native writing direction for the active locale. */
33
+ readonly direction: ComputedRef<TextDirection>;
34
+ /** Return a cached number formatter for the active locale and options. */
35
+ readonly number: (options?: Intl.NumberFormatOptions) => Intl.NumberFormat;
36
+ /** Return a cached date and time formatter for the active locale and options. */
37
+ readonly dateTime: (options?: Intl.DateTimeFormatOptions) => Intl.DateTimeFormat;
38
+ /** Return a cached list formatter for the active locale and options. */
39
+ readonly list: (options?: Intl.ListFormatOptions) => Intl.ListFormat;
40
+ /** Return a cached relative-time formatter for the active locale and options. */
41
+ readonly relativeTime: (options?: Intl.RelativeTimeFormatOptions) => Intl.RelativeTimeFormat;
42
+ }
43
+ /**
44
+ * Create reactive locale metadata and platform-native formatter factories.
45
+ *
46
+ * Equivalent formatter options reuse instances. The bounded cache follows the
47
+ * active locale automatically and prevents repeated constructor overhead in
48
+ * reactive render paths.
49
+ *
50
+ * Detection is lazy and guarded: the default detector reads
51
+ * `navigator.language` behind a `typeof` check at call time, so importing and
52
+ * calling this during server rendering is safe, and runtimes without a
53
+ * `navigator` resolve to the fallback locale. The composable owns no timers
54
+ * or listeners, so no scope cleanup is required.
55
+ *
56
+ * @param source Reactive locale source. Empty values defer to the detector.
57
+ * @param options Detection and fallback behavior.
58
+ * @default options {}
59
+ * @throws `RangeError` on first read of the reactive values when the winning
60
+ * candidate is not a structurally valid locale identifier.
61
+ * @returns Reactive locale metadata and cached formatter factories.
62
+ */
63
+ declare function useLocale(source?: MaybeRefOrGetter<Intl.Locale | string | null | undefined>, options?: UseLocaleOptions): LocaleControls;
64
+ //#endregion
65
+ export { LocaleControls, TextDirection, UseLocaleOptions, useLocale };
@@ -0,0 +1,72 @@
1
+ import { computed, toValue } from "vue";
2
+ //#region src/locale.ts
3
+ const FORMATTER_CACHE_LIMIT = 32;
4
+ /**
5
+ * Create reactive locale metadata and platform-native formatter factories.
6
+ *
7
+ * Equivalent formatter options reuse instances. The bounded cache follows the
8
+ * active locale automatically and prevents repeated constructor overhead in
9
+ * reactive render paths.
10
+ *
11
+ * Detection is lazy and guarded: the default detector reads
12
+ * `navigator.language` behind a `typeof` check at call time, so importing and
13
+ * calling this during server rendering is safe, and runtimes without a
14
+ * `navigator` resolve to the fallback locale. The composable owns no timers
15
+ * or listeners, so no scope cleanup is required.
16
+ *
17
+ * @param source Reactive locale source. Empty values defer to the detector.
18
+ * @param options Detection and fallback behavior.
19
+ * @default options {}
20
+ * @throws `RangeError` on first read of the reactive values when the winning
21
+ * candidate is not a structurally valid locale identifier.
22
+ * @returns Reactive locale metadata and cached formatter factories.
23
+ */
24
+ function useLocale(source, options = {}) {
25
+ const locale = computed(() => {
26
+ const candidate = (source === void 0 ? void 0 : toValue(source)) ?? (options.detect ?? detectBrowserLocale)() ?? options.fallback ?? "en";
27
+ return candidate instanceof Intl.Locale ? candidate.toString() : new Intl.Locale(candidate).toString();
28
+ });
29
+ const details = computed(() => new Intl.Locale(locale.value));
30
+ const direction = computed(() => details.value.getTextInfo().direction ?? "ltr");
31
+ const number = createFormatterCache((activeLocale, formatOptions) => new Intl.NumberFormat(activeLocale, formatOptions));
32
+ const dateTime = createFormatterCache((activeLocale, formatOptions) => new Intl.DateTimeFormat(activeLocale, formatOptions));
33
+ const list = createFormatterCache((activeLocale, formatOptions) => new Intl.ListFormat(activeLocale, formatOptions));
34
+ const relativeTime = createFormatterCache((activeLocale, formatOptions) => new Intl.RelativeTimeFormat(activeLocale, formatOptions));
35
+ return {
36
+ locale,
37
+ details,
38
+ direction,
39
+ number: (formatOptions) => number(locale.value, formatOptions),
40
+ dateTime: (formatOptions) => dateTime(locale.value, formatOptions),
41
+ list: (formatOptions) => list(locale.value, formatOptions),
42
+ relativeTime: (formatOptions) => relativeTime(locale.value, formatOptions)
43
+ };
44
+ }
45
+ function detectBrowserLocale() {
46
+ return typeof navigator === "undefined" ? void 0 : navigator.language;
47
+ }
48
+ function createFormatterCache(create) {
49
+ const cache = /* @__PURE__ */ new Map();
50
+ return (locale, options) => {
51
+ const key = `${locale}\u0000${serializeOptions(options)}`;
52
+ const cached = cache.get(key);
53
+ if (cached !== void 0) {
54
+ cache.delete(key);
55
+ cache.set(key, cached);
56
+ return cached;
57
+ }
58
+ const formatter = create(locale, options);
59
+ if (cache.size >= FORMATTER_CACHE_LIMIT) {
60
+ const oldest = cache.keys().next().value;
61
+ if (oldest !== void 0) cache.delete(oldest);
62
+ }
63
+ cache.set(key, formatter);
64
+ return formatter;
65
+ };
66
+ }
67
+ function serializeOptions(options) {
68
+ if (options === void 0) return "";
69
+ return JSON.stringify(Object.entries(options).sort(([left], [right]) => left.localeCompare(right)));
70
+ }
71
+ //#endregion
72
+ export { useLocale };
@@ -0,0 +1,56 @@
1
+ import { ComputedRef, MaybeRefOrGetter, Ref } from "vue";
2
+
3
+ //#region src/media-query.d.ts
4
+ /** Capability required to evaluate media queries. */
5
+ interface MediaQueryHost {
6
+ /** Create an observable result for a media query. */
7
+ readonly matchMedia: (query: string) => MediaQueryList;
8
+ }
9
+ /** Options for {@link useMediaQuery}. */
10
+ interface UseMediaQueryOptions {
11
+ /**
12
+ * Value exposed when no media-query capability is available.
13
+ *
14
+ * @default false
15
+ */
16
+ readonly ssrValue?: boolean;
17
+ /**
18
+ * Reactive media-query capability for alternate runtimes and tests.
19
+ *
20
+ * @default globalThis.window when available
21
+ */
22
+ readonly host?: MaybeRefOrGetter<MediaQueryHost | null | undefined>;
23
+ }
24
+ /**
25
+ * Evaluate a reactive media query without requiring browser globals.
26
+ *
27
+ * During server rendering (or whenever no capability host resolves) the ref
28
+ * holds the configured server value and no subscription is created. The
29
+ * change subscription follows the reactive query and host: each
30
+ * re-evaluation removes the previous listener, and the final listener is
31
+ * removed when the owning reactive scope stops. Call inside an active scope
32
+ * so the subscription is released. A host whose matcher throws propagates
33
+ * the error to the active effect run; the browser default never throws.
34
+ *
35
+ * @param query Reactive media-query source.
36
+ * @param options Runtime capability and server-rendered fallback.
37
+ * @default options {}
38
+ * @returns Readonly ref that is `true` while the query matches.
39
+ */
40
+ declare function useMediaQuery(query: MaybeRefOrGetter<string>, options?: UseMediaQueryOptions): Readonly<Ref<boolean>>;
41
+ /** User motion preference exposed by {@link useReducedMotion}. */
42
+ type MotionPreference = "reduce" | "no-preference";
43
+ /**
44
+ * Return the reactive user motion preference.
45
+ *
46
+ * Shares {@link useMediaQuery} semantics: during server rendering the
47
+ * preference is `"no-preference"` unless `ssrValue` is `true`, and the
48
+ * underlying subscription is removed when the owning reactive scope stops.
49
+ *
50
+ * @param options Runtime capability and server-rendered fallback.
51
+ * @default options {}
52
+ * @returns Computed preference for `(prefers-reduced-motion: reduce)`.
53
+ */
54
+ declare function useReducedMotion(options?: UseMediaQueryOptions): ComputedRef<MotionPreference>;
55
+ //#endregion
56
+ export { MediaQueryHost, MotionPreference, UseMediaQueryOptions, useMediaQuery, useReducedMotion };
@@ -0,0 +1,56 @@
1
+ import { computed, readonly, ref, toValue, watchEffect } from "vue";
2
+ //#region src/media-query.ts
3
+ /**
4
+ * Evaluate a reactive media query without requiring browser globals.
5
+ *
6
+ * During server rendering (or whenever no capability host resolves) the ref
7
+ * holds the configured server value and no subscription is created. The
8
+ * change subscription follows the reactive query and host: each
9
+ * re-evaluation removes the previous listener, and the final listener is
10
+ * removed when the owning reactive scope stops. Call inside an active scope
11
+ * so the subscription is released. A host whose matcher throws propagates
12
+ * the error to the active effect run; the browser default never throws.
13
+ *
14
+ * @param query Reactive media-query source.
15
+ * @param options Runtime capability and server-rendered fallback.
16
+ * @default options {}
17
+ * @returns Readonly ref that is `true` while the query matches.
18
+ */
19
+ function useMediaQuery(query, options = {}) {
20
+ const matches = ref(options.ssrValue ?? false);
21
+ watchEffect((onCleanup) => {
22
+ const host = options.host === void 0 ? browserMediaQueryHost() : toValue(options.host);
23
+ if (!host) {
24
+ matches.value = options.ssrValue ?? false;
25
+ return;
26
+ }
27
+ const media = host.matchMedia(toValue(query));
28
+ const update = () => {
29
+ matches.value = media.matches;
30
+ };
31
+ update();
32
+ media.addEventListener("change", update);
33
+ onCleanup(() => media.removeEventListener("change", update));
34
+ });
35
+ return readonly(matches);
36
+ }
37
+ /**
38
+ * Return the reactive user motion preference.
39
+ *
40
+ * Shares {@link useMediaQuery} semantics: during server rendering the
41
+ * preference is `"no-preference"` unless `ssrValue` is `true`, and the
42
+ * underlying subscription is removed when the owning reactive scope stops.
43
+ *
44
+ * @param options Runtime capability and server-rendered fallback.
45
+ * @default options {}
46
+ * @returns Computed preference for `(prefers-reduced-motion: reduce)`.
47
+ */
48
+ function useReducedMotion(options = {}) {
49
+ const reduced = useMediaQuery("(prefers-reduced-motion: reduce)", options);
50
+ return computed(() => reduced.value ? "reduce" : "no-preference");
51
+ }
52
+ function browserMediaQueryHost() {
53
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window : void 0;
54
+ }
55
+ //#endregion
56
+ export { useMediaQuery, useReducedMotion };
@@ -0,0 +1,91 @@
1
+ import { TimeoutScheduler } from "./timeout-scheduler.mjs";
2
+ import { RetryDelayOptions } from "./retry-delay.mjs";
3
+
4
+ //#region src/retry-async.d.ts
5
+ /** Context supplied to every invocation of a retried operation. */
6
+ interface RetryAttemptContext {
7
+ /** One-based operation attempt, including the initial call. */
8
+ readonly attempt: number;
9
+ /** Shared cancellation signal for the complete retry execution. */
10
+ readonly signal: AbortSignal;
11
+ }
12
+ /** Context supplied after an operation fails and before retry policy runs. */
13
+ interface RetryFailureContext extends RetryAttemptContext {
14
+ /** Exact value thrown or rejected by the failed operation. */
15
+ readonly error: unknown;
16
+ /** One-based retry that would follow this failure. */
17
+ readonly retryAttempt: number;
18
+ }
19
+ /** Context supplied when an approved retry is about to wait. */
20
+ interface RetryScheduledContext extends RetryFailureContext {
21
+ /** One-based operation attempt that will run after the wait. */
22
+ readonly nextAttempt: number;
23
+ /** Calculated backoff delay in integer milliseconds. */
24
+ readonly delayMs: number;
25
+ }
26
+ /** Options for {@link retryAsync}. */
27
+ interface RetryAsyncOptions extends RetryDelayOptions {
28
+ /**
29
+ * Maximum retries after the initial operation attempt.
30
+ *
31
+ * @default 3
32
+ */
33
+ readonly maximumRetries?: number;
34
+ /**
35
+ * Cancels the active operation, policy hook, notification hook, or backoff
36
+ * wait. The returned promise rejects with the signal's exact reason.
37
+ *
38
+ * @default a private non-aborting signal
39
+ */
40
+ readonly signal?: AbortSignal;
41
+ /**
42
+ * Deterministic or host-specific scheduler used for backoff waits.
43
+ *
44
+ * @default globalThis timer functions
45
+ */
46
+ readonly scheduler?: TimeoutScheduler;
47
+ /**
48
+ * Decide whether an operation failure is retryable. Returning `false`
49
+ * rejects with the original operation error without calculating a delay.
50
+ * The decision may be asynchronous and remains abortable.
51
+ *
52
+ * @default every failure is retryable while retries remain
53
+ */
54
+ readonly shouldRetry?: (context: RetryFailureContext) => boolean | PromiseLike<boolean>;
55
+ /**
56
+ * Observe an approved retry before its backoff wait begins. The hook may be
57
+ * asynchronous and remains abortable; a hook failure is propagated exactly
58
+ * and the next operation attempt is not started.
59
+ *
60
+ * @default undefined
61
+ */
62
+ readonly onRetry?: (context: RetryScheduledContext) => void | PromiseLike<void>;
63
+ }
64
+ /**
65
+ * Execute an operation with bounded, abortable retries and deterministic delay policy.
66
+ *
67
+ * Synchronous throws and asynchronous rejections follow the same path. A
68
+ * successful value is returned unchanged. Exhaustion rejects with the final
69
+ * operation error, a negative retry decision rejects with the error that was
70
+ * evaluated, and cancellation rejects with the signal's exact reason. These
71
+ * values are deliberately not wrapped.
72
+ *
73
+ * Cancellation races every asynchronous stage, so callers are not forced to
74
+ * wait for an operation or hook that ignores its signal. Late settlements are
75
+ * still observed internally and cannot become unhandled rejections. Backoff
76
+ * options and entropy are evaluated lazily only after a failure is approved
77
+ * for retry.
78
+ *
79
+ * @typeParam Value Value produced by the operation.
80
+ * @param operation Work to invoke with a one-based attempt and shared signal.
81
+ * @param options Retry count, cancellation, policy, hooks, and delay options.
82
+ * @default options {}
83
+ * @throws {RangeError} A tagged error when `maximumRetries` or inherited delay
84
+ * options are outside their documented ranges.
85
+ * @throws {TypeError} A tagged error when `options` is not an object, a
86
+ * callback is not callable, or a retry decision does not resolve to a boolean.
87
+ * @returns The first successful operation value.
88
+ */
89
+ declare function retryAsync<Value>(operation: (context: RetryAttemptContext) => Value | PromiseLike<Value>, options?: RetryAsyncOptions): Promise<Value>;
90
+ //#endregion
91
+ export { RetryAsyncOptions, RetryAttemptContext, RetryFailureContext, RetryScheduledContext, retryAsync };
@@ -0,0 +1,136 @@
1
+ import { timeoutAbortSignal } from "./abort-signal.mjs";
2
+ import { calculateRetryDelay } from "./retry-delay.mjs";
3
+ //#region src/retry-async.ts
4
+ /**
5
+ * Execute an operation with bounded, abortable retries and deterministic delay policy.
6
+ *
7
+ * Synchronous throws and asynchronous rejections follow the same path. A
8
+ * successful value is returned unchanged. Exhaustion rejects with the final
9
+ * operation error, a negative retry decision rejects with the error that was
10
+ * evaluated, and cancellation rejects with the signal's exact reason. These
11
+ * values are deliberately not wrapped.
12
+ *
13
+ * Cancellation races every asynchronous stage, so callers are not forced to
14
+ * wait for an operation or hook that ignores its signal. Late settlements are
15
+ * still observed internally and cannot become unhandled rejections. Backoff
16
+ * options and entropy are evaluated lazily only after a failure is approved
17
+ * for retry.
18
+ *
19
+ * @typeParam Value Value produced by the operation.
20
+ * @param operation Work to invoke with a one-based attempt and shared signal.
21
+ * @param options Retry count, cancellation, policy, hooks, and delay options.
22
+ * @default options {}
23
+ * @throws {RangeError} A tagged error when `maximumRetries` or inherited delay
24
+ * options are outside their documented ranges.
25
+ * @throws {TypeError} A tagged error when `options` is not an object, a
26
+ * callback is not callable, or a retry decision does not resolve to a boolean.
27
+ * @returns The first successful operation value.
28
+ */
29
+ async function retryAsync(operation, options = {}) {
30
+ if (typeof operation !== "function") throw new TypeError(`[VIZE_COMPOSE_RETRY_INVALID_OPERATION] operation must be a function; received ${typeof operation}`);
31
+ if (options === null || typeof options !== "object") throw new TypeError(`[VIZE_COMPOSE_RETRY_INVALID_OPTIONS] options must be an object; received ${options === null ? "null" : typeof options}`);
32
+ const maximumRetries = options.maximumRetries === void 0 ? 3 : options.maximumRetries;
33
+ if (!Number.isSafeInteger(maximumRetries) || maximumRetries < 0 || maximumRetries >= Number.MAX_SAFE_INTEGER) throw new RangeError(`[VIZE_COMPOSE_RETRY_INVALID_MAXIMUM_RETRIES] maximumRetries must be an integer from 0 through ${String(Number.MAX_SAFE_INTEGER - 1)}; received ${String(maximumRetries)}`);
34
+ const shouldRetryPolicy = options.shouldRetry;
35
+ const retryObserver = options.onRetry;
36
+ if (shouldRetryPolicy !== void 0 && typeof shouldRetryPolicy !== "function") throw new TypeError(`[VIZE_COMPOSE_RETRY_INVALID_CALLBACK] shouldRetry must be a function; received ${typeof shouldRetryPolicy}`);
37
+ if (retryObserver !== void 0 && typeof retryObserver !== "function") throw new TypeError(`[VIZE_COMPOSE_RETRY_INVALID_CALLBACK] onRetry must be a function; received ${typeof retryObserver}`);
38
+ const signal = options.signal === void 0 ? new AbortController().signal : options.signal;
39
+ let attempt = 1;
40
+ while (true) {
41
+ throwIfAborted(signal);
42
+ const outcome = await capture(raceWithAbort(Promise.resolve().then(() => operation({
43
+ attempt,
44
+ signal
45
+ })), signal));
46
+ if (outcome.status === "success") return outcome.value;
47
+ if (signal.aborted) throw signal.reason;
48
+ if (attempt > maximumRetries) throw outcome.error;
49
+ const failure = {
50
+ attempt,
51
+ error: outcome.error,
52
+ retryAttempt: attempt,
53
+ signal
54
+ };
55
+ if (shouldRetryPolicy !== void 0) {
56
+ const shouldRetry = await raceWithAbort(Promise.resolve().then(() => shouldRetryPolicy(failure)), signal);
57
+ if (typeof shouldRetry !== "boolean") throw new TypeError(`[VIZE_COMPOSE_RETRY_INVALID_DECISION] shouldRetry must resolve to a boolean; received ${typeof shouldRetry}`);
58
+ if (!shouldRetry) throw outcome.error;
59
+ }
60
+ throwIfAborted(signal);
61
+ const delayMs = calculateRetryDelay(attempt, options);
62
+ const scheduled = {
63
+ ...failure,
64
+ delayMs,
65
+ nextAttempt: attempt + 1
66
+ };
67
+ if (retryObserver !== void 0) await raceWithAbort(Promise.resolve().then(() => retryObserver(scheduled)), signal);
68
+ await waitForRetry(delayMs, signal, options.scheduler);
69
+ attempt += 1;
70
+ }
71
+ }
72
+ async function capture(promise) {
73
+ try {
74
+ return {
75
+ status: "success",
76
+ value: await promise
77
+ };
78
+ } catch (error) {
79
+ return {
80
+ status: "error",
81
+ error
82
+ };
83
+ }
84
+ }
85
+ function raceWithAbort(promise, signal) {
86
+ if (signal.aborted) return Promise.reject(signal.reason);
87
+ return new Promise((resolve, reject) => {
88
+ let listening = false;
89
+ let settled = false;
90
+ const cleanup = () => {
91
+ if (!listening) return;
92
+ listening = false;
93
+ try {
94
+ signal.removeEventListener("abort", onAbort);
95
+ } catch {}
96
+ };
97
+ const settle = (callback) => {
98
+ if (settled) return;
99
+ settled = true;
100
+ cleanup();
101
+ callback();
102
+ };
103
+ const onAbort = () => settle(() => reject(signal.reason));
104
+ promise.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
105
+ try {
106
+ listening = true;
107
+ signal.addEventListener("abort", onAbort, { once: true });
108
+ if (settled) cleanup();
109
+ else if (signal.aborted) onAbort();
110
+ } catch (error) {
111
+ settle(() => reject(error));
112
+ }
113
+ });
114
+ }
115
+ async function waitForRetry(delayMs, signal, scheduler) {
116
+ const elapsed = {};
117
+ const delaySignal = timeoutAbortSignal(delayMs, scheduler === void 0 ? {
118
+ reason: elapsed,
119
+ signal
120
+ } : {
121
+ reason: elapsed,
122
+ scheduler,
123
+ signal
124
+ });
125
+ try {
126
+ await raceWithAbort(new Promise(() => void 0), delaySignal);
127
+ } catch (reason) {
128
+ if (reason === elapsed) return;
129
+ throw reason;
130
+ }
131
+ }
132
+ function throwIfAborted(signal) {
133
+ if (signal.aborted) throw signal.reason;
134
+ }
135
+ //#endregion
136
+ export { retryAsync };
@@ -0,0 +1,73 @@
1
+ //#region src/retry-delay.d.ts
2
+ /** Options for {@link calculateRetryDelay}. */
3
+ interface RetryDelayOptions {
4
+ /**
5
+ * Delay before the first retry, in milliseconds.
6
+ *
7
+ * @default 100
8
+ */
9
+ readonly initialDelayMs?: number;
10
+ /**
11
+ * Exponential multiplier applied for each subsequent retry.
12
+ *
13
+ * Values may be fractional but must be finite and at least one. The
14
+ * calculated delay is rounded up so a retry never starts earlier than the
15
+ * requested backoff.
16
+ *
17
+ * @default 2
18
+ */
19
+ readonly multiplier?: number;
20
+ /**
21
+ * Inclusive ceiling for the calculated delay, in milliseconds.
22
+ *
23
+ * The ceiling may be lower than {@link RetryDelayOptions.initialDelayMs};
24
+ * in that case it also caps the first retry.
25
+ *
26
+ * @default 30000
27
+ */
28
+ readonly maximumDelayMs?: number;
29
+ /**
30
+ * Fraction of the capped delay eligible for downward jitter.
31
+ *
32
+ * `0` is deterministic exponential backoff, `0.5` samples from the upper
33
+ * half of the range, and `1` applies full jitter from zero through the
34
+ * capped delay. Jitter never exceeds the unjittered delay.
35
+ *
36
+ * @default 0
37
+ */
38
+ readonly jitterRatio?: number;
39
+ /**
40
+ * Entropy source returning a number in the half-open interval `[0, 1)`.
41
+ * It is called exactly once when the selected jitter range contains more
42
+ * than one integer millisecond, and is otherwise not read.
43
+ *
44
+ * @default Math.random
45
+ */
46
+ readonly random?: () => number;
47
+ }
48
+ /**
49
+ * Calculate a bounded exponential-backoff delay for a one-based retry.
50
+ *
51
+ * The first retry is `retryAttempt = 1`. The result is always an integer from
52
+ * zero through `2_147_483_647`, making it safe to pass to common host timer
53
+ * APIs without implementation-specific clamping. The calculation is pure
54
+ * unless jitter requires the supplied entropy source, and it performs no work
55
+ * when the module is imported.
56
+ *
57
+ * Jitter samples uniformly from the inclusive integer range
58
+ * `ceil(cappedDelay * (1 - jitterRatio))...cappedDelay`. Capping occurs before
59
+ * jitter, so neither floating-point overflow nor entropy can exceed the
60
+ * configured maximum.
61
+ *
62
+ * @param retryAttempt One-based retry number after a failed initial attempt.
63
+ * @param options Backoff, cap, jitter, and entropy configuration.
64
+ * @default options {}
65
+ * @throws {RangeError} A tagged error when the retry number, delay options, or
66
+ * entropy value is outside its documented range.
67
+ * @throws {TypeError} A tagged error when `options` is not an object or
68
+ * `random` is not callable.
69
+ * @returns An integer delay in milliseconds.
70
+ */
71
+ declare function calculateRetryDelay(retryAttempt: number, options?: RetryDelayOptions): number;
72
+ //#endregion
73
+ export { RetryDelayOptions, calculateRetryDelay };
@@ -0,0 +1,50 @@
1
+ //#region src/retry-delay.ts
2
+ /**
3
+ * Calculate a bounded exponential-backoff delay for a one-based retry.
4
+ *
5
+ * The first retry is `retryAttempt = 1`. The result is always an integer from
6
+ * zero through `2_147_483_647`, making it safe to pass to common host timer
7
+ * APIs without implementation-specific clamping. The calculation is pure
8
+ * unless jitter requires the supplied entropy source, and it performs no work
9
+ * when the module is imported.
10
+ *
11
+ * Jitter samples uniformly from the inclusive integer range
12
+ * `ceil(cappedDelay * (1 - jitterRatio))...cappedDelay`. Capping occurs before
13
+ * jitter, so neither floating-point overflow nor entropy can exceed the
14
+ * configured maximum.
15
+ *
16
+ * @param retryAttempt One-based retry number after a failed initial attempt.
17
+ * @param options Backoff, cap, jitter, and entropy configuration.
18
+ * @default options {}
19
+ * @throws {RangeError} A tagged error when the retry number, delay options, or
20
+ * entropy value is outside its documented range.
21
+ * @throws {TypeError} A tagged error when `options` is not an object or
22
+ * `random` is not callable.
23
+ * @returns An integer delay in milliseconds.
24
+ */
25
+ function calculateRetryDelay(retryAttempt, options = {}) {
26
+ if (!Number.isSafeInteger(retryAttempt) || retryAttempt < 1) throw new RangeError(`[VIZE_COMPOSE_RETRY_INVALID_ATTEMPT] retryAttempt must be a positive safe integer; received ${String(retryAttempt)}`);
27
+ if (options === null || typeof options !== "object") throw new TypeError(`[VIZE_COMPOSE_RETRY_INVALID_OPTIONS] options must be an object; received ${options === null ? "null" : typeof options}`);
28
+ const initialDelayMs = options.initialDelayMs === void 0 ? 100 : options.initialDelayMs;
29
+ const multiplier = options.multiplier === void 0 ? 2 : options.multiplier;
30
+ const maximumDelayMs = options.maximumDelayMs === void 0 ? 3e4 : options.maximumDelayMs;
31
+ const jitterRatio = options.jitterRatio === void 0 ? 0 : options.jitterRatio;
32
+ if (!isPortableDelay(initialDelayMs) || !isPortableDelay(maximumDelayMs) || !Number.isFinite(multiplier) || multiplier < 1 || !Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) throw new RangeError(`[VIZE_COMPOSE_RETRY_INVALID_OPTIONS] initialDelayMs and maximumDelayMs must be integers from 0 through ${String(maximumPortableTimeoutMs)}, multiplier must be finite and at least 1, and jitterRatio must be from 0 through 1; received initialDelayMs=${String(initialDelayMs)}, maximumDelayMs=${String(maximumDelayMs)}, multiplier=${String(multiplier)}, jitterRatio=${String(jitterRatio)}`);
33
+ if (initialDelayMs === 0 || maximumDelayMs === 0) return 0;
34
+ const scaledDelay = initialDelayMs * multiplier ** (retryAttempt - 1);
35
+ const cappedDelay = Math.min(maximumDelayMs, Math.ceil(scaledDelay));
36
+ const minimumDelay = Math.ceil(cappedDelay * (1 - jitterRatio));
37
+ const integerRange = cappedDelay - minimumDelay;
38
+ if (integerRange === 0) return cappedDelay;
39
+ const random = options.random === void 0 ? Math.random : options.random;
40
+ if (typeof random !== "function") throw new TypeError(`[VIZE_COMPOSE_RETRY_INVALID_RANDOM] random must be a function; received ${typeof random}`);
41
+ const sample = random();
42
+ if (!Number.isFinite(sample) || sample < 0 || sample >= 1) throw new RangeError(`[VIZE_COMPOSE_RETRY_INVALID_RANDOM] random must return a finite number from 0 up to but excluding 1; received ${String(sample)}`);
43
+ return minimumDelay + Math.floor(sample * (integerRange + 1));
44
+ }
45
+ function isPortableDelay(value) {
46
+ return Number.isSafeInteger(value) && value >= 0 && value <= maximumPortableTimeoutMs;
47
+ }
48
+ const maximumPortableTimeoutMs = 2147483647;
49
+ //#endregion
50
+ export { calculateRetryDelay };
@@ -0,0 +1,18 @@
1
+ //#region src/scope.d.ts
2
+ /**
3
+ * Register cleanup in the active reactive scope when one exists.
4
+ *
5
+ * This is the shared lifecycle primitive of the package: composables hand
6
+ * their teardown here so owned resources are released when the surrounding
7
+ * scope stops.
8
+ *
9
+ * Never throws and is safe during server rendering; no browser globals are
10
+ * read. When no scope is active the cleanup is not registered and disposal
11
+ * ownership stays with the caller.
12
+ *
13
+ * @param cleanup Cleanup invoked exactly once when the scope is disposed.
14
+ * @returns Whether the cleanup was registered.
15
+ */
16
+ declare function tryOnScopeDispose(cleanup: () => void): boolean;
17
+ //#endregion
18
+ export { tryOnScopeDispose };
package/dist/scope.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import { getCurrentScope, onScopeDispose } 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
+ export { tryOnScopeDispose };
@@ -0,0 +1,18 @@
1
+ //#region src/timeout-scheduler.d.ts
2
+ /**
3
+ * Single-shot timer host used by the debounced and throttled state
4
+ * utilities.
5
+ *
6
+ * Implement this interface to integrate a deterministic test clock, a native
7
+ * runtime timer, or an application-owned scheduler. Handles are opaque: the
8
+ * utilities only hand them back to {@link TimeoutScheduler.clearTimeout}.
9
+ * This module declares types only and contributes no runtime code.
10
+ */
11
+ interface TimeoutScheduler {
12
+ /** Starts a single-shot callback and returns its opaque cancellation handle. */
13
+ readonly setTimeout: (callback: () => void, delayMs: number) => unknown;
14
+ /** Cancels a handle previously returned by {@link TimeoutScheduler.setTimeout}. */
15
+ readonly clearTimeout: (handle: unknown) => void;
16
+ }
17
+ //#endregion
18
+ export { TimeoutScheduler };
@@ -0,0 +1 @@
1
+ export {};