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
7 changes: 7 additions & 0 deletions .changeset/skip-gc-on-pause.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"fetchium": patch
---

Paused queries no longer schedule garbage collection, so resuming reuses the cached result instead of refetching. Previously a paused query with a low `gcTime` was evicted immediately and recreated on resume, dropping its polling interval. GC still runs on genuine teardown.

This relies on the `isPausing` flag signalium passes to `RelayHooks.deactivate` (see https://github.com/Signalium/signalium/pull/242), so the `signalium` peer requirement is now `>=3.0.3` (was `>=3.0.1`).
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/fetchium/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
},
"peerDependencies": {
"react": ">=19.0.0",
"signalium": ">=3.0.1"
"signalium": ">=3.0.3"
},
"peerDependenciesMeta": {
"react": {
Expand Down Expand Up @@ -176,7 +176,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rollup-plugin-const-enum": "^1.1.4",
"signalium": "3.0.1",
"signalium": "3.0.3",
"vite": "^7.1.2",
"vite-plugin-babel": "^1.3.0",
"vite-plugin-dts": "^4.5.4",
Expand Down
17 changes: 14 additions & 3 deletions packages/fetchium/src/QueryResult.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { relay, reactiveSignal, type RelayState, ReactivePromise, type ReadonlySignal } from 'signalium';
import {
relay,
reactiveSignal,
type RelayState,
ReactivePromise,
type ReadonlySignal,
type DeactivateOptions,
} from 'signalium';
import { NetworkMode, type QueryResult, type EntityDef } from './types.js';
import {
type QueryClient,
Expand Down Expand Up @@ -113,7 +120,9 @@ export class QueryInstance<T extends Query> {
state => {
this._relayState = state;

const deactivate = () => {
// When pausing (vs a genuine cleanup) we tear down the fetch/subscription
// but skip GC, so resuming reuses the cached result instead of refetching.
const deactivate = ({ isPausing = false }: DeactivateOptions = {}) => {
this._isActive = false;

clearTimeout(this.debounceTimer);
Expand All @@ -130,6 +139,8 @@ export class QueryInstance<T extends Query> {
this.unsubscribe = undefined;
this.lastSubscribeFn = undefined;

if (isPausing) return;

const gcTime = this.config?.gcTime ?? DEFAULT_GC_TIME;
this.queryClient.gcManager.schedule(this.queryKey, gcTime, GcKeyType.Query);
};
Expand All @@ -139,7 +150,7 @@ export class QueryInstance<T extends Query> {
this.wasPaused = isPaused;

if (isPaused && !wasPaused && initialized) {
deactivate();
deactivate({ isPausing: true });
return;
}

Expand Down
39 changes: 38 additions & 1 deletion packages/fetchium/src/__tests__/network-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { QueryClient, QueryClientContext } from '../QueryClient.js';
import { SyncQueryStore, MemoryPersistentStore } from '../stores/sync.js';
import { RESTQuery } from '../rest/index.js';
import { fetchQuery } from '../query.js';
import { fetchQuery, queryKeyForClass } from '../query.js';
import { NetworkManager } from '../NetworkManager.js';
import { NetworkMode } from '../types.js';
import { createMockFetch, testWithClient, sleep, createTestWatcher } from './utils.js';
Expand Down Expand Up @@ -596,4 +596,41 @@ describe('Network Mode', () => {
});
});
});

describe('pause does not schedule garbage collection', () => {
// A temporary pause must not schedule GC (which would evict the cache and
// force a refetch on resume); a genuine teardown still does.
it('skips GC when an active query is paused offline, but schedules it on teardown', async () => {
mockFetch.get('/users/1', { id: '1', name: 'Alice' });

class GetUser extends RESTQuery {
path = '/users/1';
result = t.object({ id: t.string, name: t.string });
config = { networkMode: NetworkMode.Online, staleTime: 0 };
}

const queryKey = queryKeyForClass(GetUser, undefined);
const scheduleSpy = vi.spyOn(client.gcManager, 'schedule');
const queryGcCalls = () => scheduleSpy.mock.calls.filter(call => call[0] === queryKey).length;

const { unsub } = withContexts([[QueryClientContext, client]], () =>
createTestWatcher(() => fetchQuery(GetUser).value),
);

await sleep(50);
expect(client.queryInstances.has(queryKey)).toBe(true);
expect(queryGcCalls()).toBe(0);

// Network-driven pause: tears down the active fetch but must not GC.
networkManager.setNetworkStatus(false);
await sleep(20);
expect(queryGcCalls()).toBe(0);
expect(client.queryInstances.has(queryKey)).toBe(true);

// Genuine teardown (last watcher removed) schedules GC as before.
unsub();
await sleep(20);
expect(queryGcCalls()).toBeGreaterThan(0);
});
});
});
Loading