import { AbstractPowerSyncDatabase, DBAdapter, PowerSyncDatabaseOptions, PowerSyncDatabaseOptionsWithDBAdapter, PowerSyncDatabaseOptionsWithOpenFactory, PowerSyncDatabaseOptionsWithSettings, SqliteBucketStorage, StreamingSyncImplementation, TriggerManagerConfig, isDBAdapter, isSQLOpenFactory, Mutex, type BucketStorageAdapter, type PowerSyncBackendConnector, type PowerSyncCloseOptions, type RequiredAdditionalConnectionOptions } from '@powersync/common'; import { getNavigatorLocks } from '../shared/navigator.js'; import { NAVIGATOR_TRIGGER_CLAIM_MANAGER } from './NavigatorTriggerClaimManager.js'; import { WebDBAdapter } from './adapters/WebDBAdapter.js'; import { WASQLiteOpenFactory } from './adapters/wa-sqlite/WASQLiteOpenFactory.js'; import { DEFAULT_WEB_SQL_FLAGS, ResolvedWebSQLOpenOptions, WebSQLFlags, isServerSide, resolveWebSQLFlags } from './adapters/web-sql-flags.js'; import { SSRStreamingSyncImplementation } from './sync/SSRWebStreamingSyncImplementation.js'; import { SharedWebStreamingSyncImplementation } from './sync/SharedWebStreamingSyncImplementation.js'; import { WebRemote } from './sync/WebRemote.js'; import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from './sync/WebStreamingSyncImplementation.js'; import { AsyncDbAdapter } from './adapters/AsyncWebAdapter.js'; export interface WebPowerSyncFlags extends WebSQLFlags { /** * Externally unload open PowerSync database instances when the window closes. * Setting this to `true` requires calling `close` on all open PowerSyncDatabase * instances before the window unloads */ externallyUnload?: boolean; } type WithWebFlags = Base & { flags?: WebPowerSyncFlags }; export interface WebSyncOptions { /** * Allows you to override the default sync worker. * * You can either provide a path to the worker script * or a factory method that returns a worker. */ worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => SharedWorker); } type WithWebSyncOptions = Base & { sync?: WebSyncOptions; }; export interface WebEncryptionOptions { /** * Encryption key for the database. * If set, the database will be encrypted using Multiple Ciphers. */ encryptionKey?: string; } type WithWebEncryptionOptions = Base & WebEncryptionOptions; export type WebPowerSyncDatabaseOptionsWithAdapter = WithWebSyncOptions< WithWebFlags >; export type WebPowerSyncDatabaseOptionsWithOpenFactory = WithWebSyncOptions< WithWebFlags >; export type WebPowerSyncDatabaseOptionsWithSettings = WithWebSyncOptions< WithWebFlags> >; export type WebPowerSyncDatabaseOptions = WithWebSyncOptions>; export const DEFAULT_POWERSYNC_FLAGS: Required = { ...DEFAULT_WEB_SQL_FLAGS, externallyUnload: false }; export const resolveWebPowerSyncFlags = (flags?: WebPowerSyncFlags): Required => { return { ...DEFAULT_POWERSYNC_FLAGS, ...flags, ...resolveWebSQLFlags(flags) }; }; /** * Asserts that the database options are valid for custom database constructors. */ function assertValidDatabaseOptions(options: WebPowerSyncDatabaseOptions): void { if ('database' in options && 'encryptionKey' in options) { const { database } = options; if (isSQLOpenFactory(database) || isDBAdapter(database)) { throw new Error( `Invalid configuration: 'encryptionKey' should only be included inside the database object when using a custom ${isSQLOpenFactory(database) ? 'WASQLiteOpenFactory' : 'WASQLiteDBAdapter'} constructor.` ); } } } /** * A PowerSync database which provides SQLite functionality * which is automatically synced. * * @example * ```typescript * export const db = new PowerSyncDatabase({ * schema: AppSchema, * database: { * dbFilename: 'example.db' * } * }); * ``` */ export class PowerSyncDatabase extends AbstractPowerSyncDatabase { static SHARED_MUTEX = new Mutex(); protected unloadListener?: () => Promise; protected resolvedFlags: WebPowerSyncFlags; constructor(options: WebPowerSyncDatabaseOptionsWithAdapter); constructor(options: WebPowerSyncDatabaseOptionsWithOpenFactory); constructor(options: WebPowerSyncDatabaseOptionsWithSettings); constructor(options: WebPowerSyncDatabaseOptions); constructor(protected options: WebPowerSyncDatabaseOptions) { super(options); assertValidDatabaseOptions(options); this.resolvedFlags = resolveWebPowerSyncFlags(options.flags); if (this.resolvedFlags.enableMultiTabs && !this.resolvedFlags.externallyUnload) { this.unloadListener = () => this.close({ disconnect: false }); window.addEventListener('unload', this.unloadListener); } } async _initialize(): Promise { if (this.database instanceof AsyncDbAdapter) { /** * While init is done automatically, * LockedAsyncDatabaseAdapter only exposes config after init. * We can explicitly wait for init here in order to access config. */ await this.database.init(); } // In some cases, like the SQLJs adapter, we don't pass a WebDBAdapter, so we need to check. if (typeof (this.database as WebDBAdapter).getConfiguration == 'function') { const config = (this.database as WebDBAdapter).getConfiguration(); if (config.requiresPersistentTriggers) { this.triggersImpl.updateDefaults({ useStorageByDefault: true }); } } } protected generateTriggerManagerConfig(): TriggerManagerConfig { return { // We need to share hold information between tabs for web claimManager: NAVIGATOR_TRIGGER_CLAIM_MANAGER }; } protected openDBAdapter(options: WebPowerSyncDatabaseOptionsWithSettings): DBAdapter { const defaultFactory = new WASQLiteOpenFactory({ ...options.database, flags: resolveWebPowerSyncFlags(options.flags), encryptionKey: options.encryptionKey }); return defaultFactory.openDB(); } /** * Closes the database connection. * By default the sync stream client is only disconnected if * multiple tabs are not enabled. */ close(options?: PowerSyncCloseOptions): Promise { if (this.unloadListener) { window.removeEventListener('unload', this.unloadListener); } return super.close({ // Don't disconnect by default if multiple tabs are enabled disconnect: options?.disconnect ?? !this.resolvedFlags.enableMultiTabs }); } protected async loadVersion(): Promise { if (isServerSide()) { return; } return super.loadVersion(); } protected async resolveOfflineSyncStatus() { if (isServerSide()) { return; } return super.resolveOfflineSyncStatus(); } protected generateBucketStorageAdapter(): BucketStorageAdapter { return new SqliteBucketStorage(this.database); } protected async runExclusive(cb: () => Promise) { if (this.resolvedFlags.ssrMode) { return PowerSyncDatabase.SHARED_MUTEX.runExclusive(cb); } return getNavigatorLocks().request(`lock-${this.database.name}`, cb); } protected generateSyncStreamImplementation( connector: PowerSyncBackendConnector, options: RequiredAdditionalConnectionOptions ): StreamingSyncImplementation { const remote = new WebRemote(connector, this.logger); const syncOptions: WebStreamingSyncImplementationOptions = { ...(this.options as {}), ...options, flags: this.resolvedFlags, adapter: this.bucketStorageAdapter, remote, uploadCrud: async () => { await this.waitForReady(); await connector.uploadData(this); }, identifier: this.database.name, logger: this.logger }; switch (true) { case this.resolvedFlags.ssrMode: return new SSRStreamingSyncImplementation(syncOptions); case this.resolvedFlags.enableMultiTabs: if (!this.resolvedFlags.broadcastLogs) { const warning = ` Multiple tabs are enabled, but broadcasting of logs is disabled. Logs for shared sync worker will only be available in the shared worker context `; const logger = this.options.logger; logger ? logger.warn(warning) : console.warn(warning); } return new SharedWebStreamingSyncImplementation({ ...syncOptions, db: this.database as WebDBAdapter // This should always be the case }); default: return new WebStreamingSyncImplementation(syncOptions); } } }