diff --git a/readme.md b/readme.md
index a2e4a3fd5cf6057c4332cfb493a34c64085bdf30..f86612a2c934f2ae0b5f7ffa0a95361d64e7bf6a 100644
--- a/readme.md
+++ b/readme.md
@@ -10,7 +10,7 @@ patterns and verbose code that is hard to review.
The primary gains React Mutation provides are
-- **Automatic error handling**. If a `useMutation` hook does not observe
+- **Automatic error handling**. If a `useMutate` hook does not observe
`isError`, unhandled errors will be propagated to a global handler, which can
display a UI toast. Otherwise, the component can display the error locally.
- Optimistic helpers allow defining rollbacks and refetching logic independant
@@ -22,13 +22,13 @@ The primary gains React Mutation provides are
This library declares two kinds of mutations. Each kind has different behavior
around concurrent operations.
-- [**Queued Mutations**](#Queued-Mutations): A mutation blocks the UI until it
+- [**Blocking Mutations**](#Blocking-Mutations): A mutation blocks the UI until it
is complete. You press a button, a pending state appears, then it completes.
This works great for forms, creations and deletions, and is similar to React
Query's mutation system.
-- [**Batched Mutations**](#Batched-Mutations): Each call to the mutation applies
- new optimistic state, and after a debounce or throttle, the new optimistic
- state is committed to the API. UI never shows a pending state for batches.
+- [**Debounced Mutations**](#Debounced-Mutations): Each call to the mutation applies
+ new optimistic state, and after a debounce (or throttle) the new optimistic
+ state is committed to the API. UI never shows a pending state for these.
This works great for auto-saving input fields, follow buttons, and is
preferred whenever possible.
@@ -37,7 +37,7 @@ React Mutation starts with a `MutationClient`, which shares global state for an
```ts
const queryClient = new QueryClient();
export const mutations = new MutationClient({
- // All properties in `context` are available within mutation functions.
+ // All properties in `context` are available within every function.
context: {
client: queryClient,
// Can add any easy helpers for your codebase.
@@ -45,29 +45,40 @@ export const mutations = new MutationClient({
get: (k: QueryKey) => client.getQueryData(k),
},
- // Optimistic helpers are a second type of context, only available
- // within optimistic update functions. The built in React Query helpers
- // add many query cache mutating operations that automatically
+ // 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.
- reportError(description: string, error: unknown) {
- console.error("Mutation error:", error);
+ reportError(userFriendlyErrorMessage: string, error: unknown) {
+ 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);
},
})
```
-### Queued Mutations
+### Blocking Mutations
-A queued mutation is defined with `mutations.defineQueued`.
+A blocking mutation is defined with `mutations.defineBlocking`. Example use cases:
+
+- A form to create a new resource.
+- Button operations such as deleting or resyncing.
+- Any case where it is unclear what the optimistic state should be.
```tsx
const queryItemList = queryOptions({ ... });
const queryItem = (id: string) => queryOptions({ ... });
+// The convention is to name handlers starting with `mut`
const mutDeleteItem = mutations.defineQueued({
// `mutate` comes first, is only worried about syncing with the backend.
async mutate(id: string) {
@@ -76,23 +87,31 @@ const mutDeleteItem = mutations.defineQueued({
},
optimistic({ client, get, helpers, args: [id] }) {
+ // Remove the matching items, but restore and refetch them on failure.
helpers.arrayRemove(queryItemList, (item) => item === id);
+ // Remove this query from the client, but restore as stale and refetch it on failure.
helpers.removeQuery(queryItem);
},
+ // Example: `Could not {description}`
describe({ get, args: [id] }) {
const title = get(queryItem().queryKey)?.title ?? "Unknown Item";
- return `delete '${title}'`;
+ return `Delete '${get(queryItem(id))?.title ?? 'unknown'}'`;
},
+ // Example: `Successfully {description}`
+ describeResult: ({ get, args: [id] }) =>
+ `Deleted '${get(queryItem(id))?.title ?? 'Unknown Item'}'`,
- // since the optimistic handler is perfect, there is no need
+ // Since the optimistic handler is perfect, there is no need
// to refetch any data once a success case is hit.
refetchOnSuccess: false,
});
+// React example. Since `error` and `result` are not destructed, messages are
+// indicated through UI toasts from the mutation client.
export function Example({ id }: { id: string }) {
const { data: list } = useSuspenseQuery(queryItemList);
- const { run } = useMutation(mutDeleteItem);
+ const { run } = useMutate(mutDeleteItem);
return list.map((id) =>
@@ -101,27 +120,25 @@ export function Example({ id }: { id: string }) {
}
```
-### Batched Mutations
+### Debounced Mutations
-A batched mutation is defined with `mutations.defineBatched`.
+A debounced mutation is defined with `mutations.defineBatched`.
```tsx
-const mutSetItemName = mutationClient.defineBatched({
- mode: "debounce",
- time: 200,
-
- // start by mutating the optimistic state
+const mutSetItemName = mutationClient.defineDebounced({
+ // Think of your mutator in terms of how it applies optimistic state.
optimistic({ helpers }, id: string, name: string) {
helpers.objSet(queryItem(id), ["title"], name);
},
- // a value is snapshot before calling `optimistic` and after the
- // timer. if the snapshots differ, the `commit` function is called.
+ // 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(queryCounter)?.title ?? "",
- // batch the same `id`s together
+ // Split different `id`s into their own debounces.
key: ({ args: [id] }) => id,
- // commit the result to the backend
+ // 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",
@@ -130,6 +147,35 @@ const mutSetItemName = mutationClient.defineBatched({
if (!response.ok) throw new Error(`HTTP ${response.status}`);
},
- describe: ({ get }) => `rename '${get(queryItem())?.title ?? 'unknown'}'`,
+ describe: ({ get, args: [id] }) =>
+ `Rename '${get(queryItem())?.title ?? 'Unknown Item'}'`,
+ describeResult: ({ get, args: [id] }) =>
+ `Renamed '${get(queryItem(id))?.title ?? 'Unknown Item'}'`,
});
+
+// React example. Since the error and result are read in this hook,
+// the success and failure states will be driven through the component UI.
+function Item({ id }: { id: string }) {
+ const { data: item } = useSuspenseQuery(queryItem(id));
+ const { run, isSuccess, errorMessage } = useMutate(mutDeleteItem);
+
+ // TODO: test this pattern. maybe introduce another hook for doing good input
+ // fields that hook could also support an "Undo" button.
+ return <>
+ {
+ run(e.target.value);
+ }}
+ />
+ {
+ isSuccess
+ ? "Saved"
+ : errorMessage
+ ? "Error: " + errorMessage : null
+ }
+ >
+}
```
+
+###
diff --git a/src/batch.ts b/src/batch.ts
index 5f3feaced24afdde2e8dfd028a6d1af433db807a..50c47a8cb6f2ff4892eef134ab193ca87161676b 100644
--- a/src/batch.ts
+++ b/src/batch.ts
@@ -23,7 +23,10 @@ export interface BatchMutationOptions<
*/
getValue: (context: Config["context"], ...args: Args) => Optimistic;
- mode: "debounce" | "throttle";
+ /**
+ * @default "debounce"
+ */
+ mode?: "debounce" | "throttle";
/**
* Milliseconds
* @default 200
@@ -50,6 +53,17 @@ export interface BatchMutationOptions<
| ((
context: BatchCommitContext, Optimistic, Config>,
) => string);
+ /**
+ * Used in success messages.
+ * Phrase it as a complete success message, e.g., "Renamed item successfully"
+ * Set to null to suppress success reporting.
+ */
+ describeResult?:
+ | string
+ | ((
+ context: BatchCommitContext, Optimistic, Config> & { result: Result },
+ ) => string)
+ | null;
/**
* Refetch all of the data this mutation could have affected.
*/
@@ -97,6 +111,7 @@ interface BatchChannel {
args: Args;
resolve: (result: Result) => void;
reject: (error: unknown) => void;
+ reportSuccessGlobally?: boolean;
}>;
}
@@ -197,15 +212,33 @@ export class BatchMutation<
return describe;
}
- /** Calling the mutation in a global scope. Errors are turned into UI toasts. */
+ describeResult(args: Args, initial: Optimistic, current: Optimistic, result: Result): string | undefined {
+ const { describeResult } = this.#options;
+ if (describeResult === null || describeResult === undefined) return undefined;
+ return typeof describeResult === "function"
+ ? describeResult({
+ ...this.#client.context,
+ args,
+ initial,
+ current,
+ result,
+ })
+ : describeResult;
+ }
+
+ /** Calling the mutation in a global scope. Errors and successes are turned into UI toasts. */
run(...args: Args): void {
- this.runAndReturn(...args).catch((error) => {
+ this.#runAndReturn(args, true).catch((error) => {
this.#client.reportError(error);
});
}
/** Calls the mutation, treating the errors as promise rejection. */
runAndReturn(...args: Args): Promise {
+ return this.#runAndReturn(args, false);
+ }
+
+ #runAndReturn(args: Args, reportSuccessGlobally: boolean): Promise {
const key = this.key(args);
const channel = this.#getOrPutChannel(key);
@@ -260,7 +293,7 @@ export class BatchMutation<
// Create promise for this caller
const { promise, resolve, reject } = Promise.withResolvers();
- channel.pending.push({ args, resolve, reject });
+ channel.pending.push({ args, resolve, reject, reportSuccessGlobally });
// Set status to waiting and notify
if (channel.status === "idle") {
@@ -280,7 +313,7 @@ export class BatchMutation<
) {
const time = this.#options.time ?? 200;
- if (this.#options.mode === "debounce") {
+ if (this.#options.mode !== "throttle") {
// Debounce: reset timer on each call
if (channel.timer !== null) {
clearTimeout(channel.timer);
@@ -364,6 +397,15 @@ export class BatchMutation<
// Resolve all pending promises
pendingItems.forEach(({ resolve }) => resolve(result));
+ // Report success globally if any of the pending items requested it
+ const shouldReportSuccess = pendingItems.some((item) => item.reportSuccessGlobally);
+ if (shouldReportSuccess) {
+ const message = this.describeResult(firstArgs, initial, current, result);
+ if (message && this.#client.reportSuccess) {
+ this.#client.reportSuccess(message);
+ }
+ }
+
// Record commit time for throttle mode
channel.lastCommitTime = Date.now();
diff --git a/src/client.ts b/src/client.ts
index 8eb510f81e9f63703db64bf0e02d6909691eb174..079c68afc182472494331f5910c2b2a5020e1b11 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -22,6 +22,7 @@ export interface MutationClientOptions<
events: OptimisticEvents,
) => OptimisticHelpers;
reportError: (error: unknown) => void;
+ reportSuccess?: (message: string) => void;
/**
* Compare two values for deep equality. Used by BatchMutation to determine
* if the optimistic state has changed from the initial snapshot.
@@ -42,12 +43,14 @@ export class MutationClient<
context: Context;
getOptimisticHelpers: (event: OptimisticEvents) => OptimisticHelpers;
reportError: (error: unknown) => void;
+ reportSuccess?: (message: string) => void;
deepEquals: (a: unknown, b: unknown) => boolean;
constructor(options: MutationClientOptions) {
this.context = options.context;
this.getOptimisticHelpers = options.getOptimisticHelpers;
this.reportError = options.reportError;
+ this.reportSuccess = options.reportSuccess;
this.deepEquals = options.deepEquals ?? defaultDeepEquals;
}
@@ -56,7 +59,7 @@ export class MutationClient<
* You press a button, a pending state appears, then it completes. This works
* great for forms, and is similar to React Query's mutation system.
*/
- defineQueued(
+ defineBlocking(
options: MutationOptions<
Args,
Result,
@@ -77,7 +80,7 @@ export class MutationClient<
* works great for auto-saving input fields, follow buttons, and is preferred
* whenever possible.
*/
- defineBatched(
+ defineDebounced(
options: BatchMutationOptions<
Args,
Result,
diff --git a/src/mod.ts b/src/mod.ts
index 21ac9148d39de84c0e44e813edf290e4f774d9b0..37cd2172f93dfefafdc97234dc9726ddd3f87201 100644
--- a/src/mod.ts
+++ b/src/mod.ts
@@ -14,10 +14,10 @@ export type { Mutation, MutationEvent } from "./types.ts";
export {
createMutationButton,
type MutationButtonProps,
- useMutation,
- type UseMutationError,
- type UseMutationIdle,
- type UseMutationResult,
- type UseMutationResultBase,
- type UseMutationSuccess,
+ useMutate,
+ type UseMutateError,
+ type UseMutateIdle,
+ type UseMutateResult,
+ type UseMutateResultBase,
+ type UseMutateSuccess,
} from "./react.tsx";
diff --git a/src/queued.ts b/src/queued.ts
index e705917632d5895de021b87a42c526d0c030e9da..5a60eb64314be27f95f55734602e46a51b2e42ee 100644
--- a/src/queued.ts
+++ b/src/queued.ts
@@ -19,13 +19,22 @@ export interface MutationOptions<
* params type is used to allow type inference. Place this function first to
* ensure TypeScript correctly infers the argument type for the rest of the
* functions.
+ *
+ * In practice, optimistic context is never needed in this function, but it
+ * is provided as the `this` value if you truly desire it.
*/
- mutate: (context: Config["context"], ...args: Args) => Promise;
+ mutate: (this: Config["context"], ...args: Args) => Promise;
/**
* Used in error messages and debug tools.
* Phrase it considering the template `Failed to ${describe(...)}`
*/
describe: string | ((context: Config["context"] & { args: Args }) => string);
+ /**
+ * Used in success messages.
+ * Phrase it as a complete success message, e.g., "Deleted item successfully"
+ * Set to null to suppress success reporting.
+ */
+ describeResult?: string | ((context: Config["context"] & { args: Args; result: Result }) => string) | null;
/**
* Specifying the optimistic strategy is required. To disable, pass an empty
* function with a comment to document why it isn't needed.
@@ -33,6 +42,7 @@ export interface MutationOptions<
optimistic: (context: OptimisticContext) => void;
/**
* Refetch all of the data this mutation could have affected.
+ * Normally, optimistic helpers will perform
* This is called automatically on errors.
*/
refetch?: (context: Config["context"] & { args: Args }) => Promise;
@@ -154,9 +164,22 @@ export class QueuedMutation<
: describe;
}
+ describeResult(args: Args, result: Result): string | undefined {
+ const { describeResult } = this.#options;
+ if (describeResult === null || describeResult === undefined) return undefined;
+ return typeof describeResult === "function"
+ ? describeResult({ ...this.#client.context, args, result })
+ : describeResult;
+ }
+
/** Calling the mutation in a global scope. Errors are turned into UI toasts. */
run(...args: Args) {
- this.runAndReturn(...args).catch((error) => {
+ this.runAndReturn(...args).then((result) => {
+ const message = this.describeResult(args, result);
+ if (message && this.#client.reportSuccess) {
+ this.#client.reportSuccess(message);
+ }
+ }).catch((error) => {
this.#client.reportError(error);
});
}
@@ -244,7 +267,7 @@ export class QueuedMutation<
channel.status = "mutating";
this.#notify(channel, "mutating");
- this.#options.mutate(this.#client.context, ...args).then((result) => {
+ this.#options.mutate.call(this.#client.context, ...args).then((result) => {
// remove rollbacks and apply optimistic success handlers
channel.rollbacks.splice(0, item.rollbacks);
onSuccess.forEach((cb) => cb(result));
diff --git a/src/react.tsx b/src/react.tsx
index cdfb9b62f0c8bc74cd23a3a74653f16b622216e1..d8379fb345ffa69336d67dfe0128f8afcb07b478 100644
--- a/src/react.tsx
+++ b/src/react.tsx
@@ -13,12 +13,12 @@ import type { Mutation } from "./types.ts";
* Subscribe to a mutation's status, as well as accessing a local `run` method.
* The resulting mutation can be used directly or passed to a {@link createMutationButton|mutation button}.
*/
-export function useMutation<
+export function useMutate<
Args extends unknown[],
Result,
>(
mutation: Mutation | null,
-): UseMutationResult {
+): UseMutateResult {
const [_, setRerender] = useState(0);
const [observer] = useState(() => new Observer(setRerender));
useEffect(() => () => void observer.reset(), []);
@@ -29,20 +29,20 @@ export function useMutation<
return observer.binding;
}
-export type UseMutationResult =
- & UseMutationResultBase
+export type UseMutateResult =
+ & UseMutateResultBase
& (
- | UseMutationSuccess
- | UseMutationError
- | UseMutationIdle
+ | UseMutateSuccess
+ | UseMutateError
+ | UseMutateIdle
);
-export interface UseMutationResultBase {
+export interface UseMutateResultBase {
run: (...args: Args) => void;
clear: () => void;
}
-export interface UseMutationSuccess {
+export interface UseMutateSuccess {
status: "success";
result: Result;
error: undefined;
@@ -57,7 +57,7 @@ export interface UseMutationSuccess {
/** `true` when there is optimistic state applied. */
isOptimisticData: boolean;
}
-export interface UseMutationError {
+export interface UseMutateError {
status: "error";
result: undefined;
error: unknown;
@@ -72,7 +72,7 @@ export interface UseMutationError {
/** `true` when there is optimistic state applied. */
isOptimisticData: boolean;
}
-export interface UseMutationIdle {
+export interface UseMutateIdle {
status: "idle" | "mutating";
result: undefined;
error: undefined;
@@ -90,7 +90,7 @@ export interface UseMutationIdle {
type AnyMutationState =
& Omit<
- UseMutationIdle,
+ UseMutateIdle,
"status" | "result" | "error" | "isSuccess" | "isError"
>
& {
@@ -147,7 +147,7 @@ class Observer {
this.state = initialState();
}
- binding: UseMutationResult = ((self: this) => ({
+ binding: UseMutateResult = ((self: this) => ({
run(...args: Args) {
const mutation = self.mutation;
if (!mutation) return;
@@ -189,10 +189,13 @@ class Observer {
},
);
}
- // use global error handling if this usage of the hook doesnt check for
- // errors this makes it act pretty awesome in terms of defaults. you don't
- // have to worry about the errors, they'll surface exactly once.
- if (self.watched.has("isError") || self.watched.has("error")) {
+ // 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 the errors/successes, they'll surface exactly once.
+ if (
+ self.watched.has("isError") || self.watched.has("error") ||
+ self.watched.has("isSuccess") || self.watched.has("result")
+ ) {
mutation.runAndReturn(...args).catch(() => {
// caught in event listener
});
@@ -243,7 +246,7 @@ class Observer {
self.watched.add("isOptimisticData");
return self.state.isOptimisticData;
},
- } as UseMutationResult))(this);
+ } as UseMutateResult))(this);
}
interface BaseButtonProps {
@@ -263,7 +266,7 @@ interface MutationButtonComponent {
export interface MutationButtonProps {
mutation:
| Mutation
- | Pick, "run" | "status" | "isPending">;
+ | Pick, "run" | "status" | "isPending">;
/** Preventing default will interrupt the mutation */
args: Args | ((e: MouseEvent) => Args | null);
/** Preventing default will interrupt the mutation */
@@ -274,7 +277,7 @@ export interface MutationButtonProps {
* Wraps a custom button component with logic to execute a mutation. The wrapped
* component must accept `onClick` and an `isPending` property. When the inner
* component emits `onClick`, that will begin the mutation. This is a trival
- * abstraction on top of `useMutation`, but with type gymnastics to allow safe
+ * abstraction on top of `useMutate`, but with type gymnastics to allow safe
* types.
*/
export function createMutationButton(
@@ -315,7 +318,7 @@ function GenericMutationButton<
const { mutation, args, onClick, ...forwarded } = props;
forwarded satisfies Omit>;
- const localHook = useMutation("subscribe" in mutation ? mutation : null);
+ const localHook = useMutate("subscribe" in mutation ? mutation : null);
const state = "subscribe" in mutation ? localHook : mutation;
return (
diff --git a/test/batch.test.ts b/test/batch.test.ts
index 31bfd3543a501cede9357717d99442e2e72a8db2..074b6160166044210803cfbbbde1ea1d21dc761b 100644
--- a/test/batch.test.ts
+++ b/test/batch.test.ts
@@ -65,7 +65,7 @@ test("BatchMutation - basic mutation success with debounce", async () => {
let commitCallCount = 0;
let refetchCallCount = 0;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -98,7 +98,7 @@ test("BatchMutation - run() catches errors", async () => {
testStore.clear();
testStore.set("counter", 0);
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -125,7 +125,7 @@ test("BatchMutation - runAndReturn() rejects on error", async () => {
testStore.clear();
testStore.set("counter", 0);
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -159,7 +159,7 @@ test("BatchMutation - debounce batches rapid calls", async () => {
let commitCallCount = 0;
const commitArgs: Array<{ initial: number; current: number }> = [];
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -201,7 +201,7 @@ test("BatchMutation - debounce resets timer on each call", async () => {
let commitCallCount = 0;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -250,7 +250,7 @@ test("BatchMutation - debounce separates batches after timeout", async () => {
let commitCallCount = 0;
const commitArgs: Array<{ initial: number; current: number }> = [];
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -295,7 +295,7 @@ test("BatchMutation - throttle commits immediately on first call", async () => {
let commitTime = 0;
const startTime = Date.now();
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -325,7 +325,7 @@ test("BatchMutation - throttle batches calls within time window", async () => {
let commitCallCount = 0;
const commitArgs: Array<{ initial: number; current: number }> = [];
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -377,7 +377,7 @@ test("BatchMutation - throttle allows new batch after time window", async () =>
let commitCallCount = 0;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -420,7 +420,7 @@ test("BatchMutation - skips commit when value unchanged", async () => {
let commitCallCount = 0;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -479,7 +479,7 @@ test("BatchMutation - uses deepEquals for comparison", async () => {
let commitCallCount = 0;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, count: number) {
helpers.setCount(count);
},
@@ -531,7 +531,7 @@ test("BatchMutation - custom deepEquals function", async () => {
testStore.clear();
testStore.set("counter", 0);
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -546,7 +546,9 @@ test("BatchMutation - custom deepEquals function", async () => {
async refetch() {},
});
- await mutation.runAndReturn(5);
+ await mutation.runAndReturn(5).catch(() => {
+ // Expected to fail due to commit error
+ });
await delay(30);
// Custom deepEquals should have been called
@@ -562,7 +564,7 @@ test("BatchMutation - rollback on commit error", async () => {
testStore.clear();
testStore.set("counter", 10);
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -594,7 +596,7 @@ test("BatchMutation - error event includes error details", async () => {
const tracker = createEventTracker();
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -629,7 +631,7 @@ test("BatchMutation - key() returns JSON stringified key", () => {
const { client } = createTestClient();
testStore.clear();
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic(_ctx, _id: string) {},
mode: "debounce",
time: 20,
@@ -649,7 +651,7 @@ test("BatchMutation - key() can return array", () => {
const { client } = createTestClient();
testStore.clear();
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic(_ctx, _id: string) {},
mode: "debounce",
time: 20,
@@ -676,7 +678,7 @@ test("BatchMutation - different keys create separate batches", async () => {
let commitCallCount = 0;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, key: string, amount: number) {
helpers.increment(`counter-${key}`, amount);
},
@@ -713,7 +715,7 @@ test("BatchMutation - describe() with string", () => {
const { client } = createTestClient();
testStore.clear();
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic(_ctx, _amount: number) {},
mode: "debounce",
time: 20,
@@ -733,7 +735,7 @@ test("BatchMutation - describe() with function", () => {
const { client } = createTestClient();
testStore.clear();
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic(_ctx, _amount: number) {},
mode: "debounce",
time: 20,
@@ -758,7 +760,7 @@ test("BatchMutation - all pending promises resolve with same result", async () =
testStore.clear();
testStore.set("counter", 0);
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -794,7 +796,7 @@ test("BatchMutation - all pending promises reject with same error", async () =>
testStore.clear();
testStore.set("counter", 0);
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -837,7 +839,7 @@ test("BatchMutation - handles empty getValue result", async () => {
let commitCallCount = 0;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.setValue("nonexistent", amount);
},
@@ -865,7 +867,7 @@ test("BatchMutation - channel cleanup after idle with no listeners", async () =>
testStore.clear();
testStore.set("counter", 0);
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -900,7 +902,7 @@ test("BatchMutation - default time is 200ms", async () => {
let commitTime: number | null = null;
const startTime = Date.now();
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -931,7 +933,7 @@ test("BatchMutation - context is passed to getValue", async () => {
let receivedUserId: string | undefined;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -962,7 +964,7 @@ test("BatchMutation - context is passed to commit", async () => {
let receivedUserId: string | undefined;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, amount: number) {
helpers.increment("counter", amount);
},
@@ -991,7 +993,7 @@ test("BatchMutation - first args are used for commit", async () => {
let receivedArgs: [string, number] | undefined;
- const mutation = client.defineBatched({
+ const mutation = client.defineDebounced({
optimistic({ helpers }, _label: string, amount: number) {
helpers.increment("counter", amount);
},
diff --git a/test/queued.test.ts b/test/queued.test.ts
index 3b4aaf9be8802de6efb5d1cb9bc222df6c13d980..85f287f1bc6da3ab9415fbae3701b10946dd808d 100644
--- a/test/queued.test.ts
+++ b/test/queued.test.ts
@@ -45,8 +45,8 @@ test("QueuedMutation - basic mutation success", async () => {
let mutateCallCount = 0;
let refetchCallCount = 0;
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
mutateCallCount++;
await delay(10);
return `result-${value}`;
@@ -73,8 +73,8 @@ test("QueuedMutation - basic mutation success", async () => {
test("QueuedMutation - run() catches errors", async () => {
const { client, errors } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
throw new Error("mutation failed");
},
describe: "failing mutation",
@@ -92,8 +92,8 @@ test("QueuedMutation - run() catches errors", async () => {
test("QueuedMutation - runAndReturn() rejects on error", async () => {
const { client } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
throw new Error("mutation failed");
},
describe: "failing mutation",
@@ -112,8 +112,8 @@ test("QueuedMutation - optimistic updates are applied immediately", async () =>
const { client } = createTestClient();
testStore.clear();
- const mutation = client.defineQueued({
- async mutate(_, _key: string, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_key: string, value: string) {
await delay(50);
return value;
},
@@ -139,8 +139,8 @@ test("QueuedMutation - rollback on error", async () => {
const { client } = createTestClient();
testStore.clear();
- const mutation = client.defineQueued({
- async mutate(_, _key: string, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_key: string, _value: string) {
await delay(10);
throw new Error("mutation failed");
},
@@ -162,8 +162,8 @@ test("QueuedMutation - onSuccess callback is called", async () => {
const { client } = createTestClient();
const successResults: string[] = [];
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
return `result-${value}`;
},
describe: "test mutation",
@@ -184,8 +184,8 @@ test("QueuedMutation - mutations with same key execute serially", async () => {
const { client } = createTestClient();
const executionOrder: string[] = [];
- const mutation = client.defineQueued({
- async mutate(_, id: string) {
+ const mutation = client.defineBlocking({
+ async mutate(id: string) {
executionOrder.push(`start-${id}`);
await delay(20);
executionOrder.push(`end-${id}`);
@@ -215,8 +215,8 @@ test("QueuedMutation - mutations with different keys execute in parallel", async
const { client } = createTestClient();
const executionOrder: string[] = [];
- const mutation = client.defineQueued({
- async mutate(_, id: string) {
+ const mutation = client.defineBlocking({
+ async mutate(id: string) {
executionOrder.push(`start-${id}`);
await delay(20);
executionOrder.push(`end-${id}`);
@@ -244,8 +244,8 @@ test("QueuedMutation - mutations with different keys execute in parallel", async
test("QueuedMutation - key() returns JSON stringified key", () => {
const { client } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, id: string) {
+ const mutation = client.defineBlocking({
+ async mutate(id: string) {
return id;
},
describe: "test mutation",
@@ -263,8 +263,8 @@ test("QueuedMutation - key() returns JSON stringified key", () => {
test("QueuedMutation - key() defaults to 'shared' when no key function", () => {
const { client } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, id: string) {
+ const mutation = client.defineBlocking({
+ async mutate(id: string) {
return id;
},
describe: "test mutation",
@@ -278,8 +278,8 @@ test("QueuedMutation - key() defaults to 'shared' when no key function", () => {
test("QueuedMutation - key() can return array", () => {
const { client } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, _userId: string, _itemId: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_userId: string, _itemId: string) {
return "result";
},
describe: "test mutation",
@@ -300,8 +300,8 @@ test("QueuedMutation - key() can return array", () => {
test("QueuedMutation - describe() with string", () => {
const { client } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
return value;
},
describe: "create item",
@@ -315,8 +315,8 @@ test("QueuedMutation - describe() with string", () => {
test("QueuedMutation - describe() with function", () => {
const { client } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, id: string) {
+ const mutation = client.defineBlocking({
+ async mutate(id: string) {
return id;
},
describe({ args }) {
@@ -333,8 +333,8 @@ test("QueuedMutation - describe() with function", () => {
test("QueuedMutation - describe() receives context", () => {
const { client } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, id: string) {
+ const mutation = client.defineBlocking({
+ async mutate(id: string) {
return id;
},
describe({ userId, args }) {
@@ -355,8 +355,8 @@ test("QueuedMutation - subscribe() tracks mutation events", async () => {
const { client } = createTestClient();
const tracker = createEventTracker();
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
await delay(10);
return `result-${value}`;
},
@@ -384,8 +384,8 @@ test("QueuedMutation - unsubscribe stops receiving events", async () => {
const { client } = createTestClient();
const tracker = createEventTracker();
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
await delay(10);
return value;
},
@@ -411,8 +411,8 @@ test("QueuedMutation - refetchOnSuccess can be disabled", async () => {
const { client } = createTestClient();
let refetchCallCount = 0;
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
return _value;
},
describe: "test mutation",
@@ -432,8 +432,8 @@ test("QueuedMutation - refetch is called on error", async () => {
const { client } = createTestClient();
let refetchCallCount = 0;
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
throw new Error("mutation failed");
},
describe: "failing mutation",
@@ -452,8 +452,8 @@ test("QueuedMutation - queued mutations are cancelled on error", async () => {
const { client } = createTestClient();
const executionOrder: string[] = [];
- const mutation = client.defineQueued({
- async mutate(_, id: string) {
+ const mutation = client.defineBlocking({
+ async mutate(id: string) {
executionOrder.push(`start-${id}`);
await delay(10);
if (id === "1") {
@@ -486,8 +486,8 @@ test("QueuedMutation - rollbacks are called in reverse order on error", async ()
const { client } = createTestClient();
const rollbackOrder: number[] = [];
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
throw new Error("mutation failed");
},
describe: "failing mutation",
@@ -509,8 +509,8 @@ test("QueuedMutation - multiple mutations: rollbacks only affect failed mutation
const { client } = createTestClient();
const rollbackOrder: string[] = [];
- const mutation = client.defineQueued({
- async mutate(_, id: string) {
+ const mutation = client.defineBlocking({
+ async mutate(id: string) {
await delay(10);
if (id === "fail") {
throw new Error("mutation failed");
@@ -542,8 +542,8 @@ test("QueuedMutation - onRestore throws error if called after optimistic phase",
const { client } = createTestClient();
let capturedOnRestore: ((cb: () => void) => void) | null = null;
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
return "result";
},
describe: "test mutation",
@@ -573,8 +573,8 @@ test("QueuedMutation - onSuccess throws error if called after optimistic phase",
const { client } = createTestClient();
let capturedOnSuccess: ((cb: (result: string) => void) => void) | null = null;
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
return "result";
},
describe: "test mutation",
@@ -603,8 +603,8 @@ test("QueuedMutation - onSuccess throws error if called after optimistic phase",
test("QueuedMutation - error during optimistic update is rejected immediately", async () => {
const { client } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
return "result";
},
describe: "test mutation",
@@ -625,8 +625,8 @@ test("QueuedMutation - error during optimistic update rolls back registered call
const { client } = createTestClient();
const rollbackOrder: number[] = [];
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
return "result";
},
describe: "test mutation",
@@ -648,8 +648,8 @@ test("QueuedMutation - error during optimistic update rolls back registered call
test("QueuedMutation - refetch errors are reported but don't fail mutation", async () => {
const { client, errors } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
return value;
},
describe: "test mutation",
@@ -674,8 +674,8 @@ test("QueuedMutation - optimistic function receives args and helpers", async ()
let receivedArgs: unknown[] | undefined;
let receivedHelpers: unknown | undefined;
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
return "result";
},
describe: "test mutation",
@@ -697,8 +697,8 @@ test("QueuedMutation - refetch receives context and args", async () => {
let receivedUserId: string | undefined;
let receivedArgs: unknown[] | undefined;
- const mutation = client.defineQueued({
- async mutate(_, _id: string, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_id: string, value: string) {
return value;
},
describe: "test mutation",
@@ -719,8 +719,8 @@ test("QueuedMutation - notifies error on mutation failure", async () => {
const { client } = createTestClient();
const tracker = createEventTracker();
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
await delay(10);
throw new Error("mutation failed");
},
@@ -747,8 +747,8 @@ test("QueuedMutation - multiple subscribers receive events", async () => {
const tracker1 = createEventTracker();
const tracker2 = createEventTracker();
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
await delay(5);
return value;
},
@@ -774,8 +774,8 @@ test("QueuedMutation - onSuccess is called before mutation resolves", async () =
const { client } = createTestClient();
const callOrder: string[] = [];
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
return value;
},
describe: "test mutation",
@@ -804,8 +804,8 @@ test("QueuedMutation - result is passed to notification on success", async () =>
const { client } = createTestClient();
const tracker = createEventTracker();
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
await delay(5);
return `result-${value}`;
},
@@ -834,8 +834,8 @@ test("QueuedMutation - channel is reused for same key", async () => {
const { client } = createTestClient();
const events: string[] = [];
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
events.push(`mutate-${value}`);
return value;
},
@@ -859,8 +859,8 @@ test("QueuedMutation - channel is reused for same key", async () => {
test("QueuedMutation - empty queue after all mutations complete", async () => {
const { client } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
await delay(5);
return value;
},
@@ -893,8 +893,8 @@ test("QueuedMutation - multiple onSuccess callbacks are all called", async () =>
const { client } = createTestClient();
const results: string[] = [];
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
return value;
},
describe: "test mutation",
@@ -916,8 +916,8 @@ test("QueuedMutation - refetchOnSuccess false skips refetch", async () => {
const { client } = createTestClient();
let refetchCalled = false;
- const mutation = client.defineQueued({
- async mutate(_, value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(value: string) {
return value;
},
describe: "test mutation",
@@ -938,8 +938,8 @@ test("QueuedMutation - refetchOnSuccess false skips refetch", async () => {
test("QueuedMutation - refetch error after mutation failure is reported", async () => {
const { client, errors } = createTestClient();
- const mutation = client.defineQueued({
- async mutate(_, _value: string) {
+ const mutation = client.defineBlocking({
+ async mutate(_value: string) {
throw new Error("mutation failed");
},
describe: "failing mutation",