authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-29 18:16:06-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-29 22:05:20-08:00
log4591d794aff944fbb510d589770021c9ccb60806
tree189aadc63be13ce6975bbd45a18a6363728664a5
parentbcb14cf5bd2f877987a158c4e40556f070172435
signaturelock-open Commit is signed but in an unrecognized format.

chore: stuff


3 files changed, 34 insertions(+), 8 deletions(-)

jsr.json+1-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1{1{
2 "name": "@clo/react-mutation",2 "name": "@clo/react-mutation",
3 "version": "1.0.0-beta.9",3 "version": "1.0.0-beta.10",
4 "exports": {4 "exports": {
5 ".": "./src/mod.ts",5 ".": "./src/mod.ts",
6 "./tanstack-query.ts": "./src/tanstack-query.ts",6 "./tanstack-query.ts": "./src/tanstack-query.ts",
readme.md+26-3
...@@ -120,7 +120,6 @@ export function Example({ id }: { id: string }) {...@@ -120,7 +120,6 @@ export function Example({ id }: { id: string }) {
120}120}
121```121```
122122
123
124## Optimistic Updates123## Optimistic Updates
125124
126The `optimistic` function is given an object with the following APIs125The `optimistic` function is given an object with the following APIs
...@@ -132,7 +131,7 @@ The `optimistic` function is given an object with the following APIs...@@ -132,7 +131,7 @@ The `optimistic` function is given an object with the following APIs
132- `onRestore` - add a callback to revert your optimistic update131- `onRestore` - add a callback to revert your optimistic update
133- `onRefetch` - add a callback to fetch data after a success132- `onRefetch` - add a callback to fetch data after a success
134133
135### React Query Optimistic Helpers134#### React Query Optimistic Helpers
136135
137When using React Query, you can opt into some incredible helpers for making it136When using React Query, you can opt into some incredible helpers for making it
138very easy to write Optimistic Updates. Our setup at work is with this client137very easy to write Optimistic Updates. Our setup at work is with this client
...@@ -228,6 +227,30 @@ function Item({ id }: { id: string }) {...@@ -228,6 +227,30 @@ function Item({ id }: { id: string }) {
228}227}
229```228```
230229
230## Snapshotting to Skip No-Ops
231
232For operations that might be passed a parameter that doesn't actually change
233anything, `snapshot` can be used to detect no-op mutations.
234
235```tsx
236const mutUpdateField = mutations.define({
237 async mutate(id: string, value: string) { /* mutation */ },
238
239 optimistic({ args: [id, value], helpers }) {
240 helpers.objSet(queryItem(id), ["value"], value);
241 },
242
243 // called once before `optimistic` and once after. if the values are equal,
244 // then the mutation is cancelled (won't call `onSuccess`, but will `onSettled`)
245 // (defaulting to a json-based deep equal check, customize in MutationClient)
246 snapshot({ args: [id], get }) {
247 return get(queryItem(id))?.value;
248 }
249
250 // (...describe and optionally debounce stuff...)
251});
252```
253
231## Calling Mutations254## Calling Mutations
232255
233Three methods exist for calling mutations:256Three methods exist for calling mutations:
...@@ -319,7 +342,7 @@ It can now be used for easy mutations:...@@ -319,7 +342,7 @@ It can now be used for easy mutations:
319342
320## Batched Mutations343## Batched Mutations
321344
322This is an advanced feature. Complete Documentation is pending.345This is an advanced feature. Complete Documentation is pending. It is not recommended to use this.
323346
324Each call to the mutation applies new optimistic state on top of the previous,347Each call to the mutation applies new optimistic state on top of the previous,
325and after a debounce / throttle, the new optimistic state is committed to the348and after a debounce / throttle, the new optimistic state is committed to the
src/blocking.ts+7-4
...@@ -292,7 +292,7 @@ export class BlockingMutation<...@@ -292,7 +292,7 @@ export class BlockingMutation<
292292
293 // Check if debouncing is enabled293 // Check if debouncing is enabled
294 if (this.#options.debounceMs !== undefined) {294 if (this.#options.debounceMs !== undefined) {
295 return this.#runDebouncedAndReturn(args, key, channel, userOnRestore);295 return this.#runDebouncedAndReturn(args, key, channel, userOnRestore, true);
296 }296 }
297297
298 // Create shared optimistic helpers instance for the channel if it doesn't exist298 // Create shared optimistic helpers instance for the channel if it doesn't exist
...@@ -466,6 +466,7 @@ export class BlockingMutation<...@@ -466,6 +466,7 @@ export class BlockingMutation<
466 key: string,466 key: string,
467 channel: Channel<Args, Result, Config["optimisticHelpers"]>,467 channel: Channel<Args, Result, Config["optimisticHelpers"]>,
468 userOnRestore?: () => void,468 userOnRestore?: () => void,
469 fromRunWithOptions = false,
469 ): Promise<Result> {470 ): Promise<Result> {
470 // If there's a pending debounced call, roll it back471 // If there's a pending debounced call, roll it back
471 if (channel.pendingDebounced) {472 if (channel.pendingDebounced) {
...@@ -624,13 +625,14 @@ export class BlockingMutation<...@@ -624,13 +625,14 @@ export class BlockingMutation<
624 reject: wrapperReject,625 reject: wrapperReject,
625 } = Promise.withResolvers<Result>();626 } = Promise.withResolvers<Result>();
626627
627 // Add global handler logic for no-listener case628 // Resolve/reject pending promises and add global handler logic for execution-time checks
628 wrapperPromise.then(629 wrapperPromise.then(
629 (result) => {630 (result) => {
630 // Resolve all pending promises631 // Resolve all pending promises
631 pending.forEach((p) => p.resolve(result));632 pending.forEach((p) => p.resolve(result));
632 633
633 // If no listeners, call global success handler634 // Check if there are any listeners at execution time
635 const hasListeners = channel.listeners.size > 0;
634 if (!hasListeners) {636 if (!hasListeners) {
635 const message = this.describeResult(args, result);637 const message = this.describeResult(args, result);
636 if (message && this.#client.reportSuccess) {638 if (message && this.#client.reportSuccess) {
...@@ -642,7 +644,8 @@ export class BlockingMutation<...@@ -642,7 +644,8 @@ export class BlockingMutation<
642 // Reject all pending promises644 // Reject all pending promises
643 pending.forEach((p) => p.reject(error));645 pending.forEach((p) => p.reject(error));
644 646
645 // If no listeners, call global error handler647 // Check if there are any listeners at execution time
648 const hasListeners = channel.listeners.size > 0;
646 if (!hasListeners) {649 if (!hasListeners) {
647 const message = `Failed to ${this.describe(...args)}: ${650 const message = `Failed to ${this.describe(...args)}: ${
648 errMessage(error)651 errMessage(error)