@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.
- package/dist/abort-signal.d.mts +96 -0
- package/dist/abort-signal.mjs +203 -0
- package/dist/async-resource.d.mts +97 -0
- package/dist/async-resource.mjs +99 -0
- package/dist/capability-BNpkvSy5.d.mts +93 -0
- package/dist/capability.d.mts +2 -0
- package/dist/capability.mjs +37 -0
- package/dist/catalog-CuWhR-0q.d.mts +506 -0
- package/dist/catalog-DOXTbk4r.mjs +529 -0
- package/dist/catalog.d.mts +2 -0
- package/dist/catalog.mjs +2 -0
- package/dist/disposal-scope.d.mts +93 -0
- package/dist/disposal-scope.mjs +119 -0
- package/dist/event-listener.d.mts +85 -0
- package/dist/event-listener.mjs +55 -0
- package/dist/index.d.mts +19 -786
- package/dist/index.mjs +19 -751
- package/dist/locale.d.mts +65 -0
- package/dist/locale.mjs +72 -0
- package/dist/media-query.d.mts +56 -0
- package/dist/media-query.mjs +56 -0
- package/dist/retry-async.d.mts +91 -0
- package/dist/retry-async.mjs +136 -0
- package/dist/retry-delay.d.mts +73 -0
- package/dist/retry-delay.mjs +50 -0
- package/dist/scope.d.mts +18 -0
- package/dist/scope.mjs +23 -0
- package/dist/timeout-scheduler.d.mts +18 -0
- package/dist/timeout-scheduler.mjs +1 -0
- package/dist/use-counter.d.mts +91 -0
- package/dist/use-counter.mjs +67 -0
- package/dist/use-debounced.d.mts +87 -0
- package/dist/use-debounced.mjs +98 -0
- package/dist/use-history.d.mts +105 -0
- package/dist/use-history.mjs +137 -0
- package/dist/use-previous.d.mts +43 -0
- package/dist/use-previous.mjs +11 -0
- package/dist/use-throttled.d.mts +107 -0
- package/dist/use-throttled.mjs +124 -0
- package/dist/use-toggle.d.mts +45 -0
- package/dist/use-toggle.mjs +34 -0
- 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 };
|
package/dist/locale.mjs
ADDED
|
@@ -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 };
|
package/dist/scope.d.mts
ADDED
|
@@ -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 {};
|