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);
}
}
}