From 005380d2610353262fa5acd170352dbbf3e4e6c2 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Thu, 29 Jan 2026 20:36:36 -0800 Subject: [PATCH] autofmt --- dprint.jsonc | 13 ++++ example/src/App.tsx | 7 +- jsr.json | 2 +- readme.md | 91 +++++++---------------- src/blocking.ts | 37 ++++----- src/client.ts | 14 ++-- src/debounced.ts | 22 ++---- src/mod.ts | 8 +- src/object-path.ts | 7 +- src/react.ts | 32 ++++---- src/tanstack-query.ts | 26 ++----- test/blocking-debounce-edge-cases.test.ts | 4 +- test/blocking.test.ts | 10 +-- test/debounced.test.ts | 2 +- test/object-path.test.ts | 4 +- test/object-path.types.ts | 19 ++--- test/react-button.test.tsx | 8 +- test/react.test.tsx | 8 +- test/tanstack-query-helpers.test.ts | 2 +- 19 files changed, 113 insertions(+), 203 deletions(-) create mode 100644 dprint.jsonc diff --git a/dprint.jsonc b/dprint.jsonc new file mode 100644 index 0000000000000000000000000000000000000000..38cbd41ba9bbee4341adcef11088b7d05940d62d --- /dev/null +++ b/dprint.jsonc @@ -0,0 +1,13 @@ +{ + "excludes": [ + "**/node_modules", + "**/*-lock.json", + ], + "plugins": [ + "https://plugins.dprint.dev/typescript-0.95.13.wasm", + "https://plugins.dprint.dev/json-0.21.1.wasm", + "https://plugins.dprint.dev/markdown-0.20.0.wasm", + "https://plugins.dprint.dev/g-plane/malva-v0.15.2.wasm", + "https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm", + ], +} diff --git a/example/src/App.tsx b/example/src/App.tsx index fe1b0ded221f42d302f84168ea70719870cc3038..8103ab197a7d227fa1e1bc2f5b63db26fe5208a6 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,10 +1,5 @@ +import { createMutationButton, MutationClient, queryClientOptimisticHelpers, useMutate } from "@clo/react-mutation"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { - createMutationButton, - MutationClient, - queryClientOptimisticHelpers, - useMutate, -} from "@clo/react-mutation"; import { queryOptions as queryOptions } from "@tanstack/react-query"; import { useSuspenseQuery } from "@tanstack/react-query"; import { QueryKeyAndFn } from "../../src/tanstack-query.ts"; diff --git a/jsr.json b/jsr.json index 1b4e5420fc5a0cd60bbf78e870097d1eb6a22ca7..1b9ddcaf6e684aaa2f420e292d895f2c049ffb62 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@clo/react-mutation", - "version": "1.0.0-beta.10", + "version": "1.0.0-beta.11", "exports": { ".": "./src/mod.ts", "./tanstack-query.ts": "./src/tanstack-query.ts", diff --git a/readme.md b/readme.md index acb7ca8fc939ea5c74aff3b3081fbfea18446941..f5a9618023d53cd3e5dd5214e04b38c86886a2fe 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ their mutation story falls apart, is confusing, and misses a few obvious features. Additionally, coworkers using AI agents continue to propagate bad patterns and verbose code that is hard to review. -The primary gains React Mutation provides are +The primary gains React Mutation provides are: - **Automatic result handling**. If a `useMutate` hook does not observe `isError`, unhandled errors will be propagated to a global handler, which can @@ -19,17 +19,20 @@ The primary gains React Mutation provides are UI without worrying about bugged error states. The [built in helpers for React Query](#react-query-optimistic-helpers) shows this power in more detail. -- Easy debouncing and batching utilities. +- Extra treats such as debouncing (toggle button spam) and no-op filters (auto-save text inputs). ## Setup React Mutation starts with a `MutationClient`, which shares global state for an application. ```ts -import { QueryClient } from "@tanstack/react-query"; -import { MutationClient } from "@clo/react-mutation"; -import { queryClientOptimisticHelpers, boundQueryClientGet } from "@clo/react-mutation"; import { showToastUI } from "..."; +import { MutationClient } from "@clo/react-mutation"; +import { + boundQueryClientGet, + queryClientOptimisticHelpers, +} from "@clo/react-mutation"; +import { QueryClient } from "@tanstack/react-query"; const queryClient = new QueryClient(); export const mutations = new MutationClient({ @@ -40,12 +43,12 @@ export const mutations = new MutationClient({ // (btw, the correctly typed version of `get` is exported as `boundQueryClientGet(client)`) get: (k: QueryKey) => client.getQueryData(k), }, - + // Optimistic helpers are a second type of context, only available within // optimistic update functions. These functions are bound to each mutation, // which means they can handle automatic rollbacks and query invalidation. getOptimisticHelpers: queryClientOptimisticHelpers(queryClient), - + // When call sites do not opt into handling errors, or a pending // mutation hook is unmounted, errors are sent to this function. // An example is to bind this to global a UI toast. @@ -53,7 +56,7 @@ export const mutations = new MutationClient({ showToastUI("error", userFriendlyErrorMessage); console.error(error); // or send to telemetry }, - + // Similarly, when call sites do opt into handling success. reportSuccess(userFriendlySuccessMessage: string) { showToastUI("success", userFriendlyErrorMessage); @@ -234,19 +237,18 @@ anything, `snapshot` can be used to detect no-op mutations. ```tsx const mutUpdateField = mutations.define({ - async mutate(id: string, value: string) { /* mutation */ }, - + async mutate(id: string, value: string) {/* mutation */}, + optimistic({ args: [id, value], helpers }) { helpers.objSet(queryItem(id), ["value"], value); }, - + // called once before `optimistic` and once after. if the values are equal, // then the mutation is cancelled (won't call `onSuccess`, but will `onSettled`) // (defaulting to a json-based deep equal check, customize in MutationClient) snapshot({ args: [id], get }) { return get(queryItem(id))?.value; - } - + }, // (...describe and optionally debounce stuff...) }); ``` @@ -328,56 +330,19 @@ It can now be used for easy mutations: ```tsx <> {/* Static Arguments */} - Follow + + Follow + {/* Dynamic Arguments */} - { - if (Math.random() < 0.5) e.preventDefault(); // prevent the submit - return [userId, messageContent]; - }}>Send Message - -``` - - - -## Batched Mutations - -This is an advanced feature. Complete Documentation is pending. It is not recommended to use this. - -Each call to the mutation applies new optimistic state on top of the previous, -and after a debounce / throttle, the new optimistic state is committed to the -API. UI never shows a pending state for these. This works great for toggle buttons -and any other state where you'd like to define an optimistic state - -In many places, similar behavior can be achieved with standard mutations and its -`debounceMs` field. - -```tsx -const mutToggleFollow = mutations.defineBatched({ - // Think of your mutator in terms of how it applies optimistic state. - optimistic({ helpers }, userId: string) { - helpers.objToggle(queryUser(userId), ["following"]); - }, - // A value is snapshotted *before* calling `optimistic`, and then again after - // the timer. If the snapshots differ, then `commit` function is called. - getValue: ({ get }) => get(queryUser(id))?.following, - - // Split different `id`s into their own batches. - key: ({ args: [id] }) => id, - - // Commit the result to the backend. - // Here, you can observe the two snapshotted values and form an API request. - async commit({ initial, current, args: [id] }) { - const response = await fetch(`/items/${id}`, { - method: "patch", - body: JSON.stringify({ title: current }), - }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - }, - - describe: ({ get, args: [id] }) => - `Rename '${get(queryItem())?.title ?? 'Unknown Item'}'`, - describeResult: ({ get, args: [id] }) => - `Renamed '${get(queryItem(id))?.title ?? 'Unknown Item'}'`, -}); + { + if (Math.random() < 0.5) e.preventDefault(); // prevent the submit + return [userId, messageContent]; + }} + > + Send Message + +; ``` diff --git a/src/blocking.ts b/src/blocking.ts index 0ca7e409325dcfe7cb8d408d1360e4cb68856f76..a125e8a6b6e1108d89691b03726a8155d3066eec 100644 --- a/src/blocking.ts +++ b/src/blocking.ts @@ -1,7 +1,7 @@ +import { message as errMessage } from "@clo/lib/error.ts"; import type { MutationClient, MutationClientFromConfig } from "./client.ts"; import type { MutationClientConfig } from "./client.ts"; import type { Mutation, MutationEvent, RunOptions } from "./types.ts"; -import { message as errMessage } from "@clo/lib/error.ts"; /** * Argument to `defineBlocking`. @@ -138,8 +138,8 @@ export class BlockingMutation< } key(args: Args) { - const k = this.#options.key?.({ ...this.#client.context, args }) ?? - "shared"; + const k = this.#options.key?.({ ...this.#client.context, args }) + ?? "shared"; return JSON.stringify(k); } @@ -263,9 +263,7 @@ export class BlockingMutation< // Call global handler unless suppressed if (!suppressGlobalError) { - const message = `Failed to ${this.describe(...args)}: ${ - errMessage(error) - }`; + const message = `Failed to ${this.describe(...args)}: ${errMessage(error)}`; this.#client.reportError(message, error); } }); @@ -354,8 +352,7 @@ export class BlockingMutation< expired = true; let next; while ( - next = - channel.rollbacks.splice(channel.rollbacks.length - rollbacks, 1)[0] + next = channel.rollbacks.splice(channel.rollbacks.length - rollbacks, 1)[0] ) { next(); } @@ -408,9 +405,7 @@ export class BlockingMutation< // Report any errors from refetch or callbacks results.forEach((result) => { if (result.status === "rejected") { - const message = `Failed to refetch after ${ - this.describe(...args) - }: ${errMessage(result.reason)}`; + const message = `Failed to refetch after ${this.describe(...args)}: ${errMessage(result.reason)}`; this.#client.reportError(message, result.reason); } }); @@ -447,9 +442,7 @@ export class BlockingMutation< // Report any errors from refetch or callbacks results.forEach((result) => { if (result.status === "rejected") { - const message = `Failed to refetch after ${ - this.describe(...args) - }: ${errMessage(result.reason)}`; + const message = `Failed to refetch after ${this.describe(...args)}: ${errMessage(result.reason)}`; this.#client.reportError(message, result.reason); } }); @@ -533,8 +526,7 @@ export class BlockingMutation< // Roll back the rollbacks we just added let next; while ( - next = - channel.rollbacks.splice(channel.rollbacks.length - rollbacks, 1)[0] + next = channel.rollbacks.splice(channel.rollbacks.length - rollbacks, 1)[0] ) { next(); } @@ -611,13 +603,12 @@ export class BlockingMutation< return; } - const { args, rollbackCount, pending, onSuccess } = - channel.pendingDebounced; + const { args, rollbackCount, pending, onSuccess } = channel.pendingDebounced; channel.pendingDebounced = null; // Check if there are any listeners at time of enqueue const hasListeners = channel.listeners.size > 0; - + // Create wrapper resolve/reject that resolves ALL pending promises const { promise: wrapperPromise, @@ -630,7 +621,7 @@ export class BlockingMutation< (result) => { // Resolve all pending promises pending.forEach((p) => p.resolve(result)); - + // Check if there are any listeners at execution time const hasListeners = channel.listeners.size > 0; if (!hasListeners) { @@ -643,13 +634,11 @@ export class BlockingMutation< (error) => { // Reject all pending promises pending.forEach((p) => p.reject(error)); - + // Check if there are any listeners at execution time const hasListeners = channel.listeners.size > 0; if (!hasListeners) { - const message = `Failed to ${this.describe(...args)}: ${ - errMessage(error) - }`; + const message = `Failed to ${this.describe(...args)}: ${errMessage(error)}`; this.#client.reportError(message, error); } }, diff --git a/src/client.ts b/src/client.ts index 48a6dbb1a099bb3833da6784c8d328f1c54fadee..968f7a26909956b60736fdd0666b79bd9607b60d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,8 +1,5 @@ -import { - DebouncedMutation, - type DebouncedMutationOptions, -} from "./debounced.ts"; import { BlockingMutation, type MutationOptions } from "./blocking.ts"; +import { DebouncedMutation, type DebouncedMutationOptions } from "./debounced.ts"; import type { Mutation } from "./types.ts"; export interface MutationClientConfig { @@ -10,11 +7,12 @@ export interface MutationClientConfig { optimisticHelpers: {}; } -export type MutationClientFromConfig = - MutationClient; +export type MutationClientFromConfig = MutationClient< + Config["context"], + Config["optimisticHelpers"] +>; -const defaultDeepEquals = (a: unknown, b: unknown): boolean => - JSON.stringify(a) === JSON.stringify(b); +const defaultDeepEquals = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b); export interface MutationClientOptions< Context extends object, diff --git a/src/debounced.ts b/src/debounced.ts index 22f3e1ecdc048683c795798f338edda11ae4a3b4..e2dfd4fa493d646607afe7bbf2934ef76740773d 100644 --- a/src/debounced.ts +++ b/src/debounced.ts @@ -1,7 +1,7 @@ +import { message as errMessage } from "@clo/lib/error.ts"; import type { MutationClient, MutationClientFromConfig } from "./client.ts"; import type { MutationClientConfig } from "./client.ts"; import type { Mutation, MutationEvent, RunOptions } from "./types.ts"; -import { message as errMessage } from "@clo/lib/error.ts"; export interface DebouncedMutationOptions< Args extends unknown[], @@ -288,9 +288,7 @@ export class DebouncedMutation< ); } this.#runAndReturn(args, true, undefined).catch((error) => { - const message = `Failed to ${this.describe(...args)}: ${ - errMessage(error) - }`; + const message = `Failed to ${this.describe(...args)}: ${errMessage(error)}`; this.#client.reportError(message, error); }); } @@ -321,9 +319,7 @@ export class DebouncedMutation< // Call global error handler unless suppressed if (!suppressGlobalError) { - const message = `Failed to ${this.describe(...args)}: ${ - errMessage(error) - }`; + const message = `Failed to ${this.describe(...args)}: ${errMessage(error)}`; this.#client.reportError(message, error); } }); @@ -525,9 +521,7 @@ export class DebouncedMutation< pendingItems.forEach(({ resolve }) => resolve(result)); // Report success globally if any of the pending items requested it - const shouldReportSuccess = pendingItems.some((item) => - item.reportSuccessGlobally - ); + const shouldReportSuccess = pendingItems.some((item) => item.reportSuccessGlobally); if (shouldReportSuccess) { const message = this.#describeResult( firstArgs, @@ -557,9 +551,7 @@ export class DebouncedMutation< // Report any errors from refetch or callbacks results.forEach((result) => { if (result.status === "rejected") { - const message = `Failed to refetch after ${ - this.describe(...firstArgs) - }: ${errMessage(result.reason)}`; + const message = `Failed to refetch after ${this.describe(...firstArgs)}: ${errMessage(result.reason)}`; this.#client.reportError(message, result.reason); } }); @@ -596,9 +588,7 @@ export class DebouncedMutation< // Report any errors from refetch or callbacks results.forEach((result) => { if (result.status === "rejected") { - const message = `Failed to refetch after ${ - this.describe(...firstArgs) - }: ${errMessage(result.reason)}`; + const message = `Failed to refetch after ${this.describe(...firstArgs)}: ${errMessage(result.reason)}`; this.#client.reportError(message, result.reason); } }); diff --git a/src/mod.ts b/src/mod.ts index 4a58684c55e252c62e57d3c11cec66e3c1530e7d..8ab5314ace755c914048145be1b57cbab8cb8f2a 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -1,16 +1,11 @@ export type { MutationOptions, OptimisticContext } from "./blocking.ts"; -export type { - DebouncedCommitContext, - DebouncedMutationOptions, - DebouncedOptimisticContext, -} from "./debounced.ts"; export { MutationClient, type MutationClientConfig, type MutationClientFromConfig, type MutationClientOptions, } from "./client.ts"; -export type { Mutation, MutationEvent } from "./types.ts"; +export type { DebouncedCommitContext, DebouncedMutationOptions, DebouncedOptimisticContext } from "./debounced.ts"; export { createMutationButton, type MutationButtonComponent, @@ -22,3 +17,4 @@ export { type UseMutateResultBase, type UseMutateSuccess, } from "./react.ts"; +export type { Mutation, MutationEvent } from "./types.ts"; diff --git a/src/object-path.ts b/src/object-path.ts index 436308f37559394d375551d394ca0d79ef3ccd9d..addfc355733c84962998c4cda3204d18912ba7a8 100644 --- a/src/object-path.ts +++ b/src/object-path.ts @@ -1,13 +1,12 @@ -export type AllObjectPaths = T extends - ReadonlyArray ? [] | [number, ...AllObjectPaths] +export type AllObjectPaths = T extends ReadonlyArray ? [] | [number, ...AllObjectPaths] : T extends object ? | { [K in keyof T]-?: [K, ...AllObjectPaths]; }[keyof T] | [] : []; -export type GetObjectPath = P extends - [infer K extends keyof T, ...infer Rest] ? GetObjectPath +export type GetObjectPath = P extends [infer K extends keyof T, ...infer Rest] + ? GetObjectPath : T; /** Get an object path property */ diff --git a/src/react.ts b/src/react.ts index aaf73159b18f7e394c43ea52bd7467449c190c33..fa7e10bf3f78745978d687e9008f4162d118a9fb 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,3 +1,4 @@ +import { message as errMessage } from "@clo/lib/error.ts"; import { type FC, type MouseEvent, @@ -7,9 +8,8 @@ import { useEffect, useState, } from "react"; -import { message as errMessage } from "@clo/lib/error.ts"; -import type { Mutation, RunOptions } from "./types.ts"; import { jsx } from "react/jsx-runtime"; +import type { Mutation, RunOptions } from "./types.ts"; /** * Subscribe to a mutation's status, as well as accessing a local `run` method. @@ -170,9 +170,7 @@ class Observer { if (!error) return undefined; const mutation = this.mutation; if (!mutation || !this.state.args) return errMessage(error); - return `Failed to ${mutation.describe(...this.state.args)}: ${ - errMessage(error) - }`; + return `Failed to ${mutation.describe(...this.state.args)}: ${errMessage(error)}`; } run(...args: Args) { @@ -212,8 +210,8 @@ class Observer { isPending: status === "mutating" || status === "refetching", isSuccess: hasResult && !hasError, isError: hasError, - isOptimisticData: status === "waiting" || status === "mutating" || - status === "refetching", + isOptimisticData: status === "waiting" || status === "mutating" + || status === "refetching", args: hasError || hasResult ? undefined : this.state.args, }); }, @@ -222,10 +220,10 @@ class Observer { // Use global error/success handling if this usage of the hook doesn't check for // errors or success. This makes it act pretty awesome in terms of defaults. // You don't have to worry about result UI, they'll surface exactly once. - const watchesError = this.watched.has("isError") || - this.watched.has("error") || this.watched.has("errorMessage"); - const watchesSuccess = this.watched.has("isSuccess") || - this.watched.has("result"); + const watchesError = this.watched.has("isError") + || this.watched.has("error") || this.watched.has("errorMessage"); + const watchesSuccess = this.watched.has("isSuccess") + || this.watched.has("result"); const promise = mutation.runAsPromise(...args) .then((result) => { if (!watchesSuccess && mutation.describeResult) { @@ -237,9 +235,7 @@ class Observer { }); promise.catch((err) => { if (!watchesError) { - const message = `Failed to ${mutation.describe(...args)}: ${ - errMessage(err) - }`; + const message = `Failed to ${mutation.describe(...args)}: ${errMessage(err)}`; mutation.client.reportError(message, err); } }); @@ -289,8 +285,8 @@ class Observer { isPending: status === "mutating" || status === "refetching", isSuccess: hasResult && !hasError, isError: hasError, - isOptimisticData: status === "waiting" || status === "mutating" || - status === "refetching", + isOptimisticData: status === "waiting" || status === "mutating" + || status === "refetching", args: hasError || hasResult ? undefined : this.state.args, }); }, @@ -429,9 +425,7 @@ export function createMutationButton( // back into unspecified generics. .bind(null, Component) as MutationButtonComponent; // react devtools loves display names - bound.displayName = `MutationButton[${ - Component.displayName ?? Component.name - }]`; + bound.displayName = `MutationButton[${Component.displayName ?? Component.name}]`; return bound; } diff --git a/src/tanstack-query.ts b/src/tanstack-query.ts index 1f7dfb4610661d1a316a5348c90b7b188dd760a3..953fe4a23db3c6a3162e6a1b4f1b46478270de47 100644 --- a/src/tanstack-query.ts +++ b/src/tanstack-query.ts @@ -1,16 +1,6 @@ -import { - QueryClient, - type QueryFunction, - type QueryKey, - type Updater, -} from "@tanstack/react-query"; -import { - type AllObjectPaths, - type GetObjectPath, - getPath, - setPath, -} from "./object-path.ts"; +import { QueryClient, type QueryFunction, type QueryKey, type Updater } from "@tanstack/react-query"; import type { OptimisticEvents } from "./client.ts"; +import { type AllObjectPaths, type GetObjectPath, getPath, setPath } from "./object-path.ts"; export type QueryKeyAndFn = { queryKey: Key; @@ -336,9 +326,7 @@ class TanstackQueryOptimisticHelpers { const { value: original, exists } = getPath(prev, path); if (!exists || !Array.isArray(original)) return; - const newArray = original.filter((item, index) => - !removeFilter(item, index) - ); + const newArray = original.filter((item, index) => !removeFilter(item, index)); this.#set( queryKey, (obj) => obj ? setPath(obj, path, newArray as any) : obj, @@ -405,9 +393,7 @@ class TanstackQueryOptimisticHelpers { const { value: original, exists } = getPath(prev, path); if (!exists || !Array.isArray(original)) return; - const newArray = original.map((item, index) => - filter(item, index) ? update(item) : item - ); + const newArray = original.map((item, index) => filter(item, index) ? update(item) : item); this.#set( queryKey, (obj) => obj ? setPath(obj, path, newArray as any) : obj, @@ -549,9 +535,7 @@ class TanstackQueryOptimisticHelpers { const prev = this.#get(queryKey); if (!prev || !Array.isArray(prev)) return; - const newArray = prev.map((item, index) => - (filter ? filter(item, index) : true) ? update(item) : item - ); + const newArray = prev.map((item, index) => (filter ? filter(item, index) : true) ? update(item) : item); this.#set(queryKey, newArray); this.#onRestore(() => { // TODO: splice items back in case original changed diff --git a/test/blocking-debounce-edge-cases.test.ts b/test/blocking-debounce-edge-cases.test.ts index a96180342efaa6c473b20297b6de94fded74b0c5..9a43bff674fee7f84e104e4c4bc844a4cdee5ad8 100644 --- a/test/blocking-debounce-edge-cases.test.ts +++ b/test/blocking-debounce-edge-cases.test.ts @@ -1,7 +1,7 @@ import { assertEquals, assertRejects } from "@std/assert"; +import { test } from "vitest"; import { MutationClient } from "../src/client.ts"; import type { MutationEvent } from "../src/types.ts"; -import { test } from "vitest"; // Helper to create a test mutation client function createTestClient() { @@ -116,7 +116,7 @@ test("BlockingMutation - debounce: mutation still executes after all listeners u // Mutation should have been called despite no listeners assertEquals(mutateCallCount, 1); - + // Global success handler should be called since no local listeners assertEquals(successes.length, 1); assertEquals(successes[0], "Successfully processed test"); diff --git a/test/blocking.test.ts b/test/blocking.test.ts index 1ecd03f51d187513dc92504333a48bff16fd8d0d..ed57358ae7b242daa46573a3f2cac042e870fe23 100644 --- a/test/blocking.test.ts +++ b/test/blocking.test.ts @@ -1,7 +1,7 @@ import { assertEquals, assertRejects } from "@std/assert"; +import { test } from "vitest"; import { MutationClient } from "../src/client.ts"; import type { MutationEvent } from "../src/types.ts"; -import { test } from "vitest"; // Helper to create a test mutation client function createTestClient() { @@ -731,9 +731,7 @@ test("BlockingMutation - notifies error on mutation failure", async () => { await assertRejects(() => mutation.runAsPromise("test")); // Should have error event - const errorEvents = tracker.events.filter((e) => - e.status === "mutating" && e.error - ); + const errorEvents = tracker.events.filter((e) => e.status === "mutating" && e.error); assertEquals(errorEvents.length > 0, true); assertEquals((errorEvents[0]?.error as Error).message, "mutation failed"); }); @@ -819,9 +817,7 @@ test("BlockingMutation - result is passed to notification on success", async () await delay(20); // Should have refetching event with result - const refetchingEvents = tracker.events.filter((e) => - e.status === "refetching" - ); + const refetchingEvents = tracker.events.filter((e) => e.status === "refetching"); assertEquals(refetchingEvents.length > 0, true); assertEquals(refetchingEvents[0]?.result, "result-test"); }); diff --git a/test/debounced.test.ts b/test/debounced.test.ts index 51789eedce8ed6d736b05a5a3a2a24afd4a6fe3e..c020d77833fc5b08917d2f97a7947dbfcdcfee0d 100644 --- a/test/debounced.test.ts +++ b/test/debounced.test.ts @@ -1,7 +1,7 @@ import { assertEquals, assertRejects } from "@std/assert"; +import { test } from "vitest"; import { MutationClient } from "../src/client.ts"; import type { MutationEvent } from "../src/types.ts"; -import { test } from "vitest"; // Shared test store for optimistic updates const testStore = new Map(); diff --git a/test/object-path.test.ts b/test/object-path.test.ts index e2bd46ddcd4eefbff795d499cc2afa969279d3a5..8fba1b09cf0f83214569b96ed87ed52d6e7ac90b 100644 --- a/test/object-path.test.ts +++ b/test/object-path.test.ts @@ -282,11 +282,11 @@ test("set - should share references for unchanged branches (structural sharing)" }; const result = setPath(obj, ["address", "city"], "LA"); - + // Changed path should have new references assertEquals(result === obj, false); // Root is new assertEquals(result.address === obj.address, false); // Address is new - + // Unchanged branches should share references assertEquals(result.hobbies === obj.hobbies, true); // Same reference assertEquals(result.nested === obj.nested, true); // Same reference diff --git a/test/object-path.types.ts b/test/object-path.types.ts index e6fa6989ee5a8615949999f224bf996ded32061e..0d272bf3834cce445a0abf151cd7c5e1d3bf8d66 100644 --- a/test/object-path.types.ts +++ b/test/object-path.types.ts @@ -7,9 +7,8 @@ import type { AllObjectPaths, GetObjectPath } from "../src/object-path.ts"; // Type testing utilities type Expect = T; -type Equal = (() => T extends X ? 1 : 2) extends - () => T extends Y ? 1 - : 2 ? true +type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 + : 2 ? true : false; type NotEqual = Equal extends true ? false : true; type IsAny = 0 extends 1 & T ? true : false; @@ -325,8 +324,7 @@ objSetCount(["count"], (n) => n + 1); type ArrayPushSignature< Data extends object, Path extends AllObjectPaths, -> = GetObjectPath extends readonly (infer T)[] - ? (path: Path, ...items: T[]) => void +> = GetObjectPath extends readonly (infer T)[] ? (path: Path, ...items: T[]) => void : never; // This should accept individual items, not arrays @@ -359,8 +357,7 @@ arrayRemoveTags(["tags"], (tag) => tag === "alpha"); type IncrementSignature< Data extends object, Path extends AllObjectPaths, -> = GetObjectPath extends number - ? (path: Path, amount?: number) => void +> = GetObjectPath extends number ? (path: Path, amount?: number) => void : never; declare const increment: IncrementSignature; @@ -398,10 +395,4 @@ type NoAnyTest4 = Expect< NotAny> >; -export type { - ArrayPushSignature, - ArrayRemoveSignature, - IncrementSignature, - ObjSetSignature, - ToggleSignature, -}; +export type { ArrayPushSignature, ArrayRemoveSignature, IncrementSignature, ObjSetSignature, ToggleSignature }; diff --git a/test/react-button.test.tsx b/test/react-button.test.tsx index c0bfa02aab25b0f533e17044a253e458d5d76af6..2db95cfdf819fe4be3dbda542c42c0627037c325 100644 --- a/test/react-button.test.tsx +++ b/test/react-button.test.tsx @@ -1,9 +1,9 @@ import { render, screen, waitFor } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; -import { describe, test, expect, vi } from "vitest"; -import { MutationClient } from "../src/client.ts"; -import { useMutate, createMutationButton } from "../src/react.ts"; import type { FC } from "react"; +import { describe, expect, test, vi } from "vitest"; +import { MutationClient } from "../src/client.ts"; +import { createMutationButton, useMutate } from "../src/react.ts"; // Helper to create a test mutation client function createTestClient() { @@ -632,7 +632,7 @@ describe("createMutationButton - Edge Cases", () => { expect(completedCount).toBeGreaterThan(0); expect(screen.getByTestId("pending-status").textContent).toBe("idle"); }, - { timeout: 200 } + { timeout: 200 }, ); // All clicks should be processed diff --git a/test/react.test.tsx b/test/react.test.tsx index 20b8a34118d6a44c061e0fcd1d4e419a1a5d3088..5ef485eaa1082da09389dcb247eb008ed95e6f0d 100644 --- a/test/react.test.tsx +++ b/test/react.test.tsx @@ -1,11 +1,11 @@ +import { assertEquals } from "@std/assert"; import { render, screen, waitFor } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; -import { assertEquals } from "@std/assert"; -import { describe, test, expect, beforeEach, vi } from "vitest"; +import { useState } from "react"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { MutationClient } from "../src/client.ts"; -import { useMutate, createMutationButton } from "../src/react.ts"; +import { createMutationButton, useMutate } from "../src/react.ts"; import type { Mutation } from "../src/types.ts"; -import { useState } from "react"; // Helper to create a test mutation client function createTestClient() { diff --git a/test/tanstack-query-helpers.test.ts b/test/tanstack-query-helpers.test.ts index 5599a99295c9f7f584380056c2a17772418cec1b..e4bff6b103aca0388fafd58b2c9bc4dba7a66312 100644 --- a/test/tanstack-query-helpers.test.ts +++ b/test/tanstack-query-helpers.test.ts @@ -1,6 +1,6 @@ import { assertEquals } from "@std/assert"; -import { test } from "vitest"; import { QueryClient, queryOptions } from "@tanstack/react-query"; +import { test } from "vitest"; import { queryClientOptimisticHelpers } from "../src/tanstack-query.ts"; interface TestData { -- 2.54.0