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 @@...@@ -1,6 +1,6 @@
1{1{
2 "name": "@clo/react-mutation",2 "name": "@clo/react-mutation",
3 "version": "1.0.0-beta.8",3 "version": "1.0.0-beta.9",
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",
src/blocking.ts+23-3
...@@ -237,12 +237,12 @@ export class BlockingMutation<...@@ -237,12 +237,12 @@ export class BlockingMutation<
237 }237 }
238238
239 const args = array.slice() as Args;239 const args = array.slice() as Args;
240 const { onSuccess, onSuccessDataOnly, onError, onSettled } = args240 const { onSuccess, onSuccessDataOnly, onError, onSettled, onRestore } = args
241 .pop() as RunOptions<Result>;241 .pop() as RunOptions<Result>;
242 const suppressGlobalSuccess = onSuccess !== undefined;242 const suppressGlobalSuccess = onSuccess !== undefined;
243 const suppressGlobalError = onError !== undefined;243 const suppressGlobalError = onError !== undefined;
244244
245 const promise = this.runAsPromise(...args);245 const promise = this.#runAsPromiseWithOptions(args, { onRestore });
246 promise.then((result) => {246 promise.then((result) => {
247 // Call user handlers247 // Call user handlers
248 onSuccess?.(result);248 onSuccess?.(result);
...@@ -275,6 +275,13 @@ export class BlockingMutation<...@@ -275,6 +275,13 @@ export class BlockingMutation<
275275
276 /** Calls the mutation, treating the errors as promise rejection. */276 /** Calls the mutation, treating the errors as promise rejection. */
277 runAsPromise(...args: Args): Promise<Result> {277 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> {
278 if (!this.#client.enabled) {285 if (!this.#client.enabled) {
279 throw new Error(286 throw new Error(
280 "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?",287 "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?",
...@@ -285,7 +292,7 @@ export class BlockingMutation<...@@ -285,7 +292,7 @@ export class BlockingMutation<
285292
286 // Check if debouncing is enabled293 // Check if debouncing is enabled
287 if (this.#options.debounceMs !== undefined) {294 if (this.#options.debounceMs !== undefined) {
288 return this.#runDebouncedAndReturn(args, key, channel);295 return this.#runDebouncedAndReturn(args, key, channel, userOnRestore);
289 }296 }
290297
291 // 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
...@@ -315,6 +322,12 @@ export class BlockingMutation<...@@ -315,6 +322,12 @@ export class BlockingMutation<
315 rollbacks += 1;322 rollbacks += 1;
316 };323 };
317324
325 // Register user's onRestore callback if provided
326 if (userOnRestore) {
327 channel.rollbacks.push(userOnRestore);
328 rollbacks += 1;
329 }
330
318 try {331 try {
319 this.#options.optimistic({332 this.#options.optimistic({
320 args,333 args,
...@@ -452,6 +465,7 @@ export class BlockingMutation<...@@ -452,6 +465,7 @@ export class BlockingMutation<
452 args: Args,465 args: Args,
453 key: string,466 key: string,
454 channel: Channel<Args, Result, Config["optimisticHelpers"]>,467 channel: Channel<Args, Result, Config["optimisticHelpers"]>,
468 userOnRestore?: () => void,
455 ): Promise<Result> {469 ): Promise<Result> {
456 // If there's a pending debounced call, roll it back470 // If there's a pending debounced call, roll it back
457 if (channel.pendingDebounced) {471 if (channel.pendingDebounced) {
...@@ -485,6 +499,12 @@ export class BlockingMutation<...@@ -485,6 +499,12 @@ export class BlockingMutation<
485 rollbacks += 1;499 rollbacks += 1;
486 };500 };
487501
502 // Register user's onRestore callback if provided
503 if (userOnRestore) {
504 channel.rollbacks.push(userOnRestore);
505 rollbacks += 1;
506 }
507
488 try {508 try {
489 this.#options.optimistic({509 this.#options.optimistic({
490 args,510 args,
src/debounced.ts+14-5
...@@ -285,7 +285,7 @@ export class DebouncedMutation<...@@ -285,7 +285,7 @@ export class DebouncedMutation<
285 "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?",285 "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?",
286 );286 );
287 }287 }
288 this.#runAndReturn(args, true).catch((error) => {288 this.#runAndReturn(args, true, undefined).catch((error) => {
289 const message = `Failed to ${this.describe(...args)}: ${289 const message = `Failed to ${this.describe(...args)}: ${
290 errMessage(error)290 errMessage(error)
291 }`;291 }`;
...@@ -300,12 +300,12 @@ export class DebouncedMutation<...@@ -300,12 +300,12 @@ export class DebouncedMutation<
300 );300 );
301 }301 }
302 const args = array.slice() as Args;302 const args = array.slice() as Args;
303 const { onSuccess, onSuccessDataOnly, onError, onSettled } = args303 const { onSuccess, onSuccessDataOnly, onError, onSettled, onRestore } = args
304 .pop() as RunOptions<Result>;304 .pop() as RunOptions<Result>;
305 const suppressGlobalSuccess = onSuccess !== undefined;305 const suppressGlobalSuccess = onSuccess !== undefined;
306 const suppressGlobalError = onError !== undefined;306 const suppressGlobalError = onError !== undefined;
307307
308 const promise = this.#runAndReturn(args, !suppressGlobalSuccess);308 const promise = this.#runAndReturn(args, !suppressGlobalSuccess, onRestore);
309309
310 promise.then((result) => {310 promise.then((result) => {
311 // Call user handlers311 // Call user handlers
...@@ -334,10 +334,14 @@ export class DebouncedMutation<...@@ -334,10 +334,14 @@ export class DebouncedMutation<
334 "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?",334 "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?",
335 );335 );
336 }336 }
337 return this.#runAndReturn(args, false);337 return this.#runAndReturn(args, false, undefined);
338 }338 }
339339
340 #runAndReturn(args: Args, reportSuccessGlobally: boolean): Promise<Result> {340 #runAndReturn(
341 args: Args,
342 reportSuccessGlobally: boolean,
343 userOnRestore?: () => void,
344 ): Promise<Result> {
341 const key = this.key(args);345 const key = this.key(args);
342 const channel = this.#getOrPutChannel(key);346 const channel = this.#getOrPutChannel(key);
343347
...@@ -374,6 +378,11 @@ export class DebouncedMutation<...@@ -374,6 +378,11 @@ export class DebouncedMutation<
374 channel.rollbacks.push(cb);378 channel.rollbacks.push(cb);
375 };379 };
376380
381 // Register user's onRestore callback if provided
382 if (userOnRestore) {
383 channel.rollbacks.push(userOnRestore);
384 }
385
377 try {386 try {
378 this.#options.optimistic(387 this.#options.optimistic(
379 {388 {
src/react.ts+24-18
...@@ -45,6 +45,7 @@ export interface UseMutateResultBase<Args extends unknown[], Result> {...@@ -45,6 +45,7 @@ export interface UseMutateResultBase<Args extends unknown[], Result> {
45 ..._: [...args: Args, options: RunOptions<Result>]45 ..._: [...args: Args, options: RunOptions<Result>]
46 ) => Promise<Result>;46 ) => Promise<Result>;
47 clear: () => void;47 clear: () => void;
48 setError: (error: unknown) => void;
48 args: Args | undefined;49 args: Args | undefined;
49}50}
5051
...@@ -98,7 +99,7 @@ export interface UseMutateIdle {...@@ -98,7 +99,7 @@ export interface UseMutateIdle {
98 isOptimisticData: boolean;99 isOptimisticData: boolean;
99}100}
100101
101type AnyMutationStateWithoutRun<Result> =102type AnyMutationStateWithoutRun<Args extends unknown[], Result> =
102 & Omit<103 & Omit<
103 UseMutateIdle,104 UseMutateIdle,
104 "status" | "result" | "error" | "isSuccess" | "isError" | "errorMessage"105 "status" | "result" | "error" | "isSuccess" | "isError" | "errorMessage"
...@@ -110,10 +111,11 @@ type AnyMutationStateWithoutRun<Result> =...@@ -110,10 +111,11 @@ type AnyMutationStateWithoutRun<Result> =
110 errorMessage: undefined | string;111 errorMessage: undefined | string;
111 isSuccess: boolean;112 isSuccess: boolean;
112 isError: boolean;113 isError: boolean;
114 args: Args | undefined;
113 };115 };
114116
115export type AnyMutationState<Args extends unknown[], Result> =117export type AnyMutationState<Args extends unknown[], Result> =
116 & AnyMutationStateWithoutRun<Result>118 & AnyMutationStateWithoutRun<Args, Result>
117 & UseMutateResultBase<Args, Result>;119 & UseMutateResultBase<Args, Result>;
118120
119function initialState() {121function initialState() {
...@@ -127,6 +129,7 @@ function initialState() {...@@ -127,6 +129,7 @@ function initialState() {
127 isSuccess: false,129 isSuccess: false,
128 isError: false,130 isError: false,
129 isOptimisticData: false,131 isOptimisticData: false,
132 args: undefined,
130 } as const;133 } as const;
131}134}
132135
...@@ -135,15 +138,14 @@ class Observer<Args extends unknown[], Result> {...@@ -135,15 +138,14 @@ class Observer<Args extends unknown[], Result> {
135 mutation: Mutation<Args, Result> | null = null;138 mutation: Mutation<Args, Result> | null = null;
136 unsubscribe: (() => void) | null = null;139 unsubscribe: (() => void) | null = null;
137 currentKey: string | null = null;140 currentKey: string | null = null;
138 currentArgs: Args | null = null;
139141
140 constructor(setRerender: (fn: number) => void) {142 constructor(setRerender: (fn: number) => void) {
141 this.setRerender = setRerender;143 this.setRerender = setRerender;
142 }144 }
143145
144 watched: Set<string> = new Set();146 watched: Set<string> = new Set();
145 state: AnyMutationStateWithoutRun<Result> = initialState();147 state: AnyMutationStateWithoutRun<Args, Result> = initialState();
146 setState(newState: Partial<AnyMutationStateWithoutRun<Result>>) {148 setState(newState: Partial<AnyMutationStateWithoutRun<Args, Result>>) {
147 let updateUi = false;149 let updateUi = false;
148 const current: Record<string, unknown> = this.state;150 const current: Record<string, unknown> = this.state;
149 for (const [key, value] of Object.entries(newState)) {151 for (const [key, value] of Object.entries(newState)) {
...@@ -167,8 +169,8 @@ class Observer<Args extends unknown[], Result> {...@@ -167,8 +169,8 @@ class Observer<Args extends unknown[], Result> {
167 computeErrorMessage(error: unknown): string | undefined {169 computeErrorMessage(error: unknown): string | undefined {
168 if (!error) return undefined;170 if (!error) return undefined;
169 const mutation = this.mutation;171 const mutation = this.mutation;
170 if (!mutation || !this.currentArgs) return errMessage(error);172 if (!mutation || !this.state.args) return errMessage(error);
171 return `Failed to ${mutation.describe(...this.currentArgs)}: ${173 return `Failed to ${mutation.describe(...this.state.args)}: ${
172 errMessage(error)174 errMessage(error)
173 }`;175 }`;
174 }176 }
...@@ -176,11 +178,7 @@ class Observer<Args extends unknown[], Result> {...@@ -176,11 +178,7 @@ class Observer<Args extends unknown[], Result> {
176 run(...args: Args) {178 run(...args: Args) {
177 const mutation = this.mutation;179 const mutation = this.mutation;
178 if (!mutation) return;180 if (!mutation) return;
179 const argsChanged = this.currentArgs !== args;181 this.setState({ args });
180 this.currentArgs = args;
181 if (argsChanged && this.watched.has("args")) {
182 this.setRerender(Math.random());
183 }
184 const key = mutation.key(args);182 const key = mutation.key(args);
185 if (key !== this.currentKey) {183 if (key !== this.currentKey) {
186 this.currentKey = key;184 this.currentKey = key;
...@@ -216,6 +214,7 @@ class Observer<Args extends unknown[], Result> {...@@ -216,6 +214,7 @@ class Observer<Args extends unknown[], Result> {
216 isError: hasError,214 isError: hasError,
217 isOptimisticData: status === "waiting" || status === "mutating" ||215 isOptimisticData: status === "waiting" || status === "mutating" ||
218 status === "refetching",216 status === "refetching",
217 args: hasError || hasResult ? undefined : this.state.args,
219 });218 });
220 },219 },
221 );220 );
...@@ -254,11 +253,7 @@ class Observer<Args extends unknown[], Result> {...@@ -254,11 +253,7 @@ class Observer<Args extends unknown[], Result> {
254 const args = array.slice() as Args;253 const args = array.slice() as Args;
255 const options = args.pop() as RunOptions<Result>;254 const options = args.pop() as RunOptions<Result>;
256255
257 const argsChanged = this.currentArgs !== args;256 this.setState({ args });
258 this.currentArgs = args;
259 if (argsChanged && this.watched.has("args")) {
260 this.setRerender(Math.random());
261 }
262 const key = mutation.key(args);257 const key = mutation.key(args);
263258
264 // Set up subscription if key changed259 // Set up subscription if key changed
...@@ -296,6 +291,7 @@ class Observer<Args extends unknown[], Result> {...@@ -296,6 +291,7 @@ class Observer<Args extends unknown[], Result> {
296 isError: hasError,291 isError: hasError,
297 isOptimisticData: status === "waiting" || status === "mutating" ||292 isOptimisticData: status === "waiting" || status === "mutating" ||
298 status === "refetching",293 status === "refetching",
294 args: hasError || hasResult ? undefined : this.state.args,
299 });295 });
300 },296 },
301 );297 );
...@@ -320,6 +316,16 @@ class Observer<Args extends unknown[], Result> {...@@ -320,6 +316,16 @@ class Observer<Args extends unknown[], Result> {
320 result: undefined,316 result: undefined,
321 });317 });
322 },318 },
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 },
323 get status() {329 get status() {
324 self.watched.add("status");330 self.watched.add("status");
325 return self.state.status;331 return self.state.status;
...@@ -358,7 +364,7 @@ class Observer<Args extends unknown[], Result> {...@@ -358,7 +364,7 @@ class Observer<Args extends unknown[], Result> {
358 },364 },
359 get args() {365 get args() {
360 self.watched.add("args");366 self.watched.add("args");
361 return self.currentArgs ?? undefined;367 return self.state.args;
362 },368 },
363 } as UseMutateResult<Args, Result>))(this);369 } as UseMutateResult<Args, Result>))(this);
364}370}
src/types.ts+2
...@@ -33,6 +33,8 @@ export interface RunOptions<Result> {...@@ -33,6 +33,8 @@ export interface RunOptions<Result> {
33 | { status: "success"; result: Result }33 | { status: "success"; result: Result }
34 | { status: "error"; error: unknown },34 | { status: "error"; error: unknown },
35 ) => void;35 ) => void;
36 /** Called when optimistic state is being restored/rolled back */
37 onRestore?: () => void;
36}38}
3739
38export interface MutationEvent<Result> {40export interface MutationEvent<Result> {