electric-sql / electric-sql/pglite

unsubscribe(callback) on a shared live query removes all subscribers and tears down the query

Open Beginner friendly
#1,048 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
16k
Forks
442
Avg merge
20h 19m
Merged PRs (30d)
7

Description

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

```js
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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in packages/pglite/src/live/index.ts, especially the selective-unsubscribe logic at lines 243, 506, and 666, and reproduce the behavior with the provided repro.mjs. Verify that unsubscribing cbA removes only that callback, leaves the shared query running, and allows cbB to receive the subsequent INSERT update.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgres, typescript
Domain
databases
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.