Skip to content

Commit 7659533

Browse files
improve retry delay logic. Update diagnostics app error detection.
1 parent 73ce560 commit 7659533

File tree

6 files changed

+58
-69
lines changed

6 files changed

+58
-69
lines changed

.changeset/fuzzy-beers-occur.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/diagnostics-app': patch
3+
---
4+
5+
Fix bug where clicking signOut would not disconnect from the PowerSync service. Updated implementation to fetch sync errors from the SyncStatus.

packages/common/src/client/ConnectionManager.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ export class ConnectionManager extends BaseObserver<ConnectionManagerListener> {
122122
if (this.pendingConnectionOptions) {
123123
// Pending options have been placed while connecting.
124124
// Need to reconnect.
125-
this.connectingPromise = this.connectInternal().finally(checkConnection);
125+
this.connectingPromise = this.connectInternal()
126+
.catch(() => {})
127+
.finally(checkConnection);
126128
return this.connectingPromise;
127129
} else {
128130
// Clear the connecting promise, done.
@@ -131,7 +133,9 @@ export class ConnectionManager extends BaseObserver<ConnectionManagerListener> {
131133
}
132134
};
133135

134-
this.connectingPromise ??= this.connectInternal().finally(checkConnection);
136+
this.connectingPromise ??= this.connectInternal()
137+
.catch(() => {})
138+
.finally(checkConnection);
135139
return this.connectingPromise;
136140
}
137141

packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ The next upload iteration will be delayed.`);
442442
*/
443443
while (true) {
444444
this.updateSyncStatus({ connecting: true });
445+
let shouldDelayRetry = true;
445446
try {
446447
if (signal?.aborted) {
447448
break;
@@ -458,10 +459,10 @@ The next upload iteration will be delayed.`);
458459
* The nested abort controller will cleanup any open network requests and streams.
459460
* The WebRemote should only abort pending fetch requests or close active Readable streams.
460461
*/
461-
let delay = true;
462+
462463
if (ex instanceof AbortOperation) {
463464
this.logger.warn(ex);
464-
delay = false;
465+
shouldDelayRetry = false;
465466
// A disconnect was requested, we should not delay since there is no explicit retry
466467
} else {
467468
this.logger.error(ex);
@@ -472,11 +473,6 @@ The next upload iteration will be delayed.`);
472473
downloadError: ex
473474
}
474475
});
475-
476-
// On error, wait a little before retrying
477-
if (delay) {
478-
await this.delayRetry();
479-
}
480476
} finally {
481477
if (!signal.aborted) {
482478
nestedAbortController.abort(new AbortOperation('Closing sync stream network requests before retry.'));
@@ -487,6 +483,11 @@ The next upload iteration will be delayed.`);
487483
connected: false,
488484
connecting: true // May be unnecessary
489485
});
486+
487+
// On error, wait a little before retrying
488+
if (shouldDelayRetry) {
489+
await this.delayRetry(nestedAbortController.signal);
490+
}
490491
}
491492
}
492493

@@ -854,7 +855,29 @@ The next upload iteration will be delayed.`);
854855
this.iterateListeners((cb) => cb.statusUpdated?.(options));
855856
}
856857

857-
private async delayRetry() {
858-
return new Promise((resolve) => setTimeout(resolve, this.options.retryDelayMs));
858+
private async delayRetry(signal?: AbortSignal): Promise<void> {
859+
return new Promise((resolve) => {
860+
if (signal?.aborted) {
861+
// If the signal is already aborted, resolve immediately
862+
resolve();
863+
return;
864+
}
865+
866+
const { retryDelayMs } = this.options;
867+
868+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
869+
870+
const endDelay = () => {
871+
resolve();
872+
if (timeoutId) {
873+
clearTimeout(timeoutId);
874+
timeoutId = undefined;
875+
}
876+
signal?.removeEventListener('abort', endDelay);
877+
};
878+
879+
signal?.addEventListener('abort', endDelay, { once: true });
880+
timeoutId = setTimeout(endDelay, retryDelayMs);
881+
});
859882
}
860883
}

tools/diagnostics-app/src/app/views/layout.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import SouthIcon from '@mui/icons-material/South';
66
import StorageIcon from '@mui/icons-material/Storage';
77
import TableChartIcon from '@mui/icons-material/TableChart';
88
import TerminalIcon from '@mui/icons-material/Terminal';
9-
import WifiIcon from '@mui/icons-material/Wifi';
109
import UserIcon from '@mui/icons-material/VerifiedUser';
10+
import WifiIcon from '@mui/icons-material/Wifi';
1111

1212
import {
1313
AppBar,
@@ -34,7 +34,7 @@ import {
3434
SYNC_DIAGNOSTICS_ROUTE
3535
} from '@/app/router';
3636
import { useNavigationPanel } from '@/components/navigation/NavigationPanelContext';
37-
import { signOut, sync, syncErrorTracker } from '@/library/powersync/ConnectionManager';
37+
import { signOut, sync } from '@/library/powersync/ConnectionManager';
3838
import { usePowerSync } from '@powersync/react';
3939
import { useNavigate } from 'react-router-dom';
4040

@@ -103,19 +103,12 @@ export default function ViewsLayout({ children }: { children: React.ReactNode })
103103
const l = sync.registerListener({
104104
statusChanged: (status) => {
105105
setSyncStatus(status);
106+
setSyncError(status.dataFlowStatus.downloadError ?? null);
106107
}
107108
});
108109
return () => l();
109110
}, []);
110111

111-
React.useEffect(() => {
112-
const l = syncErrorTracker.registerListener({
113-
lastErrorUpdated(error) {
114-
setSyncError(error);
115-
}
116-
});
117-
return () => l();
118-
}, []);
119112
const drawerWidth = 320;
120113

121114
const drawer = (
@@ -220,4 +213,4 @@ namespace S {
220213
object-fit: contain;
221214
padding: 20px;
222215
`;
223-
}
216+
}

