From 7943626ee73b0689b859ff94753d411841093cc5 Mon Sep 17 00:00:00 2001 From: Dominic Gunther Bauer <46312751+DominicGBauer@users.noreply.github.com> Date: Wed, 22 May 2024 17:10:00 +0200 Subject: [PATCH] feat: allow for web sdk to be used without web worker (#178) Co-authored-by: DominicGBauer --- .changeset/rotten-cherries-reflect.md | 5 ++ packages/web/src/db/PowerSyncDatabase.ts | 22 ++--- ...AbstractWebPowerSyncDatabaseOpenFactory.ts | 4 + .../adapters/wa-sqlite/WASQLiteDBAdapter.ts | 63 +++++++++------ .../web/src/{worker/db => shared}/open-db.ts | 74 +++++++++-------- packages/web/src/shared/types.ts | 28 +++++++ .../src/worker/db/SharedWASQLiteDB.worker.ts | 9 +-- .../web/src/worker/db/WASQLiteDB.worker.ts | 2 +- .../web/src/worker/db/open-worker-database.ts | 2 +- .../sync/AbstractSharedSyncClientProvider.ts | 2 +- .../web/src/worker/sync/BroadcastLogger.ts | 4 +- .../worker/sync/SharedSyncImplementation.ts | 16 ++-- .../sync/SharedSyncImplementation.worker.ts | 6 +- packages/web/tests/bucket_storage.test.ts | 2 +- packages/web/tests/crud.test.ts | 15 +--- packages/web/tests/main.test.ts | 61 +++++++------- packages/web/tests/multiple_instances.test.ts | 2 +- packages/web/tests/stream.test.ts | 81 +++++++++++-------- packages/web/tests/utils/test-schema.ts | 28 ------- packages/web/tests/utils/testDb.ts | 51 ++++++++++++ packages/web/tests/watch.test.ts | 2 +- 21 files changed, 276 insertions(+), 203 deletions(-) create mode 100644 .changeset/rotten-cherries-reflect.md rename packages/web/src/{worker/db => shared}/open-db.ts (81%) create mode 100644 packages/web/src/shared/types.ts delete mode 100644 packages/web/tests/utils/test-schema.ts create mode 100644 packages/web/tests/utils/testDb.ts diff --git a/.changeset/rotten-cherries-reflect.md b/.changeset/rotten-cherries-reflect.md new file mode 100644 index 00000000..1b9b4dc7 --- /dev/null +++ b/.changeset/rotten-cherries-reflect.md @@ -0,0 +1,5 @@ +--- +'@powersync/web': minor +--- + +Allow package to be used without web workers diff --git a/packages/web/src/db/PowerSyncDatabase.ts b/packages/web/src/db/PowerSyncDatabase.ts index b2b976eb..3f0f6a4d 100644 --- a/packages/web/src/db/PowerSyncDatabase.ts +++ b/packages/web/src/db/PowerSyncDatabase.ts @@ -1,15 +1,15 @@ import { + type AbstractStreamingSyncImplementation, + type PowerSyncBackendConnector, + type BucketStorageAdapter, + type PowerSyncDatabaseOptions, + type PowerSyncCloseOptions, + type PowerSyncConnectionOptions, AbstractPowerSyncDatabase, - AbstractStreamingSyncImplementation, - PowerSyncBackendConnector, SqliteBucketStorage, - BucketStorageAdapter, - PowerSyncDatabaseOptions, - PowerSyncCloseOptions, - DEFAULT_POWERSYNC_CLOSE_OPTIONS, - PowerSyncConnectionOptions + DEFAULT_POWERSYNC_CLOSE_OPTIONS } from '@powersync/common'; - +import { Mutex } from 'async-mutex'; import { WebRemote } from './sync/WebRemote'; import { SharedWebStreamingSyncImplementation } from './sync/SharedWebStreamingSyncImplementation'; import { SSRStreamingSyncImplementation } from './sync/SSRWebStreamingSyncImplementation'; @@ -17,13 +17,13 @@ import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from './sync/WebStreamingSyncImplementation'; -import { Mutex } from 'async-mutex'; export interface WebPowerSyncFlags { /** * Enables multi tab support */ enableMultiTabs?: boolean; + useWebWorker?: boolean; /** * Open in SSR placeholder mode. DB operations and Sync operations will be a No-op */ @@ -126,8 +126,8 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase { case flags?.enableMultiTabs: if (!flags?.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 + 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); diff --git a/packages/web/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.ts b/packages/web/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.ts index 1815e72f..9f7d28a1 100644 --- a/packages/web/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.ts +++ b/packages/web/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.ts @@ -16,6 +16,7 @@ export interface WebPowerSyncOpenFactoryOptions extends PowerSyncOpenFactoryOpti } export const DEFAULT_POWERSYNC_FLAGS: WebPowerSyncOpenFlags = { + useWebWorker: true, /** * Multiple tabs are by default not supported on Android, iOS and Safari. * Other platforms will have multiple tabs enabled by default. @@ -80,6 +81,9 @@ export abstract class AbstractWebPowerSyncDatabaseOpenFactory extends AbstractPo if (typeof this.options.flags?.enableMultiTabs != 'undefined') { flags.enableMultiTabs = this.options.flags.enableMultiTabs; } + if (flags.useWebWorker === false) { + flags.enableMultiTabs = false; + } return flags; } diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts index a5799d15..d8082482 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts @@ -1,21 +1,23 @@ import { - BaseObserver, - DBAdapter, - DBAdapterListener, - DBGetUtils, - DBLockOptions, - LockContext, - PowerSyncOpenFactoryOptions, - QueryResult, - Transaction + type DBAdapter, + type DBAdapterListener, + type DBGetUtils, + type DBLockOptions, + type LockContext, + type PowerSyncOpenFactoryOptions, + type QueryResult, + type Transaction, + BaseObserver } from '@powersync/common'; import * as Comlink from 'comlink'; -import Logger, { ILogger } from 'js-logger'; -import type { DBWorkerInterface, OpenDB } from '../../../worker/db/open-db'; +import Logger, { type ILogger } from 'js-logger'; +import type { DBFunctionsInterface, OpenDB } from '../../../shared/types'; +import { _openDB } from '../../../shared/open-db'; import { getWorkerDatabaseOpener } from '../../../worker/db/open-worker-database'; export type WASQLiteFlags = { enableMultiTabs?: boolean; + useWebWorker?: boolean; }; export interface WASQLiteDBAdapterOptions extends Omit { @@ -34,13 +36,13 @@ export class WASQLiteDBAdapter extends BaseObserver implement private initialized: Promise; private logger: ILogger; private dbGetHelpers: DBGetUtils | null; - private workerMethods: DBWorkerInterface | null; + private methods: DBFunctionsInterface | null; constructor(protected options: WASQLiteDBAdapterOptions) { super(); this.logger = Logger.get('WASQLite'); this.dbGetHelpers = null; - this.workerMethods = null; + this.methods = null; this.initialized = this.init(); this.dbGetHelpers = this.generateDBHelpers({ execute: this._execute.bind(this) }); } @@ -56,22 +58,31 @@ export class WASQLiteDBAdapter extends BaseObserver implement getWorker() {} protected async init() { - const { enableMultiTabs } = this.flags; + const { enableMultiTabs, useWebWorker } = this.flags; if (!enableMultiTabs) { this.logger.warn('Multiple tabs are not enabled in this browser'); } - const dbOpener = this.options.workerPort - ? Comlink.wrap(this.options.workerPort) - : getWorkerDatabaseOpener(this.options.dbFilename, enableMultiTabs); + if (useWebWorker) { + const dbOpener = this.options.workerPort + ? Comlink.wrap(this.options.workerPort) + : getWorkerDatabaseOpener(this.options.dbFilename, enableMultiTabs); - this.workerMethods = await dbOpener(this.options.dbFilename); + this.methods = await dbOpener(this.options.dbFilename); - this.workerMethods.registerOnTableChange( - Comlink.proxy((opType: number, tableName: string, rowId: number) => { - this.iterateListeners((cb) => cb.tablesUpdated?.({ opType, table: tableName, rowId })); - }) - ); + this.methods!.registerOnTableChange( + Comlink.proxy((opType: number, tableName: string, rowId: number) => { + this.iterateListeners((cb) => cb.tablesUpdated?.({ opType, table: tableName, rowId })); + }) + ); + + return; + } + this.methods = await _openDB(this.options.dbFilename, { useWebWorker: false }); + + this.methods.registerOnTableChange((opType: number, tableName: string, rowId: number) => { + this.iterateListeners((cb) => cb.tablesUpdated?.({ opType, table: tableName, rowId })); + }); } async execute(query: string, params?: any[] | undefined): Promise { @@ -87,7 +98,7 @@ export class WASQLiteDBAdapter extends BaseObserver implement */ private _execute = async (sql: string, bindings?: any[]): Promise => { await this.initialized; - const result = await this.workerMethods!.execute!(sql, bindings); + const result = await this.methods!.execute!(sql, bindings); return { ...result, rows: { @@ -102,7 +113,7 @@ export class WASQLiteDBAdapter extends BaseObserver implement */ private _executeBatch = async (query: string, params?: any[]): Promise => { await this.initialized; - const result = await this.workerMethods!.executeBatch!(query, params); + const result = await this.methods!.executeBatch!(query, params); return { ...result, rows: undefined @@ -115,7 +126,7 @@ export class WASQLiteDBAdapter extends BaseObserver implement * tabs are still using it. */ close() { - this.workerMethods?.close?.(); + this.methods?.close?.(); } async getAll(sql: string, parameters?: any[] | undefined): Promise { diff --git a/packages/web/src/worker/db/open-db.ts b/packages/web/src/shared/open-db.ts similarity index 81% rename from packages/web/src/worker/db/open-db.ts rename to packages/web/src/shared/open-db.ts index b8c9c96f..b570d8db 100644 --- a/packages/web/src/worker/db/open-db.ts +++ b/packages/web/src/shared/open-db.ts @@ -1,33 +1,14 @@ import * as SQLite from '@journeyapps/wa-sqlite'; import '@journeyapps/wa-sqlite'; import * as Comlink from 'comlink'; -import { QueryResult } from '@powersync/common'; - -export type WASQLExecuteResult = Omit & { - rows: { - _array: any[]; - length: number; - }; -}; - -export type DBWorkerInterface = { - // Close is only exposed when used in a single non shared webworker - close?: () => void; - execute: WASQLiteExecuteMethod; - executeBatch: WASQLiteExecuteBatchMethod; - registerOnTableChange: (callback: OnTableChangeCallback) => void; -}; - -export type WASQLiteExecuteMethod = (sql: string, params?: any[]) => Promise; -export type WASQLiteExecuteBatchMethod = (sql: string, params?: any[]) => Promise; -export type OnTableChangeCallback = (opType: number, tableName: string, rowId: number) => void; -export type OpenDB = (dbFileName: string) => DBWorkerInterface; - -export type SQLBatchTuple = [string] | [string, Array | Array>]; +import type { DBFunctionsInterface, OnTableChangeCallback, WASQLExecuteResult } from './types'; let nextId = 1; -export async function _openDB(dbFileName: string): Promise { +export async function _openDB( + dbFileName: string, + options: { useWebWorker: boolean } = { useWebWorker: true } +): Promise { const { default: moduleFactory } = await import('@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'); const module = await moduleFactory(); const sqlite3 = SQLite.Factory(module); @@ -47,14 +28,6 @@ export async function _openDB(dbFileName: string): Promise { Array.from(listeners.values()).forEach((l) => l(opType, tableName, rowId)); }); - const registerOnTableChange = (callback: OnTableChangeCallback) => { - const id = nextId++; - listeners.set(id, callback); - return Comlink.proxy(() => { - listeners.delete(id); - }); - }; - /** * This executes single SQL statements inside a requested lock. */ @@ -198,12 +171,37 @@ export async function _openDB(dbFileName: string): Promise { }); }; + if (options.useWebWorker) { + const registerOnTableChange = (callback: OnTableChangeCallback) => { + const id = nextId++; + listeners.set(id, callback); + return Comlink.proxy(() => { + listeners.delete(id); + }); + }; + + return { + execute: Comlink.proxy(execute), + executeBatch: Comlink.proxy(executeBatch), + registerOnTableChange: Comlink.proxy(registerOnTableChange), + close: Comlink.proxy(() => { + sqlite3.close(db); + }) + }; + } + + const registerOnTableChange = (callback: OnTableChangeCallback) => { + const id = nextId++; + listeners.set(id, callback); + return () => { + listeners.delete(id); + }; + }; + return { - execute: Comlink.proxy(execute), - executeBatch: Comlink.proxy(executeBatch), - registerOnTableChange: Comlink.proxy(registerOnTableChange), - close: Comlink.proxy(() => { - sqlite3.close(db); - }) + execute: execute, + executeBatch: executeBatch, + registerOnTableChange: registerOnTableChange, + close: () => sqlite3.close(db) }; } diff --git a/packages/web/src/shared/types.ts b/packages/web/src/shared/types.ts new file mode 100644 index 00000000..a798023d --- /dev/null +++ b/packages/web/src/shared/types.ts @@ -0,0 +1,28 @@ +import type { QueryResult } from '@powersync/common'; + +export type WASQLExecuteResult = Omit & { + rows: { + _array: any[]; + length: number; + }; +}; + +export type DBFunctionsInterface = { + // Close is only exposed when used in a single non shared webworker + close?: () => void; + execute: WASQLiteExecuteMethod; + executeBatch: WASQLiteExecuteBatchMethod; + registerOnTableChange: (callback: OnTableChangeCallback) => void; +}; + +/** + * @deprecated use [DBFunctionsInterface instead] + */ +export type DBWorkerInterface = DBFunctionsInterface; + +export type WASQLiteExecuteMethod = (sql: string, params?: any[]) => Promise; +export type WASQLiteExecuteBatchMethod = (sql: string, params?: any[]) => Promise; +export type OnTableChangeCallback = (opType: number, tableName: string, rowId: number) => void; +export type OpenDB = (dbFileName: string) => DBWorkerInterface; + +export type SQLBatchTuple = [string] | [string, Array | Array>]; diff --git a/packages/web/src/worker/db/SharedWASQLiteDB.worker.ts b/packages/web/src/worker/db/SharedWASQLiteDB.worker.ts index 845971fb..c86cd052 100644 --- a/packages/web/src/worker/db/SharedWASQLiteDB.worker.ts +++ b/packages/web/src/worker/db/SharedWASQLiteDB.worker.ts @@ -1,8 +1,7 @@ import '@journeyapps/wa-sqlite'; - import * as Comlink from 'comlink'; - -import { DBWorkerInterface, _openDB } from './open-db'; +import { _openDB } from '../../shared/open-db'; +import type { DBFunctionsInterface } from '../../shared/types'; /** * Keeps track of open DB connections and the clients which @@ -10,7 +9,7 @@ import { DBWorkerInterface, _openDB } from './open-db'; */ type SharedDBWorkerConnection = { clientIds: Set; - db: DBWorkerInterface; + db: DBFunctionsInterface; }; const _self: SharedWorkerGlobalScope = self as any; @@ -20,7 +19,7 @@ const OPEN_DB_LOCK = 'open-wasqlite-db'; let nextClientId = 1; -const openDB = async (dbFileName: string): Promise => { +const openDB = async (dbFileName: string): Promise => { // Prevent multiple simultaneous opens from causing race conditions return navigator.locks.request(OPEN_DB_LOCK, async () => { const clientId = nextClientId++; diff --git a/packages/web/src/worker/db/WASQLiteDB.worker.ts b/packages/web/src/worker/db/WASQLiteDB.worker.ts index 043a45a3..12f43303 100644 --- a/packages/web/src/worker/db/WASQLiteDB.worker.ts +++ b/packages/web/src/worker/db/WASQLiteDB.worker.ts @@ -1,4 +1,4 @@ import * as Comlink from 'comlink'; -import { _openDB } from './open-db'; +import { _openDB } from '../../shared/open-db'; Comlink.expose(async (dbFileName: string) => Comlink.proxy(await _openDB(dbFileName))); diff --git a/packages/web/src/worker/db/open-worker-database.ts b/packages/web/src/worker/db/open-worker-database.ts index c7a2a3f0..c3400659 100644 --- a/packages/web/src/worker/db/open-worker-database.ts +++ b/packages/web/src/worker/db/open-worker-database.ts @@ -1,5 +1,5 @@ import * as Comlink from 'comlink'; -import { OpenDB } from './open-db'; +import type { OpenDB } from '../../shared/types'; /** * Opens a shared or dedicated worker which exposes opening of database connections diff --git a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts b/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts index 5bbfee56..d909119c 100644 --- a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts +++ b/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts @@ -1,4 +1,4 @@ -import { PowerSyncCredentials, SyncStatusOptions } from '@powersync/common'; +import type { PowerSyncCredentials, SyncStatusOptions } from '@powersync/common'; /** * The client side port should provide these methods. diff --git a/packages/web/src/worker/sync/BroadcastLogger.ts b/packages/web/src/worker/sync/BroadcastLogger.ts index 9c9990d3..099f65d7 100644 --- a/packages/web/src/worker/sync/BroadcastLogger.ts +++ b/packages/web/src/worker/sync/BroadcastLogger.ts @@ -1,4 +1,4 @@ -import Logger, { ILogLevel, ILogger } from 'js-logger'; +import Logger, { type ILogLevel, type ILogger } from 'js-logger'; import { type WrappedSyncPort } from './SharedSyncImplementation'; /** @@ -88,7 +88,7 @@ export class BroadcastLogger implements ILogger { * and proceeds to execute for all clients. */ protected async iterateClients(callback: (client: WrappedSyncPort) => Promise) { - for (let client of this.clients) { + for (const client of this.clients) { try { await callback(client); } catch (ex) { diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 1ec942f4..cd12a5f1 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -1,16 +1,16 @@ import * as Comlink from 'comlink'; -import Logger, { ILogger } from 'js-logger'; +import Logger, { type ILogger } from 'js-logger'; import { - AbstractStreamingSyncImplementation, - StreamingSyncImplementation, + type AbstractStreamingSyncImplementation, + type StreamingSyncImplementation, + type LockOptions, + type StreamingSyncImplementationListener, + type SyncStatusOptions, + type PowerSyncConnectionOptions, BaseObserver, - LockOptions, SqliteBucketStorage, - StreamingSyncImplementationListener, SyncStatus, - SyncStatusOptions, - AbortOperation, - PowerSyncConnectionOptions + AbortOperation } from '@powersync/common'; import { WebStreamingSyncImplementation, diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts b/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts index e210c080..f5712e5b 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts @@ -1,5 +1,9 @@ import * as Comlink from 'comlink'; -import { SharedSyncImplementation, SharedSyncClientEvent, ManualSharedSyncPayload } from './SharedSyncImplementation'; +import { + SharedSyncImplementation, + SharedSyncClientEvent, + type ManualSharedSyncPayload +} from './SharedSyncImplementation'; import Logger from 'js-logger'; import { Buffer } from 'buffer'; diff --git a/packages/web/tests/bucket_storage.test.ts b/packages/web/tests/bucket_storage.test.ts index 33eecbe3..c7d3c268 100644 --- a/packages/web/tests/bucket_storage.test.ts +++ b/packages/web/tests/bucket_storage.test.ts @@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { AbstractPowerSyncDatabase, Checkpoint } from '@powersync/common'; import { WASQLitePowerSyncDatabaseOpenFactory } from '@powersync/web'; import { Mutex } from 'async-mutex'; -import { testSchema } from './utils/test-schema'; +import { testSchema } from './utils/testDb'; const putAsset1_1 = OplogEntry.fromRow({ op_id: '1', diff --git a/packages/web/tests/crud.test.ts b/packages/web/tests/crud.test.ts index 338868a1..6a880336 100644 --- a/packages/web/tests/crud.test.ts +++ b/packages/web/tests/crud.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { AbstractPowerSyncDatabase, Column, ColumnType, CrudEntry, Schema, Table, UpdateType } from '@powersync/common'; import { WASQLitePowerSyncDatabaseOpenFactory } from '@powersync/web'; import { v4 as uuid } from 'uuid'; -import { testSchema } from './utils/test-schema'; +import { generateTestDb, testSchema } from './utils/testDb'; const testId = '2290de4f-0488-4e50-abed-f8e8eb1d0b42'; @@ -10,18 +10,7 @@ describe('CRUD Tests', () => { let powersync: AbstractPowerSyncDatabase; beforeEach(async () => { - powersync = new WASQLitePowerSyncDatabaseOpenFactory({ - /** - * Deleting the IndexDB seems to freeze the test. - * Use a new DB for each run to keep CRUD counters - * consistent - */ - dbFilename: `test-crud-${uuid()}.db`, - schema: testSchema, - flags: { - enableMultiTabs: false - } - }).getInstance(); + powersync = generateTestDb(); }); afterEach(async () => { diff --git a/packages/web/tests/main.test.ts b/packages/web/tests/main.test.ts index 3769e85e..452ac88e 100644 --- a/packages/web/tests/main.test.ts +++ b/packages/web/tests/main.test.ts @@ -1,67 +1,62 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { AbstractPowerSyncDatabase, Column, ColumnType, Schema, Table } from '@powersync/common'; +import { AbstractPowerSyncDatabase } from '@powersync/common'; import { v4 as uuid } from 'uuid'; -import { WASQLitePowerSyncDatabaseOpenFactory } from '@powersync/web'; +import { TestDatabase, generateTestDb } from './utils/testDb'; // TODO import tests from a common package -type User = { - name: string; -}; - describe('Basic', () => { - const factory = new WASQLitePowerSyncDatabaseOpenFactory({ - dbFilename: 'test.db', - flags: { - enableMultiTabs: false - }, - schema: new Schema([ - new Table({ - name: 'users', - columns: [new Column({ name: 'name', type: ColumnType.TEXT })] - }) - ]) - }); - - let db: AbstractPowerSyncDatabase; + let dbWithoutWebWorker: AbstractPowerSyncDatabase; + let dbWithWebWorker: AbstractPowerSyncDatabase; beforeEach(() => { - db = factory.getInstance(); + dbWithoutWebWorker = generateTestDb({ useWebWorker: false }); + dbWithWebWorker = generateTestDb(); }); + /** + * Declares a test to be executed with multiple DB connections + */ + const itWithDBs = (name: string, test: (db: AbstractPowerSyncDatabase) => Promise) => { + it(`${name} - with web worker`, () => test(dbWithWebWorker)); + it(`${name} - without web worker`, () => test(dbWithoutWebWorker)); + }; + afterEach(async () => { - await db.disconnectAndClear(); - await db.close(); + await dbWithWebWorker.disconnectAndClear(); + await dbWithWebWorker.close(); + await dbWithoutWebWorker.disconnectAndClear(); + await dbWithoutWebWorker.close(); }); describe('executeQuery', () => { - it('should execute a select query using getAll', async () => { - const result = await db.getAll('SELECT * FROM users'); + itWithDBs('should execute a select query using getAll', async (db) => { + const result = await db.getAll('SELECT * FROM customers'); expect(result.length).toEqual(0); }); - it('should allow inserts', async () => { + itWithDBs('should allow inserts', async (db) => { const testName = 'Steven'; - await db.execute('INSERT INTO users (id, name) VALUES(?, ?)', [uuid(), testName]); - const result = await db.get('SELECT * FROM users'); + await db.execute('INSERT INTO customers (id, name) VALUES(?, ?)', [uuid(), testName]); + const result = await db.get('SELECT * FROM customers'); expect(result.name).equals(testName); }); }); describe('executeBatchQuery', () => { - it('should execute a select query using getAll', async () => { - const result = await db.getAll('SELECT * FROM users'); + itWithDBs('should execute a select query using getAll', async (db) => { + const result = await db.getAll('SELECT * FROM customers'); expect(result.length).toEqual(0); }); - it('should allow batch inserts', async () => { + itWithDBs('should allow batch inserts', async (db) => { const testName = 'Mugi'; - await db.executeBatch('INSERT INTO users (id, name) VALUES(?, ?)', [ + await db.executeBatch('INSERT INTO customers (id, name) VALUES(?, ?)', [ [uuid(), testName], [uuid(), 'Steven'], [uuid(), 'Chris'] ]); - const result = await db.getAll('SELECT * FROM users'); + const result = await db.getAll('SELECT * FROM customers'); expect(result.length).equals(3); expect(result[0].name).equals(testName); diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index 0db7b4ef..ec107226 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -6,7 +6,7 @@ import { WebRemote, WebStreamingSyncImplementationOptions } from '@powersync/web'; -import { testSchema } from './utils/test-schema'; +import { testSchema } from './utils/testDb'; import { TestConnector } from './utils/MockStreamOpenFactory'; import { Mutex } from 'async-mutex'; diff --git a/packages/web/tests/stream.test.ts b/packages/web/tests/stream.test.ts index 7d3609d5..46530c9f 100644 --- a/packages/web/tests/stream.test.ts +++ b/packages/web/tests/stream.test.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import Logger from 'js-logger'; import { beforeAll, describe, expect, it } from 'vitest'; import { v4 as uuid } from 'uuid'; -import { AbstractPowerSyncDatabase, Column, ColumnType, Schema, SyncStatusOptions, Table } from '@powersync/common'; +import { AbstractPowerSyncDatabase, Schema, SyncStatusOptions, TableV2, column } from '@powersync/common'; import { MockRemote, MockStreamOpenFactory, TestConnector } from './utils/MockStreamOpenFactory'; export async function waitForConnectionStatus( @@ -24,7 +24,7 @@ export async function waitForConnectionStatus( }); } -export async function generateConnectedDatabase() { +export async function generateConnectedDatabase({ useWebWorker } = { useWebWorker: true }) { /** * Very basic implementation of a listener pattern. * Required since we cannot extend multiple classes. @@ -32,18 +32,22 @@ export async function generateConnectedDatabase() { const callbacks: Map void> = new Map(); const remote = new MockRemote(new TestConnector(), () => callbacks.forEach((c) => c())); + const users = new TableV2({ + name: column.text + }); + + const schema = new Schema({ + users + }); + const factory = new MockStreamOpenFactory( { dbFilename: 'test-stream-connection.db', flags: { - enableMultiTabs: false + enableMultiTabs: false, + useWebWorker }, - schema: new Schema([ - new Table({ - name: 'users', - columns: [new Column({ name: 'name', type: ColumnType.TEXT })] - }) - ]) + schema }, remote ); @@ -78,36 +82,49 @@ export async function generateConnectedDatabase() { } describe('Stream test', () => { - beforeAll(() => Logger.useDefaults()); + /** + * Declares a test to be executed with different generated db functions + */ + const itWithGenerators = async (name: string, test: (func: () => any) => Promise) => { + const funcWithWebWorker = generateConnectedDatabase; + const funcWithoutWebWorker = () => generateConnectedDatabase({ useWebWorker: false }); - it('PowerSync reconnect on closed stream', async () => { - const { powersync, waitForStream, remote } = await generateConnectedDatabase(); - expect(powersync.connected).true; + it(`${name} - with web worker`, () => test(funcWithWebWorker)); + it(`${name} - without web worker`, () => test(funcWithoutWebWorker)); + }; - // Close the stream - const newStream = waitForStream(); - remote.streamController?.close(); + describe('With Web Worker', () => { + beforeAll(() => Logger.useDefaults()); - // A new stream should be requested - await newStream; + itWithGenerators('PowerSync reconnect on closed stream', async (func) => { + const { powersync, waitForStream, remote } = await func(); + expect(powersync.connected).toBe(true); - await powersync.disconnectAndClear(); - await powersync.close(); - }); + // Close the stream + const newStream = waitForStream(); + remote.streamController?.close(); + + // A new stream should be requested + await newStream; - it('PowerSync reconnect multiple connect calls', async () => { - // This initially performs a connect call - const { powersync, waitForStream } = await generateConnectedDatabase(); - expect(powersync.connected).true; + await powersync.disconnectAndClear(); + await powersync.close(); + }); + + itWithGenerators('PowerSync reconnect multiple connect calls', async (func) => { + // This initially performs a connect call + const { powersync, waitForStream } = await func(); + expect(powersync.connected).toBe(true); - // Call connect again, a new stream should be requested - const newStream = waitForStream(); - powersync.connect(new TestConnector()); + // Call connect again, a new stream should be requested + const newStream = waitForStream(); + powersync.connect(new TestConnector()); - // A new stream should be requested - await newStream; + // A new stream should be requested + await newStream; - await powersync.disconnectAndClear(); - await powersync.close(); + await powersync.disconnectAndClear(); + await powersync.close(); + }); }); }); diff --git a/packages/web/tests/utils/test-schema.ts b/packages/web/tests/utils/test-schema.ts deleted file mode 100644 index 4d1c13a5..00000000 --- a/packages/web/tests/utils/test-schema.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Column, ColumnType, Index, IndexedColumn, Schema, Table } from '@powersync/web'; - -export const testSchema = new Schema([ - new Table({ - name: 'assets', - columns: [ - new Column({ name: 'created_at', type: ColumnType.TEXT }), - new Column({ name: 'make', type: ColumnType.TEXT }), - new Column({ name: 'model', type: ColumnType.TEXT }), - new Column({ name: 'serial_number', type: ColumnType.TEXT }), - new Column({ name: 'quantity', type: ColumnType.INTEGER }), - new Column({ name: 'user_id', type: ColumnType.TEXT }), - new Column({ name: 'customer_id', type: ColumnType.TEXT }), - new Column({ name: 'description', type: ColumnType.TEXT }) - ], - indexes: [ - new Index({ - name: 'makemodel', - columns: [new IndexedColumn({ name: 'make' }), new IndexedColumn({ name: 'model' })] - }) - ] - }), - - new Table({ - name: 'customers', - columns: [new Column({ name: 'name', type: ColumnType.TEXT }), new Column({ name: 'email', type: ColumnType.TEXT })] - }) -]); diff --git a/packages/web/tests/utils/testDb.ts b/packages/web/tests/utils/testDb.ts new file mode 100644 index 00000000..23e47857 --- /dev/null +++ b/packages/web/tests/utils/testDb.ts @@ -0,0 +1,51 @@ +import { column, Schema, TableV2, WASQLitePowerSyncDatabaseOpenFactory } from '@powersync/web'; +import { v4 as uuid } from 'uuid'; + +const assets = new TableV2( + { + created_at: column.text, + make: column.text, + model: column.text, + serial_number: column.text, + quantity: column.integer, + user_id: column.text, + customer_id: column.text, + description: column.text + }, + { indexes: { makemodel: ['make, model'] } } +); + +const customers = new TableV2({ + name: column.text, + email: column.text +}); + +export const testSchema = new Schema({ assets, customers }); + +export const dbFactory = new WASQLitePowerSyncDatabaseOpenFactory({ + dbFilename: 'test-bucket-storage.db', + flags: { + enableMultiTabs: false + }, + schema: testSchema +}); + +export const generateTestDb = ({ useWebWorker } = { useWebWorker: true }) => { + const db = new WASQLitePowerSyncDatabaseOpenFactory({ + /** + * Deleting the IndexDB seems to freeze the test. + * Use a new DB for each run to keep CRUD counters + * consistent + */ + dbFilename: `test-crud-${uuid()}.db`, + schema: testSchema, + flags: { + enableMultiTabs: false, + useWebWorker + } + }).getInstance(); + + return db; +}; + +export type TestDatabase = (typeof testSchema)['types']; diff --git a/packages/web/tests/watch.test.ts b/packages/web/tests/watch.test.ts index f52d78a2..5cbccdbc 100644 --- a/packages/web/tests/watch.test.ts +++ b/packages/web/tests/watch.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { v4 as uuid } from 'uuid'; import { AbstractPowerSyncDatabase } from '@powersync/common'; import { WASQLitePowerSyncDatabaseOpenFactory } from '@powersync/web'; -import { testSchema } from './utils/test-schema'; +import { testSchema } from './utils/testDb'; vi.useRealTimers(); /**