@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'octane';
|
|
2
|
+
import { AsyncRateLimiter } from '@tanstack/pacer/async-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 {
|
|
8
|
+
AsyncRateLimiterOptions,
|
|
9
|
+
AsyncRateLimiterState,
|
|
10
|
+
} from '@tanstack/pacer/async-rate-limiter';
|
|
11
|
+
import type { AnyAsyncFunction } from '@tanstack/pacer/types';
|
|
12
|
+
|
|
13
|
+
const subscribeSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncRateLimiter:Subscribe');
|
|
14
|
+
const stateSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncRateLimiter:state');
|
|
15
|
+
|
|
16
|
+
export interface ReactAsyncRateLimiterOptions<
|
|
17
|
+
TFn extends AnyAsyncFunction,
|
|
18
|
+
TSelected = {},
|
|
19
|
+
> extends AsyncRateLimiterOptions<TFn> {
|
|
20
|
+
/**
|
|
21
|
+
* Custom unmount behavior. Defaults to aborting in-flight executions.
|
|
22
|
+
*/
|
|
23
|
+
onUnmount?: (rateLimiter: ReactAsyncRateLimiter<TFn, TSelected>) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReactAsyncRateLimiter<TFn extends AnyAsyncFunction, TSelected = {}> extends Omit<
|
|
27
|
+
AsyncRateLimiter<TFn>,
|
|
28
|
+
'store'
|
|
29
|
+
> {
|
|
30
|
+
/**
|
|
31
|
+
* Render-prop component subscribing to a slice of the rate limiter's state.
|
|
32
|
+
*/
|
|
33
|
+
Subscribe: <TSelected>(props: {
|
|
34
|
+
selector: (state: AsyncRateLimiterState<TFn>) => TSelected;
|
|
35
|
+
children: ((state: TSelected) => unknown) | unknown;
|
|
36
|
+
}) => unknown;
|
|
37
|
+
/**
|
|
38
|
+
* Reactive state selected by the hook's `selector` (empty object without one).
|
|
39
|
+
*/
|
|
40
|
+
readonly state: Readonly<TSelected>;
|
|
41
|
+
/**
|
|
42
|
+
* The underlying TanStack Store instance.
|
|
43
|
+
*/
|
|
44
|
+
readonly store: Store<Readonly<AsyncRateLimiterState<TFn>>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An Octane hook that creates and manages an AsyncRateLimiter instance.
|
|
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 = useAsyncRateLimiter(fetchData, {
|
|
57
|
+
* limit: 5,
|
|
58
|
+
* window: 60000,
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function useAsyncRateLimiter<TFn extends AnyAsyncFunction, TSelected = {}>(
|
|
63
|
+
fn: TFn,
|
|
64
|
+
options: ReactAsyncRateLimiterOptions<TFn, TSelected>,
|
|
65
|
+
selector: (state: AsyncRateLimiterState<TFn>) => TSelected = () => ({}) as TSelected,
|
|
66
|
+
): ReactAsyncRateLimiter<TFn, TSelected> {
|
|
67
|
+
const mergedOptions = {
|
|
68
|
+
...useDefaultPacerOptions().asyncRateLimiter,
|
|
69
|
+
...options,
|
|
70
|
+
} as ReactAsyncRateLimiterOptions<TFn, TSelected>;
|
|
71
|
+
const [asyncRateLimiter] = useState(() => {
|
|
72
|
+
const asyncRateLimiterInstance = new AsyncRateLimiter<TFn>(
|
|
73
|
+
fn,
|
|
74
|
+
mergedOptions,
|
|
75
|
+
) as unknown as ReactAsyncRateLimiter<TFn, TSelected>;
|
|
76
|
+
|
|
77
|
+
asyncRateLimiterInstance.Subscribe = function Subscribe<TSelected>(props: {
|
|
78
|
+
selector: (state: AsyncRateLimiterState<TFn>) => TSelected;
|
|
79
|
+
children: ((state: TSelected) => unknown) | unknown;
|
|
80
|
+
}) {
|
|
81
|
+
const selected = useSelectorSlot(
|
|
82
|
+
asyncRateLimiterInstance.store,
|
|
83
|
+
props.selector,
|
|
84
|
+
{ compare: shallow },
|
|
85
|
+
subscribeSlot,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return typeof props.children === 'function'
|
|
89
|
+
? (props.children as (state: TSelected) => unknown)(selected)
|
|
90
|
+
: props.children;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return asyncRateLimiterInstance;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
asyncRateLimiter.fn = fn;
|
|
97
|
+
asyncRateLimiter.setOptions(mergedOptions);
|
|
98
|
+
|
|
99
|
+
const state = useSelectorSlot(asyncRateLimiter.store, selector, { compare: shallow }, stateSlot);
|
|
100
|
+
|
|
101
|
+
// Unmount cleanup only; empty deps keep teardown stable (as upstream).
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
return () => {
|
|
104
|
+
if (mergedOptions.onUnmount) {
|
|
105
|
+
mergedOptions.onUnmount(asyncRateLimiter);
|
|
106
|
+
} else {
|
|
107
|
+
asyncRateLimiter.abort();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
return useMemo(
|
|
113
|
+
() =>
|
|
114
|
+
({
|
|
115
|
+
...asyncRateLimiter,
|
|
116
|
+
state,
|
|
117
|
+
}) as ReactAsyncRateLimiter<TFn, TSelected>, // omit `store` in favor of `state`
|
|
118
|
+
[asyncRateLimiter, state],
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@tanstack/pacer/async-retryer';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback } from 'octane';
|
|
2
|
+
import { useAsyncThrottler } from './useAsyncThrottler';
|
|
3
|
+
import type { ReactAsyncThrottlerOptions } from './useAsyncThrottler';
|
|
4
|
+
import type { AnyAsyncFunction } from '@tanstack/pacer/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An Octane hook that creates a throttled version of an async callback
|
|
8
|
+
* function.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const throttledSave = useAsyncThrottledCallback(saveApi, { wait: 1000 });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function useAsyncThrottledCallback<TFn extends AnyAsyncFunction>(
|
|
16
|
+
fn: TFn,
|
|
17
|
+
options: ReactAsyncThrottlerOptions<TFn, {}>,
|
|
18
|
+
): (...args: Parameters<TFn>) => Promise<ReturnType<TFn>> {
|
|
19
|
+
const asyncThrottledFn = useAsyncThrottler(fn, options).maybeExecute;
|
|
20
|
+
return useCallback(
|
|
21
|
+
(...args) => asyncThrottledFn(...args) as Promise<ReturnType<TFn>>,
|
|
22
|
+
[asyncThrottledFn],
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'octane';
|
|
2
|
+
import { AsyncThrottler } from '@tanstack/pacer/async-throttler';
|
|
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 { AsyncThrottlerOptions, AsyncThrottlerState } from '@tanstack/pacer/async-throttler';
|
|
8
|
+
import type { AnyAsyncFunction } from '@tanstack/pacer/types';
|
|
9
|
+
|
|
10
|
+
const subscribeSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncThrottler:Subscribe');
|
|
11
|
+
const stateSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncThrottler:state');
|
|
12
|
+
|
|
13
|
+
export interface ReactAsyncThrottlerOptions<
|
|
14
|
+
TFn extends AnyAsyncFunction,
|
|
15
|
+
TSelected = {},
|
|
16
|
+
> extends AsyncThrottlerOptions<TFn> {
|
|
17
|
+
/**
|
|
18
|
+
* Custom unmount behavior. Defaults to cancelling pending executions and
|
|
19
|
+
* aborting in-flight ones.
|
|
20
|
+
*/
|
|
21
|
+
onUnmount?: (throttler: ReactAsyncThrottler<TFn, TSelected>) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ReactAsyncThrottler<TFn extends AnyAsyncFunction, TSelected = {}> extends Omit<
|
|
25
|
+
AsyncThrottler<TFn>,
|
|
26
|
+
'store'
|
|
27
|
+
> {
|
|
28
|
+
/**
|
|
29
|
+
* Render-prop component subscribing to a slice of the throttler's state.
|
|
30
|
+
*/
|
|
31
|
+
Subscribe: <TSelected>(props: {
|
|
32
|
+
selector: (state: AsyncThrottlerState<TFn>) => TSelected;
|
|
33
|
+
children: ((state: TSelected) => unknown) | unknown;
|
|
34
|
+
}) => unknown;
|
|
35
|
+
/**
|
|
36
|
+
* Reactive state selected by the hook's `selector` (empty object without one).
|
|
37
|
+
*/
|
|
38
|
+
readonly state: Readonly<TSelected>;
|
|
39
|
+
/**
|
|
40
|
+
* The underlying TanStack Store instance.
|
|
41
|
+
*/
|
|
42
|
+
readonly store: Store<Readonly<AsyncThrottlerState<TFn>>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An Octane hook that creates and manages an AsyncThrottler instance.
|
|
47
|
+
*
|
|
48
|
+
* **By default there are no reactive state subscriptions** — opt in by
|
|
49
|
+
* providing a `selector`; only then does the component re-render when the
|
|
50
|
+
* selected state values change.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* const throttler = useAsyncThrottler(async (v) => save(v), { wait: 1000 });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function useAsyncThrottler<TFn extends AnyAsyncFunction, TSelected = {}>(
|
|
58
|
+
fn: TFn,
|
|
59
|
+
options: ReactAsyncThrottlerOptions<TFn, TSelected>,
|
|
60
|
+
selector: (state: AsyncThrottlerState<TFn>) => TSelected = () => ({}) as TSelected,
|
|
61
|
+
): ReactAsyncThrottler<TFn, TSelected> {
|
|
62
|
+
const mergedOptions = {
|
|
63
|
+
...useDefaultPacerOptions().asyncThrottler,
|
|
64
|
+
...options,
|
|
65
|
+
} as ReactAsyncThrottlerOptions<TFn, TSelected>;
|
|
66
|
+
const [asyncThrottler] = useState(() => {
|
|
67
|
+
const asyncThrottlerInstance = new AsyncThrottler<TFn>(
|
|
68
|
+
fn,
|
|
69
|
+
mergedOptions,
|
|
70
|
+
) as unknown as ReactAsyncThrottler<TFn, TSelected>;
|
|
71
|
+
|
|
72
|
+
asyncThrottlerInstance.Subscribe = function Subscribe<TSelected>(props: {
|
|
73
|
+
selector: (state: AsyncThrottlerState<TFn>) => TSelected;
|
|
74
|
+
children: ((state: TSelected) => unknown) | unknown;
|
|
75
|
+
}) {
|
|
76
|
+
const selected = useSelectorSlot(
|
|
77
|
+
asyncThrottlerInstance.store,
|
|
78
|
+
props.selector,
|
|
79
|
+
{ compare: shallow },
|
|
80
|
+
subscribeSlot,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return typeof props.children === 'function'
|
|
84
|
+
? (props.children as (state: TSelected) => unknown)(selected)
|
|
85
|
+
: props.children;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return asyncThrottlerInstance;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
asyncThrottler.fn = fn;
|
|
92
|
+
asyncThrottler.setOptions(mergedOptions);
|
|
93
|
+
|
|
94
|
+
const state = useSelectorSlot(asyncThrottler.store, selector, { compare: shallow }, stateSlot);
|
|
95
|
+
|
|
96
|
+
// Unmount cleanup only; empty deps keep teardown stable (as upstream).
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
return () => {
|
|
99
|
+
if (mergedOptions.onUnmount) {
|
|
100
|
+
mergedOptions.onUnmount(asyncThrottler);
|
|
101
|
+
} else {
|
|
102
|
+
asyncThrottler.cancel();
|
|
103
|
+
asyncThrottler.abort();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
return useMemo(
|
|
109
|
+
() =>
|
|
110
|
+
({
|
|
111
|
+
...asyncThrottler,
|
|
112
|
+
state,
|
|
113
|
+
}) as ReactAsyncThrottler<TFn, TSelected>, // omit `store` in favor of `state`
|
|
114
|
+
[asyncThrottler, state],
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useCallback } from 'octane';
|
|
2
|
+
import { useBatcher } from './useBatcher';
|
|
3
|
+
import type { ReactBatcherOptions } from './useBatcher';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An Octane hook that creates a batched version of a callback function.
|
|
7
|
+
* Calling the returned function adds the item to the batch.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const addToBatch = useBatchedCallback(processBatch, { maxSize: 10 });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function useBatchedCallback<TValue>(
|
|
15
|
+
fn: (items: Array<TValue>) => void,
|
|
16
|
+
options: ReactBatcherOptions<TValue, {}>,
|
|
17
|
+
): (item: TValue) => void {
|
|
18
|
+
const batchedFn = useBatcher(fn, options).addItem;
|
|
19
|
+
return useCallback((item: TValue) => batchedFn(item), [batchedFn]);
|
|
20
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'octane';
|
|
2
|
+
import { Batcher } from '@tanstack/pacer/batcher';
|
|
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 { BatcherOptions, BatcherState } from '@tanstack/pacer/batcher';
|
|
8
|
+
|
|
9
|
+
const subscribeSlot = Symbol.for('@octanejs/tanstack-pacer:useBatcher:Subscribe');
|
|
10
|
+
const stateSlot = Symbol.for('@octanejs/tanstack-pacer:useBatcher:state');
|
|
11
|
+
|
|
12
|
+
export interface ReactBatcherOptions<TValue, TSelected = {}> extends BatcherOptions<TValue> {
|
|
13
|
+
/**
|
|
14
|
+
* Custom unmount behavior. Defaults to cancelling any pending batch.
|
|
15
|
+
*/
|
|
16
|
+
onUnmount?: (batcher: ReactBatcher<TValue, TSelected>) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ReactBatcher<TValue, TSelected = {}> extends Omit<Batcher<TValue>, 'store'> {
|
|
20
|
+
/**
|
|
21
|
+
* Render-prop component subscribing to a slice of the batcher's state.
|
|
22
|
+
*/
|
|
23
|
+
Subscribe: <TSelected>(props: {
|
|
24
|
+
selector: (state: BatcherState<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<BatcherState<TValue>>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* An Octane hook that creates and manages a Batcher instance — collects items
|
|
39
|
+
* and processes them together by size or time threshold.
|
|
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 batcher = useBatcher(processBatch, { maxSize: 10, wait: 2000 });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function useBatcher<TValue, TSelected = {}>(
|
|
51
|
+
fn: (items: Array<TValue>) => void,
|
|
52
|
+
options: ReactBatcherOptions<TValue, TSelected> = {},
|
|
53
|
+
selector: (state: BatcherState<TValue>) => TSelected = () => ({}) as TSelected,
|
|
54
|
+
): ReactBatcher<TValue, TSelected> {
|
|
55
|
+
const mergedOptions = {
|
|
56
|
+
...useDefaultPacerOptions().batcher,
|
|
57
|
+
...options,
|
|
58
|
+
} as ReactBatcherOptions<TValue, TSelected>;
|
|
59
|
+
const [batcher] = useState(() => {
|
|
60
|
+
const batcherInstance = new Batcher<TValue>(fn, mergedOptions) as unknown as ReactBatcher<
|
|
61
|
+
TValue,
|
|
62
|
+
TSelected
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
batcherInstance.Subscribe = function Subscribe<TSelected>(props: {
|
|
66
|
+
selector: (state: BatcherState<TValue>) => TSelected;
|
|
67
|
+
children: ((state: TSelected) => unknown) | unknown;
|
|
68
|
+
}) {
|
|
69
|
+
const selected = useSelectorSlot(
|
|
70
|
+
batcherInstance.store,
|
|
71
|
+
props.selector,
|
|
72
|
+
{ compare: shallow },
|
|
73
|
+
subscribeSlot,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return typeof props.children === 'function'
|
|
77
|
+
? (props.children as (state: TSelected) => unknown)(selected)
|
|
78
|
+
: props.children;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return batcherInstance;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
batcher.fn = fn;
|
|
85
|
+
batcher.setOptions(mergedOptions);
|
|
86
|
+
|
|
87
|
+
// Unmount cleanup only; empty deps keep teardown stable (as upstream).
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
return () => {
|
|
90
|
+
if (mergedOptions.onUnmount) {
|
|
91
|
+
mergedOptions.onUnmount(batcher);
|
|
92
|
+
} else {
|
|
93
|
+
batcher.cancel();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const state = useSelectorSlot(batcher.store, selector, { compare: shallow }, stateSlot);
|
|
99
|
+
|
|
100
|
+
return useMemo(
|
|
101
|
+
() =>
|
|
102
|
+
({
|
|
103
|
+
...batcher,
|
|
104
|
+
state,
|
|
105
|
+
}) as ReactBatcher<TValue, TSelected>, // omit `store` in favor of `state`
|
|
106
|
+
[batcher, state],
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// re-export everything from the core pacer package, BUT ONLY from the debouncer module
|
|
2
|
+
export * from '@tanstack/pacer/debouncer';
|
|
3
|
+
|
|
4
|
+
export * from './useDebouncedCallback';
|
|
5
|
+
export * from './useDebouncedState';
|
|
6
|
+
export * from './useDebouncedValue';
|
|
7
|
+
export * from './useDebouncer';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useCallback } from 'octane';
|
|
2
|
+
import { useDebouncer } from './useDebouncer';
|
|
3
|
+
import type { ReactDebouncerOptions } from './useDebouncer';
|
|
4
|
+
import type { AnyFunction } from '@tanstack/pacer/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An Octane hook that creates a debounced version of a callback function.
|
|
8
|
+
* The returned function delays invoking the callback until after the
|
|
9
|
+
* specified wait time has elapsed since its last invocation.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const debouncedSearch = useDebouncedCallback(performSearch, { wait: 500 });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function useDebouncedCallback<TFn extends AnyFunction>(
|
|
17
|
+
fn: TFn,
|
|
18
|
+
options: ReactDebouncerOptions<TFn, {}>,
|
|
19
|
+
): (...args: Parameters<TFn>) => void {
|
|
20
|
+
const debouncedFn = useDebouncer(fn, options).maybeExecute;
|
|
21
|
+
return useCallback((...args) => debouncedFn(...args), [debouncedFn]);
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useState } from 'octane';
|
|
2
|
+
import { useDebouncer } from './useDebouncer';
|
|
3
|
+
import type { ReactDebouncer, ReactDebouncerOptions } from './useDebouncer';
|
|
4
|
+
import type { Dispatch, SetStateAction } from '../internal';
|
|
5
|
+
import type { DebouncerState } from '@tanstack/pacer/debouncer';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An Octane hook that creates a debounced state value, combining `useState`
|
|
9
|
+
* with debouncing functionality. Returns a tuple of the current debounced
|
|
10
|
+
* value, a debounced updater, and the debouncer instance.
|
|
11
|
+
*
|
|
12
|
+
* **By default there are no reactive state subscriptions** — opt in by
|
|
13
|
+
* providing a `selector` (see {@link useDebouncer}).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const [searchTerm, setSearchTerm, debouncer] = useDebouncedState('', {
|
|
18
|
+
* wait: 500,
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function useDebouncedState<
|
|
23
|
+
TValue,
|
|
24
|
+
TSelected = DebouncerState<Dispatch<SetStateAction<TValue>>>,
|
|
25
|
+
>(
|
|
26
|
+
value: TValue,
|
|
27
|
+
options: ReactDebouncerOptions<Dispatch<SetStateAction<TValue>>, TSelected>,
|
|
28
|
+
selector?: (state: DebouncerState<Dispatch<SetStateAction<TValue>>>) => TSelected,
|
|
29
|
+
): [
|
|
30
|
+
TValue,
|
|
31
|
+
Dispatch<SetStateAction<TValue>>,
|
|
32
|
+
ReactDebouncer<Dispatch<SetStateAction<TValue>>, TSelected>,
|
|
33
|
+
] {
|
|
34
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
35
|
+
const debouncer = useDebouncer(setDebouncedValue, options, selector);
|
|
36
|
+
return [debouncedValue, debouncer.maybeExecute, debouncer];
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect } from 'octane';
|
|
2
|
+
import { useDebouncedState } from './useDebouncedState';
|
|
3
|
+
import type { ReactDebouncer, ReactDebouncerOptions } from './useDebouncer';
|
|
4
|
+
import type { Dispatch, SetStateAction } from '../internal';
|
|
5
|
+
import type { DebouncerState } from '@tanstack/pacer/debouncer';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An Octane hook that creates a debounced value that updates only after a
|
|
9
|
+
* specified delay. Unlike `useDebouncedState`, this hook automatically tracks
|
|
10
|
+
* changes to the input value and updates the debounced value accordingly.
|
|
11
|
+
*
|
|
12
|
+
* **By default there are no reactive state subscriptions** — opt in by
|
|
13
|
+
* providing a `selector` (see {@link useDebouncer}).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const [debouncedQuery, debouncer] = useDebouncedValue(searchQuery, {
|
|
18
|
+
* wait: 500, // Wait 500ms after last change
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function useDebouncedValue<
|
|
23
|
+
TValue,
|
|
24
|
+
TSelected = DebouncerState<Dispatch<SetStateAction<TValue>>>,
|
|
25
|
+
>(
|
|
26
|
+
value: TValue,
|
|
27
|
+
options: ReactDebouncerOptions<Dispatch<SetStateAction<TValue>>, TSelected>,
|
|
28
|
+
selector?: (state: DebouncerState<Dispatch<SetStateAction<TValue>>>) => TSelected,
|
|
29
|
+
): [TValue, ReactDebouncer<Dispatch<SetStateAction<TValue>>, TSelected>] {
|
|
30
|
+
const [debouncedValue, setDebouncedValue, debouncer] = useDebouncedState(
|
|
31
|
+
value,
|
|
32
|
+
options,
|
|
33
|
+
selector,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setDebouncedValue(value);
|
|
38
|
+
}, [value, setDebouncedValue]);
|
|
39
|
+
|
|
40
|
+
return [debouncedValue, debouncer];
|
|
41
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'octane';
|
|
2
|
+
import { Debouncer } from '@tanstack/pacer/debouncer';
|
|
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 { DebouncerOptions, DebouncerState } from '@tanstack/pacer/debouncer';
|
|
8
|
+
import type { AnyFunction } from '@tanstack/pacer/types';
|
|
9
|
+
|
|
10
|
+
const subscribeSlot = Symbol.for('@octanejs/tanstack-pacer:useDebouncer:Subscribe');
|
|
11
|
+
const stateSlot = Symbol.for('@octanejs/tanstack-pacer:useDebouncer:state');
|
|
12
|
+
|
|
13
|
+
export interface ReactDebouncerOptions<
|
|
14
|
+
TFn extends AnyFunction,
|
|
15
|
+
TSelected = {},
|
|
16
|
+
> extends DebouncerOptions<TFn> {
|
|
17
|
+
/**
|
|
18
|
+
* Custom unmount behavior. Defaults to cancelling any pending execution.
|
|
19
|
+
*/
|
|
20
|
+
onUnmount?: (debouncer: ReactDebouncer<TFn, TSelected>) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ReactDebouncer<TFn extends AnyFunction, TSelected = {}> extends Omit<
|
|
24
|
+
Debouncer<TFn>,
|
|
25
|
+
'store'
|
|
26
|
+
> {
|
|
27
|
+
/**
|
|
28
|
+
* Render-prop component subscribing to a slice of the debouncer's state.
|
|
29
|
+
*/
|
|
30
|
+
Subscribe: <TSelected>(props: {
|
|
31
|
+
selector: (state: DebouncerState<TFn>) => 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<DebouncerState<TFn>>>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* An Octane hook that creates and manages a Debouncer instance.
|
|
46
|
+
*
|
|
47
|
+
* The debouncer delays function execution until after a specified wait time
|
|
48
|
+
* has elapsed since the last call.
|
|
49
|
+
*
|
|
50
|
+
* ## State Management and Selector
|
|
51
|
+
*
|
|
52
|
+
* Uses TanStack Store for reactive state management. **By default there are no
|
|
53
|
+
* reactive state subscriptions** — opt in by providing a `selector`; only then
|
|
54
|
+
* does the component re-render when the selected state values change.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* // Opt-in to re-render when isPending changes
|
|
59
|
+
* const debouncer = useDebouncer(setValue, { wait: 500 }, (state) => ({
|
|
60
|
+
* isPending: state.isPending,
|
|
61
|
+
* }));
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function useDebouncer<TFn extends AnyFunction, TSelected = {}>(
|
|
65
|
+
fn: TFn,
|
|
66
|
+
options: ReactDebouncerOptions<TFn, TSelected>,
|
|
67
|
+
selector: (state: DebouncerState<TFn>) => TSelected = () => ({}) as TSelected,
|
|
68
|
+
): ReactDebouncer<TFn, TSelected> {
|
|
69
|
+
const mergedOptions = {
|
|
70
|
+
...useDefaultPacerOptions().debouncer,
|
|
71
|
+
...options,
|
|
72
|
+
} as ReactDebouncerOptions<TFn, TSelected>;
|
|
73
|
+
const [debouncer] = useState(() => {
|
|
74
|
+
const debouncerInstance = new Debouncer(fn, mergedOptions) as unknown as ReactDebouncer<
|
|
75
|
+
TFn,
|
|
76
|
+
TSelected
|
|
77
|
+
>;
|
|
78
|
+
|
|
79
|
+
debouncerInstance.Subscribe = function Subscribe<TSelected>(props: {
|
|
80
|
+
selector: (state: DebouncerState<TFn>) => TSelected;
|
|
81
|
+
children: ((state: TSelected) => unknown) | unknown;
|
|
82
|
+
}) {
|
|
83
|
+
const selected = useSelectorSlot(
|
|
84
|
+
debouncerInstance.store,
|
|
85
|
+
props.selector,
|
|
86
|
+
{ compare: shallow },
|
|
87
|
+
subscribeSlot,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return typeof props.children === 'function'
|
|
91
|
+
? (props.children as (state: TSelected) => unknown)(selected)
|
|
92
|
+
: props.children;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return debouncerInstance;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
debouncer.fn = fn;
|
|
99
|
+
debouncer.setOptions(mergedOptions);
|
|
100
|
+
|
|
101
|
+
// Unmount cleanup only; empty deps keep teardown stable (as upstream).
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
return () => {
|
|
104
|
+
if (mergedOptions.onUnmount) {
|
|
105
|
+
mergedOptions.onUnmount(debouncer);
|
|
106
|
+
} else {
|
|
107
|
+
debouncer.cancel();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const state = useSelectorSlot(debouncer.store, selector, { compare: shallow }, stateSlot);
|
|
113
|
+
|
|
114
|
+
return useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
({
|
|
117
|
+
...debouncer,
|
|
118
|
+
state,
|
|
119
|
+
}) as ReactDebouncer<TFn, TSelected>, // omit `store` in favor of `state`
|
|
120
|
+
[debouncer, state],
|
|
121
|
+
);
|
|
122
|
+
}
|