Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/backport-1053-crudlock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/common': patch
---

Internal: Fix obtaining crud lock not being abortable.
5 changes: 5 additions & 0 deletions .changeset/backport-1053-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/web': patch
---

Fix timeout option having no effect with OPFS WriteAhead VFS.
5 changes: 5 additions & 0 deletions .changeset/backport-1053-worker-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/web': patch
---

Log error when a worker fails to load.
5 changes: 5 additions & 0 deletions .changeset/backport-1054.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/web': patch
---

Increase maximum `dbFilename` length to 112 and improve errors (closes https://github.com/powersync-ja/powersync-js/issues/1052).
25 changes: 0 additions & 25 deletions .github/workflows/audit.yaml

This file was deleted.

2 changes: 1 addition & 1 deletion packages/common/src/client/AbstractPowerSyncDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,7 @@ SELECT * FROM crud_entries;

/**
* Open a read-only transaction.
* Read transactions can run concurrently to a write transaction.
* When multiple connections are available, read transactions can run concurrently to a write transaction.
* Changes from any write transaction are not visible to read transactions started before it.
*
* @param callback - Function to execute within the transaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ export abstract class AbstractStreamingSyncImplementation
private async _uploadAllCrud(signal: AbortSignal): Promise<void> {
return this.obtainLock({
type: LockType.CRUD,
signal,
callback: async () => {
/**
* Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
Expand Down
15 changes: 9 additions & 6 deletions packages/common/src/utils/ControlledExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,17 @@ export class ControlledExecutor<T> {

private async execute(param: T) {
this.runningTask = this.task(param);
await this.runningTask;
this.runningTask = undefined;
try {
await this.runningTask;
} finally {
this.runningTask = undefined;

if (this.pendingTaskParam) {
const pendingParam = this.pendingTaskParam;
this.pendingTaskParam = undefined;
if (this.pendingTaskParam) {
const pendingParam = this.pendingTaskParam;
this.pendingTaskParam = undefined;

this.execute(pendingParam);
this.execute(pendingParam);
}
}
}
}
2 changes: 1 addition & 1 deletion packages/web/src/db/adapters/AsyncWebAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ function readWritePoolState(writer: DatabaseClient, readers: DatabaseClient[]):
let timeout: any = null;
let release: UnlockFn | undefined;
if (options?.timeoutMs) {
timeout = setTimeout(() => abortController.abort, options.timeoutMs);
timeout = setTimeout(() => abortController.abort('requesting database timed out'), options.timeoutMs);
}

try {
Expand Down
10 changes: 10 additions & 0 deletions packages/web/src/db/adapters/wa-sqlite/RawSqliteConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { Factory as WaSqliteFactory, SQLITE_ROW } from '@journeyapps/wa-sqlite';
import { loadModuleAndVfs } from './vfs.js';
import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory.js';

/**
* The maximum length of a db filename we support.
*
* We configure the same on WA-SQLite (which otherwise defaults to a maximum length of 64). We don't want to support
* very long path names as Safari maps OPFS files directly to OS files, and APFS has a 255-byte filename limit. Since
* some VFS append additional characters for pooled file access handles, we want to stay well below that.
*/
export const maxPathNameLength = 128;

export interface RawResultSet {
columns: string[];
rows: SQLiteCompatibleType[][];
Expand Down Expand Up @@ -52,6 +61,7 @@ export class RawSqliteConnection {

private async openSQLiteAPI(): Promise<SQLiteAPI> {
const { module, vfs } = await loadModuleAndVfs(this.options);
vfs.mxPathname = maxPathNameLength;
const sqlite3 = WaSqliteFactory(module);
sqlite3.vfs_register(vfs, true);
/**
Expand Down
33 changes: 24 additions & 9 deletions packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MultiDatabaseServer } from '../../../worker/db/MultiDatabaseServer.js';
import { DatabaseClient, OpenWorkerConnection } from './DatabaseClient.js';
import { generateTabCloseSignal } from '../../../shared/tab_close_signal.js';
import { AsyncDbAdapter, PoolConnection } from '../AsyncWebAdapter.js';
import { maxPathNameLength } from './RawSqliteConnection.js';

export interface WASQLiteOpenFactoryOptions extends WebSQLOpenFactoryOptions {
vfs?: WASQLiteVFS;
Expand Down Expand Up @@ -134,16 +135,24 @@ export class WASQLiteOpenFactory implements SQLOpenFactory {
): Promise<DatabaseClient> => {
const workerPort =
typeof optionsDbWorker == 'function'
? resolveWorkerDatabasePortFactory(() =>
optionsDbWorker({
...this.options,
temporaryStorage,
cacheSizeKb,
flags: this.resolvedFlags,
encryptionKey
})
? resolveWorkerDatabasePortFactory(
() =>
optionsDbWorker({
...this.options,
temporaryStorage,
cacheSizeKb,
flags: this.resolvedFlags,
encryptionKey
}),
this.logger
)
: openWorkerDatabasePort(this.options.dbFilename, enableMultiTabs, optionsDbWorker, this.waOptions.vfs);
: openWorkerDatabasePort(
this.options.dbFilename,
enableMultiTabs,
optionsDbWorker,
this.waOptions.vfs,
this.logger
);

const source = Comlink.wrap<OpenWorkerConnection>(workerPort);
const closeSignal = new AbortController();
Expand Down Expand Up @@ -220,4 +229,10 @@ function assertValidWASQLiteOpenFactoryOptions(options: WASQLiteOpenFactoryOptio
);
}
}

// Account for the fact that SQLite might append -journal suffixes
const maxLength = maxPathNameLength - 16;
if (options.dbFilename.length > maxLength) {
throw new Error(`dbFilename too long (max length is ${maxLength})`);
}
}
28 changes: 17 additions & 11 deletions packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,6 +15,7 @@ import {
WebStreamingSyncImplementationOptions
} from './WebStreamingSyncImplementation.js';
import { generateTabCloseSignal } from '../../shared/tab_close_signal.js';
import { logWorkerErrors } from '../../worker/errors.js';

/**
* The shared worker will trigger methods on this side of the message port
Expand Down Expand Up @@ -128,22 +128,23 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
const syncWorker = options.sync?.worker;
if (syncWorker) {
if (typeof syncWorker === 'function') {
this.messagePort = syncWorker(resolvedWorkerOptions).port;
this.messagePort = this.workerPort(syncWorker(resolvedWorkerOptions));
} else {
this.messagePort = new SharedWorker(`${syncWorker}`, {
/* @vite-ignore */
name: `shared-sync-${this.webOptions.identifier}`
}).port;
this.messagePort = this.workerPort(
new SharedWorker(`${syncWorker}`, {
/* @vite-ignore */
name: `shared-sync-${this.webOptions.identifier}`
})
);
}
} else {
this.messagePort = new SharedWorker(
new URL('../../worker/sync/SharedSyncImplementation.worker.js', import.meta.url),
{
this.messagePort = this.workerPort(
new SharedWorker(new URL('../../worker/sync/SharedSyncImplementation.worker.js', import.meta.url), {
/* @vite-ignore */
name: `shared-sync-${this.webOptions.identifier}`,
type: 'module'
}
).port;
})
);
}

