Passing a specific callback to unsubscribe() on a live query removes every subscriber, not just that callback, and destroys the query's backing state. With two callbacks subscribed to one live.query, calling unsubscribe(cbA) and then committing a mutation delivers nothing to cbB; a second mutation delivers nothing either, so the query is fully torn down rather than left running for the remaining subscriber.
Environment:
@electric-sql/pglite 0.5.4 (latest on npm), also present on current main
- Node v22.23.1, Linux
Reproduction:
npm i @electric-sql/pglite@0.5.4
node repro.mjs
repro.mjs
import { PGlite } from '@electric-sql/pglite'
import { live } from '@electric-sql/pglite/live'
const db = await PGlite.create({ extensions: { live } })
await db.exec(`CREATE TABLE t (id int primary key, v text);`)
let aCalls = 0, bCalls = 0
const cbA = () => { aCalls++ }
const cbB = () => { bCalls++ }
const sub = await db.live.query('SELECT * FROM t ORDER BY id', [], cbA)
sub.subscribe(cbB)
await new Promise((r) => setTimeout(r, 100))
// remove only cbA; cbB is still a subscriber
await sub.unsubscribe(cbA)
const bBefore = bCalls
await db.exec(`INSERT INTO t VALUES (1, 'a');`)
await new Promise((r) => setTimeout(r, 100))
console.log(`cbB fired ${bCalls - bBefore} time(s) after the INSERT; expected 1`)
await db.close()
Observed output:
cbB fired 0 time(s) after the INSERT; expected 1
Expected: cbB should keep receiving updates. The live-query docs describe multiple consumers sharing one live query, and the API exposes unsubscribe(callback) specifically to remove one callback while leaving the others subscribed. https://pglite.dev/docs/live-queries
This appears to come from the selective-unsubscribe filter in packages/pglite/src/live/index.ts (lines 243, 506, 666), which reads callbacks = callbacks.filter((callback) => callback !== callback). The arrow's parameter shadows the outer callback, so the predicate is always false, the filtered array is empty, and the length === 0 branch then drops the view and deallocates the query's state. Binding the predicate to the outer callback ((cb) => cb !== callback) would remove only the named one.
Passing a specific callback to
unsubscribe()on a live query removes every subscriber, not just that callback, and destroys the query's backing state. With two callbacks subscribed to onelive.query, callingunsubscribe(cbA)and then committing a mutation delivers nothing tocbB; a second mutation delivers nothing either, so the query is fully torn down rather than left running for the remaining subscriber.Environment:
@electric-sql/pglite0.5.4 (latest on npm), also present on currentmainReproduction:
repro.mjs
Observed output:
Expected:
cbBshould keep receiving updates. The live-query docs describe multiple consumers sharing one live query, and the API exposesunsubscribe(callback)specifically to remove one callback while leaving the others subscribed. https://pglite.dev/docs/live-queriesThis appears to come from the selective-unsubscribe filter in
packages/pglite/src/live/index.ts(lines 243, 506, 666), which readscallbacks = callbacks.filter((callback) => callback !== callback). The arrow's parameter shadows the outercallback, so the predicate is always false, the filtered array is empty, and thelength === 0branch then drops the view and deallocates the query's state. Binding the predicate to the outer callback ((cb) => cb !== callback) would remove only the named one.