-
Notifications
You must be signed in to change notification settings - Fork 812
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.