authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-29 17:22:15-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-29 22:05:19-08:00
logf33b11a3c0e6206b106b2cc655632d14ea2650c6
tree1c69bc4109f33148dccab6459071a6804dd8a134
parentd5c11b294ef2257d31778609caddf9980e24e6f6
signaturelock-open Commit is signed but in an unrecognized format.

chore: some more stuff


5 files changed, 64 insertions(+), 27 deletions(-)

jsr.json+1-1
......@@ -1,6 +1,6 @@
11{
22 "name": "@clo/react-mutation",
3 "version": "1.0.0-beta.8",
3 "version": "1.0.0-beta.9",
44 "exports": {
55 ".": "./src/mod.ts",
66 "./tanstack-query.ts": "./src/tanstack-query.ts",
src/blocking.ts+23-3
......@@ -237,12 +237,12 @@ export class BlockingMutation<
237237 }
238238
239239 const args = array.slice() as Args;
240 const { onSuccess, onSuccessDataOnly, onError, onSettled } = args
240 const { onSuccess, onSuccessDataOnly, onError, onSettled, onRestore } = args
241241 .pop() as RunOptions<Result>;
242242 const suppressGlobalSuccess = onSuccess !== undefined;
243243 const suppressGlobalError = onError !== undefined;
244244
245 const promise = this.runAsPromise(...args);
245 const promise = this.#runAsPromiseWithOptions(args, { onRestore });
246246 promise.then((result) => {
247247 // Call user handlers
248248 onSuccess?.(result);
......@@ -275,6 +275,13 @@ export class BlockingMutation<
275275
276276 /** Calls the mutation, treating the errors as promise rejection. */
277277 runAsPromise(...args: Args): Promise<Result> {
278 return this.#runAsPromiseWithOptions(args, {});
279 }
280
281 #runAsPromiseWithOptions(
282 args: Args,
283 { onRestore: userOnRestore }: Pick<RunOptions<Result>, "onRestore">,
284 ): Promise<Result> {
278285 if (!this.#client.enabled) {
279286 throw new Error(
280287 "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?",
......@@ -285,7 +292,7 @@ export class BlockingMutation<
285292
286293 // Check if debouncing is enabled
287294 if (this.#options.debounceMs !== undefined) {
288 return this.#runDebouncedAndReturn(args, key, channel);
295 return this.#runDebouncedAndReturn(args, key, channel, userOnRestore);
289296 }
290297
291298 // Create shared optimistic helpers instance for the channel if it doesn't exist
......@@ -315,6 +322,12 @@ export class BlockingMutation<
315322 rollbacks += 1;
316323 };
317324
325 // Register user's onRestore callback if provided
326 if (userOnRestore) {
327 channel.rollbacks.push(userOnRestore);
328 rollbacks += 1;
329 }
330
318331 try {
319332 this.#options.optimistic({
320333 args,
......@@ -452,6 +465,7 @@ export class BlockingMutation<
452465 args: Args,
453466 key: string,
454467 channel: Channel<Args, Result, Config["optimisticHelpers"]>,
468 userOnRestore?: () => void,
455469 ): Promise<Result> {
456470 // If there's a pending debounced call, roll it back
457471 if (channel.pendingDebounced) {
......@@ -485,6 +499,12 @@ export class BlockingMutation<
485499 rollbacks += 1;
486500 };
487501
502 // Register user's onRestore callback if provided
503 if (userOnRestore) {
504 channel.rollbacks.push(userOnRestore);
505 rollbacks += 1;
506 }
507
488508 try {
489509 this.#options.optimistic({
490510 args,
src/debounced.ts+14-5
......@@ -285,7 +285,7 @@ export class DebouncedMutation<
285285 "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?",
286286 );
287287 }
288 this.#runAndReturn(args, true).catch((error) => {
288 this.#runAndReturn(args, true, undefined).catch((error) => {
289289 const message = `Failed to ${this.describe(...args)}: ${
290290 errMessage(error)
291291 }`;
......@@ -300,12 +300,12 @@ export class DebouncedMutation<
300300 );
301301 }
302302 const args = array.slice() as Args;
303 const { onSuccess, onSuccessDataOnly, onError, onSettled } = args
303 const { onSuccess, onSuccessDataOnly, onError, onSettled, onRestore } = args
304304 .pop() as RunOptions<Result>;
305305 const suppressGlobalSuccess = onSuccess !== undefined;
306306 const suppressGlobalError = onError !== undefined;
307307
308 const promise = this.#runAndReturn(args, !suppressGlobalSuccess);
308 const promise = this.#runAndReturn(args, !suppressGlobalSuccess, onRestore);
309309
310310 promise.then((result) => {
311311 // Call user handlers
......@@ -334,10 +334,14 @@ export class DebouncedMutation<
334334 "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?",
335335 );
336336 }
337 return this.#runAndReturn(args, false);
337 return this.#runAndReturn(args, false, undefined);
338338 }
339339
340 #runAndReturn(args: Args, reportSuccessGlobally: boolean): Promise<Result> {
340 #runAndReturn(
341 args: Args,
342 reportSuccessGlobally: boolean,
343 userOnRestore?: () => void,
344 ): Promise<Result> {
341345 const key = this.key(args);
342346 const channel = this.#getOrPutChannel(key);
343347
......@@ -374,6 +378,11 @@ export class DebouncedMutation<
374378 channel.rollbacks.push(cb);
375379 };
376380
381 // Register user's onRestore callback if provided
382 if (userOnRestore) {
383 channel.rollbacks.push(userOnRestore);
384 }
385
377386 try {
378387 this.#options.optimistic(
379388 {
src/react.ts+24-18
......@@ -45,6 +45,7 @@ export interface UseMutateResultBase<Args extends unknown[], Result> {
4545 ..._: [...args: Args, options: RunOptions<Result>]
4646 ) => Promise<Result>;
4747 clear: () => void;
48 setError: (error: unknown) => void;
4849 args: Args | undefined;
4950}
5051
......@@ -98,7 +99,7 @@ export interface UseMutateIdle {
9899 isOptimisticData: boolean;
99100}
100101
101type AnyMutationStateWithoutRun<Result> =
102type AnyMutationStateWithoutRun<Args extends unknown[], Result> =
102103 & Omit<
103104 UseMutateIdle,
104105 "status" | "result" | "error" | "isSuccess" | "isError" | "errorMessage"
......@@ -110,10 +111,11 @@ type AnyMutationStateWithoutRun<Result> =
110111 errorMessage: undefined | string;
111112 isSuccess: boolean;
112113 isError: boolean;
114 args: Args | undefined;
113115 };
114116
115117export type AnyMutationState<Args extends unknown[], Result> =
116 & AnyMutationStateWithoutRun<Result>
118 & AnyMutationStateWithoutRun<Args, Result>
117119 & UseMutateResultBase<Args, Result>;
118120
119121function initialState() {
......@@ -127,6 +129,7 @@ function initialState() {
127129 isSuccess: false,
128130 isError: false,
129131 isOptimisticData: false,
132 args: undefined,
130133 } as const;
131134}
132135
......@@ -135,15 +138,14 @@ class Observer<Args extends unknown[], Result> {
135138 mutation: Mutation<Args, Result> | null = null;
136139 unsubscribe: (() => void) | null = null;
137140 currentKey: string | null = null;
138 currentArgs: Args | null = null;
139141
140142 constructor(setRerender: (fn: number) => void) {
141143 this.setRerender = setRerender;
142144 }
143145
144146 watched: Set<string> = new Set();
145 state: AnyMutationStateWithoutRun<Result> = initialState();
146 setState(newState: Partial<AnyMutationStateWithoutRun<Result>>) {
147 state: AnyMutationStateWithoutRun<Args, Result> = initialState();
148 setState(newState: Partial<AnyMutationStateWithoutRun<Args, Result>>) {
147149 let updateUi = false;
148150 const current: Record<string, unknown> = this.state;
149151 for (const [key, value] of Object.entries(newState)) {
......@@ -167,8 +169,8 @@ class Observer<Args extends unknown[], Result> {
167169 computeErrorMessage(error: unknown): string | undefined {
168170 if (!error) return undefined;
169171 const mutation = this.mutation;
170 if (!mutation || !this.currentArgs) return errMessage(error);
171 return `Failed to ${mutation.describe(...this.currentArgs)}: ${
172 if (!mutation || !this.state.args) return errMessage(error);
173 return `Failed to ${mutation.describe(...this.state.args)}: ${
172174 errMessage(error)
173175 }`;
174176 }
......@@ -176,11 +178,7 @@ class Observer<Args extends unknown[], Result> {
176178 run(...args: Args) {
177179 const mutation = this.mutation;
178180 if (!mutation) return;
179 const argsChanged = this.currentArgs !== args;
180 this.currentArgs = args;
181 if (argsChanged && this.watched.has("args")) {
182 this.setRerender(Math.random());
183 }
181 this.setState({ args });
184182 const key = mutation.key(args);
185183 if (key !== this.currentKey) {
186184 this.currentKey = key;
......@@ -216,6 +214,7 @@ class Observer<Args extends unknown[], Result> {
216214 isError: hasError,
217215 isOptimisticData: status === "waiting" || status === "mutating" ||
218216 status === "refetching",
217 args: hasError || hasResult ? undefined : this.state.args,
219218 });
220219 },
221220 );
......@@ -254,11 +253,7 @@ class Observer<Args extends unknown[], Result> {
254253 const args = array.slice() as Args;
255254 const options = args.pop() as RunOptions<Result>;
256255
257 const argsChanged = this.currentArgs !== args;
258 this.currentArgs = args;
259 if (argsChanged && this.watched.has("args")) {
260 this.setRerender(Math.random());
261 }
256 this.setState({ args });
262257 const key = mutation.key(args);
263258
264259 // Set up subscription if key changed
......@@ -296,6 +291,7 @@ class Observer<Args extends unknown[], Result> {
296291 isError: hasError,
297292 isOptimisticData: status === "waiting" || status === "mutating" ||
298293 status === "refetching",
294 args: hasError || hasResult ? undefined : this.state.args,
299295 });
300296 },
301297 );
......@@ -320,6 +316,16 @@ class Observer<Args extends unknown[], Result> {
320316 result: undefined,
321317 });
322318 },
319 setError(error: unknown) {
320 self.setState({
321 status: "error",
322 error,
323 errorMessage: errMessage(error),
324 isError: true,
325 isSuccess: false,
326 result: undefined,
327 });
328 },
323329 get status() {
324330 self.watched.add("status");
325331 return self.state.status;
......@@ -358,7 +364,7 @@ class Observer<Args extends unknown[], Result> {
358364 },
359365 get args() {
360366 self.watched.add("args");
361 return self.currentArgs ?? undefined;
367 return self.state.args;
362368 },
363369 } as UseMutateResult<Args, Result>))(this);
364370}
src/types.ts+2
......@@ -33,6 +33,8 @@ export interface RunOptions<Result> {
3333 | { status: "success"; result: Result }
3434 | { status: "error"; error: unknown },
3535 ) => void;
36 /** Called when optimistic state is being restored/rolled back */
37 onRestore?: () => void;
3638}
3739
3840export interface MutationEvent<Result> {