@@ -15,6 +15,7 @@ export type SharedConnectionWorker = {
1515export type WrappedWorkerConnectionOptions < Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions > = {
1616 baseConnection : AsyncDatabaseConnection ;
1717 identifier : string ;
18+ remoteCanCloseUnexpectedly : boolean ;
1819 /**
1920 * Need a remote in order to keep a reference to the Proxied worker
2021 */
@@ -29,10 +30,13 @@ export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOpti
2930export class WorkerWrappedAsyncDatabaseConnection < Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions >
3031 implements AsyncDatabaseConnection
3132{
32- protected lockAbortController : AbortController ;
33+ protected lockAbortController = new AbortController ( ) ;
34+ protected notifyRemoteClosed : AbortController | undefined ;
3335
3436 constructor ( protected options : WrappedWorkerConnectionOptions < Config > ) {
35- this . lockAbortController = new AbortController ( ) ;
37+ if ( options . remoteCanCloseUnexpectedly ) {
38+ this . notifyRemoteClosed = new AbortController ( ) ;
39+ }
3640 }
3741
3842 protected get baseConnection ( ) {
@@ -43,6 +47,48 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
4347 return this . baseConnection . init ( ) ;
4448 }
4549
50+ /**
51+ * Marks the remote as closed.
52+ *
53+ * This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
54+ * it happens, all methods on the {@link baseConnection} would never resolve. To avoid livelocks in this scenario, we
55+ * throw on all outstanding promises and forbid new calls.
56+ */
57+ markRemoteClosed ( ) {
58+ // Can non-null assert here because this function is only supposed to be called when remoteCanCloseUnexpectedly was
59+ // set.
60+ this . notifyRemoteClosed ! . abort ( ) ;
61+ }
62+
63+ private withRemote < T > ( workerPromise : ( ) => Promise < T > ) : Promise < T > {
64+ const controller = this . notifyRemoteClosed ;
65+ if ( controller ) {
66+ return new Promise ( ( resolve , reject ) => {
67+ if ( controller . signal . aborted ) {
68+ reject ( new Error ( 'Called operation on closed remote' ) ) ;
69+ }
70+
71+ function handleAbort ( ) {
72+ reject ( new Error ( 'Remote peer closed with request in flight' ) ) ;
73+ }
74+
75+ function completePromise ( action : ( ) => void ) {
76+ controller ! . signal . removeEventListener ( 'abort' , handleAbort ) ;
77+ action ( ) ;
78+ }
79+
80+ controller . signal . addEventListener ( 'abort' , handleAbort ) ;
81+
82+ workerPromise ( )
83+ . then ( ( data ) => completePromise ( ( ) => resolve ( data ) ) )
84+ . catch ( ( e ) => completePromise ( ( ) => reject ( e ) ) ) ;
85+ } ) ;
86+ } else {
87+ // Can't close, so just return the inner worker promise unguarded.
88+ return workerPromise ( ) ;
89+ }
90+ }
91+
4692 /**
4793 * Get a MessagePort which can be used to share the internals of this connection.
4894 */
@@ -103,24 +149,27 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
103149 async close ( ) : Promise < void > {
104150 // Abort any pending lock requests.
105151 this . lockAbortController . abort ( ) ;
106- await this . baseConnection . close ( ) ;
107- this . options . remote [ Comlink . releaseProxy ] ( ) ;
108- this . options . onClose ?.( ) ;
152+ try {
153+ await this . withRemote ( ( ) => this . baseConnection . close ( ) ) ;
154+ } finally {
155+ this . options . remote [ Comlink . releaseProxy ] ( ) ;
156+ this . options . onClose ?.( ) ;
157+ }
109158 }
110159
111160 execute ( sql : string , params ?: any [ ] ) : Promise < ProxiedQueryResult > {
112- return this . baseConnection . execute ( sql , params ) ;
161+ return this . withRemote ( ( ) => this . baseConnection . execute ( sql , params ) ) ;
113162 }
114163
115164 executeRaw ( sql : string , params ?: any [ ] ) : Promise < any [ ] [ ] > {
116- return this . baseConnection . executeRaw ( sql , params ) ;
165+ return this . withRemote ( ( ) => this . baseConnection . executeRaw ( sql , params ) ) ;
117166 }
118167
119168 executeBatch ( sql : string , params ?: any [ ] ) : Promise < ProxiedQueryResult > {
120- return this . baseConnection . executeBatch ( sql , params ) ;
169+ return this . withRemote ( ( ) => this . baseConnection . executeBatch ( sql , params ) ) ;
121170 }
122171
123172 getConfig ( ) : Promise < ResolvedWebSQLOpenOptions > {
124- return this . baseConnection . getConfig ( ) ;
173+ return this . withRemote ( ( ) => this . baseConnection . getConfig ( ) ) ;
125174 }
126175}
0 commit comments