/**
Expand Down Expand Up @@ -179,6 +180,11 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
this.isInitialized = this._init();
}

private workerPort(worker: SharedWorker): MessagePort {
logWorkerErrors(worker, this.logger);
return worker.port;
}

protected async _init() {
/**
* The general flow of initialization is:
Expand Down
56 changes: 34 additions & 22 deletions packages/web/src/worker/db/open-worker-database.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as Comlink from 'comlink';
import { vfsRequiresDedicatedWorkers, WASQLiteVFS } from '../../db/adapters/wa-sqlite/vfs.js';
import { OpenWorkerConnection } from '../../db/adapters/wa-sqlite/DatabaseClient.js';
import type { ILogger } from '@powersync/common';
import { logWorkerErrors } from '../errors.js';

/**
* Opens a shared or dedicated worker which exposes opening of database connections
Expand All @@ -9,39 +11,45 @@ export function openWorkerDatabasePort(
workerIdentifier: string,
multipleTabs = true,
worker: string | URL = '',
vfs?: WASQLiteVFS
vfs?: WASQLiteVFS,
logger?: ILogger
) {
const needsDedicated = vfs && vfsRequiresDedicatedWorkers(vfs);
let resolvedWorker: Worker | SharedWorker;

if (worker) {
return !needsDedicated && multipleTabs
? new SharedWorker(`${worker}`, {
/* @vite-ignore */
name: `shared-DB-worker-${workerIdentifier}`
}).port
: new Worker(`${worker}`, {
/* @vite-ignore */
name: `DB-worker-${workerIdentifier}`
});
resolvedWorker =
!needsDedicated && multipleTabs
? new SharedWorker(`${worker}`, {
/* @vite-ignore */
name: `shared-DB-worker-${workerIdentifier}`
})
: new Worker(`${worker}`, {
/* @vite-ignore */
name: `DB-worker-${workerIdentifier}`
});
} else {
/**
* Webpack V5 can bundle the worker automatically if the full Worker constructor syntax is used
* https://webpack.js.org/guides/web-workers/
* This enables multi tab support by default, but falls back if SharedWorker is not available
* (in the case of Android)
*/
return !needsDedicated && multipleTabs
? new SharedWorker(new URL('./WASQLiteDB.worker.js', import.meta.url), {
/* @vite-ignore */
name: `shared-DB-worker-${workerIdentifier}`,
type: 'module'
}).port
: new Worker(new URL('./WASQLiteDB.worker.js', import.meta.url), {
/* @vite-ignore */
name: `DB-worker-${workerIdentifier}`,
type: 'module'
});
resolvedWorker =
!needsDedicated && multipleTabs
? new SharedWorker(new URL('./WASQLiteDB.worker.js', import.meta.url), {
/* @vite-ignore */
name: `shared-DB-worker-${workerIdentifier}`,
type: 'module'
})
: new Worker(new URL('./WASQLiteDB.worker.js', import.meta.url), {
/* @vite-ignore */
name: `DB-worker-${workerIdentifier}`,
type: 'module'
});
}

