Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e61059a
No longer awaiting when aborting connection on tab closure. Fixes som…
Chriztiaan Nov 26, 2025
a12145f
fix opfs deadlocks
stevensJourney Nov 27, 2025
b4f5c1b
Add multiple tabs test
stevensJourney Nov 27, 2025
355e396
enable headless
stevensJourney Nov 27, 2025
c6ba9f6
reenable browsers
stevensJourney Nov 27, 2025
9730337
cleanup tests
stevensJourney Nov 27, 2025
58f412b
Merge branch 'dead-tab-detection' into fix/opfs-multitab-issue
stevensJourney Nov 27, 2025
8721538
Add default lock timeout for shared sync workers. Add flagging for u…
stevensJourney Nov 28, 2025
feca863
fix broken test
stevensJourney Nov 28, 2025
b0dd596
cleanup tests
stevensJourney Nov 28, 2025
de7804f
restore collectActiveSubscriptions
stevensJourney Nov 28, 2025
c08e664
cleanup code flow
stevensJourney Nov 28, 2025
aedc855
Add withTimeout for opening db connections
stevensJourney Nov 28, 2025
62b04de
reduce number of iframes for CI
stevensJourney Nov 28, 2025
0fc0d73
Use a distributed database adapter instead of reconnecting. Retry ope…
stevensJourney Nov 28, 2025
91db686
Catch closed errors for hold requests. Use crud throttle time for cru…
stevensJourney Nov 28, 2025
deaf83c
Synchronize lock requests better.
stevensJourney Nov 28, 2025
3780223
Re-open database as soon as it's closed. Trigger uploads if database …
stevensJourney Nov 29, 2025
8f95e10
Update tabs test to be more indicative of the actual issues.
stevensJourney Nov 29, 2025
1e880d6
Add mocked sync tests for shared webworkers
stevensJourney Dec 1, 2025
da53396
Update tests for shared web workers
stevensJourney Dec 1, 2025
28472e3
Add automatic responses or mocked sync service.
stevensJourney Dec 1, 2025
11b7a36
Update more tests to use mocked sync service
stevensJourney Dec 1, 2025
7e7ec91
increase timeout for ci
stevensJourney Dec 1, 2025
1e9fa3f
Improve test stability
stevensJourney Dec 1, 2025
07231cb
cleanup port assignments and init flow
stevensJourney Dec 1, 2025
cbfb683
cleanup test code
stevensJourney Dec 1, 2025
f881992
Listen for database close events to catch closed items earlier.
stevensJourney Dec 1, 2025
ffe5abe
Fire and forget close operations.
stevensJourney Dec 1, 2025
58d9194
Common changeset.
Chriztiaan Dec 1, 2025
1ef44a3
Try and catch Accesshandle errors. Fork OPFSCoopSyncVFS for potential…
stevensJourney Dec 2, 2025
d73d9d2
Cleanup aborted operations if aborted. Try and gracefully handle Acce…
stevensJourney Dec 2, 2025
a1fd0bb
cleanup
stevensJourney Dec 2, 2025
445ec69
add a finalization registry entry to release access handles
stevensJourney Dec 2, 2025
6630097
Wrap entire open operation init in writeLock.
stevensJourney Dec 3, 2025
bbfdcf7
revert opfs test
stevensJourney Dec 3, 2025
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/rare-windows-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/web': patch
---

No longer awaiting when aborting connection on tab closure. Fixes some edge cases where multiple tabs with OPFS/Safari can cause deadlocks.
5 changes: 5 additions & 0 deletions .changeset/witty-steaks-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/common': minor
---

Serializing upload and download errors for SyncStatus events. Small changes to how delay values are passed to the sync implementation internally.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { AppSchema, ListRecord, LISTS_TABLE, TODOS_TABLE } from '@/library/power
import { SupabaseConnector } from '@/library/powersync/SupabaseConnector';
import { CircularProgress } from '@mui/material';
import { PowerSyncContext } from '@powersync/react';
import { createBaseLogger, DifferentialWatchedQuery, LogLevel, PowerSyncDatabase } from '@powersync/web';
import {
createBaseLogger,
DifferentialWatchedQuery,
LogLevel,
PowerSyncDatabase,
WASQLiteOpenFactory,
WASQLiteVFS
} from '@powersync/web';
import React, { Suspense } from 'react';
import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext';

