@octanejs/tanstack-pacer 0.0.5

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/package.json +62 -0
  4. package/src/async-batcher/index.ts +5 -0
  5. package/src/async-batcher/useAsyncBatchedCallback.ts +20 -0
  6. package/src/async-batcher/useAsyncBatcher.ts +116 -0
  7. package/src/async-debouncer/index.ts +5 -0
  8. package/src/async-debouncer/useAsyncDebouncedCallback.ts +25 -0
  9. package/src/async-debouncer/useAsyncDebouncer.ts +123 -0
  10. package/src/async-queuer/index.ts +5 -0
  11. package/src/async-queuer/useAsyncQueuedState.ts +30 -0
  12. package/src/async-queuer/useAsyncQueuer.ts +116 -0
  13. package/src/async-rate-limiter/index.ts +5 -0
  14. package/src/async-rate-limiter/useAsyncRateLimitedCallback.ts +27 -0
  15. package/src/async-rate-limiter/useAsyncRateLimiter.ts +120 -0
  16. package/src/async-retryer/index.ts +1 -0
  17. package/src/async-throttler/index.ts +5 -0
  18. package/src/async-throttler/useAsyncThrottledCallback.ts +24 -0
  19. package/src/async-throttler/useAsyncThrottler.ts +116 -0
  20. package/src/batcher/index.ts +5 -0
  21. package/src/batcher/useBatchedCallback.ts +20 -0
  22. package/src/batcher/useBatcher.ts +108 -0
  23. package/src/debouncer/index.ts +7 -0
  24. package/src/debouncer/useDebouncedCallback.ts +22 -0
  25. package/src/debouncer/useDebouncedState.ts +37 -0
  26. package/src/debouncer/useDebouncedValue.ts +41 -0
  27. package/src/debouncer/useDebouncer.ts +122 -0
  28. package/src/index.ts +63 -0
  29. package/src/internal.ts +45 -0
  30. package/src/provider/PacerProvider.tsrx +25 -0
  31. package/src/provider/PacerProvider.tsrx.d.ts +12 -0
  32. package/src/provider/context.ts +43 -0
  33. package/src/provider/index.ts +4 -0
  34. package/src/queuer/index.ts +6 -0
  35. package/src/queuer/useQueuedState.ts +27 -0
  36. package/src/queuer/useQueuedValue.ts +37 -0
  37. package/src/queuer/useQueuer.ts +110 -0
  38. package/src/rate-limiter/index.ts +7 -0
  39. package/src/rate-limiter/useRateLimitedCallback.ts +24 -0
  40. package/src/rate-limiter/useRateLimitedState.ts +34 -0
  41. package/src/rate-limiter/useRateLimitedValue.ts +38 -0
  42. package/src/rate-limiter/useRateLimiter.ts +119 -0
  43. package/src/throttler/index.ts +7 -0
  44. package/src/throttler/useThrottledCallback.ts +20 -0
  45. package/src/throttler/useThrottledState.ts +35 -0
  46. package/src/throttler/useThrottledValue.ts +38 -0
  47. package/src/throttler/useThrottler.ts +119 -0
  48. package/src/types/index.ts +1 -0
  49. package/src/utils/index.ts +1 -0
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ // Octane port of @tanstack/react-pacer@0.22.1 — hook wrappers over the
2
+ // framework-agnostic @tanstack/pacer core, mirroring the upstream module
3
+ // layout (src/index.ts).
4
+
5
+ // re-export everything from the core pacer package
6
+ export * from '@tanstack/pacer';
7
+
8
+ // provider
9
+ export { PacerProvider } from './provider/PacerProvider.tsrx';
10
+ export type { PacerProviderProps } from './provider/PacerProvider.tsrx';
11
+ export { usePacerContext, useDefaultPacerOptions } from './provider/context';
12
+ export type { PacerProviderOptions } from './provider/context';
13
+
14
+ /**
15
+ * Export every hook individually - DON'T export from barrel files
16
+ */
17
+
18
+ // async-batcher
19
+ export * from './async-batcher/useAsyncBatcher';
20
+ export * from './async-batcher/useAsyncBatchedCallback';
21
+
22
+ // async-debouncer
23
+ export * from './async-debouncer/useAsyncDebouncer';
24
+ export * from './async-debouncer/useAsyncDebouncedCallback';
25
+
26
+ // async-queuer
27
+ export * from './async-queuer/useAsyncQueuer';
28
+ export * from './async-queuer/useAsyncQueuedState';
29
+
30
+ // async-rate-limiter
31
+ export * from './async-rate-limiter/useAsyncRateLimiter';
32
+ export * from './async-rate-limiter/useAsyncRateLimitedCallback';
33
+
34
+ // async-throttler
35
+ export * from './async-throttler/useAsyncThrottler';
36
+ export * from './async-throttler/useAsyncThrottledCallback';
37
+
38
+ // batcher
39
+ export * from './batcher/useBatcher';
40
+ export * from './batcher/useBatchedCallback';
41
+
42
+ // debouncer
43
+ export * from './debouncer/useDebouncedCallback';
44
+ export * from './debouncer/useDebouncedState';
45
+ export * from './debouncer/useDebouncedValue';
46
+ export * from './debouncer/useDebouncer';
47
+
48
+ // queuer
49
+ export * from './queuer/useQueuer';
50
+ export * from './queuer/useQueuedState';
51
+ export * from './queuer/useQueuedValue';
52
+
53
+ // rate-limiter
54
+ export * from './rate-limiter/useRateLimitedCallback';
55
+ export * from './rate-limiter/useRateLimiter';
56
+ export * from './rate-limiter/useRateLimitedState';
57
+ export * from './rate-limiter/useRateLimitedValue';
58
+
59
+ // throttler
60
+ export * from './throttler/useThrottledCallback';
61
+ export * from './throttler/useThrottledState';
62
+ export * from './throttler/useThrottledValue';
63
+ export * from './throttler/useThrottler';
@@ -0,0 +1,45 @@
1
+ import { useSelector } from '@octanejs/tanstack-store';
2
+
3
+ // React type aliases used by upstream signatures (`React.Dispatch<React.SetStateAction<T>>`).
4
+ // Octane's `useState` setter is structurally identical.
5
+ export type SetStateAction<S> = S | ((prev: S) => S);
6
+ export type Dispatch<A> = (value: A) => void;
7
+
8
+ // Slot mechanics for the binding's plain-`.ts` hooks. The octane compiler
9
+ // wraps custom-hook CALLS made from compiled `.tsrx`/`.tsx` modules in
10
+ // `withSlot`, but calls made from plain `.ts` modules (this binding's hooks
11
+ // composing `useSelector` from @octanejs/tanstack-store) are not wrapped —
12
+ // so each `useSelector` call site here hands over its own slot symbol.
13
+ // `useSelector` reads the slot off its last argument; the public overloads
14
+ // don't declare it, hence the cast.
15
+
16
+ type SelectionSource<T> = {
17
+ get: () => T;
18
+ subscribe: (listener: (value: T) => void) => {
19
+ unsubscribe: () => void;
20
+ };
21
+ };
22
+
23
+ // An omitted optional selector can be occupied by Octane's trailing hook slot.
24
+ // Preserve Pacer's non-reactive empty selection without treating that slot as a selector.
25
+ function selectEmpty<TSelected>(): TSelected {
26
+ return {} as TSelected;
27
+ }
28
+
29
+ export function useSelectorSlot<TSource, TSelected>(
30
+ source: SelectionSource<TSource>,
31
+ selector: ((snapshot: TSource) => TSelected) | symbol,
32
+ options: { compare?: (a: TSelected, b: TSelected) => boolean } | undefined,
33
+ slot: symbol,
34
+ ): TSelected {
35
+ const resolvedSelector = typeof selector === 'symbol' ? selectEmpty<TSelected> : selector;
36
+
37
+ return (
38
+ useSelector as (
39
+ source: SelectionSource<TSource>,
40
+ selector: (snapshot: TSource) => TSelected,
41
+ options: { compare?: (a: TSelected, b: TSelected) => boolean } | undefined,
42
+ slot: symbol,
43
+ ) => TSelected
44
+ )(source, resolvedSelector, options, slot);
45
+ }
@@ -0,0 +1,25 @@
1
+ import { useMemo } from 'octane';
2
+ import type { OctaneNode } from 'octane';
3
+ import { PacerContext } from './context.ts';
4
+ import type { PacerContextValue, PacerProviderOptions } from './context.ts';
5
+
6
+ export interface PacerProviderProps {
7
+ children: OctaneNode;
8
+ defaultOptions?: PacerProviderOptions;
9
+ }
10
+
11
+ const DEFAULT_OPTIONS: PacerProviderOptions = {};
12
+
13
+ // PacerProvider — provides shared default options for every pacer hook family.
14
+ // It's authored in .tsrx because it's a COMPONENT that renders a context
15
+ // Provider with children (the binding's hooks stay plain TS).
16
+ export function PacerProvider({ children, defaultOptions = DEFAULT_OPTIONS }: PacerProviderProps) @{
17
+ const contextValue: PacerContextValue = useMemo(
18
+ () => ({
19
+ defaultOptions,
20
+ }),
21
+ [defaultOptions],
22
+ );
23
+
24
+ <PacerContext.Provider value={contextValue}>{children}</PacerContext.Provider>
25
+ }
@@ -0,0 +1,12 @@
1
+ // Type declaration for the .tsrx provider component (PacerProvider.tsrx).
2
+ // It's a SPECIFIC module declaration (resolved by relative path), not an ambient
3
+ // `declare module '*.tsrx'` — so it types only this module and doesn't pollute a
4
+ // consumer's own .tsrx imports. The runtime resolves the real compiled .tsrx.
5
+ import type { PacerProviderOptions } from './context';
6
+
7
+ export interface PacerProviderProps {
8
+ children: unknown;
9
+ defaultOptions?: PacerProviderOptions;
10
+ }
11
+
12
+ export declare function PacerProvider(props: PacerProviderProps): unknown;
@@ -0,0 +1,43 @@
1
+ import { createContext, useContext } from 'octane';
2
+ import type {
3
+ AnyAsyncFunction,
4
+ AnyFunction,
5
+ AsyncBatcherOptions,
6
+ AsyncDebouncerOptions,
7
+ AsyncQueuerOptions,
8
+ AsyncRateLimiterOptions,
9
+ AsyncThrottlerOptions,
10
+ BatcherOptions,
11
+ DebouncerOptions,
12
+ QueuerOptions,
13
+ RateLimiterOptions,
14
+ ThrottlerOptions,
15
+ } from '@tanstack/pacer';
16
+
17
+ export interface PacerProviderOptions {
18
+ asyncBatcher?: Partial<AsyncBatcherOptions<any>>;
19
+ asyncDebouncer?: Partial<AsyncDebouncerOptions<AnyAsyncFunction>>;
20
+ asyncQueuer?: Partial<AsyncQueuerOptions<any>>;
21
+ asyncRateLimiter?: Partial<AsyncRateLimiterOptions<AnyAsyncFunction>>;
22
+ asyncThrottler?: Partial<AsyncThrottlerOptions<AnyAsyncFunction>>;
23
+ batcher?: Partial<BatcherOptions<any>>;
24
+ debouncer?: Partial<DebouncerOptions<AnyFunction>>;
25
+ queuer?: Partial<QueuerOptions<any>>;
26
+ rateLimiter?: Partial<RateLimiterOptions<AnyFunction>>;
27
+ throttler?: Partial<ThrottlerOptions<AnyFunction>>;
28
+ }
29
+
30
+ export interface PacerContextValue {
31
+ defaultOptions: PacerProviderOptions;
32
+ }
33
+
34
+ export const PacerContext = createContext<PacerContextValue | null>(null);
35
+
36
+ export function usePacerContext() {
37
+ return useContext(PacerContext);
38
+ }
39
+
40
+ export function useDefaultPacerOptions() {
41
+ const context = useContext(PacerContext);
42
+ return context?.defaultOptions ?? {};
43
+ }
@@ -0,0 +1,4 @@
1
+ export { PacerProvider } from './PacerProvider.tsrx';
2
+ export type { PacerProviderProps } from './PacerProvider.tsrx';
3
+ export { usePacerContext, useDefaultPacerOptions } from './context';
4
+ export type { PacerProviderOptions } from './context';
@@ -0,0 +1,6 @@
1
+ // re-export everything from the core pacer package, BUT ONLY from the queuer module
2
+ export * from '@tanstack/pacer/queuer';
3
+
4
+ export * from './useQueuer';
5
+ export * from './useQueuedState';
6
+ export * from './useQueuedValue';
@@ -0,0 +1,27 @@
1
+ import { useQueuer } from './useQueuer';
2
+ import type { ReactQueuer, ReactQueuerOptions } from './useQueuer';
3
+ import type { Queuer, QueuerState } from '@tanstack/pacer/queuer';
4
+
5
+ /**
6
+ * An Octane hook that creates a queue with managed state. Returns a tuple of
7
+ * the current queue items, an addItem function, and the queuer instance.
8
+ *
9
+ * The selector defaults to tracking `items` (this hook is inherently
10
+ * reactive to the queue contents).
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const [items, addItem, queuer] = useQueuedState(processItem, { wait: 1000 });
15
+ * ```
16
+ */
17
+ export function useQueuedState<
18
+ TValue,
19
+ TSelected extends Pick<QueuerState<TValue>, 'items'> = Pick<QueuerState<TValue>, 'items'>,
20
+ >(
21
+ fn: (item: TValue) => void,
22
+ options: ReactQueuerOptions<TValue, TSelected> = {},
23
+ selector?: (state: QueuerState<TValue>) => TSelected,
24
+ ): [Array<TValue>, Queuer<TValue>['addItem'], ReactQueuer<TValue, TSelected>] {
25
+ const queue = useQueuer(fn, options, selector);
26
+ return [queue.state.items, queue.addItem, queue];
27
+ }
@@ -0,0 +1,37 @@
1
+ import { useEffect, useState } from 'octane';
2
+ import { useQueuedState } from './useQueuedState';
3
+ import type { ReactQueuer, ReactQueuerOptions } from './useQueuer';
4
+ import type { QueuerState } from '@tanstack/pacer/queuer';
5
+
6
+ /**
7
+ * An Octane hook that creates a queued value that processes changes through a
8
+ * queue, updating at the queue's pace.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * const [queuedValue, queuer] = useQueuedValue(value, { wait: 500 });
13
+ * ```
14
+ */
15
+ export function useQueuedValue<
16
+ TValue,
17
+ TSelected extends Pick<QueuerState<TValue>, 'items'> = Pick<QueuerState<TValue>, 'items'>,
18
+ >(
19
+ initialValue: TValue,
20
+ options: ReactQueuerOptions<TValue, TSelected> = {},
21
+ selector?: (state: QueuerState<TValue>) => TSelected,
22
+ ): [TValue, ReactQueuer<TValue, TSelected>] {
23
+ const [value, setValue] = useState<TValue>(initialValue);
24
+ const [, addItem, queuer] = useQueuedState(
25
+ (item) => {
26
+ setValue(item);
27
+ },
28
+ options,
29
+ selector,
30
+ );
31
+
32
+ useEffect(() => {
33
+ addItem(initialValue);
34
+ }, [initialValue, addItem]);
35
+
36
+ return [value, queuer];
37
+ }
@@ -0,0 +1,110 @@
1
+ import { useEffect, useMemo, useState } from 'octane';
2
+ import { Queuer } from '@tanstack/pacer/queuer';
3
+ import { shallow } from '@octanejs/tanstack-store';
4
+ import { useDefaultPacerOptions } from '../provider/context';
5
+ import { useSelectorSlot } from '../internal';
6
+ import type { Store } from '@octanejs/tanstack-store';
7
+ import type { QueuerOptions, QueuerState } from '@tanstack/pacer/queuer';
8
+
9
+ const subscribeSlot = Symbol.for('@octanejs/tanstack-pacer:useQueuer:Subscribe');
10
+ const stateSlot = Symbol.for('@octanejs/tanstack-pacer:useQueuer:state');
11
+
12
+ export interface ReactQueuerOptions<TValue, TSelected = {}> extends QueuerOptions<TValue> {
13
+ /**
14
+ * Custom unmount behavior. Defaults to stopping the queue.
15
+ */
16
+ onUnmount?: (queuer: ReactQueuer<TValue, TSelected>) => void;
17
+ }
18
+
19
+ export interface ReactQueuer<TValue, TSelected = {}> extends Omit<Queuer<TValue>, 'store'> {
20
+ /**
21
+ * Render-prop component subscribing to a slice of the queuer's state.
22
+ */
23
+ Subscribe: <TSelected>(props: {
24
+ selector: (state: QueuerState<TValue>) => TSelected;
25
+ children: ((state: TSelected) => unknown) | unknown;
26
+ }) => unknown;
27
+ /**
28
+ * Reactive state selected by the hook's `selector` (empty object without one).
29
+ */
30
+ readonly state: Readonly<TSelected>;
31
+ /**
32
+ * The underlying TanStack Store instance.
33
+ */
34
+ readonly store: Store<Readonly<QueuerState<TValue>>>;
35
+ }
36
+
37
+ /**
38
+ * An Octane hook that creates and manages a Queuer instance — a FIFO/LIFO/
39
+ * priority queue processing items at a configurable interval.
40
+ *
41
+ * **By default there are no reactive state subscriptions** — opt in by
42
+ * providing a `selector`; only then does the component re-render when the
43
+ * selected state values change.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * const queuer = useQueuer(processItem, { wait: 1000 }, (state) => ({
48
+ * items: state.items,
49
+ * }));
50
+ * ```
51
+ */
52
+ export function useQueuer<TValue, TSelected = {}>(
53
+ fn: (item: TValue) => void,
54
+ options: ReactQueuerOptions<TValue, TSelected> = {},
55
+ selector: (state: QueuerState<TValue>) => TSelected = () => ({}) as TSelected,
56
+ ): ReactQueuer<TValue, TSelected> {
57
+ const mergedOptions = {
58
+ ...useDefaultPacerOptions().queuer,
59
+ ...options,
60
+ } as ReactQueuerOptions<TValue, TSelected>;
61
+ const [queuer] = useState(() => {
62
+ const queuerInstance = new Queuer<TValue>(fn, mergedOptions) as unknown as ReactQueuer<
63
+ TValue,
64
+ TSelected
65
+ >;
66
+
67
+ queuerInstance.Subscribe = function Subscribe<TSelected>(props: {
68
+ selector: (state: QueuerState<TValue>) => TSelected;
69
+ children: ((state: TSelected) => unknown) | unknown;
70
+ }) {
71
+ const selected = useSelectorSlot(
72
+ queuerInstance.store,
73
+ props.selector,
74
+ { compare: shallow },
75
+ subscribeSlot,
76
+ );
77
+
78
+ return typeof props.children === 'function'
79
+ ? (props.children as (state: TSelected) => unknown)(selected)
80
+ : props.children;
81
+ };
82
+
83
+ return queuerInstance;
84
+ });
85
+
86
+ queuer.fn = fn;
87
+ queuer.setOptions(mergedOptions);
88
+
89
+ // Unmount cleanup only; empty deps keep teardown stable (as upstream).
90
+ useEffect(() => {
91
+ return () => {
92
+ if (mergedOptions.onUnmount) {
93
+ mergedOptions.onUnmount(queuer);
94
+ } else {
95
+ queuer.stop();
96
+ }
97
+ };
98
+ }, []);
99
+
100
+ const state = useSelectorSlot(queuer.store, selector, { compare: shallow }, stateSlot);
101
+
102
+ return useMemo(
103
+ () =>
104
+ ({
105
+ ...queuer,
106
+ state,
107
+ }) as ReactQueuer<TValue, TSelected>, // omit `store` in favor of `state`
108
+ [queuer, state],
109
+ );
110
+ }
@@ -0,0 +1,7 @@
1
+ // re-export everything from the core pacer package, BUT ONLY from the rate-limiter module
2
+ export * from '@tanstack/pacer/rate-limiter';
3
+
4
+ export * from './useRateLimitedCallback';
5
+ export * from './useRateLimiter';
6
+ export * from './useRateLimitedState';
7
+ export * from './useRateLimitedValue';
@@ -0,0 +1,24 @@
1
+ import { useCallback } from 'octane';
2
+ import { useRateLimiter } from './useRateLimiter';
3
+ import type { ReactRateLimiterOptions } from './useRateLimiter';
4
+ import type { AnyFunction } from '@tanstack/pacer/types';
5
+
6
+ /**
7
+ * An Octane hook that creates a rate-limited version of a callback function.
8
+ * The returned function reports whether the call was allowed to execute.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * const rateLimitedCall = useRateLimitedCallback(makeApiCall, {
13
+ * limit: 5,
14
+ * window: 60000,
15
+ * });
16
+ * ```
17
+ */
18
+ export function useRateLimitedCallback<TFn extends AnyFunction>(
19
+ fn: TFn,
20
+ options: ReactRateLimiterOptions<TFn, {}>,
21
+ ): (...args: Parameters<TFn>) => boolean {
22
+ const rateLimitedFn = useRateLimiter(fn, options).maybeExecute;
23
+ return useCallback((...args) => rateLimitedFn(...args), [rateLimitedFn]);
24
+ }
@@ -0,0 +1,34 @@
1
+ import { useState } from 'octane';
2
+ import { useRateLimiter } from './useRateLimiter';
3
+ import type { ReactRateLimiter, ReactRateLimiterOptions } from './useRateLimiter';
4
+ import type { Dispatch, SetStateAction } from '../internal';
5
+ import type { RateLimiterState } from '@tanstack/pacer/rate-limiter';
6
+
7
+ /**
8
+ * An Octane hook that creates a rate-limited state value. Returns a tuple of
9
+ * the current value, a rate-limited updater, and the rate limiter instance.
10
+ *
11
+ * **By default there are no reactive state subscriptions** — opt in by
12
+ * providing a `selector` (see {@link useRateLimiter}).
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const [value, setValue, rateLimiter] = useRateLimitedState(0, {
17
+ * limit: 5,
18
+ * window: 60000,
19
+ * });
20
+ * ```
21
+ */
22
+ export function useRateLimitedState<TValue, TSelected = RateLimiterState>(
23
+ value: TValue,
24
+ options: ReactRateLimiterOptions<Dispatch<SetStateAction<TValue>>, TSelected>,
25
+ selector?: (state: RateLimiterState) => TSelected,
26
+ ): [
27
+ TValue,
28
+ Dispatch<SetStateAction<TValue>>,
29
+ ReactRateLimiter<Dispatch<SetStateAction<TValue>>, TSelected>,
30
+ ] {
31
+ const [rateLimitedValue, setRateLimitedValue] = useState<TValue>(value);
32
+ const rateLimiter = useRateLimiter(setRateLimitedValue, options, selector);
33
+ return [rateLimitedValue, rateLimiter.maybeExecute, rateLimiter];
34
+ }
@@ -0,0 +1,38 @@
1
+ import { useEffect } from 'octane';
2
+ import { useRateLimitedState } from './useRateLimitedState';
3
+ import type { ReactRateLimiter, ReactRateLimiterOptions } from './useRateLimiter';
4
+ import type { Dispatch, SetStateAction } from '../internal';
5
+ import type { RateLimiterState } from '@tanstack/pacer/rate-limiter';
6
+
7
+ /**
8
+ * An Octane hook that creates a rate-limited value that automatically tracks
9
+ * changes to the input value.
10
+ *
11
+ * **By default there are no reactive state subscriptions** — opt in by
12
+ * providing a `selector` (see {@link useRateLimiter}).
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const [rateLimitedValue, rateLimiter] = useRateLimitedValue(value, {
17
+ * limit: 5,
18
+ * window: 60000,
19
+ * });
20
+ * ```
21
+ */
22
+ export function useRateLimitedValue<TValue, TSelected = RateLimiterState>(
23
+ value: TValue,
24
+ options: ReactRateLimiterOptions<Dispatch<SetStateAction<TValue>>, TSelected>,
25
+ selector?: (state: RateLimiterState) => TSelected,
26
+ ): [TValue, ReactRateLimiter<Dispatch<SetStateAction<TValue>>, TSelected>] {
27
+ const [rateLimitedValue, setRateLimitedValue, rateLimiter] = useRateLimitedState(
28
+ value,
29
+ options,
30
+ selector,
31
+ );
32
+
33
+ useEffect(() => {
34
+ setRateLimitedValue(value);
35
+ }, [value, setRateLimitedValue]);
36
+
37
+ return [rateLimitedValue, rateLimiter];
38
+ }
@@ -0,0 +1,119 @@
1
+ import { useEffect, useMemo, useState } from 'octane';
2
+ import { RateLimiter } from '@tanstack/pacer/rate-limiter';
3
+ import { shallow } from '@octanejs/tanstack-store';
4
+ import { useDefaultPacerOptions } from '../provider/context';
5
+ import { useSelectorSlot } from '../internal';
6
+ import type { Store } from '@octanejs/tanstack-store';
7
+ import type { RateLimiterOptions, RateLimiterState } from '@tanstack/pacer/rate-limiter';
8
+ import type { AnyFunction } from '@tanstack/pacer/types';
9
+
10
+ const subscribeSlot = Symbol.for('@octanejs/tanstack-pacer:useRateLimiter:Subscribe');
11
+ const stateSlot = Symbol.for('@octanejs/tanstack-pacer:useRateLimiter:state');
12
+
13
+ export interface ReactRateLimiterOptions<
14
+ TFn extends AnyFunction,
15
+ TSelected = {},
16
+ > extends RateLimiterOptions<TFn> {
17
+ /**
18
+ * Custom unmount behavior (rate limiters have nothing to cancel by default).
19
+ */
20
+ onUnmount?: (rateLimiter: ReactRateLimiter<TFn, TSelected>) => void;
21
+ }
22
+
23
+ export interface ReactRateLimiter<TFn extends AnyFunction, TSelected = {}> extends Omit<
24
+ RateLimiter<TFn>,
25
+ 'store'
26
+ > {
27
+ /**
28
+ * Render-prop component subscribing to a slice of the rate limiter's state.
29
+ */
30
+ Subscribe: <TSelected>(props: {
31
+ selector: (state: RateLimiterState) => TSelected;
32
+ children: ((state: TSelected) => unknown) | unknown;
33
+ }) => unknown;
34
+ /**
35
+ * Reactive state selected by the hook's `selector` (empty object without one).
36
+ */
37
+ readonly state: Readonly<TSelected>;
38
+ /**
39
+ * The underlying TanStack Store instance.
40
+ */
41
+ readonly store: Store<Readonly<RateLimiterState>>;
42
+ }
43
+
44
+ /**
45
+ * An Octane hook that creates and manages a RateLimiter instance.
46
+ *
47
+ * The rate limiter allows a maximum number of executions within a time window
48
+ * (fixed or sliding).
49
+ *
50
+ * **By default there are no reactive state subscriptions** — opt in by
51
+ * providing a `selector`; only then does the component re-render when the
52
+ * selected state values change.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * const rateLimiter = useRateLimiter(
57
+ * makeApiCall,
58
+ * { limit: 5, window: 60000 },
59
+ * (state) => ({ rejectionCount: state.rejectionCount }),
60
+ * );
61
+ * ```
62
+ */
63
+ export function useRateLimiter<TFn extends AnyFunction, TSelected = {}>(
64
+ fn: TFn,
65
+ options: ReactRateLimiterOptions<TFn, TSelected>,
66
+ selector: (state: RateLimiterState) => TSelected = () => ({}) as TSelected,
67
+ ): ReactRateLimiter<TFn, TSelected> {
68
+ const mergedOptions = {
69
+ ...useDefaultPacerOptions().rateLimiter,
70
+ ...options,
71
+ } as ReactRateLimiterOptions<TFn, TSelected>;
72
+ const [rateLimiter] = useState(() => {
73
+ const rateLimiterInstance = new RateLimiter<TFn>(
74
+ fn,
75
+ mergedOptions,
76
+ ) as unknown as ReactRateLimiter<TFn, TSelected>;
77
+
78
+ rateLimiterInstance.Subscribe = function Subscribe<TSelected>(props: {
79
+ selector: (state: RateLimiterState) => TSelected;
80
+ children: ((state: TSelected) => unknown) | unknown;
81
+ }) {
82
+ const selected = useSelectorSlot(
83
+ rateLimiterInstance.store,
84
+ props.selector,
85
+ { compare: shallow },
86
+ subscribeSlot,
87
+ );
88
+
89
+ return typeof props.children === 'function'
90
+ ? (props.children as (state: TSelected) => unknown)(selected)
91
+ : props.children;
92
+ };
93
+
94
+ return rateLimiterInstance;
95
+ });
96
+
97
+ rateLimiter.fn = fn;
98
+ rateLimiter.setOptions(mergedOptions);
99
+
100
+ // Unmount cleanup only; empty deps keep teardown stable (as upstream).
101
+ useEffect(() => {
102
+ return () => {
103
+ if (mergedOptions.onUnmount) {
104
+ mergedOptions.onUnmount(rateLimiter);
105
+ }
106
+ };
107
+ }, []);
108
+
109
+ const state = useSelectorSlot(rateLimiter.store, selector, { compare: shallow }, stateSlot);
110
+
111
+ return useMemo(
112
+ () =>
113
+ ({
114
+ ...rateLimiter,
115
+ state,
116
+ }) as ReactRateLimiter<TFn, TSelected>, // omit `store` in favor of `state`
117
+ [rateLimiter, state],
118
+ );
119
+ }
@@ -0,0 +1,7 @@
1
+ // re-export everything from the core pacer package, BUT ONLY from the throttler module
2
+ export * from '@tanstack/pacer/throttler';
3
+
4
+ export * from './useThrottledCallback';
5
+ export * from './useThrottledState';
6
+ export * from './useThrottledValue';
7
+ export * from './useThrottler';
@@ -0,0 +1,20 @@
1
+ import { useCallback } from 'octane';
2
+ import { useThrottler } from './useThrottler';
3
+ import type { ReactThrottlerOptions } from './useThrottler';
4
+ import type { AnyFunction } from '@tanstack/pacer/types';
5
+
6
+ /**
7
+ * An Octane hook that creates a throttled version of a callback function.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * const throttledScroll = useThrottledCallback(onScroll, { wait: 200 });
12
+ * ```
13
+ */
14
+ export function useThrottledCallback<TFn extends AnyFunction>(
15
+ fn: TFn,
16
+ options: ReactThrottlerOptions<TFn, {}>,
17
+ ): (...args: Parameters<TFn>) => void {
18
+ const throttledFn = useThrottler(fn, options).maybeExecute;
19
+ return useCallback((...args) => throttledFn(...args), [throttledFn]);
20
+ }
@@ -0,0 +1,35 @@
1
+ import { useState } from 'octane';
2
+ import { useThrottler } from './useThrottler';
3
+ import type { ReactThrottler, ReactThrottlerOptions } from './useThrottler';
4
+ import type { Dispatch, SetStateAction } from '../internal';
5
+ import type { ThrottlerState } from '@tanstack/pacer/throttler';
6
+
7
+ /**
8
+ * An Octane hook that creates a throttled state value, combining `useState`
9
+ * with throttling functionality. Returns a tuple of the current throttled
10
+ * value, a throttled updater, and the throttler instance.
11
+ *
12
+ * **By default there are no reactive state subscriptions** — opt in by
13
+ * providing a `selector` (see {@link useThrottler}).
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const [value, setValue, throttler] = useThrottledState(0, { wait: 1000 });
18
+ * ```
19
+ */
20
+ export function useThrottledState<
21
+ TValue,
22
+ TSelected = ThrottlerState<Dispatch<SetStateAction<TValue>>>,
23
+ >(
24
+ value: TValue,
25
+ options: ReactThrottlerOptions<Dispatch<SetStateAction<TValue>>, TSelected>,
26
+ selector?: (state: ThrottlerState<Dispatch<SetStateAction<TValue>>>) => TSelected,
27
+ ): [
28
+ TValue,
29
+ Dispatch<SetStateAction<TValue>>,
30
+ ReactThrottler<Dispatch<SetStateAction<TValue>>, TSelected>,
31
+ ] {
32
+ const [throttledValue, setThrottledValue] = useState<TValue>(value);
33
+ const throttler = useThrottler(setThrottledValue, options, selector);
34
+ return [throttledValue, throttler.maybeExecute, throttler];
35
+ }