TanStack / TanStack/db

CleanupQueue schedules GC deadlines on wall-clock time but services them with an elapsed-time timer

Open
#1,796 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
3.9k
Forks
266
Avg merge
1d 4h
Merged PRs (30d)
55

Description

CleanupQueue schedules GC deadlines on wall-clock time but services them with an elapsed-time timer

Package: @tanstack/db 0.8.7 — packages/db/src/collection/cleanup-queue.ts (unchanged on main at the time of writing).

What happens

CleanupQueue.schedule records executeAt = Date.now() + gcTime. updateTimeout arms setTimeout(process, executeAt - Date.now()), and process runs a task only when Date.now() >= task.executeAt, otherwise re-arming for the remainder.

setTimeout measures elapsed time; Date.now() is the realtime clock. When realtime moves backwards between schedule and process — an NTP step, a VM or container guest resynchronising with its host, a manual clock change — process finds now < executeAt and re-arms for executeAt - now, which is roughly the size of the step. Every pending cleanup is delayed by that amount regardless of its gcTime.

For a live query collection this delays the release of its subscriptions to its source collections: a component unmounts, the live query's gcTime (1 ms under @tanstack/react-db's useLiveQuery) elapses, and the source still counts it as a subscriber seconds later.

How it was observed

In a Vitest lane that waits for source collections to reach zero subscribers before disposing them, the wait timed out intermittently with the queue holding one task whose executeAt was 1.7–2.7 s ahead of Date.now() and its timer armed for that difference. A sampler on the same machine (a WSL2 guest with an in-guest time-sync service) recorded the realtime clock stepping back ~3.7 s periodically while performance.now() advanced steadily. The environment is only how it was noticed; any realtime step reproduces it.

Deterministic reproduction

import { createCollection, createLiveQueryCollection } from '@tanstack/db'

const source = createCollection<{ id: string }>({
  id: 'source',
  getKey: (row) => row.id,
  startSync: true,
  sync: { sync: ({ markReady }) => { markReady(); return () => {} } },
})
const live = createLiveQueryCollection({
  query: (q) => q.from({ s: source }),
  gcTime: 1,
  startSync: true,
})
const subscription = live.subscribeChanges(() => {})
console.log(source.subscriberCount) // 1

subscription.unsubscribe() // live query's GC is scheduled: executeAt = Date.now() + 1
const realNow = Date.now
Date.now = () => realNow() - 3000 // realtime steps back 3 s before the timer fires

setTimeout(() => console.log(source.subscriberCount), 100) // still 1
setTimeout(() => console.log(source.subscriberCount), 3500) // 0 — released ~3 s late

With a monotonic deadline the second log would already read 0 at 100 ms.

Suggested property

Deadlines and the timer that services them should share a clock: compute executeAt from a monotonic elapsed-time source (for example performance.now(), with Date.now() only as a fallback where it is unavailable) so that a realtime step cannot move a scheduled cleanup. The specific implementation is up to you; the property is that process runs a task once the elapsed time since schedule reaches gcTime.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/db/src/collection/cleanup-queue.ts and trace schedule, updateTimeout, and process. Reproduce the clock-step scenario with the provided deterministic example, then verify that deadlines and timer servicing use the same elapsed-time clock. Done means cleanup runs after gcTime despite Date.now() moving backwards, with the relevant Vitest behavior passing.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
performance
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.