clockworklabs / clockworklabs/SpacetimeDB
useTable: rapid query parameter changes cause 'Subscription not found' errors
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 25.2k
- Forks
- 1.1k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 46
Description
Summary
useTable with dynamic query parameters (e.g. a prop that changes frequently) causes Subscription not found errors:
Error: Subscription not found: ((Identity(...), ConnectionId(...)), QuerySetId { id: 11745 })
Reproduction
A React component that subscribes to a filtered query where the filter value comes from a prop that changes rapidly (e.g. as the page scrolls):
const ClickerGame: React.FC<{ plotId: bigint }> = ({ plotId }) => {
const [gameStates] = useTable(tables.game_clicker.where(game => game.plotId.eq(plotId)));
// ...
};
When multiple instances of this component are mounted and plotId changes on scroll, the error spams continuously.
Root Cause
Each render where plotId changes produces a new SQL string (e.g. SELECT * FROM "ClickerGame" WHERE "ClickerGame"."plot_id" = 5 then ... = 6, ... = 7, etc.). The useEffect in useTable has querySql as a dependency, so every change triggers:
- Cleanup:
cancel.unsubscribe()for the old QuerySetId - New effect:
connection.subscriptionBuilder().subscribe(newSql)→ assigns a new QuerySetId
When the prop changes on every frame during a scroll, this creates dozens of subscribe/unsubscribe round-trips per second. The server processes an unsubscribe for a QuerySetId that was already removed (or races with a previous unsubscribe) and returns the error.
Proposed Fixes
Two independent improvements:
1. Resilient unsubscribe (error suppression)
unsubscribe() should treat "subscription not found" as a benign no-op rather than surfacing it as an error. The subscription is already gone, which is the desired outcome.
2. Debounce query changes in useTable
When querySql changes rapidly, useTable should debounce the subscribe/unsubscribe cycle (e.g. 100-200ms). This avoids flooding the server with subscription churn from props that change every frame.
In the meantime, the where clause also filters client-side, so while a subscription is in flight, the hook still returns correct results from the local cache.
Current Workaround
Subscribe to all rows and filter in the component:
const [allGames] = useTable(tables.game_clicker);
const gameState = allGames.find(g => g.plotId === plotId);
This avoids subscription churn entirely since the SQL does not change.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the useTable hook and the unsubscribe and subscriptionBuilder paths described in the issue. Reproduce rapid query changes with multiple components, then verify that subscription churn is reduced and a missing subscription during cleanup no longer surfaces as an error. Confirm the existing client-side filtering still returns correct results while a subscription is in flight.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, sql, typescript
- Domain
- databases, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100