Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Worker: a tab that closes while waiting for the query/transaction lock leaks it — every later query hangs

Open
#1,116 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@Fatin-Ishraq is already working on this.

Since Oct 17, 2026.

  • #1146 by @Fatin-Ishraq — open

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
typescript

Research direction

Read makeWorkerApi in src/worker/index.ts and trace _acquireQueryLock and _acquireTransactionLock alongside the tab-close cleanup. Reproduce the two-tab navigation scenario, then verify that a lock granted after the waiting tab closes is released and a later tab's query completes without a closed-channel error.

Written by the indexing model from the issue text.

Description

Summary

In the multi-tab worker (@electric-sql/pglite/worker), if a tab closes or navigates away while it is waiting for the query or transaction lock, the leader grants that lock after the tab's cleanup has already run. Nothing ever releases it, so every later query from every tab waits forever.

Reproduced with 0.5.5. The same code is in 0.5.8 (makeWorkerApi in src/worker/index.ts).

What happens

makeWorkerApi() releases a departed tab's locks when that tab's close lock frees up:

acquireLock(tabCloseLockId).then(() => {
  if (transactionLockRelease) db.exec('ROLLBACK')
  queryLockRelease?.()
  transactionLockRelease?.()
})

That only covers locks the tab holds. Here's the sequence when it was still waiting:

  1. The tab calls _acquireQueryLock over RPC. Another tab holds the lock, so db._runExclusiveQuery(...) queues it.
  2. The tab navigates away. Its close lock is released, and the handler above runs. queryLockRelease is still null, so nothing is released.
  3. The queued exclusive query then runs and stores queryLockRelease = release. It answers the RPC on the tab's channel, which connectTab has already closed. The leader logs InvalidStateError: Failed to execute 'postMessage' on 'BroadcastChannel': Channel is closed.
  4. Nobody ever calls release. The query lock stays held, and every other tab's queries hang. A new tab can connect, but its first query never returns.

The same applies to _acquireTransactionLock.

Reproduction

This happens in a real app with two tabs of the same origin sharing one PGliteWorker. Tab A is the leader.

  1. Tab B opens and starts issuing queries.
  2. While those queries are in flight, tab B does a full navigation to another page that also uses PGlite.
  3. Tab B's new page never gets a result.

With a 20 s budget we hit this in 8/8 runs in Chromium, and intermittently in Firefox and WebKit.

Fix we're running

Remember that the tab closed, and release any lock granted afterwards straight away:

function makeWorkerApi(tabId: string, db: PGlite) {
  let queryLockRelease: (() => void) | null = null
  let transactionLockRelease: (() => void) | null = null
  let tabClosed = false

  acquireLock(`pglite-tab-close:${tabId}`).then(() => {
    tabClosed = true
    if (transactionLockRelease) db.exec('ROLLBACK')
    queryLockRelease?.()
    transactionLockRelease?.()
  })

  return {
    // ...
    async _acquireQueryLock() {
      return new Promise<void>((resolve) => {
        db._runExclusiveQuery(() => {
          return new Promise<void>((release) => {
            if (tabClosed) { release(); resolve(); return }
            queryLockRelease = release
            resolve()
          })
        })
      })
    },
    // same guard in _acquireTransactionLock
  }
}

With this patch the hang is gone: 3/3 passes in each of Chromium, Firefox and WebKit, and the new tab renders in about 1 s. With the patch disabled, the hang and the Channel is closed error come back.

#1112 is related, but it's the tab-side notify loop posting after close(). This one is the leader-side lock leak.

Dominant language
TypeScript
Stars
16.1k
Forks
456
Avg merge
1d 7h
Merged PRs (30d)
15

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

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.

More from electric-sql/pglite

All issues in electric-sql/pglite

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.