[wip] OPFS Multiple Tab Trigger Invocation #804
Draft
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Overview
Our SQLite trigger helpers currently allow for tracking INSERT, UPDATE, and DELETE operations on a PowerSync view. These operations are stored in a SQLite table for each managed consumer to process.
Our managed triggers currently only support temporary SQLite triggers, where diff operations are stored in temporary SQLite tables. This approach avoids extra storage overhead for diff records and simplifies cleanup. The temporary nature means the triggers and tables are scoped to the SQLite connection and are disposed automatically once the connection is closed.
The Problem
For OPFS (in Web), we create a separate SQLite connection per tab. Triggers are also scoped to each tab, so each tab creates its own managed trigger (hence its own SQLite trigger and destination table). Having multiple connections across multiple tabs causes issues:
A mutation performed by one connection does not trigger a temporary trigger defined for another connection.
This causes our trigger consumers to miss updates from other tabs.
The Solution
Persisted SQLite triggers are stored in the database. A mutation from any connection will trigger all persisted triggers, ensuring that mutations from other tabs are written to the destination table of other tabs' managed triggers.
This PR adds the ability to optionally use persisted SQLite tables and triggers (enabled by default for OPFS connections).
Implementation
Using persisted triggers and destination tables solves the multiple SQLite connection problem, but introduces a new challenge: how do we dispose of the persisted tables and triggers when they are no longer needed?
The Hold Mechanism
Disposing stale items for closed tabs (and SQLite connections) is not straightforward. When a tab is closed, there is usually no reliable method for ensuring cleanup has completed successfully.
To address this, the implementation introduces a "hold" mechanism through a new
TriggerHoldManagerinterface. This interface provides two core capabilities: obtaining a hold on a resource (returning a release callback), and checking whether a resource is currently held.A heartbeat or time-to-live mechanism was also considered, but this approach seemed vulnerable to slow or frozen tab issues. A tab that's temporarily unresponsive could have its resources incorrectly cleaned up. The hold method avoids this problem since a hold remains valid regardless of how responsive the owning tab is.
When a persisted trigger is created, we store its metadata in the
ps_kvkey/value store and obtain a hold to declare that something is actively using the managed trigger and its corresponding resources. This entry is removed if the trigger is manually disposed before the tab closes.When a new
PowerSyncDatabaseis created, we check the list of tracked triggers and use the hold manager to verify if any active holds exist on each entry. If no hold is found, we know the trigger is orphaned and safe to delete, so we remove both the triggers and their destination tables.Platform-Specific Hold Managers
The hold mechanism needs to work differently depending on the platform.
For shared-memory SDKs like Node and React Native, a
MemoryTriggerHoldManageruses a global in-memory store to track holds. Since these environments typically share memory within a single process, this straightforward approach works well.For web environments, a
NavigatorTriggerHoldManagerleverages the Navigator Locks API to manage holds across browser tabs. This allows us to determine if any tab is still holding on to a trigger, even when those tabs can't directly communicate with each other.The web
PowerSyncDatabaseenables persistence by default for OPFS VFS modes, while keeping temporary triggers forIDBBatchAtomicVFSwhere multiple connections aren't an issue.