@@ -6,21 +6,22 @@ import {
66 initializeCoreReplicationMetrics ,
77 InternalOpId ,
88 OplogEntry ,
9+ settledPromise ,
910 storage ,
10- SyncRulesBucketStorage
11+ SyncRulesBucketStorage ,
12+ unsettledPromise
1113} from '@powersync/service-core' ;
1214import { METRICS_HELPER , test_utils } from '@powersync/service-core-tests' ;
1315import * as pgwire from '@powersync/service-jpgwire' ;
1416import { clearTestDb , getClientCheckpoint , TEST_CONNECTION_OPTIONS } from './util.js' ;
1517import { CustomTypeRegistry } from '@module/types/registry.js' ;
18+ import { ReplicationAbortedError } from '@powersync/lib-services-framework' ;
1619
1720export class WalStreamTestContext implements AsyncDisposable {
1821 private _walStream ?: WalStream ;
1922 private abortController = new AbortController ( ) ;
20- private streamPromise ?: Promise < void > ;
2123 public storage ?: SyncRulesBucketStorage ;
22- private replicationConnection ?: pgwire . PgConnection ;
23- private snapshotPromise ?: Promise < void > ;
24+ private settledReplicationPromise ?: Promise < PromiseSettledResult < void > > ;
2425
2526 /**
2627 * Tests operating on the wal stream need to configure the stream and manage asynchronous
@@ -55,21 +56,10 @@ export class WalStreamTestContext implements AsyncDisposable {
5556 await this . dispose ( ) ;
5657 }
5758
58- /**
59- * Clear any errors from startStream, to allow for a graceful dispose when streaming errors
60- * were expected.
61- */
62- async clearStreamError ( ) {
63- if ( this . streamPromise != null ) {
64- this . streamPromise = this . streamPromise . catch ( ( e ) => { } ) ;
65- }
66- }
67-
6859 async dispose ( ) {
6960 this . abortController . abort ( ) ;
7061 try {
71- await this . snapshotPromise ;
72- await this . streamPromise ;
62+ await this . settledReplicationPromise ;
7363 await this . connectionManager . destroy ( ) ;
7464 await this . factory ?. [ Symbol . asyncDispose ] ( ) ;
7565 } catch ( e ) {
@@ -143,36 +133,38 @@ export class WalStreamTestContext implements AsyncDisposable {
143133 */
144134 async initializeReplication ( ) {
145135 await this . replicateSnapshot ( ) ;
146- this . startStreaming ( ) ;
147136 // Make sure we're up to date
148137 await this . getCheckpoint ( ) ;
149138 }
150139
140+ /**
141+ * Replicate the initial snapshot, and start streaming.
142+ */
151143 async replicateSnapshot ( ) {
152- const promise = ( async ( ) => {
153- this . replicationConnection = await this . connectionManager . replicationConnection ( ) ;
154- await this . walStream . initReplication ( this . replicationConnection ) ;
155- } ) ( ) ;
156- this . snapshotPromise = promise . catch ( ( e ) => e ) ;
157- await promise ;
158- }
159-
160- startStreaming ( ) {
161- if ( this . replicationConnection == null ) {
162- throw new Error ( 'Call replicateSnapshot() before startStreaming()' ) ;
144+ // Use a settledPromise to avoid unhandled rejections
145+ this . settledReplicationPromise = settledPromise ( this . walStream . replicate ( ) ) ;
146+ try {
147+ await Promise . race ( [ unsettledPromise ( this . settledReplicationPromise ) , this . walStream . waitForInitialSnapshot ( ) ] ) ;
148+ } catch ( e ) {
149+ if ( e instanceof ReplicationAbortedError && e . cause != null ) {
150+ // Edge case for tests: replicate() can throw an error, but we'd receive the ReplicationAbortedError from
151+ // waitForInitialSnapshot() first. In that case, prioritize the cause, e.g. MissingReplicationSlotError.
152+ // This is not a concern for production use, since we only use waitForInitialSnapshot() in tests.
153+ throw e . cause ;
154+ }
155+ throw e ;
163156 }
164- this . streamPromise = this . walStream . streamChanges ( this . replicationConnection ! ) ;
165157 }
166158
167159 async getCheckpoint ( options ?: { timeout ?: number } ) {
168160 let checkpoint = await Promise . race ( [
169161 getClientCheckpoint ( this . pool , this . factory , { timeout : options ?. timeout ?? 15_000 } ) ,
170- this . streamPromise
162+ unsettledPromise ( this . settledReplicationPromise ! )
171163 ] ) ;
172164 if ( checkpoint == null ) {
173- // This indicates an issue with the test setup - streamingPromise completed instead
165+ // This indicates an issue with the test setup - replicationPromise completed instead
174166 // of getClientCheckpoint()
175- throw new Error ( 'Test failure - streamingPromise completed' ) ;
167+ throw new Error ( 'Test failure - replicationPromise completed' ) ;
176168 }
177169 return checkpoint ;
178170 }
0 commit comments