@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.
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/package.json +62 -0
- package/src/async-batcher/index.ts +5 -0
- package/src/async-batcher/useAsyncBatchedCallback.ts +20 -0
- package/src/async-batcher/useAsyncBatcher.ts +116 -0
- package/src/async-debouncer/index.ts +5 -0
- package/src/async-debouncer/useAsyncDebouncedCallback.ts +25 -0
- package/src/async-debouncer/useAsyncDebouncer.ts +123 -0
- package/src/async-queuer/index.ts +5 -0
- package/src/async-queuer/useAsyncQueuedState.ts +30 -0
- package/src/async-queuer/useAsyncQueuer.ts +116 -0
- package/src/async-rate-limiter/index.ts +5 -0
- package/src/async-rate-limiter/useAsyncRateLimitedCallback.ts +27 -0
- package/src/async-rate-limiter/useAsyncRateLimiter.ts +120 -0
- package/src/async-retryer/index.ts +1 -0
- package/src/async-throttler/index.ts +5 -0
- package/src/async-throttler/useAsyncThrottledCallback.ts +24 -0
- package/src/async-throttler/useAsyncThrottler.ts +116 -0
- package/src/batcher/index.ts +5 -0
- package/src/batcher/useBatchedCallback.ts +20 -0
- package/src/batcher/useBatcher.ts +108 -0
- package/src/debouncer/index.ts +7 -0
- package/src/debouncer/useDebouncedCallback.ts +22 -0
- package/src/debouncer/useDebouncedState.ts +37 -0
- package/src/debouncer/useDebouncedValue.ts +41 -0
- package/src/debouncer/useDebouncer.ts +122 -0
- package/src/index.ts +63 -0
- package/src/internal.ts +45 -0
- package/src/provider/PacerProvider.tsrx +25 -0
- package/src/provider/PacerProvider.tsrx.d.ts +12 -0
- package/src/provider/context.ts +43 -0
- package/src/provider/index.ts +4 -0
- package/src/queuer/index.ts +6 -0
- package/src/queuer/useQueuedState.ts +27 -0
- package/src/queuer/useQueuedValue.ts +37 -0
- package/src/queuer/useQueuer.ts +110 -0
- package/src/rate-limiter/index.ts +7 -0
- package/src/rate-limiter/useRateLimitedCallback.ts +24 -0
- package/src/rate-limiter/useRateLimitedState.ts +34 -0
- package/src/rate-limiter/useRateLimitedValue.ts +38 -0
- package/src/rate-limiter/useRateLimiter.ts +119 -0
- package/src/throttler/index.ts +7 -0
- package/src/throttler/useThrottledCallback.ts +20 -0
- package/src/throttler/useThrottledState.ts +35 -0
- package/src/throttler/useThrottledValue.ts +38 -0
- package/src/throttler/useThrottler.ts +119 -0
- package/src/types/index.ts +1 -0
- 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';
|
package/src/internal.ts
ADDED
|
@@ -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,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
|
+
}
|