return resolveWorkerDatabasePortFactory(() => resolvedWorker, logger);
}

/**
Expand All @@ -52,8 +60,12 @@ export function getWorkerDatabaseOpener(workerIdentifier: string, multipleTabs =
return Comlink.wrap<OpenWorkerConnection>(openWorkerDatabasePort(workerIdentifier, multipleTabs, worker));
}

export function resolveWorkerDatabasePortFactory(worker: () => Worker | SharedWorker) {
export function resolveWorkerDatabasePortFactory(worker: () => Worker | SharedWorker, logger?: ILogger) {
const workerInstance = worker();
if (logger) {
logWorkerErrors(workerInstance, logger);
}

return isSharedWorker(workerInstance) ? workerInstance.port : workerInstance;
}

Expand Down
9 changes: 9 additions & 0 deletions packages/web/src/worker/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ILogger } from '@powersync/common';

export function logWorkerErrors(worker: AbstractWorker, logger: ILogger) {
function logError(event: ErrorEvent) {
logger.error('Error in database or sync worker, this likely disrupts PowerSync.', event.error);
}

worker.addEventListener('error', logError);
}
4 changes: 3 additions & 1 deletion packages/web/src/worker/sync/SharedSyncImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
broadCastLogger: ILogger;
protected readonly database = this.generateReconnectableDatabase();

private sharedCloseSignal = generateTabCloseSignal();

constructor() {
super();
this.ports = [];
Expand Down Expand Up @@ -508,7 +510,7 @@ export class SharedSyncImplementation extends BaseObserver<SharedSyncImplementat
const remote = Comlink.wrap<OpenWorkerConnection>(workerPort);
const identifier = this.syncParams!.dbParams.dbFilename;

const clientLockName = await generateTabCloseSignal();
const clientLockName = await this.sharedCloseSignal;

/**
* The open could fail if the tab is closed while we're busy opening the database.
Expand Down
Loading
Loading