diff --git a/jsr.json b/jsr.json index 90ed039c9595323b459889470b8f00e9e82541b5..c8aacea001f96e133b7e8f885ed1f9fb98dd9a53 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@clo/react-mutation", - "version": "1.0.0-beta.5", + "version": "1.0.0-beta.6", "exports": { ".": "./src/mod.ts", "./tanstack-query.ts": "./src/tanstack-query.ts", diff --git a/package-lock.json b/package-lock.json index c52f6544b3c5e5689dcb053dc59f4d358df6c5bb..99ef1efcdeb9fceaee27118d82aa8b1a7cebf679 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@vitejs/plugin-react": "^5.1.1", "react": "^19.2.4", "react-dom": "^19.2.4", + "typescript": "^5.9.3", "vite": "^7.2.4", "vitest": "^4.0.18" }, @@ -1998,6 +1999,20 @@ "node": ">=14.0.0" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", diff --git a/package.json b/package.json index f0bd2a337048fa543ee3fbf0bc169016c763729c..18d9600449e773cf219aff4298c862ee21cd1c21 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@vitejs/plugin-react": "^5.1.1", "react": "^19.2.4", "react-dom": "^19.2.4", + "typescript": "^5.9.3", "vite": "^7.2.4", "vitest": "^4.0.18" }, diff --git a/readme.md b/readme.md index afdb0dc47fc63881b49592da1735e6a8b49ab127..726130622eb02f314e09145e00f8512b6120e821 100644 --- a/readme.md +++ b/readme.md @@ -15,9 +15,10 @@ 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 display a UI toast. Otherwise, the component can display the error locally. -- Optimistic helpers allow defining rollbacks and refetching logic independant - of the actual mutation. The [built in helpers for React Query](#react-query-optimistic-helpers) - shows this power in more detail. +- **Optimistic helpers with built-in rollbacks** make it super easy to alter the + 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. ## Usage diff --git a/src/blocking.ts b/src/blocking.ts index e3667c8dea1ce11a527a0dc89678ac3ad96f1a33..55c376e6de21cde4d1aa24b932d10e79bd61dae9 100644 --- a/src/blocking.ts +++ b/src/blocking.ts @@ -1,6 +1,6 @@ import type { MutationClient, MutationClientFromConfig } from "./client.ts"; import type { MutationClientConfig } from "./client.ts"; -import type { Mutation, MutationEvent } from "./types.ts"; +import type { Mutation, MutationEvent, RunOptions } from "./types.ts"; import { message as errMessage } from "@clo/lib/error.ts"; /** @@ -45,12 +45,6 @@ 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; /** * If the optimistic updator function is perfect, then this may be set to false. * @default true @@ -231,26 +225,56 @@ export class BlockingMutation< /** Calling the mutation in a global scope. Errors are turned into UI toasts. */ run(...args: Args) { + this.runWithOptions(...args, {}); + } + + /** Calls the mutation with custom handlers that can suppress global handlers. */ + runWithOptions(...array: [...Args, RunOptions]): Promise { if (!this.#client.enabled) { throw new Error( "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?", ); } - this.runAndReturn(...args).then((result) => { - const message = this.describeResult(args, result); - if (message && this.#client.reportSuccess) { - this.#client.reportSuccess(message); + + const args = array.slice() as Args; + const { onSuccess, onSuccessDataOnly, onError, onSettled } = args + .pop() as RunOptions; + const suppressGlobalSuccess = onSuccess !== undefined; + const suppressGlobalError = onError !== undefined; + + const promise = this.runAsPromise(...args); + promise.then((result) => { + // Call user handlers + onSuccess?.(result); + onSuccessDataOnly?.(result); + onSettled?.({ status: "success", result }); + + // Call global handler unless suppressed + if (!suppressGlobalSuccess) { + const message = this.describeResult(args, result); + if (message && this.#client.reportSuccess) { + this.#client.reportSuccess(message); + } } }).catch((error) => { - const message = `Failed to ${this.describe(...args)}: ${ - errMessage(error) - }`; - this.#client.reportError(message, error); + // Call user handlers + onError?.(error); + onSettled?.({ status: "error", error }); + + // Call global handler unless suppressed + if (!suppressGlobalError) { + const message = `Failed to ${this.describe(...args)}: ${ + errMessage(error) + }`; + this.#client.reportError(message, error); + } }); + + return promise; } /** Calls the mutation, treating the errors as promise rejection. */ - runAndReturn(...args: Args): Promise { + runAsPromise(...args: Args): Promise { if (!this.#client.enabled) { throw new Error( "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?", @@ -366,23 +390,19 @@ export class BlockingMutation< this.#notify(channel, "refetching", result); // Call refetch and all refetch callbacks in parallel const refetchCallbacks = channel.refetches.splice(0); - Promise.allSettled([ - this.#options.refetch?.({ - ...this.#client.context, - args, - }), - ...refetchCallbacks.map((cb) => cb()), - ]).then((results) => { - // 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)}`; - this.#client.reportError(message, result.reason); - } - }); - }).finally(() => { + Promise.allSettled(refetchCallbacks.map((cb) => cb())).then( + (results) => { + // 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)}`; + this.#client.reportError(message, result.reason); + } + }); + }, + ).finally(() => { this.#executeNext(key, channel); }); } else { @@ -410,13 +430,7 @@ export class BlockingMutation< this.#notify(channel, "refetching", null, error); // Call refetch and all refetch callbacks in parallel const refetchCallbacks = channel.refetches.splice(0); - Promise.allSettled([ - this.#options.refetch?.({ - ...this.#client.context, - args, - }), - ...refetchCallbacks.map((cb) => cb()), - ]).then((results) => { + Promise.allSettled(refetchCallbacks.map((cb) => cb())).then((results) => { // Report any errors from refetch or callbacks results.forEach((result) => { if (result.status === "rejected") { diff --git a/src/debounced.ts b/src/debounced.ts index da01ab2bfc991e87a5e0bdb79446f60d6b595f5f..910f0c8322ad5e0f50253e3136cc1ef1259a1b84 100644 --- a/src/debounced.ts +++ b/src/debounced.ts @@ -1,6 +1,6 @@ import type { MutationClient, MutationClientFromConfig } from "./client.ts"; import type { MutationClientConfig } from "./client.ts"; -import type { Mutation, MutationEvent } from "./types.ts"; +import type { Mutation, MutationEvent, RunOptions } from "./types.ts"; import { message as errMessage } from "@clo/lib/error.ts"; export interface DebouncedMutationOptions< @@ -293,8 +293,42 @@ export class DebouncedMutation< }); } + runWithOptions(...array: [...args: Args, options: RunOptions]): void { + if (!this.#client.enabled) { + throw new Error( + "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?", + ); + } + const args = array.slice() as Args; + const { onSuccess, onSuccessDataOnly, onError, onSettled } = args + .pop() as RunOptions; + const suppressGlobalSuccess = onSuccess !== undefined; + const suppressGlobalError = onError !== undefined; + + const promise = this.#runAndReturn(args, !suppressGlobalSuccess); + + promise.then((result) => { + // Call user handlers + onSuccess?.(result); + onSuccessDataOnly?.(result); + onSettled?.({ status: "success", result }); + }).catch((error) => { + // Call user handlers + onError?.(error); + onSettled?.({ status: "error", error }); + + // Call global error handler unless suppressed + if (!suppressGlobalError) { + const message = `Failed to ${this.describe(...args)}: ${ + errMessage(error) + }`; + this.#client.reportError(message, error); + } + }); + } + /** Calls the mutation, treating the errors as promise rejection. */ - runAndReturn(...args: Args): Promise { + runAsPromise(...args: Args): Promise { if (!this.#client.enabled) { throw new Error( "MutationClient was passed enabled: false. Are you trying to perform a mutation from SSR?", @@ -546,20 +580,19 @@ export class DebouncedMutation< channel.status = "refetching"; this.#notify(channel, "refetching", null, error); // Call refetch and all refetch callbacks in parallel - Promise.allSettled([ - this.#options.refetch?.(), - ...refetchCallbacks.map((cb) => cb()), - ]).then((results) => { - // 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)}`; - this.#client.reportError(message, result.reason); - } - }); - }).finally(() => { + Promise.allSettled(refetchCallbacks.map((cb) => cb())).then( + (results) => { + // 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)}`; + this.#client.reportError(message, result.reason); + } + }); + }, + ).finally(() => { // Check if new calls came in during the commit if (channel.pending.length > 0) { // There are pending calls that need to be committed diff --git a/src/react.ts b/src/react.ts index 528511caae7f100e7d60cdf60b3ddd2ca2c23796..604c95f457146f56867aa261bb47e4768dc9a993 100644 --- a/src/react.ts +++ b/src/react.ts @@ -8,7 +8,7 @@ import { useState, } from "react"; import { message as errMessage } from "@clo/lib/error.ts"; -import type { Mutation } from "./types.ts"; +import type { Mutation, RunOptions } from "./types.ts"; import { jsx } from "react/jsx-runtime"; /** @@ -41,7 +41,10 @@ export type UseMutateResult = export interface UseMutateResultBase { run: (...args: Args) => void; - runWithResult: (...args: Args) => Promise; + runWithOptions: ( + options: RunOptions, + ...args: Args + ) => Promise; clear: () => void; } @@ -220,7 +223,7 @@ class Observer { this.watched.has("error") || this.watched.has("errorMessage"); const watchesSuccess = this.watched.has("isSuccess") || this.watched.has("result"); - const promise = mutation.runAndReturn(...args) + const promise = mutation.runAsPromise(...args) .then((result) => { if (!watchesSuccess && mutation.describeResult) { const message = mutation.describeResult(args, result); @@ -240,12 +243,63 @@ class Observer { return promise; } + runWithOptions(options: RunOptions, ...args: Args): void { + const mutation = this.mutation; + if (!mutation) return; + + this.currentArgs = args; + const key = mutation.key(args); + + // Set up subscription if key changed + if (key !== this.currentKey) { + this.currentKey = key; + this.unsubscribe?.(); + this.unsubscribe = mutation.subscribe( + mutation.key(args), + ({ status, error, result }) => { + if (status === "idle") { + this.setState({ + isMutating: false, + isPending: false, + isOptimisticData: false, + }); + return; + } + const hasError = error != null; + const hasResult = result != null; + + this.setState({ + status: hasError + ? "error" + : hasResult + ? "success" + : status === "mutating" + ? "mutating" + : "idle", + error: error ?? undefined, + errorMessage: this.computeErrorMessage(error ?? undefined), + result: result ?? undefined, + isMutating: status === "mutating", + isPending: status === "mutating" || status === "refetching", + isSuccess: hasResult && !hasError, + isError: hasError, + isOptimisticData: status === "waiting" || status === "mutating" || + status === "refetching", + }); + }, + ); + } + + // Delegate to the mutation's runWithOptions + mutation.runWithOptions(...args, options); + } + binding: UseMutateResult = ((self: this) => ({ run(...args) { return self.run(...args); }, - runWithResult(...args) { - return self.run(...args); + runWithOptions(options, ...args) { + return self.runWithOptions(options, ...args); }, clear() { self.setState({ @@ -394,9 +448,6 @@ function GenericMutationButton< const localHook = useMutate("subscribe" in mutation ? mutation : null); const state = "subscribe" in mutation ? localHook : mutation; - if (onError) void state.isError; // subscribe to the events - if (onSuccess) void state.isSuccess; // subscribe to the events - // NOTE: the JSR has trouble with JSX syntax for some reason. return jsx( Component, @@ -407,15 +458,10 @@ function GenericMutationButton< if (e.defaultPrevented) return; const computedArgs = typeof args === "function" ? args(e) : args; if (!computedArgs || e.defaultPrevented) return; - state.runWithResult(...computedArgs) - .then((result) => { - onSuccess?.(result); - onSettled?.({ status: "success", result }); - }) - .catch((error) => { - onError?.(error); - onSettled?.({ status: "error", error }); - }); + state.runWithOptions( + { onSuccess, onError, onSettled }, + ...computedArgs, + ); }, [state]), isPending: state.isPending, } satisfies Parameters[0], diff --git a/src/tanstack-query.ts b/src/tanstack-query.ts index 76375c7dd7f82d006d82eeea4f5e745e7375b95c..a4a5b0d51f0d7dff893ad7e3a0283f41b5ef8ce0 100644 --- a/src/tanstack-query.ts +++ b/src/tanstack-query.ts @@ -322,6 +322,39 @@ class TanstackQueryOptimisticHelpers { objArrayRemove< Data extends object, const Path extends AllObjectPaths, + >( + queryKey: QueryKeyAndFn, + path: Path, + removeFilter: ( + item: GetObjectPath extends Array ? T : never, + index: number, + ) => boolean, + ) { + const prev = this.#get(queryKey); + if (!prev) return; + const { value: original, exists } = getPath(prev, path); + if (!exists || !Array.isArray(original)) return; + + const newArray = original.filter((item, index) => + !removeFilter(item, index) + ); + this.#set( + queryKey, + (obj) => obj ? setPath(obj, path, newArray as any) : obj, + ); + this.#onRestore(() => { + // TODO: splice items back in case original changed + this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj); + }); + } + + /** + * Filter items to just include items that match `filter`. This is the inverse of `objArrayRemove` + * If the query or path doesn't exist, the updater is skipped. + */ + objArrayFilter< + Data extends object, + const Path extends AllObjectPaths, >( queryKey: QueryKeyAndFn, path: Path, @@ -335,7 +368,7 @@ class TanstackQueryOptimisticHelpers { const { value: original, exists } = getPath(prev, path); if (!exists || !Array.isArray(original)) return; - const newArray = original.filter((item, index) => !filter(item, index)); + const newArray = original.filter(filter); this.#set( queryKey, (obj) => obj ? setPath(obj, path, newArray as any) : obj, diff --git a/src/types.ts b/src/types.ts index b58f8e1d0983019b8dc2529c4e94f310673e6076..527d087474b91bd64fbdef8eb429f0281de0bc02 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,8 +3,10 @@ import type { MutationClient } from "./client.ts"; export interface Mutation { /** Calling the mutation. Errors are turned into UI toasts. */ run(...args: Args): void; - /** Calls the mutation, treating the errors as promise rejection. */ - runAndReturn(...args: Args): Promise; + /** Calls the mutation with custom handlers that can suppress global handlers. */ + runWithOptions(...args: [...args: Args, options: RunOptions]): void; + /** Calling the mutation. Errors are thrown in the promise. */ + runAsPromise(...args: Args): Promise; /** Returns the concurrency key used for a given set of arguments */ key(args: Args): string; @@ -18,6 +20,21 @@ export interface Mutation { client: MutationClient; } +export interface RunOptions { + /** Called on success, suppresses the global success handler */ + onSuccess?: (result: Result) => void; + /** Called on success, does NOT suppress the global success handler */ + onSuccessDataOnly?: (result: Result) => void; + /** Called on error, suppresses the global error handler */ + onError?: (error: unknown) => void; + /** Called on settled (doesn't suppress global handlers) */ + onSettled?: ( + status: + | { status: "success"; result: Result } + | { status: "error"; error: unknown }, + ) => void; +} + export interface MutationEvent { status: "idle" | "waiting" | "mutating" | "refetching"; result: Result | null; diff --git a/test/blocking.test.ts b/test/blocking.test.ts index 39bfe6c3ce2840a0eb2d4a3ffcac9de61004c50a..1ecd03f51d187513dc92504333a48bff16fd8d0d 100644 --- a/test/blocking.test.ts +++ b/test/blocking.test.ts @@ -57,16 +57,15 @@ test("BlockingMutation - basic mutation success", async () => { }, describe: "test mutation", describeResult: "Success", - optimistic() { - // Empty optimistic update - }, - async refetch() { - refetchCallCount++; - await delay(5); + optimistic({ onRefetch }) { + onRefetch(async () => { + refetchCallCount++; + await delay(5); + }); }, }); - const result = await mutation.runAndReturn("test"); + const result = await mutation.runAsPromise("test"); // Wait for refetch to complete await delay(20); @@ -85,7 +84,6 @@ test("BlockingMutation - run() catches errors", async () => { describe: "failing mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, }); mutation.run("test"); @@ -105,11 +103,10 @@ test("BlockingMutation - runAndReturn() rejects on error", async () => { describe: "failing mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, }); await assertRejects( - () => mutation.runAndReturn("test"), + () => mutation.runAsPromise("test"), Error, "mutation failed", ); @@ -130,10 +127,9 @@ test("BlockingMutation - optimistic updates are applied immediately", async () = const [key, value] = args; helpers.setValue(key, value); }, - async refetch() {}, }); - const promise = mutation.runAndReturn("key1", "value1"); + const promise = mutation.runAsPromise("key1", "value1"); // Optimistic update should be applied synchronously assertEquals(testStore.get("key1"), "value1"); @@ -158,10 +154,9 @@ test("BlockingMutation - rollback on error", async () => { const [key, value] = args; helpers.setValue(key, value); }, - async refetch() {}, }); - await assertRejects(() => mutation.runAndReturn("key1", "value1")); + await assertRejects(() => mutation.runAsPromise("key1", "value1")); // Optimistic update should be rolled back assertEquals(testStore.has("key1"), false); @@ -182,10 +177,9 @@ test("BlockingMutation - onSuccess callback is called", async () => { successResults.push(result); }); }, - async refetch() {}, }); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); assertEquals(successResults, ["result-test"]); }); @@ -204,7 +198,7 @@ test("BlockingMutation - mutations with same key execute serially", async () => describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + refetchOnSuccess: false, key() { return "same-key"; @@ -212,8 +206,8 @@ test("BlockingMutation - mutations with same key execute serially", async () => }); // Start two mutations with the same key - const promise1 = mutation.runAndReturn("1"); - const promise2 = mutation.runAndReturn("2"); + const promise1 = mutation.runAsPromise("1"); + const promise2 = mutation.runAsPromise("2"); await Promise.all([promise1, promise2]); await delay(10); @@ -236,7 +230,7 @@ test("BlockingMutation - mutations with different keys execute in parallel", asy describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + key({ args }) { const [id] = args; return id; @@ -244,8 +238,8 @@ test("BlockingMutation - mutations with different keys execute in parallel", asy }); // Start two mutations with different keys - const promise1 = mutation.runAndReturn("key1"); - const promise2 = mutation.runAndReturn("key2"); + const promise1 = mutation.runAsPromise("key1"); + const promise2 = mutation.runAsPromise("key2"); await Promise.all([promise1, promise2]); @@ -263,7 +257,7 @@ test("BlockingMutation - key() returns JSON stringified key", () => { describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + key({ args }) { const [id] = args; return id; @@ -283,7 +277,6 @@ test("BlockingMutation - key() defaults to 'shared' when no key function", () => describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, }); assertEquals(mutation.key(["test-id"]), JSON.stringify("shared")); @@ -299,7 +292,7 @@ test("BlockingMutation - key() can return array", () => { describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + key({ args }) { const [userId, itemId] = args; return [userId, itemId]; @@ -322,7 +315,6 @@ test("BlockingMutation - describe() with string", () => { describe: "create item", describeResult: "Success", optimistic() {}, - async refetch() {}, }); assertEquals(mutation.describe("test"), "create item"); @@ -339,8 +331,8 @@ test("BlockingMutation - describe() with function", () => { const [id] = args; return `delete item ${id}`; }, + describeResult: null, optimistic() {}, - async refetch() {}, }); assertEquals(mutation.describe("123"), "delete item 123"); @@ -358,7 +350,7 @@ test("BlockingMutation - describe() receives context", () => { return `user ${userId} editing item ${id}`; }, optimistic() {}, - async refetch() {}, + describeResult: null, }); assertEquals( @@ -378,16 +370,17 @@ test("BlockingMutation - subscribe() tracks mutation events", async () => { }, describe: "test mutation", describeResult: "Success", - optimistic() {}, - async refetch() { - await delay(5); + optimistic({ onRefetch }) { + onRefetch(async () => { + await delay(5); + }); }, }); const key = mutation.key(["test"]); mutation.subscribe(key, tracker.callback); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); // Wait for refetch to complete await delay(20); @@ -409,7 +402,7 @@ test("BlockingMutation - unsubscribe stops receiving events", async () => { describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + refetchOnSuccess: false, }); @@ -418,7 +411,7 @@ test("BlockingMutation - unsubscribe stops receiving events", async () => { unsubscribe(); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); await delay(10); // Should not have received any events @@ -435,14 +428,15 @@ test("BlockingMutation - refetchOnSuccess can be disabled", async () => { }, describe: "test mutation", describeResult: "Success", - optimistic() {}, - async refetch() { - refetchCallCount++; + optimistic({ onRefetch }) { + onRefetch(async () => { + refetchCallCount++; + }); }, refetchOnSuccess: false, }); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); assertEquals(refetchCallCount, 0); }); @@ -457,13 +451,14 @@ test("BlockingMutation - refetch is called on error", async () => { }, describe: "failing mutation", describeResult: "Success", - optimistic() {}, - async refetch() { - refetchCallCount++; + optimistic({ onRefetch }) { + onRefetch(async () => { + refetchCallCount++; + }); }, }); - await assertRejects(() => mutation.runAndReturn("test")); + await assertRejects(() => mutation.runAsPromise("test")); assertEquals(refetchCallCount, 1); }); @@ -485,15 +480,15 @@ test("BlockingMutation - queued mutations are cancelled on error", async () => { describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + key() { return "same-key"; }, }); - const promise1 = mutation.runAndReturn("1"); - const promise2 = mutation.runAndReturn("2"); - const promise3 = mutation.runAndReturn("3"); + const promise1 = mutation.runAsPromise("1"); + const promise2 = mutation.runAsPromise("2"); + const promise3 = mutation.runAsPromise("3"); await assertRejects(() => promise1, Error, "first mutation failed"); await assertRejects(() => promise2, Error, "first mutation failed"); @@ -518,10 +513,9 @@ test("BlockingMutation - rollbacks are called in reverse order on error", async onRestore(() => rollbackOrder.push(2)); onRestore(() => rollbackOrder.push(3)); }, - async refetch() {}, }); - await assertRejects(() => mutation.runAndReturn("test")); + await assertRejects(() => mutation.runAsPromise("test")); // Rollbacks should be called in reverse order assertEquals(rollbackOrder, [3, 2, 1]); @@ -544,17 +538,17 @@ test("BlockingMutation - multiple mutations: rollbacks only affect failed mutati optimistic({ args: [id], onRestore }) { onRestore(() => rollbackOrder.push(`rollback-${id}`)); }, - async refetch() {}, + key() { return "same-key"; }, }); // First mutation succeeds - await mutation.runAndReturn("success"); + await mutation.runAsPromise("success"); // Second mutation fails - await assertRejects(() => mutation.runAndReturn("fail")); + await assertRejects(() => mutation.runAsPromise("fail")); // Only the failed mutation's rollback should be called // And all rollbacks from queued items @@ -574,10 +568,9 @@ test("BlockingMutation - onRestore throws error if called after optimistic phase optimistic({ onRestore }) { capturedOnRestore = onRestore; }, - async refetch() {}, }); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); // Calling onRestore after the optimistic phase should throw let error: Error | null = null; @@ -606,10 +599,9 @@ test("BlockingMutation - onSuccess throws error if called after optimistic phase optimistic({ onSuccess }) { capturedOnSuccess = onSuccess; }, - async refetch() {}, }); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); // Calling onSuccess after the optimistic phase should throw let error: Error | null = null; @@ -637,11 +629,10 @@ test("BlockingMutation - error during optimistic update is rejected immediately" optimistic() { throw new Error("optimistic update failed"); }, - async refetch() {}, }); await assertRejects( - () => mutation.runAndReturn("test"), + () => mutation.runAsPromise("test"), Error, "optimistic update failed", ); @@ -662,10 +653,9 @@ test("BlockingMutation - error during optimistic update rolls back registered ca onRestore(() => rollbackOrder.push(2)); throw new Error("optimistic update failed"); }, - async refetch() {}, }); - await assertRejects(() => mutation.runAndReturn("test")); + await assertRejects(() => mutation.runAsPromise("test")); // Rollbacks should be called even though optimistic update failed // Note: during optimistic error, rollbacks are executed in the order they were added @@ -681,14 +671,15 @@ test("BlockingMutation - refetch errors are reported but don't fail mutation", a }, describe: "test mutation", describeResult: "Success", - optimistic() {}, - async refetch() { - throw new Error("refetch failed"); + optimistic({ onRefetch }) { + onRefetch(async () => { + throw new Error("refetch failed"); + }); }, }); // Mutation should still succeed - const result = await mutation.runAndReturn("test"); + const result = await mutation.runAsPromise("test"); assertEquals(result, "test"); // But refetch error should be reported @@ -712,39 +703,14 @@ test("BlockingMutation - optimistic function receives args and helpers", async ( receivedArgs = args; receivedHelpers = helpers; }, - async refetch() {}, }); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); assertEquals(receivedArgs, ["test"]); assertEquals(typeof receivedHelpers, "object"); }); -test("BlockingMutation - refetch receives context and args", async () => { - const { client } = createTestClient(); - let receivedUserId: string | undefined; - let receivedArgs: unknown[] | undefined; - - const mutation = client.define({ - async mutate(_id: string, value: string) { - return value; - }, - describe: "test mutation", - describeResult: "Success", - optimistic() {}, - async refetch({ userId, args }) { - receivedUserId = userId; - receivedArgs = args; - }, - }); - - await mutation.runAndReturn("test-id", "test-value"); - - assertEquals(receivedUserId, "test-user"); - assertEquals(receivedArgs, ["test-id", "test-value"]); -}); - test("BlockingMutation - notifies error on mutation failure", async () => { const { client } = createTestClient(); const tracker = createEventTracker(); @@ -757,13 +723,12 @@ test("BlockingMutation - notifies error on mutation failure", async () => { describe: "failing mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, }); const key = mutation.key(["test"]); mutation.subscribe(key, tracker.callback); - await assertRejects(() => mutation.runAndReturn("test")); + await assertRejects(() => mutation.runAsPromise("test")); // Should have error event const errorEvents = tracker.events.filter((e) => @@ -786,7 +751,7 @@ test("BlockingMutation - multiple subscribers receive events", async () => { describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + refetchOnSuccess: false, }); @@ -794,7 +759,7 @@ test("BlockingMutation - multiple subscribers receive events", async () => { mutation.subscribe(key, tracker1.callback); mutation.subscribe(key, tracker2.callback); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); await delay(10); // Both subscribers should receive events @@ -817,11 +782,11 @@ test("BlockingMutation - onSuccess is called before mutation resolves", async () callOrder.push("onSuccess"); }); }, - async refetch() {}, + refetchOnSuccess: false, }); - const promise = mutation.runAndReturn("test"); + const promise = mutation.runAsPromise("test"); promise.then(() => { callOrder.push("then"); }); @@ -845,15 +810,12 @@ test("BlockingMutation - result is passed to notification on success", async () describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() { - await delay(5); - }, }); const key = mutation.key(["test"]); mutation.subscribe(key, tracker.callback); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); await delay(20); // Should have refetching event with result @@ -876,16 +838,16 @@ test("BlockingMutation - channel is reused for same key", async () => { describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + refetchOnSuccess: false, }); // First mutation - await mutation.runAndReturn("first"); + await mutation.runAsPromise("first"); await delay(5); // Second mutation with same key - await mutation.runAndReturn("second"); + await mutation.runAsPromise("second"); await delay(5); assertEquals(events, ["mutate-first", "mutate-second"]); @@ -902,7 +864,7 @@ test("BlockingMutation - empty queue after all mutations complete", async () => describe: "test mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + refetchOnSuccess: false, key() { return "test-key"; @@ -910,15 +872,15 @@ test("BlockingMutation - empty queue after all mutations complete", async () => }); // Run multiple mutations - await mutation.runAndReturn("1"); - await mutation.runAndReturn("2"); - await mutation.runAndReturn("3"); + await mutation.runAsPromise("1"); + await mutation.runAsPromise("2"); + await mutation.runAsPromise("3"); await delay(10); // All mutations should have completed // (We can't directly check the queue, but we can verify by running another mutation) const start = Date.now(); - await mutation.runAndReturn("4"); + await mutation.runAsPromise("4"); const duration = Date.now() - start; // Should execute immediately, not be queued (< 10ms if not queued) @@ -940,11 +902,11 @@ test("BlockingMutation - multiple onSuccess callbacks are all called", async () onSuccess((result) => results.push(`second-${result}`)); onSuccess((result) => results.push(`third-${result}`)); }, - async refetch() {}, + refetchOnSuccess: false, }); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); assertEquals(results, ["first-test", "second-test", "third-test"]); }); @@ -959,14 +921,15 @@ test("BlockingMutation - refetchOnSuccess false skips refetch", async () => { }, describe: "test mutation", describeResult: "Success", - optimistic() {}, - async refetch() { - refetchCalled = true; + optimistic({ onRefetch }) { + onRefetch(async () => { + refetchCalled = true; + }); }, refetchOnSuccess: false, }); - await mutation.runAndReturn("test"); + await mutation.runAsPromise("test"); await delay(10); // Refetch should not have been called @@ -982,14 +945,15 @@ test("BlockingMutation - refetch error after mutation failure is reported", asyn }, describe: "failing mutation", describeResult: "Success", - optimistic() {}, - async refetch() { - throw new Error("refetch also failed"); + optimistic({ onRefetch }) { + onRefetch(async () => { + throw new Error("refetch also failed"); + }); }, }); await assertRejects( - () => mutation.runAndReturn("test"), + () => mutation.runAsPromise("test"), Error, "mutation failed", ); @@ -1026,11 +990,11 @@ test("BlockingMutation - debounce: basic debounced execution", async () => { const [key, value] = args; helpers.setValue(key, value); }, - async refetch() {}, + debounceMs: 50, }); - const promise = mutation.runAndReturn("key1", "value1"); + const promise = mutation.runAsPromise("key1", "value1"); // Optimistic update should be applied immediately assertEquals(testStore.get("key1"), "value1"); @@ -1063,14 +1027,14 @@ test("BlockingMutation - debounce: last call wins with multiple rapid calls", as const [key, value] = args; helpers.setValue(key, value); }, - async refetch() {}, + debounceMs: 50, }); // Make three rapid calls - const promise1 = mutation.runAndReturn("key1", "a"); - const promise2 = mutation.runAndReturn("key1", "b"); - const promise3 = mutation.runAndReturn("key1", "c"); + const promise1 = mutation.runAsPromise("key1", "a"); + const promise2 = mutation.runAsPromise("key1", "b"); + const promise3 = mutation.runAsPromise("key1", "c"); // Last optimistic update should be applied assertEquals(testStore.get("key1"), "c"); @@ -1109,17 +1073,17 @@ test("BlockingMutation - debounce: optimistic rollback and reapply", async () => // Add a second value to test multiple rollbacks helpers.setValue(`${key}-2`, `${value}-2`); }, - async refetch() {}, + debounceMs: 50, }); // First call sets two values - mutation.runAndReturn("key1", "a"); + mutation.runAsPromise("key1", "a"); assertEquals(testStore.get("key1"), "a"); assertEquals(testStore.get("key1-2"), "a-2"); // Second call should rollback first call's optimistic and apply its own - const promise = mutation.runAndReturn("key1", "b"); + const promise = mutation.runAsPromise("key1", "b"); assertEquals(testStore.get("key1"), "b"); assertEquals(testStore.get("key1-2"), "b-2"); @@ -1144,16 +1108,16 @@ test("BlockingMutation - debounce: timer reset behavior", async () => { describe: "debounced mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + debounceMs: 100, }); // Call at t=0 - const promise1 = mutation.runAndReturn("first"); + const promise1 = mutation.runAsPromise("first"); // Call at t=50 (should reset timer) await delay(50); - const promise2 = mutation.runAndReturn("second"); + const promise2 = mutation.runAsPromise("second"); // At t=100, mutation should NOT have executed yet await delay(50); @@ -1180,18 +1144,18 @@ test("BlockingMutation - debounce: integration with blocking queue", async () => describe: "debounced mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + debounceMs: 30, key: () => "shared", }); // Start a debounced call that will enter queue first - const promise1 = mutation.runAndReturn("first"); + const promise1 = mutation.runAsPromise("first"); // While it's waiting in debounce, fire more debounced calls await delay(10); - const promise2 = mutation.runAndReturn("second"); - const promise3 = mutation.runAndReturn("third"); + const promise2 = mutation.runAsPromise("second"); + const promise3 = mutation.runAsPromise("third"); // Wait for all to complete await Promise.all([promise1, promise2, promise3]); @@ -1220,13 +1184,13 @@ test("BlockingMutation - debounce: error during optimistic update", async () => } helpers.setValue("key", value); }, - async refetch() {}, + debounceMs: 50, }); // Call that throws during optimistic await assertRejects( - () => mutation.runAndReturn("error"), + () => mutation.runAsPromise("error"), Error, "optimistic error", ); @@ -1235,7 +1199,7 @@ test("BlockingMutation - debounce: error during optimistic update", async () => assertEquals(testStore.has("key"), false); // Subsequent successful call should work - const promise = mutation.runAndReturn("good"); + const promise = mutation.runAsPromise("good"); assertEquals(testStore.get("key"), "good"); await promise; }); @@ -1251,10 +1215,12 @@ test("BlockingMutation - debounce: status transitions", async () => { }, describe: "debounced mutation", describeResult: "Success", - optimistic() {}, - async refetch() { - await delay(10); + optimistic({ onRefetch }) { + onRefetch(async () => { + await delay(10); + }); }, + debounceMs: 50, }); @@ -1262,7 +1228,7 @@ test("BlockingMutation - debounce: status transitions", async () => { const unsubscribe = mutation.subscribe(key, callback); // First call should transition to waiting - mutation.runAndReturn("test"); + mutation.runAsPromise("test"); await delay(10); assertEquals(events[events.length - 1].status, "waiting"); @@ -1291,19 +1257,20 @@ test("BlockingMutation - debounce: debounced call executes after queue error", a }, describe: "debounced mutation", describeResult: "Success", - optimistic() {}, - async refetch() { - await delay(10); + optimistic({ onRefetch }) { + onRefetch(async () => { + await delay(10); + }); }, debounceMs: 50, key: () => "shared", }); // Start a call that will fail (enters debounce) - const promise1 = mutation.runAndReturn("fail"); + const promise1 = mutation.runAsPromise("fail"); // Immediately override with a successful call (last call wins) - const promise2 = mutation.runAndReturn("success"); + const promise2 = mutation.runAsPromise("success"); // Both promises should resolve with the same successful result // (because debouncing causes "last call wins") @@ -1327,20 +1294,20 @@ test("BlockingMutation - debounce: all promises resolve together", async () => { describe: "debounced mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + debounceMs: 50, }); // Create three rapid calls - const promise1 = mutation.runAndReturn("id", "a").then((result) => { + const promise1 = mutation.runAsPromise("id", "a").then((result) => { resolvedAt.push(Date.now()); return result; }); - const promise2 = mutation.runAndReturn("id", "b").then((result) => { + const promise2 = mutation.runAsPromise("id", "b").then((result) => { resolvedAt.push(Date.now()); return result; }); - const promise3 = mutation.runAndReturn("id", "c").then((result) => { + const promise3 = mutation.runAsPromise("id", "c").then((result) => { resolvedAt.push(Date.now()); return result; }); @@ -1367,7 +1334,7 @@ test("BlockingMutation - debounce: cleanup on channel deletion", async () => { describe: "debounced mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + debounceMs: 100, }); @@ -1377,7 +1344,7 @@ test("BlockingMutation - debounce: cleanup on channel deletion", async () => { const unsubscribe = mutation.subscribe(key, () => {}); // Start a debounced call - mutation.runAndReturn("test"); + mutation.runAsPromise("test"); await delay(10); // Unsubscribe while debounce is pending @@ -1403,16 +1370,16 @@ test("BlockingMutation - debounce: multiple keys debounce independently", async describe: "debounced mutation", describeResult: "Success", optimistic() {}, - async refetch() {}, + debounceMs: 50, key: ({ args }) => args[0], }); // Rapid calls to different keys - const promise1a = mutation.runAndReturn("key1"); - const promise1b = mutation.runAndReturn("key1"); - const promise2a = mutation.runAndReturn("key2"); - const promise2b = mutation.runAndReturn("key2"); + const promise1a = mutation.runAsPromise("key1"); + const promise1b = mutation.runAsPromise("key1"); + const promise2a = mutation.runAsPromise("key2"); + const promise2b = mutation.runAsPromise("key2"); await Promise.all([promise1a, promise1b, promise2a, promise2b]); @@ -1437,15 +1404,15 @@ test("BlockingMutation - debounce: onSuccess callbacks from last call only", asy successResults.push(`${value}->${result}`); }); }, - async refetch() {}, + debounceMs: 50, }); // Make three rapid calls with different onSuccess callbacks await Promise.all([ - mutation.runAndReturn("a"), - mutation.runAndReturn("b"), - mutation.runAndReturn("c"), + mutation.runAsPromise("a"), + mutation.runAsPromise("b"), + mutation.runAsPromise("c"), ]); await delay(20); diff --git a/test/debounced.test.ts b/test/debounced.test.ts index de2eed583613c614f8a099a2464a7c27f6d7e15b..51789eedce8ed6d736b05a5a3a2a24afd4a6fe3e 100644 --- a/test/debounced.test.ts +++ b/test/debounced.test.ts @@ -89,7 +89,7 @@ test("DebouncedMutation - basic mutation success with debounce", async () => { }, }); - const result = await mutation.runAndReturn(5); + const result = await mutation.runAsPromise(5); await delay(20); // Wait for refetch assertEquals(result, 5); @@ -116,7 +116,6 @@ test("DebouncedMutation - run() catches errors", async () => { }, describe: "failing mutation", describeResult: "Success", - async refetch() {}, }); mutation.run(5); @@ -144,11 +143,10 @@ test("DebouncedMutation - runAndReturn() rejects on error", async () => { }, describe: "failing mutation", describeResult: "Success", - async refetch() {}, }); await assertRejects( - () => mutation.runAndReturn(5), + () => mutation.runAsPromise(5), Error, "commit failed", ); @@ -181,13 +179,12 @@ test("DebouncedMutation - debounce batches rapid calls", async () => { }, describe: "increment counter", describeResult: "Success", - async refetch() {}, }); // Rapid calls within debounce window - const promise1 = mutation.runAndReturn(1); - const promise2 = mutation.runAndReturn(2); - const promise3 = mutation.runAndReturn(3); + const promise1 = mutation.runAsPromise(1); + const promise2 = mutation.runAsPromise(2); + const promise3 = mutation.runAsPromise(3); // Optimistic updates should be applied immediately assertEquals(testStore.get("counter"), 6); @@ -223,17 +220,16 @@ test("DebouncedMutation - debounce resets timer on each call", async () => { }, describe: "increment counter", describeResult: "Success", - async refetch() {}, }); // First call - const promise1 = mutation.runAndReturn(1); + const promise1 = mutation.runAsPromise(1); // Wait less than debounce time await delay(15); // Second call should reset the timer - const promise2 = mutation.runAndReturn(2); + const promise2 = mutation.runAsPromise(2); // Wait less than debounce time again await delay(15); @@ -242,7 +238,7 @@ test("DebouncedMutation - debounce resets timer on each call", async () => { assertEquals(commitCallCount, 0); // Third call - const promise3 = mutation.runAndReturn(3); + const promise3 = mutation.runAsPromise(3); // Wait for all to complete await Promise.all([promise1, promise2, promise3]); @@ -274,15 +270,14 @@ test("DebouncedMutation - debounce separates batches after timeout", async () => }, describe: "increment counter", describeResult: "Success", - async refetch() {}, }); // First batch - await mutation.runAndReturn(1); + await mutation.runAsPromise(1); await delay(50); // Wait for first batch to complete // Second batch (after timeout) - await mutation.runAndReturn(2); + await mutation.runAsPromise(2); await delay(50); // Two separate commits @@ -319,10 +314,9 @@ test("DebouncedMutation - throttle commits immediately on first call", async () }, describe: "increment counter", describeResult: "Success", - async refetch() {}, }); - await mutation.runAndReturn(5); + await mutation.runAsPromise(5); // First call should commit immediately (within a small tolerance) assertEquals(commitTime < 20, true); @@ -352,19 +346,18 @@ test("DebouncedMutation - throttle batches calls within time window", async () = }, describe: "increment counter", describeResult: "Success", - async refetch() {}, }); // First call commits immediately - const promise1 = mutation.runAndReturn(1); + const promise1 = mutation.runAsPromise(1); await delay(5); // Second call within throttle window - should batch - const promise2 = mutation.runAndReturn(2); + const promise2 = mutation.runAsPromise(2); await delay(5); // Third call within throttle window - should batch with second - const promise3 = mutation.runAndReturn(3); + const promise3 = mutation.runAsPromise(3); // Wait for first to complete await promise1; @@ -403,11 +396,10 @@ test("DebouncedMutation - throttle allows new batch after time window", async () }, describe: "increment counter", describeResult: "Success", - async refetch() {}, }); // First call - await mutation.runAndReturn(1); + await mutation.runAsPromise(1); await delay(10); assertEquals(commitCallCount, 1); @@ -416,7 +408,7 @@ test("DebouncedMutation - throttle allows new batch after time window", async () await delay(60); // Second call should commit immediately - await mutation.runAndReturn(2); + await mutation.runAsPromise(2); await delay(10); assertEquals(commitCallCount, 2); @@ -447,12 +439,11 @@ test("DebouncedMutation - skips commit when value unchanged", async () => { }, describe: "increment counter", describeResult: "Success", - async refetch() {}, }); // +5 and -5 cancel out - const promise1 = mutation.runAndReturn(5); - const promise2 = mutation.runAndReturn(-5); + const promise1 = mutation.runAsPromise(5); + const promise2 = mutation.runAsPromise(-5); const [result1, result2] = await Promise.all([promise1, promise2]); @@ -507,11 +498,10 @@ test("DebouncedMutation - uses deepEquals for comparison", async () => { }, describe: "set count", describeResult: "Success", - async refetch() {}, }); // Set to same value (different object reference but same content) - await mutation.runAndReturn(0); + await mutation.runAsPromise(0); await delay(30); // Should skip commit because value is deeply equal @@ -559,10 +549,9 @@ test("DebouncedMutation - custom deepEquals function", async () => { }, describe: "failing mutation", describeResult: "Success", - async refetch() {}, }); - await mutation.runAndReturn(5).catch(() => { + await mutation.runAsPromise(5).catch(() => { // Expected to fail due to commit error }); await delay(30); @@ -593,11 +582,10 @@ test("DebouncedMutation - rollback on commit error", async () => { }, describe: "failing mutation", describeResult: "Success", - async refetch() {}, }); // Optimistic update applied - const promise = mutation.runAndReturn(5); + const promise = mutation.runAsPromise(5); assertEquals(testStore.get("counter"), 15); await assertRejects(() => promise, Error, "commit failed"); @@ -626,13 +614,12 @@ test("DebouncedMutation - error event includes error details", async () => { }, describe: "failing mutation", describeResult: "Success", - async refetch() {}, }); const key = mutation.key([5]); mutation.subscribe(key, tracker.callback); - await assertRejects(() => mutation.runAndReturn(5)); + await assertRejects(() => mutation.runAsPromise(5)); await delay(30); // Should have error in events @@ -660,7 +647,6 @@ test("DebouncedMutation - key() returns JSON stringified key", () => { }, describe: "test mutation", describeResult: "Success", - async refetch() {}, }); assertEquals(mutation.key(["test-id"]), JSON.stringify("test-id")); @@ -681,7 +667,6 @@ test("DebouncedMutation - key() can return array", () => { }, describe: "test mutation", describeResult: "Success", - async refetch() {}, }); assertEquals( @@ -712,12 +697,11 @@ test("DebouncedMutation - different keys create separate batches", async () => { }, describe: "test mutation", describeResult: "Success", - async refetch() {}, }); // Two different keys - const promise1 = mutation.runAndReturn("a", 5); - const promise2 = mutation.runAndReturn("b", 10); + const promise1 = mutation.runAsPromise("a", 5); + const promise2 = mutation.runAsPromise("b", 10); await Promise.all([promise1, promise2]); await delay(30); @@ -747,7 +731,6 @@ test("DebouncedMutation - describe() with string", () => { }, describe: "update counter", describeResult: "Success", - async refetch() {}, }); assertEquals(mutation.describe(5), "update counter"); @@ -768,7 +751,6 @@ test("DebouncedMutation - describe() with function", () => { }, describe: ({ args }) => `increment by ${args[0]}`, describeResult: "Success", - async refetch() {}, }); assertEquals(mutation.describe(5), "increment by 5"); @@ -796,12 +778,11 @@ test("DebouncedMutation - all pending promises resolve with same result", async }, describe: "increment counter", describeResult: "Success", - async refetch() {}, }); - const promise1 = mutation.runAndReturn(1); - const promise2 = mutation.runAndReturn(2); - const promise3 = mutation.runAndReturn(3); + const promise1 = mutation.runAsPromise(1); + const promise2 = mutation.runAsPromise(2); + const promise3 = mutation.runAsPromise(3); const [result1, result2, result3] = await Promise.all([ promise1, @@ -833,12 +814,11 @@ test("DebouncedMutation - all pending promises reject with same error", async () }, describe: "increment counter", describeResult: "Success", - async refetch() {}, }); - const promise1 = mutation.runAndReturn(1); - const promise2 = mutation.runAndReturn(2); - const promise3 = mutation.runAndReturn(3); + const promise1 = mutation.runAsPromise(1); + const promise2 = mutation.runAsPromise(2); + const promise3 = mutation.runAsPromise(3); const errors: Error[] = []; await Promise.all([ @@ -880,7 +860,7 @@ test("DebouncedMutation - handles empty getValue result", async () => { describeResult: "Success", }); - const result = await mutation.runAndReturn(5); + const result = await mutation.runAsPromise(5); await delay(30); assertEquals(commitCallCount, 1); @@ -906,15 +886,14 @@ test("DebouncedMutation - channel cleanup after idle with no listeners", async ( }, describe: "test mutation", describeResult: "Success", - async refetch() {}, }); // Run mutation without subscribing - await mutation.runAndReturn(5); + await mutation.runAsPromise(5); await delay(30); // Run another mutation - should work fine (channel recreated if needed) - const result = await mutation.runAndReturn(3); + const result = await mutation.runAsPromise(3); await delay(30); assertEquals(result, 3); @@ -943,10 +922,9 @@ test("DebouncedMutation - default time is 200ms", async () => { }, describe: "test mutation", describeResult: "Success", - async refetch() {}, }); - await mutation.runAndReturn(5); + await mutation.runAsPromise(5); // Should commit after ~200ms (with some tolerance) assertEquals(commitTime !== null, true); @@ -977,10 +955,9 @@ test("DebouncedMutation - context is passed to getValue", async () => { }, describe: "test mutation", describeResult: "Success", - async refetch() {}, }); - await mutation.runAndReturn(5); + await mutation.runAsPromise(5); await delay(30); assertEquals(receivedUserId, "test-user"); @@ -1007,10 +984,9 @@ test("DebouncedMutation - context is passed to commit", async () => { }, describe: "test mutation", describeResult: "Success", - async refetch() {}, }); - await mutation.runAndReturn(5); + await mutation.runAsPromise(5); await delay(30); assertEquals(receivedUserId, "test-user"); @@ -1037,12 +1013,11 @@ test("DebouncedMutation - first args are used for commit", async () => { }, describe: "test mutation", describeResult: "Success", - async refetch() {}, }); - mutation.runAndReturn("first", 1); - mutation.runAndReturn("second", 2); - await mutation.runAndReturn("third", 3); + mutation.runAsPromise("first", 1); + mutation.runAsPromise("second", 2); + await mutation.runAsPromise("third", 3); await delay(10); // Should use first args