Expand All @@ -12,8 +19,15 @@ export const useSupabase = () => React.useContext(SupabaseContext);

export const db = new PowerSyncDatabase({
schema: AppSchema,
database: {
dbFilename: 'example.db'
database: new WASQLiteOpenFactory({
dbFilename: 'example.db',
vfs: WASQLiteVFS.OPFSCoopSyncVFS,
flags: {
enableMultiTabs: typeof SharedWorker !== 'undefined'
}
}),
flags: {
enableMultiTabs: typeof SharedWorker !== 'undefined'
}
});

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,10 @@
"rollup-plugin-dts": "^6.2.1",
"typescript": "^5.7.2",
"vitest": "^3.2.4"
},
"pnpm": {
"overrides": {
"@journeyapps/wa-sqlite": "0.0.0-dev-20251201120934"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO remove this

}
}
}
2 changes: 1 addition & 1 deletion packages/common/src/client/ConnectionManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ILogger } from 'js-logger';
import { SyncStatus } from '../db/crud/SyncStatus.js';
import { BaseListener, BaseObserver } from '../utils/BaseObserver.js';
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
import {
Expand All @@ -13,7 +14,6 @@ import {
SyncStreamSubscribeOptions,
SyncStreamSubscription
} from './sync/sync-streams.js';
import { SyncStatus } from '../db/crud/SyncStatus.js';

/**
* @internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { CrudEntry } from '../bucket/CrudEntry.js';
import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemote.js';
import { coreStatusToJs, EstablishSyncStream, Instruction, SyncPriorityStatus } from './core-instruction.js';
import { EstablishSyncStream, Instruction, coreStatusToJs } from './core-instruction.js';
import {
BucketRequest,
CrudUploadNotification,
Expand Down Expand Up @@ -429,7 +429,7 @@ The next upload iteration will be delayed.`);
uploadError: ex
}
});
await this.delayRetry(controller.signal);
await this.delayRetry(controller.signal, this.options.crudUploadThrottleMs);
if (!this.isConnected) {
// Exit the upload loop if the sync stream is no longer connected
break;
Expand Down Expand Up @@ -1216,15 +1216,14 @@ The next upload iteration will be delayed.`);
this.iterateListeners((cb) => cb.statusUpdated?.(options));
}

private async delayRetry(signal?: AbortSignal): Promise<void> {
private async delayRetry(signal?: AbortSignal, delayMs?: number): Promise<void> {
return new Promise((resolve) => {
if (signal?.aborted) {
// If the signal is already aborted, resolve immediately
resolve();
return;
}

const { retryDelayMs } = this.options;
const delay = delayMs ?? this.options.retryDelayMs;

let timeoutId: ReturnType<typeof setTimeout> | undefined;

Expand All @@ -1238,7 +1237,7 @@ The next upload iteration will be delayed.`);
};

signal?.addEventListener('abort', endDelay, { once: true });
timeoutId = setTimeout(endDelay, retryDelayMs);
timeoutId = setTimeout(endDelay, delay);
});
}

Expand Down
21 changes: 18 additions & 3 deletions packages/common/src/db/crud/SyncStatus.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CoreStreamSubscription } from '../../client/sync/stream/core-instruction.js';
import { SyncClientImplementation } from '../../client/sync/stream/AbstractStreamingSyncImplementation.js';
import { InternalProgressInformation, ProgressWithOperations, SyncProgress } from './SyncProgress.js';
import { CoreStreamSubscription } from '../../client/sync/stream/core-instruction.js';
import { SyncStreamDescription, SyncSubscriptionDescription } from '../../client/sync/sync-streams.js';
import { InternalProgressInformation, ProgressWithOperations, SyncProgress } from './SyncProgress.js';

export type SyncDataFlowStatus = Partial<{
downloading: boolean;
Expand Down Expand Up @@ -250,13 +250,28 @@ export class SyncStatus {
return {
connected: this.connected,
connecting: this.connecting,
dataFlow: this.dataFlowStatus,
dataFlow: {
...this.dataFlowStatus,
uploadError: this.serializeError(this.dataFlowStatus.uploadError),
downloadError: this.serializeError(this.dataFlowStatus.downloadError)
},
lastSyncedAt: this.lastSyncedAt,
hasSynced: this.hasSynced,
priorityStatusEntries: this.priorityStatusEntries
};
}

protected serializeError(error?: Error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all errors are serialisable over a MessagePort. E.g. some DomExceptions fail to be passed across workers. This explicitly serialises errors in the SyncStatus now.

if (typeof error == 'undefined') {
return undefined;
}
return {
name: error.name,
message: error.message,
stack: error.stack
};
}

private static comparePriorities(a: SyncPriorityStatus, b: SyncPriorityStatus) {
return b.priority - a.priority; // Reverse because higher priorities have lower numbers
}
Expand Down
28 changes: 13 additions & 15 deletions packages/web/src/db/PowerSyncDatabase.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
import {
type BucketStorageAdapter,
type PowerSyncBackendConnector,
type PowerSyncCloseOptions,
type RequiredAdditionalConnectionOptions,
AbstractPowerSyncDatabase,
DBAdapter,
DEFAULT_POWERSYNC_CLOSE_OPTIONS,
isDBAdapter,
isSQLOpenFactory,
PowerSyncDatabaseOptions,
PowerSyncDatabaseOptionsWithDBAdapter,
PowerSyncDatabaseOptionsWithOpenFactory,
PowerSyncDatabaseOptionsWithSettings,
SqliteBucketStorage,
StreamingSyncImplementation
StreamingSyncImplementation,
isDBAdapter,
isSQLOpenFactory,
type BucketStorageAdapter,
type PowerSyncBackendConnector,
type PowerSyncCloseOptions,
type RequiredAdditionalConnectionOptions
} from '@powersync/common';
import { Mutex } from 'async-mutex';
import { getNavigatorLocks } from '../shared/navigator';
import { WebDBAdapter } from './adapters/WebDBAdapter';
import { WASQLiteOpenFactory } from './adapters/wa-sqlite/WASQLiteOpenFactory';
import {
DEFAULT_WEB_SQL_FLAGS,
ResolvedWebSQLOpenOptions,
resolveWebSQLFlags,
WebSQLFlags
WebSQLFlags,
resolveWebSQLFlags
} from './adapters/web-sql-flags';
import { WebDBAdapter } from './adapters/WebDBAdapter';
import { SharedWebStreamingSyncImplementation } from './sync/SharedWebStreamingSyncImplementation';
import { SSRStreamingSyncImplementation } from './sync/SSRWebStreamingSyncImplementation';
import { SharedWebStreamingSyncImplementation } from './sync/SharedWebStreamingSyncImplementation';
import { WebRemote } from './sync/WebRemote';
import {
WebStreamingSyncImplementation,
Expand Down Expand Up @@ -160,14 +159,13 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
* By default the sync stream client is only disconnected if
* multiple tabs are not enabled.
*/
close(options: PowerSyncCloseOptions = DEFAULT_POWERSYNC_CLOSE_OPTIONS): Promise<void> {
close(options?: PowerSyncCloseOptions): Promise<void> {
if (this.unloadListener) {
window.removeEventListener('unload', this.unloadListener);
}

return super.close({
// Don't disconnect by default if multiple tabs are enabled
disconnect: options.disconnect ?? !this.resolvedFlags.enableMultiTabs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default param above actually causes the opposite of this to occur. A close would perform a disconnect on web if no argument was supplied. Removing the default fixes this.

disconnect: options?.disconnect ?? !this.resolvedFlags.enableMultiTabs
});
}

Expand Down
12 changes: 12 additions & 0 deletions packages/web/src/db/adapters/AsyncDatabaseConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ export type ProxiedQueryResult = Omit<QueryResult, 'rows'> & {
*/
export type OnTableChangeCallback = (event: BatchedUpdateNotification) => void;

/**
* Thrown when an underlying database connection is closed.
* This is particularly relevant when worker connections are marked as closed while
* operations are still in progress.
*/
export class ConnectionClosedError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConnectionClosedError';
}
}

/**
* @internal
* An async Database connection which provides basic async SQL methods.
Expand Down
Loading
Loading