clockworklabs / clockworklabs/SpacetimeDB

useTable: rapid query parameter changes cause 'Subscription not found' errors

Open
#4,528 1 comment 1 reaction 0 assignees View on GitHub

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:

  1. Cleanup: cancel.unsubscribe() for the old QuerySetId
  2. 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

  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 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.