Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-relay-deactivation-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fetchium": patch
---

Fix relay not recovering after deactivation during in-flight refetch
16 changes: 13 additions & 3 deletions packages/fetchium/src/QueryResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,19 @@ export class QueryInstance<T extends Query> {
this.setupSubscription();
}

const refreshStaleOnReconnect = this.config?.refreshStaleOnReconnect ?? true;
if (refreshStaleOnReconnect && this.isStale) {
this.runDebounced();
// If the relay shows pending but the abort controller is gone, the
// previous fetch was aborted during deactivation. runDebounced()
// would bail out because isPending is still true from the doomed
// promise. Force an immediate refetch so the new setPromise() call
// replaces _promise, causing the stale AbortError rejection to hit
// the `promise !== this._promise` guard and be silently ignored.
if (this.relayState.isPending && this._abortController === undefined) {
this.runQueryImmediately();
} else {
const refreshStaleOnReconnect = this.config?.refreshStaleOnReconnect ?? true;
if (refreshStaleOnReconnect && this.isStale) {
this.runDebounced();
}
}
} else if (paramsDidChange) {
this.setupSubscription();
Expand Down
57 changes: 56 additions & 1 deletion packages/fetchium/src/__tests__/reactivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { t } from '../typeDefs.js';
import { RESTQuery } from '../rest/index.js';
import { fetchQuery } from '../query.js';
import { watcher, reactive } from 'signalium';
import { testWithClient, setupTestClient } from './utils.js';
import { testWithClient, sleep, setupTestClient } from './utils.js';

/**
* Signalium Reactivity Tests
Expand Down Expand Up @@ -72,6 +72,61 @@ describe('Signalium Reactivity', () => {
expect(relay.error).toBe(error);
});
});

it('should recover after deactivation during in-flight refetch', async () => {
const { client, mockFetch } = getClient();

class GetPrice extends RESTQuery {
path = '/price';
result = { price: t.number };
config = { staleTime: 50, retry: false };
}

// Phase 1: Initial fetch succeeds
mockFetch.get('/price', { price: 100 });

await testWithClient(client, async () => {
const relay = fetchQuery(GetPrice);
await relay;
expect(relay.value!).toMatchObject({ price: 100 });
});

// Wait for staleTime to expire
await sleep(100);

// Phase 2: Resubscribe (triggers stale refetch), then deactivate mid-flight.
// The long delay ensures the fetch is still in-flight when the watcher ends.
mockFetch.get('/price', { price: 200 }, { delay: 500 });

await testWithClient(client, async () => {
const relay = fetchQuery(GetPrice);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- trigger stale refetch
relay.value;
await sleep(20); // Let setTimeout(0) fire so the refetch starts
// testWithClient ends here -> watcher unsubscribes -> relay deactivates
// -> AbortController.abort() -> in-flight fetch will reject with AbortError
});

// Let the AbortError microtask settle
await sleep(50);

// Phase 3: Resubscribe again. The relay must recover and fetch fresh data
// instead of being permanently stuck with the AbortError.
mockFetch.get('/price', { price: 300 }, { delay: 50 });

await testWithClient(client, async () => {
const relay = fetchQuery(GetPrice);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- trigger reactivation
relay.value;

// Wait for the recovery fetch to complete
await sleep(200);

expect(relay.isPending).toBe(false);
expect(relay.isRejected).toBe(false);
expect(relay.value!).toMatchObject({ price: 300 });
});
});
});

describe('Reactive Computations', () => {
Expand Down
Loading