| author | |
| committer | |
| log | 331a3e7e7454101d1933ba3d28996ad554a0bf6a |
| tree | 8160eeedbf1b0891674630fb694e639fed662619 |
| parent | de7e1ab0f88b02444249d69a59c5fed7c981b6c9 |
| signature |
13 files changed, 132 insertions(+), 66 deletions(-)
jsr.json+1-1| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "@clo/react-mutation", |
| 3 | "version": "1.0.0-beta.1", | |
| 3 | "version": "1.0.0-beta.2", | |
| 4 | 4 | "exports": { |
| 5 | 5 | ".": "./src/mod.ts", |
| 6 | 6 | "./tanstack-query.ts": "./src/tanstack-query.ts", |
package-lock.json+9-2| ... | ... | @@ -1,14 +1,15 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "@clo/react-mutation", |
| 3 | "version": "0.0.0", | |
| 3 | "version": "0.1.0", | |
| 4 | 4 | "lockfileVersion": 3, |
| 5 | 5 | "requires": true, |
| 6 | 6 | "packages": { |
| 7 | 7 | "": { |
| 8 | 8 | "name": "@clo/react-mutation", |
| 9 | "version": "0.0.0", | |
| 9 | "version": "0.1.0", | |
| 10 | 10 | "license": "ISC", |
| 11 | 11 | "dependencies": { |
| 12 | "@clo/lib": "npm:@jsr/clo__lib@^3.0.0", | |
| 12 | 13 | "@std/assert": "npm:@jsr/std__assert@^1.0.17" |
| 13 | 14 | }, |
| 14 | 15 | "devDependencies": { |
| ... | ... | @@ -315,6 +316,12 @@ |
| 315 | 316 | "node": ">=6.9.0" |
| 316 | 317 | } |
| 317 | 318 | }, |
| 319 | "node_modules/@clo/lib": { | |
| 320 | "name": "@jsr/clo__lib", | |
| 321 | "version": "3.0.0", | |
| 322 | "resolved": "https://npm.jsr.io/~/11/@jsr/clo__lib/3.0.0.tgz", | |
| 323 | "integrity": "sha512-oseZwHCAcXNPbqnGZ37l7+wAoj6ikIXE1VM0s6eD6fz4DcgM030Slf0T7Lgtn7fIdas5hlfx4JF54TR+vo4THw==" | |
| 324 | }, | |
| 318 | 325 | "node_modules/@esbuild/aix-ppc64": { |
| 319 | 326 | "version": "0.27.2", |
| 320 | 327 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", |
package.json+1| ... | ... | @@ -13,6 +13,7 @@ |
| 13 | 13 | "check": "tsc --noEmit" |
| 14 | 14 | }, |
| 15 | 15 | "dependencies": { |
| 16 | "@clo/lib": "npm:@jsr/clo__lib@^3.0.0", | |
| 16 | 17 | "@std/assert": "npm:@jsr/std__assert@^1.0.17" |
| 17 | 18 | }, |
| 18 | 19 | "devDependencies": { |
src/batch.ts+14-5| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | import type { MutationClient, MutationClientFromConfig } from "./client.ts"; |
| 2 | 2 | import type { MutationClientConfig } from "./client.ts"; |
| 3 | 3 | import type { Mutation, MutationEvent } from "./types.ts"; |
| 4 | import { message as errMessage } from "@clo/lib/error.ts"; | |
| 4 | 5 | |
| 5 | 6 | export interface BatchMutationOptions< |
| 6 | 7 | Args extends unknown[], |
| ... | ... | @@ -124,6 +125,7 @@ export class BatchMutation< |
| 124 | 125 | #options: BatchMutationOptions<Args, Result, Optimistic, Config>; |
| 125 | 126 | #client: MutationClientFromConfig<Config>; |
| 126 | 127 | #channels: Map<string, BatchChannel<Args, Result, Optimistic>> = new Map(); |
| 128 | client: MutationClientFromConfig<Config>; | |
| 127 | 129 | |
| 128 | 130 | constructor( |
| 129 | 131 | client: MutationClient<Config["context"], Config["optimisticHelpers"]>, |
| ... | ... | @@ -131,6 +133,7 @@ export class BatchMutation< |
| 131 | 133 | ) { |
| 132 | 134 | this.#options = options; |
| 133 | 135 | this.#client = client; |
| 136 | this.client = client; | |
| 134 | 137 | } |
| 135 | 138 | |
| 136 | 139 | key(args: Args): string { |
| ... | ... | @@ -212,7 +215,10 @@ export class BatchMutation< |
| 212 | 215 | return describe; |
| 213 | 216 | } |
| 214 | 217 | |
| 215 | describeResult(args: Args, initial: Optimistic, current: Optimistic, result: Result): string | undefined { | |
| 218 | // Not available for batched mutations - success reporting happens during commit | |
| 219 | describeResult: undefined = undefined; | |
| 220 | ||
| 221 | #describeResult(args: Args, initial: Optimistic, current: Optimistic, result: Result): string | undefined { | |
| 216 | 222 | const { describeResult } = this.#options; |
| 217 | 223 | if (describeResult === null || describeResult === undefined) return undefined; |
| 218 | 224 | return typeof describeResult === "function" |
| ... | ... | @@ -229,7 +235,8 @@ export class BatchMutation< |
| 229 | 235 | /** Calling the mutation in a global scope. Errors and successes are turned into UI toasts. */ |
| 230 | 236 | run(...args: Args): void { |
| 231 | 237 | this.#runAndReturn(args, true).catch((error) => { |
| 232 | this.#client.reportError(error); | |
| 238 | const message = `Failed to ${this.describe(...args)}: ${errMessage(error)}`; | |
| 239 | this.#client.reportError(message, error); | |
| 233 | 240 | }); |
| 234 | 241 | } |
| 235 | 242 | |
| ... | ... | @@ -400,7 +407,7 @@ export class BatchMutation< |
| 400 | 407 | // Report success globally if any of the pending items requested it |
| 401 | 408 | const shouldReportSuccess = pendingItems.some((item) => item.reportSuccessGlobally); |
| 402 | 409 | if (shouldReportSuccess) { |
| 403 | const message = this.describeResult(firstArgs, initial, current, result); | |
| 410 | const message = this.#describeResult(firstArgs, initial, current, result); | |
| 404 | 411 | if (message && this.#client.reportSuccess) { |
| 405 | 412 | this.#client.reportSuccess(message); |
| 406 | 413 | } |
| ... | ... | @@ -420,7 +427,8 @@ export class BatchMutation< |
| 420 | 427 | // Report any errors from refetch or callbacks |
| 421 | 428 | results.forEach((result) => { |
| 422 | 429 | if (result.status === "rejected") { |
| 423 | this.#client.reportError(result.reason); | |
| 430 | const message = `Failed to refetch after ${this.describe(...firstArgs)}: ${errMessage(result.reason)}`; | |
| 431 | this.#client.reportError(message, result.reason); | |
| 424 | 432 | } |
| 425 | 433 | }); |
| 426 | 434 | }).finally(() => { |
| ... | ... | @@ -458,7 +466,8 @@ export class BatchMutation< |
| 458 | 466 | // Report any errors from refetch or callbacks |
| 459 | 467 | results.forEach((result) => { |
| 460 | 468 | if (result.status === "rejected") { |
| 461 | this.#client.reportError(result.reason); | |
| 469 | const message = `Failed to refetch after ${this.describe(...firstArgs)}: ${errMessage(result.reason)}`; | |
| 470 | this.#client.reportError(message, result.reason); | |
| 462 | 471 | } |
| 463 | 472 | }); |
| 464 | 473 | }).finally(() => { |
src/client.ts+2-2| ... | ... | @@ -21,7 +21,7 @@ export interface MutationClientOptions< |
| 21 | 21 | getOptimisticHelpers: ( |
| 22 | 22 | events: OptimisticEvents, |
| 23 | 23 | ) => OptimisticHelpers; |
| 24 | reportError: (error: unknown) => void; | |
| 24 | reportError: (message: string, error: unknown) => void; | |
| 25 | 25 | reportSuccess?: (message: string) => void; |
| 26 | 26 | /** |
| 27 | 27 | * Compare two values for deep equality. Used by BatchMutation to determine |
| ... | ... | @@ -42,7 +42,7 @@ export class MutationClient< |
| 42 | 42 | > { |
| 43 | 43 | context: Context; |
| 44 | 44 | getOptimisticHelpers: (event: OptimisticEvents) => OptimisticHelpers; |
| 45 | reportError: (error: unknown) => void; | |
| 45 | reportError: (message: string, error: unknown) => void; | |
| 46 | 46 | reportSuccess?: (message: string) => void; |
| 47 | 47 | deepEquals: (a: unknown, b: unknown) => boolean; |
| 48 | 48 |
src/queued.ts+9-3| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | import type { MutationClient, MutationClientFromConfig } from "./client.ts"; |
| 2 | 2 | import type { MutationClientConfig } from "./client.ts"; |
| 3 | 3 | import type { Mutation, MutationEvent } from "./types.ts"; |
| 4 | import { message as errMessage } from "@clo/lib/error.ts"; | |
| 4 | 5 | |
| 5 | 6 | /** |
| 6 | 7 | * Argument to `defineMutation`. |
| ... | ... | @@ -96,6 +97,7 @@ export class QueuedMutation< |
| 96 | 97 | #options: MutationOptions<Args, Result, Config>; |
| 97 | 98 | #client: MutationClientFromConfig<Config>; |
| 98 | 99 | #queues: Map<string, Channel<Args, Result>> = new Map(); |
| 100 | client: MutationClientFromConfig<Config>; | |
| 99 | 101 | |
| 100 | 102 | constructor( |
| 101 | 103 | client: MutationClient<Config["context"], Config["optimisticHelpers"]>, |
| ... | ... | @@ -103,6 +105,7 @@ export class QueuedMutation< |
| 103 | 105 | ) { |
| 104 | 106 | this.#options = options; |
| 105 | 107 | this.#client = client; |
| 108 | this.client = client; | |
| 106 | 109 | } |
| 107 | 110 | |
| 108 | 111 | key(args: Args) { |
| ... | ... | @@ -180,7 +183,8 @@ export class QueuedMutation< |
| 180 | 183 | this.#client.reportSuccess(message); |
| 181 | 184 | } |
| 182 | 185 | }).catch((error) => { |
| 183 | this.#client.reportError(error); | |
| 186 | const message = `Failed to ${this.describe(...args)}: ${errMessage(error)}`; | |
| 187 | this.#client.reportError(message, error); | |
| 184 | 188 | }); |
| 185 | 189 | } |
| 186 | 190 | |
| ... | ... | @@ -287,7 +291,8 @@ export class QueuedMutation< |
| 287 | 291 | // Report any errors from refetch or callbacks |
| 288 | 292 | results.forEach((result) => { |
| 289 | 293 | if (result.status === "rejected") { |
| 290 | this.#client.reportError(result.reason); | |
| 294 | const message = `Failed to refetch after ${this.describe(...args)}: ${errMessage(result.reason)}`; | |
| 295 | this.#client.reportError(message, result.reason); | |
| 291 | 296 | } |
| 292 | 297 | }); |
| 293 | 298 | }).finally(() => { |
| ... | ... | @@ -328,7 +333,8 @@ export class QueuedMutation< |
| 328 | 333 | // Report any errors from refetch or callbacks |
| 329 | 334 | results.forEach((result) => { |
| 330 | 335 | if (result.status === "rejected") { |
| 331 | this.#client.reportError(result.reason); | |
| 336 | const message = `Failed to refetch after ${this.describe(...args)}: ${errMessage(result.reason)}`; | |
| 337 | this.#client.reportError(message, result.reason); | |
| 332 | 338 | } |
| 333 | 339 | }); |
| 334 | 340 | }).finally(() => { |
src/react.tsx+45-12| ... | ... | @@ -7,6 +7,7 @@ import { |
| 7 | 7 | useEffect, |
| 8 | 8 | useState, |
| 9 | 9 | } from "react"; |
| 10 | import { message as errMessage } from "@clo/lib/error.ts"; | |
| 10 | 11 | import type { Mutation } from "./types.ts"; |
| 11 | 12 | |
| 12 | 13 | /** |
| ... | ... | @@ -46,6 +47,7 @@ export interface UseMutateSuccess<Result> { |
| 46 | 47 | status: "success"; |
| 47 | 48 | result: Result; |
| 48 | 49 | error: undefined; |
| 50 | errorMessage: undefined; | |
| 49 | 51 | /** `true` when a `mutate` function is currently running. */ |
| 50 | 52 | isMutating: false; |
| 51 | 53 | /** `true` when a loading indicator should be shown. */ |
| ... | ... | @@ -61,6 +63,8 @@ export interface UseMutateError { |
| 61 | 63 | status: "error"; |
| 62 | 64 | result: undefined; |
| 63 | 65 | error: unknown; |
| 66 | /** User-friendly in this format: `Failed to {action}: {details}` */ | |
| 67 | errorMessage: string; | |
| 64 | 68 | /** `true` when a `mutate` function is currently running. */ |
| 65 | 69 | isMutating: false; |
| 66 | 70 | /** `true` when a loading indicator should be shown. */ |
| ... | ... | @@ -76,6 +80,7 @@ export interface UseMutateIdle { |
| 76 | 80 | status: "idle" | "mutating"; |
| 77 | 81 | result: undefined; |
| 78 | 82 | error: undefined; |
| 83 | errorMessage: undefined; | |
| 79 | 84 | /** `true` when a `mutate` function is currently running. */ |
| 80 | 85 | isMutating: boolean; |
| 81 | 86 | /** `true` when a loading indicator should be shown. */ |
| ... | ... | @@ -91,12 +96,13 @@ export interface UseMutateIdle { |
| 91 | 96 | type AnyMutationState<Result> = |
| 92 | 97 | & Omit< |
| 93 | 98 | UseMutateIdle, |
| 94 | "status" | "result" | "error" | "isSuccess" | "isError" | |
| 99 | "status" | "result" | "error" | "isSuccess" | "isError" | "errorMessage" | |
| 95 | 100 | > |
| 96 | 101 | & { |
| 97 | 102 | status: "idle" | "mutating" | "error" | "success"; |
| 98 | 103 | result: undefined | Result; |
| 99 | 104 | error: undefined | unknown; |
| 105 | errorMessage: undefined | string; | |
| 100 | 106 | isSuccess: boolean; |
| 101 | 107 | isError: boolean; |
| 102 | 108 | }; |
| ... | ... | @@ -106,6 +112,7 @@ function initialState() { |
| 106 | 112 | status: "idle", |
| 107 | 113 | result: undefined, |
| 108 | 114 | error: undefined, |
| 115 | errorMessage: undefined, | |
| 109 | 116 | isMutating: false, |
| 110 | 117 | isPending: false, |
| 111 | 118 | isSuccess: false, |
| ... | ... | @@ -119,6 +126,7 @@ class Observer<Args extends unknown[], Result> { |
| 119 | 126 | mutation: Mutation<Args, Result> | null = null; |
| 120 | 127 | unsubscribe: (() => void) | null = null; |
| 121 | 128 | currentKey: string | null = null; |
| 129 | currentArgs: Args | null = null; | |
| 122 | 130 | |
| 123 | 131 | constructor(setRerender: (fn: number) => void) { |
| 124 | 132 | this.setRerender = setRerender; |
| ... | ... | @@ -147,10 +155,20 @@ class Observer<Args extends unknown[], Result> { |
| 147 | 155 | this.state = initialState(); |
| 148 | 156 | } |
| 149 | 157 | |
| 158 | computeErrorMessage(error: unknown): string | undefined { | |
| 159 | if (!error) return undefined; | |
| 160 | const mutation = this.mutation; | |
| 161 | if (!mutation || !this.currentArgs) return errMessage(error); | |
| 162 | return `Failed to ${mutation.describe(...this.currentArgs)}: ${ | |
| 163 | errMessage(error) | |
| 164 | }`; | |
| 165 | } | |
| 166 | ||
| 150 | 167 | binding: UseMutateResult<Args, Result> = ((self: this) => ({ |
| 151 | 168 | run(...args: Args) { |
| 152 | 169 | const mutation = self.mutation; |
| 153 | 170 | if (!mutation) return; |
| 171 | self.currentArgs = args; | |
| 154 | 172 | const key = mutation.key(args); |
| 155 | 173 | if (key !== self.currentKey) { |
| 156 | 174 | self.currentKey = key; |
| ... | ... | @@ -178,6 +196,7 @@ class Observer<Args extends unknown[], Result> { |
| 178 | 196 | ? "mutating" |
| 179 | 197 | : "idle", |
| 180 | 198 | error: error ?? undefined, |
| 199 | errorMessage: self.computeErrorMessage(error ?? undefined), | |
| 181 | 200 | result: result ?? undefined, |
| 182 | 201 | isMutating: status === "mutating", |
| 183 | 202 | isPending: status === "mutating" || status === "refetching", |
| ... | ... | @@ -189,19 +208,28 @@ class Observer<Args extends unknown[], Result> { |
| 189 | 208 | }, |
| 190 | 209 | ); |
| 191 | 210 | } |
| 192 | // use global error/success handling if this usage of the hook doesn't check for | |
| 211 | // Use global error/success handling if this usage of the hook doesn't check for | |
| 193 | 212 | // errors or success. This makes it act pretty awesome in terms of defaults. |
| 194 | // You don't have to worry about the errors/successes, they'll surface exactly once. | |
| 195 | if ( | |
| 196 | self.watched.has("isError") || self.watched.has("error") || | |
| 197 | self.watched.has("isSuccess") || self.watched.has("result") | |
| 198 | ) { | |
| 199 | mutation.runAndReturn(...args).catch(() => { | |
| 200 | // caught in event listener | |
| 213 | // You don't have to worry about result UI, they'll surface exactly once. | |
| 214 | const watchesError = self.watched.has("isError") || | |
| 215 | self.watched.has("error") || self.watched.has("errorMessage"); | |
| 216 | const watchesSuccess = self.watched.has("isSuccess") || | |
| 217 | self.watched.has("result"); | |
| 218 | mutation.runAndReturn(...args) | |
| 219 | .then((result) => { | |
| 220 | if (!watchesSuccess && mutation.describeResult) { | |
| 221 | const message = mutation.describeResult(args, result); | |
| 222 | if (message && mutation.client.reportSuccess) { | |
| 223 | mutation.client.reportSuccess(message); | |
| 224 | } | |
| 225 | } | |
| 226 | }) | |
| 227 | .catch((err) => { | |
| 228 | if (!watchesError) { | |
| 229 | const message = `Failed to ${mutation.describe(...args)}: ${errMessage(err)}`; | |
| 230 | mutation.client.reportError(message, err); | |
| 231 | } | |
| 201 | 232 | }); |
| 202 | } else { | |
| 203 | mutation.run(...args); | |
| 204 | } | |
| 205 | 233 | }, |
| 206 | 234 | clear() { |
| 207 | 235 | self.setState({ |
| ... | ... | @@ -211,6 +239,7 @@ class Observer<Args extends unknown[], Result> { |
| 211 | 239 | isError: false, |
| 212 | 240 | isSuccess: false, |
| 213 | 241 | error: undefined, |
| 242 | errorMessage: undefined, | |
| 214 | 243 | result: undefined, |
| 215 | 244 | }); |
| 216 | 245 | }, |
| ... | ... | @@ -226,6 +255,10 @@ class Observer<Args extends unknown[], Result> { |
| 226 | 255 | self.watched.add("error"); |
| 227 | 256 | return self.state.error; |
| 228 | 257 | }, |
| 258 | get errorMessage() { | |
| 259 | self.watched.add("errorMessage"); | |
| 260 | return self.state.errorMessage; | |
| 261 | }, | |
| 229 | 262 | get isMutating() { |
| 230 | 263 | self.watched.add("isMutating"); |
| 231 | 264 | return self.state.isMutating; |
src/tanstack-query.ts+25-21| ... | ... | @@ -307,7 +307,7 @@ class TanstackQueryOptimisticHelpers { |
| 307 | 307 | } |
| 308 | 308 | |
| 309 | 309 | /** |
| 310 | * Remove items from an array that match a predicate. | |
| 310 | * Remove items from an array that match `filter`. | |
| 311 | 311 | * If the query or path doesn't exist, the updater is skipped. |
| 312 | 312 | */ |
| 313 | 313 | objArrayRemove< |
| ... | ... | @@ -316,7 +316,7 @@ class TanstackQueryOptimisticHelpers { |
| 316 | 316 | >( |
| 317 | 317 | queryKey: QueryKeyAndFn<Data>, |
| 318 | 318 | path: Path, |
| 319 | predicate: ( | |
| 319 | filter: ( | |
| 320 | 320 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, |
| 321 | 321 | index: number, |
| 322 | 322 | ) => boolean, |
| ... | ... | @@ -326,7 +326,7 @@ class TanstackQueryOptimisticHelpers { |
| 326 | 326 | const { value: original, exists } = getPath(prev, path); |
| 327 | 327 | if (!exists || !Array.isArray(original)) return; |
| 328 | 328 | |
| 329 | const newArray = original.filter((item, index) => !predicate(item, index)); | |
| 329 | const newArray = original.filter((item, index) => !filter(item, index)); | |
| 330 | 330 | this.#set( |
| 331 | 331 | queryKey, |
| 332 | 332 | (obj) => obj ? setPath(obj, path, newArray as any) : obj, |
| ... | ... | @@ -347,13 +347,15 @@ class TanstackQueryOptimisticHelpers { |
| 347 | 347 | >( |
| 348 | 348 | queryKey: QueryKeyAndFn<Data>, |
| 349 | 349 | path: Path, |
| 350 | predicate: ( | |
| 351 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, | |
| 352 | index: number, | |
| 353 | ) => boolean, | |
| 354 | updater: ( | |
| 355 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, | |
| 356 | ) => GetObjectPath<Data, Path> extends Array<infer T> ? T : never, | |
| 350 | { filter, update }: { | |
| 351 | filter: ( | |
| 352 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, | |
| 353 | index: number, | |
| 354 | ) => boolean; | |
| 355 | update: ( | |
| 356 | item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never, | |
| 357 | ) => GetObjectPath<Data, Path> extends Array<infer T> ? T : never; | |
| 358 | }, | |
| 357 | 359 | ) { |
| 358 | 360 | const prev = this.#get(queryKey); |
| 359 | 361 | if (!prev) return; |
| ... | ... | @@ -361,7 +363,7 @@ class TanstackQueryOptimisticHelpers { |
| 361 | 363 | if (!exists || !Array.isArray(original)) return; |
| 362 | 364 | |
| 363 | 365 | const newArray = original.map((item, index) => |
| 364 | predicate(item, index) ? updater(item) : item | |
| 366 | filter(item, index) ? update(item) : item | |
| 365 | 367 | ); |
| 366 | 368 | this.#set( |
| 367 | 369 | queryKey, |
| ... | ... | @@ -444,12 +446,12 @@ class TanstackQueryOptimisticHelpers { |
| 444 | 446 | } |
| 445 | 447 | |
| 446 | 448 | /** |
| 447 | * Remove items from an array that match a predicate. | |
| 449 | * Remove items from an array that match a `filter`. | |
| 448 | 450 | * If the query or path doesn't exist, the updater is skipped. |
| 449 | 451 | */ |
| 450 | 452 | arrayRemove<Data>( |
| 451 | 453 | queryKey: QueryKeyAndFn<Data[]>, |
| 452 | predicate: ( | |
| 454 | filter: ( | |
| 453 | 455 | item: Data, |
| 454 | 456 | index: number, |
| 455 | 457 | ) => boolean, |
| ... | ... | @@ -457,7 +459,7 @@ class TanstackQueryOptimisticHelpers { |
| 457 | 459 | const prev = this.#get(queryKey); |
| 458 | 460 | if (!prev || !Array.isArray(prev)) return; |
| 459 | 461 | |
| 460 | const newArray = prev.filter((item, index) => !predicate(item, index)); | |
| 462 | const newArray = prev.filter((item, index) => !filter(item, index)); | |
| 461 | 463 | this.#set(queryKey, newArray); |
| 462 | 464 | this.#onRestore(() => { |
| 463 | 465 | // TODO: splice items back in case original changed |
| ... | ... | @@ -466,22 +468,24 @@ class TanstackQueryOptimisticHelpers { |
| 466 | 468 | } |
| 467 | 469 | |
| 468 | 470 | /** |
| 469 | * Update items in an array that match a predicate. | |
| 471 | * Update items in an array that match a `filter`. | |
| 470 | 472 | * If the query or path doesn't exist, the updater is skipped. |
| 471 | 473 | */ |
| 472 | 474 | arrayUpdate<Data>( |
| 473 | 475 | queryKey: QueryKeyAndFn<Data[]>, |
| 474 | predicate: ( | |
| 475 | item: Data, | |
| 476 | index: number, | |
| 477 | ) => boolean, | |
| 478 | updater: (item: Data) => Data, | |
| 476 | { | |
| 477 | filter, | |
| 478 | update, | |
| 479 | }: { | |
| 480 | filter?: (item: Data, index: number) => boolean; | |
| 481 | update: (item: Data) => Data; | |
| 482 | }, | |
| 479 | 483 | ) { |
| 480 | 484 | const prev = this.#get(queryKey); |
| 481 | 485 | if (!prev || !Array.isArray(prev)) return; |
| 482 | 486 | |
| 483 | 487 | const newArray = prev.map((item, index) => |
| 484 | predicate(item, index) ? updater(item) : item | |
| 488 | (filter ? filter(item, index) : true) ? update(item) : item | |
| 485 | 489 | ); |
| 486 | 490 | this.#set(queryKey, newArray); |
| 487 | 491 | this.#onRestore(() => { |
src/types.ts+4| ... | ... | @@ -1,3 +1,5 @@ |
| 1 | import type { MutationClient } from "./client.ts"; | |
| 2 | ||
| 1 | 3 | export interface Mutation<Args extends unknown[], Result> { |
| 2 | 4 | /** Calling the mutation. Errors are turned into UI toasts. */ |
| 3 | 5 | run(...args: Args): void; |
| ... | ... | @@ -12,6 +14,8 @@ export interface Mutation<Args extends unknown[], Result> { |
| 12 | 14 | cb: (update: MutationEvent<Result>) => void, |
| 13 | 15 | ): () => void; |
| 14 | 16 | describe(...args: Args): string; |
| 17 | describeResult?: (args: Args, result: Result) => string | undefined; | |
| 18 | client: MutationClient<object, object>; | |
| 15 | 19 | } |
| 16 | 20 | |
| 17 | 21 | export interface MutationEvent<Result> { |
test/batch.test.ts+3-3| ... | ... | @@ -31,7 +31,7 @@ function createTestClient() { |
| 31 | 31 | }, |
| 32 | 32 | }; |
| 33 | 33 | }, |
| 34 | reportError(error) { | |
| 34 | reportError(message, error) { | |
| 35 | 35 | errors.push(error); |
| 36 | 36 | }, |
| 37 | 37 | }); |
| ... | ... | @@ -472,7 +472,7 @@ test("BatchMutation - uses deepEquals for comparison", async () => { |
| 472 | 472 | }, |
| 473 | 473 | }; |
| 474 | 474 | }, |
| 475 | reportError(error) { | |
| 475 | reportError(message, error) { | |
| 476 | 476 | errors.push(error); |
| 477 | 477 | }, |
| 478 | 478 | }); |
| ... | ... | @@ -518,7 +518,7 @@ test("BatchMutation - custom deepEquals function", async () => { |
| 518 | 518 | }, |
| 519 | 519 | }; |
| 520 | 520 | }, |
| 521 | reportError(error) { | |
| 521 | reportError(message, error) { | |
| 522 | 522 | errors.push(error); |
| 523 | 523 | }, |
| 524 | 524 | deepEquals(a, b) { |
test/object-path-types.test.ts+6-10| ... | ... | @@ -3,16 +3,13 @@ |
| 3 | 3 | * These tests verify that TypeScript types work correctly at compile time |
| 4 | 4 | */ |
| 5 | 5 | |
| 6 | import type { | |
| 7 | AllObjectPaths, | |
| 8 | GetObjectPath, | |
| 9 | } from "../src/object-path.ts"; | |
| 6 | import type { AllObjectPaths, GetObjectPath } from "../src/object-path.ts"; | |
| 10 | 7 | |
| 11 | 8 | // Type testing utilities |
| 12 | 9 | type Expect<T extends true> = T; |
| 13 | type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y | |
| 14 | ? 1 | |
| 15 | : 2 ? true | |
| 10 | type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends | |
| 11 | <T>() => T extends Y ? 1 | |
| 12 | : 2 ? true | |
| 16 | 13 | : false; |
| 17 | 14 | type NotEqual<X, Y> = Equal<X, Y> extends true ? false : true; |
| 18 | 15 | type IsAny<T> = 0 extends 1 & T ? true : false; |
| ... | ... | @@ -348,7 +345,7 @@ type ArrayRemoveSignature< |
| 348 | 345 | Data extends object, |
| 349 | 346 | Path extends AllObjectPaths<Data>, |
| 350 | 347 | > = GetObjectPath<Data, Path> extends readonly (infer T)[] |
| 351 | ? (path: Path, predicate: (item: T, index: number) => boolean) => void | |
| 348 | ? (path: Path, filter: (item: T, index: number) => boolean) => void | |
| 352 | 349 | : never; |
| 353 | 350 | |
| 354 | 351 | declare const arrayRemoveItems: ArrayRemoveSignature<TestData, ["items"]>; |
| ... | ... | @@ -379,8 +376,7 @@ type IncrementNameTest = Expect< |
| 379 | 376 | type ToggleSignature< |
| 380 | 377 | Data extends object, |
| 381 | 378 | Path extends AllObjectPaths<Data>, |
| 382 | > = GetObjectPath<Data, Path> extends boolean | |
| 383 | ? (path: Path) => void | |
| 379 | > = GetObjectPath<Data, Path> extends boolean ? (path: Path) => void | |
| 384 | 380 | : never; |
| 385 | 381 | |
| 386 | 382 | declare const toggle: ToggleSignature<TestData, ["active"]>; |
test/queued.test.ts+1-1| ... | ... | @@ -16,7 +16,7 @@ function createTestClient() { |
| 16 | 16 | }, |
| 17 | 17 | }; |
| 18 | 18 | }, |
| 19 | reportError(error) { | |
| 19 | reportError(message, error) { | |
| 20 | 20 | errors.push(error); |
| 21 | 21 | }, |
| 22 | 22 | }); |
test/tanstack-query-helpers.test.ts+12-6| ... | ... | @@ -586,8 +586,10 @@ test("arrayUpdateItem - should update items matching predicate", () => { |
| 586 | 586 | helpers.objArrayUpdate( |
| 587 | 587 | queryTest, |
| 588 | 588 | ["items"], |
| 589 | (item) => item.id === 2, | |
| 590 | (item) => ({ ...item, label: "UPDATED" }), | |
| 589 | { | |
| 590 | filter: (item) => item.id === 2, | |
| 591 | update: (item) => ({ ...item, label: "UPDATED" }), | |
| 592 | }, | |
| 591 | 593 | ); |
| 592 | 594 | |
| 593 | 595 | const result = client.getQueryData<TestData>(queryTest.queryKey); |
| ... | ... | @@ -611,8 +613,10 @@ test("arrayUpdateItem - should update multiple items", () => { |
| 611 | 613 | helpers.objArrayUpdate( |
| 612 | 614 | queryTest, |
| 613 | 615 | ["items"], |
| 614 | (item) => item.id > 1, | |
| 615 | (item) => ({ ...item, label: item.label.toUpperCase() }), | |
| 616 | { | |
| 617 | filter: (item) => item.id > 1, | |
| 618 | update: (item) => ({ ...item, label: item.label.toUpperCase() }), | |
| 619 | }, | |
| 616 | 620 | ); |
| 617 | 621 | |
| 618 | 622 | const result = client.getQueryData<TestData>(queryTest.queryKey); |
| ... | ... | @@ -632,8 +636,10 @@ test("arrayUpdateItem - predicate receives index", () => { |
| 632 | 636 | helpers.objArrayUpdate( |
| 633 | 637 | queryTest, |
| 634 | 638 | ["items"], |
| 635 | (_item, index) => index === 0, | |
| 636 | (item) => ({ ...item, label: "FIRST" }), | |
| 639 | { | |
| 640 | filter: (_item, index) => index === 0, | |
| 641 | update: (item) => ({ ...item, label: "FIRST" }), | |
| 642 | }, | |
| 637 | 643 | ); |
| 638 | 644 | |
| 639 | 645 | const result = client.getQueryData<TestData>(queryTest.queryKey); |