@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # @octanejs/tanstack-pacer
2
+
3
+ Octane port of `@tanstack/react-pacer` — debouncing, throttling, rate
4
+ limiting, queuing, batching, and their async variants. Re-exports the
5
+ framework-agnostic `@tanstack/pacer` core unchanged and implements every
6
+ upstream hook family (`useDebouncer`/`useDebouncedState`/`useDebouncedValue`/
7
+ `useDebouncedCallback`, throttler, rate-limiter, queuer, batcher, and the
8
+ async families) plus `PacerProvider` on Octane hooks, with store
9
+ subscriptions via `@octanejs/tanstack-store`.
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@octanejs/tanstack-pacer",
3
+ "version": "0.0.5",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "description": "TanStack Pacer bindings for Octane — debouncing, throttling, rate limiting, queuing, and batching hooks over the framework-agnostic @tanstack/pacer core.",
10
+ "author": {
11
+ "name": "Dominic Gannaway",
12
+ "email": "dg@domgan.com"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/octanejs/octane.git",
20
+ "directory": "packages/tanstack-pacer"
21
+ },
22
+ "main": "src/index.ts",
23
+ "module": "src/index.ts",
24
+ "types": "src/index.ts",
25
+ "files": [
26
+ "src",
27
+ "README.md"
28
+ ],
29
+ "exports": {
30
+ ".": "./src/index.ts",
31
+ "./async-batcher": "./src/async-batcher/index.ts",
32
+ "./async-debouncer": "./src/async-debouncer/index.ts",
33
+ "./async-queuer": "./src/async-queuer/index.ts",
34
+ "./async-rate-limiter": "./src/async-rate-limiter/index.ts",
35
+ "./async-retryer": "./src/async-retryer/index.ts",
36
+ "./async-throttler": "./src/async-throttler/index.ts",
37
+ "./batcher": "./src/batcher/index.ts",
38
+ "./debouncer": "./src/debouncer/index.ts",
39
+ "./provider": "./src/provider/index.ts",
40
+ "./queuer": "./src/queuer/index.ts",
41
+ "./rate-limiter": "./src/rate-limiter/index.ts",
42
+ "./throttler": "./src/throttler/index.ts",
43
+ "./types": "./src/types/index.ts",
44
+ "./utils": "./src/utils/index.ts"
45
+ },
46
+ "dependencies": {
47
+ "@tanstack/pacer": "0.21.1"
48
+ },
49
+ "peerDependencies": {
50
+ "@octanejs/tanstack-store": "0.0.10",
51
+ "octane": "0.1.16"
52
+ },
53
+ "devDependencies": {
54
+ "vitest": "^4.1.10",
55
+ "octane": "0.1.16",
56
+ "@octanejs/tanstack-store": "0.0.10"
57
+ },
58
+ "scripts": {
59
+ "test": "vitest run --project tanstack-pacer",
60
+ "typecheck": "tsrx-tsc --noEmit"
61
+ }
62
+ }
@@ -0,0 +1,5 @@
1
+ // re-export everything from the core pacer package, BUT ONLY from the async-batcher module
2
+ export * from '@tanstack/pacer/async-batcher';
3
+
4
+ export * from './useAsyncBatcher';
5
+ export * from './useAsyncBatchedCallback';
@@ -0,0 +1,20 @@
1
+ import { useCallback } from 'octane';
2
+ import { useAsyncBatcher } from './useAsyncBatcher';
3
+ import type { ReactAsyncBatcherOptions } from './useAsyncBatcher';
4
+
5
+ /**
6
+ * An Octane hook that creates a batched version of an async callback
7
+ * function. Calling the returned function adds the item to the batch.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * const addToBatch = useAsyncBatchedCallback(processBatch, { maxSize: 10 });
12
+ * ```
13
+ */
14
+ export function useAsyncBatchedCallback<TValue>(
15
+ fn: (items: Array<TValue>) => Promise<unknown>,
16
+ options: ReactAsyncBatcherOptions<TValue, {}>,
17
+ ): (item: TValue) => Promise<void> {
18
+ const asyncBatchedFn = useAsyncBatcher(fn, options).addItem;
19
+ return useCallback((item: TValue) => asyncBatchedFn(item), [asyncBatchedFn]);
20
+ }
@@ -0,0 +1,116 @@
1
+ import { useEffect, useMemo, useState } from 'octane';
2
+ import { AsyncBatcher } from '@tanstack/pacer/async-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 { AsyncBatcherOptions, AsyncBatcherState } from '@tanstack/pacer/async-batcher';
8
+
9
+ const subscribeSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncBatcher:Subscribe');
10
+ const stateSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncBatcher:state');
11
+
12
+ export interface ReactAsyncBatcherOptions<
13
+ TValue,
14
+ TSelected = {},
15
+ > extends AsyncBatcherOptions<TValue> {
16
+ /**
17
+ * Custom unmount behavior. Defaults to cancelling any pending batch and
18
+ * aborting in-flight executions.
19
+ */
20
+ onUnmount?: (batcher: ReactAsyncBatcher<TValue, TSelected>) => void;
21
+ }
22
+
23
+ export interface ReactAsyncBatcher<TValue, TSelected = {}> extends Omit<
24
+ AsyncBatcher<TValue>,
25
+ 'store'
26
+ > {
27
+ /**
28
+ * Render-prop component subscribing to a slice of the batcher's state.
29
+ */
30
+ Subscribe: <TSelected>(props: {
31
+ selector: (state: AsyncBatcherState<TValue>) => 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<AsyncBatcherState<TValue>>>;
42
+ }
43
+
44
+ /**
45
+ * An Octane hook that creates and manages an AsyncBatcher instance — collects
46
+ * items and processes them together through an async function.
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 batcher = useAsyncBatcher(processBatch, { maxSize: 10, wait: 2000 });
55
+ * ```
56
+ */
57
+ export function useAsyncBatcher<TValue, TSelected = {}>(
58
+ fn: (items: Array<TValue>) => Promise<any>,
59
+ options: ReactAsyncBatcherOptions<TValue, TSelected> = {},
60
+ selector: (state: AsyncBatcherState<TValue>) => TSelected = () => ({}) as TSelected,
61
+ ): ReactAsyncBatcher<TValue, TSelected> {
62
+ const mergedOptions = {
63
+ ...useDefaultPacerOptions().asyncBatcher,
64
+ ...options,
65
+ } as ReactAsyncBatcherOptions<TValue, TSelected>;
66
+ const [asyncBatcher] = useState(() => {
67
+ const asyncBatcherInstance = new AsyncBatcher<TValue>(
68
+ fn,
69
+ mergedOptions,
70
+ ) as unknown as ReactAsyncBatcher<TValue, TSelected>;
71
+
72
+ asyncBatcherInstance.Subscribe = function Subscribe<TSelected>(props: {
73
+ selector: (state: AsyncBatcherState<TValue>) => TSelected;
74
+ children: ((state: TSelected) => unknown) | unknown;
75
+ }) {
76
+ const selected = useSelectorSlot(
77
+ asyncBatcherInstance.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 asyncBatcherInstance;
89
+ });
90
+
91
+ asyncBatcher.fn = fn;
92
+ asyncBatcher.setOptions(mergedOptions);
93
+
94
+ // Unmount cleanup only; empty deps keep teardown stable (as upstream).
95
+ useEffect(() => {
96
+ return () => {
97
+ if (mergedOptions.onUnmount) {
98
+ mergedOptions.onUnmount(asyncBatcher);
99
+ } else {
100
+ asyncBatcher.cancel();
101
+ asyncBatcher.abort();
102
+ }
103
+ };
104
+ }, []);
105
+
106
+ const state = useSelectorSlot(asyncBatcher.store, selector, { compare: shallow }, stateSlot);
107
+
108
+ return useMemo(
109
+ () =>
110
+ ({
111
+ ...asyncBatcher,
112
+ state,
113
+ }) as ReactAsyncBatcher<TValue, TSelected>, // omit `store` in favor of `state`
114
+ [asyncBatcher, state],
115
+ );
116
+ }
@@ -0,0 +1,5 @@
1
+ // re-export everything from the core pacer package, BUT ONLY from the async-debouncer module
2
+ export * from '@tanstack/pacer/async-debouncer';
3
+
4
+ export * from './useAsyncDebouncer';
5
+ export * from './useAsyncDebouncedCallback';
@@ -0,0 +1,25 @@
1
+ import { useCallback } from 'octane';
2
+ import { useAsyncDebouncer } from './useAsyncDebouncer';
3
+ import type { ReactAsyncDebouncerOptions } from './useAsyncDebouncer';
4
+ import type { AnyAsyncFunction } from '@tanstack/pacer/types';
5
+
6
+ /**
7
+ * An Octane hook that creates a debounced version of an async callback
8
+ * function. The returned function resolves with the callback's result once
9
+ * the debounced execution runs.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const debouncedSearch = useAsyncDebouncedCallback(searchApi, { wait: 500 });
14
+ * ```
15
+ */
16
+ export function useAsyncDebouncedCallback<TFn extends AnyAsyncFunction>(
17
+ fn: TFn,
18
+ options: ReactAsyncDebouncerOptions<TFn, {}>,
19
+ ): (...args: Parameters<TFn>) => Promise<ReturnType<TFn>> {
20
+ const asyncDebouncedFn = useAsyncDebouncer(fn, options).maybeExecute;
21
+ return useCallback(
22
+ (...args) => asyncDebouncedFn(...args) as Promise<ReturnType<TFn>>,
23
+ [asyncDebouncedFn],
24
+ );
25
+ }
@@ -0,0 +1,123 @@
1
+ import { useEffect, useMemo, useState } from 'octane';
2
+ import { AsyncDebouncer } from '@tanstack/pacer/async-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 { AsyncDebouncerOptions, AsyncDebouncerState } from '@tanstack/pacer/async-debouncer';
8
+ import type { AnyAsyncFunction } from '@tanstack/pacer/types';
9
+
10
+ const subscribeSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncDebouncer:Subscribe');
11
+ const stateSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncDebouncer:state');
12
+
13
+ export interface ReactAsyncDebouncerOptions<
14
+ TFn extends AnyAsyncFunction,
15
+ TSelected = {},
16
+ > extends AsyncDebouncerOptions<TFn> {
17
+ /**
18
+ * Custom unmount behavior. Defaults to cancelling pending executions and
19
+ * aborting in-flight ones.
20
+ */
21
+ onUnmount?: (debouncer: ReactAsyncDebouncer<TFn, TSelected>) => void;
22
+ }
23
+
24
+ export interface ReactAsyncDebouncer<TFn extends AnyAsyncFunction, TSelected = {}> extends Omit<
25
+ AsyncDebouncer<TFn>,
26
+ 'store'
27
+ > {
28
+ /**
29
+ * Render-prop component subscribing to a slice of the debouncer's state.
30
+ */
31
+ Subscribe: <TSelected>(props: {
32
+ selector: (state: AsyncDebouncerState<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<AsyncDebouncerState<TFn>>>;
43
+ }
44
+
45
+ /**
46
+ * An Octane hook that creates and manages an AsyncDebouncer instance.
47
+ *
48
+ * Like the sync debouncer but for async functions: adds success/error/settled
49
+ * lifecycle callbacks, abort support, and execution tracking.
50
+ *
51
+ * **By default there are no reactive state subscriptions** — opt in by
52
+ * providing a `selector`; only then does the component re-render when the
53
+ * selected state values change.
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * const debouncer = useAsyncDebouncer(
58
+ * async (query: string) => fetchResults(query),
59
+ * { wait: 500 },
60
+ * (state) => ({ isExecuting: state.isExecuting }),
61
+ * );
62
+ * ```
63
+ */
64
+ export function useAsyncDebouncer<TFn extends AnyAsyncFunction, TSelected = {}>(
65
+ fn: TFn,
66
+ options: ReactAsyncDebouncerOptions<TFn, TSelected>,
67
+ selector: (state: AsyncDebouncerState<TFn>) => TSelected = () => ({}) as TSelected,
68
+ ): ReactAsyncDebouncer<TFn, TSelected> {
69
+ const mergedOptions = {
70
+ ...useDefaultPacerOptions().asyncDebouncer,
71
+ ...options,
72
+ } as ReactAsyncDebouncerOptions<TFn, TSelected>;
73
+ const [asyncDebouncer] = useState(() => {
74
+ const asyncDebouncerInstance = new AsyncDebouncer<TFn>(
75
+ fn,
76
+ mergedOptions,
77
+ ) as unknown as ReactAsyncDebouncer<TFn, TSelected>;
78
+
79
+ asyncDebouncerInstance.Subscribe = function Subscribe<TSelected>(props: {
80
+ selector: (state: AsyncDebouncerState<TFn>) => TSelected;
81
+ children: ((state: TSelected) => unknown) | unknown;
82
+ }) {
83
+ const selected = useSelectorSlot(
84
+ asyncDebouncerInstance.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 asyncDebouncerInstance;
96
+ });
97
+
98
+ asyncDebouncer.fn = fn;
99
+ asyncDebouncer.setOptions(mergedOptions);
100
+
101
+ const state = useSelectorSlot(asyncDebouncer.store, selector, { compare: shallow }, stateSlot);
102
+
103
+ // Unmount cleanup only; empty deps keep teardown stable (as upstream).
104
+ useEffect(() => {
105
+ return () => {
106
+ if (mergedOptions.onUnmount) {
107
+ mergedOptions.onUnmount(asyncDebouncer);
108
+ } else {
109
+ asyncDebouncer.cancel();
110
+ asyncDebouncer.abort();
111
+ }
112
+ };
113
+ }, []);
114
+
115
+ return useMemo(
116
+ () =>
117
+ ({
118
+ ...asyncDebouncer,
119
+ state,
120
+ }) as ReactAsyncDebouncer<TFn, TSelected>, // omit `store` in favor of `state`
121
+ [asyncDebouncer, state],
122
+ );
123
+ }
@@ -0,0 +1,5 @@
1
+ // re-export everything from the core pacer package, BUT ONLY from the async-queuer module
2
+ export * from '@tanstack/pacer/async-queuer';
3
+
4
+ export * from './useAsyncQueuer';
5
+ export * from './useAsyncQueuedState';
@@ -0,0 +1,30 @@
1
+ import { useAsyncQueuer } from './useAsyncQueuer';
2
+ import type { ReactAsyncQueuer, ReactAsyncQueuerOptions } from './useAsyncQueuer';
3
+ import type { AsyncQueuerState } from '@tanstack/pacer/async-queuer';
4
+
5
+ /**
6
+ * An Octane hook that creates an async queue with managed state. Returns a
7
+ * tuple of the current queue items 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, queuer] = useAsyncQueuedState(processItem, { concurrency: 2 });
15
+ * ```
16
+ */
17
+ export function useAsyncQueuedState<
18
+ TValue,
19
+ TSelected extends Pick<AsyncQueuerState<TValue>, 'items'> = Pick<
20
+ AsyncQueuerState<TValue>,
21
+ 'items'
22
+ >,
23
+ >(
24
+ fn: (value: TValue) => Promise<any>,
25
+ options: ReactAsyncQueuerOptions<TValue, TSelected> = {},
26
+ selector?: (state: AsyncQueuerState<TValue>) => TSelected,
27
+ ): [Array<TValue>, ReactAsyncQueuer<TValue, TSelected>] {
28
+ const asyncQueuer = useAsyncQueuer(fn, options, selector);
29
+ return [asyncQueuer.state.items, asyncQueuer];
30
+ }
@@ -0,0 +1,116 @@
1
+ import { useEffect, useMemo, useState } from 'octane';
2
+ import { AsyncQueuer } from '@tanstack/pacer/async-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 { AsyncQueuerOptions, AsyncQueuerState } from '@tanstack/pacer/async-queuer';
8
+
9
+ const subscribeSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncQueuer:Subscribe');
10
+ const stateSlot = Symbol.for('@octanejs/tanstack-pacer:useAsyncQueuer:state');
11
+
12
+ export interface ReactAsyncQueuerOptions<
13
+ TValue,
14
+ TSelected = {},
15
+ > extends AsyncQueuerOptions<TValue> {
16
+ /**
17
+ * Custom unmount behavior. Defaults to stopping the queue and aborting
18
+ * in-flight executions.
19
+ */
20
+ onUnmount?: (queuer: ReactAsyncQueuer<TValue, TSelected>) => void;
21
+ }
22
+
23
+ export interface ReactAsyncQueuer<TValue, TSelected = {}> extends Omit<
24
+ AsyncQueuer<TValue>,
25
+ 'store'
26
+ > {
27
+ /**
28
+ * Render-prop component subscribing to a slice of the queuer's state.
29
+ */
30
+ Subscribe: <TSelected>(props: {
31
+ selector: (state: AsyncQueuerState<TValue>) => 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<AsyncQueuerState<TValue>>>;
42
+ }
43
+
44
+ /**
45
+ * An Octane hook that creates and manages an AsyncQueuer instance — processes
46
+ * queued items through an async function with configurable concurrency.
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 queuer = useAsyncQueuer(processItem, { concurrency: 2 });
55
+ * ```
56
+ */
57
+ export function useAsyncQueuer<TValue, TSelected = {}>(
58
+ fn: (value: TValue) => Promise<any>,
59
+ options: ReactAsyncQueuerOptions<TValue, TSelected> = {},
60
+ selector: (state: AsyncQueuerState<TValue>) => TSelected = () => ({}) as TSelected,
61
+ ): ReactAsyncQueuer<TValue, TSelected> {
62
+ const mergedOptions = {
63
+ ...useDefaultPacerOptions().asyncQueuer,
64
+ ...options,
65
+ } as ReactAsyncQueuerOptions<TValue, TSelected>;
66
+ const [asyncQueuer] = useState(() => {
67
+ const asyncQueuerInstance = new AsyncQueuer<TValue>(
68
+ fn,
69
+ mergedOptions,
70
+ ) as unknown as ReactAsyncQueuer<TValue, TSelected>;
71
+
72
+ asyncQueuerInstance.Subscribe = function Subscribe<TSelected>(props: {
73
+ selector: (state: AsyncQueuerState<TValue>) => TSelected;
74
+ children: ((state: TSelected) => unknown) | unknown;
75
+ }) {
76
+ const selected = useSelectorSlot(
77
+ asyncQueuerInstance.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 asyncQueuerInstance;
89
+ });
90
+
91
+ asyncQueuer.fn = fn;
92
+ asyncQueuer.setOptions(mergedOptions);
93
+
94
+ // Unmount cleanup only; empty deps keep teardown stable (as upstream).
95
+ useEffect(() => {
96
+ return () => {
97
+ if (mergedOptions.onUnmount) {
98
+ mergedOptions.onUnmount(asyncQueuer);
99
+ } else {
100
+ asyncQueuer.stop();
101
+ asyncQueuer.abort();
102
+ }
103
+ };
104
+ }, []);
105
+
106
+ const state = useSelectorSlot(asyncQueuer.store, selector, { compare: shallow }, stateSlot);
107
+
108
+ return useMemo(
109
+ () =>
110
+ ({
111
+ ...asyncQueuer,
112
+ state,
113
+ }) as ReactAsyncQueuer<TValue, TSelected>, // omit `store` in favor of `state`
114
+ [asyncQueuer, state],
115
+ );
116
+ }
@@ -0,0 +1,5 @@
1
+ // re-export everything from the core pacer package, BUT ONLY from the async-rate-limiter module
2
+ export * from '@tanstack/pacer/async-rate-limiter';
3
+
4
+ export * from './useAsyncRateLimiter';
5
+ export * from './useAsyncRateLimitedCallback';
@@ -0,0 +1,27 @@
1
+ import { useCallback } from 'octane';
2
+ import { useAsyncRateLimiter } from './useAsyncRateLimiter';
3
+ import type { ReactAsyncRateLimiterOptions } from './useAsyncRateLimiter';
4
+ import type { AnyAsyncFunction } from '@tanstack/pacer/types';
5
+
6
+ /**
7
+ * An Octane hook that creates a rate-limited version of an async callback
8
+ * function.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * const rateLimitedFetch = useAsyncRateLimitedCallback(fetchData, {
13
+ * limit: 5,
14
+ * window: 60000,
15
+ * });
16
+ * ```
17
+ */
18
+ export function useAsyncRateLimitedCallback<TFn extends AnyAsyncFunction>(
19
+ fn: TFn,
20
+ options: ReactAsyncRateLimiterOptions<TFn, {}>,
21
+ ): (...args: Parameters<TFn>) => Promise<ReturnType<TFn>> {
22
+ const asyncRateLimitedFn = useAsyncRateLimiter(fn, options).maybeExecute;
23
+ return useCallback(
24
+ (...args) => asyncRateLimitedFn(...args) as Promise<ReturnType<TFn>>,
25
+ [asyncRateLimitedFn],
26
+ );
27
+ }