tools/diagnostics-app/src/app/views/sync-diagnostics.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NavigationPage } from '@/components/navigation/NavigationPage';
2-
import { clearData, db, sync, syncErrorTracker } from '@/library/powersync/ConnectionManager';
2+
import { clearData, db, sync } from '@/library/powersync/ConnectionManager';
33
import {
44
Box,
55
Button,
@@ -73,7 +73,6 @@ FROM local_bucket_data local`;
7373
export default function SyncDiagnosticsPage() {
7474
const [bucketRows, setBucketRows] = React.useState<null | any[]>(null);
7575
const [tableRows, setTableRows] = React.useState<null | any[]>(null);
76-
const [syncError, setSyncError] = React.useState<Error | null>(syncErrorTracker.lastSyncError);
7776
const [lastSyncedAt, setlastSyncedAt] = React.useState<Date | null>(null);
7877

7978
const bucketRowsLoading = bucketRows == null;
@@ -125,15 +124,6 @@ export default function SyncDiagnosticsPage() {
125124
};
126125
}, []);
127126

128-
React.useEffect(() => {
129-
const l = syncErrorTracker.registerListener({
130-
lastErrorUpdated(error) {
131-
setSyncError(error);
132-
}
133-
});
134-
return () => l();
135-
}, []);
136-
137127
const columns: GridColDef[] = [
138128
{ field: 'name', headerName: 'Name', flex: 2 },
139129
{ field: 'tables', headerName: 'Table(s)', flex: 1, type: 'text' },

tools/diagnostics-app/src/library/powersync/ConnectionManager.ts

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import {
22
BaseListener,
3-
BaseObserver,
3+
createBaseLogger,
4+
LogLevel,
45
PowerSyncDatabase,
5-
WebRemote,
6-
WebStreamingSyncImplementation,
7-
WebStreamingSyncImplementationOptions,
8-
WASQLiteOpenFactory,
96
TemporaryStorageOption,
7+
WASQLiteOpenFactory,
108
WASQLiteVFS,
11-
createBaseLogger,
12-
LogLevel
9+
WebRemote,
10+
WebStreamingSyncImplementation,
11+
WebStreamingSyncImplementationOptions
1312
} from '@powersync/web';
13+
import { safeParse } from '../safeParse/safeParse';
1414
import { DynamicSchemaManager } from './DynamicSchemaManager';
1515
import { RecordingStorageAdapter } from './RecordingStorageAdapter';
1616
import { TokenConnector } from './TokenConnector';
17-
import { safeParse } from '../safeParse/safeParse';
1817

1918
const baseLogger = createBaseLogger();
2019
baseLogger.useDefaults();
@@ -62,31 +61,6 @@ export interface SyncErrorListener extends BaseListener {
6261
lastErrorUpdated?: ((error: Error) => void) | undefined;
6362
}
6463

65-
class SyncErrorTracker extends BaseObserver<SyncErrorListener> {
66-
public lastSyncError: Error | null = null;
67-
68-
constructor() {
69-
super();
70-
// Big hack: Use the logger to get access to connection errors
71-
const defaultHandler = baseLogger.createDefaultHandler();
72-
baseLogger.setHandler((messages, context) => {
73-
defaultHandler(messages, context);
74-
if (context.name == 'PowerSyncStream' && context.level.name == 'ERROR') {
75-
if (messages[0] instanceof Error) {
76-
this.lastSyncError = messages[0];
77-
} else {
78-
this.lastSyncError = new Error('' + messages[0]);
79-
}
80-
this.iterateListeners((listener) => {
81-
listener.lastErrorUpdated?.(this.lastSyncError!);
82-
});
83-
}
84-
});
85-
}
86-
}
87-
88-
export const syncErrorTracker = new SyncErrorTracker();
89-
9064
if (connector.hasCredentials()) {
9165
connect();
9266
}
@@ -95,11 +69,10 @@ export async function connect() {
9569
const params = getParams();
9670
await sync.connect({ params });
9771
if (!sync.syncStatus.connected) {
72+
const error = sync.syncStatus.dataFlowStatus.downloadError ?? new Error('Failed to connect');
9873
// Disconnect but don't wait for it
9974
sync.disconnect();
100-
throw syncErrorTracker.lastSyncError ?? new Error('Failed to connect');
101-
} else {
102-
syncErrorTracker.lastSyncError = null;
75+
throw error;
10376
}
10477
}
10578

@@ -120,6 +93,7 @@ export async function disconnect() {
12093

12194
export async function signOut() {
12295
connector.clearCredentials();
96+
await disconnect();
12397
await db.disconnectAndClear();
12498
await schemaManager.clear();
12599
}

0 commit comments

Comments
 (0)