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