authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-28 18:08:10-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-28 18:51:56-08:00
logc384cb9c7ac09948889010176f298fb775481b4b
tree1569b005e34ffedbe1cb6c8cc5a70bdfd396cb5f
parent2486cbb9d095be3d97251f02f076d0e2a1cb0033
signaturelock-open Commit is signed but in an unrecognized format.

feat: some more things


4 files changed, 113 insertions(+), 18 deletions(-)

src/blocking.ts+10
......@@ -70,6 +70,8 @@ export type BlockingOptimisticContext<
7070 onRestore: (cb: () => void) => void;
7171 /** Add an event listener to apply `Result` to the store. */
7272 onSuccess: (cb: (result: Result) => void) => void;
73 /** Add an event listener to refetch data after mutation. */
74 onRefetch: (cb: () => Promise<void>) => void;
7375};
7476
7577interface BlockingChannel<Args extends unknown[], Result, OptimisticHelpers> {
......@@ -245,6 +247,14 @@ export class BlockingMutation<
245247 }
246248 onSuccess.push(cb);
247249 },
250 onRefetch(cb) {
251 if (expired) {
252 throw new Error(
253 "Can only call onRefetch from within the optimistic update function.",
254 );
255 }
256 channel.refetches.push(cb);
257 },
248258 });
249259 } catch (error) {
250260 expired = true;
src/debounced.ts+7-2
......@@ -72,7 +72,9 @@ export interface DebouncedMutationOptions<
7272 /**
7373 * Refetch all of the data this mutation could have affected.
7474 */
75 refetch?: () => Promise<void>;
75 refetch?: (
76 context: Config["context"] & { args: NoInfer<Args> },
77 ) => Promise<void>;
7678}
7779
7880export type DebouncedOptimisticContext<Config extends MutationClientConfig> =
......@@ -501,7 +503,10 @@ export class DebouncedMutation<
501503 this.#notify(channel, "refetching", result);
502504 // Call refetch and all refetch callbacks in parallel
503505 Promise.allSettled([
504 this.#options.refetch?.(),
506 this.#options.refetch?.({
507 ...this.#client.context,
508 args: firstArgs,
509 }),
505510 ...refetchCallbacks.map((cb) => cb()),
506511 ]).then((results) => {
507512 // Report any errors from refetch or callbacks
src/tanstack-query.ts+25-16
......@@ -49,17 +49,7 @@ class TanstackQueryOptimisticHelpers {
4949 if (result) {
5050 this.#client.cancelQueries({ queryKey: query.queryKey, exact: true })
5151 .catch(() => {});
52 const state = this.#client.getQueryCache().find({
53 queryKey: query.queryKey,
54 exact: true,
55 });
56 if (!state || this.#refetchHashes.includes(state.queryHash)) return;
57 this.#refetchHashes.push(state.queryHash);
58 try {
59 this.#onRefetch(async () => {
60 await this.#client.refetchQueries(query);
61 });
62 } catch { /* Ignore expiry */ }
52 this.refetchOnSettled(query);
6353 }
6454 }
6555
......@@ -111,6 +101,25 @@ class TanstackQueryOptimisticHelpers {
111101 });
112102 }
113103
104 /**
105 * Mark a query as changed and schedule a refetch.
106 * Does not modify any data.
107 * If the query doesn't exist, the updater is skipped.
108 */
109 refetchOnSettled(queryKey: QueryKeyAndFn) {
110 const state = this.#client.getQueryCache().find({
111 queryKey: queryKey.queryKey,
112 exact: true,
113 });
114 if (!state || this.#refetchHashes.includes(state.queryHash)) return;
115 this.#refetchHashes.push(state.queryHash);
116 try {
117 this.#onRefetch(async () => {
118 await this.#client.refetchQueries(queryKey);
119 });
120 } catch { /* Ignore expiry */ }
121 }
122
114123 /**
115124 * Set a property at an object path.
116125 * If the query or path doesn't exist, the updater is skipped.
......@@ -413,7 +422,7 @@ class TanstackQueryOptimisticHelpers {
413422 * Push item(s) to the end of an array at an object path.
414423 * If the query or path doesn't exist, the updater is skipped.
415424 */
416 arrayPush<Data>(queryKey: QueryKeyAndFn<Data[]>, ...items: Data[]) {
425 arrayPush<Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) {
417426 const prev = this.#get(queryKey);
418427 if (!prev || !Array.isArray(prev)) return;
419428
......@@ -431,7 +440,7 @@ class TanstackQueryOptimisticHelpers {
431440 * Add item(s) to the beginning of an array at an object path.
432441 * If the query or path doesn't exist, the updater is skipped.
433442 */
434 arrayUnshift<Data>(queryKey: QueryKeyAndFn<Data[]>, ...items: Data[]) {
443 arrayUnshift<Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) {
435444 const prev = this.#get(queryKey);
436445 if (!prev || !Array.isArray(prev)) return;
437446
......@@ -450,7 +459,7 @@ class TanstackQueryOptimisticHelpers {
450459 * If the query or path doesn't exist, the updater is skipped.
451460 */
452461 arrayRemove<Data>(
453 queryKey: QueryKeyAndFn<Data[]>,
462 queryKey: QueryKeyAndFn<Data[] | null>,
454463 filter: (
455464 item: Data,
456465 index: number,
......@@ -472,7 +481,7 @@ class TanstackQueryOptimisticHelpers {
472481 * If the query or path doesn't exist, the updater is skipped.
473482 */
474483 arrayUpdate<Data>(
475 queryKey: QueryKeyAndFn<Data[]>,
484 queryKey: QueryKeyAndFn<Data[] | null>,
476485 {
477486 filter,
478487 update,
......@@ -499,7 +508,7 @@ class TanstackQueryOptimisticHelpers {
499508 * If the query or path doesn't exist, the updater is skipped.
500509 */
501510 arrayInsertIndex<Data>(
502 queryKey: QueryKeyAndFn<Data[]>,
511 queryKey: QueryKeyAndFn<Data[] | null>,
503512 index: number,
504513 ...items: Data[]
505514 ) {
test/tanstack-query-helpers.test.ts+71
......@@ -779,6 +779,77 @@ test("removeQuery - should skip if query doesn't exist", () => {
779779 assertEquals(restoreFns.length, 0);
780780});
781781
782// ============================================================================
783// refetchOnSettled() tests
784// ============================================================================
785
786test("refetchOnSettled - should schedule a refetch without modifying data", () => {
787 const { client, queryTest } = createTestQueryClient();
788 const refetchFns: Array<() => void> = [];
789 const restoreFns: Array<() => void> = [];
790
791 const helpers = queryClientOptimisticHelpers(client)({
792 onRestore: (fn) => restoreFns.push(fn),
793 onRefetch: (fn) => refetchFns.push(fn),
794 });
795
796 const originalData = client.getQueryData<TestData>(queryTest.queryKey);
797
798 helpers.refetchOnSettled(queryTest);
799
800 const result = client.getQueryData<TestData>(queryTest.queryKey);
801 // Data should not be modified
802 assertEquals(result, originalData);
803
804 // A refetch should have been scheduled
805 assertEquals(refetchFns.length, 1);
806
807 // No restore callbacks should be registered
808 assertEquals(restoreFns.length, 0);
809});
810
811test("refetchOnSettled - should skip if query doesn't exist", () => {
812 const { client } = createTestQueryClient();
813 const refetchFns: Array<() => void> = [];
814 const restoreFns: Array<() => void> = [];
815
816 const helpers = queryClientOptimisticHelpers(client)({
817 onRestore: (fn) => restoreFns.push(fn),
818 onRefetch: (fn) => refetchFns.push(fn),
819 });
820
821 const queryNonexistent = queryOptions({
822 queryKey: ["nonexistent"],
823 queryFn: (): TestData => initialData,
824 });
825
826 helpers.refetchOnSettled(queryNonexistent);
827
828 // No refetch should be scheduled for non-existent query
829 assertEquals(refetchFns.length, 0);
830 assertEquals(restoreFns.length, 0);
831});
832
833test("refetchOnSettled - should deduplicate refetches for the same query", () => {
834 const { client, queryTest } = createTestQueryClient();
835 const refetchFns: Array<() => void> = [];
836 const restoreFns: Array<() => void> = [];
837
838 const helpers = queryClientOptimisticHelpers(client)({
839 onRestore: (fn) => restoreFns.push(fn),
840 onRefetch: (fn) => refetchFns.push(fn),
841 });
842
843 // Call refetchOnSettled multiple times on the same query
844 helpers.refetchOnSettled(queryTest);
845 helpers.refetchOnSettled(queryTest);
846 helpers.refetchOnSettled(queryTest);
847
848 // Only one refetch should be scheduled (deduplicated by query hash)
849 assertEquals(refetchFns.length, 1);
850 assertEquals(restoreFns.length, 0);
851});
852
782853// ============================================================================
783854// Integration tests
784855// ============================================================================