Skip to content

Conversation

@stevensJourney
Copy link
Collaborator

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 TriggerHoldManager interface. 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_kv key/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 PowerSyncDatabase is 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 MemoryTriggerHoldManager uses 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 NavigatorTriggerHoldManager leverages 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 PowerSyncDatabase enables persistence by default for OPFS VFS modes, while keeping temporary triggers for IDBBatchAtomicVFS where multiple connections aren't an issue.

@changeset-bot
Copy link

changeset-bot bot commented Dec 30, 2025

⚠️ No Changeset found

Latest commit: 9637aaf

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants