From bb1530acf430e3b4e38f1376d263cc576992d96c Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 7 May 2026 10:50:30 +0200 Subject: [PATCH 01/21] Remove JS logger dependency --- packages/common/package.json | 3 +- .../src/attachments/AttachmentContext.ts | 11 +-- .../common/src/attachments/AttachmentQueue.ts | 8 +- .../src/attachments/AttachmentService.ts | 6 +- .../common/src/attachments/SyncingService.ts | 12 +-- .../src/client/AbstractPowerSyncDatabase.ts | 14 +-- .../client/AbstractPowerSyncOpenFactory.ts | 4 +- .../common/src/client/ConnectionManager.ts | 16 ++-- .../client/sync/bucket/SqliteBucketStorage.ts | 14 +-- .../src/client/sync/stream/AbstractRemote.ts | 20 +++-- .../AbstractStreamingSyncImplementation.ts | 48 +++++----- .../src/client/triggers/TriggerManagerImpl.ts | 11 ++- .../processors/AbstractQueryProcessor.ts | 3 +- packages/common/src/utils/Logger.ts | 88 +++++++++++-------- packages/node/package.json | 1 - packages/node/src/sync/stream/NodeRemote.ts | 5 +- packages/node/tests/sync-stream.test.ts | 25 +++--- packages/node/tests/sync.test.ts | 21 ++--- packages/node/tests/utils.ts | 9 +- packages/react-native/rollup.config.mjs | 1 - pnpm-lock.yaml | 14 --- pnpm-workspace.yaml | 1 - 22 files changed, 180 insertions(+), 155 deletions(-) diff --git a/packages/common/package.json b/packages/common/package.json index ea56c2150..bd6bde63d 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -59,8 +59,7 @@ "test:exports": "attw --pack . --exclude-entrypoints internal/sync_protocol" }, "dependencies": { - "event-iterator": "^2.0.0", - "js-logger": "catalog:" + "event-iterator": "^2.0.0" }, "devDependencies": { "@rollup/plugin-commonjs": "catalog:", diff --git a/packages/common/src/attachments/AttachmentContext.ts b/packages/common/src/attachments/AttachmentContext.ts index 47080fd68..e5d7fc472 100644 --- a/packages/common/src/attachments/AttachmentContext.ts +++ b/packages/common/src/attachments/AttachmentContext.ts @@ -1,5 +1,5 @@ import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js'; -import { ILogger } from '../utils/Logger.js'; +import { LogLevels, PowerSyncLogger } from '../utils/Logger.js'; import { Transaction } from '../db/DBAdapter.js'; import { AttachmentRecord, AttachmentState, attachmentFromSql } from './Schema.js'; @@ -19,7 +19,7 @@ export class AttachmentContext { tableName: string; /** Logger instance for diagnostic information */ - logger: ILogger; + logger: PowerSyncLogger; /** Maximum number of archived attachments to keep before cleanup */ archivedCacheLimit: number = 100; @@ -34,7 +34,7 @@ export class AttachmentContext { constructor( db: AbstractPowerSyncDatabase, tableName: string = 'attachments', - logger: ILogger, + logger: PowerSyncLogger, archivedCacheLimit: number ) { this.db = db; @@ -233,7 +233,8 @@ export class AttachmentContext { if (archivedAttachments.length === 0) return false; await callback?.(archivedAttachments); - this.logger.info( + this.logger.log( + LogLevels.info, `Deleting ${archivedAttachments.length} archived attachments. Archived attachment exceeds cache archiveCacheLimit of ${this.archivedCacheLimit}.` ); @@ -254,7 +255,7 @@ export class AttachmentContext { [JSON.stringify(ids)] ); - this.logger.info(`Deleted ${archivedAttachments.length} archived attachments`); + this.logger.log(LogLevels.info, `Deleted ${archivedAttachments.length} archived attachments`); return archivedAttachments.length < limit; } diff --git a/packages/common/src/attachments/AttachmentQueue.ts b/packages/common/src/attachments/AttachmentQueue.ts index 9c867b186..46e14a4c9 100644 --- a/packages/common/src/attachments/AttachmentQueue.ts +++ b/packages/common/src/attachments/AttachmentQueue.ts @@ -1,7 +1,7 @@ import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js'; import { DEFAULT_WATCH_THROTTLE_MS } from '../client/watched/WatchedQuery.js'; import { DifferentialWatchedQuery } from '../client/watched/processors/DifferentialQueryProcessor.js'; -import { ILogger } from '../utils/Logger.js'; +import { LogLevels, PowerSyncLogger } from '../utils/Logger.js'; import { Transaction } from '../db/DBAdapter.js'; import { AttachmentData, LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; @@ -49,7 +49,7 @@ export class AttachmentQueue { readonly tableName: string; /** Logger instance for diagnostic information */ - readonly logger: ILogger; + readonly logger: PowerSyncLogger; /** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry * failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */ @@ -115,7 +115,7 @@ export class AttachmentQueue { localStorage: LocalStorageAdapter; watchAttachments: (onUpdate: (attachment: WatchedAttachmentItem[]) => Promise, signal: AbortSignal) => void; tableName?: string; - logger?: ILogger; + logger?: PowerSyncLogger; syncIntervalMs?: number; syncThrottleDuration?: number; downloadAttachments?: boolean; @@ -190,7 +190,7 @@ export class AttachmentQueue { if (status.connected) { // Device came online, process attachments immediately this.syncStorage().catch((error) => { - this.logger.error('Error syncing storage on connection:', error); + this.logger.log(LogLevels.error, 'Error syncing storage on connection:', error); }); } } diff --git a/packages/common/src/attachments/AttachmentService.ts b/packages/common/src/attachments/AttachmentService.ts index c1c804550..2c7ef344b 100644 --- a/packages/common/src/attachments/AttachmentService.ts +++ b/packages/common/src/attachments/AttachmentService.ts @@ -1,6 +1,6 @@ import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js'; import { DifferentialWatchedQuery } from '../client/watched/processors/DifferentialQueryProcessor.js'; -import { ILogger } from '../utils/Logger.js'; +import { PowerSyncLogger, LogLevels } from '../utils/Logger.js'; import { Mutex } from '../utils/mutex.js'; import { AttachmentContext } from './AttachmentContext.js'; import { AttachmentRecord, AttachmentState } from './Schema.js'; @@ -16,7 +16,7 @@ export class AttachmentService { constructor( private db: AbstractPowerSyncDatabase, - private logger: ILogger, + private logger: PowerSyncLogger, private tableName: string = 'attachments', archivedCacheLimit: number = 100 ) { @@ -28,7 +28,7 @@ export class AttachmentService { * @returns Watch query that emits changes for queued uploads, downloads, and deletes */ watchActiveAttachments({ throttleMs }: { throttleMs?: number } = {}): DifferentialWatchedQuery { - this.logger.info('Watching active attachments...'); + this.logger.log(LogLevels.info, 'Watching active attachments...'); const watch = this.db .query({ sql: /* sql */ ` diff --git a/packages/common/src/attachments/SyncingService.ts b/packages/common/src/attachments/SyncingService.ts index 1da66c682..8e2cd594f 100644 --- a/packages/common/src/attachments/SyncingService.ts +++ b/packages/common/src/attachments/SyncingService.ts @@ -1,4 +1,4 @@ -import { ILogger } from '../utils/Logger.js'; +import { LogLevels, PowerSyncLogger } from '../utils/Logger.js'; import { AttachmentService } from './AttachmentService.js'; import { LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; @@ -16,14 +16,14 @@ export class SyncingService { private attachmentService: AttachmentService; private localStorage: LocalStorageAdapter; private remoteStorage: RemoteStorageAdapter; - private logger: ILogger; + private logger: PowerSyncLogger; private errorHandler?: AttachmentErrorHandler; constructor( attachmentService: AttachmentService, localStorage: LocalStorageAdapter, remoteStorage: RemoteStorageAdapter, - logger: ILogger, + logger: PowerSyncLogger, errorHandler?: AttachmentErrorHandler ) { this.attachmentService = attachmentService; @@ -75,7 +75,7 @@ export class SyncingService { * @throws Error if the attachment has no localUri */ async uploadAttachment(attachment: AttachmentRecord): Promise { - this.logger.info(`Uploading attachment ${attachment.filename}`); + this.logger.log(LogLevels.info, `Uploading attachment ${attachment.filename}`); try { if (attachment.localUri == null) { throw new Error(`No localUri for attachment ${attachment.id}`); @@ -111,7 +111,7 @@ export class SyncingService { * @returns Updated attachment record with local URI and new state */ async downloadAttachment(attachment: AttachmentRecord): Promise { - this.logger.info(`Downloading attachment ${attachment.filename}`); + this.logger.log(LogLevels.info, `Downloading attachment ${attachment.filename}`); try { const fileData = await this.remoteStorage.downloadFile(attachment); @@ -183,7 +183,7 @@ export class SyncingService { try { await this.localStorage.deleteFile(attachment.localUri); } catch (error) { - this.logger.error('Error deleting local file for archived attachment', error); + this.logger.log(LogLevels.error, 'Error deleting local file for archived attachment', error); } } } diff --git a/packages/common/src/client/AbstractPowerSyncDatabase.ts b/packages/common/src/client/AbstractPowerSyncDatabase.ts index 4af5a25f1..e5c6b2f10 100644 --- a/packages/common/src/client/AbstractPowerSyncDatabase.ts +++ b/packages/common/src/client/AbstractPowerSyncDatabase.ts @@ -1,5 +1,4 @@ import { EventIterator } from 'event-iterator'; -import Logger, { ILogger } from 'js-logger'; import { BatchedUpdateNotification, DBAdapter, @@ -47,6 +46,7 @@ import { DEFAULT_WATCH_THROTTLE_MS, WatchCompatibleQuery } from './watched/Watch import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js'; import { WatchedQueryComparator } from './watched/processors/comparators.js'; import { Mutex } from '../utils/mutex.js'; +import { createPowerSyncLogger, LogLevels, PowerSyncLogger } from '../utils/Logger.js'; export interface DisconnectAndClearOptions { /** When set to false, data in local-only tables is preserved. */ @@ -60,7 +60,7 @@ export interface BasePowerSyncDatabaseOptions extends AdditionalConnectionOption * @deprecated Use {@link retryDelayMs} instead as this will be removed in future releases. */ retryDelay?: number; - logger?: ILogger; + logger?: PowerSyncLogger; } export interface PowerSyncDatabaseOptions extends BasePowerSyncDatabaseOptions { @@ -226,7 +226,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver this.logger.error(e) } = handler ?? {}; + const { onResult, onError = (e: Error) => this.logger.log(LogLevels.error, e) } = handler ?? {}; if (!onResult) { throw new Error('onResult is required'); } @@ -1241,7 +1241,7 @@ SELECT * FROM crud_entries; * @returns A dispose function to stop watching for changes */ onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void { - const { onChange, onError = (e: Error) => this.logger.error(e) } = handler ?? {}; + const { onChange, onError = (e: Error) => this.logger.log(LogLevels.error, e) } = handler ?? {}; if (!onChange) { throw new Error('onChange is required'); } diff --git a/packages/common/src/client/AbstractPowerSyncOpenFactory.ts b/packages/common/src/client/AbstractPowerSyncOpenFactory.ts index 68f6dced5..e9fc2bb08 100644 --- a/packages/common/src/client/AbstractPowerSyncOpenFactory.ts +++ b/packages/common/src/client/AbstractPowerSyncOpenFactory.ts @@ -1,4 +1,4 @@ -import Logger from 'js-logger'; +import { createPowerSyncLogger } from '../utils/Logger.js'; import { DBAdapter } from '../db/DBAdapter.js'; import { Schema } from '../db/schema/Schema.js'; import { AbstractPowerSyncDatabase, PowerSyncDatabaseOptions } from './AbstractPowerSyncDatabase.js'; @@ -11,7 +11,7 @@ export interface PowerSyncOpenFactoryOptions extends Partial; - logger: ILogger; + logger: PowerSyncLogger; } type StoredConnectionOptions = { @@ -194,7 +194,10 @@ export class ConnectionManager extends BaseObserver { this.syncStreamInitPromise = new Promise(async (resolve, reject) => { try { if (!this.pendingConnectionOptions) { - this.logger.debug('No pending connection options found, not creating sync stream implementation'); + this.logger.log( + LogLevels.debug, + 'No pending connection options found, not creating sync stream implementation' + ); // A disconnect could have cleared this. resolve(); return; @@ -236,7 +239,7 @@ export class ConnectionManager extends BaseObserver { // and this point. Awaiting here allows the sync stream to be cleared if disconnected. await this.disconnectingPromise; - this.logger.debug('Attempting to connect to PowerSync instance'); + this.logger.log(LogLevels.debug, 'Attempting to connect to PowerSync instance'); await this.syncStreamImplementation?.connect(appliedOptions!); } @@ -350,7 +353,7 @@ class ActiveSubscription { constructor( readonly name: string, readonly parameters: Record | null, - readonly logger: ILogger, + readonly logger: PowerSyncLogger, readonly waitForFirstSync: (abort?: AbortSignal) => Promise, private clearSubscription: () => void ) {} @@ -395,7 +398,8 @@ class SyncStreamSubscriptionHandle implements SyncStreamSubscription { const _finalizer = 'FinalizationRegistry' in globalThis ? new FinalizationRegistry((sub) => { - sub.logger.warn( + sub.logger.log( + LogLevels.warn, `A subscription to ${sub.name} with params ${JSON.stringify(sub.parameters)} leaked! Please ensure calling unsubscribe() when you don't need a subscription anymore. For global subscriptions, consider storing them in global fields to avoid this warning.` ); }) diff --git a/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts b/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts index a146a9930..2334acf9c 100644 --- a/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts +++ b/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts @@ -1,4 +1,4 @@ -import Logger, { ILogger } from 'js-logger'; +import { LogLevels, PowerSyncLogger } from '../../../utils/Logger.js'; import { DBAdapter, extractTableUpdates, Transaction } from '../../../db/DBAdapter.js'; import { BaseObserver } from '../../../utils/BaseObserver.js'; import { MAX_OP_ID } from '../../constants.js'; @@ -18,7 +18,7 @@ export class SqliteBucketStorage extends BaseObserver imp constructor( private db: DBAdapter, - private logger: ILogger = Logger.get('SqliteBucketStorage') + private logger: PowerSyncLogger ) { super(); this.tableNames = new Set(); @@ -84,7 +84,10 @@ export class SqliteBucketStorage extends BaseObserver imp const anyData = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1'); if (anyData.rows?.length) { // if isNotEmpty - this.logger.debug(`New data uploaded since write checkpoint ${opId} - need new write checkpoint`); + this.logger.log( + LogLevels.debug, + `New data uploaded since write checkpoint ${opId} - need new write checkpoint` + ); return false; } @@ -96,7 +99,8 @@ export class SqliteBucketStorage extends BaseObserver imp const seqAfter: number = rs.rows?.item(0)['seq']; if (seqAfter != seqBefore) { - this.logger.debug( + this.logger.log( + LogLevels.debug, `New data uploaded since write checpoint ${opId} - need new write checkpoint (sequence updated)` ); @@ -104,7 +108,7 @@ export class SqliteBucketStorage extends BaseObserver imp return false; } - this.logger.debug(`Updating target write checkpoint to ${opId}`); + this.logger.log(LogLevels.debug, `Updating target write checkpoint to ${opId}`); await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [opId]); return true; }); diff --git a/packages/common/src/client/sync/stream/AbstractRemote.ts b/packages/common/src/client/sync/stream/AbstractRemote.ts index 44c358b2a..7e333f937 100644 --- a/packages/common/src/client/sync/stream/AbstractRemote.ts +++ b/packages/common/src/client/sync/stream/AbstractRemote.ts @@ -1,8 +1,8 @@ import { type fetch } from 'cross-fetch'; -import Logger, { ILogger } from 'js-logger'; import { Requestable, RSocket, RSocketConnector } from 'rsocket-core'; import PACKAGE from '../../../../package.json' with { type: 'json' }; import { AbortOperation } from '../../../utils/AbortOperation.js'; +import { LogLevels, PowerSyncLogger } from '../../../utils/Logger.js'; import { PowerSyncCredentials } from '../../connection/PowerSyncCredentials.js'; import { WebsocketClientTransport } from './WebsocketClientTransport.js'; import { @@ -36,8 +36,6 @@ const SOCKET_TIMEOUT_MS = 30_000; // significantly. Therefore this is longer than the socket timeout. const KEEP_ALIVE_LIFETIME_MS = 90_000; -export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote'); - export type SyncStreamOptions = { path: string; data: unknown; @@ -117,7 +115,7 @@ export abstract class AbstractRemote { constructor( protected connector: RemoteConnector, - protected logger: ILogger = DEFAULT_REMOTE_LOGGER, + protected logger: PowerSyncLogger, options?: Partial ) { this.options = { @@ -334,7 +332,10 @@ export abstract class AbstractRemote { const resetTimeout = () => { clearTimeout(keepAliveTimeout); keepAliveTimeout = setTimeout(() => { - this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`); + this.logger.log( + LogLevels.error, + `No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.` + ); abortRequest(); }, SOCKET_TIMEOUT_MS); }; @@ -372,7 +373,7 @@ export abstract class AbstractRemote { // The connection is established, we no longer need to monitor the initial timeout pendingSocket = null; } catch (ex) { - this.logger.error(`Failed to connect WebSocket`, ex); + this.logger.log(LogLevels.error, `Failed to connect WebSocket`, ex); abortRequest(); throw ex; @@ -433,7 +434,7 @@ export abstract class AbstractRemote { // Don't log closed as an error if (e.message !== 'Closed. ') { - this.logger.error(e); + this.logger.log(LogLevels.error, e); } // RSocket will close the RSocket stream automatically // Close the downstream stream as well - this will close the RSocket connection and WebSocket @@ -545,7 +546,10 @@ export abstract class AbstractRemote { if (!res.ok || !res.body) { const text = await res.text(); - this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`); + this.logger.log( + LogLevels.error, + `Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}` + ); const error: any = new Error(`HTTP ${res.statusText}: ${text}`); error.status = res.status; throw error; diff --git a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index 14c0e2a0f..ecc7161a0 100644 --- a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -1,8 +1,7 @@ -import Logger, { ILogger } from 'js-logger'; - import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js'; import { AbortOperation } from '../../../utils/AbortOperation.js'; import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js'; +import { LogLevels, PowerSyncLogger } from '../../../utils/Logger.js'; import { BucketStorageAdapter, PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js'; import { CrudEntry } from '../bucket/CrudEntry.js'; import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemote.js'; @@ -75,7 +74,7 @@ export interface AbstractStreamingSyncImplementationOptions extends RequiredAddi * linked to. Most commonly DB name, but not restricted to DB name. */ identifier?: string; - logger?: ILogger; + logger: PowerSyncLogger; remote: AbstractRemote; } @@ -216,7 +215,7 @@ export abstract class AbstractStreamingSyncImplementation protected abortController: AbortController | null; protected crudUpdateListener?: () => void; protected streamingSyncPromise?: Promise<[void, void]>; - protected logger: ILogger; + protected logger: PowerSyncLogger; private activeStreams: SubscribedStream[]; private connectionMayHaveChanged = false; private crudUploadNotifier = asyncNotifier(); @@ -230,7 +229,7 @@ export abstract class AbstractStreamingSyncImplementation super(); this.options = options; this.activeStreams = options.subscriptions; - this.logger = options.logger ?? Logger.get('PowerSyncStream'); + this.logger = options.logger; this.syncStatus = new SyncStatus({ connected: false, @@ -310,7 +309,7 @@ export abstract class AbstractStreamingSyncImplementation let path = `/write-checkpoint2.json?client_id=${clientId}`; const response = await this.options.remote.get(path); const checkpoint = response['data']['write_checkpoint'] as string; - this.logger.debug(`Created write checkpoint: ${checkpoint}`); + this.logger.log(LogLevels.debug, `Created write checkpoint: ${checkpoint}`); return checkpoint; } @@ -350,9 +349,12 @@ export abstract class AbstractStreamingSyncImplementation if (nextCrudItem.clientId == checkedCrudItem?.clientId) { // This will force a higher log level than exceptions which are caught here. - this.logger.warn(`Potentially previously uploaded CRUD entries are still present in the upload queue. + this.logger.log( + LogLevels.warn, + `Potentially previously uploaded CRUD entries are still present in the upload queue. Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their [.complete()] method. -The next upload iteration will be delayed.`); +The next upload iteration will be delayed.` + ); throw new Error('Delaying due to previously encountered CRUD item.'); } @@ -370,7 +372,7 @@ The next upload iteration will be delayed.`); this.notifyCompletedUploads?.(); } else if (checkedCrudItem != null) { // Only log this if there was something to upload - this.logger.debug('Upload complete, no write checkpoint needed.'); + this.logger.log(LogLevels.debug, 'Upload complete, no write checkpoint needed.'); } break; } @@ -387,7 +389,8 @@ The next upload iteration will be delayed.`); // Exit the upload loop if the sync stream is no longer connected break; } - this.logger.debug( + this.logger.log( + LogLevels.debug, `Caught exception when uploading. Upload will retry after a delay. Exception: ${(ex as Error).message}` ); } finally { @@ -410,7 +413,9 @@ The next upload iteration will be delayed.`); const controller = new AbortController(); this.abortController = controller; this.streamingSyncPromise = Promise.all([ - this.crudUploadLoop(controller.signal).catch((ex) => this.logger.error('Error in crud upload loop', ex)), + this.crudUploadLoop(controller.signal).catch((ex) => + this.logger.log(LogLevels.error, 'Error in crud upload loop', ex) + ), this.streamingSync(controller.signal, options) ]); @@ -419,7 +424,7 @@ The next upload iteration will be delayed.`); const disposer = this.registerListener({ statusChanged: (status) => { if (status.dataFlowStatus.downloadError != null) { - this.logger.warn('Initial connect attempt did not successfully connect to server'); + this.logger.log(LogLevels.warn, 'Initial connect attempt did not successfully connect to server'); } else if (status.connecting) { // Still connecting. return; @@ -447,7 +452,7 @@ The next upload iteration will be delayed.`); await this.streamingSyncPromise; } catch (ex) { // The operation might have failed, all we care about is if it has completed - this.logger.warn(ex); + this.logger.log(LogLevels.warn, ex); } this.streamingSyncPromise = undefined; @@ -516,15 +521,15 @@ The next upload iteration will be delayed.`); */ if (ex instanceof AbortOperation) { - this.logger.warn(ex); + this.logger.log(LogLevels.warn, ex); shouldDelayRetry = false; // A disconnect was requested, we should not delay since there is no explicit retry } else if (this.connectionMayHaveChanged && (ex as Error).message?.indexOf('No iteration is active') >= 0) { this.connectionMayHaveChanged = false; - this.logger.info('Sync error after changed connection, retrying immediately'); + this.logger.log(LogLevels.info, 'Sync error after changed connection, retrying immediately'); shouldDelayRetry = false; } else { - this.logger.error(ex); + this.logger.log(LogLevels.error, ex); } this.updateSyncStatus({ @@ -718,7 +723,8 @@ The next upload iteration will be delayed.`); ): Promise { const rawResponse = await adapter.control(op, payload ?? null); const logger = syncImplementation.logger; - logger.trace( + logger.log( + LogLevels.trace, 'powersync_control', op, payload == null || typeof payload == 'string' ? payload : '', @@ -737,13 +743,13 @@ The next upload iteration will be delayed.`); if ('LogLine' in instruction) { switch (instruction.LogLine.severity) { case 'DEBUG': - syncImplementation.logger.debug(instruction.LogLine.line); + syncImplementation.logger.log(LogLevels.debug, instruction.LogLine.line); break; case 'INFO': - syncImplementation.logger.info(instruction.LogLine.line); + syncImplementation.logger.log(LogLevels.info, instruction.LogLine.line); break; case 'WARNING': - syncImplementation.logger.warn(instruction.LogLine.line); + syncImplementation.logger.log(LogLevels.warn, instruction.LogLine.line); break; } } else if ('UpdateSyncStatus' in instruction) { @@ -760,7 +766,7 @@ The next upload iteration will be delayed.`); notifyTokenRefreshed?.(); }, (err) => { - syncImplementation.logger.warn('Could not prefetch credentials', err); + syncImplementation.logger.log(LogLevels.warn, 'Could not prefetch credentials', err); } ); } diff --git a/packages/common/src/client/triggers/TriggerManagerImpl.ts b/packages/common/src/client/triggers/TriggerManagerImpl.ts index 66f6908a6..5c76efa72 100644 --- a/packages/common/src/client/triggers/TriggerManagerImpl.ts +++ b/packages/common/src/client/triggers/TriggerManagerImpl.ts @@ -1,5 +1,6 @@ import { LockContext } from '../../db/DBAdapter.js'; import { Schema } from '../../db/schema/Schema.js'; +import { LogLevels } from '../../utils/Logger.js'; import type { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js'; import { DEFAULT_WATCH_THROTTLE_MS } from '../watched/WatchedQuery.js'; import { @@ -81,7 +82,7 @@ export class TriggerManagerImpl implements TriggerManager { try { await this.cleanupResources(); } catch (ex) { - this.db.logger.error(`Caught error while attempting to cleanup triggers`, ex); + this.db.logger.log(LogLevels.error, `Caught error while attempting to cleanup triggers`, ex); } finally { // if not closed, set another timeout if (this.isDisposed) { @@ -183,7 +184,10 @@ export class TriggerManagerImpl implements TriggerManager { continue; } - this.db.logger.debug(`Clearing resources for trigger ${trackedItem.id} with table ${trackedItem.table}`); + this.db.logger.log( + LogLevels.debug, + `Clearing resources for trigger ${trackedItem.id} with table ${trackedItem.table}` + ); // We need to delete the triggers and table for (const triggerName of trackedItem.triggerNames) { @@ -259,7 +263,8 @@ export class TriggerManagerImpl implements TriggerManager { const disposeWarningListener = this.db.registerListener({ schemaChanged: () => { - this.db.logger.warn( + this.db.logger.log( + LogLevels.warn, `The PowerSync schema has changed while previously configured triggers are still operational. This might cause unexpected results.` ); } diff --git a/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts b/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts index 18c165ee5..7c07843f8 100644 --- a/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts +++ b/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts @@ -1,3 +1,4 @@ +import { LogLevels } from '../../../utils/Logger.js'; import { AbstractPowerSyncDatabase } from '../../../client/AbstractPowerSyncDatabase.js'; import { MetaBaseObserver } from '../../../utils/MetaBaseObserver.js'; import { @@ -219,7 +220,7 @@ export abstract class AbstractQueryProcessor< } catch (error) { // Errors here are ignored // since we are already in an error state - this.options.db.logger.error('Watched query error handler threw an Error', error); + this.options.db.logger.log(LogLevels.error, 'Watched query error handler threw an Error', error); } } } diff --git a/packages/common/src/utils/Logger.ts b/packages/common/src/utils/Logger.ts index f954622ef..08d204559 100644 --- a/packages/common/src/utils/Logger.ts +++ b/packages/common/src/utils/Logger.ts @@ -1,47 +1,65 @@ -import Logger, { type ILogger, type ILogLevel } from 'js-logger'; - -export { GlobalLogger, ILogger, ILoggerOpts, ILogHandler, ILogLevel } from 'js-logger'; - -const TypedLogger: ILogger = Logger as any; - -export const LogLevel = { - TRACE: TypedLogger.TRACE, - DEBUG: TypedLogger.DEBUG, - INFO: TypedLogger.INFO, - TIME: TypedLogger.TIME, - WARN: TypedLogger.WARN, - ERROR: TypedLogger.ERROR, - OFF: TypedLogger.OFF -}; - -export interface CreateLoggerOptions { - logLevel?: ILogLevel; -} +export const LogLevels = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50 +} as const; /** - * Retrieves the base (default) logger instance. + * A logger used by the PowerSync SDK. * - * This base logger controls the default logging configuration and is shared - * across all loggers created with `createLogger`. Adjusting settings on this - * base logger affects all loggers derived from it unless explicitly overridden. + * This is deliberately a very simple interface, and it's not a designed to be a general-purpose logger you would use in + * your application. Instead, you can provide an implementation of this to PowerSync to make it use your preferred + * logging libraries. * + * By default, the SDK uses a {@link createPowerSyncLogger} instance forwarding messages to `console.log`. */ -export function createBaseLogger() { - return Logger; +export interface PowerSyncLogger { + /** + * Log a message. + * + * @param level The log level (see {@link LogLevels} for preconfigured values) for the message. Depending on how the + * logger has been confired, messages below a configured minimum level may be ignored. + * @param message Components of the message to log. Components aren't necessarily strings. + */ + log(level: number, ...message: any[]): void; +} + +export interface CreateLoggerOptions { + /** + * A prefix for messages emitted by {@link createPowerSyncLogger} to make them more recognizable. + * + * Defaults to `'PowerSync'`. + */ + prefix: string; + + /** + * The minimum log level to consider for messages. Defaults to {@link LogLevels.info}. + */ + minLevel: number; } /** - * Creates and configures a new named logger based on the base logger. + * A very simple {@link PowerSyncLogger} implementation forwarding messages to `console.log`. * - * Named loggers allow specific modules or areas of your application to have - * their own logging levels and behaviors. These loggers inherit configuration - * from the base logger by default but can override settings independently. + * @param options Options to configure a minimum severity of the logger or a prefix to make messages more recognizable. */ -export function createLogger(name: string, options: CreateLoggerOptions = {}): ILogger { - const logger = Logger.get(name); - if (options.logLevel) { - logger.setLevel(options.logLevel); - } +export function createPowerSyncLogger(options?: Partial): PowerSyncLogger { + const { prefix = 'PowerSync', minLevel = LogLevels.info } = options ?? {}; + + return { + log(level, ...message) { + if (level < minLevel) return; + + let emitter = console.log; + if (level >= LogLevels.error) { + emitter = console.error; + } else if (level >= LogLevels.warn) { + emitter = console.warn; + } - return logger; + emitter(prefix, ...message); + } + }; } diff --git a/packages/node/package.json b/packages/node/package.json index 03d06f342..5e46255ba 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -77,7 +77,6 @@ "@types/better-sqlite3": "^7.6.13", "bson": "catalog:", "drizzle-orm": "catalog:", - "js-logger": "catalog:", "rollup": "catalog:", "rollup-plugin-dts": "catalog:", "typescript": "catalog:", diff --git a/packages/node/src/sync/stream/NodeRemote.ts b/packages/node/src/sync/stream/NodeRemote.ts index 6dd5f2777..783b833ac 100644 --- a/packages/node/src/sync/stream/NodeRemote.ts +++ b/packages/node/src/sync/stream/NodeRemote.ts @@ -1,12 +1,11 @@ import * as os from 'node:os'; import { - type ILogger, AbstractRemote, AbstractRemoteOptions, - DEFAULT_REMOTE_LOGGER, FetchImplementation, FetchImplementationProvider, + PowerSyncLogger, RemoteConnector } from '@powersync/common'; import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, ProxyAgent, WebSocket as UndiciWebSocket } from 'undici'; @@ -36,7 +35,7 @@ export class NodeRemote extends AbstractRemote { constructor( protected connector: RemoteConnector, - protected logger: ILogger = DEFAULT_REMOTE_LOGGER, + protected logger: PowerSyncLogger, options?: Partial ) { const fetchDispatcher = options?.dispatcher ?? defaultFetchDispatcher(); diff --git a/packages/node/tests/sync-stream.test.ts b/packages/node/tests/sync-stream.test.ts index 51aa08146..489f0416e 100644 --- a/packages/node/tests/sync-stream.test.ts +++ b/packages/node/tests/sync-stream.test.ts @@ -1,17 +1,20 @@ import { describe, vi, expect, onTestFinished } from 'vitest'; -import { PowerSyncConnectionOptions, SyncStreamConnectionMethod } from '@powersync/common'; -import Logger from 'js-logger'; +import { + createPowerSyncLogger, + LogLevels, + PowerSyncConnectionOptions, + SyncStreamConnectionMethod +} from '@powersync/common'; import { bucket, checkpoint, mockSyncServiceTest, nextStatus, stream, TestConnector } from './utils.js'; -Logger.useDefaults({ defaultLevel: (Logger as any).WARN }); - describe('Sync streams', () => { const defaultOptions = { connectionMethod: SyncStreamConnectionMethod.HTTP } satisfies PowerSyncConnectionOptions; + const logger = createPowerSyncLogger({ minLevel: LogLevels.warn }); mockSyncServiceTest('can disable default streams', async ({ syncService }) => { - const database = await syncService.createDatabase(); + const database = await syncService.createDatabase({ logger }); await database.connect(new TestConnector(), { includeDefaultStreams: false, ...defaultOptions @@ -26,7 +29,7 @@ describe('Sync streams', () => { }); mockSyncServiceTest('subscribes with streams', async ({ syncService }) => { - const database = await syncService.createDatabase(); + const database = await syncService.createDatabase({ logger }); const a = await database.syncStream('stream', { foo: 'a' }).subscribe(); const b = await database.syncStream('stream', { foo: 'b' }).subscribe({ priority: 1 }); onTestFinished(() => { @@ -84,7 +87,7 @@ describe('Sync streams', () => { }); mockSyncServiceTest('reports default streams', async ({ syncService }) => { - const database = await syncService.createDatabase(); + const database = await syncService.createDatabase({ logger }); await database.connect(new TestConnector(), defaultOptions); let statusPromise = nextStatus(database); @@ -109,7 +112,7 @@ describe('Sync streams', () => { }); mockSyncServiceTest('changes subscriptions dynamically', async ({ syncService }) => { - const database = await syncService.createDatabase(); + const database = await syncService.createDatabase({ logger }); await database.connect(new TestConnector(), defaultOptions); syncService.pushLine( @@ -142,7 +145,7 @@ describe('Sync streams', () => { }); mockSyncServiceTest('subscriptions update while offline', async ({ syncService }) => { - const database = await syncService.createDatabase(); + const database = await syncService.createDatabase({ logger }); let statusPromise = nextStatus(database); const subscription = await database.syncStream('foo').subscribe(); @@ -151,7 +154,7 @@ describe('Sync streams', () => { }); mockSyncServiceTest('unsubscribing multiple times has no effect', async ({ syncService }) => { - const database = await syncService.createDatabase(); + const database = await syncService.createDatabase({ logger }); const a = await database.syncStream('a').subscribe(); const aAgain = await database.syncStream('a').subscribe(); a.unsubscribe(); @@ -172,7 +175,7 @@ describe('Sync streams', () => { }); mockSyncServiceTest('unsubscribeAll', async ({ syncService }) => { - const database = await syncService.createDatabase(); + const database = await syncService.createDatabase({ logger }); const a = await database.syncStream('a').subscribe(); database.syncStream('a').unsubscribeAll(); diff --git a/packages/node/tests/sync.test.ts b/packages/node/tests/sync.test.ts index a142cc5a1..e8e3ee481 100644 --- a/packages/node/tests/sync.test.ts +++ b/packages/node/tests/sync.test.ts @@ -3,14 +3,13 @@ import { beforeEach, describe, expect, vi } from 'vitest'; import { AbstractPowerSyncDatabase, - createLogger, PowerSyncConnectionOptions, + PowerSyncLogger, ProgressWithOperations, Schema, SyncClientImplementation, SyncStreamConnectionMethod } from '@powersync/common'; -import Logger from 'js-logger'; import { bucket, MockSyncService, @@ -606,11 +605,12 @@ function defineSyncTests(bson: boolean) { }); mockSyncServiceTest('handles uploads across checkpoints', async ({ syncService }) => { - const logger = createLogger('test', { logLevel: (Logger as any).TRACE }); const logMessages: string[] = []; - (logger as any).invoke = (level, args) => { - console.log(...args); - logMessages.push(util.format(...args)); + const logger: PowerSyncLogger = { + log(_level, ...message) { + console.log(...message); + logMessages.push(util.format(...message)); + } }; // Regression test for https://github.com/powersync-ja/powersync-js/pull/665 @@ -937,11 +937,12 @@ function defineSyncTests(bson: boolean) { mockSyncServiceTest('can reconnect based on query changes', async ({ syncService }) => { // Test for https://discord.com/channels/1138230179878154300/1399340612435710034/1399340612435710034 - const logger = createLogger('test', { logLevel: (Logger as any).TRACE }); const logMessages: string[] = []; - (logger as any).invoke = (level, args) => { - console.log(...args); - logMessages.push(util.format(...args)); + const logger: PowerSyncLogger = { + log(_level, ...message) { + console.log(...message); + logMessages.push(util.format(...message)); + } }; const powersync = await syncService.createDatabase({ logger }); diff --git a/packages/node/tests/utils.ts b/packages/node/tests/utils.ts index 5ce460865..f230e4056 100644 --- a/packages/node/tests/utils.ts +++ b/packages/node/tests/utils.ts @@ -3,9 +3,7 @@ import os from 'node:os'; import path from 'node:path'; import { ReadableStream, TransformStream } from 'node:stream/web'; -import { createLogger } from '@powersync/common'; import { BucketChecksum, StreamingSyncCheckpoint, StreamingSyncLine } from '@powersync/common/internal/sync_protocol'; -import Logger from 'js-logger'; import { onTestFinished, test } from 'vitest'; import { AbstractPowerSyncDatabase, @@ -19,6 +17,7 @@ import { column } from '../lib/index.js'; import { BSON } from 'bson'; +import { createPowerSyncLogger, LogLevels } from '@powersync/common'; export async function createTempDir() { const ostmpdir = os.tmpdir(); @@ -57,10 +56,7 @@ export async function createDatabase( tmpdir: string, options: Partial = {} ): Promise { - const defaultLogger = createLogger('PowerSyncTest', { logLevel: (Logger as any).TRACE }); - (defaultLogger as any).invoke = (_: any, args: any) => { - console.log(...args); - }; + const defaultLogger = createPowerSyncLogger({ prefix: 'PowerSyncTest', minLevel: LogLevels.trace }); const database = new PowerSyncDatabase({ schema: AppSchema, @@ -200,6 +196,7 @@ export interface MockSyncService { installRequestInterceptor(interceptor: (request: Request) => Promise): void; pushLine: (line: StreamingSyncLine) => void; connectedListeners: any[]; + createDatabase: (options?: Partial) => Promise; } diff --git a/packages/react-native/rollup.config.mjs b/packages/react-native/rollup.config.mjs index 140573022..6ba9a4a86 100644 --- a/packages/react-native/rollup.config.mjs +++ b/packages/react-native/rollup.config.mjs @@ -58,7 +58,6 @@ export default () => { '@powersync/common', '@powersync/react', 'node-fetch', - 'js-logger', 'react-native', 'react' ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6a8f03df..a7daae9f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,6 @@ catalogs: glob: specifier: ^11.0.0 version: 11.1.0 - js-logger: - specifier: ^1.6.1 - version: 1.6.1 jsdom: specifier: ^24.0.0 version: 24.1.3 @@ -400,9 +397,6 @@ importers: event-iterator: specifier: ^2.0.0 version: 2.0.0 - js-logger: - specifier: 'catalog:' - version: 1.6.1 devDependencies: '@rollup/plugin-commonjs': specifier: 'catalog:' @@ -554,9 +548,6 @@ importers: drizzle-orm: specifier: 'catalog:' version: 0.44.7(@op-engineering/op-sqlite@15.2.7(react-native@0.84.1(@babel/core@7.29.0)(@react-native-community/cli@20.0.0(typescript@6.0.3))(@react-native/metro-config@0.83.1(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.10.0)(kysely@0.28.11)(sql.js@1.14.0) - js-logger: - specifier: 'catalog:' - version: 1.6.1 rollup: specifier: 'catalog:' version: 4.59.0 @@ -11722,9 +11713,6 @@ packages: joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - js-logger@1.6.1: - resolution: {integrity: sha512-yTgMCPXVjhmg28CuUH8CKjU+cIKL/G+zTu4Fn4lQxs8mRFH/03QTNvEFngcxfg/gRDiQAOoyCKmMTOm9ayOzXA==} - js-message@1.0.7: resolution: {integrity: sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==} engines: {node: '>=0.6.0'} @@ -30699,8 +30687,6 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - js-logger@1.6.1: {} - js-message@1.0.7: {} js-queue@2.0.2: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 09509693f..238835462 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,7 +33,6 @@ catalog: eslint: ^8.57.1 eslint-plugin-prettier: ^5.5.4 glob: ^11.0.0 - js-logger: ^1.6.1 jsdom: ^24.0.0 kysely: ^0.28.0 p-defer: ^4.0.1 From b20ec5bb1ccab964b19fbb4c2c2779441a918121 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 7 May 2026 11:52:58 +0200 Subject: [PATCH 02/21] Fix more logger usages --- .../src/AbstractAttachmentQueue.ts | 45 +++++++------- .../composables/useDiagnosticsLogger.ts | 59 ++++++++----------- 2 files changed, 50 insertions(+), 54 deletions(-) diff --git a/packages/attachments/src/AbstractAttachmentQueue.ts b/packages/attachments/src/AbstractAttachmentQueue.ts index 3c4d6a0af..4c0c9d923 100644 --- a/packages/attachments/src/AbstractAttachmentQueue.ts +++ b/packages/attachments/src/AbstractAttachmentQueue.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, Transaction } from '@powersync/common'; +import { AbstractPowerSyncDatabase, LogLevels, Transaction } from '@powersync/common'; import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js'; import { EncodingType, StorageAdapter } from './StorageAdapter.js'; @@ -92,7 +92,7 @@ export abstract class AbstractAttachmentQueue { const _ids = `${ids.map((id) => `'${id}'`).join(',')}`; - this.logger.debug(`Queuing for sync, attachment IDs: [${_ids}]`); + this.logger.log(LogLevels.debug, `Queuing for sync, attachment IDs: [${_ids}]`); if (this.initialSync) { this.initialSync = false; @@ -155,11 +155,14 @@ export abstract class AbstractAttachmentQueue 0) { const id = this.downloadQueue.values().next().value!; this.downloadQueue.delete(id); @@ -478,9 +481,9 @@ export abstract class AbstractAttachmentQueue { for (const record of res) { await this.delete(record, tx); @@ -531,7 +534,7 @@ export abstract class AbstractAttachmentQueue { - this.logger.debug(`Clearing attachment queue...`); + this.logger.log(LogLevels.debug, `Clearing attachment queue...`); await this.powersync.writeTransaction(async (tx) => { await tx.execute(`DELETE FROM ${this.table}`); }); diff --git a/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts b/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts index 36f5ae35f..6346d236d 100644 --- a/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts +++ b/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts @@ -1,4 +1,4 @@ -import { createBaseLogger, LogLevel, type ILogHandler } from '@powersync/common'; +import { createPowerSyncLogger, LogLevels, type PowerSyncLogger } from '@powersync/common'; import { createStorage } from 'unstorage'; import localStorageDriver from 'unstorage/drivers/session-storage'; import mitt from 'mitt'; @@ -15,7 +15,7 @@ const logsStorage = createStorage({ * This composable creates a logger instance that is automatically configured for diagnostics * recording. The logger stores logs in session storage and emits events for real-time log monitoring. * - * @param customHandler - Optional custom log handler to process log messages + * @param additional - Optional logger that messages will also be forwarded to. * * @returns An object containing: * - `logger` - The configured ILogHandler instance @@ -30,38 +30,31 @@ const logsStorage = createStorage({ * // Use it in your PowerSync setup if needed * ``` */ -export const useDiagnosticsLogger = (customHandler?: ILogHandler) => { - const logger = createBaseLogger(); - - // Console output: use js-logger's default handler (optional formatter for [PowerSync] prefix) - const consoleHandler = logger.createDefaultHandler({ - formatter: (messages, context) => { - messages.unshift(`[PowerSync]${context.name ? ` [${context.name}]` : ''}`); +export const useDiagnosticsLogger = (additional?: PowerSyncLogger) => { + const consoleLogger = createPowerSyncLogger({ minLevel: LogLevels.debug }); + + const logger: PowerSyncLogger = { + async log(level, ...messages) { + consoleLogger.log(level, ...messages); + + // Storage + emitter + const messageArray = Array.from(messages); + const mainMessage = String(messageArray[0] ?? 'Empty log message'); + // Store extra args as-is so objects are shown as JSON in LogsTab + const extra = + messageArray.length > 1 ? (messageArray.length === 2 ? messageArray[1] : messageArray.slice(1)) : undefined; + const logObject = { + date: new Date(), + args: [mainMessage, extra] + }; + const key = `log:${logObject.date.toISOString()}`; + await logsStorage.set(key, logObject); + emitter.emit('log', { key, value: logObject }); + + // User callback + additional?.log(level, ...messages); } - }); - - logger.setLevel(LogLevel.DEBUG); - logger.setHandler(async (messages, context) => { - consoleHandler(messages, context); - - // Storage + emitter - const messageArray = Array.from(messages); - const mainMessage = String(messageArray[0] ?? 'Empty log message'); - // Store extra args as-is so objects are shown as JSON in LogsTab - const extra = - messageArray.length > 1 ? (messageArray.length === 2 ? messageArray[1] : messageArray.slice(1)) : undefined; - const logObject = { - date: new Date(), - type: context.level.name.toLowerCase(), - args: [mainMessage, extra, context] - }; - const key = `log:${logObject.date.toISOString()}`; - await logsStorage.set(key, logObject); - emitter.emit('log', { key, value: logObject }); - - // User callback - await customHandler?.(messages, context); - }); + }; return { logger, logsStorage, emitter }; }; From 4b4595b96b4edaca48d76dbfca5f57a876d3691b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 7 May 2026 13:03:38 +0200 Subject: [PATCH 03/21] Migrate web package --- packages/common/src/utils/Logger.ts | 8 +- packages/web/src/db/PowerSyncDatabase.ts | 34 ++++-- .../db/adapters/wa-sqlite/DatabaseServer.ts | 10 +- .../adapters/wa-sqlite/WASQLiteOpenFactory.ts | 27 +++-- .../WASQLitePowerSyncDatabaseOpenFactory.ts | 14 ++- packages/web/src/db/adapters/web-sql-flags.ts | 4 +- .../SharedWebStreamingSyncImplementation.ts | 31 +---- packages/web/src/db/sync/WebRemote.ts | 8 +- .../db/sync/WebStreamingSyncImplementation.ts | 5 +- .../web/src/worker/db/MultiDatabaseServer.ts | 32 +++-- .../web/src/worker/db/WASQLiteDB.worker.ts | 6 +- .../sync/AbstractSharedSyncClientProvider.ts | 9 +- .../web/src/worker/sync/BroadcastLogger.ts | 110 +++--------------- .../worker/sync/SharedSyncImplementation.ts | 45 ++++--- .../sync/SharedSyncImplementation.worker.ts | 3 - packages/web/src/worker/sync/WorkerClient.ts | 4 +- packages/web/tests/encryption.test.ts | 3 + packages/web/tests/main.test.ts | 3 +- packages/web/tests/mocks/MockWebRemote.ts | 5 +- packages/web/tests/multiple_instances.test.ts | 23 ++-- packages/web/tests/open.test.ts | 10 +- .../tests/src/db/PowersyncDatabase.test.ts | 17 +-- .../tests/src/db/write_ahead_log_opfs.test.ts | 5 +- packages/web/tests/stream.test.ts | 11 +- packages/web/tests/triggers.test.ts | 6 +- packages/web/tests/uploads.test.ts | 29 ++--- .../web/tests/utils/MockStreamOpenFactory.ts | 6 +- .../tests/utils/generateConnectedDatabase.ts | 3 +- packages/web/tests/utils/iframeInitializer.ts | 16 ++- .../web/tests/utils/mockSyncServiceTest.ts | 11 +- packages/web/tests/utils/testDb.ts | 7 +- .../utils/triggers/IFrameTriggerConfig.ts | 2 + 32 files changed, 240 insertions(+), 267 deletions(-) diff --git a/packages/common/src/utils/Logger.ts b/packages/common/src/utils/Logger.ts index 08d204559..711d2c1e4 100644 --- a/packages/common/src/utils/Logger.ts +++ b/packages/common/src/utils/Logger.ts @@ -45,12 +45,14 @@ export interface CreateLoggerOptions { * * @param options Options to configure a minimum severity of the logger or a prefix to make messages more recognizable. */ -export function createPowerSyncLogger(options?: Partial): PowerSyncLogger { +export function createPowerSyncLogger(options?: Partial): PowerSyncLogger & CreateLoggerOptions { const { prefix = 'PowerSync', minLevel = LogLevels.info } = options ?? {}; return { + prefix, + minLevel, log(level, ...message) { - if (level < minLevel) return; + if (level < this.minLevel) return; let emitter = console.log; if (level >= LogLevels.error) { @@ -59,7 +61,7 @@ export function createPowerSyncLogger(options?: Partial): P emitter = console.warn; } - emitter(prefix, ...message); + emitter(this.prefix, ...message); } }; } diff --git a/packages/web/src/db/PowerSyncDatabase.ts b/packages/web/src/db/PowerSyncDatabase.ts index d0a3a5724..7311a4324 100644 --- a/packages/web/src/db/PowerSyncDatabase.ts +++ b/packages/web/src/db/PowerSyncDatabase.ts @@ -14,7 +14,8 @@ import { type BucketStorageAdapter, type PowerSyncBackendConnector, type PowerSyncCloseOptions, - type RequiredAdditionalConnectionOptions + type RequiredAdditionalConnectionOptions, + LogLevels } from '@powersync/common'; import { getNavigatorLocks } from '../shared/navigator.js'; import { NAVIGATOR_TRIGGER_CLAIM_MANAGER } from './NavigatorTriggerClaimManager.js'; @@ -44,6 +45,13 @@ export interface WebPowerSyncFlags extends WebSQLFlags { * instances before the window unloads */ externallyUnload?: boolean; + + /** + * The log level for database workers. + * + * Defaults to {@link LogLevels.info}. + */ + databaseWorkerLogLevel?: number; } type WithWebFlags = Base & { flags?: WebPowerSyncFlags }; @@ -56,6 +64,13 @@ export interface WebSyncOptions { * or a factory method that returns a worker. */ worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => SharedWorker); + + /** + * The log level for logs from the sync worker. + * + * Defaults to {@link LogLevels.info}. + */ + logLevel?: number; } type WithWebSyncOptions = Base & { @@ -86,7 +101,8 @@ export type WebPowerSyncDatabaseOptions = WithWebSyncOptions = { ...DEFAULT_WEB_SQL_FLAGS, - externallyUnload: false + externallyUnload: false, + databaseWorkerLogLevel: LogLevels.info }; export const resolveWebPowerSyncFlags = (flags?: WebPowerSyncFlags): Required => { @@ -171,10 +187,13 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase { } protected openDBAdapter(options: WebPowerSyncDatabaseOptionsWithSettings): DBAdapter { + const resolvedFlags = resolveWebPowerSyncFlags(options.flags); const defaultFactory = new WASQLiteOpenFactory({ ...options.database, - flags: resolveWebPowerSyncFlags(options.flags), - encryptionKey: options.encryptionKey + flags: resolvedFlags, + encryptionKey: options.encryptionKey, + logger: this.logger, + logLevel: resolvedFlags.databaseWorkerLogLevel }); return defaultFactory.openDB(); } @@ -206,7 +225,7 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase { } protected generateBucketStorageAdapter(): BucketStorageAdapter { - return new SqliteBucketStorage(this.database); + return new SqliteBucketStorage(this.database, this.logger); } protected async runExclusive(cb: () => Promise) { @@ -245,11 +264,12 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase { 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); + logger ? logger.log(LogLevels.warn, warning) : console.warn(warning); } return new SharedWebStreamingSyncImplementation({ ...syncOptions, - db: this.database as WebDBAdapter // This should always be the case + db: this.database as WebDBAdapter, // This should always be the case + logLevel: this.options.sync?.logLevel ?? LogLevels.info }); default: return new WebStreamingSyncImplementation(syncOptions); diff --git a/packages/web/src/db/adapters/wa-sqlite/DatabaseServer.ts b/packages/web/src/db/adapters/wa-sqlite/DatabaseServer.ts index f4cf863bc..1d696c2bc 100644 --- a/packages/web/src/db/adapters/wa-sqlite/DatabaseServer.ts +++ b/packages/web/src/db/adapters/wa-sqlite/DatabaseServer.ts @@ -1,11 +1,11 @@ -import { ILogger } from '@powersync/common'; +import { LogLevels, PowerSyncLogger } from '@powersync/common'; import { ConcurrentSqliteConnection, ConnectionLeaseToken } from './ConcurrentConnection.js'; import { RawQueryResult } from './RawSqliteConnection.js'; export interface DatabaseServerOptions { inner: ConcurrentSqliteConnection; onClose: () => void; - logger: ILogger; + logger: PowerSyncLogger; } /** @@ -85,7 +85,7 @@ export class DatabaseServer { // If the client holds a connection lease it hasn't returned, return that now. for (const { lease } of connectionLeases.values()) { - this.#logger.debug(`Closing connection lease that hasn't been returned.`); + this.#logger.log(LogLevels.debug, `Closing connection lease that hasn't been returned.`); await lease.returnLease(); } @@ -94,7 +94,7 @@ export class DatabaseServer { if (this.#activeClients.size == 0) { await this.forceClose(); } else { - this.#logger.debug('Keeping underlying connection active since its used by other clients.'); + this.#logger.log(LogLevels.debug, 'Keeping underlying connection active since its used by other clients.'); } } }; @@ -169,7 +169,7 @@ export class DatabaseServer { } async forceClose() { - this.#logger.debug(`Closing connection to ${this.#inner.options}.`); + this.#logger.log(LogLevels.debug, `Closing connection to ${this.#inner.options}.`); const connection = this.#inner; this.#options.onClose(); this.#updateBroadcastChannel.close(); diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts index 864592386..eb0647044 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts @@ -1,4 +1,4 @@ -import { createLogger, DBAdapter, ILogger, SQLOpenFactory, type ILogLevel } from '@powersync/common'; +import { createPowerSyncLogger, DBAdapter, LogLevels, PowerSyncLogger, SQLOpenFactory } from '@powersync/common'; import * as Comlink from 'comlink'; import { openWorkerDatabasePort, resolveWorkerDatabasePortFactory } from '../../../worker/db/open-worker-database.js'; import { @@ -29,6 +29,13 @@ export interface WASQLiteOpenFactoryOptions extends WebSQLOpenFactoryOptions { * Defaults to 1. */ additionalReaders?: number; + + /** + * The {@link LogLevels} value to use for logs in the worker. + */ + logLevel: number; + + logger: PowerSyncLogger; } export interface ResolvedWASQLiteOpenFactoryOptions extends ResolvedWebSQLOpenOptions { @@ -41,7 +48,7 @@ export interface ResolvedWASQLiteOpenFactoryOptions extends ResolvedWebSQLOpenOp } export interface WorkerDBOpenerOptions extends ResolvedWASQLiteOpenFactoryOptions { - logLevel: ILogLevel; + logLevel: number; /** * A lock that is currently held by the client. When the lock is returned, we know the client is gone and that we need * to clean up resources. @@ -54,12 +61,12 @@ export interface WorkerDBOpenerOptions extends ResolvedWASQLiteOpenFactoryOption */ export class WASQLiteOpenFactory implements SQLOpenFactory { private resolvedFlags: ResolvedWebSQLFlags; - private logger: ILogger; + private logger: PowerSyncLogger; constructor(private options: WASQLiteOpenFactoryOptions) { assertValidWASQLiteOpenFactoryOptions(options); this.resolvedFlags = resolveWebSQLFlags(options.flags); - this.logger = options.logger ?? createLogger(`WASQLiteOpenFactory - ${this.options.dbFilename}`); + this.logger = options.logger; } get waOptions(): WASQLiteOpenFactoryOptions { @@ -77,7 +84,8 @@ export class WASQLiteOpenFactory implements SQLOpenFactory { } = this; if (ssrMode) { if (!disableSSRWarning) { - this.logger.warn( + this.logger.log( + LogLevels.warn, ` Running PowerSync in SSR mode. Only empty query results will be returned. @@ -89,7 +97,8 @@ export class WASQLiteOpenFactory implements SQLOpenFactory { } if (!enableMultiTabs) { - this.logger.warn( + this.logger.log( + LogLevels.warn, 'Multiple tab support is not enabled. Using this site across multiple tabs may not function correctly.' ); } @@ -107,7 +116,7 @@ export class WASQLiteOpenFactory implements SQLOpenFactory { } = this.waOptions; if (!enableMultiTabs) { - this.logger.warn('Multiple tabs are not enabled in this browser'); + this.logger.log(LogLevels.warn, 'Multiple tabs are not enabled in this browser'); } const resolveOptions = (isReadOnly: boolean): ResolvedWASQLiteOpenFactoryOptions => ({ @@ -149,7 +158,7 @@ export class WASQLiteOpenFactory implements SQLOpenFactory { const closeSignal = new AbortController(); const connection = await source.connect({ ...resolvedOptions, - logLevel: this.logger.getLevel(), + logLevel: this.options.logLevel, lockName: await generateTabCloseSignal(closeSignal.signal) }); const clientOptions = { @@ -190,7 +199,7 @@ export class WASQLiteOpenFactory implements SQLOpenFactory { requiresPersistentTriggers = true; const resolvedOptions = resolveOptions(false); - const connection = await localServer.openConnectionLocally(resolvedOptions); + const connection = await localServer.openConnectionLocally(this.logger, resolvedOptions); client = new DatabaseClient( { connection, source: null, remoteCanCloseUnexpectedly: false }, { diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.ts index 1ae08294d..22dfa10da 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.ts @@ -1,4 +1,10 @@ -import { AbstractPowerSyncDatabase, DBAdapter, PowerSyncDatabaseOptions } from '@powersync/common'; +import { + AbstractPowerSyncDatabase, + createPowerSyncLogger, + DBAdapter, + LogLevels, + PowerSyncDatabaseOptions +} from '@powersync/common'; import { PowerSyncDatabase } from '../../../db/PowerSyncDatabase.js'; import { AbstractWebPowerSyncDatabaseOpenFactory } from '../AbstractWebPowerSyncDatabaseOpenFactory.js'; import { WASQLiteOpenFactory } from './WASQLiteOpenFactory.js'; @@ -14,7 +20,11 @@ import { WASQLiteOpenFactory } from './WASQLiteOpenFactory.js'; */ export class WASQLitePowerSyncDatabaseOpenFactory extends AbstractWebPowerSyncDatabaseOpenFactory { protected openDB(): DBAdapter { - const factory = new WASQLiteOpenFactory(this.options); + const factory = new WASQLiteOpenFactory({ + logger: createPowerSyncLogger(), + logLevel: LogLevels.info, + ...this.options + }); return factory.openDB(); } diff --git a/packages/web/src/db/adapters/web-sql-flags.ts b/packages/web/src/db/adapters/web-sql-flags.ts index 92739451e..84fe3046b 100644 --- a/packages/web/src/db/adapters/web-sql-flags.ts +++ b/packages/web/src/db/adapters/web-sql-flags.ts @@ -1,4 +1,4 @@ -import { type ILogger, SQLOpenOptions } from '@powersync/common'; +import { type PowerSyncLogger, SQLOpenOptions } from '@powersync/common'; /** * Common settings used when creating SQL connections on web. @@ -82,7 +82,7 @@ export interface WebSQLOpenFactoryOptions extends SQLOpenOptions { */ workerPort?: MessagePort; - logger?: ILogger; + logger?: PowerSyncLogger; /** * Where to store SQLite temporary files. Defaults to 'MEMORY'. diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index 4890e5783..e3bee846d 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -5,7 +5,6 @@ import { SyncStatusOptions } from '@powersync/common'; import * as Comlink from 'comlink'; -import { getNavigatorLocks } from '../../shared/navigator.js'; import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider.js'; import { ManualSharedSyncPayload, SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation.js'; import { WorkerClient } from '../../worker/sync/WorkerClient.js'; @@ -68,33 +67,13 @@ class SharedSyncClientProvider extends AbstractSharedSyncClientProvider { return this.options.logger; } - trace(...x: any[]): void { - this.logger?.trace(...x); - } - debug(...x: any[]): void { - this.logger?.debug(...x); - } - info(...x: any[]): void { - this.logger?.info(...x); - } - log(...x: any[]): void { - this.logger?.log(...x); - } - warn(...x: any[]): void { - this.logger?.warn(...x); - } - error(...x: any[]): void { - this.logger?.error(...x); - } - time(label: string): void { - this.logger?.time(label); - } - timeEnd(label: string): void { - this.logger?.timeEnd(label); + log(level: number, ...message: any[]): void { + this.logger?.log(level, message); } } export interface SharedWebStreamingSyncImplementationOptions extends WebStreamingSyncImplementationOptions { + logLevel: number; db: WebDBAdapter; } @@ -109,10 +88,12 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem protected isInitialized: Promise; protected dbAdapter: WebDBAdapter; private abortOnClose = new AbortController(); + private logLevel: number; constructor(options: SharedWebStreamingSyncImplementationOptions) { super(options); this.dbAdapter = options.db; + this.logLevel = options.logLevel; /** * Configure or connect to the shared sync worker. * This worker will manage all syncing operations remotely. @@ -165,7 +146,7 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem */ Comlink.expose(this.clientProvider, this.messagePort); - this.syncManager.setLogLevel(this.logger.getLevel()); + this.syncManager.setLogLevel(this.logLevel); this.triggerCrudUpload = this.syncManager.triggerCrudUpload; diff --git a/packages/web/src/db/sync/WebRemote.ts b/packages/web/src/db/sync/WebRemote.ts index 8368830dd..077a2daed 100644 --- a/packages/web/src/db/sync/WebRemote.ts +++ b/packages/web/src/db/sync/WebRemote.ts @@ -1,10 +1,10 @@ import { AbstractRemote, AbstractRemoteOptions, - DEFAULT_REMOTE_LOGGER, FetchImplementation, FetchImplementationProvider, - ILogger, + LogLevels, + PowerSyncLogger, RemoteConnector } from '@powersync/common'; @@ -22,7 +22,7 @@ class WebFetchProvider extends FetchImplementationProvider { export class WebRemote extends AbstractRemote { constructor( protected connector: RemoteConnector, - protected logger: ILogger = DEFAULT_REMOTE_LOGGER, + protected logger: PowerSyncLogger, options?: Partial ) { super(connector, logger, { @@ -36,7 +36,7 @@ export class WebRemote extends AbstractRemote { try { ua.push(...getUserAgentInfo()); } catch (e) { - this.logger.warn('Failed to get user agent info', e); + this.logger.log(LogLevels.warn, 'Failed to get user agent info', e); } return ua.join(' '); } diff --git a/packages/web/src/db/sync/WebStreamingSyncImplementation.ts b/packages/web/src/db/sync/WebStreamingSyncImplementation.ts index 5eb7b5aad..260b8e7d7 100644 --- a/packages/web/src/db/sync/WebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/WebStreamingSyncImplementation.ts @@ -2,7 +2,8 @@ import { AbstractStreamingSyncImplementation, AbstractStreamingSyncImplementationOptions, LockOptions, - LockType + LockType, + LogLevels } from '@powersync/common'; import { getNavigatorLocks } from '../../shared/navigator.js'; import { ResolvedWebSQLOpenOptions, WebSQLFlags } from '../adapters/web-sql-flags.js'; @@ -27,7 +28,7 @@ export class WebStreamingSyncImplementation extends AbstractStreamingSyncImpleme async obtainLock(lockOptions: LockOptions): Promise { const identifier = `streaming-sync-${lockOptions.type}-${this.webOptions.identifier}`; if (lockOptions.type == LockType.SYNC) { - this.logger.debug('requesting lock for ', identifier); + this.logger.log(LogLevels.debug, 'requesting lock for ', identifier); } return getNavigatorLocks().request(identifier, { signal: lockOptions.signal }, lockOptions.callback); } diff --git a/packages/web/src/worker/db/MultiDatabaseServer.ts b/packages/web/src/worker/db/MultiDatabaseServer.ts index 6345a6625..dafa38e10 100644 --- a/packages/web/src/worker/db/MultiDatabaseServer.ts +++ b/packages/web/src/worker/db/MultiDatabaseServer.ts @@ -1,4 +1,4 @@ -import { ILogger } from '@powersync/common'; +import { LogLevels, PowerSyncLogger } from '@powersync/common'; import * as Comlink from 'comlink'; import { ClientConnectionView, DatabaseServer } from '../../db/adapters/wa-sqlite/DatabaseServer.js'; import { @@ -17,11 +17,16 @@ const OPEN_DB_LOCK = 'open-wasqlite-db'; export class MultiDatabaseServer { private activeDatabases = new Map(); - constructor(readonly logger: ILogger) {} + constructor(readonly logger: PowerSyncLogger) {} async handleConnection(options: WorkerDBOpenerOptions): Promise { - this.logger.setLevel(options.logLevel); - return Comlink.proxy(await this.openConnectionLocally(options, options.lockName)); + const logger: PowerSyncLogger = { + log: (level, ...message) => { + if (level >= options.logLevel) logger.log(level, message); + } + }; + + return Comlink.proxy(await this.openConnectionLocally(logger, options, options.lockName)); } async connectToExisting(name: string, lockName: string): Promise { @@ -35,7 +40,7 @@ export class MultiDatabaseServer { }); } - async openConnectionLocally(options: ResolvedWASQLiteOpenFactoryOptions, lockName?: string) { + async openConnectionLocally(logger: PowerSyncLogger, options: ResolvedWASQLiteOpenFactoryOptions, lockName?: string) { // Especially on Firefox, we're sometimes seeing "NoModificationAllowedError"s when opening OPFS databases we can // work around by retrying. const maxAttempts = 3; @@ -43,19 +48,26 @@ export class MultiDatabaseServer { for (let count = 0; count < maxAttempts - 1; count++) { try { - server = await this.databaseOpenAttempt(options); + server = await this.databaseOpenAttempt(logger, options); } catch (ex) { - this.logger.warn(`Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, ex); + this.logger.log( + LogLevels.warn, + `Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, + ex + ); await new Promise((resolve) => setTimeout(resolve, 1000)); } } // Final attempt if we haven't been able to open the server - rethrow errors if we still can't open. - server ??= await this.databaseOpenAttempt(options); + server ??= await this.databaseOpenAttempt(logger, options); return server.connect(lockName); } - private async databaseOpenAttempt(options: ResolvedWASQLiteOpenFactoryOptions): Promise { + private async databaseOpenAttempt( + logger: PowerSyncLogger, + options: ResolvedWASQLiteOpenFactoryOptions + ): Promise { return getNavigatorLocks().request(OPEN_DB_LOCK, async () => { const { dbFilename } = options; @@ -84,7 +96,7 @@ export class MultiDatabaseServer { const onClose = () => this.activeDatabases.delete(dbFilename); server = new DatabaseServer({ inner: withSafeConcurrency, - logger: this.logger, + logger, onClose }); this.activeDatabases.set(dbFilename, server); diff --git a/packages/web/src/worker/db/WASQLiteDB.worker.ts b/packages/web/src/worker/db/WASQLiteDB.worker.ts index 9909bff0a..611544b66 100644 --- a/packages/web/src/worker/db/WASQLiteDB.worker.ts +++ b/packages/web/src/worker/db/WASQLiteDB.worker.ts @@ -3,14 +3,12 @@ */ import '@journeyapps/wa-sqlite'; -import { createBaseLogger, createLogger } from '@powersync/common'; +import { createPowerSyncLogger, LogLevels } from '@powersync/common'; import * as Comlink from 'comlink'; import { isSharedWorker, MultiDatabaseServer } from './MultiDatabaseServer.js'; import { OpenWorkerConnection } from '../../db/adapters/wa-sqlite/DatabaseClient.js'; -const baseLogger = createBaseLogger(); -baseLogger.useDefaults(); -const logger = createLogger('db-worker'); +const logger = createPowerSyncLogger({ prefix: 'db-worker', minLevel: LogLevels.trace }); const server = new MultiDatabaseServer(logger); const exposedFunctions: OpenWorkerConnection = { diff --git a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts b/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts index c33c5ffc8..3f740efba 100644 --- a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts +++ b/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts @@ -10,12 +10,5 @@ export abstract class AbstractSharedSyncClientProvider { abstract statusChanged(status: SyncStatusOptions): void; abstract getDBWorkerPort(): Promise; - abstract trace(...x: any[]): void; - abstract debug(...x: any[]): void; - abstract info(...x: any[]): void; - abstract log(...x: any[]): void; - abstract warn(...x: any[]): void; - abstract error(...x: any[]): void; - abstract time(label: string): void; - abstract timeEnd(label: string): void; + abstract log(level: number, ...message: any[]): void; } diff --git a/packages/web/src/worker/sync/BroadcastLogger.ts b/packages/web/src/worker/sync/BroadcastLogger.ts index ab624bc4d..d5701223a 100644 --- a/packages/web/src/worker/sync/BroadcastLogger.ts +++ b/packages/web/src/worker/sync/BroadcastLogger.ts @@ -1,113 +1,39 @@ -import { type ILogger, type ILogLevel, LogLevel } from '@powersync/common'; +import { PowerSyncLogger, LogLevels, CreateLoggerOptions, createPowerSyncLogger } from '@powersync/common'; import { type WrappedSyncPort } from './SharedSyncImplementation.js'; /** * Broadcasts logs to all clients */ -export class BroadcastLogger implements ILogger { - TRACE: ILogLevel; - DEBUG: ILogLevel; - INFO: ILogLevel; - TIME: ILogLevel; - WARN: ILogLevel; - ERROR: ILogLevel; - OFF: ILogLevel; +export class BroadcastLogger implements PowerSyncLogger { + private readonly inner: PowerSyncLogger & CreateLoggerOptions; + private currentLevel: number = LogLevels.info; - private currentLevel: ILogLevel = LogLevel.INFO; + sendBroadcasts = true; - constructor(protected clients: WrappedSyncPort[]) { - this.TRACE = LogLevel.TRACE; - this.DEBUG = LogLevel.DEBUG; - this.INFO = LogLevel.INFO; - this.TIME = LogLevel.TIME; - this.WARN = LogLevel.WARN; - this.ERROR = LogLevel.ERROR; - this.OFF = LogLevel.OFF; + constructor( + prefix: string, + private clients: WrappedSyncPort[] + ) { + this.inner = createPowerSyncLogger({ prefix: prefix }); } - trace(...x: any[]): void { - if (!this.enabledFor(this.TRACE)) return; + log(level: number, ...message: any[]) { + this.inner.log(level, ...message); - console.trace(...x); - const sanitized = this.sanitizeArgs(x); - this.iterateClients((client) => client.clientProvider.trace(...sanitized)); - } - - debug(...x: any[]): void { - if (!this.enabledFor(this.DEBUG)) return; - - console.debug(...x); - const sanitized = this.sanitizeArgs(x); - this.iterateClients((client) => client.clientProvider.debug(...sanitized)); - } - - info(...x: any[]): void { - if (!this.enabledFor(this.INFO)) return; - - console.info(...x); - const sanitized = this.sanitizeArgs(x); - this.iterateClients((client) => client.clientProvider.info(...sanitized)); - } - - log(...x: any[]): void { - if (!this.enabledFor(this.INFO)) return; - - console.log(...x); - const sanitized = this.sanitizeArgs(x); - this.iterateClients((client) => client.clientProvider.log(...sanitized)); - } - - warn(...x: any[]): void { - if (!this.enabledFor(this.WARN)) return; - - console.warn(...x); - const sanitized = this.sanitizeArgs(x); - this.iterateClients((client) => client.clientProvider.warn(...sanitized)); - } - - error(...x: any[]): void { - if (!this.enabledFor(this.ERROR)) return; - - console.error(...x); - const sanitized = this.sanitizeArgs(x); - this.iterateClients((client) => client.clientProvider.error(...sanitized)); - } - - time(label: string): void { - if (!this.enabledFor(this.TIME)) return; - - console.time(label); - this.iterateClients((client) => client.clientProvider.time(label)); - } - - timeEnd(label: string): void { - if (!this.enabledFor(this.TIME)) return; - - console.timeEnd(label); - this.iterateClients((client) => client.clientProvider.timeEnd(label)); + if (this.sendBroadcasts && level >= this.currentLevel) { + const sanitized = this.sanitizeArgs(message); + this.iterateClients((client) => client.clientProvider.log(level, ...sanitized)); + } } /** * Set the global log level. */ - setLevel(level: ILogLevel): void { + setLevel(level: number): void { + this.inner.minLevel = level; this.currentLevel = level; } - /** - * Get the current log level. - */ - getLevel(): ILogLevel { - return this.currentLevel; - } - - /** - * Returns true if the given level is enabled. - */ - enabledFor(level: ILogLevel): boolean { - return level.value >= this.currentLevel.value; - } - /** * Iterates all clients, catches individual client exceptions * and proceeds to execute for all clients. diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index e68862472..822c9b38b 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -12,14 +12,15 @@ import { SqliteBucketStorage, SubscribedStream, SyncStatus, - createLogger, + createPowerSyncLogger, Mutex, - type ILogLevel, - type ILogger, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, - type SyncStatusOptions + type SyncStatusOptions, + CreateLoggerOptions, + PowerSyncLogger, + LogLevels } from '@powersync/common'; import * as Comlink from 'comlink'; import { WebRemote } from '../../db/sync/WebRemote.js'; @@ -60,7 +61,10 @@ export type ManualSharedSyncPayload = { * @internal */ export type SharedSyncInitOptions = { - streamOptions: Omit; + streamOptions: Omit< + WebStreamingSyncImplementationOptions, + 'adapter' | 'uploadCrud' | 'remote' | 'subscriptions' | 'logger' + >; dbParams: ResolvedWebSQLOpenOptions; }; @@ -112,21 +116,19 @@ export class SharedSyncImplementation extends BaseObserver { @@ -209,7 +211,7 @@ export class SharedSyncImplementation extends BaseObserver(); for (const port of this.ports) { for (const stream of port.currentSubscriptions) { @@ -218,7 +220,7 @@ export class SharedSyncImplementation extends BaseObserver {}); self.onerror = (event) => { // Share any uncaught events on the broadcast logger - this.logger.error('Uncaught exception in PowerSync shared sync worker', event); + this.logger.log(LogLevels.error, 'Uncaught exception in PowerSync shared sync worker', event); }; this.iterateListeners((l) => l.initialized?.()); @@ -321,7 +320,7 @@ export class SharedSyncImplementation extends BaseObserver { const index = this.ports.findIndex((p) => p == port); if (index < 0) { - this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`); + this.logger.log(LogLevels.warn, `Could not remove port ${port} since it is not present in active ports.`); return () => {}; } @@ -397,10 +396,10 @@ export class SharedSyncImplementation extends BaseObserver { @@ -417,7 +416,7 @@ export class SharedSyncImplementation extends BaseObserver { - this.logger.info('Aborting open connection because associated tab closed.'); + this.logger.log(LogLevels.info, 'Aborting open connection because associated tab closed.'); handleClosed(db); /** * Don't await this close operation. It might never resolve if the tab is closed. @@ -550,7 +549,7 @@ export class SharedSyncImplementation extends BaseObserver this.logger.warn('error closing database connection', ex)); + db.close().catch((ex) => this.logger.log(LogLevels.warn, 'error closing database connection', ex)); }); return db; } diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts b/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts index 7395d08dd..37e05fccc 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts @@ -1,10 +1,7 @@ -import { createBaseLogger } from '@powersync/common'; import { SharedSyncImplementation } from './SharedSyncImplementation.js'; import { WorkerClient } from './WorkerClient.js'; const _self: SharedWorkerGlobalScope = self as any; -const logger = createBaseLogger(); -logger.useDefaults(); const sharedSyncImplementation = new SharedSyncImplementation(); diff --git a/packages/web/src/worker/sync/WorkerClient.ts b/packages/web/src/worker/sync/WorkerClient.ts index 023dc505e..f028b0a92 100644 --- a/packages/web/src/worker/sync/WorkerClient.ts +++ b/packages/web/src/worker/sync/WorkerClient.ts @@ -1,4 +1,4 @@ -import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream } from '@powersync/common'; +import { PowerSyncConnectionOptions, SubscribedStream } from '@powersync/common'; import * as Comlink from 'comlink'; import { getNavigatorLocks } from '../../shared/navigator.js'; import { @@ -65,7 +65,7 @@ export class WorkerClient { }); } - setLogLevel(level: ILogLevel) { + setLogLevel(level: number) { this.sync.setLogLevel(level); } diff --git a/packages/web/tests/encryption.test.ts b/packages/web/tests/encryption.test.ts index 5c474c182..62f339bda 100644 --- a/packages/web/tests/encryption.test.ts +++ b/packages/web/tests/encryption.test.ts @@ -8,6 +8,7 @@ import { import { v4 as uuid } from 'uuid'; import { describe, expect, it } from 'vitest'; import { TEST_SCHEMA } from './utils/test-schema.js'; +import { defaultLoggerConfig } from './utils/testDb.js'; describe('Encryption Tests', { sequential: true }, () => { it('IDBBatchAtomicVFS encryption', async () => { @@ -22,6 +23,7 @@ describe('Encryption Tests', { sequential: true }, () => { await testEncryption({ schema: TEST_SCHEMA, database: new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename: 'opfs-file.db', vfs: WASQLiteVFS.OPFSCoopSyncVFS, encryptionKey: 'opfs-key' @@ -33,6 +35,7 @@ describe('Encryption Tests', { sequential: true }, () => { await testEncryption({ schema: TEST_SCHEMA, database: new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename: 'ahp-file.db', vfs: WASQLiteVFS.AccessHandlePoolVFS, encryptionKey: 'ahp-key' diff --git a/packages/web/tests/main.test.ts b/packages/web/tests/main.test.ts index 15025b568..634ec2783 100644 --- a/packages/web/tests/main.test.ts +++ b/packages/web/tests/main.test.ts @@ -2,7 +2,7 @@ import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/ import { v4 as uuid } from 'uuid'; import { describe, expect, it } from 'vitest'; import { TEST_SCHEMA, TestDatabase } from './utils/test-schema.js'; -import { generateTestDb } from './utils/testDb.js'; +import { defaultLoggerConfig, generateTestDb } from './utils/testDb.js'; // TODO import tests from a common package describe( @@ -33,6 +33,7 @@ describe( describeBasicTests(() => generateTestDb({ database: new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename: 'basic-opfs.sqlite', vfs: WASQLiteVFS.OPFSCoopSyncVFS }), diff --git a/packages/web/tests/mocks/MockWebRemote.ts b/packages/web/tests/mocks/MockWebRemote.ts index f70661f93..25112b060 100644 --- a/packages/web/tests/mocks/MockWebRemote.ts +++ b/packages/web/tests/mocks/MockWebRemote.ts @@ -1,10 +1,9 @@ import { AbstractRemote, AbstractRemoteOptions, - DEFAULT_REMOTE_LOGGER, FetchImplementation, FetchImplementationProvider, - ILogger, + PowerSyncLogger, RemoteConnector, SimpleAsyncIterator, SocketSyncStreamOptions @@ -87,7 +86,7 @@ export class WebRemote extends AbstractRemote { constructor( protected connector: RemoteConnector, - protected logger: ILogger = DEFAULT_REMOTE_LOGGER, + protected logger: PowerSyncLogger, options?: Partial ) { // Use mock service fetch provider if we're in a shared worker context diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index 516766edc..ea5abcdfa 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -1,9 +1,9 @@ import { AbstractPowerSyncDatabase, - createBaseLogger, - createLogger, + createPowerSyncLogger, DBAdapterDefaultMixin, - LogLevel + LogLevels, + PowerSyncLogger } from '@powersync/common'; import * as Comlink from 'comlink'; import { beforeAll, describe, expect, it, onTestFinished, vi } from 'vitest'; @@ -26,8 +26,6 @@ describe('Multiple Instances', { sequential: true }, () => { schema: TEST_SCHEMA }); - beforeAll(() => createBaseLogger().useDefaults()); - function createAsset(powersync: AbstractPowerSyncDatabase) { return powersync.execute('INSERT INTO assets(id, description) VALUES(uuid(), ?)', ['test']); } @@ -48,10 +46,8 @@ describe('Multiple Instances', { sequential: true }, () => { 'should broadcast logs from shared sync worker', { timeout: 10_000 }, async ({ context: { openDatabase, mockService } }) => { - const logger = createLogger('test-logger'); - logger.setLevel(LogLevel.TRACE); - const spiedErrorLogger = vi.spyOn(logger, 'error'); - const spiedTraceLogger = vi.spyOn(logger, 'trace'); + const logFn = vi.fn(); + const logger: PowerSyncLogger = { log: logFn }; // Open an additional database which we can spy on the logs. const powersync = openDatabase({ @@ -84,7 +80,7 @@ describe('Multiple Instances', { sequential: true }, () => { await vi.waitFor( () => expect( - spiedTraceLogger.mock.calls + logFn.mock.calls .flat(1) .find((argument) => typeof argument == 'string' && argument.includes('powersync_control')) ).exist, @@ -92,7 +88,12 @@ describe('Multiple Instances', { sequential: true }, () => { ); // The connection should fail with an error - await vi.waitFor(() => expect(spiedErrorLogger.mock.calls.length).gt(0), { timeout: 2000 }); + await vi.waitFor( + () => + expect(logFn.mock.calls.flat(1).find((argument) => typeof argument == 'string' && argument.includes('error'))) + .exist, + { timeout: 2000 } + ); } ); diff --git a/packages/web/tests/open.test.ts b/packages/web/tests/open.test.ts index f636a7b4d..646086250 100644 --- a/packages/web/tests/open.test.ts +++ b/packages/web/tests/open.test.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, createLogger, DBAdapterDefaultMixin, Schema } from '@powersync/common'; +import { AbstractPowerSyncDatabase, createPowerSyncLogger, DBAdapterDefaultMixin, Schema } from '@powersync/common'; import { PowerSyncDatabase, ResolvedWASQLiteOpenFactoryOptions, @@ -11,6 +11,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { TEST_SCHEMA } from './utils/test-schema.js'; import { MultiDatabaseServer } from '../src/worker/db/MultiDatabaseServer.js'; import { DatabaseClient } from '../src/db/adapters/wa-sqlite/DatabaseClient.js'; +import { defaultLoggerConfig } from './utils/testDb.js'; const testId = '2290de4f-0488-4e50-abed-f8e8eb1d0b42'; @@ -78,7 +79,8 @@ describe('Open Methods', { sequential: true }, () => { }); it('Should open with an existing DBAdapter', async () => { - const server = new MultiDatabaseServer(createLogger('adapter-test')); + const logger = createPowerSyncLogger({ prefix: 'adapter-test' }); + const server = new MultiDatabaseServer(logger); const options: ResolvedWASQLiteOpenFactoryOptions = { vfs: WASQLiteVFS.IDBBatchAtomicVFS, flags: { @@ -93,7 +95,7 @@ describe('Open Methods', { sequential: true }, () => { dbFilename: '', isReadOnly: false }; - const connection = await server.openConnectionLocally(options); + const connection = await server.openConnectionLocally(logger, options); const Adapter = DBAdapterDefaultMixin(DatabaseClient); const client = new Adapter( { @@ -109,7 +111,7 @@ describe('Open Methods', { sequential: true }, () => { }); it('Should open with provided factory', async () => { - const factory = new WASQLiteOpenFactory({ dbFilename: 'factory-test.db' }); + const factory = new WASQLiteOpenFactory({ ...defaultLoggerConfig, dbFilename: 'factory-test.db' }); const db = new PowerSyncDatabase({ database: factory, schema: TEST_SCHEMA }); await basicTest(db); diff --git a/packages/web/tests/src/db/PowersyncDatabase.test.ts b/packages/web/tests/src/db/PowersyncDatabase.test.ts index fad757d03..74ef67f65 100644 --- a/packages/web/tests/src/db/PowersyncDatabase.test.ts +++ b/packages/web/tests/src/db/PowersyncDatabase.test.ts @@ -1,19 +1,14 @@ -import { createLogger, PowerSyncDatabase } from '@powersync/web'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { LogLevels, PowerSyncDatabase } from '@powersync/web'; +import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { TEST_SCHEMA } from '../../utils/test-schema.js'; describe('PowerSyncDatabase', () => { let db: PowerSyncDatabase; let mockConnector: any; - let mockLogger: any; - let mockSyncImplementation: any; + let mockLogger: Mock<(level: number, ...mesage: any[]) => void>; beforeEach(() => { - const logger = createLogger('test'); - mockLogger = { - debug: vi.spyOn(logger, 'debug'), - warn: vi.spyOn(logger, 'warn') - }; + mockLogger = vi.fn(); // Initialize with minimal required options db = new PowerSyncDatabase({ @@ -21,7 +16,7 @@ describe('PowerSyncDatabase', () => { database: { dbFilename: 'test.db' }, - logger + logger: { log: mockLogger } }); }); @@ -32,7 +27,7 @@ describe('PowerSyncDatabase', () => { describe('connect', () => { it('should log debug message when attempting to connect', async () => { await db.connect(mockConnector); - expect(mockLogger.debug).toHaveBeenCalledWith('Attempting to connect to PowerSync instance'); + expect(mockLogger).toHaveBeenCalledWith(LogLevels.debug, 'Attempting to connect to PowerSync instance'); }); }); }); diff --git a/packages/web/tests/src/db/write_ahead_log_opfs.test.ts b/packages/web/tests/src/db/write_ahead_log_opfs.test.ts index ae77793d6..416fba351 100644 --- a/packages/web/tests/src/db/write_ahead_log_opfs.test.ts +++ b/packages/web/tests/src/db/write_ahead_log_opfs.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { generateTestDb } from '../../utils/testDb.js'; +import { defaultLoggerConfig, generateTestDb } from '../../utils/testDb.js'; import { WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { TEST_SCHEMA } from '../../utils/test-schema.js'; @@ -8,7 +8,8 @@ test('supports concurrent reads', async () => { database: new WASQLiteOpenFactory({ dbFilename: 'basic-opfs.sqlite', vfs: WASQLiteVFS.OPFSWriteAheadVFS, - additionalReaders: 1 + additionalReaders: 1, + ...defaultLoggerConfig }), schema: TEST_SCHEMA }); diff --git a/packages/web/tests/stream.test.ts b/packages/web/tests/stream.test.ts index ab87fddf4..cc9be1b86 100644 --- a/packages/web/tests/stream.test.ts +++ b/packages/web/tests/stream.test.ts @@ -1,5 +1,6 @@ import { - createBaseLogger, + createPowerSyncLogger, + LogLevels, PowerSyncConnectionOptions, Schema, SyncClientImplementation, @@ -13,14 +14,13 @@ import { describe, expect, it, onTestFinished, vi } from 'vitest'; import { TestConnector } from './utils/MockStreamOpenFactory.js'; import { ConnectedDatabaseUtils, generateConnectedDatabase } from './utils/generateConnectedDatabase.js'; import { BucketChecksum } from '@powersync/common/internal/sync_protocol'; +import { defaultLoggerConfig } from './utils/testDb.js'; const UPLOAD_TIMEOUT_MS = 3000; -const baseLogger = createBaseLogger(); -baseLogger.useDefaults(); -const logger = baseLogger.get('stream test'); - describe('Streaming', { sequential: true }, () => { + const logger = createPowerSyncLogger({ prefix: 'stream test', minLevel: LogLevels.trace }); + describe( 'Streaming - With Web Workers', { @@ -62,6 +62,7 @@ describe('Streaming', { sequential: true }, () => { generateConnectedDatabase({ powerSyncOptions: { database: new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename: 'streaming-opfs.sqlite', vfs: WASQLiteVFS.OPFSCoopSyncVFS }), diff --git a/packages/web/tests/triggers.test.ts b/packages/web/tests/triggers.test.ts index 585879f5b..ddc0afd2c 100644 --- a/packages/web/tests/triggers.test.ts +++ b/packages/web/tests/triggers.test.ts @@ -2,7 +2,7 @@ import { DiffTriggerOperation } from '@powersync/common'; import { WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { describe, expect, it, onTestFinished, vi } from 'vitest'; import { TEST_SCHEMA } from './utils/test-schema.js'; -import { generateTestDb } from './utils/testDb.js'; +import { defaultLoggerConfig, generateTestDb } from './utils/testDb.js'; // Shared helper to spin up an iframe that creates a persisted trigger table const createTriggerInIframe = () => { @@ -37,6 +37,7 @@ describe('Triggers', () => { it('should use temporary triggers by default with IndexedDB VFS', async () => { const db = generateTestDb({ database: new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename: 'temp-triggers.sqlite' // default VFS (IndexedDB) - no vfs specified }), @@ -85,6 +86,7 @@ describe('Triggers', () => { it('should automatically configure persistence for OPFS triggers', async () => { const db = generateTestDb({ database: new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename: 'triggers.sqlite', vfs: WASQLiteVFS.OPFSCoopSyncVFS }), @@ -126,6 +128,7 @@ describe('Triggers', () => { const openDB = () => generateTestDb({ database: new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename: 'triggers.sqlite', vfs: WASQLiteVFS.OPFSCoopSyncVFS }), @@ -194,6 +197,7 @@ describe('Triggers', () => { const openDB = (filename: string) => generateTestDb({ database: new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename: filename, vfs: WASQLiteVFS.OPFSCoopSyncVFS }), diff --git a/packages/web/tests/uploads.test.ts b/packages/web/tests/uploads.test.ts index 0218b88ec..c6cc4891f 100644 --- a/packages/web/tests/uploads.test.ts +++ b/packages/web/tests/uploads.test.ts @@ -1,7 +1,7 @@ -import { createBaseLogger, createLogger, WebPowerSyncDatabaseOptions } from '@powersync/web'; +import { PowerSyncLogger, WebPowerSyncDatabaseOptions } from '@powersync/web'; import p from 'p-defer'; import { v4 } from 'uuid'; -import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { DEFAULT_CONNECTED_POWERSYNC_OPTIONS, generateConnectedDatabase } from './utils/generateConnectedDatabase.js'; // Don't want to actually export the warning string from the package @@ -39,11 +39,20 @@ describe( function describeCrudUploadTests(getDatabaseOptions: () => WebPowerSyncDatabaseOptions) { return () => { - beforeAll(() => createBaseLogger().useDefaults()); + let logLines: string[] = []; + let logger: PowerSyncLogger; + + beforeEach(() => { + const lines: string[] = []; + logLines = lines; + logger = { + log(_level, ...message) { + lines.push(message[0] as string); + } + }; + }); it('should warn for missing upload operations in uploadData', async () => { - const logger = createLogger('crud-logger'); - const options = getDatabaseOptions(); const { powersync, uploadSpy } = await generateConnectedDatabase({ @@ -53,8 +62,6 @@ function describeCrudUploadTests(getDatabaseOptions: () => WebPowerSyncDatabaseO } }); - const loggerSpy = vi.spyOn(logger, 'warn'); - const deferred = p(); uploadSpy.mockImplementation(async (db) => { @@ -72,7 +79,7 @@ function describeCrudUploadTests(getDatabaseOptions: () => WebPowerSyncDatabaseO await vi.waitFor( () => { - expect(loggerSpy.mock.calls.find((logArgs) => logArgs[0].includes(PARTIAL_WARNING))).exist; + expect(logLines).contains(expect.stringContaining(PARTIAL_WARNING)); }, { timeout: 500, @@ -82,8 +89,6 @@ function describeCrudUploadTests(getDatabaseOptions: () => WebPowerSyncDatabaseO }); it('should immediately upload sequential transactions', async () => { - const logger = createLogger('crud-logger'); - const options = getDatabaseOptions(); const { powersync, uploadSpy } = await generateConnectedDatabase({ @@ -93,8 +98,6 @@ function describeCrudUploadTests(getDatabaseOptions: () => WebPowerSyncDatabaseO } }); - const loggerSpy = vi.spyOn(logger, 'warn'); - const deferred = p(); uploadSpy.mockImplementation(async (db) => { @@ -140,7 +143,7 @@ function describeCrudUploadTests(getDatabaseOptions: () => WebPowerSyncDatabaseO } ); - expect(loggerSpy.mock.calls.find((logArgs) => logArgs[0].includes(PARTIAL_WARNING))).toBeUndefined; + expect(logLines).not.contains(expect.stringContaining(PARTIAL_WARNING)); }); }; } diff --git a/packages/web/tests/utils/MockStreamOpenFactory.ts b/packages/web/tests/utils/MockStreamOpenFactory.ts index 89f69a5ea..75fbb0c05 100644 --- a/packages/web/tests/utils/MockStreamOpenFactory.ts +++ b/packages/web/tests/utils/MockStreamOpenFactory.ts @@ -6,6 +6,7 @@ import { PowerSyncBackendConnector, PowerSyncCredentials, PowerSyncDatabaseOptions, + PowerSyncLogger, RemoteConnector, SimpleAsyncIterator, SyncStreamOptions @@ -40,9 +41,10 @@ export class MockRemote extends AbstractRemote { constructor( connector: RemoteConnector, + logger: PowerSyncLogger, protected onStreamRequested: () => void ) { - super(connector); + super(connector, logger); this.streamController = null; this.generateCheckpoint = vi.fn(() => { return { @@ -136,7 +138,7 @@ export class MockedStreamPowerSync extends PowerSyncDatabase { connector: PowerSyncBackendConnector ): AbstractStreamingSyncImplementation { return new WebStreamingSyncImplementation({ - logger: this.options.logger, + logger: this.logger, adapter: this.bucketStorageAdapter, remote: this.remote, uploadCrud: async () => { diff --git a/packages/web/tests/utils/generateConnectedDatabase.ts b/packages/web/tests/utils/generateConnectedDatabase.ts index 9939605d6..7adaae72e 100644 --- a/packages/web/tests/utils/generateConnectedDatabase.ts +++ b/packages/web/tests/utils/generateConnectedDatabase.ts @@ -3,6 +3,7 @@ import { WebPowerSyncOpenFactoryOptions } from '@powersync/web'; import { v4 as uuid, v4 } from 'uuid'; import { onTestFinished, vi } from 'vitest'; import { MockRemote, MockStreamOpenFactory, TestConnector } from './MockStreamOpenFactory.js'; +import { defaultLoggerConfig } from './testDb.js'; type UnwrapPromise = T extends Promise ? U : T; @@ -42,7 +43,7 @@ export async function generateConnectedDatabase(options: GenerateConnectedDataba const callbacks: Map void> = new Map(); const connector = new TestConnector(); const uploadSpy = vi.spyOn(connector, 'uploadData'); - const remote = new MockRemote(connector, () => callbacks.forEach((c) => c())); + const remote = new MockRemote(connector, defaultLoggerConfig.logger, () => callbacks.forEach((c) => c())); const factory = new MockStreamOpenFactory( { diff --git a/packages/web/tests/utils/iframeInitializer.ts b/packages/web/tests/utils/iframeInitializer.ts index 2e7c150c1..9fe87034f 100644 --- a/packages/web/tests/utils/iframeInitializer.ts +++ b/packages/web/tests/utils/iframeInitializer.ts @@ -1,6 +1,14 @@ -import { LogLevel, Schema, SyncStreamConnectionMethod, TableV2, column, createLogger } from '@powersync/common'; +import { + LogLevels, + Schema, + SyncStreamConnectionMethod, + TableV2, + column, + createPowerSyncLogger +} from '@powersync/common'; import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { getMockSyncServiceFromWorker } from './MockSyncServiceClient.js'; +import { defaultLoggerConfig } from './testDb.js'; /** * Initializes a PowerSync client in the current iframe context and notifies the parent. @@ -39,14 +47,16 @@ export async function setupPowerSyncInIframe( // The vfs string value is the enum value itself (string enums) const databaseOptions = vfs ? new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename, vfs: vfs as WASQLiteVFS }) : { dbFilename }; // Configure verbose logging - const logger = createLogger('iFrame test', { - logLevel: LogLevel.DEBUG + const logger = createPowerSyncLogger({ + prefix: 'iFrame test', + minLevel: LogLevels.debug }); const db = new PowerSyncDatabase({ diff --git a/packages/web/tests/utils/mockSyncServiceTest.ts b/packages/web/tests/utils/mockSyncServiceTest.ts index 8c1244f5e..a0b978aea 100644 --- a/packages/web/tests/utils/mockSyncServiceTest.ts +++ b/packages/web/tests/utils/mockSyncServiceTest.ts @@ -1,12 +1,12 @@ import { - LogLevel, + LogLevels, PowerSyncBackendConnector, PowerSyncCredentials, Schema, SyncStreamConnectionMethod, Table, column, - createBaseLogger + createPowerSyncLogger } from '@powersync/common'; import { PowerSyncDatabase, WebPowerSyncDatabaseOptions } from '@powersync/web'; import { MockedFunction, expect, onTestFinished, test, vi } from 'vitest'; @@ -67,12 +67,7 @@ export const sharedMockSyncServiceTest = test.extend<{ }>({ context: async ({}, use) => { const dbFilename = `test-${crypto.randomUUID()}.db`; - const globalLogger = createBaseLogger(); - globalLogger.useDefaults({ - defaultLevel: LogLevel.DEBUG - }); - - const logger = globalLogger.get('mocked sync'); + const logger = createPowerSyncLogger({ prefix: 'mocked sync', minLevel: LogLevels.debug }); const openDatabase = (customConfig: Partial = {}) => { const db = new PowerSyncDatabase({ diff --git a/packages/web/tests/utils/testDb.ts b/packages/web/tests/utils/testDb.ts index 055b6dc8f..01661d8f4 100644 --- a/packages/web/tests/utils/testDb.ts +++ b/packages/web/tests/utils/testDb.ts @@ -1,4 +1,4 @@ -import { PowerSyncDatabase, WebPowerSyncDatabaseOptions } from '@powersync/web'; +import { createPowerSyncLogger, LogLevels, PowerSyncDatabase, WebPowerSyncDatabaseOptions } from '@powersync/web'; import { v4 as uuid } from 'uuid'; import { onTestFinished } from 'vitest'; import { TEST_SCHEMA } from './test-schema.js'; @@ -26,3 +26,8 @@ export const generateTestDb = (options?: WebPowerSyncDatabaseOptions) => { return db; }; + +export const defaultLoggerConfig = { + logLevel: LogLevels.trace, + logger: createPowerSyncLogger({ prefix: 'test', minLevel: LogLevels.trace }) +}; diff --git a/packages/web/tests/utils/triggers/IFrameTriggerConfig.ts b/packages/web/tests/utils/triggers/IFrameTriggerConfig.ts index 13ee5765d..46dc26bc1 100644 --- a/packages/web/tests/utils/triggers/IFrameTriggerConfig.ts +++ b/packages/web/tests/utils/triggers/IFrameTriggerConfig.ts @@ -7,9 +7,11 @@ import { DiffTriggerOperation, PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { TEST_SCHEMA } from '../test-schema.js'; +import { defaultLoggerConfig } from '../testDb.js'; const db = new PowerSyncDatabase({ database: new WASQLiteOpenFactory({ + ...defaultLoggerConfig, dbFilename: 'triggers.sqlite', vfs: WASQLiteVFS.OPFSCoopSyncVFS }), From cee4cdd11170975661123e798d37990f28c9a1c5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 7 May 2026 13:23:45 +0200 Subject: [PATCH 04/21] Fix multiple instances test --- .../SharedWebStreamingSyncImplementation.ts | 2 +- packages/web/tests/multiple_instances.test.ts | 33 ++++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index e3bee846d..4eb0a496c 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -68,7 +68,7 @@ class SharedSyncClientProvider extends AbstractSharedSyncClientProvider { } log(level: number, ...message: any[]): void { - this.logger?.log(level, message); + this.logger?.log(level, ...message); } } diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index ea5abcdfa..bfb0d9d73 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -46,12 +46,19 @@ describe('Multiple Instances', { sequential: true }, () => { 'should broadcast logs from shared sync worker', { timeout: 10_000 }, async ({ context: { openDatabase, mockService } }) => { - const logFn = vi.fn(); - const logger: PowerSyncLogger = { log: logFn }; + const logLines: string[] = []; + const logger: PowerSyncLogger = { + log(_level, ...message) { + logLines.push(message[0]); + } + }; // Open an additional database which we can spy on the logs. const powersync = openDatabase({ - logger + logger, + sync: { + logLevel: LogLevels.trace + } }); powersync.connect({ @@ -78,22 +85,16 @@ describe('Multiple Instances', { sequential: true }, () => { // Asserting that powersync_control logs exists verifies that some connection attempt was made. await vi.waitFor( - () => - expect( - logFn.mock.calls - .flat(1) - .find((argument) => typeof argument == 'string' && argument.includes('powersync_control')) - ).exist, - { timeout: 2000 } + () => expect(logLines).toEqual(expect.arrayContaining([expect.stringContaining('powersync_control')])), + { + timeout: 2000 + } ); // The connection should fail with an error - await vi.waitFor( - () => - expect(logFn.mock.calls.flat(1).find((argument) => typeof argument == 'string' && argument.includes('error'))) - .exist, - { timeout: 2000 } - ); + await vi.waitFor(() => expect(logLines).toEqual(expect.arrayContaining([expect.any(Error)])), { + timeout: 2000 + }); } ); From 3e4406aa57dcd445e382c6530c8c04d2abf100de Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 7 May 2026 13:51:04 +0200 Subject: [PATCH 05/21] Fix package build --- packages/adapter-sql-js/src/SQLJSAdapter.ts | 15 ++++++------ packages/capacitor/src/PowerSyncDatabase.ts | 24 ++++++++++++------- .../RNQSDBOpenFactory.ts | 6 ++++- .../src/sync/stream/ReactNativeRemote.ts | 9 +++---- .../vue/src/composables/useSingleQuery.ts | 7 +++--- .../vue/src/composables/useWatchedQuery.ts | 4 ++-- 6 files changed, 40 insertions(+), 25 deletions(-) diff --git a/packages/adapter-sql-js/src/SQLJSAdapter.ts b/packages/adapter-sql-js/src/SQLJSAdapter.ts index 9cbb3bf2f..82bc90898 100644 --- a/packages/adapter-sql-js/src/SQLJSAdapter.ts +++ b/packages/adapter-sql-js/src/SQLJSAdapter.ts @@ -4,15 +4,16 @@ import { BatchedUpdateNotification, ConnectionPool, ControlledExecutor, - createLogger, + createPowerSyncLogger, DBAdapter, DBAdapterDefaultMixin, DBAdapterListener, DBGetUtilsDefaultMixin, DBLockOptions, - ILogger, LockContext, + LogLevels, Mutex, + PowerSyncLogger, QueryResult, SqlExecutor, SQLOpenFactory, @@ -30,12 +31,12 @@ export interface SQLJSPersister { export interface SQLJSOpenOptions extends SQLOpenOptions { persister?: SQLJSPersister; - logger?: ILogger; + logger?: PowerSyncLogger; } export interface ResolvedSQLJSOpenOptions extends SQLJSOpenOptions { persister?: SQLJSPersister; - logger: ILogger; + logger: PowerSyncLogger; } export class SQLJSOpenFactory implements SQLOpenFactory { @@ -110,7 +111,7 @@ class SqlJsConnectionPool extends BaseObserver implements Con } protected resolveOptions(options: SQLJSOpenOptions): ResolvedSQLJSOpenOptions { - const logger = options.logger ?? createLogger('SQLJSDBAdapter'); + const logger = options.logger ?? createPowerSyncLogger({ prefix: 'SQLJSDBAdapter' }); return { ...options, @@ -122,10 +123,10 @@ class SqlJsConnectionPool extends BaseObserver implements Con const SQL = await SQLJs({ locateFile: (filename: any) => `../dist/${filename}`, print: (text) => { - this.options.logger.info(text); + this.options.logger.log(LogLevels.info, text); }, printErr: (text) => { - this.options.logger.error('[stderr]', text); + this.options.logger.log(LogLevels.error, '[stderr]', text); } }); const existing = await this.options.persister?.readFile(); diff --git a/packages/capacitor/src/PowerSyncDatabase.ts b/packages/capacitor/src/PowerSyncDatabase.ts index d9e5a53cf..e16b67a76 100644 --- a/packages/capacitor/src/PowerSyncDatabase.ts +++ b/packages/capacitor/src/PowerSyncDatabase.ts @@ -2,6 +2,7 @@ import { Capacitor } from '@capacitor/core'; import { DBAdapter, DEFAULT_STREAM_CONNECTION_OPTIONS, + LogLevels, MEMORY_TRIGGER_CLAIM_MANAGER, PowerSyncBackendConnector, PowerSyncConnectionOptions, @@ -37,7 +38,8 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase { ? SyncStreamConnectionMethod.HTTP : DEFAULT_STREAM_CONNECTION_OPTIONS.connectionMethod; if (options?.connectionMethod == SyncStreamConnectionMethod.WEB_SOCKET && isUsingCapacitorDriver) { - this.logger.warn( + this.logger.log( + LogLevels.warn, `Connecting via 'SyncStreamConnectionMethod.WEB_SOCKET' when using the 'CapacitorSQLiteAdapter' will result in poor sync performance. Use 'SyncStreamConnectionMethod.HTTP' (the default for native) instead.` ); } @@ -57,17 +59,20 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase { const platform = Capacitor.getPlatform(); if (platform == 'ios' || platform == 'android') { if (options.database.dbLocation) { - options.logger?.warn(` + options.logger?.log( + LogLevels.warn, + ` dbLocation is ignored on iOS and Android platforms. The database directory can be configured in the Capacitor project. - See https://github.com/capacitor-community/sqlite?tab=readme-ov-file#installation`); + See https://github.com/capacitor-community/sqlite?tab=readme-ov-file#installation` + ); } - options.logger?.debug(`Using CapacitorSQLiteAdapter for platform: ${platform}`); + options.logger?.log(LogLevels.debug, `Using CapacitorSQLiteAdapter for platform: ${platform}`); return new CapacitorSQLiteAdapter({ ...options.database }); } else { - options.logger?.debug(`Using default web adapter for web platform`); + options.logger?.log(LogLevels.debug, `Using default web adapter for web platform`); return super.openDBAdapter(options); } } @@ -102,9 +107,12 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase { if (this.isNativeCapacitorPlatform) { // We don't want to support multi-tab on mobile platforms. // We technically can, but it's not a common use case and requires additional work/testing. - this.logger.debug(`Using Capacitor sync implementation`); + this.logger.log(LogLevels.debug, `Using Capacitor sync implementation`); if (this.options.flags?.enableMultiTabs) { - this.logger.warn(`enableMultiTabs is not supported on Capacitor mobile platforms. Ignoring the flag.`); + this.logger.log( + LogLevels.warn, + `enableMultiTabs is not supported on Capacitor mobile platforms. Ignoring the flag.` + ); } const remote = new CapacitorRemote(connector, this.logger); @@ -124,7 +132,7 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase { subscriptions: options.subscriptions }); } else { - this.logger.debug(`Using default web sync implementation for web platform`); + this.logger.log(LogLevels.debug, `Using default web sync implementation for web platform`); return super.generateSyncStreamImplementation(connector, options); } } diff --git a/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts b/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts index 9d4dd00dd..3dbf11c41 100644 --- a/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts +++ b/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts @@ -2,6 +2,7 @@ import { AbstractPowerSyncDatabase, AbstractPowerSyncDatabaseOpenFactory, DBAdapter, + LogLevels, PowerSyncDatabaseOptions, PowerSyncOpenFactoryOptions, SQLOpenFactory @@ -36,7 +37,10 @@ export class RNQSPowerSyncDatabaseOpenFactory extends AbstractPowerSyncDatabaseO generateInstance(options: PowerSyncDatabaseOptions): AbstractPowerSyncDatabase { if (this.instanceGenerated) { - this.options.logger?.warn('Generating multiple PowerSync instances can sometimes cause unexpected results.'); + this.options.logger?.log( + LogLevels.warn, + 'Generating multiple PowerSync instances can sometimes cause unexpected results.' + ); } this.instanceGenerated = true; return new PowerSyncDatabase(options); diff --git a/packages/react-native/src/sync/stream/ReactNativeRemote.ts b/packages/react-native/src/sync/stream/ReactNativeRemote.ts index cc9061368..aabf653e5 100644 --- a/packages/react-native/src/sync/stream/ReactNativeRemote.ts +++ b/packages/react-native/src/sync/stream/ReactNativeRemote.ts @@ -1,10 +1,10 @@ import { AbstractRemote, AbstractRemoteOptions, - DEFAULT_REMOTE_LOGGER, FetchImplementation, FetchImplementationProvider, - ILogger, + LogLevels, + PowerSyncLogger, RemoteConnector, SyncStreamOptions } from '@powersync/common'; @@ -35,7 +35,7 @@ class ReactNativeFetchProvider extends FetchImplementationProvider { export class ReactNativeRemote extends AbstractRemote { constructor( protected connector: RemoteConnector, - protected logger: ILogger = DEFAULT_REMOTE_LOGGER, + protected logger: PowerSyncLogger, options?: Partial ) { super(connector, logger, { @@ -81,7 +81,8 @@ export class ReactNativeRemote extends AbstractRemote { const timeout = Platform.OS == 'android' ? setTimeout(() => { - this.logger.warn( + this.logger.log( + LogLevels.warn, `HTTP Streaming POST is taking longer than ${Math.ceil( STREAMING_POST_TIMEOUT_MS / 1000 )} seconds to resolve. If using a debug build, please ensure Flipper Network plugin is disabled.` diff --git a/packages/vue/src/composables/useSingleQuery.ts b/packages/vue/src/composables/useSingleQuery.ts index b63006714..24329d479 100644 --- a/packages/vue/src/composables/useSingleQuery.ts +++ b/packages/vue/src/composables/useSingleQuery.ts @@ -1,6 +1,7 @@ import { type CompilableQuery, DifferentialWatchedQueryComparator, + LogLevels, ParsedQuery, parseQuery, SQLOnChangeOptions @@ -85,7 +86,7 @@ export const useSingleQuery = ( let fetchData: () => Promise | undefined; const powerSync = usePowerSync(); - const logger = powerSync?.value?.logger ?? console; + const logger = powerSync?.value?.logger; const finishLoading = () => { isLoading.value = false; @@ -130,7 +131,7 @@ export const useSingleQuery = ( try { parsedQuery = parseQuery(queryValue, toValue(sqlParameters).map(toValue)); } catch (e) { - logger.error('Failed to parse query:', e); + logger?.log(LogLevels.error, 'Failed to parse query:', e); handleError(e); return; } @@ -146,7 +147,7 @@ export const useSingleQuery = ( const result = await executor(); handleResult(result); } catch (e) { - logger.error('Failed to fetch data:', e); + logger?.log(LogLevels.error, 'Failed to fetch data:', e); handleError(e); } }; diff --git a/packages/vue/src/composables/useWatchedQuery.ts b/packages/vue/src/composables/useWatchedQuery.ts index 33e53e43d..d891ee6b6 100644 --- a/packages/vue/src/composables/useWatchedQuery.ts +++ b/packages/vue/src/composables/useWatchedQuery.ts @@ -1,4 +1,4 @@ -import { type CompilableQuery, ParsedQuery, parseQuery, WatchCompatibleQuery } from '@powersync/common'; +import { type CompilableQuery, LogLevels, ParsedQuery, parseQuery, WatchCompatibleQuery } from '@powersync/common'; import { type MaybeRef, type Ref, ref, toValue, watchEffect } from 'vue'; import { usePowerSync } from './powerSync.js'; import { AdditionalOptions, WatchedQueryResult } from './useSingleQuery.js'; @@ -47,7 +47,7 @@ export const useWatchedQuery = ( try { parsedQuery = parseQuery(queryValue, toValue(sqlParameters).map(toValue)); } catch (e: any) { - logger.error('Failed to parse query:', e); + logger.log(LogLevels.error, 'Failed to parse query:', e); handleError(e); return; } From e6f47c648385a451e86732ac828938bfc24210ef Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 15 May 2026 14:07:31 +0200 Subject: [PATCH 06/21] Simplify logger design --- packages/adapter-sql-js/src/SQLJSAdapter.ts | 4 +- .../src/AbstractAttachmentQueue.ts | 61 +++++++++------ packages/capacitor/src/PowerSyncDatabase.ts | 35 +++++---- .../src/attachments/AttachmentContext.ts | 10 +-- .../common/src/attachments/AttachmentQueue.ts | 2 +- .../src/attachments/AttachmentService.ts | 2 +- .../common/src/attachments/SyncingService.ts | 10 ++- .../src/client/AbstractPowerSyncDatabase.ts | 16 +++- .../common/src/client/ConnectionManager.ts | 18 ++--- .../client/sync/bucket/SqliteBucketStorage.ts | 21 ++--- .../src/client/sync/stream/AbstractRemote.ts | 20 ++--- .../AbstractStreamingSyncImplementation.ts | 76 ++++++++++++------- .../src/client/triggers/TriggerManagerImpl.ts | 24 +++--- .../processors/AbstractQueryProcessor.ts | 6 +- packages/common/src/utils/Logger.ts | 48 +++++++++--- .../composables/useDiagnosticsLogger.ts | 15 ++-- .../runtime/utils/NuxtPowerSyncDatabase.ts | 9 ++- .../vue/src/composables/useSingleQuery.ts | 12 +-- .../vue/src/composables/useWatchedQuery.ts | 6 +- packages/web/src/db/PowerSyncDatabase.ts | 2 +- .../db/adapters/wa-sqlite/DatabaseServer.ts | 9 ++- .../adapters/wa-sqlite/WASQLiteOpenFactory.ts | 18 ++--- .../SharedWebStreamingSyncImplementation.ts | 5 +- packages/web/src/db/sync/WebRemote.ts | 4 +- .../db/sync/WebStreamingSyncImplementation.ts | 2 +- .../web/src/worker/db/MultiDatabaseServer.ts | 16 ++-- .../sync/AbstractSharedSyncClientProvider.ts | 4 +- .../web/src/worker/sync/BroadcastLogger.ts | 40 +++++----- .../worker/sync/SharedSyncImplementation.ts | 38 +++++++--- packages/web/tests/multiple_instances.test.ts | 4 +- .../tests/src/db/PowersyncDatabase.test.ts | 9 ++- packages/web/tests/uploads.test.ts | 4 +- 32 files changed, 334 insertions(+), 216 deletions(-) diff --git a/packages/adapter-sql-js/src/SQLJSAdapter.ts b/packages/adapter-sql-js/src/SQLJSAdapter.ts index 82bc90898..9c1c245bc 100644 --- a/packages/adapter-sql-js/src/SQLJSAdapter.ts +++ b/packages/adapter-sql-js/src/SQLJSAdapter.ts @@ -123,10 +123,10 @@ class SqlJsConnectionPool extends BaseObserver implements Con const SQL = await SQLJs({ locateFile: (filename: any) => `../dist/${filename}`, print: (text) => { - this.options.logger.log(LogLevels.info, text); + this.options.logger.log({ level: LogLevels.info, message: text }); }, printErr: (text) => { - this.options.logger.log(LogLevels.error, '[stderr]', text); + this.options.logger.log({ level: LogLevels.error, message: `[stderr]: ${text}` }); } }); const existing = await this.options.persister?.readFile(); diff --git a/packages/attachments/src/AbstractAttachmentQueue.ts b/packages/attachments/src/AbstractAttachmentQueue.ts index 4c0c9d923..988ec93c1 100644 --- a/packages/attachments/src/AbstractAttachmentQueue.ts +++ b/packages/attachments/src/AbstractAttachmentQueue.ts @@ -127,7 +127,7 @@ export abstract class AbstractAttachmentQueue { const _ids = `${ids.map((id) => `'${id}'`).join(',')}`; - this.logger.log(LogLevels.debug, `Queuing for sync, attachment IDs: [${_ids}]`); + this.logger.log({ level: LogLevels.debug, message: `Queuing for sync, attachment IDs: [${_ids}]` }); if (this.initialSync) { this.initialSync = false; @@ -155,14 +155,17 @@ export abstract class AbstractAttachmentQueue 0) { const id = this.downloadQueue.values().next().value!; this.downloadQueue.delete(id); @@ -481,9 +494,9 @@ export abstract class AbstractAttachmentQueue { for (const record of res) { await this.delete(record, tx); @@ -534,7 +547,7 @@ export abstract class AbstractAttachmentQueue { - this.logger.log(LogLevels.debug, `Clearing attachment queue...`); + this.logger.log({ level: LogLevels.debug, message: `Clearing attachment queue...` }); await this.powersync.writeTransaction(async (tx) => { await tx.execute(`DELETE FROM ${this.table}`); }); diff --git a/packages/capacitor/src/PowerSyncDatabase.ts b/packages/capacitor/src/PowerSyncDatabase.ts index e16b67a76..810879a0e 100644 --- a/packages/capacitor/src/PowerSyncDatabase.ts +++ b/packages/capacitor/src/PowerSyncDatabase.ts @@ -38,10 +38,10 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase { ? SyncStreamConnectionMethod.HTTP : DEFAULT_STREAM_CONNECTION_OPTIONS.connectionMethod; if (options?.connectionMethod == SyncStreamConnectionMethod.WEB_SOCKET && isUsingCapacitorDriver) { - this.logger.log( - LogLevels.warn, - `Connecting via 'SyncStreamConnectionMethod.WEB_SOCKET' when using the 'CapacitorSQLiteAdapter' will result in poor sync performance. Use 'SyncStreamConnectionMethod.HTTP' (the default for native) instead.` - ); + this.logger.log({ + level: LogLevels.warn, + message: `Connecting via 'SyncStreamConnectionMethod.WEB_SOCKET' when using the 'CapacitorSQLiteAdapter' will result in poor sync performance. Use 'SyncStreamConnectionMethod.HTTP' (the default for native) instead.` + }); } return super.connect(connector, { @@ -59,20 +59,23 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase { const platform = Capacitor.getPlatform(); if (platform == 'ios' || platform == 'android') { if (options.database.dbLocation) { - options.logger?.log( - LogLevels.warn, - ` + options.logger?.log({ + level: LogLevels.warn, + message: ` dbLocation is ignored on iOS and Android platforms. The database directory can be configured in the Capacitor project. See https://github.com/capacitor-community/sqlite?tab=readme-ov-file#installation` - ); + }); } - options.logger?.log(LogLevels.debug, `Using CapacitorSQLiteAdapter for platform: ${platform}`); + options.logger?.log({ + level: LogLevels.debug, + message: `Using CapacitorSQLiteAdapter for platform: ${platform}` + }); return new CapacitorSQLiteAdapter({ ...options.database }); } else { - options.logger?.log(LogLevels.debug, `Using default web adapter for web platform`); + options.logger?.log({ level: LogLevels.debug, message: `Using default web adapter for web platform` }); return super.openDBAdapter(options); } } @@ -107,12 +110,12 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase { if (this.isNativeCapacitorPlatform) { // We don't want to support multi-tab on mobile platforms. // We technically can, but it's not a common use case and requires additional work/testing. - this.logger.log(LogLevels.debug, `Using Capacitor sync implementation`); + this.logger.log({ level: LogLevels.debug, message: `Using Capacitor sync implementation` }); if (this.options.flags?.enableMultiTabs) { - this.logger.log( - LogLevels.warn, - `enableMultiTabs is not supported on Capacitor mobile platforms. Ignoring the flag.` - ); + this.logger.log({ + level: LogLevels.warn, + message: `enableMultiTabs is not supported on Capacitor mobile platforms. Ignoring the flag.` + }); } const remote = new CapacitorRemote(connector, this.logger); @@ -132,7 +135,7 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase { subscriptions: options.subscriptions }); } else { - this.logger.log(LogLevels.debug, `Using default web sync implementation for web platform`); + this.logger.log({ level: LogLevels.debug, message: `Using default web sync implementation for web platform` }); return super.generateSyncStreamImplementation(connector, options); } } diff --git a/packages/common/src/attachments/AttachmentContext.ts b/packages/common/src/attachments/AttachmentContext.ts index e5d7fc472..c88214c30 100644 --- a/packages/common/src/attachments/AttachmentContext.ts +++ b/packages/common/src/attachments/AttachmentContext.ts @@ -233,10 +233,10 @@ export class AttachmentContext { if (archivedAttachments.length === 0) return false; await callback?.(archivedAttachments); - this.logger.log( - LogLevels.info, - `Deleting ${archivedAttachments.length} archived attachments. Archived attachment exceeds cache archiveCacheLimit of ${this.archivedCacheLimit}.` - ); + this.logger.log({ + level: LogLevels.info, + message: `Deleting ${archivedAttachments.length} archived attachments. Archived attachment exceeds cache archiveCacheLimit of ${this.archivedCacheLimit}.` + }); const ids = archivedAttachments.map((attachment) => attachment.id); @@ -255,7 +255,7 @@ export class AttachmentContext { [JSON.stringify(ids)] ); - this.logger.log(LogLevels.info, `Deleted ${archivedAttachments.length} archived attachments`); + this.logger.log({ level: LogLevels.info, message: `Deleted ${archivedAttachments.length} archived attachments` }); return archivedAttachments.length < limit; } diff --git a/packages/common/src/attachments/AttachmentQueue.ts b/packages/common/src/attachments/AttachmentQueue.ts index 46e14a4c9..719403387 100644 --- a/packages/common/src/attachments/AttachmentQueue.ts +++ b/packages/common/src/attachments/AttachmentQueue.ts @@ -190,7 +190,7 @@ export class AttachmentQueue { if (status.connected) { // Device came online, process attachments immediately this.syncStorage().catch((error) => { - this.logger.log(LogLevels.error, 'Error syncing storage on connection:', error); + this.logger.log({ level: LogLevels.error, message: 'Error syncing storage on connection:', error }); }); } } diff --git a/packages/common/src/attachments/AttachmentService.ts b/packages/common/src/attachments/AttachmentService.ts index 2c7ef344b..482ed0dd6 100644 --- a/packages/common/src/attachments/AttachmentService.ts +++ b/packages/common/src/attachments/AttachmentService.ts @@ -28,7 +28,7 @@ export class AttachmentService { * @returns Watch query that emits changes for queued uploads, downloads, and deletes */ watchActiveAttachments({ throttleMs }: { throttleMs?: number } = {}): DifferentialWatchedQuery { - this.logger.log(LogLevels.info, 'Watching active attachments...'); + this.logger.log({ level: LogLevels.info, message: 'Watching active attachments...' }); const watch = this.db .query({ sql: /* sql */ ` diff --git a/packages/common/src/attachments/SyncingService.ts b/packages/common/src/attachments/SyncingService.ts index 8e2cd594f..2a30ddeda 100644 --- a/packages/common/src/attachments/SyncingService.ts +++ b/packages/common/src/attachments/SyncingService.ts @@ -75,7 +75,7 @@ export class SyncingService { * @throws Error if the attachment has no localUri */ async uploadAttachment(attachment: AttachmentRecord): Promise { - this.logger.log(LogLevels.info, `Uploading attachment ${attachment.filename}`); + this.logger.log({ level: LogLevels.info, message: `Uploading attachment ${attachment.filename}` }); try { if (attachment.localUri == null) { throw new Error(`No localUri for attachment ${attachment.id}`); @@ -111,7 +111,7 @@ export class SyncingService { * @returns Updated attachment record with local URI and new state */ async downloadAttachment(attachment: AttachmentRecord): Promise { - this.logger.log(LogLevels.info, `Downloading attachment ${attachment.filename}`); + this.logger.log({ level: LogLevels.info, message: `Downloading attachment ${attachment.filename}` }); try { const fileData = await this.remoteStorage.downloadFile(attachment); @@ -183,7 +183,11 @@ export class SyncingService { try { await this.localStorage.deleteFile(attachment.localUri); } catch (error) { - this.logger.log(LogLevels.error, 'Error deleting local file for archived attachment', error); + this.logger.log({ + level: LogLevels.error, + message: 'Error deleting local file for archived attachment', + error + }); } } } diff --git a/packages/common/src/client/AbstractPowerSyncDatabase.ts b/packages/common/src/client/AbstractPowerSyncDatabase.ts index e5c6b2f10..ce62d28d0 100644 --- a/packages/common/src/client/AbstractPowerSyncDatabase.ts +++ b/packages/common/src/client/AbstractPowerSyncDatabase.ts @@ -496,7 +496,11 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver this.logger.log(LogLevels.error, e) } = handler ?? {}; + const { + onResult, + onError = (e: Error) => this.logger.log({ level: LogLevels.error, message: 'Error in watch', error: e }) + } = handler ?? {}; if (!onResult) { throw new Error('onResult is required'); } @@ -1241,7 +1248,10 @@ SELECT * FROM crud_entries; * @returns A dispose function to stop watching for changes */ onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void { - const { onChange, onError = (e: Error) => this.logger.log(LogLevels.error, e) } = handler ?? {}; + const { + onChange, + onError = (e: Error) => this.logger.log({ level: LogLevels.error, message: 'error in onChange', error: e }) + } = handler ?? {}; if (!onChange) { throw new Error('onChange is required'); } diff --git a/packages/common/src/client/ConnectionManager.ts b/packages/common/src/client/ConnectionManager.ts index f1cfa7774..d25d3ecf4 100644 --- a/packages/common/src/client/ConnectionManager.ts +++ b/packages/common/src/client/ConnectionManager.ts @@ -194,10 +194,10 @@ export class ConnectionManager extends BaseObserver { this.syncStreamInitPromise = new Promise(async (resolve, reject) => { try { if (!this.pendingConnectionOptions) { - this.logger.log( - LogLevels.debug, - 'No pending connection options found, not creating sync stream implementation' - ); + this.logger.log({ + level: LogLevels.debug, + message: 'No pending connection options found, not creating sync stream implementation' + }); // A disconnect could have cleared this. resolve(); return; @@ -239,7 +239,7 @@ export class ConnectionManager extends BaseObserver { // and this point. Awaiting here allows the sync stream to be cleared if disconnected. await this.disconnectingPromise; - this.logger.log(LogLevels.debug, 'Attempting to connect to PowerSync instance'); + this.logger.log({ level: LogLevels.debug, message: 'Attempting to connect to PowerSync instance' }); await this.syncStreamImplementation?.connect(appliedOptions!); } @@ -398,9 +398,9 @@ class SyncStreamSubscriptionHandle implements SyncStreamSubscription { const _finalizer = 'FinalizationRegistry' in globalThis ? new FinalizationRegistry((sub) => { - sub.logger.log( - LogLevels.warn, - `A subscription to ${sub.name} with params ${JSON.stringify(sub.parameters)} leaked! Please ensure calling unsubscribe() when you don't need a subscription anymore. For global subscriptions, consider storing them in global fields to avoid this warning.` - ); + sub.logger.log({ + level: LogLevels.warn, + message: `A subscription to ${sub.name} with params ${JSON.stringify(sub.parameters)} leaked! Please ensure calling unsubscribe() when you don't need a subscription anymore. For global subscriptions, consider storing them in global fields to avoid this warning.` + }); }) : null; diff --git a/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts b/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts index 2334acf9c..341212d48 100644 --- a/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts +++ b/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts @@ -84,10 +84,10 @@ export class SqliteBucketStorage extends BaseObserver imp const anyData = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1'); if (anyData.rows?.length) { // if isNotEmpty - this.logger.log( - LogLevels.debug, - `New data uploaded since write checkpoint ${opId} - need new write checkpoint` - ); + this.logger.log({ + level: LogLevels.debug, + message: `New data uploaded since write checkpoint ${opId} - need new write checkpoint` + }); return false; } @@ -99,16 +99,19 @@ export class SqliteBucketStorage extends BaseObserver imp const seqAfter: number = rs.rows?.item(0)['seq']; if (seqAfter != seqBefore) { - this.logger.log( - LogLevels.debug, - `New data uploaded since write checpoint ${opId} - need new write checkpoint (sequence updated)` - ); + this.logger.log({ + level: LogLevels.debug, + message: `New data uploaded since write checpoint ${opId} - need new write checkpoint (sequence updated)` + }); // New crud data may have been uploaded since we got the checkpoint. Abort. return false; } - this.logger.log(LogLevels.debug, `Updating target write checkpoint to ${opId}`); + this.logger.log({ + level: LogLevels.debug, + message: `Updating target write checkpoint to ${opId}` + }); await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [opId]); return true; }); diff --git a/packages/common/src/client/sync/stream/AbstractRemote.ts b/packages/common/src/client/sync/stream/AbstractRemote.ts index 7e333f937..34deeedfd 100644 --- a/packages/common/src/client/sync/stream/AbstractRemote.ts +++ b/packages/common/src/client/sync/stream/AbstractRemote.ts @@ -332,10 +332,10 @@ export abstract class AbstractRemote { const resetTimeout = () => { clearTimeout(keepAliveTimeout); keepAliveTimeout = setTimeout(() => { - this.logger.log( - LogLevels.error, - `No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.` - ); + this.logger.log({ + level: LogLevels.error, + message: `No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.` + }); abortRequest(); }, SOCKET_TIMEOUT_MS); }; @@ -373,7 +373,7 @@ export abstract class AbstractRemote { // The connection is established, we no longer need to monitor the initial timeout pendingSocket = null; } catch (ex) { - this.logger.log(LogLevels.error, `Failed to connect WebSocket`, ex); + this.logger.log({ level: LogLevels.error, message: `Failed to connect WebSocket`, error: ex }); abortRequest(); throw ex; @@ -434,7 +434,7 @@ export abstract class AbstractRemote { // Don't log closed as an error if (e.message !== 'Closed. ') { - this.logger.log(LogLevels.error, e); + this.logger.log({ level: LogLevels.error, message: 'RSocket error', error: e }); } // RSocket will close the RSocket stream automatically // Close the downstream stream as well - this will close the RSocket connection and WebSocket @@ -546,10 +546,10 @@ export abstract class AbstractRemote { if (!res.ok || !res.body) { const text = await res.text(); - this.logger.log( - LogLevels.error, - `Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}` - ); + this.logger.log({ + level: LogLevels.error, + message: `Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}` + }); const error: any = new Error(`HTTP ${res.statusText}: ${text}`); error.status = res.status; throw error; diff --git a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index ecc7161a0..e2f0d04f9 100644 --- a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -309,7 +309,7 @@ export abstract class AbstractStreamingSyncImplementation let path = `/write-checkpoint2.json?client_id=${clientId}`; const response = await this.options.remote.get(path); const checkpoint = response['data']['write_checkpoint'] as string; - this.logger.log(LogLevels.debug, `Created write checkpoint: ${checkpoint}`); + this.logger.log({ level: LogLevels.debug, message: `Created write checkpoint: ${checkpoint}` }); return checkpoint; } @@ -349,12 +349,13 @@ export abstract class AbstractStreamingSyncImplementation if (nextCrudItem.clientId == checkedCrudItem?.clientId) { // This will force a higher log level than exceptions which are caught here. - this.logger.log( - LogLevels.warn, - `Potentially previously uploaded CRUD entries are still present in the upload queue. + this.logger.log({ + level: LogLevels.warn, + message: `Potentially previously uploaded CRUD entries are still present in the upload queue. Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their [.complete()] method. The next upload iteration will be delayed.` - ); + }); + throw new Error('Delaying due to previously encountered CRUD item.'); } @@ -372,7 +373,7 @@ The next upload iteration will be delayed.` this.notifyCompletedUploads?.(); } else if (checkedCrudItem != null) { // Only log this if there was something to upload - this.logger.log(LogLevels.debug, 'Upload complete, no write checkpoint needed.'); + this.logger.log({ level: LogLevels.debug, message: 'Upload complete, no write checkpoint needed.' }); } break; } @@ -389,10 +390,10 @@ The next upload iteration will be delayed.` // Exit the upload loop if the sync stream is no longer connected break; } - this.logger.log( - LogLevels.debug, - `Caught exception when uploading. Upload will retry after a delay. Exception: ${(ex as Error).message}` - ); + this.logger.log({ + level: LogLevels.debug, + message: `Caught exception when uploading. Upload will retry after a delay. Exception: ${(ex as Error).message}` + }); } finally { this.updateSyncStatus({ dataFlow: { @@ -424,7 +425,10 @@ The next upload iteration will be delayed.` const disposer = this.registerListener({ statusChanged: (status) => { if (status.dataFlowStatus.downloadError != null) { - this.logger.log(LogLevels.warn, 'Initial connect attempt did not successfully connect to server'); + this.logger.log({ + level: LogLevels.warn, + message: 'Initial connect attempt did not successfully connect to server' + }); } else if (status.connecting) { // Still connecting. return; @@ -452,7 +456,7 @@ The next upload iteration will be delayed.` await this.streamingSyncPromise; } catch (ex) { // The operation might have failed, all we care about is if it has completed - this.logger.log(LogLevels.warn, ex); + this.logger.log({ level: LogLevels.warn, message: 'Error in sync while disconnecting', error: ex }); } this.streamingSyncPromise = undefined; @@ -521,15 +525,18 @@ The next upload iteration will be delayed.` */ if (ex instanceof AbortOperation) { - this.logger.log(LogLevels.warn, ex); + this.logger.log({ level: LogLevels.warn, message: 'Sync aborted', error: ex }); shouldDelayRetry = false; // A disconnect was requested, we should not delay since there is no explicit retry } else if (this.connectionMayHaveChanged && (ex as Error).message?.indexOf('No iteration is active') >= 0) { this.connectionMayHaveChanged = false; - this.logger.log(LogLevels.info, 'Sync error after changed connection, retrying immediately'); + this.logger.log({ + level: LogLevels.info, + message: 'Sync error after changed connection, retrying immediately' + }); shouldDelayRetry = false; } else { - this.logger.log(LogLevels.error, ex); + this.logger.log({ level: LogLevels.error, message: 'Sync error', error: ex }); } this.updateSyncStatus({ @@ -722,14 +729,12 @@ The next upload iteration will be delayed.` payload?: Uint8Array | string ): Promise { const rawResponse = await adapter.control(op, payload ?? null); - const logger = syncImplementation.logger; - logger.log( - LogLevels.trace, - 'powersync_control', - op, - payload == null || typeof payload == 'string' ? payload : '', - rawResponse - ); + const payloadDesc = payload == null || typeof payload == 'string' ? payload : ''; + + syncImplementation.logger.log({ + level: LogLevels.trace, + message: `powersync_control(${op}, ${payloadDesc}) -> ${rawResponse}` + }); if (op != PowerSyncControlCommand.STOP) { // Evidently we have a working connection here, otherwise powersync_control would have failed. @@ -741,15 +746,26 @@ The next upload iteration will be delayed.` async function handleInstruction(instruction: NonInterruptingInstruction) { if ('LogLine' in instruction) { - switch (instruction.LogLine.severity) { + const { severity, line } = instruction.LogLine; + + switch (severity) { case 'DEBUG': - syncImplementation.logger.log(LogLevels.debug, instruction.LogLine.line); + syncImplementation.logger.log({ + level: LogLevels.debug, + message: line + }); break; case 'INFO': - syncImplementation.logger.log(LogLevels.info, instruction.LogLine.line); + syncImplementation.logger.log({ + level: LogLevels.info, + message: line + }); break; case 'WARNING': - syncImplementation.logger.log(LogLevels.warn, instruction.LogLine.line); + syncImplementation.logger.log({ + level: LogLevels.warn, + message: line + }); break; } } else if ('UpdateSyncStatus' in instruction) { @@ -766,7 +782,11 @@ The next upload iteration will be delayed.` notifyTokenRefreshed?.(); }, (err) => { - syncImplementation.logger.log(LogLevels.warn, 'Could not prefetch credentials', err); + syncImplementation.logger.log({ + level: LogLevels.warn, + message: 'Could not prefetch credentials', + error: err + }); } ); } diff --git a/packages/common/src/client/triggers/TriggerManagerImpl.ts b/packages/common/src/client/triggers/TriggerManagerImpl.ts index 5c76efa72..278319ee7 100644 --- a/packages/common/src/client/triggers/TriggerManagerImpl.ts +++ b/packages/common/src/client/triggers/TriggerManagerImpl.ts @@ -81,8 +81,12 @@ export class TriggerManagerImpl implements TriggerManager { } try { await this.cleanupResources(); - } catch (ex) { - this.db.logger.log(LogLevels.error, `Caught error while attempting to cleanup triggers`, ex); + } catch (error) { + this.db.logger.log({ + level: LogLevels.error, + error, + message: `Caught error while attempting to cleanup triggers` + }); } finally { // if not closed, set another timeout if (this.isDisposed) { @@ -184,10 +188,10 @@ export class TriggerManagerImpl implements TriggerManager { continue; } - this.db.logger.log( - LogLevels.debug, - `Clearing resources for trigger ${trackedItem.id} with table ${trackedItem.table}` - ); + this.db.logger.log({ + level: LogLevels.debug, + message: `Clearing resources for trigger ${trackedItem.id} with table ${trackedItem.table}` + }); // We need to delete the triggers and table for (const triggerName of trackedItem.triggerNames) { @@ -263,10 +267,10 @@ export class TriggerManagerImpl implements TriggerManager { const disposeWarningListener = this.db.registerListener({ schemaChanged: () => { - this.db.logger.log( - LogLevels.warn, - `The PowerSync schema has changed while previously configured triggers are still operational. This might cause unexpected results.` - ); + this.db.logger.log({ + level: LogLevels.warn, + message: `The PowerSync schema has changed while previously configured triggers are still operational. This might cause unexpected results.` + }); } }); diff --git a/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts b/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts index 7c07843f8..b711605da 100644 --- a/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts +++ b/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts @@ -220,7 +220,11 @@ export abstract class AbstractQueryProcessor< } catch (error) { // Errors here are ignored // since we are already in an error state - this.options.db.logger.log(LogLevels.error, 'Watched query error handler threw an Error', error); + this.options.db.logger.log({ + level: LogLevels.error, + message: 'Watched query error handler threw an Error', + error + }); } } } diff --git a/packages/common/src/utils/Logger.ts b/packages/common/src/utils/Logger.ts index 711d2c1e4..e60c989fc 100644 --- a/packages/common/src/utils/Logger.ts +++ b/packages/common/src/utils/Logger.ts @@ -6,6 +6,34 @@ export const LogLevels = { error: 50 } as const; +/** + * A log record passed to a {@link PowerSyncLogger}. + */ +export interface LogRecord { + /** + * The log level (see {@link LogLevels} for preconfigured values) for the message. Depending on how a receiving logger + * has been confired, messages below a configured minimum level may be ignored. + */ + level: number; + /** + * The main message to log. + */ + message: string; + + /** + * The (optional) component within the PowerSync SDK logging the message. + */ + tag?: string; + + /** + * When the log message contains an error, the error causing the log. + * + * This is not guaranteed to be an `Error` instance. On the web, we might have to serialize objects across message + * channels and represent them as a string. + */ + error?: unknown; +} + /** * A logger used by the PowerSync SDK. * @@ -16,14 +44,7 @@ export const LogLevels = { * By default, the SDK uses a {@link createPowerSyncLogger} instance forwarding messages to `console.log`. */ export interface PowerSyncLogger { - /** - * Log a message. - * - * @param level The log level (see {@link LogLevels} for preconfigured values) for the message. Depending on how the - * logger has been confired, messages below a configured minimum level may be ignored. - * @param message Components of the message to log. Components aren't necessarily strings. - */ - log(level: number, ...message: any[]): void; + log(record: LogRecord): void; } export interface CreateLoggerOptions { @@ -51,7 +72,7 @@ export function createPowerSyncLogger(options?: Partial): P return { prefix, minLevel, - log(level, ...message) { + log({ level, message, tag, error }) { if (level < this.minLevel) return; let emitter = console.log; @@ -61,7 +82,14 @@ export function createPowerSyncLogger(options?: Partial): P emitter = console.warn; } - emitter(this.prefix, ...message); + let resolvedPrefix = tag != null ? `${prefix}.${tag}` : prefix; + const messageWithPrefix = `[${resolvedPrefix}]: ${message}`; + + if (error) { + emitter(messageWithPrefix, error); + } else { + emitter(messageWithPrefix); + } } }; } diff --git a/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts b/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts index 6346d236d..1e6f4a022 100644 --- a/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts +++ b/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts @@ -34,25 +34,22 @@ export const useDiagnosticsLogger = (additional?: PowerSyncLogger) => { const consoleLogger = createPowerSyncLogger({ minLevel: LogLevels.debug }); const logger: PowerSyncLogger = { - async log(level, ...messages) { - consoleLogger.log(level, ...messages); + async log(record) { + consoleLogger.log(record); - // Storage + emitter - const messageArray = Array.from(messages); - const mainMessage = String(messageArray[0] ?? 'Empty log message'); // Store extra args as-is so objects are shown as JSON in LogsTab - const extra = - messageArray.length > 1 ? (messageArray.length === 2 ? messageArray[1] : messageArray.slice(1)) : undefined; const logObject = { date: new Date(), - args: [mainMessage, extra] + tag: record.tag, + message: record.message, + error: record.error }; const key = `log:${logObject.date.toISOString()}`; await logsStorage.set(key, logObject); emitter.emit('log', { key, value: logObject }); // User callback - additional?.log(level, ...messages); + additional?.log(record); } }; diff --git a/packages/nuxt/src/runtime/utils/NuxtPowerSyncDatabase.ts b/packages/nuxt/src/runtime/utils/NuxtPowerSyncDatabase.ts index fd20dc70c..2b3634ba2 100644 --- a/packages/nuxt/src/runtime/utils/NuxtPowerSyncDatabase.ts +++ b/packages/nuxt/src/runtime/utils/NuxtPowerSyncDatabase.ts @@ -1,5 +1,4 @@ import { - DEFAULT_SYNC_CLIENT_IMPLEMENTATION, PowerSyncDatabase, Schema, SharedWebStreamingSyncImplementation, @@ -11,7 +10,8 @@ import { type RequiredAdditionalConnectionOptions, type StreamingSyncImplementation, type WebPowerSyncDatabaseOptions, - type WebDBAdapter + type WebDBAdapter, + LogLevels } from '@powersync/web'; import type { DynamicSchemaManager } from './DynamicSchemaManager'; import { usePowerSyncInspector } from '../composables/usePowerSyncInspector'; @@ -116,7 +116,7 @@ export class NuxtPowerSyncDatabase extends PowerSyncDatabase { Multiple tabs are enabled, but broadcasting of logs is disabled. Logs for shared sync worker will only be available in the shared worker context `; - logger ? logger.warn(warning) : console.warn(warning); + logger ? logger.log({ level: LogLevels.warn, message: warning }) : console.warn(warning); } return new SharedWebStreamingSyncImplementation({ ...options, @@ -127,7 +127,8 @@ export class NuxtPowerSyncDatabase extends PowerSyncDatabase { await connector.uploadData(this); }, logger, - db: this.database as WebDBAdapter + db: this.database as WebDBAdapter, + logLevel: this.options.flags.databaseWorkerLogLevel ?? LogLevels.info }); } else { return new WebStreamingSyncImplementation({ diff --git a/packages/vue/src/composables/useSingleQuery.ts b/packages/vue/src/composables/useSingleQuery.ts index 24329d479..49589ffc4 100644 --- a/packages/vue/src/composables/useSingleQuery.ts +++ b/packages/vue/src/composables/useSingleQuery.ts @@ -130,9 +130,9 @@ export const useSingleQuery = ( const queryValue = toValue(query); try { parsedQuery = parseQuery(queryValue, toValue(sqlParameters).map(toValue)); - } catch (e) { - logger?.log(LogLevels.error, 'Failed to parse query:', e); - handleError(e); + } catch (error) { + logger?.log({ level: LogLevels.error, message: 'Failed to parse query:', error }); + handleError(error); return; } @@ -146,9 +146,9 @@ export const useSingleQuery = ( try { const result = await executor(); handleResult(result); - } catch (e) { - logger?.log(LogLevels.error, 'Failed to fetch data:', e); - handleError(e); + } catch (error) { + logger?.log({ level: LogLevels.error, message: 'Failed to fetch data:', error }); + handleError(error); } }; diff --git a/packages/vue/src/composables/useWatchedQuery.ts b/packages/vue/src/composables/useWatchedQuery.ts index d891ee6b6..2d1a3c80c 100644 --- a/packages/vue/src/composables/useWatchedQuery.ts +++ b/packages/vue/src/composables/useWatchedQuery.ts @@ -46,9 +46,9 @@ export const useWatchedQuery = ( const queryValue = toValue(query); try { parsedQuery = parseQuery(queryValue, toValue(sqlParameters).map(toValue)); - } catch (e: any) { - logger.log(LogLevels.error, 'Failed to parse query:', e); - handleError(e); + } catch (error) { + logger.log({ level: LogLevels.error, message: 'Failed to parse query:', error }); + handleError(error); return; } diff --git a/packages/web/src/db/PowerSyncDatabase.ts b/packages/web/src/db/PowerSyncDatabase.ts index 7311a4324..91d9fbb87 100644 --- a/packages/web/src/db/PowerSyncDatabase.ts +++ b/packages/web/src/db/PowerSyncDatabase.ts @@ -264,7 +264,7 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase { Logs for shared sync worker will only be available in the shared worker context `; const logger = this.options.logger; - logger ? logger.log(LogLevels.warn, warning) : console.warn(warning); + logger ? logger.log({ level: LogLevels.warn, message: warning }) : console.warn(warning); } return new SharedWebStreamingSyncImplementation({ ...syncOptions, diff --git a/packages/web/src/db/adapters/wa-sqlite/DatabaseServer.ts b/packages/web/src/db/adapters/wa-sqlite/DatabaseServer.ts index 1d696c2bc..192f79001 100644 --- a/packages/web/src/db/adapters/wa-sqlite/DatabaseServer.ts +++ b/packages/web/src/db/adapters/wa-sqlite/DatabaseServer.ts @@ -85,7 +85,7 @@ export class DatabaseServer { // If the client holds a connection lease it hasn't returned, return that now. for (const { lease } of connectionLeases.values()) { - this.#logger.log(LogLevels.debug, `Closing connection lease that hasn't been returned.`); + this.#logger.log({ level: LogLevels.debug, message: `Closing connection lease that hasn't been returned.` }); await lease.returnLease(); } @@ -94,7 +94,10 @@ export class DatabaseServer { if (this.#activeClients.size == 0) { await this.forceClose(); } else { - this.#logger.log(LogLevels.debug, 'Keeping underlying connection active since its used by other clients.'); + this.#logger.log({ + level: LogLevels.debug, + message: 'Keeping underlying connection active since its used by other clients.' + }); } } }; @@ -169,7 +172,7 @@ export class DatabaseServer { } async forceClose() { - this.#logger.log(LogLevels.debug, `Closing connection to ${this.#inner.options}.`); + this.#logger.log({ level: LogLevels.debug, message: `Closing connection to ${this.#inner.options}.` }); const connection = this.#inner; this.#options.onClose(); this.#updateBroadcastChannel.close(); diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts index eb0647044..9ed24eae4 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts @@ -84,23 +84,23 @@ export class WASQLiteOpenFactory implements SQLOpenFactory { } = this; if (ssrMode) { if (!disableSSRWarning) { - this.logger.log( - LogLevels.warn, - ` + this.logger.log({ + level: LogLevels.warn, + message: ` Running PowerSync in SSR mode. Only empty query results will be returned. Disable this warning by setting 'disableSSRWarning: true' in options.` - ); + }); } return new SSRDBAdapter(); } if (!enableMultiTabs) { - this.logger.log( - LogLevels.warn, - 'Multiple tab support is not enabled. Using this site across multiple tabs may not function correctly.' - ); + this.logger.log({ + level: LogLevels.warn, + message: 'Multiple tab support is not enabled. Using this site across multiple tabs may not function correctly.' + }); } return this.openAdapter(); @@ -116,7 +116,7 @@ export class WASQLiteOpenFactory implements SQLOpenFactory { } = this.waOptions; if (!enableMultiTabs) { - this.logger.log(LogLevels.warn, 'Multiple tabs are not enabled in this browser'); + this.logger.log({ level: LogLevels.warn, message: 'Multiple tabs are not enabled in this browser' }); } const resolveOptions = (isReadOnly: boolean): ResolvedWASQLiteOpenFactoryOptions => ({ diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index 4eb0a496c..ae06c9c7a 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -1,4 +1,5 @@ import { + LogRecord, PowerSyncConnectionOptions, PowerSyncCredentials, SubscribedStream, @@ -67,8 +68,8 @@ class SharedSyncClientProvider extends AbstractSharedSyncClientProvider { return this.options.logger; } - log(level: number, ...message: any[]): void { - this.logger?.log(level, ...message); + log(record: LogRecord): void { + this.logger.log(record); } } diff --git a/packages/web/src/db/sync/WebRemote.ts b/packages/web/src/db/sync/WebRemote.ts index 077a2daed..aee30ea75 100644 --- a/packages/web/src/db/sync/WebRemote.ts +++ b/packages/web/src/db/sync/WebRemote.ts @@ -35,8 +35,8 @@ export class WebRemote extends AbstractRemote { let ua = [super.getUserAgent(), `powersync-web`]; try { ua.push(...getUserAgentInfo()); - } catch (e) { - this.logger.log(LogLevels.warn, 'Failed to get user agent info', e); + } catch (error) { + this.logger.log({ level: LogLevels.warn, message: 'Failed to get user agent info', error }); } return ua.join(' '); } diff --git a/packages/web/src/db/sync/WebStreamingSyncImplementation.ts b/packages/web/src/db/sync/WebStreamingSyncImplementation.ts index 260b8e7d7..1365a4685 100644 --- a/packages/web/src/db/sync/WebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/WebStreamingSyncImplementation.ts @@ -28,7 +28,7 @@ export class WebStreamingSyncImplementation extends AbstractStreamingSyncImpleme async obtainLock(lockOptions: LockOptions): Promise { const identifier = `streaming-sync-${lockOptions.type}-${this.webOptions.identifier}`; if (lockOptions.type == LockType.SYNC) { - this.logger.log(LogLevels.debug, 'requesting lock for ', identifier); + this.logger.log({ level: LogLevels.debug, message: `requesting lock for ${identifier}` }); } return getNavigatorLocks().request(identifier, { signal: lockOptions.signal }, lockOptions.callback); } diff --git a/packages/web/src/worker/db/MultiDatabaseServer.ts b/packages/web/src/worker/db/MultiDatabaseServer.ts index dafa38e10..7047c4044 100644 --- a/packages/web/src/worker/db/MultiDatabaseServer.ts +++ b/packages/web/src/worker/db/MultiDatabaseServer.ts @@ -21,8 +21,8 @@ export class MultiDatabaseServer { async handleConnection(options: WorkerDBOpenerOptions): Promise { const logger: PowerSyncLogger = { - log: (level, ...message) => { - if (level >= options.logLevel) logger.log(level, message); + log: (record) => { + if (record.level >= options.logLevel) logger.log(record); } }; @@ -49,12 +49,12 @@ export class MultiDatabaseServer { for (let count = 0; count < maxAttempts - 1; count++) { try { server = await this.databaseOpenAttempt(logger, options); - } catch (ex) { - this.logger.log( - LogLevels.warn, - `Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, - ex - ); + } catch (error) { + this.logger.log({ + level: LogLevels.warn, + message: `Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, + error + }); await new Promise((resolve) => setTimeout(resolve, 1000)); } } diff --git a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts b/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts index 3f740efba..be4dfabc5 100644 --- a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts +++ b/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts @@ -1,4 +1,4 @@ -import type { PowerSyncCredentials, SyncStatusOptions } from '@powersync/common'; +import type { LogRecord, PowerSyncCredentials, SyncStatusOptions } from '@powersync/common'; /** * The client side port should provide these methods. @@ -10,5 +10,5 @@ export abstract class AbstractSharedSyncClientProvider { abstract statusChanged(status: SyncStatusOptions): void; abstract getDBWorkerPort(): Promise; - abstract log(level: number, ...message: any[]): void; + abstract log(record: LogRecord): void; } diff --git a/packages/web/src/worker/sync/BroadcastLogger.ts b/packages/web/src/worker/sync/BroadcastLogger.ts index d5701223a..e3e2d4a20 100644 --- a/packages/web/src/worker/sync/BroadcastLogger.ts +++ b/packages/web/src/worker/sync/BroadcastLogger.ts @@ -1,4 +1,4 @@ -import { PowerSyncLogger, LogLevels, CreateLoggerOptions, createPowerSyncLogger } from '@powersync/common'; +import { PowerSyncLogger, LogLevels, CreateLoggerOptions, createPowerSyncLogger, LogRecord } from '@powersync/common'; import { type WrappedSyncPort } from './SharedSyncImplementation.js'; /** @@ -17,12 +17,12 @@ export class BroadcastLogger implements PowerSyncLogger { this.inner = createPowerSyncLogger({ prefix: prefix }); } - log(level: number, ...message: any[]) { - this.inner.log(level, ...message); + log(record: LogRecord) { + this.inner.log(record); - if (this.sendBroadcasts && level >= this.currentLevel) { - const sanitized = this.sanitizeArgs(message); - this.iterateClients((client) => client.clientProvider.log(level, ...sanitized)); + if (this.sendBroadcasts && record.level >= this.currentLevel) { + const sanitized = this.sanitizeRecord(record); + this.iterateClients((client) => client.clientProvider.log(sanitized)); } } @@ -52,17 +52,23 @@ export class BroadcastLogger implements PowerSyncLogger { * Guards against any logging errors. * We don't want a logging exception to cause further issues upstream */ - protected sanitizeArgs(x: any[]): any[] { - const sanitizedParams = x.map((param) => { - try { - // Try and clone here first. If it fails it won't be passable over a MessagePort - return structuredClone(param); - } catch (ex) { - console.error(ex); - return 'Could not serialize log params. Check shared worker logs for more details.'; - } - }); + protected sanitizeRecord(record: LogRecord): LogRecord { + if (!record.error) { + return record; + } + + let error; + try { + // Try and clone here first. If it fails it won't be passable over a MessagePort + error = structuredClone(record.error); + } catch (ex) { + console.error(ex); + error = 'Could not serialize log params. Check shared worker logs for more details.'; + } - return sanitizedParams; + return { + ...record, + error + }; } } diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 822c9b38b..a7f09571d 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -211,7 +211,7 @@ export class SharedSyncImplementation extends BaseObserver(); for (const port of this.ports) { for (const stream of port.currentSubscriptions) { @@ -220,7 +220,10 @@ export class SharedSyncImplementation extends BaseObserver { // Share any uncaught events on the broadcast logger - this.logger.log(LogLevels.error, 'Uncaught exception in PowerSync shared sync worker', event); + this.logger.log({ + level: LogLevels.error, + message: 'Uncaught exception in PowerSync shared sync worker', + error: event + }); }; this.iterateListeners((l) => l.initialized?.()); @@ -320,7 +327,10 @@ export class SharedSyncImplementation extends BaseObserver { const index = this.ports.findIndex((p) => p == port); if (index < 0) { - this.logger.log(LogLevels.warn, `Could not remove port ${port} since it is not present in active ports.`); + this.logger.log({ + level: LogLevels.warn, + message: `Could not remove port ${port} since it is not present in active ports.` + }); return () => {}; } @@ -396,10 +406,13 @@ export class SharedSyncImplementation extends BaseObserver { @@ -416,7 +429,10 @@ export class SharedSyncImplementation extends BaseObserver { - this.logger.log(LogLevels.info, 'Aborting open connection because associated tab closed.'); + this.logger.log({ level: LogLevels.info, message: 'Aborting open connection because associated tab closed.' }); handleClosed(db); /** * Don't await this close operation. It might never resolve if the tab is closed. @@ -549,7 +565,9 @@ export class SharedSyncImplementation extends BaseObserver this.logger.log(LogLevels.warn, 'error closing database connection', ex)); + db.close().catch((error) => + this.logger.log({ level: LogLevels.warn, message: 'error closing database connection', error }) + ); }); return db; } diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index bfb0d9d73..d3afd862e 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -48,8 +48,8 @@ describe('Multiple Instances', { sequential: true }, () => { async ({ context: { openDatabase, mockService } }) => { const logLines: string[] = []; const logger: PowerSyncLogger = { - log(_level, ...message) { - logLines.push(message[0]); + log({ message }) { + logLines.push(message); } }; diff --git a/packages/web/tests/src/db/PowersyncDatabase.test.ts b/packages/web/tests/src/db/PowersyncDatabase.test.ts index 74ef67f65..b4793b5fd 100644 --- a/packages/web/tests/src/db/PowersyncDatabase.test.ts +++ b/packages/web/tests/src/db/PowersyncDatabase.test.ts @@ -1,11 +1,11 @@ -import { LogLevels, PowerSyncDatabase } from '@powersync/web'; +import { LogLevels, LogRecord, PowerSyncDatabase } from '@powersync/web'; import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { TEST_SCHEMA } from '../../utils/test-schema.js'; describe('PowerSyncDatabase', () => { let db: PowerSyncDatabase; let mockConnector: any; - let mockLogger: Mock<(level: number, ...mesage: any[]) => void>; + let mockLogger: Mock<(record: LogRecord) => void>; beforeEach(() => { mockLogger = vi.fn(); @@ -27,7 +27,10 @@ describe('PowerSyncDatabase', () => { describe('connect', () => { it('should log debug message when attempting to connect', async () => { await db.connect(mockConnector); - expect(mockLogger).toHaveBeenCalledWith(LogLevels.debug, 'Attempting to connect to PowerSync instance'); + expect(mockLogger).toHaveBeenCalledWith({ + level: LogLevels.debug, + message: 'Attempting to connect to PowerSync instance' + }); }); }); }); diff --git a/packages/web/tests/uploads.test.ts b/packages/web/tests/uploads.test.ts index c6cc4891f..29b24be8f 100644 --- a/packages/web/tests/uploads.test.ts +++ b/packages/web/tests/uploads.test.ts @@ -46,8 +46,8 @@ function describeCrudUploadTests(getDatabaseOptions: () => WebPowerSyncDatabaseO const lines: string[] = []; logLines = lines; logger = { - log(_level, ...message) { - lines.push(message[0] as string); + log({ message }) { + lines.push(message); } }; }); From 39cced0542d19c9af64c201439d1aa266e2f0892 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 15 May 2026 14:22:49 +0200 Subject: [PATCH 07/21] Fix RNQS build --- .../react-native-quick-sqlite/RNQSDBOpenFactory.ts | 8 ++++---- .../react-native/src/sync/stream/ReactNativeRemote.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts b/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts index 3dbf11c41..3b5c8a17a 100644 --- a/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts +++ b/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts @@ -37,10 +37,10 @@ export class RNQSPowerSyncDatabaseOpenFactory extends AbstractPowerSyncDatabaseO generateInstance(options: PowerSyncDatabaseOptions): AbstractPowerSyncDatabase { if (this.instanceGenerated) { - this.options.logger?.log( - LogLevels.warn, - 'Generating multiple PowerSync instances can sometimes cause unexpected results.' - ); + this.options.logger?.log({ + level: LogLevels.warn, + message: 'Generating multiple PowerSync instances can sometimes cause unexpected results.' + }); } this.instanceGenerated = true; return new PowerSyncDatabase(options); diff --git a/packages/react-native/src/sync/stream/ReactNativeRemote.ts b/packages/react-native/src/sync/stream/ReactNativeRemote.ts index aabf653e5..14d2c2f83 100644 --- a/packages/react-native/src/sync/stream/ReactNativeRemote.ts +++ b/packages/react-native/src/sync/stream/ReactNativeRemote.ts @@ -81,12 +81,12 @@ export class ReactNativeRemote extends AbstractRemote { const timeout = Platform.OS == 'android' ? setTimeout(() => { - this.logger.log( - LogLevels.warn, - `HTTP Streaming POST is taking longer than ${Math.ceil( + this.logger.log({ + level: LogLevels.warn, + message: `HTTP Streaming POST is taking longer than ${Math.ceil( STREAMING_POST_TIMEOUT_MS / 1000 )} seconds to resolve. If using a debug build, please ensure Flipper Network plugin is disabled.` - ); + }); }, STREAMING_POST_TIMEOUT_MS) : null; From 0b8743f7b24c6716c060d75911b2742f80bb2d04 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 15 May 2026 14:34:13 +0200 Subject: [PATCH 08/21] Fix attachments tests --- .../tests/attachments/AttachmentQueue.test.ts | 6 +++++- .../src/library/powersync/ConnectionManager.ts | 17 +++++++++-------- .../src/library/powersync/LocalStateManager.ts | 6 +++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/attachments/tests/attachments/AttachmentQueue.test.ts b/packages/attachments/tests/attachments/AttachmentQueue.test.ts index 69e8c1593..c145e1176 100644 --- a/packages/attachments/tests/attachments/AttachmentQueue.test.ts +++ b/packages/attachments/tests/attachments/AttachmentQueue.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AbstractAttachmentQueue } from '../../src/AbstractAttachmentQueue.js'; import { AttachmentRecord, AttachmentState } from '../../src/Schema.js'; import { StorageAdapter } from '../../src/StorageAdapter.js'; +import { PowerSyncLogger } from '@powersync/common'; const record = { id: 'test-1', @@ -24,7 +25,10 @@ const mockPowerSync = { await callback({ execute: vi.fn(() => Promise.resolve()) }); - }) + }), + logger: { + log: vi.fn((record) => {}) + } satisfies PowerSyncLogger }; const mockStorage: StorageAdapter = { diff --git a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts index 835028bbb..5b3ecf3ca 100644 --- a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts +++ b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts @@ -1,8 +1,8 @@ import { BaseListener, - createBaseLogger, + createPowerSyncLogger, DEFAULT_STREAMING_SYNC_OPTIONS, - LogLevel, + LogLevels, PowerSyncDatabase, SyncClientImplementation, SyncStreamSubscription, @@ -19,9 +19,7 @@ import { DynamicSchemaManager } from './DynamicSchemaManager'; import { RustClientInterceptor } from './RustClientInterceptor'; import { TokenConnector } from './TokenConnector'; -const baseLogger = createBaseLogger(); -baseLogger.useDefaults(); -baseLogger.setLevel(LogLevel.DEBUG); +const baseLogger = createPowerSyncLogger({ minLevel: LogLevels.debug }); export type JSONValue = string | number | boolean | null | { [key: string]: JSONValue } | JSONValue[]; @@ -60,7 +58,9 @@ const openFactory = new WASQLiteOpenFactory({ debugMode: true, cacheSizeKb: 500 * 1024, temporaryStorage: TemporaryStorageOption.MEMORY, - vfs: WASQLiteVFS.OPFSCoopSyncVFS + vfs: WASQLiteVFS.OPFSCoopSyncVFS, + logger: baseLogger, + logLevel: LogLevels.info }); export const db = new PowerSyncDatabase({ @@ -128,7 +128,7 @@ export async function connect() { await schemaManager.loadFromDb(); const params = await getParams(); await sync?.disconnect(); - const remote = new WebRemote(connector); + const remote = new WebRemote(connector, baseLogger); const adapter = new RustClientInterceptor(db, remote, schemaManager); const syncOptions: WebStreamingSyncImplementationOptions = { @@ -139,7 +139,8 @@ export async function connect() { }, identifier: 'diagnostics', ...DEFAULT_STREAMING_SYNC_OPTIONS, - subscriptions: [] + subscriptions: [], + logger: baseLogger }; sync = new WebStreamingSyncImplementation(syncOptions); notifySyncChange(); diff --git a/tools/diagnostics-app/src/library/powersync/LocalStateManager.ts b/tools/diagnostics-app/src/library/powersync/LocalStateManager.ts index baf6a5df6..02112c2a4 100644 --- a/tools/diagnostics-app/src/library/powersync/LocalStateManager.ts +++ b/tools/diagnostics-app/src/library/powersync/LocalStateManager.ts @@ -1,5 +1,7 @@ import { column, + createPowerSyncLogger, + LogLevels, PowerSyncDatabase, Schema, Table, @@ -47,7 +49,9 @@ const openFactory = new WASQLiteOpenFactory({ debugMode: true, cacheSizeKb: 100 * 1024, temporaryStorage: TemporaryStorageOption.MEMORY, - vfs: WASQLiteVFS.OPFSCoopSyncVFS + vfs: WASQLiteVFS.OPFSCoopSyncVFS, + logger: createPowerSyncLogger({ prefix: 'database' }), + logLevel: LogLevels.info }); /** From 2a90f28412e9ec57b8089f2eae09e5bd72983743 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 15 May 2026 14:37:04 +0200 Subject: [PATCH 09/21] Fix node tests --- packages/node/tests/sync.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node/tests/sync.test.ts b/packages/node/tests/sync.test.ts index e8e3ee481..1885b977c 100644 --- a/packages/node/tests/sync.test.ts +++ b/packages/node/tests/sync.test.ts @@ -607,9 +607,9 @@ function defineSyncTests(bson: boolean) { mockSyncServiceTest('handles uploads across checkpoints', async ({ syncService }) => { const logMessages: string[] = []; const logger: PowerSyncLogger = { - log(_level, ...message) { - console.log(...message); - logMessages.push(util.format(...message)); + log({ message }) { + console.log(message); + logMessages.push(message); } }; From 5da5097a7edf30fdb94c66d36e21aa1e523983c5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 15 May 2026 15:24:46 +0200 Subject: [PATCH 10/21] Fix web tests --- .../src/client/AbstractPowerSyncDatabase.ts | 3 +-- .../web/src/worker/db/MultiDatabaseServer.ts | 2 +- packages/web/tests/encryption.test.ts | 2 +- packages/web/tests/main.test.ts | 3 ++- packages/web/tests/multiple_instances.test.ts | 21 ++++++++++++------- packages/web/tests/open.test.ts | 2 +- .../tests/src/db/write_ahead_log_opfs.test.ts | 3 ++- packages/web/tests/stream.test.ts | 2 +- packages/web/tests/triggers.test.ts | 3 ++- packages/web/tests/uploads.test.ts | 4 ++-- .../tests/utils/generateConnectedDatabase.ts | 2 +- packages/web/tests/utils/iframeInitializer.ts | 2 +- packages/web/tests/utils/logger.ts | 7 +++++++ packages/web/tests/utils/test-schema.ts | 2 +- packages/web/tests/utils/testDb.ts | 7 +------ .../utils/triggers/IFrameTriggerConfig.ts | 2 +- packages/web/vitest.config.ts | 2 +- 17 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 packages/web/tests/utils/logger.ts diff --git a/packages/common/src/client/AbstractPowerSyncDatabase.ts b/packages/common/src/client/AbstractPowerSyncDatabase.ts index ce62d28d0..eae52244d 100644 --- a/packages/common/src/client/AbstractPowerSyncDatabase.ts +++ b/packages/common/src/client/AbstractPowerSyncDatabase.ts @@ -234,6 +234,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver { const logger: PowerSyncLogger = { log: (record) => { - if (record.level >= options.logLevel) logger.log(record); + if (record.level >= options.logLevel) this.logger.log(record); } }; diff --git a/packages/web/tests/encryption.test.ts b/packages/web/tests/encryption.test.ts index 62f339bda..ce4d32e0b 100644 --- a/packages/web/tests/encryption.test.ts +++ b/packages/web/tests/encryption.test.ts @@ -8,7 +8,7 @@ import { import { v4 as uuid } from 'uuid'; import { describe, expect, it } from 'vitest'; import { TEST_SCHEMA } from './utils/test-schema.js'; -import { defaultLoggerConfig } from './utils/testDb.js'; +import { defaultLoggerConfig } from './utils/logger.js'; describe('Encryption Tests', { sequential: true }, () => { it('IDBBatchAtomicVFS encryption', async () => { diff --git a/packages/web/tests/main.test.ts b/packages/web/tests/main.test.ts index 634ec2783..bc76749b0 100644 --- a/packages/web/tests/main.test.ts +++ b/packages/web/tests/main.test.ts @@ -2,7 +2,8 @@ import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/ import { v4 as uuid } from 'uuid'; import { describe, expect, it } from 'vitest'; import { TEST_SCHEMA, TestDatabase } from './utils/test-schema.js'; -import { defaultLoggerConfig, generateTestDb } from './utils/testDb.js'; +import { generateTestDb } from './utils/testDb.js'; +import { defaultLoggerConfig } from './utils/logger.js'; // TODO import tests from a common package describe( diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index d3afd862e..5c0bf9e06 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -3,6 +3,7 @@ import { createPowerSyncLogger, DBAdapterDefaultMixin, LogLevels, + LogRecord, PowerSyncLogger } from '@powersync/common'; import * as Comlink from 'comlink'; @@ -46,10 +47,10 @@ describe('Multiple Instances', { sequential: true }, () => { 'should broadcast logs from shared sync worker', { timeout: 10_000 }, async ({ context: { openDatabase, mockService } }) => { - const logLines: string[] = []; + const logLines: LogRecord[] = []; const logger: PowerSyncLogger = { - log({ message }) { - logLines.push(message); + log(msg) { + logLines.push(msg); } }; @@ -85,16 +86,22 @@ describe('Multiple Instances', { sequential: true }, () => { // Asserting that powersync_control logs exists verifies that some connection attempt was made. await vi.waitFor( - () => expect(logLines).toEqual(expect.arrayContaining([expect.stringContaining('powersync_control')])), + () => + expect(logLines.map((l) => l.message)).toEqual( + expect.arrayContaining([expect.stringContaining('powersync_control')]) + ), { timeout: 2000 } ); // The connection should fail with an error - await vi.waitFor(() => expect(logLines).toEqual(expect.arrayContaining([expect.any(Error)])), { - timeout: 2000 - }); + await vi.waitFor( + () => expect(logLines.map((l) => l.error)).toEqual(expect.arrayContaining([expect.any(Error)])), + { + timeout: 2000 + } + ); } ); diff --git a/packages/web/tests/open.test.ts b/packages/web/tests/open.test.ts index 646086250..746998adc 100644 --- a/packages/web/tests/open.test.ts +++ b/packages/web/tests/open.test.ts @@ -11,7 +11,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { TEST_SCHEMA } from './utils/test-schema.js'; import { MultiDatabaseServer } from '../src/worker/db/MultiDatabaseServer.js'; import { DatabaseClient } from '../src/db/adapters/wa-sqlite/DatabaseClient.js'; -import { defaultLoggerConfig } from './utils/testDb.js'; +import { defaultLoggerConfig } from './utils/logger.js'; const testId = '2290de4f-0488-4e50-abed-f8e8eb1d0b42'; diff --git a/packages/web/tests/src/db/write_ahead_log_opfs.test.ts b/packages/web/tests/src/db/write_ahead_log_opfs.test.ts index 416fba351..5652699f9 100644 --- a/packages/web/tests/src/db/write_ahead_log_opfs.test.ts +++ b/packages/web/tests/src/db/write_ahead_log_opfs.test.ts @@ -1,7 +1,8 @@ import { expect, test } from 'vitest'; -import { defaultLoggerConfig, generateTestDb } from '../../utils/testDb.js'; +import { generateTestDb } from '../../utils/testDb.js'; import { WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { TEST_SCHEMA } from '../../utils/test-schema.js'; +import { defaultLoggerConfig } from '../../utils/logger.js'; test('supports concurrent reads', async () => { const db = generateTestDb({ diff --git a/packages/web/tests/stream.test.ts b/packages/web/tests/stream.test.ts index cc9be1b86..3d42be3df 100644 --- a/packages/web/tests/stream.test.ts +++ b/packages/web/tests/stream.test.ts @@ -14,7 +14,7 @@ import { describe, expect, it, onTestFinished, vi } from 'vitest'; import { TestConnector } from './utils/MockStreamOpenFactory.js'; import { ConnectedDatabaseUtils, generateConnectedDatabase } from './utils/generateConnectedDatabase.js'; import { BucketChecksum } from '@powersync/common/internal/sync_protocol'; -import { defaultLoggerConfig } from './utils/testDb.js'; +import { defaultLoggerConfig } from './utils/logger.js'; const UPLOAD_TIMEOUT_MS = 3000; diff --git a/packages/web/tests/triggers.test.ts b/packages/web/tests/triggers.test.ts index ddc0afd2c..e3c77d464 100644 --- a/packages/web/tests/triggers.test.ts +++ b/packages/web/tests/triggers.test.ts @@ -2,7 +2,8 @@ import { DiffTriggerOperation } from '@powersync/common'; import { WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { describe, expect, it, onTestFinished, vi } from 'vitest'; import { TEST_SCHEMA } from './utils/test-schema.js'; -import { defaultLoggerConfig, generateTestDb } from './utils/testDb.js'; +import { generateTestDb } from './utils/testDb.js'; +import { defaultLoggerConfig } from './utils/logger.js'; // Shared helper to spin up an iframe that creates a persisted trigger table const createTriggerInIframe = () => { diff --git a/packages/web/tests/uploads.test.ts b/packages/web/tests/uploads.test.ts index 29b24be8f..43ab11db2 100644 --- a/packages/web/tests/uploads.test.ts +++ b/packages/web/tests/uploads.test.ts @@ -79,7 +79,7 @@ function describeCrudUploadTests(getDatabaseOptions: () => WebPowerSyncDatabaseO await vi.waitFor( () => { - expect(logLines).contains(expect.stringContaining(PARTIAL_WARNING)); + expect(logLines).toEqual(expect.arrayContaining([expect.stringContaining(PARTIAL_WARNING)])); }, { timeout: 500, @@ -143,7 +143,7 @@ function describeCrudUploadTests(getDatabaseOptions: () => WebPowerSyncDatabaseO } ); - expect(logLines).not.contains(expect.stringContaining(PARTIAL_WARNING)); + expect(logLines).not.toEqual(expect.arrayContaining([expect.stringContaining(PARTIAL_WARNING)])); }); }; } diff --git a/packages/web/tests/utils/generateConnectedDatabase.ts b/packages/web/tests/utils/generateConnectedDatabase.ts index 7adaae72e..852f0fd33 100644 --- a/packages/web/tests/utils/generateConnectedDatabase.ts +++ b/packages/web/tests/utils/generateConnectedDatabase.ts @@ -3,7 +3,7 @@ import { WebPowerSyncOpenFactoryOptions } from '@powersync/web'; import { v4 as uuid, v4 } from 'uuid'; import { onTestFinished, vi } from 'vitest'; import { MockRemote, MockStreamOpenFactory, TestConnector } from './MockStreamOpenFactory.js'; -import { defaultLoggerConfig } from './testDb.js'; +import { defaultLoggerConfig } from './logger.js'; type UnwrapPromise = T extends Promise ? U : T; diff --git a/packages/web/tests/utils/iframeInitializer.ts b/packages/web/tests/utils/iframeInitializer.ts index 9fe87034f..9ae898d03 100644 --- a/packages/web/tests/utils/iframeInitializer.ts +++ b/packages/web/tests/utils/iframeInitializer.ts @@ -8,7 +8,7 @@ import { } from '@powersync/common'; import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { getMockSyncServiceFromWorker } from './MockSyncServiceClient.js'; -import { defaultLoggerConfig } from './testDb.js'; +import { defaultLoggerConfig } from './logger.js'; /** * Initializes a PowerSync client in the current iframe context and notifies the parent. diff --git a/packages/web/tests/utils/logger.ts b/packages/web/tests/utils/logger.ts new file mode 100644 index 000000000..bbba42348 --- /dev/null +++ b/packages/web/tests/utils/logger.ts @@ -0,0 +1,7 @@ +import { createPowerSyncLogger, LogLevels } from '@powersync/web'; + +// NOTE: This file is imported from iframes and must not import vitest. +export const defaultLoggerConfig = { + logLevel: LogLevels.trace, + logger: createPowerSyncLogger({ prefix: 'test', minLevel: LogLevels.trace }) +}; diff --git a/packages/web/tests/utils/test-schema.ts b/packages/web/tests/utils/test-schema.ts index e97a87862..5862258aa 100644 --- a/packages/web/tests/utils/test-schema.ts +++ b/packages/web/tests/utils/test-schema.ts @@ -11,7 +11,7 @@ const assets = new Table( customer_id: column.text, description: column.text }, - { indexes: { makemodel: ['make, model'] } } + { indexes: { makemodel: ['make', 'model'] } } ); const customers = new Table({ diff --git a/packages/web/tests/utils/testDb.ts b/packages/web/tests/utils/testDb.ts index 01661d8f4..055b6dc8f 100644 --- a/packages/web/tests/utils/testDb.ts +++ b/packages/web/tests/utils/testDb.ts @@ -1,4 +1,4 @@ -import { createPowerSyncLogger, LogLevels, PowerSyncDatabase, WebPowerSyncDatabaseOptions } from '@powersync/web'; +import { PowerSyncDatabase, WebPowerSyncDatabaseOptions } from '@powersync/web'; import { v4 as uuid } from 'uuid'; import { onTestFinished } from 'vitest'; import { TEST_SCHEMA } from './test-schema.js'; @@ -26,8 +26,3 @@ export const generateTestDb = (options?: WebPowerSyncDatabaseOptions) => { return db; }; - -export const defaultLoggerConfig = { - logLevel: LogLevels.trace, - logger: createPowerSyncLogger({ prefix: 'test', minLevel: LogLevels.trace }) -}; diff --git a/packages/web/tests/utils/triggers/IFrameTriggerConfig.ts b/packages/web/tests/utils/triggers/IFrameTriggerConfig.ts index 46dc26bc1..af3e44681 100644 --- a/packages/web/tests/utils/triggers/IFrameTriggerConfig.ts +++ b/packages/web/tests/utils/triggers/IFrameTriggerConfig.ts @@ -7,7 +7,7 @@ import { DiffTriggerOperation, PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { TEST_SCHEMA } from '../test-schema.js'; -import { defaultLoggerConfig } from '../testDb.js'; +import { defaultLoggerConfig } from '../logger.js'; const db = new PowerSyncDatabase({ database: new WASQLiteOpenFactory({ diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index d42afc725..02b107ac5 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -43,7 +43,7 @@ const config: UserConfigExport = { */ isolate: true, provider: 'playwright', - headless: true, + headless: false, instances: [ { browser: 'chromium' From b579f1f3d0b6ae93f62d7ba5e24a5eae467f2a33 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 15 May 2026 15:32:11 +0200 Subject: [PATCH 11/21] Re-enable headless mode --- packages/web/vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index 02b107ac5..d42afc725 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -43,7 +43,7 @@ const config: UserConfigExport = { */ isolate: true, provider: 'playwright', - headless: false, + headless: true, instances: [ { browser: 'chromium' From d401dd89eccd06e7087af52d2c19e3ee62e1e62c Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 15 May 2026 16:11:56 +0200 Subject: [PATCH 12/21] Fix nuxt tests --- packages/common/src/utils/Logger.ts | 4 +- .../composables/useDiagnosticsLogger.ts | 19 ++- .../composables/NuxtPowerSyncDatabase.test.ts | 46 +++---- .../composables/useDiagnosticsLogger.test.ts | 127 +++++------------- 4 files changed, 79 insertions(+), 117 deletions(-) diff --git a/packages/common/src/utils/Logger.ts b/packages/common/src/utils/Logger.ts index e60c989fc..b124c8fe1 100644 --- a/packages/common/src/utils/Logger.ts +++ b/packages/common/src/utils/Logger.ts @@ -76,7 +76,9 @@ export function createPowerSyncLogger(options?: Partial): P if (level < this.minLevel) return; let emitter = console.log; - if (level >= LogLevels.error) { + if (level >= LogLevels.info) { + emitter = console.info; + } else if (level >= LogLevels.error) { emitter = console.error; } else if (level >= LogLevels.warn) { emitter = console.warn; diff --git a/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts b/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts index 1e6f4a022..10c0a6b2f 100644 --- a/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts +++ b/packages/nuxt/src/runtime/composables/useDiagnosticsLogger.ts @@ -42,7 +42,8 @@ export const useDiagnosticsLogger = (additional?: PowerSyncLogger) => { date: new Date(), tag: record.tag, message: record.message, - error: record.error + error: record.error, + level: nameOfLogLevel(record.level) }; const key = `log:${logObject.date.toISOString()}`; await logsStorage.set(key, logObject); @@ -55,3 +56,19 @@ export const useDiagnosticsLogger = (additional?: PowerSyncLogger) => { return { logger, logsStorage, emitter }; }; + +const levelNames: [string, number][] = [ + ['ERROR', LogLevels.error], + ['WARNING', LogLevels.warn], + ['INFO', LogLevels.info], + ['DEBUG', LogLevels.debug], + ['TRACE', LogLevels.trace] +]; + +function nameOfLogLevel(level: number): string | undefined { + for (const [name, minLevel] of levelNames) { + if (level >= minLevel) return name; + } + + return undefined; +} diff --git a/packages/nuxt/tests/composables/NuxtPowerSyncDatabase.test.ts b/packages/nuxt/tests/composables/NuxtPowerSyncDatabase.test.ts index 6e7304fb5..4f240aa9f 100644 --- a/packages/nuxt/tests/composables/NuxtPowerSyncDatabase.test.ts +++ b/packages/nuxt/tests/composables/NuxtPowerSyncDatabase.test.ts @@ -19,20 +19,20 @@ describe('NuxtPowerSyncDatabase', () => { it('should use default sync implementation when diagnostics is disabled', async () => { const db = openPowerSync(false); const connector = createMockConnector(); - + // Spy on the parent class method to verify it's called const superGenerateSpy = vi.spyOn( Object.getPrototypeOf(Object.getPrototypeOf(db)), 'generateSyncStreamImplementation' ); - + await db.init(); await db.connect(connector); - + // When diagnostics is disabled, should call super.generateSyncStreamImplementation // We can't directly test this, but we can verify the sync implementation exists expect(db.syncStreamImplementation).toBeDefined(); - + await db.disconnect(); }); @@ -40,28 +40,28 @@ describe('NuxtPowerSyncDatabase', () => { setUseDiagnostics(true); const db = openPowerSync(true); const connector = createMockConnector(); - + await db.init(); await db.connect(connector); - + // When diagnostics is enabled, should use SharedWebStreamingSyncImplementation // (because enableMultiTabs is set to true) const syncImpl = db.syncStreamImplementation; expect(syncImpl).toBeDefined(); expect(syncImpl).toBeInstanceOf(SharedWebStreamingSyncImplementation); - + await db.disconnect(); }); it('should extend schema with diagnostics tables when diagnostics is enabled', () => { setUseDiagnostics(true); const db = openPowerSync(true); - + // Verify that diagnostics schema tables are included // The schema should include both the app schema and diagnostics schema const schema = db.dbOptions.schema; expect(schema).toBeDefined(); - + // Check that diagnostics tables are present const tableNames = schema.tables.map((t: any) => t.name); // Diagnostics schema includes local_bucket_data and local_schema @@ -74,7 +74,7 @@ describe('NuxtPowerSyncDatabase', () => { it('should set enableMultiTabs and broadcastLogs flags when diagnostics is enabled', () => { setUseDiagnostics(true); const db = openPowerSync(true); - + // Verify flags are set const flags = db.dbOptions.flags; expect(flags?.enableMultiTabs).toBe(true); @@ -84,28 +84,26 @@ describe('NuxtPowerSyncDatabase', () => { it('should use diagnostics logger when diagnostics is enabled', () => { setUseDiagnostics(true); const db = openPowerSync(true); - + // Verify logger is set (it should be the diagnostics logger, not default) const logger = db.dbOptions.logger; expect(logger).toBeDefined(); - // The diagnostics logger should have DEBUG level - expect(logger?.getLevel()).toBeDefined(); }); it('should store connector and connectionOptions internally', async () => { const db = openPowerSync(false); const connector = createMockConnector(); const connectionOptions = { clientImplementation: undefined }; - + await db.init(); await db.connect(connector, connectionOptions); - + // Verify connector is stored expect(db.connector).toBe(connector); expect(db.connectionOptions).toBe(connectionOptions); - + await db.disconnect(); - + // After disconnect, should be cleared expect(db.connector).toBeNull(); expect(db.connectionOptions).toBeNull(); @@ -115,29 +113,29 @@ describe('NuxtPowerSyncDatabase', () => { setUseDiagnostics(true); const db = openPowerSync(true); const connector = createMockConnector(); - + await db.init(); await db.connect(connector); - + // When diagnostics is enabled, enableMultiTabs is set to true, // so it should use SharedWebStreamingSyncImplementation const syncImpl = db.syncStreamImplementation; expect(syncImpl).toBeInstanceOf(SharedWebStreamingSyncImplementation); - + await db.disconnect(); }); it('should clear connector and connectionOptions on disconnectAndClear', async () => { const db = openPowerSync(false); const connector = createMockConnector(); - + await db.init(); await db.connect(connector); - + expect(db.connector).toBe(connector); - + await db.disconnectAndClear(); - + expect(db.connector).toBeNull(); expect(db.connectionOptions).toBeNull(); }); diff --git a/packages/nuxt/tests/composables/useDiagnosticsLogger.test.ts b/packages/nuxt/tests/composables/useDiagnosticsLogger.test.ts index afe2208fd..ef0e4021f 100644 --- a/packages/nuxt/tests/composables/useDiagnosticsLogger.test.ts +++ b/packages/nuxt/tests/composables/useDiagnosticsLogger.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from 'vitest'; import { useDiagnosticsLogger } from '../../src/runtime/composables/useDiagnosticsLogger'; -import { LogLevel, type ILogHandler } from '@powersync/web'; import { withSetup } from '../utils'; +import { LogLevels, type LogRecord, type PowerSyncLogger } from '@powersync/common'; describe('useDiagnosticsLogger', () => { beforeEach(() => { @@ -17,8 +17,7 @@ describe('useDiagnosticsLogger', () => { const [{ logger, logsStorage }] = withSetup(() => useDiagnosticsLogger()); const testMessage = 'Test log message for storage'; - const extraPayload = { name: 'TestContext', level: { name: 'INFO' } }; - await logger.log(testMessage, extraPayload as any); + await logger.log({ level: LogLevels.info, message: testMessage, tag: 'TestContext' }); // Wait for async storage operations await new Promise((resolve) => setTimeout(resolve, 150)); @@ -30,87 +29,64 @@ describe('useDiagnosticsLogger', () => { expect(logKeys.length).toBeGreaterThan(0); // Find the log that contains our message (key order may vary) - let storedLog: { args?: unknown[] } | null = null; + let storedLog: { message: string } | null = null; for (const key of logKeys) { - const log = (await logsStorage.getItem(key)) as { args?: unknown[] } | null; - const args = log?.args; - if (args?.some((arg) => typeof arg === 'string' && arg.includes(testMessage))) { + const log = (await logsStorage.getItem(key)) as { message: string }; + if (log.message.indexOf(testMessage) >= 0) { storedLog = log; break; } } - expect(storedLog).toBeDefined(); - expect(storedLog).toHaveProperty('args'); - expect(Array.isArray(storedLog?.args)).toBe(true); - const logArgs = storedLog?.args as any[]; - expect(logArgs[0]).toBe(testMessage); - // args[1] is stored as-is (object, not "[object Object]") - expect(logArgs[1]).toBeDefined(); - expect(logArgs[1]).toHaveProperty('name', 'TestContext'); - expect(logArgs[2]).toBeDefined(); - expect(logArgs[2]).toHaveProperty('level'); + expect(storedLog).toMatchObject({ + tag: 'TestContext', + level: 'INFO', + message: testMessage + }); }); it('should emit log events with correct payload structure', async () => { const logEvents: any[] = []; const [{ logger, emitter }] = withSetup(() => useDiagnosticsLogger()); - + // Listen for log events emitter.on('log', (event) => { logEvents.push(event); }); - + const testMessage = 'Test event message'; - await logger.log(testMessage, { - name: 'TestContext', - level: { name: 'WARN' } + await logger.log({ + message: testMessage, + tag: 'TestContext', + level: LogLevels.warn } as any); - + // Wait for async operations - await new Promise(resolve => setTimeout(resolve, 150)); - + await new Promise((resolve) => setTimeout(resolve, 150)); + // Verify event was emitted with correct structure expect(logEvents.length).toBeGreaterThan(0); const event = logEvents[0]; - expect(event).toHaveProperty('key'); - expect(event).toHaveProperty('value'); - expect(typeof event.key).toBe('string'); - expect(event.key).toMatch(/^log:/); - expect(event.value).toBeDefined(); - expect(event.value).toHaveProperty('args'); + expect(event.value).toMatchObject({ + tag: 'TestContext', + message: testMessage, + level: 'WARNING' + }); }); it('should call custom handler with correct messages and context', async () => { - const customHandler = vi.fn(); - const [{ logger }] = withSetup(() => useDiagnosticsLogger(customHandler)); + const log = vi.fn((_record: LogRecord) => {}); + const [{ logger }] = withSetup(() => useDiagnosticsLogger({ log })); - const testMessages = ['Message 1', 'Message 2']; - const testContext = { name: 'TestContext', level: { name: 'ERROR' } } as any; - - await logger.log(testMessages, testContext); + await logger.log({ level: LogLevels.warn, message: 'Message 1', tag: 'TestContext' }); // Wait for async handler await new Promise((resolve) => setTimeout(resolve, 150)); - expect(customHandler).toHaveBeenCalledTimes(1); - const [messages, context] = customHandler.mock.calls[0]; - - const messagesArray = Array.from(messages); - expect(messagesArray.length).toBeGreaterThan(0); - // First argument to log() is what we passed (array or string) - const firstArg = messagesArray[0]; - const hasMessage1 = - (Array.isArray(firstArg) && firstArg.includes('Message 1')) || - (typeof firstArg === 'string' && firstArg.includes('Message 1')); - expect(hasMessage1).toBe(true); - // When two args passed to log(), second is in messages (js-logger passes all log args) - if (messagesArray.length > 1 && typeof messagesArray[1] === 'object' && messagesArray[1]?.name) { - expect(messagesArray[1].name).toBe('TestContext'); - expect(messagesArray[1].level?.name).toBe('ERROR'); - } - expect(context).toBeDefined(); - expect(context.level?.name).toBeDefined(); + expect(log).toHaveBeenCalledTimes(1); + const [record] = log.mock.calls[0]; + + expect(record).toStrictEqual({ level: LogLevels.warn, message: 'Message 1', tag: 'TestContext' }); }); it('should format messages with PowerSync prefix to console', async () => { @@ -120,46 +96,15 @@ describe('useDiagnosticsLogger', () => { const [{ logger }] = withSetup(() => useDiagnosticsLogger()); const testMessage = 'Test formatted message'; - await logger.log([testMessage, 'Extra data'], { - name: 'PowerSyncTest', - level: { name: 'INFO' } - } as any); + await logger.log({ message: testMessage, level: LogLevels.info, tag: 'tag' }); await new Promise((resolve) => setTimeout(resolve, 150)); expect(consoleSpy).toHaveBeenCalled(); - const hasPowerSyncPrefix = consoleSpy.mock.calls.some((call) => { - const firstArg = call[0]; - return typeof firstArg === 'string' && firstArg.includes('[PowerSync]'); - }); - expect(hasPowerSyncPrefix).toBe(true); + expect(consoleSpy.mock.calls.map((call) => call[0])).toEqual( + expect.arrayContaining([expect.stringContaining('[PowerSync.tag]')]) + ); consoleSpy.mockRestore(); }); - - it('should store object as extra arg (not "[object Object]")', async () => { - const [{ logger, logsStorage }] = withSetup(() => useDiagnosticsLogger()); - - const payload = { userId: 'u1', synced: true }; - await logger.log('User is logged in', payload); - - await new Promise((resolve) => setTimeout(resolve, 150)); - - const allKeys = await logsStorage.getKeys(); - const logKeys = allKeys.filter((key: string) => key.startsWith('log:')); - expect(logKeys.length).toBeGreaterThan(0); - - const storedLog = (await logsStorage.getItem(logKeys[0])) as { args?: unknown[] } | null; - const args = storedLog?.args; - expect(args).toBeDefined(); - expect(args?.length).toBeGreaterThanOrEqual(2); - expect(args?.[1]).toEqual(payload); - expect(args?.[1]).not.toBe('[object Object]'); - }); - - it('should configure logger with DEBUG level', () => { - const [{ logger }] = withSetup(() => useDiagnosticsLogger()); - - expect(logger.getLevel()).toBe(LogLevel.DEBUG); - }); }); From 17a689e6346502d41143a502268a0710e57cfe94 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 15 May 2026 16:44:20 +0200 Subject: [PATCH 13/21] Migrate demos --- .../components/providers/SystemProvider.tsx | 9 +- demos/example-electron-node/src/main/index.ts | 15 +-- .../components/providers/SystemProvider.tsx | 9 +- demos/example-node/src/main.ts | 9 +- demos/example-vite-encryption/src/index.js | 7 +- demos/example-vite/src/index.js | 7 +- demos/example-webpack/src/index.js | 7 +- .../components/providers/SystemProvider.tsx | 11 +-- .../powersync/SystemContext.ts | 96 +++++++++---------- .../src/providers/PowerSyncProvider.tsx | 8 +- .../library/powersync/system.ts | 10 +- .../library/powersync/system.ts | 13 +-- .../src/lib/powersync.ts | 69 +++++-------- .../components/providers/SystemProvider.tsx | 10 +- .../components/providers/SystemProvider.tsx | 10 +- .../library/powersync/SupabaseConnector.ts | 2 +- .../components/providers/SystemProvider.tsx | 11 +-- .../components/providers/SystemProvider.tsx | 14 +-- .../components/widgets/SearchBarWidget.tsx | 2 +- .../library/powersync/SupabaseConnector.ts | 2 +- .../components/providers/SystemProvider.tsx | 15 +-- demos/vue-supabase-todolist/src/App.vue | 5 - .../src/plugins/powerSync.ts | 5 +- .../components/providers/SystemProvider.tsx | 9 +- .../vite.config.mts | 6 +- 25 files changed, 153 insertions(+), 208 deletions(-) diff --git a/demos/example-capacitor/src/components/providers/SystemProvider.tsx b/demos/example-capacitor/src/components/providers/SystemProvider.tsx index 55a383d5a..a5ea45210 100644 --- a/demos/example-capacitor/src/components/providers/SystemProvider.tsx +++ b/demos/example-capacitor/src/components/providers/SystemProvider.tsx @@ -1,15 +1,11 @@ import { CircularProgress } from '@mui/material'; import { PowerSyncDatabase } from '@powersync/capacitor'; import { PowerSyncContext } from '@powersync/react'; -import { createBaseLogger, LogLevel } from '@powersync/web'; +import { createPowerSyncLogger, LogLevels } from '@powersync/web'; import React, { Suspense } from 'react'; import { AppSchema } from '../../library/powersync/AppSchema.js'; import { BackendConnector } from '../../library/powersync/BackendConnector.js'; -const logger = createBaseLogger(); -logger.useDefaults(); -logger.setLevel(LogLevel.DEBUG); - // Uses the Web SDK for web, and Capacitor adapters for iOS/Android. const powerSync = new PowerSyncDatabase({ database: { @@ -18,7 +14,8 @@ const powerSync = new PowerSyncDatabase({ schema: AppSchema, flags: { enableMultiTabs: typeof SharedWorker !== 'undefined' - } + }, + logger: createPowerSyncLogger({ minLevel: LogLevels.debug }) }); const connector = new BackendConnector(); diff --git a/demos/example-electron-node/src/main/index.ts b/demos/example-electron-node/src/main/index.ts index 7cb44813c..7d5c2902d 100644 --- a/demos/example-electron-node/src/main/index.ts +++ b/demos/example-electron-node/src/main/index.ts @@ -1,21 +1,10 @@ import fs from 'node:fs'; import { Worker } from 'node:worker_threads'; -import { - createBaseLogger, - createLogger, - LogLevel, - PowerSyncDatabase, - SyncStreamConnectionMethod -} from '@powersync/node'; +import { createPowerSyncLogger, LogLevels, PowerSyncDatabase, SyncStreamConnectionMethod } from '@powersync/node'; import { app, BrowserWindow, ipcMain, MessagePortMain } from 'electron'; import { AppSchema, BackendConnector } from './powersync'; -const baseLogger = createBaseLogger(); -baseLogger.useDefaults({ defaultLevel: LogLevel.WARN }); - -const logger = createLogger('PowerSyncDemo'); - // This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack // plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on // whether you're running in development or production). @@ -47,7 +36,7 @@ const database = new PowerSyncDatabase({ return new Worker(new URL('./worker.ts', import.meta.url), options); } }, - logger + logger: createPowerSyncLogger({ minLevel: LogLevels.debug }) }); const createWindow = (): void => { diff --git a/demos/example-electron/src/components/providers/SystemProvider.tsx b/demos/example-electron/src/components/providers/SystemProvider.tsx index 0fe7aaac3..b4567efe0 100644 --- a/demos/example-electron/src/components/providers/SystemProvider.tsx +++ b/demos/example-electron/src/components/providers/SystemProvider.tsx @@ -1,20 +1,17 @@ import { PowerSyncContext } from '@powersync/react'; -import { createBaseLogger, LogLevel, PowerSyncDatabase } from '@powersync/web'; +import { createPowerSyncLogger, LogLevels, PowerSyncDatabase } from '@powersync/web'; import { CircularProgress } from '@mui/material'; import React, { Suspense } from 'react'; import { AppSchema } from '../../library/powersync/AppSchema.js'; import { BackendConnector } from '../../library/powersync/BackendConnector.js'; -const logger = createBaseLogger(); -logger.useDefaults(); -logger.setLevel(LogLevel.DEBUG); - const powerSync = new PowerSyncDatabase({ database: { dbFilename: 'powersync2.db' }, schema: AppSchema, flags: { disableSSRWarning: true - } + }, + logger: createPowerSyncLogger({ minLevel: LogLevels.debug }) }); const connector = new BackendConnector(); diff --git a/demos/example-node/src/main.ts b/demos/example-node/src/main.ts index bfef05d51..d01aac0e8 100644 --- a/demos/example-node/src/main.ts +++ b/demos/example-node/src/main.ts @@ -3,8 +3,8 @@ import repl_factory from 'node:repl'; import { Worker } from 'node:worker_threads'; import { - createBaseLogger, - createLogger, + createPowerSyncLogger, + LogLevels, PowerSyncDatabase, SyncClientImplementation, SyncStreamConnectionMethod @@ -15,11 +15,10 @@ import { AppSchema, DemoConnector } from './powersync.js'; import { enableUncidiDiagnostics } from './UndiciDiagnostics.js'; const main = async () => { - const baseLogger = createBaseLogger(); - const logger = createLogger('PowerSyncDemo'); const debug = process.env.POWERSYNC_DEBUG == '1'; + const logger = createPowerSyncLogger({ prefix: 'PowerSyncDemo', minLevel: debug ? LogLevels.trace : LogLevels.warn }); + const encryptionKey = process.env.ENCRYPTION_KEY ?? ''; - baseLogger.useDefaults({ defaultLevel: debug ? logger.TRACE : logger.WARN }); // Enable detailed request/response logging for debugging purposes. if (debug) { diff --git a/demos/example-vite-encryption/src/index.js b/demos/example-vite-encryption/src/index.js index 209005ba5..af4800a82 100644 --- a/demos/example-vite-encryption/src/index.js +++ b/demos/example-vite-encryption/src/index.js @@ -1,6 +1,6 @@ -import { column, Schema, Table, PowerSyncDatabase, createBaseLogger } from '@powersync/web'; +import { column, Schema, Table, PowerSyncDatabase, createPowerSyncLogger, LogLevels } from '@powersync/web'; -createBaseLogger().useDefaults(); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); const customers = new Table({ name: column.text }); @@ -12,7 +12,8 @@ const openDatabase = async (encryptionKey) => { PowerSync = new PowerSyncDatabase({ schema: AppSchema, database: { dbFilename: 'example-encryption.db' }, - encryptionKey: encryptionKey + encryptionKey: encryptionKey, + logge }); await PowerSync.init(); diff --git a/demos/example-vite/src/index.js b/demos/example-vite/src/index.js index 05111b12f..e4e5c16d1 100644 --- a/demos/example-vite/src/index.js +++ b/demos/example-vite/src/index.js @@ -1,6 +1,6 @@ -import { PowerSyncDatabase, Schema, Table, column, createBaseLogger } from '@powersync/web'; +import { PowerSyncDatabase, Schema, Table, column, createPowerSyncLogger, LogLevels } from '@powersync/web'; -createBaseLogger().useDefaults(); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); /** * A placeholder connector which doesn't do anything. @@ -27,7 +27,8 @@ let PowerSync; const openDatabase = async () => { PowerSync = new PowerSyncDatabase({ schema: AppSchema, - database: { dbFilename: 'test.sqlite' } + database: { dbFilename: 'test.sqlite' }, + logger }); await PowerSync.init(); diff --git a/demos/example-webpack/src/index.js b/demos/example-webpack/src/index.js index 0ec04b988..94cff2d7b 100644 --- a/demos/example-webpack/src/index.js +++ b/demos/example-webpack/src/index.js @@ -1,6 +1,6 @@ -import { Schema, Table, PowerSyncDatabase, column, createBaseLogger } from '@powersync/web'; +import { Schema, Table, PowerSyncDatabase, column, createPowerSyncLogger, LogLevels } from '@powersync/web'; -createBaseLogger().useDefaults(); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); /** * A placeholder connector which doesn't do anything. @@ -27,7 +27,8 @@ let PowerSync; const openDatabase = async () => { PowerSync = new PowerSyncDatabase({ schema: AppSchema, - database: { dbFilename: 'test.sqlite' } + database: { dbFilename: 'test.sqlite' }, + logger }); await PowerSync.init(); diff --git a/demos/react-multi-client/src/components/providers/SystemProvider.tsx b/demos/react-multi-client/src/components/providers/SystemProvider.tsx index 0c22ca0ca..59c2f0f4b 100644 --- a/demos/react-multi-client/src/components/providers/SystemProvider.tsx +++ b/demos/react-multi-client/src/components/providers/SystemProvider.tsx @@ -6,11 +6,9 @@ import { PowerSyncContext, usePowerSync as _usePowerSync } from '@powersync/reac import { AppSchema } from '@/definitions/Schema'; import { SupabaseConnector } from '@/library/SupabaseConnector'; import { useSupabase } from './SupabaseProvider'; -import { createBaseLogger, LogLevel } from '@powersync/web'; +import { createPowerSyncLogger, LogLevels } from '@powersync/web'; -const logger = createBaseLogger(); -logger.useDefaults(); -logger.setLevel(LogLevel.DEBUG); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); export interface SystemProviderProps { dbFilename: string; @@ -27,7 +25,8 @@ const SystemProvider: React.FC> = (props) schema: AppSchema, flags: { disableSSRWarning: false - } + }, + logger: logger }).getInstance() ); @@ -35,7 +34,7 @@ const SystemProvider: React.FC> = (props) powersync.init(); const l = connector.registerListener({ - initialized: () => { }, + initialized: () => {}, sessionStarted: async () => { await powersync.connect(connector); } diff --git a/demos/react-native-supabase-background-sync/powersync/SystemContext.ts b/demos/react-native-supabase-background-sync/powersync/SystemContext.ts index 6c22fbee6..b277101b6 100644 --- a/demos/react-native-supabase-background-sync/powersync/SystemContext.ts +++ b/demos/react-native-supabase-background-sync/powersync/SystemContext.ts @@ -1,63 +1,59 @@ import React from 'react'; -import { createBaseLogger, LogLevel, PowerSyncDatabase, SyncClientImplementation } from '@powersync/react-native'; +import { createPowerSyncLogger, LogLevels, PowerSyncDatabase, SyncClientImplementation } from '@powersync/react-native'; import { SupabaseConnector } from '@/supabase/SupabaseConnector'; import { AppSchema } from '@/powersync/AppSchema'; import { OPSqliteOpenFactory } from '@powersync/op-sqlite'; -const logger = createBaseLogger(); -logger.useDefaults(); -logger.setLevel(LogLevel.DEBUG); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); export class System { - connector: SupabaseConnector; - powersync: PowerSyncDatabase; - - constructor() { - this.connector = new SupabaseConnector(); - - const opSqlite = new OPSqliteOpenFactory({ - dbFilename: 'powersync.db' - }); - - this.powersync = new PowerSyncDatabase({ - schema: AppSchema, - database: opSqlite, - logger: logger - }); - } - - async init() { - await this.connector.signInAnonymously(); - - await this.powersync.init(); - - await this.powersync.connect(this.connector, { - clientImplementation: SyncClientImplementation.RUST - }); - - this.powersync.registerListener({ - statusChanged: (status) => { - const hasSynced = Boolean(status.lastSyncedAt); - const downloading = status.dataFlowStatus?.downloading || false; - const uploading = status.dataFlowStatus?.uploading || false; - console.log( - '[PowerSync] Status changed:', - hasSynced ? '✅ Synced' : '⏳ Not yet synced', - downloading ? '📥 Downloading' : '✅ Not downloading', - uploading ? '📤 Uploading' : '✅ Not uploading' - ); - }, - }); - } - - async disconnect() { - await this.powersync.disconnect(); - } + connector: SupabaseConnector; + powersync: PowerSyncDatabase; + + constructor() { + this.connector = new SupabaseConnector(); + + const opSqlite = new OPSqliteOpenFactory({ + dbFilename: 'powersync.db' + }); + + this.powersync = new PowerSyncDatabase({ + schema: AppSchema, + database: opSqlite, + logger: logger + }); + } + + async init() { + await this.connector.signInAnonymously(); + + await this.powersync.init(); + + await this.powersync.connect(this.connector, { + clientImplementation: SyncClientImplementation.RUST + }); + + this.powersync.registerListener({ + statusChanged: (status) => { + const hasSynced = Boolean(status.lastSyncedAt); + const downloading = status.dataFlowStatus?.downloading || false; + const uploading = status.dataFlowStatus?.uploading || false; + console.log( + '[PowerSync] Status changed:', + hasSynced ? '✅ Synced' : '⏳ Not yet synced', + downloading ? '📥 Downloading' : '✅ Not downloading', + uploading ? '📤 Uploading' : '✅ Not uploading' + ); + } + }); + } + + async disconnect() { + await this.powersync.disconnect(); + } } export const system = new System(); export const SystemContext = React.createContext(system); export const useSystem = () => React.useContext(SystemContext); - - diff --git a/demos/react-native-supabase-group-chat/src/providers/PowerSyncProvider.tsx b/demos/react-native-supabase-group-chat/src/providers/PowerSyncProvider.tsx index 1bd1cc882..cea26e2b1 100644 --- a/demos/react-native-supabase-group-chat/src/providers/PowerSyncProvider.tsx +++ b/demos/react-native-supabase-group-chat/src/providers/PowerSyncProvider.tsx @@ -1,12 +1,12 @@ import '@azure/core-asynciterator-polyfill'; -import { createBaseLogger, PowerSyncContext, PowerSyncDatabase } from '@powersync/react-native'; +import { createPowerSyncLogger, LogLevels, PowerSyncContext, PowerSyncDatabase } from '@powersync/react-native'; import { ReactNode, useEffect, useMemo } from 'react'; import { useAuth } from './AuthProvider'; import { Connector } from '@/library/connector'; import { AppSchema } from '@/library/schema'; -createBaseLogger().useDefaults(); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); const connector = new Connector(); @@ -16,7 +16,9 @@ export const PowerSyncProvider = ({ children }: { children: ReactNode }) => { const powerSync = useMemo(() => { const powerSync = new PowerSyncDatabase({ schema: AppSchema, - database: { dbFilename: 'test.sqlite' } + database: { dbFilename: 'test.sqlite' }, + + logger: logger }); powerSync.init(); return powerSync; diff --git a/demos/react-native-supabase-todolist/library/powersync/system.ts b/demos/react-native-supabase-todolist/library/powersync/system.ts index 84c1194f1..7c217b5f6 100644 --- a/demos/react-native-supabase-todolist/library/powersync/system.ts +++ b/demos/react-native-supabase-todolist/library/powersync/system.ts @@ -1,13 +1,13 @@ import '@azure/core-asynciterator-polyfill'; import { - createBaseLogger, - LogLevel, + createPowerSyncLogger, + LogLevels, PowerSyncDatabase, SyncClientImplementation, AttachmentQueue, type AttachmentRecord, - type WatchedAttachmentItem, + type WatchedAttachmentItem } from '@powersync/react-native'; import { ReactNativeFileSystemStorageAdapter } from '@powersync/attachments-storage-react-native'; import React from 'react'; @@ -18,9 +18,7 @@ import { AppConfig } from '../supabase/AppConfig'; import { SupabaseConnector } from '../supabase/SupabaseConnector'; import { AppSchema, TODO_TABLE } from './AppSchema'; -const logger = createBaseLogger(); -logger.useDefaults(); -logger.setLevel(LogLevel.DEBUG); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); export class System { kvStorage: KVStorage; diff --git a/demos/react-native-web-supabase-todolist/library/powersync/system.ts b/demos/react-native-web-supabase-todolist/library/powersync/system.ts index 415d5c848..98701525b 100644 --- a/demos/react-native-web-supabase-todolist/library/powersync/system.ts +++ b/demos/react-native-web-supabase-todolist/library/powersync/system.ts @@ -1,10 +1,7 @@ import '@azure/core-asynciterator-polyfill'; import React from 'react'; -import { - PowerSyncDatabase as PowerSyncDatabaseNative, - AbstractPowerSyncDatabase -} from '@powersync/react-native'; +import { PowerSyncDatabase as PowerSyncDatabaseNative, AbstractPowerSyncDatabase } from '@powersync/react-native'; import { PowerSyncDatabase as PowerSyncDatabaseWeb, WASQLiteOpenFactory, @@ -14,8 +11,8 @@ import { ReactNativeFileSystemStorageAdapter } from '@powersync/attachments-stor import { type AttachmentRecord, AttachmentQueue, - LogLevel, - createBaseLogger, + createPowerSyncLogger, + LogLevels, WatchedAttachmentItem } from '@powersync/common'; import { SupabaseRemoteStorageAdapter } from '../storage/SupabaseRemoteStorageAdapter'; @@ -25,9 +22,7 @@ import { SupabaseConnector } from '../supabase/SupabaseConnector'; import { AppSchema, TODO_TABLE } from './AppSchema'; import { Platform } from 'react-native'; -const logger = createBaseLogger(); -logger.useDefaults(); -logger.setLevel(LogLevel.DEBUG); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); export class System { kvStorage: ExpoKVStorage | WebKVStorage; diff --git a/demos/react-neon-tanstack-query-notes/src/lib/powersync.ts b/demos/react-neon-tanstack-query-notes/src/lib/powersync.ts index 1ea638f3b..8b6a47bfb 100644 --- a/demos/react-neon-tanstack-query-notes/src/lib/powersync.ts +++ b/demos/react-neon-tanstack-query-notes/src/lib/powersync.ts @@ -1,37 +1,32 @@ import { AbstractPowerSyncDatabase, BaseObserver, - LogLevel, PowerSyncBackendConnector, type PowerSyncCredentials, PowerSyncDatabase, - createBaseLogger, + createPowerSyncLogger, + LogLevels, CrudEntry, - UpdateType, -} from "@powersync/web"; -import { client } from "@/lib/auth"; -import { - wrapPowerSyncWithDrizzle, - DrizzleAppSchema, -} from "@powersync/drizzle-driver"; + UpdateType +} from '@powersync/web'; +import { client } from '@/lib/auth'; +import { wrapPowerSyncWithDrizzle, DrizzleAppSchema } from '@powersync/drizzle-driver'; -import { drizzleSchema } from "./powersync-schema"; +import { drizzleSchema } from './powersync-schema'; /// Postgres Response codes that we cannot recover from by retrying. const FATAL_RESPONSE_CODES = [ // Class 22 — Data Exception // Examples include data type mismatch. - new RegExp("^22...$"), + new RegExp('^22...$'), // Class 23 — Integrity Constraint Violation. // Examples include NOT NULL, FOREIGN KEY and UNIQUE violations. - new RegExp("^23...$"), + new RegExp('^23...$'), // INSUFFICIENT PRIVILEGE - typically a row-level security violation - new RegExp("^42501$"), + new RegExp('^42501$') ]; -export const powersyncLogger = createBaseLogger(); -powersyncLogger.useDefaults(); -powersyncLogger.setLevel(LogLevel.DEBUG); +export const powersyncLogger = createPowerSyncLogger({ minLevel: LogLevels.debug }); // Type for the session returned by client.auth.getSession() export type NeonSession = { @@ -44,12 +39,9 @@ export type NeonConnectorListener = { sessionStarted: (session: NeonSession) => void; }; -const SESSION_STORAGE_KEY = "neon_session"; +const SESSION_STORAGE_KEY = 'neon_session'; -export class NeonConnector - extends BaseObserver - implements PowerSyncBackendConnector -{ +export class NeonConnector extends BaseObserver implements PowerSyncBackendConnector { ready: boolean = false; currentSession: NeonSession | null = null; @@ -66,7 +58,7 @@ export class NeonConnector this.currentSession = JSON.parse(cached); } } catch (error) { - console.debug("Could not load cached session:", error); + console.debug('Could not load cached session:', error); } } @@ -78,7 +70,7 @@ export class NeonConnector localStorage.removeItem(SESSION_STORAGE_KEY); } } catch (error) { - console.debug("Could not persist session:", error); + console.debug('Could not persist session:', error); } } @@ -89,10 +81,7 @@ export class NeonConnector const sessionResponse = await client.auth.getSession(); this.updateSession(sessionResponse.data ?? null); } catch (error) { - console.debug( - "Could not fetch session during init (most likely offline), using cached session:", - error, - ); + console.debug('Could not fetch session during init (most likely offline), using cached session:', error); } this.ready = true; @@ -117,19 +106,16 @@ export class NeonConnector } } catch (error) { // Network error - use cached session if available (offline mode) - console.debug( - "Could not refresh session (most likely offline), using cached:", - error, - ); + console.debug('Could not refresh session (most likely offline), using cached:', error); } if (!this.currentSession) { - throw new Error("Could not fetch Neon credentials."); + throw new Error('Could not fetch Neon credentials.'); } return { endpoint: import.meta.env.VITE_POWERSYNC_URL, - token: this.currentSession.session.token ?? "", + token: this.currentSession.session.token ?? '' } satisfies PowerSyncCredentials; } @@ -154,10 +140,10 @@ export class NeonConnector result = await table.upsert(record as any); break; case UpdateType.PATCH: - result = await table.update(op.opData as any).eq("id", op.id); + result = await table.update(op.opData as any).eq('id', op.id); break; case UpdateType.DELETE: - result = await table.delete().eq("id", op.id); + result = await table.delete().eq('id', op.id); break; } @@ -171,10 +157,7 @@ export class NeonConnector await transaction.complete(); } catch (ex: any) { console.debug(ex); - if ( - typeof ex.code == "string" && - FATAL_RESPONSE_CODES.some((regex) => regex.test(ex.code)) - ) { + if (typeof ex.code == 'string' && FATAL_RESPONSE_CODES.some((regex) => regex.test(ex.code))) { /** * Instead of blocking the queue with these errors, * discard the (rest of the) transaction. @@ -183,7 +166,7 @@ export class NeonConnector * If protecting against data loss is important, save the failing records * elsewhere instead of discarding, and/or notify the user. */ - console.error("Data upload error - discarding:", lastOp, ex); + console.error('Data upload error - discarding:', lastOp, ex); await transaction.complete(); } else { // Error may be retryable - e.g. network error or temporary server error. @@ -201,8 +184,8 @@ export const AppSchema = new DrizzleAppSchema(drizzleSchema); export const powersync = new PowerSyncDatabase({ schema: AppSchema, database: { - dbFilename: "powersync.db", - }, + dbFilename: 'powersync.db' + } }); export const powersyncDrizzle = wrapPowerSyncWithDrizzle(powersync); @@ -215,7 +198,7 @@ export async function connectPowerSync() { } await powersync.connect(neonConnector); isInitialized = true; - console.log("powersync connected"); + console.log('powersync connected'); } export async function disconnectPowerSync() { diff --git a/demos/react-supabase-time-based-sync/src/components/providers/SystemProvider.tsx b/demos/react-supabase-time-based-sync/src/components/providers/SystemProvider.tsx index 618fd172f..0aba4832b 100644 --- a/demos/react-supabase-time-based-sync/src/components/providers/SystemProvider.tsx +++ b/demos/react-supabase-time-based-sync/src/components/providers/SystemProvider.tsx @@ -2,7 +2,7 @@ import { AppSchema } from '@/library/powersync/AppSchema'; import { SupabaseConnector } from '@/library/powersync/SupabaseConnector'; import { CircularProgress } from '@mui/material'; import { PowerSyncContext } from '@powersync/react'; -import { createBaseLogger, LogLevel, PowerSyncDatabase, SyncClientImplementation } from '@powersync/web'; +import { createPowerSyncLogger, LogLevels, PowerSyncDatabase } from '@powersync/web'; import React, { Suspense } from 'react'; import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext'; @@ -13,7 +13,8 @@ export const db = new PowerSyncDatabase({ schema: AppSchema, database: { dbFilename: 'time.db' - } + }, + logger: createPowerSyncLogger({ minLevel: LogLevels.debug }) }); export const SystemProvider = ({ children }: { children: React.ReactNode }) => { @@ -21,9 +22,6 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => { const [powerSync] = React.useState(db); React.useEffect(() => { - const logger = createBaseLogger(); - logger.useDefaults(); - logger.setLevel(LogLevel.DEBUG); // For console testing purposes (window as any)._powersync = powerSync; @@ -31,7 +29,7 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => { const l = connector.registerListener({ initialized: () => {}, sessionStarted: () => { - powerSync.connect(connector, { clientImplementation: SyncClientImplementation.RUST }); + powerSync.connect(connector); } }); diff --git a/demos/react-supabase-todolist-optional-sync/src/components/providers/SystemProvider.tsx b/demos/react-supabase-todolist-optional-sync/src/components/providers/SystemProvider.tsx index a77833e3f..5f9f49321 100644 --- a/demos/react-supabase-todolist-optional-sync/src/components/providers/SystemProvider.tsx +++ b/demos/react-supabase-todolist-optional-sync/src/components/providers/SystemProvider.tsx @@ -2,7 +2,7 @@ import { makeSchema, switchToSyncedSchema } from '@/library/powersync/AppSchema' import { SupabaseConnector } from '@/library/powersync/SupabaseConnector'; import { CircularProgress } from '@mui/material'; import { PowerSyncContext } from '@powersync/react'; -import { createBaseLogger, LogLevel, PowerSyncDatabase } from '@powersync/web'; +import { createPowerSyncLogger, LogLevels, PowerSyncDatabase } from '@powersync/web'; import React, { Suspense } from 'react'; import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext'; import { getSyncEnabled } from '@/library/powersync/SyncMode'; @@ -17,7 +17,8 @@ const db = new PowerSyncDatabase({ schema: makeSchema(syncEnabled), database: { dbFilename: dbName - } + }, + logger: createPowerSyncLogger({ minLevel: LogLevels.debug }) }); export const SystemProvider = ({ children }: { children: React.ReactNode }) => { @@ -25,15 +26,12 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => { const [powerSync] = React.useState(db); React.useEffect(() => { - const logger = createBaseLogger(); - logger.useDefaults(); - logger.setLevel(LogLevel.DEBUG); // For console testing purposes (window as any)._powersync = powerSync; powerSync.init(); const l = connector.registerListener({ - initialized: () => { }, + initialized: () => {}, sessionStarted: async () => { var isSyncMode = getSyncEnabled(dbName); diff --git a/demos/react-supabase-todolist-optional-sync/src/library/powersync/SupabaseConnector.ts b/demos/react-supabase-todolist-optional-sync/src/library/powersync/SupabaseConnector.ts index eb90c4357..a69e972d2 100644 --- a/demos/react-supabase-todolist-optional-sync/src/library/powersync/SupabaseConnector.ts +++ b/demos/react-supabase-todolist-optional-sync/src/library/powersync/SupabaseConnector.ts @@ -138,7 +138,7 @@ export class SupabaseConnector extends BaseObserver i result = await table.upsert(record); break; case UpdateType.PATCH: - result = await table.update(op.opData).eq('id', op.id); + result = await table.update(op.opData ?? {}).eq('id', op.id); break; case UpdateType.DELETE: result = await table.delete().eq('id', op.id); diff --git a/demos/react-supabase-todolist-sync-streams/src/components/providers/SystemProvider.tsx b/demos/react-supabase-todolist-sync-streams/src/components/providers/SystemProvider.tsx index 3a54211ee..e2fe0a2b4 100644 --- a/demos/react-supabase-todolist-sync-streams/src/components/providers/SystemProvider.tsx +++ b/demos/react-supabase-todolist-sync-streams/src/components/providers/SystemProvider.tsx @@ -4,9 +4,9 @@ import { SupabaseConnector } from '@/library/powersync/SupabaseConnector'; import { CircularProgress } from '@mui/material'; import { PowerSyncContext } from '@powersync/react'; import { - createBaseLogger, + createPowerSyncLogger, + LogLevels, DifferentialWatchedQuery, - LogLevel, PowerSyncDatabase, SyncClientImplementation } from '@powersync/web'; @@ -16,11 +16,13 @@ import { NavigationPanelContextProvider } from '../navigation/NavigationPanelCon const SupabaseContext = React.createContext(null); export const useSupabase = () => React.useContext(SupabaseContext); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); export const db = new PowerSyncDatabase({ schema: AppSchema, database: { dbFilename: 'example.db' - } + }, + logger }); export type EnhancedListRecord = ListRecord & { total_tasks: number; completed_tasks: number }; @@ -64,9 +66,6 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => { }); React.useEffect(() => { - const logger = createBaseLogger(); - logger.useDefaults(); // eslint-disable-line - logger.setLevel(LogLevel.DEBUG); // For console testing purposes (window as any)._powersync = powerSync; diff --git a/demos/react-supabase-todolist-tanstackdb/src/components/providers/SystemProvider.tsx b/demos/react-supabase-todolist-tanstackdb/src/components/providers/SystemProvider.tsx index d6ac28c01..88faae435 100644 --- a/demos/react-supabase-todolist-tanstackdb/src/components/providers/SystemProvider.tsx +++ b/demos/react-supabase-todolist-tanstackdb/src/components/providers/SystemProvider.tsx @@ -4,7 +4,7 @@ import { SupabaseConnector } from '@/library/powersync/SupabaseConnector'; import { TodosDeserializationSchema, TodosSchema } from '@/library/powersync/TodosSchema'; import { CircularProgress } from '@mui/material'; import { PowerSyncContext } from '@powersync/react'; -import { createBaseLogger, LogLevel, PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; +import { createPowerSyncLogger, LogLevels, PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { createCollection } from '@tanstack/db'; import { powerSyncCollectionOptions } from '@tanstack/powersync-db-collection'; import React, { Suspense } from 'react'; @@ -13,12 +13,17 @@ import { NavigationPanelContextProvider } from '../navigation/NavigationPanelCon const SupabaseContext = React.createContext(null); export const useSupabase = () => React.useContext(SupabaseContext); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); + export const db = new PowerSyncDatabase({ schema: AppSchema, database: new WASQLiteOpenFactory({ dbFilename: 'example.db', - vfs: WASQLiteVFS.OPFSCoopSyncVFS - }) + vfs: WASQLiteVFS.OPFSCoopSyncVFS, + logger, + logLevel: LogLevels.debug + }), + logger }); export const listsCollection = createCollection( @@ -54,9 +59,6 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => { const [powerSync] = React.useState(db); React.useEffect(() => { - const logger = createBaseLogger(); - logger.useDefaults(); // eslint-disable-line - logger.setLevel(LogLevel.DEBUG); // For console testing purposes (window as any)._powersync = powerSync; diff --git a/demos/react-supabase-todolist-tanstackdb/src/components/widgets/SearchBarWidget.tsx b/demos/react-supabase-todolist-tanstackdb/src/components/widgets/SearchBarWidget.tsx index b775354d5..3b247e20f 100644 --- a/demos/react-supabase-todolist-tanstackdb/src/components/widgets/SearchBarWidget.tsx +++ b/demos/react-supabase-todolist-tanstackdb/src/components/widgets/SearchBarWidget.tsx @@ -41,7 +41,7 @@ export const SearchBarWidget: React.FC = () => { ); const searchResults = [ - ...todoMatches.map((todo) => new SearchResult(todo.id, todo.list_name, todo.description)), + ...todoMatches.map((todo) => new SearchResult(todo.id, todo.list_name!, todo.description)), ...listMatches.map((list) => new SearchResult(list.id, list.name)) ]; diff --git a/demos/react-supabase-todolist-tanstackdb/src/library/powersync/SupabaseConnector.ts b/demos/react-supabase-todolist-tanstackdb/src/library/powersync/SupabaseConnector.ts index 07472b7e0..d7349249a 100644 --- a/demos/react-supabase-todolist-tanstackdb/src/library/powersync/SupabaseConnector.ts +++ b/demos/react-supabase-todolist-tanstackdb/src/library/powersync/SupabaseConnector.ts @@ -124,7 +124,7 @@ export class SupabaseConnector extends BaseObserver i result = await table.upsert(record); break; case UpdateType.PATCH: - result = await table.update(op.opData).eq('id', op.id); + result = await table.update(op.opData ?? {}).eq('id', op.id); break; case UpdateType.DELETE: result = await table.delete().eq('id', op.id); diff --git a/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx b/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx index f4e800b5f..b9b4c3c97 100644 --- a/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx +++ b/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx @@ -4,9 +4,9 @@ import { SupabaseConnector } from '@/library/powersync/SupabaseConnector'; import { CircularProgress } from '@mui/material'; import { PowerSyncContext } from '@powersync/react'; import { - createBaseLogger, + createPowerSyncLogger, + LogLevels, DifferentialWatchedQuery, - LogLevel, PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS @@ -18,6 +18,7 @@ declare const APP_VERSION: string; const SupabaseContext = React.createContext(null); export const useSupabase = () => React.useContext(SupabaseContext); +const logger = createPowerSyncLogger({ minLevel: LogLevels.debug }); export const db = new PowerSyncDatabase({ schema: AppSchema, @@ -26,11 +27,14 @@ export const db = new PowerSyncDatabase({ vfs: WASQLiteVFS.OPFSCoopSyncVFS, flags: { enableMultiTabs: typeof SharedWorker !== 'undefined' - } + }, + logger, + logLevel: LogLevels.debug }), flags: { enableMultiTabs: typeof SharedWorker !== 'undefined' - } + }, + logger }); export type EnhancedListRecord = ListRecord & { total_tasks: number; completed_tasks: number }; @@ -74,9 +78,6 @@ export const SystemProvider = ({ children }: { children: React.ReactNode }) => { }); React.useEffect(() => { - const logger = createBaseLogger(); - logger.useDefaults(); // eslint-disable-line - logger.setLevel(LogLevel.DEBUG); // For console testing purposes (window as any)._powersync = powerSync; diff --git a/demos/vue-supabase-todolist/src/App.vue b/demos/vue-supabase-todolist/src/App.vue index dde5f7bd1..81a80ab93 100644 --- a/demos/vue-supabase-todolist/src/App.vue +++ b/demos/vue-supabase-todolist/src/App.vue @@ -1,11 +1,6 @@