@@ -131,6 +131,8 @@ export class KV extends EventEmitter {
131
131
private isInTransaction : boolean = false ;
132
132
private watchdogTimer ?: number ; // Undefined if not scheduled or currently running
133
133
private watchdogPromise ?: Promise < void > ;
134
+ /** Used through .deferCompletion to make .close await the action */
135
+ private promiseWatchlist : Promise < unknown > [ ] ;
134
136
135
137
/**
136
138
* Initializes a new instance of the cross/kv main class `KV`.
@@ -142,6 +144,8 @@ export class KV extends EventEmitter {
142
144
constructor ( options : KVOptions = { } ) {
143
145
super ( ) ;
144
146
147
+ this . promiseWatchlist = [ ] ;
148
+
145
149
// Validate and set options
146
150
// - autoSync
147
151
if (
@@ -184,6 +188,54 @@ export class KV extends EventEmitter {
184
188
this . watchdogPromise = this . watchdog ( ) ;
185
189
}
186
190
}
191
+
192
+ /**
193
+ * Defers the resolution or rejection of a Promise until the `.close()` method is called.
194
+ *
195
+ * This function adds the provided promise to a `promiseWatchlist`. During the `close()` method, the database
196
+ * will wait for all promises in the watchlist to settle (resolve or reject) before finalizing the closure.
197
+ * If an `errorHandler` function is provided, it will be called with any errors that occur during the promise's
198
+ * execution. Otherwise, errors will be silently ignored.
199
+ *
200
+ * @param promiseToHandle - The Promise whose resolution or rejection is to be deferred.
201
+ * @param errorHandler - An optional function to handle errors that occur during the promise's execution.
202
+ * @returns The original promise, allowing for chaining.
203
+ */
204
+ public defer (
205
+ promiseToHandle : Promise < unknown > ,
206
+ errorHandler ?: ( error : unknown ) => void ,
207
+ ) : Promise < unknown > {
208
+ this . promiseWatchlist . push ( promiseToHandle ) ;
209
+
210
+ promiseToHandle . finally ( ( ) => {
211
+ this . removePromiseFromWatchlist ( promiseToHandle ) ;
212
+ } ) . catch ( ( error ) => {
213
+ if ( errorHandler ) {
214
+ errorHandler ( error ) ; // Call the custom error handler
215
+ } else {
216
+ /** Silently ignore */
217
+ }
218
+ this . removePromiseFromWatchlist ( promiseToHandle ) ;
219
+ } ) ;
220
+
221
+ return promiseToHandle ;
222
+ }
223
+
224
+ /**
225
+ * Removes a Promise from the `promiseWatchlist`.
226
+ *
227
+ * This function is used internally to clean up the watchlist after a promise has been settled (resolved or rejected).
228
+ * It ensures that only pending promises remain in the watchlist.
229
+ *
230
+ * @param promiseToRemove - The Promise to remove from the watchlist.
231
+ */
232
+ private removePromiseFromWatchlist ( promiseToRemove : Promise < unknown > ) {
233
+ const index = this . promiseWatchlist . indexOf ( promiseToRemove ) ;
234
+ if ( index > - 1 ) {
235
+ this . promiseWatchlist . splice ( index , 1 ) ;
236
+ }
237
+ }
238
+
187
239
/**
188
240
* Opens the Key-Value store based on a provided file path.
189
241
* Initializes the index and data files.
@@ -795,31 +847,65 @@ export class KV extends EventEmitter {
795
847
}
796
848
797
849
/**
798
- * Closes the database gracefully.
850
+ * Closes the database gracefully, awaiting pending promises and optionally applying a timeout.
851
+ *
852
+ * 1. Awaits all deferred promises in the `promiseWatchlist`.
853
+ * 2. Waits for any ongoing watchdog task to complete.
854
+ * 3. Emits a 'closing' event to notify listeners.
855
+ * 4. Closes the associated ledger.
799
856
*
800
- * 1. Waits for any ongoing watchdog task to complete.
801
- * 2. Emits a 'closing' event to notify listeners.
802
- * 3. Closes the associated ledger.
857
+ * @param timeoutMs (optional) - The maximum time in milliseconds to wait for promises to resolve before closing. Defaults to 5000ms.
803
858
*/
804
- public async close ( ) {
859
+ public async close ( timeoutMs = 5000 ) { // Default timeout of 5 seconds
805
860
// @ts -ignore emit exists
806
861
this . emit ( "closing" ) ;
807
862
808
863
// Used to stop any pending watchdog runs
809
864
this . aborted = true ;
810
865
811
- // Await running watchdog
812
- await this . watchdogPromise ;
866
+ try {
867
+ // Create a timeout promise
868
+ let promiseTimeout ;
869
+ const timeoutPromise = new Promise ( ( _ , reject ) => {
870
+ promiseTimeout = setTimeout (
871
+ ( ) => reject ( new Error ( "Database close timeout" ) ) ,
872
+ timeoutMs ,
873
+ ) ;
874
+ } ) ;
813
875
814
- // Abort any watchdog timer
815
- clearTimeout ( this . watchdogTimer ! ) ;
876
+ // Race to see if promises settle before the timeout
877
+ await Promise . race ( [
878
+ Promise . allSettled ( this . promiseWatchlist ) ,
879
+ timeoutPromise ,
880
+ ] ) ;
816
881
817
- // Clear all local variables to avoid problems with unexpected usage after closing
818
- this . ledgerPath = undefined ;
819
- this . ledger = undefined ;
820
- this . index = new KVIndex ( ) ;
821
- this . pendingTransactions = [ ] ;
822
- this . watchHandlers = [ ] ;
882
+ // Clear the promise timeout on success
883
+ clearTimeout ( promiseTimeout ) ;
884
+
885
+ // Await running watchdog if it hasn't been aborted
886
+ if ( this . watchdogPromise ) {
887
+ await this . watchdogPromise ;
888
+ }
889
+ } catch ( error ) {
890
+ if ( error . message === "Database close timeout" ) {
891
+ console . warn (
892
+ "Database close timed out. Some promises may not have resolved:" ,
893
+ this . promiseWatchlist ,
894
+ ) ;
895
+ } else {
896
+ console . error ( "Error during database close:" , error ) ;
897
+ }
898
+ } finally {
899
+ // Clear watchdog timer regardless of errors
900
+ clearTimeout ( this . watchdogTimer ! ) ;
901
+
902
+ // Reset internal state
903
+ this . ledgerPath = undefined ;
904
+ this . ledger = undefined ;
905
+ this . index = new KVIndex ( ) ;
906
+ this . pendingTransactions = [ ] ;
907
+ this . watchHandlers = [ ] ;
908
+ }
823
909
}
824
910
825
911
/**
0 commit comments