[PR #7172] [MERGED] 🐛 Using a shared worker to coordinate multiple tabs #32910

Closed
opened 2026-04-18 08:52:43 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/actualbudget/actual/pull/7172
Author: @MikesGlitch
Created: 3/11/2026
Status: Merged
Merged: 3/17/2026
Merged by: @MikesGlitch

Base: masterHead: multiple-tabs-open


📝 Commits (10+)

  • 10e8253 attempt to enable sync when multiple tabs are open
  • 528eb14 allow multiple tabs to work
  • 02fceb4 release notes
  • 42471cb rehome the host if the tab closes
  • 45992d2 ensure new tabs always receive failure messages by broadcasting them on interval
  • 03e9d2f reject after retries are exhausted
  • f5ab0bc forwarding the logs from the worker to the main browser
  • 95919e9 [autofix.ci] apply automated fixes
  • f8c9c19 add preflight fetch from main thread to server endpoint to trigger permission prompt if required
  • 06c92c7 remove the log prefix for cleaner logs

📊 Changes

6 files changed (+1886 additions, -0 deletions)

View changed files

📝 packages/desktop-client/src/browser-preload.browser.js (+229 -0)
packages/desktop-client/src/shared-browser-server-core.test.ts (+897 -0)
packages/desktop-client/src/shared-browser-server-core.ts (+738 -0)
packages/desktop-client/src/shared-browser-server.ts (+11 -0)
📝 packages/loot-core/src/server/budgetfiles/app.ts (+5 -0)
upcoming-release-notes/7172.md (+6 -0)

📄 Description

Description

Before (broken):
Tab A → Worker A → SQLite A → IndexedDB ← conflicts → Sync Server
Tab B → Worker B → SQLite B → IndexedDB ← conflicts → Sync Server

After:

Multi-tab architecture
SharedWorker is a pure message router — it never runs a backend itself. It manages per-budget groups, where each group has:

  • 1 leader tab — runs the actual backend in a dedicated Worker (SQLite, absurd-sql, sync)
  • N follower tabs — route all messages through the SharedWorker to the leader's Worker

Key data structures:

  • budgetGroups: Map<budgetId, BudgetGroup> — one group per open budget
  • portToBudget: Map<port, budgetId> — which group each tab belongs to
  • unassignedPorts: Set — tabs connected but not yet on a budget

Lifecycle flow:

  • First tab sends init→ elected as leader of a __lobby group → boots a Worker
  • Tab loads a budget → lobby group renamed to the real budget ID
  • Second tab on same budget → becomes a follower (messages proxied to the leader's Worker)
  • Second tab on a different budget → becomes leader of its own group with its own Worker
  • Leader tab closes → a follower is promoted to leader, boots a fresh Worker, restores the budget
  • Tab closes/crashes → heartbeat detects it and triggers cleanup/failover

Special handling:

  • Budget-replacing ops (create-budget, duplicate-budget, import-budget) get their own temporary Worker so they don't disrupt another tab's backend
  • Delete-budget evicts any group running that budget first, then spins up a temp Worker if needed
    Demo/test budgets (fixed IDs) evict any existing group with the same ID before recreating
    Unassigned tabs can route non-budget messages (like get-budgets) to any available Worker
  • WorkerBridge (browser-preload.browser.js) presents a Worker-like interface to the app's connection layer. All messages go through the SharedWorker for coordination. When a tab is elected leader, it creates a local Worker and wires it up. When leadership transfers, it terminates its Worker cleanly.

Would appreciate 2+ reviews.

#1495

Testing

Seems to work well. I've tested:

  • Web with external server
  • Desktop app
    • Linux
    • Windows
    • Mac
  • Multiple clients using the same server with multiple tabs each

7f6ecc26-c90d-4ae0-9769-740fb702f224.webm

Checklist

  • Release notes added (see link above)
  • No obvious regressions in affected areas
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Bundle Stats

Bundle Files count Total bundle size % Changed
desktop-client 26 11.81 MB → 11.82 MB (+4.63 kB) +0.04%
loot-core 1 4.83 MB → 4.83 MB (+63 B) +0.00%
api 4 4.05 MB → 4.05 MB (+60 B) +0.00%
View detailed bundle stats

desktop-client

Total

Files count Total bundle size % Changed
26 11.81 MB → 11.82 MB (+4.63 kB) +0.04%
Changeset
File Δ Size
src/shared-browser-server.ts?sharedworker 🆕 +187 B 0 B → 187 B
src/browser-preload.browser.js 📈 +4.03 kB (+99.04%) 4.07 kB → 8.09 kB
locale/en.json 📈 +236 B (+0.14%) 169.04 kB → 169.27 kB
locale/it.json 📈 +190 B (+0.11%) 168.79 kB → 168.97 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
static/js/index.js 3.21 MB → 3.22 MB (+4.21 kB) +0.13%
static/js/en.js 169.04 kB → 169.27 kB (+236 B) +0.14%
static/js/it.js 168.79 kB → 168.97 kB (+190 B) +0.11%

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
static/js/BackgroundImage.js 119.98 kB 0%
static/js/FormulaEditor.js 716.38 kB 0%
static/js/ReportRouter.js 1002.19 kB 0%
static/js/TransactionList.js 81.29 kB 0%
static/js/ca.js 185.62 kB 0%
static/js/da.js 104.66 kB 0%
static/js/de.js 177.63 kB 0%
static/js/en-GB.js 7.16 kB 0%
static/js/es.js 172.13 kB 0%
static/js/fr.js 177.63 kB 0%
static/js/indexeddb-main-thread-worker-e59fee74.js 13.46 kB 0%
static/js/narrow.js 353.32 kB 0%
static/js/nb-NO.js 154.72 kB 0%
static/js/nl.js 111.58 kB 0%
static/js/pl.js 88.31 kB 0%
static/js/pt-BR.js 180.55 kB 0%
static/js/resize-observer.js 18.03 kB 0%
static/js/th.js 179.94 kB 0%
static/js/theme.js 30.68 kB 0%
static/js/uk.js 213.14 kB 0%
static/js/useTransactionBatchActions.js 4.27 MB 0%
static/js/wide.js 418 B 0%
static/js/workbox-window.prod.es5.js 7.28 kB 0%

loot-core

Total

Files count Total bundle size % Changed
1 4.83 MB → 4.83 MB (+63 B) +0.00%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts 📈 +63 B (+0.57%) 10.71 kB → 10.77 kB
View detailed bundle breakdown

Added

Asset File Size % Changed
kcab.worker.0cRKCIVW.js 0 B → 4.83 MB (+4.83 MB) -

Removed

Asset File Size % Changed
kcab.worker.Bu63xj1l.js 4.83 MB → 0 B (-4.83 MB) -100%

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged
No assets were unchanged


api

Total

Files count Total bundle size % Changed
4 4.05 MB → 4.05 MB (+60 B) +0.00%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts 📈 +60 B (+0.56%) 10.39 kB → 10.45 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
index.js 3.83 MB → 3.83 MB (+60 B) +0.00%

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
from-Bl-Hslp4.js 167.73 kB 0%
multipart-parser-BnDysoMr.js 8.1 kB 0%
src-iMkUmuwR.js 43.64 kB 0%

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/actualbudget/actual/pull/7172 **Author:** [@MikesGlitch](https://github.com/MikesGlitch) **Created:** 3/11/2026 **Status:** ✅ Merged **Merged:** 3/17/2026 **Merged by:** [@MikesGlitch](https://github.com/MikesGlitch) **Base:** `master` ← **Head:** `multiple-tabs-open` --- ### 📝 Commits (10+) - [`10e8253`](https://github.com/actualbudget/actual/commit/10e82532b6819a94eaa86894f965801dc9fed1b3) attempt to enable sync when multiple tabs are open - [`528eb14`](https://github.com/actualbudget/actual/commit/528eb14d5d49693d69cab94cb1a6c9bd782218b7) allow multiple tabs to work - [`02fceb4`](https://github.com/actualbudget/actual/commit/02fceb469fafc6c8a69d9a2dc14f5b66f3960894) release notes - [`42471cb`](https://github.com/actualbudget/actual/commit/42471cb43a49ed9d10beccb71d50e59a3eed7a35) rehome the host if the tab closes - [`45992d2`](https://github.com/actualbudget/actual/commit/45992d2d84c2e4a6f16f84c75a9bb6307084f997) ensure new tabs always receive failure messages by broadcasting them on interval - [`03e9d2f`](https://github.com/actualbudget/actual/commit/03e9d2fa106973bd2b73907b84f3c846e7e96383) reject after retries are exhausted - [`f5ab0bc`](https://github.com/actualbudget/actual/commit/f5ab0bc4ebac97c73f56ce9cb38d70b0e473a62d) forwarding the logs from the worker to the main browser - [`95919e9`](https://github.com/actualbudget/actual/commit/95919e9b0a91e57d8a8905fe0fc0d372423cd7c1) [autofix.ci] apply automated fixes - [`f8c9c19`](https://github.com/actualbudget/actual/commit/f8c9c1956dac0de4961077a63f28f890201fd7ae) add preflight fetch from main thread to server endpoint to trigger permission prompt if required - [`06c92c7`](https://github.com/actualbudget/actual/commit/06c92c7cb71804ecf2678b23ef464b16ecf2158b) remove the log prefix for cleaner logs ### 📊 Changes **6 files changed** (+1886 additions, -0 deletions) <details> <summary>View changed files</summary> 📝 `packages/desktop-client/src/browser-preload.browser.js` (+229 -0) ➕ `packages/desktop-client/src/shared-browser-server-core.test.ts` (+897 -0) ➕ `packages/desktop-client/src/shared-browser-server-core.ts` (+738 -0) ➕ `packages/desktop-client/src/shared-browser-server.ts` (+11 -0) 📝 `packages/loot-core/src/server/budgetfiles/app.ts` (+5 -0) ➕ `upcoming-release-notes/7172.md` (+6 -0) </details> ### 📄 Description <!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes. Try running yarn generate:release-notes *before* pushing your PR for an interactive experience. --> ## Description **Before (broken):** Tab A → Worker A → SQLite A → IndexedDB ← conflicts → Sync Server Tab B → Worker B → SQLite B → IndexedDB ← conflicts → Sync Server **After:** **Multi-tab architecture** SharedWorker is a pure message router — it never runs a backend itself. It manages per-budget groups, where each group has: - 1 leader tab — runs the actual backend in a dedicated Worker (SQLite, absurd-sql, sync) - N follower tabs — route all messages through the SharedWorker to the leader's Worker **Key data structures:** - budgetGroups: Map<budgetId, BudgetGroup> — one group per open budget - portToBudget: Map<port, budgetId> — which group each tab belongs to - unassignedPorts: Set<port> — tabs connected but not yet on a budget **Lifecycle flow:** - First tab sends init→ elected as leader of a __lobby group → boots a Worker - Tab loads a budget → lobby group renamed to the real budget ID - Second tab on same budget → becomes a follower (messages proxied to the leader's Worker) - Second tab on a different budget → becomes leader of its own group with its own Worker - Leader tab closes → a follower is promoted to leader, boots a fresh Worker, restores the budget - Tab closes/crashes → heartbeat detects it and triggers cleanup/failover **Special handling:** - Budget-replacing ops (create-budget, duplicate-budget, import-budget) get their own temporary Worker so they don't disrupt another tab's backend - Delete-budget evicts any group running that budget first, then spins up a temp Worker if needed Demo/test budgets (fixed IDs) evict any existing group with the same ID before recreating Unassigned tabs can route non-budget messages (like get-budgets) to any available Worker - WorkerBridge (browser-preload.browser.js) presents a Worker-like interface to the app's connection layer. All messages go through the SharedWorker for coordination. When a tab is elected leader, it creates a local Worker and wires it up. When leadership transfers, it terminates its Worker cleanly. Would appreciate 2+ reviews. <!-- What does this PR do? Why is it needed? Please give context on the "why?": why do we need this change? What problem is it solving for you?--> ## Related issue(s) #1495 <!-- e.g. Fixes #123, Relates to #456 --> ## Testing Seems to work well. I've tested: - Web with external server - Desktop app - [x] Linux - [ ] Windows - [ ] Mac - Multiple clients using the same server with multiple tabs each [7f6ecc26-c90d-4ae0-9769-740fb702f224.webm](https://github.com/user-attachments/assets/880ed95e-29eb-400d-8d64-bacd56a5b9db) <!-- What did you test? How can we reproduce the issue you are fixing or how can we test the feature you built? --> ## Checklist - [x] Release notes added (see link above) - [x] No obvious regressions in affected areas - [x] Self-review has been performed - I understand what each change in the code does and why it is needed <!--- actual-bot-sections ---> <!--- bundlestats-action-comment key:combined start ---> ### Bundle Stats Bundle | Files count | Total bundle size | % Changed ------ | ----------- | ----------------- | --------- desktop-client | 26 | 11.81 MB → 11.82 MB (+4.63 kB) | +0.04% loot-core | 1 | 4.83 MB → 4.83 MB (+63 B) | +0.00% api | 4 | 4.05 MB → 4.05 MB (+60 B) | +0.00% <details> <summary>View detailed bundle stats</summary> #### desktop-client **Total** Files count | Total bundle size | % Changed ----------- | ----------------- | --------- 26 | 11.81 MB → 11.82 MB (+4.63 kB) | +0.04% <details> <summary>Changeset</summary> File | Δ | Size ---- | - | ---- `src/shared-browser-server.ts?sharedworker` | 🆕 +187 B | 0 B → 187 B `src/browser-preload.browser.js` | 📈 +4.03 kB (+99.04%) | 4.07 kB → 8.09 kB `locale/en.json` | 📈 +236 B (+0.14%) | 169.04 kB → 169.27 kB `locale/it.json` | 📈 +190 B (+0.11%) | 168.79 kB → 168.97 kB </details> <details> <summary>View detailed bundle breakdown</summary> <div> **Added** No assets were added **Removed** No assets were removed **Bigger** Asset | File Size | % Changed ----- | --------- | --------- static/js/index.js | 3.21 MB → 3.22 MB (+4.21 kB) | +0.13% static/js/en.js | 169.04 kB → 169.27 kB (+236 B) | +0.14% static/js/it.js | 168.79 kB → 168.97 kB (+190 B) | +0.11% **Smaller** No assets were smaller **Unchanged** Asset | File Size | % Changed ----- | --------- | --------- static/js/BackgroundImage.js | 119.98 kB | 0% static/js/FormulaEditor.js | 716.38 kB | 0% static/js/ReportRouter.js | 1002.19 kB | 0% static/js/TransactionList.js | 81.29 kB | 0% static/js/ca.js | 185.62 kB | 0% static/js/da.js | 104.66 kB | 0% static/js/de.js | 177.63 kB | 0% static/js/en-GB.js | 7.16 kB | 0% static/js/es.js | 172.13 kB | 0% static/js/fr.js | 177.63 kB | 0% static/js/indexeddb-main-thread-worker-e59fee74.js | 13.46 kB | 0% static/js/narrow.js | 353.32 kB | 0% static/js/nb-NO.js | 154.72 kB | 0% static/js/nl.js | 111.58 kB | 0% static/js/pl.js | 88.31 kB | 0% static/js/pt-BR.js | 180.55 kB | 0% static/js/resize-observer.js | 18.03 kB | 0% static/js/th.js | 179.94 kB | 0% static/js/theme.js | 30.68 kB | 0% static/js/uk.js | 213.14 kB | 0% static/js/useTransactionBatchActions.js | 4.27 MB | 0% static/js/wide.js | 418 B | 0% static/js/workbox-window.prod.es5.js | 7.28 kB | 0% </div> </details> --- #### loot-core **Total** Files count | Total bundle size | % Changed ----------- | ----------------- | --------- 1 | 4.83 MB → 4.83 MB (+63 B) | +0.00% <details> <summary>Changeset</summary> File | Δ | Size ---- | - | ---- `home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts` | 📈 +63 B (+0.57%) | 10.71 kB → 10.77 kB </details> <details> <summary>View detailed bundle breakdown</summary> <div> **Added** Asset | File Size | % Changed ----- | --------- | --------- kcab.worker.0cRKCIVW.js | 0 B → 4.83 MB (+4.83 MB) | - **Removed** Asset | File Size | % Changed ----- | --------- | --------- kcab.worker.Bu63xj1l.js | 4.83 MB → 0 B (-4.83 MB) | -100% **Bigger** No assets were bigger **Smaller** No assets were smaller **Unchanged** No assets were unchanged </div> </details> --- #### api **Total** Files count | Total bundle size | % Changed ----------- | ----------------- | --------- 4 | 4.05 MB → 4.05 MB (+60 B) | +0.00% <details> <summary>Changeset</summary> File | Δ | Size ---- | - | ---- `home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts` | 📈 +60 B (+0.56%) | 10.39 kB → 10.45 kB </details> <details> <summary>View detailed bundle breakdown</summary> <div> **Added** No assets were added **Removed** No assets were removed **Bigger** Asset | File Size | % Changed ----- | --------- | --------- index.js | 3.83 MB → 3.83 MB (+60 B) | +0.00% **Smaller** No assets were smaller **Unchanged** Asset | File Size | % Changed ----- | --------- | --------- from-Bl-Hslp4.js | 167.73 kB | 0% multipart-parser-BnDysoMr.js | 8.1 kB | 0% src-iMkUmuwR.js | 43.64 kB | 0% </div> </details> </details> <!--- bundlestats-action-comment key:combined end ---> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-04-18 08:52:43 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/actual#32910