diff --git a/.changeset/add-deduplication-feature.md b/.changeset/add-deduplication-feature.md new file mode 100644 index 000000000..c611e4c5f --- /dev/null +++ b/.changeset/add-deduplication-feature.md @@ -0,0 +1,44 @@ +--- +"@tanstack/pacer": minor +--- + +Add in-batch/in-queue deduplication support to Batcher and Queuer + +This feature adds `deduplicateItems` option to prevent duplicate items within the same batch or queue. + +### New Options + +- `deduplicateItems: boolean` - Enable automatic deduplication within the current batch/queue (default: false) +- `deduplicateStrategy: 'keep-first' | 'keep-last'` - Strategy for handling duplicates (default: 'keep-first') +- `getItemKey: (item) => string | number` - Extract unique key from item (defaults to JSON.stringify for objects) + +### Behavior + +When `deduplicateItems` is enabled: +- **'keep-first'**: Ignores new items if an item with the same key already exists in the batch/queue +- **'keep-last'**: Replaces existing items with new items that have the same key + +### Use Cases + +Prevents redundant items within a single batch or queue cycle: +- API batching: Avoid duplicate IDs in the same batch request +- Event processing: Deduplicate events before processing + +### Example + +```typescript +const batcher = new Batcher<{ userId: string }>( + (items) => fetchUsers(items.map(i => i.userId)), + { + deduplicateItems: true, + getItemKey: (item) => item.userId, + } +); + +batcher.addItem({ userId: 'user-1' }); // Added to batch +batcher.addItem({ userId: 'user-2' }); // Added to batch +batcher.addItem({ userId: 'user-1' }); // Ignored! Already in current batch +batcher.flush(); // Processes [user-1, user-2] +``` + +Fully opt-in with no breaking changes to existing behavior. diff --git a/docs/reference/classes/Batcher.md b/docs/reference/classes/Batcher.md index ceda95922..65cf5f755 100644 --- a/docs/reference/classes/Batcher.md +++ b/docs/reference/classes/Batcher.md @@ -5,7 +5,7 @@ title: Batcher # Class: Batcher\ -Defined in: [batcher.ts:145](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L145) +Defined in: [batcher.ts:217](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L217) A class that collects items and processes them in batches. @@ -17,6 +17,7 @@ The Batcher provides a flexible way to implement batching with configurable: - Time-based batching (process after X milliseconds) - Custom batch processing logic via getShouldExecute - Event callbacks for monitoring batch operations +- Cross-batch deduplication via deduplicateItems (similar to RateLimiter's executionTimes) State Management: - Uses TanStack Store for reactive state management @@ -27,7 +28,7 @@ State Management: - State can be accessed via `batcher.store.state` when using the class directly - When using framework adapters (React/Solid), state is accessed from `batcher.state` -## Example +## Examples ```ts const batcher = new Batcher( @@ -46,6 +47,26 @@ batcher.addItem(2); // batcher.flush() // manually trigger a batch ``` +```ts +// Cross-batch deduplication - prevent duplicate API calls +const batcher = new Batcher<{ userId: string }>( + (items) => fetchUsers(items.map(i => i.userId)), + { + deduplicateItems: true, + getItemKey: (item) => item.userId, + maxTrackedKeys: 500, // Limit memory usage + onDuplicate: (item) => console.log('Already fetched:', item.userId) + } +); + +batcher.addItem({ userId: 'user-1' }); // Added to batch +batcher.addItem({ userId: 'user-2' }); // Added to batch +batcher.flush(); // Processes [user-1, user-2] + +batcher.addItem({ userId: 'user-1' }); // Skipped! Already processed +batcher.addItem({ userId: 'user-3' }); // Added to batch +``` + ## Type Parameters ### TValue @@ -60,7 +81,7 @@ batcher.addItem(2); new Batcher(fn, initialOptions): Batcher; ``` -Defined in: [batcher.ts:153](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L153) +Defined in: [batcher.ts:225](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L225) #### Parameters @@ -84,7 +105,7 @@ Defined in: [batcher.ts:153](https://github.com/TanStack/pacer/blob/main/package fn: (items) => void; ``` -Defined in: [batcher.ts:154](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L154) +Defined in: [batcher.ts:226](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L226) #### Parameters @@ -104,7 +125,7 @@ Defined in: [batcher.ts:154](https://github.com/TanStack/pacer/blob/main/package key: string | undefined; ``` -Defined in: [batcher.ts:149](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L149) +Defined in: [batcher.ts:221](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L221) *** @@ -114,7 +135,7 @@ Defined in: [batcher.ts:149](https://github.com/TanStack/pacer/blob/main/package options: BatcherOptionsWithOptionalCallbacks; ``` -Defined in: [batcher.ts:150](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L150) +Defined in: [batcher.ts:222](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L222) *** @@ -124,20 +145,21 @@ Defined in: [batcher.ts:150](https://github.com/TanStack/pacer/blob/main/package readonly store: Store>>; ``` -Defined in: [batcher.ts:146](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L146) +Defined in: [batcher.ts:218](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L218) ## Methods ### addItem() ```ts -addItem(item): void; +addItem(item): boolean; ``` -Defined in: [batcher.ts:207](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L207) +Defined in: [batcher.ts:311](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L311) Adds an item to the batcher If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed +When deduplicateItems is enabled, items that have already been processed will be skipped #### Parameters @@ -147,7 +169,7 @@ If the batch size is reached, timeout occurs, or shouldProcess returns true, the #### Returns -`void` +`boolean` *** @@ -157,7 +179,7 @@ If the batch size is reached, timeout occurs, or shouldProcess returns true, the cancel(): void; ``` -Defined in: [batcher.ts:285](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L285) +Defined in: [batcher.ts:449](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L449) Cancels any pending execution that was scheduled. Does NOT clear out the items. @@ -174,7 +196,7 @@ Does NOT clear out the items. clear(): void; ``` -Defined in: [batcher.ts:277](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L277) +Defined in: [batcher.ts:441](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L441) Removes all items from the batcher @@ -184,13 +206,30 @@ Removes all items from the batcher *** +### clearProcessedKeys() + +```ts +clearProcessedKeys(): void; +``` + +Defined in: [batcher.ts:427](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L427) + +Clears all processed keys, allowing items with those keys to be processed again +Only meaningful when deduplicateItems is enabled + +#### Returns + +`void` + +*** + ### flush() ```ts flush(): void; ``` -Defined in: [batcher.ts:255](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L255) +Defined in: [batcher.ts:395](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L395) Processes the current batch of items immediately @@ -200,13 +239,36 @@ Processes the current batch of items immediately *** +### hasProcessedKey() + +```ts +hasProcessedKey(key): boolean; +``` + +Defined in: [batcher.ts:419](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L419) + +Checks if a key has already been processed +Only meaningful when deduplicateItems is enabled + +#### Parameters + +##### key + +`string` | `number` + +#### Returns + +`boolean` + +*** + ### peekAllItems() ```ts peekAllItems(): TValue[]; ``` -Defined in: [batcher.ts:263](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L263) +Defined in: [batcher.ts:403](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L403) Returns a copy of all items in the batcher @@ -216,15 +278,33 @@ Returns a copy of all items in the batcher *** +### peekProcessedKeys() + +```ts +peekProcessedKeys(): (string | number)[]; +``` + +Defined in: [batcher.ts:411](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L411) + +Returns a copy of all processed keys +Only meaningful when deduplicateItems is enabled + +#### Returns + +(`string` \| `number`)[] + +*** + ### reset() ```ts reset(): void; ``` -Defined in: [batcher.ts:293](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L293) +Defined in: [batcher.ts:458](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L458) Resets the batcher state to its default values +This also clears the processed keys history #### Returns @@ -238,7 +318,7 @@ Resets the batcher state to its default values setOptions(newOptions): void; ``` -Defined in: [batcher.ts:176](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L176) +Defined in: [batcher.ts:248](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L248) Updates the batcher options diff --git a/docs/reference/classes/Queuer.md b/docs/reference/classes/Queuer.md index 3cfced503..718082297 100644 --- a/docs/reference/classes/Queuer.md +++ b/docs/reference/classes/Queuer.md @@ -5,7 +5,7 @@ title: Queuer # Class: Queuer\ -Defined in: [queuer.ts:269](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L269) +Defined in: [queuer.ts:337](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L337) A flexible queue that processes items with configurable wait times, expiration, and priority. @@ -17,6 +17,7 @@ Features: - Priority-based ordering when getPriority is provided - Item expiration and removal of stale items - Callbacks for queue state changes, execution, rejection, and expiration +- Cross-execution deduplication via deduplicateItems (similar to RateLimiter's executionTimes) Running behavior: - `start()`: Begins automatically processing items in the queue (defaults to isRunning) @@ -80,6 +81,27 @@ manualQueue.execute(); // logs 1, queue is [2] manualQueue.getNextItem(); // returns 2, queue is empty ``` +## Example + +```ts +// Cross-execution deduplication - prevent duplicate processing +const queuer = new Queuer<{ userId: string }>( + (item) => fetchUser(item.userId), + { + deduplicateItems: true, + getItemKey: (item) => item.userId, + maxTrackedKeys: 500, // Limit memory usage + onDuplicate: (item) => console.log('Already processed:', item.userId) + } +); + +queuer.addItem({ userId: 'user-1' }); // Added and processed +queuer.addItem({ userId: 'user-2' }); // Added and processed + +queuer.addItem({ userId: 'user-1' }); // Skipped! Already processed +queuer.addItem({ userId: 'user-3' }); // Added and processed +``` + ## Type Parameters ### TValue @@ -94,7 +116,7 @@ manualQueue.getNextItem(); // returns 2, queue is empty new Queuer(fn, initialOptions): Queuer; ``` -Defined in: [queuer.ts:277](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L277) +Defined in: [queuer.ts:345](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L345) #### Parameters @@ -118,7 +140,7 @@ Defined in: [queuer.ts:277](https://github.com/TanStack/pacer/blob/main/packages fn: (item) => void; ``` -Defined in: [queuer.ts:278](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L278) +Defined in: [queuer.ts:346](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L346) #### Parameters @@ -138,7 +160,7 @@ Defined in: [queuer.ts:278](https://github.com/TanStack/pacer/blob/main/packages key: string | undefined; ``` -Defined in: [queuer.ts:273](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L273) +Defined in: [queuer.ts:341](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L341) *** @@ -148,7 +170,7 @@ Defined in: [queuer.ts:273](https://github.com/TanStack/pacer/blob/main/packages options: QueuerOptions; ``` -Defined in: [queuer.ts:274](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L274) +Defined in: [queuer.ts:342](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L342) *** @@ -158,7 +180,7 @@ Defined in: [queuer.ts:274](https://github.com/TanStack/pacer/blob/main/packages readonly store: Store>>; ``` -Defined in: [queuer.ts:270](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L270) +Defined in: [queuer.ts:338](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L338) ## Methods @@ -171,12 +193,13 @@ addItem( runOnItemsChange): boolean; ``` -Defined in: [queuer.ts:401](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L401) +Defined in: [queuer.ts:499](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L499) Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. Items can be inserted based on priority or at the front/back depending on configuration. +When deduplicateItems is enabled, items that have already been processed will be skipped. -Returns true if the item was added, false if the queue is full. +Returns true if the item was added, false if the queue is full or item was skipped. Example usage: ```ts @@ -210,7 +233,7 @@ queuer.addItem('task2', 'front'); clear(): void; ``` -Defined in: [queuer.ts:683](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L683) +Defined in: [queuer.ts:841](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L841) Removes all pending items from the queue. Does not affect items being processed. @@ -220,13 +243,30 @@ Removes all pending items from the queue. Does not affect items being processed. *** +### clearProcessedKeys() + +```ts +clearProcessedKeys(): void; +``` + +Defined in: [queuer.ts:809](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L809) + +Clears all processed keys, allowing items with those keys to be processed again +Only meaningful when deduplicateItems is enabled + +#### Returns + +`void` + +*** + ### execute() ```ts execute(position?): TValue | undefined; ``` -Defined in: [queuer.ts:537](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L537) +Defined in: [queuer.ts:665](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L665) Removes and returns the next item from the queue and processes it using the provided function. @@ -255,7 +295,7 @@ queuer.execute('back'); flush(numberOfItems, position?): void; ``` -Defined in: [queuer.ts:553](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L553) +Defined in: [queuer.ts:687](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L687) Processes a specified number of items to execute immediately with no wait time If no numberOfItems is provided, all items will be processed @@ -282,7 +322,7 @@ If no numberOfItems is provided, all items will be processed flushAsBatch(batchFunction): void; ``` -Defined in: [queuer.ts:568](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L568) +Defined in: [queuer.ts:702](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L702) Processes all items in the queue as a batch using the provided function as an argument The queue is cleared after processing @@ -305,7 +345,7 @@ The queue is cleared after processing getNextItem(position): TValue | undefined; ``` -Defined in: [queuer.ts:485](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L485) +Defined in: [queuer.ts:613](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L613) Removes and returns the next item from the queue without executing the function. Use for manual queue management. Normally, use execute() to process items. @@ -330,13 +370,36 @@ queuer.getNextItem('back'); *** +### hasProcessedKey() + +```ts +hasProcessedKey(key): boolean; +``` + +Defined in: [queuer.ts:801](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L801) + +Checks if a key has already been processed +Only meaningful when deduplicateItems is enabled + +#### Parameters + +##### key + +`string` | `number` + +#### Returns + +`boolean` + +*** + ### peekAllItems() ```ts peekAllItems(): TValue[]; ``` -Defined in: [queuer.ts:651](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L651) +Defined in: [queuer.ts:785](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L785) Returns a copy of all items in the queue. @@ -352,7 +415,7 @@ Returns a copy of all items in the queue. peekNextItem(position): TValue | undefined; ``` -Defined in: [queuer.ts:641](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L641) +Defined in: [queuer.ts:775](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L775) Returns the next item in the queue without removing it. @@ -374,15 +437,33 @@ queuer.peekNextItem('back'); // back *** +### peekProcessedKeys() + +```ts +peekProcessedKeys(): (string | number)[]; +``` + +Defined in: [queuer.ts:793](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L793) + +Returns a copy of all processed keys +Only meaningful when deduplicateItems is enabled + +#### Returns + +(`string` \| `number`)[] + +*** + ### reset() ```ts reset(): void; ``` -Defined in: [queuer.ts:691](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L691) +Defined in: [queuer.ts:850](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L850) Resets the queuer state to its default values +This also clears the processed keys history #### Returns @@ -396,7 +477,7 @@ Resets the queuer state to its default values setOptions(newOptions): void; ``` -Defined in: [queuer.ts:317](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L317) +Defined in: [queuer.ts:385](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L385) Updates the queuer options. New options are merged with existing options. @@ -418,7 +499,7 @@ Updates the queuer options. New options are merged with existing options. start(): void; ``` -Defined in: [queuer.ts:658](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L658) +Defined in: [queuer.ts:816](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L816) Starts processing items in the queue. If already isRunning, does nothing. @@ -434,7 +515,7 @@ Starts processing items in the queue. If already isRunning, does nothing. stop(): void; ``` -Defined in: [queuer.ts:668](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L668) +Defined in: [queuer.ts:826](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L826) Stops processing items in the queue. Does not clear the queue. diff --git a/docs/reference/functions/batch.md b/docs/reference/functions/batch.md index 5200ddeea..78b543152 100644 --- a/docs/reference/functions/batch.md +++ b/docs/reference/functions/batch.md @@ -6,10 +6,10 @@ title: batch # Function: batch() ```ts -function batch(fn, options): (item) => void; +function batch(fn, options): (item) => boolean; ``` -Defined in: [batcher.ts:319](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L319) +Defined in: [batcher.ts:484](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L484) Creates a batcher that processes items in batches. @@ -34,11 +34,12 @@ This synchronous version is lighter weight and often all you need - upgrade to a ## Returns ```ts -(item): void; +(item): boolean; ``` Adds an item to the batcher If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed +When deduplicateItems is enabled, items that have already been processed will be skipped ### Parameters @@ -48,7 +49,7 @@ If the batch size is reached, timeout occurs, or shouldProcess returns true, the ### Returns -`void` +`boolean` ## Example diff --git a/docs/reference/functions/queue.md b/docs/reference/functions/queue.md index 5a9ab949e..0446f7bfb 100644 --- a/docs/reference/functions/queue.md +++ b/docs/reference/functions/queue.md @@ -9,7 +9,7 @@ title: queue function queue(fn, initialOptions): (item, position, runOnItemsChange) => boolean; ``` -Defined in: [queuer.ts:732](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L732) +Defined in: [queuer.ts:891](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L891) Creates a queue that processes items immediately upon addition. Items are processed sequentially in FIFO order by default. @@ -72,8 +72,9 @@ processPriority(3); // Processed before 1 Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. Items can be inserted based on priority or at the front/back depending on configuration. +When deduplicateItems is enabled, items that have already been processed will be skipped. -Returns true if the item was added, false if the queue is full. +Returns true if the item was added, false if the queue is full or item was skipped. Example usage: ```ts diff --git a/docs/reference/functions/queuerOptions.md b/docs/reference/functions/queuerOptions.md index d98efeb1a..30b71199e 100644 --- a/docs/reference/functions/queuerOptions.md +++ b/docs/reference/functions/queuerOptions.md @@ -9,7 +9,7 @@ title: queuerOptions function queuerOptions(options): TOptions; ``` -Defined in: [queuer.ts:157](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L157) +Defined in: [queuer.ts:199](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L199) Utility function for sharing common `QueuerOptions` options between different `Queuer` instances. diff --git a/docs/reference/interfaces/BatcherOptions.md b/docs/reference/interfaces/BatcherOptions.md index c55269f83..f2ed2b59c 100644 --- a/docs/reference/interfaces/BatcherOptions.md +++ b/docs/reference/interfaces/BatcherOptions.md @@ -5,7 +5,7 @@ title: BatcherOptions # Interface: BatcherOptions\ -Defined in: [batcher.ts:52](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L52) +Defined in: [batcher.ts:58](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L58) Options for configuring a Batcher instance @@ -17,13 +17,77 @@ Options for configuring a Batcher instance ## Properties +### deduplicateItems? + +```ts +optional deduplicateItems: boolean; +``` + +Defined in: [batcher.ts:65](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L65) + +Enable automatic deduplication of items across batches +When enabled, items that have already been processed will be automatically skipped +The keys of processed items are tracked in state.processedKeys + +#### Default + +```ts +false +``` + +*** + +### deduplicateStrategy? + +```ts +optional deduplicateStrategy: "keep-first" | "keep-last"; +``` + +Defined in: [batcher.ts:73](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L73) + +Strategy to use when a duplicate item is detected in the current batch +- 'keep-first': Keep the existing item and ignore the new one (default) +- 'keep-last': Replace the existing item with the new one +Note: This only affects duplicates within the same batch, not across batches + +#### Default + +```ts +'keep-first' +``` + +*** + +### getItemKey()? + +```ts +optional getItemKey: (item) => string | number; +``` + +Defined in: [batcher.ts:83](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L83) + +Function to extract a unique key from each item for deduplication +If not provided, uses the item itself for primitives or JSON.stringify for objects + +#### Parameters + +##### item + +`TValue` + +#### Returns + +`string` \| `number` + +*** + ### getShouldExecute()? ```ts optional getShouldExecute: (items, batcher) => boolean; ``` -Defined in: [batcher.ts:57](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L57) +Defined in: [batcher.ts:78](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L78) Custom function to determine if a batch should be processed Return true to process the batch immediately @@ -50,7 +114,7 @@ Return true to process the batch immediately optional initialState: Partial>; ``` -Defined in: [batcher.ts:61](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L61) +Defined in: [batcher.ts:87](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L87) Initial state for the batcher @@ -62,7 +126,7 @@ Initial state for the batcher optional key: string; ``` -Defined in: [batcher.ts:66](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L66) +Defined in: [batcher.ts:92](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L92) Optional key to identify this batcher instance. If provided, the batcher will be identified by this key in the devtools and PacerProvider if applicable. @@ -75,7 +139,7 @@ If provided, the batcher will be identified by this key in the devtools and Pace optional maxSize: number; ``` -Defined in: [batcher.ts:71](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L71) +Defined in: [batcher.ts:97](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L97) Maximum number of items in a batch @@ -87,13 +151,64 @@ Infinity *** +### maxTrackedKeys? + +```ts +optional maxTrackedKeys: number; +``` + +Defined in: [batcher.ts:104](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L104) + +Maximum number of processed keys to track (prevents memory leaks) +When limit is reached, oldest keys are removed (FIFO) +Only used when deduplicateItems is enabled + +#### Default + +```ts +1000 +``` + +*** + +### onDuplicate()? + +```ts +optional onDuplicate: (newItem, existingItem, batcher) => void; +``` + +Defined in: [batcher.ts:109](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L109) + +Callback fired when a duplicate item is detected +Called both for in-batch duplicates and cross-batch duplicates + +#### Parameters + +##### newItem + +`TValue` + +##### existingItem + +`TValue` | `undefined` + +##### batcher + +[`Batcher`](../classes/Batcher.md)\<`TValue`\> + +#### Returns + +`void` + +*** + ### onExecute()? ```ts optional onExecute: (batch, batcher) => void; ``` -Defined in: [batcher.ts:75](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L75) +Defined in: [batcher.ts:117](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L117) Callback fired after a batch is processed @@ -119,7 +234,7 @@ Callback fired after a batch is processed optional onItemsChange: (batcher) => void; ``` -Defined in: [batcher.ts:79](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L79) +Defined in: [batcher.ts:121](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L121) Callback fired after items are added to the batcher @@ -141,7 +256,7 @@ Callback fired after items are added to the batcher optional started: boolean; ``` -Defined in: [batcher.ts:84](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L84) +Defined in: [batcher.ts:126](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L126) Whether the batcher should start processing immediately @@ -159,7 +274,7 @@ true optional wait: number | (batcher) => number; ``` -Defined in: [batcher.ts:91](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L91) +Defined in: [batcher.ts:133](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L133) Maximum time in milliseconds to wait before processing a batch. If the wait duration has elapsed, the batch will be processed. diff --git a/docs/reference/interfaces/BatcherState.md b/docs/reference/interfaces/BatcherState.md index 495b01020..e4a3a3601 100644 --- a/docs/reference/interfaces/BatcherState.md +++ b/docs/reference/interfaces/BatcherState.md @@ -63,13 +63,26 @@ Array of items currently queued for batch processing *** +### processedKeys + +```ts +processedKeys: (string | number)[]; +``` + +Defined in: [batcher.ts:27](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L27) + +Array of keys that have been processed (for cross-batch deduplication) +Only populated when deduplicateItems is enabled + +*** + ### size ```ts size: number; ``` -Defined in: [batcher.ts:26](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L26) +Defined in: [batcher.ts:31](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L31) Number of items currently in the batch queue @@ -81,7 +94,7 @@ Number of items currently in the batch queue status: "idle" | "pending"; ``` -Defined in: [batcher.ts:30](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L30) +Defined in: [batcher.ts:35](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L35) Current processing status - 'idle' when not processing, 'pending' when waiting for timeout @@ -93,6 +106,6 @@ Current processing status - 'idle' when not processing, 'pending' when waiting f totalItemsProcessed: number; ``` -Defined in: [batcher.ts:34](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L34) +Defined in: [batcher.ts:39](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/batcher.ts#L39) Total number of items that have been processed across all batches diff --git a/docs/reference/interfaces/QueuerOptions.md b/docs/reference/interfaces/QueuerOptions.md index 6823799dd..d8cad535c 100644 --- a/docs/reference/interfaces/QueuerOptions.md +++ b/docs/reference/interfaces/QueuerOptions.md @@ -5,7 +5,7 @@ title: QueuerOptions # Interface: QueuerOptions\ -Defined in: [queuer.ts:83](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L83) +Defined in: [queuer.ts:89](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L89) Options for configuring a Queuer instance. @@ -25,7 +25,7 @@ These options control queue behavior, item expiration, callbacks, and more. optional addItemsTo: QueuePosition; ``` -Defined in: [queuer.ts:88](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L88) +Defined in: [queuer.ts:94](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L94) Default position to add items to the queuer @@ -37,13 +37,54 @@ Default position to add items to the queuer *** +### deduplicateItems? + +```ts +optional deduplicateItems: boolean; +``` + +Defined in: [queuer.ts:101](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L101) + +Enable automatic deduplication of items across queue cycles +When enabled, items that have already been processed will be automatically skipped +The keys of processed items are tracked in state.processedKeys + +#### Default + +```ts +false +``` + +*** + +### deduplicateStrategy? + +```ts +optional deduplicateStrategy: "keep-first" | "keep-last"; +``` + +Defined in: [queuer.ts:109](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L109) + +Strategy to use when a duplicate item is detected in the current queue +- 'keep-first': Keep the existing item and ignore the new one (default) +- 'keep-last': Replace the existing item with the new one +Note: This only affects duplicates within the same queue, not across executions + +#### Default + +```ts +'keep-first' +``` + +*** + ### expirationDuration? ```ts optional expirationDuration: number; ``` -Defined in: [queuer.ts:93](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L93) +Defined in: [queuer.ts:114](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L114) Maximum time in milliseconds that an item can stay in the queue If not provided, items will never expire @@ -56,7 +97,7 @@ If not provided, items will never expire optional getIsExpired: (item, addedAt) => boolean; ``` -Defined in: [queuer.ts:98](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L98) +Defined in: [queuer.ts:119](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L119) Function to determine if an item has expired If provided, this overrides the expirationDuration behavior @@ -77,13 +118,36 @@ If provided, this overrides the expirationDuration behavior *** +### getItemKey()? + +```ts +optional getItemKey: (item) => string | number; +``` + +Defined in: [queuer.ts:129](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L129) + +Function to extract a unique key from each item for deduplication +If not provided, uses the item itself for primitives or JSON.stringify for objects + +#### Parameters + +##### item + +`TValue` + +#### Returns + +`string` \| `number` + +*** + ### getItemsFrom? ```ts optional getItemsFrom: QueuePosition; ``` -Defined in: [queuer.ts:103](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L103) +Defined in: [queuer.ts:124](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L124) Default position to get items from during processing @@ -101,7 +165,7 @@ Default position to get items from during processing optional getPriority: (item) => number; ``` -Defined in: [queuer.ts:108](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L108) +Defined in: [queuer.ts:134](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L134) Function to determine priority of items in the queuer Higher priority items will be processed first @@ -124,7 +188,7 @@ Higher priority items will be processed first optional initialItems: TValue[]; ``` -Defined in: [queuer.ts:112](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L112) +Defined in: [queuer.ts:138](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L138) Initial items to populate the queuer with @@ -136,7 +200,7 @@ Initial items to populate the queuer with optional initialState: Partial>; ``` -Defined in: [queuer.ts:116](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L116) +Defined in: [queuer.ts:142](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L142) Initial state for the queuer @@ -148,7 +212,7 @@ Initial state for the queuer optional key: string; ``` -Defined in: [queuer.ts:121](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L121) +Defined in: [queuer.ts:147](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L147) Optional key to identify this queuer instance. If provided, the queuer will be identified by this key in the devtools and PacerProvider if applicable. @@ -161,19 +225,70 @@ If provided, the queuer will be identified by this key in the devtools and Pacer optional maxSize: number; ``` -Defined in: [queuer.ts:125](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L125) +Defined in: [queuer.ts:151](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L151) Maximum number of items allowed in the queuer *** +### maxTrackedKeys? + +```ts +optional maxTrackedKeys: number; +``` + +Defined in: [queuer.ts:158](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L158) + +Maximum number of processed keys to track (prevents memory leaks) +When limit is reached, oldest keys are removed (FIFO) +Only used when deduplicateItems is enabled + +#### Default + +```ts +1000 +``` + +*** + +### onDuplicate()? + +```ts +optional onDuplicate: (newItem, existingItem, queuer) => void; +``` + +Defined in: [queuer.ts:163](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L163) + +Callback fired when a duplicate item is detected +Called both for in-queue duplicates and cross-execution duplicates + +#### Parameters + +##### newItem + +`TValue` + +##### existingItem + +`TValue` | `undefined` + +##### queuer + +[`Queuer`](../classes/Queuer.md)\<`TValue`\> + +#### Returns + +`void` + +*** + ### onExecute()? ```ts optional onExecute: (item, queuer) => void; ``` -Defined in: [queuer.ts:129](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L129) +Defined in: [queuer.ts:171](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L171) Callback fired whenever an item is removed from the queuer @@ -199,7 +314,7 @@ Callback fired whenever an item is removed from the queuer optional onExpire: (item, queuer) => void; ``` -Defined in: [queuer.ts:133](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L133) +Defined in: [queuer.ts:175](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L175) Callback fired whenever an item expires in the queuer @@ -225,7 +340,7 @@ Callback fired whenever an item expires in the queuer optional onItemsChange: (queuer) => void; ``` -Defined in: [queuer.ts:137](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L137) +Defined in: [queuer.ts:179](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L179) Callback fired whenever an item is added or removed from the queuer @@ -247,7 +362,7 @@ Callback fired whenever an item is added or removed from the queuer optional onReject: (item, queuer) => void; ``` -Defined in: [queuer.ts:141](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L141) +Defined in: [queuer.ts:183](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L183) Callback fired whenever an item is rejected from being added to the queuer @@ -273,7 +388,7 @@ Callback fired whenever an item is rejected from being added to the queuer optional started: boolean; ``` -Defined in: [queuer.ts:145](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L145) +Defined in: [queuer.ts:187](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L187) Whether the queuer should start processing tasks immediately @@ -285,7 +400,7 @@ Whether the queuer should start processing tasks immediately optional wait: number | (queuer) => number; ``` -Defined in: [queuer.ts:151](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L151) +Defined in: [queuer.ts:193](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L193) Time in milliseconds to wait between processing items. Can be a number or a function that returns a number. diff --git a/docs/reference/interfaces/QueuerState.md b/docs/reference/interfaces/QueuerState.md index f6fee041b..4df6f02ef 100644 --- a/docs/reference/interfaces/QueuerState.md +++ b/docs/reference/interfaces/QueuerState.md @@ -135,13 +135,26 @@ Whether the queuer has a pending timeout for processing the next item *** +### processedKeys + +```ts +processedKeys: (string | number)[]; +``` + +Defined in: [queuer.ts:50](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L50) + +Array of keys that have been processed (for cross-execution deduplication) +Only populated when deduplicateItems is enabled + +*** + ### rejectionCount ```ts rejectionCount: number; ``` -Defined in: [queuer.ts:49](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L49) +Defined in: [queuer.ts:54](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L54) Number of items that have been rejected from being added to the queue @@ -153,7 +166,7 @@ Number of items that have been rejected from being added to the queue size: number; ``` -Defined in: [queuer.ts:53](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L53) +Defined in: [queuer.ts:58](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L58) Number of items currently in the queue @@ -165,6 +178,6 @@ Number of items currently in the queue status: "idle" | "running" | "stopped"; ``` -Defined in: [queuer.ts:57](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L57) +Defined in: [queuer.ts:62](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L62) Current processing status - 'idle' when not processing, 'running' when active, 'stopped' when paused diff --git a/docs/reference/type-aliases/QueuePosition.md b/docs/reference/type-aliases/QueuePosition.md index 9fa8f2e31..0862b32d8 100644 --- a/docs/reference/type-aliases/QueuePosition.md +++ b/docs/reference/type-aliases/QueuePosition.md @@ -9,7 +9,7 @@ title: QueuePosition type QueuePosition = "front" | "back"; ``` -Defined in: [queuer.ts:193](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L193) +Defined in: [queuer.ts:240](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/queuer.ts#L240) Position type for addItem and getNextItem operations. diff --git a/examples/react/useBatcherInBatchDedup/.eslintrc.cjs b/examples/react/useBatcherInBatchDedup/.eslintrc.cjs new file mode 100644 index 000000000..9ff0b9fc9 --- /dev/null +++ b/examples/react/useBatcherInBatchDedup/.eslintrc.cjs @@ -0,0 +1,13 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = { + settings: { + extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'], + rules: { + 'react/no-children-prop': 'off', + }, + }, +} + +module.exports = config diff --git a/examples/react/useBatcherInBatchDedup/.gitignore b/examples/react/useBatcherInBatchDedup/.gitignore new file mode 100644 index 000000000..4673b022e --- /dev/null +++ b/examples/react/useBatcherInBatchDedup/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +pnpm-lock.yaml +yarn.lock +package-lock.json + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/react/useBatcherInBatchDedup/README.md b/examples/react/useBatcherInBatchDedup/README.md new file mode 100644 index 000000000..5f1a1daec --- /dev/null +++ b/examples/react/useBatcherInBatchDedup/README.md @@ -0,0 +1,29 @@ +# useBatcher In-Batch Deduplication Example + +This example demonstrates the in-batch deduplication feature of `useBatcher`. + +## Key Features + +- **In-batch deduplication**: Duplicate items within the same batch are automatically ignored or replaced based on the `deduplicateStrategy` +- **Visual testing**: Interactive UI to test deduplication behavior +- **Activity log**: See exactly what items are added vs. ignored + +## Running the Example + +```bash +pnpm dev +``` + +Then open http://localhost:3006 + +## How It Works + +When `deduplicateItems: true` is set: +- Items are deduplicated within the current batch only +- The `deduplicateStrategy` determines whether to keep the first or last occurrence +- Duplicates are identified using the `getItemKey` function (or JSON.stringify for objects) + +This is useful for scenarios like: +- Preventing duplicate API requests within the same batch +- Ensuring unique items in a batch operation +- Reducing redundant processing of the same data diff --git a/examples/react/useBatcherInBatchDedup/index.html b/examples/react/useBatcherInBatchDedup/index.html new file mode 100644 index 000000000..701aa26e3 --- /dev/null +++ b/examples/react/useBatcherInBatchDedup/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Pacer Example + + + +
+ + + diff --git a/examples/react/useBatcherInBatchDedup/package.json b/examples/react/useBatcherInBatchDedup/package.json new file mode 100644 index 000000000..209bc3c64 --- /dev/null +++ b/examples/react/useBatcherInBatchDedup/package.json @@ -0,0 +1,34 @@ +{ + "name": "@tanstack/pacer-example-react-use-batcher-in-batch-dedup", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3006", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/react-pacer": "^0.19.3", + "react": "^19.2.3", + "react-dom": "^19.2.3" + }, + "devDependencies": { + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "vite": "^7.3.1" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/react/useBatcherInBatchDedup/public/emblem-light.svg b/examples/react/useBatcherInBatchDedup/public/emblem-light.svg new file mode 100644 index 000000000..a58e69ad5 --- /dev/null +++ b/examples/react/useBatcherInBatchDedup/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/useBatcherInBatchDedup/src/index.tsx b/examples/react/useBatcherInBatchDedup/src/index.tsx new file mode 100644 index 000000000..7b15a25ff --- /dev/null +++ b/examples/react/useBatcherInBatchDedup/src/index.tsx @@ -0,0 +1,234 @@ +import { useState } from 'react' +import ReactDOM from 'react-dom/client' +import { useBatcher } from '@tanstack/react-pacer/batcher' +import { PacerProvider } from '@tanstack/react-pacer/provider' + +function App1() { + // Use your state management library of choice + const [processedBatches, setProcessedBatches] = useState< + Array> + >([]) + const [log, setLog] = useState([]) + + // The function that will process a batch of items + function processBatch(items: Array) { + setProcessedBatches((prev) => [...prev, items]) + setLog((prev) => [...prev, `โœ… Processed batch: [${items.join(', ')}]`]) + console.log('processing batch', items) + } + + const batcher = useBatcher( + processBatch, + { + maxSize: 5, + wait: 3000, + // Enable in-batch deduplication + deduplicateItems: true, + deduplicateStrategy: 'keep-first', // or 'keep-last' + }, + ) + + const addItem = (item: string) => { + const result = batcher.addItem(item) + if (result) { + setLog((prev) => [...prev, `โž• Added: "${item}"`]) + } else { + setLog((prev) => [...prev, `โš ๏ธ Duplicate ignored: "${item}"`]) + } + } + + return ( +
+

TanStack Pacer - In-Batch Deduplication

+

+ When deduplicateItems: true, duplicate items within the same batch are automatically ignored. + This example demonstrates how duplicates are handled before the batch is processed. +

+ + ({ + size: state.size, + executionCount: state.executionCount, + totalItemsProcessed: state.totalItemsProcessed, + })} + > + {({ size, executionCount, totalItemsProcessed }) => ( + <> +
+
+
Current Batch Size: {size} / 5
+
Batches Processed: {executionCount}
+
Current Batch: [{batcher.peekAllItems().join(', ')}]
+
Total Items Processed: {totalItemsProcessed}
+
+
+ +
+

๐Ÿงช Test Deduplication

+

+ Click buttons multiple times. Duplicates within the same batch will be ignored! +

+
+ + + + + +
+
+ + +
+
+ ๐Ÿ’ก Tip: Add the same item multiple times before the batch is processed. + Notice how duplicates are ignored! +
+
+ +
+
+

๐Ÿ“ฆ Processed Batches

+
+ {processedBatches.length === 0 ? ( + No batches processed yet + ) : ( + processedBatches.map((b, i) => ( +
+ Batch #{i + 1}: [{b.join(', ')}] +
+ )) + )} +
+
+
+

๐Ÿ“‹ Activity Log

+
+ {log.length === 0 ? ( + No activity yet + ) : ( + log.map((entry, i) => ( +
+ {entry} +
+ )) + )} +
+
+
+ + )} +
+ +
+ + ๐Ÿ› Debug: Full State + + state}> + {(state) => ( +
+              {JSON.stringify(state, null, 2)}
+            
+ )} +
+
+
+ ) +} + +const root = ReactDOM.createRoot(document.getElementById('root')!) +root.render( + + + , +) diff --git a/examples/react/useBatcherInBatchDedup/tsconfig.json b/examples/react/useBatcherInBatchDedup/tsconfig.json new file mode 100644 index 000000000..6e9088d67 --- /dev/null +++ b/examples/react/useBatcherInBatchDedup/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/react/useBatcherInBatchDedup/vite.config.ts b/examples/react/useBatcherInBatchDedup/vite.config.ts new file mode 100644 index 000000000..4e1943662 --- /dev/null +++ b/examples/react/useBatcherInBatchDedup/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react({ + // babel: { + // plugins: [['babel-plugin-react-compiler', { target: '19' }]], + // }, + }), + ], +}) diff --git a/packages/pacer/src/batcher.ts b/packages/pacer/src/batcher.ts index 158f3e633..83e8605c8 100644 --- a/packages/pacer/src/batcher.ts +++ b/packages/pacer/src/batcher.ts @@ -50,11 +50,29 @@ function getDefaultBatcherState(): BatcherState { * Options for configuring a Batcher instance */ export interface BatcherOptions { + /** + * Enable automatic deduplication of items within the current batch + * When enabled, duplicate items in the same batch will be merged based on deduplicateStrategy + * @default false + */ + deduplicateItems?: boolean + /** + * Strategy to use when a duplicate item is detected in the current batch + * - 'keep-first': Keep the existing item and ignore the new one (default) + * - 'keep-last': Replace the existing item with the new one + * @default 'keep-first' + */ + deduplicateStrategy?: 'keep-first' | 'keep-last' /** * Custom function to determine if a batch should be processed * Return true to process the batch immediately */ getShouldExecute?: (items: Array, batcher: Batcher) => boolean + /** + * Function to extract a unique key from each item for deduplication + * If not provided, uses the item itself for primitives or JSON.stringify for objects + */ + getItemKey?: (item: TValue) => string | number /** * Initial state for the batcher */ @@ -93,10 +111,12 @@ export interface BatcherOptions { type BatcherOptionsWithOptionalCallbacks = OptionalKeys< Required>, - 'initialState' | 'onExecute' | 'onItemsChange' | 'key' + 'initialState' | 'onExecute' | 'onItemsChange' | 'key' | 'getItemKey' > const defaultOptions: BatcherOptionsWithOptionalCallbacks = { + deduplicateItems: false, + deduplicateStrategy: 'keep-first', getShouldExecute: () => false, maxSize: Infinity, started: true, @@ -114,6 +134,7 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks = { * - Time-based batching (process after X milliseconds) * - Custom batch processing logic via getShouldExecute * - Event callbacks for monitoring batch operations + * - In-batch deduplication via deduplicateItems * * State Management: * - Uses TanStack Store for reactive state management @@ -141,6 +162,23 @@ const defaultOptions: BatcherOptionsWithOptionalCallbacks = { * // the batch will be processed * // batcher.flush() // manually trigger a batch * ``` + * + * @example + * ```ts + * // In-batch deduplication - prevent duplicate items within the same batch + * const batcher = new Batcher<{ userId: string }>( + * (items) => fetchUsers(items.map(i => i.userId)), + * { + * deduplicateItems: true, + * getItemKey: (item) => item.userId, + * } + * ); + * + * batcher.addItem({ userId: 'user-1' }); // Added to batch + * batcher.addItem({ userId: 'user-2' }); // Added to batch + * batcher.addItem({ userId: 'user-1' }); // Ignored! Already in current batch + * batcher.flush(); // Processes [user-1, user-2] + * ``` */ export class Batcher { readonly store: Store>> = new Store( @@ -200,11 +238,45 @@ export class Batcher { return parseFunctionOrValue(this.options.wait, this) } + #getItemKey = (item: TValue): string | number => { + if (this.options.getItemKey) { + return this.options.getItemKey(item) + } + return typeof item === 'object' ? JSON.stringify(item) : (item as any) + } + + #findItemByKey = (key: string | number): number => { + return this.store.state.items.findIndex( + (item) => this.#getItemKey(item) === key, + ) + } + /** * Adds an item to the batcher * If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed + * When deduplicateItems is enabled, duplicate items within the current batch will be merged based on deduplicateStrategy */ - addItem = (item: TValue): void => { + addItem = (item: TValue): boolean => { + if (this.options.deduplicateItems) { + const key = this.#getItemKey(item) + + // Check for duplicates in the current batch (in-batch deduplication) + const existingIndex = this.#findItemByKey(key) + if (existingIndex !== -1) { + const existingItem = this.store.state.items[existingIndex] + if (existingItem !== undefined) { + if (this.options.deduplicateStrategy === 'keep-last') { + const newItems = [...this.store.state.items] + newItems[existingIndex] = item + this.#setState({ items: newItems }) + this.options.onItemsChange?.(this) + } + // For 'keep-first' strategy, we simply return without adding + return true + } + } + } + this.#setState({ items: [...this.store.state.items, item], isPending: this.options.wait !== Infinity, @@ -221,6 +293,8 @@ export class Batcher { this.#clearTimeout() // clear any pending timeout to replace it with a new one this.#timeoutId = setTimeout(() => this.#execute(), this.#getWait()) } + + return true } /** diff --git a/packages/pacer/src/queuer.ts b/packages/pacer/src/queuer.ts index 2bdf1c0a0..d52559538 100644 --- a/packages/pacer/src/queuer.ts +++ b/packages/pacer/src/queuer.ts @@ -86,6 +86,19 @@ export interface QueuerOptions { * @default 'back' */ addItemsTo?: QueuePosition + /** + * Enable automatic deduplication of items within the current queue + * When enabled, duplicate items in the same queue will be merged based on deduplicateStrategy + * @default false + */ + deduplicateItems?: boolean + /** + * Strategy to use when a duplicate item is detected in the current queue + * - 'keep-first': Keep the existing item and ignore the new one (default) + * - 'keep-last': Replace the existing item with the new one + * @default 'keep-first' + */ + deduplicateStrategy?: 'keep-first' | 'keep-last' /** * Maximum time in milliseconds that an item can stay in the queue * If not provided, items will never expire @@ -101,6 +114,11 @@ export interface QueuerOptions { * @default 'front' */ getItemsFrom?: QueuePosition + /** + * Function to extract a unique key from each item for deduplication + * If not provided, uses the item itself for primitives or JSON.stringify for objects + */ + getItemKey?: (item: TValue) => string | number /** * Function to determine priority of items in the queuer * Higher priority items will be processed first @@ -172,8 +190,11 @@ const defaultOptions: Omit< | 'onReject' | 'onExpire' | 'key' + | 'getItemKey' > = { addItemsTo: 'back', + deduplicateItems: false, + deduplicateStrategy: 'keep-first', getItemsFrom: 'front', getPriority: (item) => item?.priority ?? 0, getIsExpired: () => false, @@ -203,6 +224,7 @@ export type QueuePosition = 'front' | 'back' * - Priority-based ordering when getPriority is provided * - Item expiration and removal of stale items * - Callbacks for queue state changes, execution, rejection, and expiration + * - In-queue deduplication via deduplicateItems * * Running behavior: * - `start()`: Begins automatically processing items in the queue (defaults to isRunning) @@ -265,6 +287,22 @@ export type QueuePosition = 'front' | 'back' * manualQueue.execute(); // logs 1, queue is [2] * manualQueue.getNextItem(); // returns 2, queue is empty * ``` + * + * @example + * ```ts + * // In-queue deduplication - prevent duplicate items within the same queue + * const queuer = new Queuer<{ userId: string }>( + * (item) => fetchUser(item.userId), + * { + * deduplicateItems: true, + * getItemKey: (item) => item.userId, + * } + * ); + * + * queuer.addItem({ userId: 'user-1' }); // Added to queue + * queuer.addItem({ userId: 'user-2' }); // Added to queue + * queuer.addItem({ userId: 'user-1' }); // Ignored! Already in current queue + * ``` */ export class Queuer { readonly store: Store>> = new Store( @@ -354,6 +392,19 @@ export class Queuer { return parseFunctionOrValue(this.options.wait ?? 0, this) } + #getItemKey = (item: TValue): string | number => { + if (this.options.getItemKey) { + return this.options.getItemKey(item) + } + return typeof item === 'object' ? JSON.stringify(item) : (item as any) + } + + #findItemByKey = (key: string | number): number => { + return this.store.state.items.findIndex( + (item) => this.#getItemKey(item) === key, + ) + } + /** * Processes items in the queue up to the wait interval. Internal use only. */ @@ -389,6 +440,7 @@ export class Queuer { /** * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called. * Items can be inserted based on priority or at the front/back depending on configuration. + * When deduplicateItems is enabled, duplicate items within the current queue will be merged based on deduplicateStrategy. * * Returns true if the item was added, false if the queue is full. * @@ -407,6 +459,28 @@ export class Queuer { addItemCount: this.store.state.addItemCount + 1, }) + if (this.options.deduplicateItems) { + const key = this.#getItemKey(item) + + // Check for duplicates in the current queue (in-queue deduplication) + const existingIndex = this.#findItemByKey(key) + if (existingIndex !== -1) { + const existingItem = this.store.state.items[existingIndex] + if (existingItem !== undefined) { + if (this.options.deduplicateStrategy === 'keep-last') { + const newItems = [...this.store.state.items] + newItems[existingIndex] = item + this.#setState({ items: newItems }) + if (runOnItemsChange) { + this.options.onItemsChange?.(this) + } + } + // For 'keep-first' strategy, we simply return without adding + return true // Item was "handled" (deduplicated) + } + } + } + if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) { this.#setState({ rejectionCount: this.store.state.rejectionCount + 1, diff --git a/packages/pacer/tests/batcher.test.ts b/packages/pacer/tests/batcher.test.ts index 99ef4f3fe..c1b086ebf 100644 --- a/packages/pacer/tests/batcher.test.ts +++ b/packages/pacer/tests/batcher.test.ts @@ -86,6 +86,8 @@ describe('Batcher', () => { expect(batcher.options.maxSize).toBe(Infinity) expect(batcher.options.wait).toBe(Infinity) expect(batcher.options.started).toBe(true) + expect(batcher.options.deduplicateItems).toBe(false) + expect(batcher.options.deduplicateStrategy).toBe('keep-first') expect(typeof batcher.options.getShouldExecute).toBe('function') }) @@ -213,6 +215,14 @@ describe('Batcher', () => { vi.advanceTimersByTime(500) expect(mockFn).toHaveBeenCalledWith([1, 2]) }) + + it('should return true when item is added', () => { + const mockFn = vi.fn() + const batcher = new Batcher(mockFn, { wait: 1000 }) + + const result = batcher.addItem(1) + expect(result).toBe(true) + }) }) describe('flush', () => { @@ -447,3 +457,116 @@ describe('batch', () => { expect(mockFn).toHaveBeenCalledWith(['test']) }) }) + +describe('Batcher Deduplication', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should not deduplicate by default', () => { + const mockFn = vi.fn() + const batcher = new Batcher(mockFn, { maxSize: 5 }) + + batcher.addItem(1) + batcher.addItem(1) + batcher.addItem(2) + + expect(batcher.store.state.items).toEqual([1, 1, 2]) + expect(batcher.store.state.size).toBe(3) + }) + + describe('In-Batch Deduplication', () => { + it('should deduplicate primitive items in the same batch with keep-first strategy', () => { + const mockFn = vi.fn() + const batcher = new Batcher(mockFn, { + maxSize: 5, + deduplicateItems: true, + }) + + batcher.addItem(1) + batcher.addItem(2) + batcher.addItem(1) // Duplicate in current batch + batcher.addItem(3) + + expect(batcher.store.state.items).toEqual([1, 2, 3]) + expect(batcher.store.state.size).toBe(3) + }) + + it('should deduplicate with keep-last strategy', () => { + const mockFn = vi.fn() + const batcher = new Batcher(mockFn, { + maxSize: 5, + deduplicateItems: true, + deduplicateStrategy: 'keep-last', + }) + + batcher.addItem('a') + batcher.addItem('b') + batcher.addItem('a') // Should replace first 'a' + + expect(batcher.store.state.items).toEqual(['a', 'b']) + expect(batcher.store.state.size).toBe(2) + }) + + it('should deduplicate objects with custom getItemKey', () => { + const mockFn = vi.fn() + const batcher = new Batcher<{ id: number; name: string }>(mockFn, { + maxSize: 5, + deduplicateItems: true, + getItemKey: (item) => item.id, + }) + + batcher.addItem({ id: 1, name: 'Alice' }) + batcher.addItem({ id: 2, name: 'Bob' }) + batcher.addItem({ id: 1, name: 'Alice Updated' }) // Duplicate id in batch + + expect(batcher.store.state.items).toEqual([ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + ]) + expect(batcher.store.state.size).toBe(2) + }) + + it('should replace item with keep-last strategy', () => { + const mockFn = vi.fn() + const batcher = new Batcher<{ id: string; value: number }>(mockFn, { + maxSize: 5, + deduplicateItems: true, + deduplicateStrategy: 'keep-last', + getItemKey: (item) => item.id, + }) + + const item1 = { id: 'user-1', value: 100 } + const item2 = { id: 'user-2', value: 200 } + const item1Updated = { id: 'user-1', value: 150 } + + batcher.addItem(item1) + batcher.addItem(item2) + batcher.addItem(item1Updated) // Should replace item1 in batch + + expect(batcher.store.state.items).toEqual([ + { id: 'user-1', value: 150 }, + { id: 'user-2', value: 200 }, + ]) + }) + + it('should handle objects with JSON.stringify when no getItemKey', () => { + const mockFn = vi.fn() + const batcher = new Batcher<{ x: number }>(mockFn, { + maxSize: 5, + deduplicateItems: true, + }) + + batcher.addItem({ x: 1 }) + batcher.addItem({ x: 2 }) + batcher.addItem({ x: 1 }) // Same object structure, should be deduplicated + + expect(batcher.store.state.items).toEqual([{ x: 1 }, { x: 2 }]) + expect(batcher.store.state.size).toBe(2) + }) + }) +}) diff --git a/packages/pacer/tests/queuer.test.ts b/packages/pacer/tests/queuer.test.ts index e65daef5b..7a3d0dd8c 100644 --- a/packages/pacer/tests/queuer.test.ts +++ b/packages/pacer/tests/queuer.test.ts @@ -24,6 +24,7 @@ describe('Queuer', () => { expect(queuer.store.state.size).toBe(2) }) + describe('addItem', () => { it('should add items to the queuer', () => { const fn = vi.fn() @@ -348,6 +349,7 @@ describe('Queuer', () => { queuer.reset() expect(queuer.peekAllItems()).toEqual([]) }) + }) describe('start', () => { @@ -535,4 +537,115 @@ describe('Queuer', () => { }) }) }) + + describe('In-Queue Deduplication', () => { + it('should not deduplicate by default', () => { + const fn = vi.fn() + const queuer = new Queuer(fn, { started: false, maxSize: 5 }) + + expect(queuer.addItem(1)).toBe(true) + expect(queuer.addItem(1)).toBe(true) + expect(queuer.addItem(2)).toBe(true) + + expect(queuer.store.state.items).toEqual([1, 1, 2]) + expect(queuer.store.state.size).toBe(3) + }) + + it('should deduplicate primitive items in current queue with keep-first strategy', () => { + const fn = vi.fn() + const queuer = new Queuer(fn, { + started: false, + maxSize: 5, + deduplicateItems: true, + }) + + queuer.addItem(1) + queuer.addItem(2) + queuer.addItem(1) // Duplicate in queue + queuer.addItem(3) + + expect(queuer.store.state.items).toEqual([1, 2, 3]) + expect(queuer.store.state.size).toBe(3) + }) + + it('should deduplicate with keep-last strategy', () => { + const fn = vi.fn() + const queuer = new Queuer(fn, { + started: false, + maxSize: 5, + deduplicateItems: true, + deduplicateStrategy: 'keep-last', + }) + + queuer.addItem('a') + queuer.addItem('b') + queuer.addItem('a') // Should replace first 'a' + + expect(queuer.store.state.items).toEqual(['a', 'b']) + expect(queuer.store.state.size).toBe(2) + }) + + it('should deduplicate before checking maxSize', () => { + const fn = vi.fn() + const onReject = vi.fn() + const queuer = new Queuer(fn, { + started: false, + maxSize: 2, + deduplicateItems: true, + onReject, + }) + + queuer.addItem(1) + queuer.addItem(2) + queuer.addItem(1) // Duplicate in queue, should not trigger rejection + + expect(queuer.store.state.size).toBe(2) + expect(onReject).not.toHaveBeenCalled() + + queuer.addItem(3) // Should be rejected + + expect(queuer.store.state.size).toBe(2) + expect(onReject).toHaveBeenCalledWith(3, queuer) + }) + + it('should deduplicate objects with custom getItemKey', () => { + const fn = vi.fn() + const queuer = new Queuer<{ id: string; value: number }>(fn, { + started: false, + maxSize: 5, + deduplicateItems: true, + getItemKey: (item) => item.id, + }) + + queuer.addItem({ id: 'user-1', value: 100 }) + queuer.addItem({ id: 'user-2', value: 200 }) + queuer.addItem({ id: 'user-1', value: 150 }) // Duplicate in queue + + expect(queuer.store.state.items).toEqual([ + { id: 'user-1', value: 100 }, + { id: 'user-2', value: 200 }, + ]) + expect(queuer.store.state.size).toBe(2) + }) + + it('should work with priority queue and deduplication', () => { + const fn = vi.fn() + const queuer = new Queuer<{ id: string; priority: number }>(fn, { + started: false, + deduplicateItems: true, + getItemKey: (item) => item.id, + getPriority: (item) => item.priority, + }) + + queuer.addItem({ id: 'task-1', priority: 1 }) + queuer.addItem({ id: 'task-2', priority: 3 }) + queuer.addItem({ id: 'task-1', priority: 5 }) // Duplicate in queue + + // Items should be sorted by priority, with duplicate deduplicated + expect(queuer.store.state.items).toEqual([ + { id: 'task-2', priority: 3 }, + { id: 'task-1', priority: 1 }, + ]) + }) + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 359775140..309ba3464 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -740,10 +740,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -765,10 +765,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -790,10 +790,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -815,10 +815,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -840,10 +840,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -865,10 +865,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -890,10 +890,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -915,14 +915,14 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@tanstack/react-devtools': specifier: 0.9.2 - version: 0.9.2(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.11) + version: 0.9.2(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) '@tanstack/react-pacer-devtools': specifier: 0.5.2 version: link:../../../packages/react-pacer-devtools @@ -946,10 +946,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -971,16 +971,16 @@ importers: version: link:../../../packages/react-pacer '@tanstack/react-query': specifier: ^5.90.20 - version: 5.90.20(react@19.2.3) + version: 5.90.20(react@19.2.4) '@tanstack/react-query-devtools': specifier: ^5.91.2 - version: 5.91.2(@tanstack/react-query@5.90.20(react@19.2.3))(react@19.2.3) + version: 5.91.2(@tanstack/react-query@5.90.20(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1002,16 +1002,16 @@ importers: version: link:../../../packages/react-pacer '@tanstack/react-query': specifier: ^5.90.20 - version: 5.90.20(react@19.2.3) + version: 5.90.20(react@19.2.4) '@tanstack/react-query-devtools': specifier: ^5.91.2 - version: 5.91.2(@tanstack/react-query@5.90.20(react@19.2.3))(react@19.2.3) + version: 5.91.2(@tanstack/react-query@5.90.20(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1033,16 +1033,16 @@ importers: version: link:../../../packages/react-pacer '@tanstack/react-query': specifier: ^5.90.20 - version: 5.90.20(react@19.2.3) + version: 5.90.20(react@19.2.4) '@tanstack/react-query-devtools': specifier: ^5.91.2 - version: 5.91.2(@tanstack/react-query@5.90.20(react@19.2.3))(react@19.2.3) + version: 5.91.2(@tanstack/react-query@5.90.20(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1064,10 +1064,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1089,10 +1089,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1114,10 +1114,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1139,10 +1139,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1164,10 +1164,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1189,10 +1189,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1214,10 +1214,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1239,13 +1239,13 @@ importers: version: link:../../../packages/react-pacer '@tanstack/react-persister': specifier: ^0.1.1 - version: 0.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1267,13 +1267,13 @@ importers: version: link:../../../packages/react-pacer '@tanstack/react-persister': specifier: ^0.1.1 - version: 0.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1295,10 +1295,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1320,10 +1320,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1345,10 +1345,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1370,10 +1370,35 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) + devDependencies: + '@types/react': + specifier: ^19.2.9 + version: 19.2.9 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.9) + '@vitejs/plugin-react': + specifier: ^5.1.2 + version: 5.1.2(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(yaml@2.8.2)) + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(yaml@2.8.2) + + examples/react/useBatcherDedup: + dependencies: + '@tanstack/react-pacer': + specifier: ^0.19.3 + version: link:../../../packages/react-pacer + react: + specifier: ^19.2.3 + version: 19.2.4 + react-dom: + specifier: ^19.2.3 + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1395,10 +1420,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1420,10 +1445,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1445,10 +1470,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1470,10 +1495,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1495,10 +1520,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1520,10 +1545,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1545,17 +1570,17 @@ importers: version: link:../../../packages/react-pacer '@tanstack/react-persister': specifier: ^0.1.1 - version: 0.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@tanstack/react-devtools': specifier: 0.9.2 - version: 0.9.2(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.11) + version: 0.9.2(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) '@tanstack/react-pacer-devtools': specifier: 0.5.2 version: link:../../../packages/react-pacer-devtools @@ -1579,13 +1604,13 @@ importers: version: link:../../../packages/react-pacer '@tanstack/react-persister': specifier: ^0.1.1 - version: 0.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1607,10 +1632,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1632,10 +1657,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1657,10 +1682,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1682,13 +1707,13 @@ importers: version: link:../../../packages/react-pacer '@tanstack/react-persister': specifier: ^0.1.1 - version: 0.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1710,13 +1735,13 @@ importers: version: link:../../../packages/react-pacer '@tanstack/react-persister': specifier: ^0.1.1 - version: 0.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1738,10 +1763,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1763,10 +1788,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1788,10 +1813,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1813,10 +1838,10 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@types/react': specifier: ^19.2.9 @@ -1838,14 +1863,14 @@ importers: version: link:../../../packages/react-pacer react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) devDependencies: '@tanstack/react-devtools': specifier: 0.9.2 - version: 0.9.2(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.11) + version: 0.9.2(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) '@tanstack/react-pacer-devtools': specifier: 0.5.2 version: link:../../../packages/react-pacer-devtools @@ -2412,7 +2437,7 @@ importers: version: 0.4.4(csstype@3.2.3)(solid-js@1.9.11) '@tanstack/devtools-utils': specifier: ^0.3.0 - version: 0.3.0(@types/react@19.2.9)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.11) + version: 0.3.0(@types/react@19.2.9)(csstype@3.2.3)(preact@10.28.2)(react@19.2.4)(solid-js@1.9.11) '@tanstack/pacer': specifier: '>=0.16.4' version: link:../pacer @@ -2465,7 +2490,7 @@ importers: dependencies: '@tanstack/devtools-utils': specifier: ^0.3.0 - version: 0.3.0(@types/react@19.2.9)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.11) + version: 0.3.0(@types/react@19.2.9)(csstype@3.2.3)(preact@10.28.2)(react@19.2.4)(solid-js@1.9.11) '@tanstack/pacer-devtools': specifier: workspace:* version: link:../pacer-devtools @@ -2487,10 +2512,10 @@ importers: version: link:../pacer '@tanstack/react-store': specifier: ^0.8.0 - version: 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.8.0(react-dom@19.2.3(react@19.2.4))(react@19.2.4) react-dom: specifier: '>=16.8' - version: 19.2.3(react@19.2.3) + version: 19.2.3(react@19.2.4) devDependencies: '@eslint-react/eslint-plugin': specifier: ^2.7.4 @@ -2509,13 +2534,13 @@ importers: version: 7.0.1(eslint@9.39.2(jiti@2.6.1)) react: specifier: ^19.2.3 - version: 19.2.3 + version: 19.2.4 packages/react-pacer-devtools: dependencies: '@tanstack/devtools-utils': specifier: ^0.3.0 - version: 0.3.0(@types/react@19.2.9)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.11) + version: 0.3.0(@types/react@19.2.9)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.11) '@tanstack/pacer-devtools': specifier: workspace:* version: link:../pacer-devtools @@ -2565,7 +2590,7 @@ importers: dependencies: '@tanstack/devtools-utils': specifier: ^0.3.0 - version: 0.3.0(@types/react@19.2.9)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.11) + version: 0.3.0(@types/react@19.2.9)(csstype@3.2.3)(preact@10.28.2)(react@19.2.4)(solid-js@1.9.11) '@tanstack/pacer-devtools': specifier: workspace:* version: link:../pacer-devtools @@ -5466,6 +5491,11 @@ packages: peerDependencies: react: ^19.2.3 + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -5480,6 +5510,10 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -7254,7 +7288,18 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-utils@0.3.0(@types/react@19.2.9)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.11)': + '@tanstack/devtools-utils@0.3.0(@types/react@19.2.9)(csstype@3.2.3)(preact@10.28.2)(react@19.2.4)(solid-js@1.9.11)': + dependencies: + '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.11) + optionalDependencies: + '@types/react': 19.2.9 + preact: 10.28.2 + react: 19.2.4 + solid-js: 1.9.11 + transitivePeerDependencies: + - csstype + + '@tanstack/devtools-utils@0.3.0(@types/react@19.2.9)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.11)': dependencies: '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.11) optionalDependencies: @@ -7318,42 +7363,42 @@ snapshots: '@tanstack/query-devtools@5.92.0': {} - '@tanstack/react-devtools@0.9.2(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.11)': + '@tanstack/react-devtools@0.9.2(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)': dependencies: '@tanstack/devtools': 0.10.3(csstype@3.2.3)(solid-js@1.9.11) '@types/react': 19.2.9 '@types/react-dom': 19.2.3(@types/react@19.2.9) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) transitivePeerDependencies: - bufferutil - csstype - solid-js - utf-8-validate - '@tanstack/react-persister@0.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-persister@0.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/persister': 0.1.1 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.20(react@19.2.3))(react@19.2.3)': + '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.20(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/query-devtools': 5.92.0 - '@tanstack/react-query': 5.90.20(react@19.2.3) - react: 19.2.3 + '@tanstack/react-query': 5.90.20(react@19.2.4) + react: 19.2.4 - '@tanstack/react-query@5.90.20(react@19.2.3)': + '@tanstack/react-query@5.90.20(react@19.2.4)': dependencies: '@tanstack/query-core': 5.90.20 - react: 19.2.3 + react: 19.2.4 - '@tanstack/react-store@0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-store@0.8.0(react-dom@19.2.3(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/store': 0.8.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - use-sync-external-store: 1.6.0(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.3(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) '@tanstack/solid-devtools@0.7.22(csstype@3.2.3)(solid-js@1.9.11)': dependencies: @@ -9273,6 +9318,16 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 + react-dom@19.2.3(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + react-is@17.0.2: {} react-is@18.3.1: {} @@ -9281,6 +9336,8 @@ snapshots: react@19.2.3: {} + react@19.2.4: {} + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -9784,9 +9841,9 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.6.0(react@19.2.3): + use-sync-external-store@1.6.0(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 util-deprecate@1.0.2: {}