@@ -38,6 +38,22 @@ export type RecoverResult<T> = {
3838 partialTail : string | null ;
3939} ;
4040
41+ /**
42+ * Statistics about the WAL file state and last recovery operation.
43+ */
44+ export type WalStats < T > = {
45+ /** File path for this WAL */
46+ filePath : string ;
47+ /** Whether the WAL file is currently closed */
48+ isClosed : boolean ;
49+ /** Whether the WAL file exists on disk */
50+ fileExists : boolean ;
51+ /** File size in bytes (0 if file doesn't exist) */
52+ fileSize : number ;
53+ /** Last recovery state from the most recent {@link recover} or {@link repack} operation */
54+ lastRecovery : RecoverResult < T | InvalidEntry < string > > | null ;
55+ } ;
56+
4157export const createTolerantCodec = < I , O = string > ( codec : {
4258 encode : ( v : I ) => O ;
4359 decode : ( d : O ) => I ;
@@ -121,6 +137,7 @@ export class WriteAheadLogFile<T> implements AppendableSink<T> {
121137 readonly #file: string ;
122138 readonly #decode: Codec < T | InvalidEntry < string > > [ 'decode' ] ;
123139 readonly #encode: Codec < T > [ 'encode' ] ;
140+ #lastRecoveryState: RecoverResult < T | InvalidEntry < string > > | null = null ;
124141
125142 /**
126143 * Create a new WAL file instance.
@@ -170,20 +187,27 @@ export class WriteAheadLogFile<T> implements AppendableSink<T> {
170187 /**
171188 * Recover all records from the WAL file.
172189 * Handles partial writes and decode errors gracefully.
190+ * Updates the recovery state (accessible via {@link getStats}).
173191 * @returns Recovery result with records, errors, and partial tail
174192 */
175193 recover ( ) : RecoverResult < T | InvalidEntry < string > > {
176194 if ( ! fs . existsSync ( this . #file) ) {
177- return { records : [ ] , errors : [ ] , partialTail : null } ;
195+ this . #lastRecoveryState = { records : [ ] , errors : [ ] , partialTail : null } ;
196+ return this . #lastRecoveryState;
178197 }
179-
180198 const txt = fs . readFileSync ( this . #file, 'utf8' ) ;
181- return recoverFromContent < T | InvalidEntry < string > > ( txt , this . #decode) ;
199+ this . #lastRecoveryState = recoverFromContent < T | InvalidEntry < string > > (
200+ txt ,
201+ this . #decode,
202+ ) ;
203+
204+ return this . #lastRecoveryState;
182205 }
183206
184207 /**
185208 * Repack the WAL by recovering all valid records and rewriting cleanly.
186209 * Removes corrupted entries and ensures clean formatting.
210+ * Updates the recovery state (accessible via {@link getStats}).
187211 * @param out - Output path (defaults to current file)
188212 */
189213 repack ( out = this . #file) {
@@ -208,6 +232,22 @@ export class WriteAheadLogFile<T> implements AppendableSink<T> {
208232 fs . mkdirSync ( path . dirname ( out ) , { recursive : true } ) ;
209233 fs . writeFileSync ( out , `${ recordsToWrite . map ( this . #encode) . join ( '\n' ) } \n` ) ;
210234 }
235+
236+ /**
237+ * Get comprehensive statistics about the WAL file state.
238+ * Includes file information, open/close status, and last recovery state.
239+ * @returns Statistics object with file info and last recovery state
240+ */
241+ getStats ( ) : WalStats < T > {
242+ const fileExists = fs . existsSync ( this . #file) ;
243+ return {
244+ filePath : this . #file,
245+ isClosed : this . #fd == null ,
246+ fileExists,
247+ fileSize : fileExists ? fs . statSync ( this . #file) . size : 0 ,
248+ lastRecovery : this . #lastRecoveryState,
249+ } ;
250+ }
211251}
212252
213253/**
@@ -246,7 +286,7 @@ export const stringCodec = <
246286/**
247287 * Parses a partial WalFormat configuration and returns a complete WalFormat object.
248288 * All fallback values are targeting string types.
249- * - baseName defaults to Date.now().toString()
289+ * - baseName defaults to 'wal'
250290 * - walExtension defaults to '.log'
251291 * - finalExtension defaults to '.log'
252292 * - codec defaults to stringCodec<T>()
@@ -258,14 +298,23 @@ export function parseWalFormat<T extends object | string = object>(
258298 format : Partial < WalFormat < T > > ,
259299) : WalFormat < T > {
260300 const {
261- baseName = 'trace ' ,
301+ baseName = 'wal ' ,
262302 walExtension = '.log' ,
263303 finalExtension = walExtension ,
264304 codec = stringCodec < T > ( ) ,
265- finalizer = ( encodedRecords : ( T | InvalidEntry < string > ) [ ] ) =>
266- `${ encodedRecords . join ( '\n' ) } \n` ,
267305 } = format ;
268306
307+ const finalizer =
308+ format . finalizer ??
309+ ( ( encodedRecords : ( T | InvalidEntry < string > ) [ ] ) => {
310+ const encoded = encodedRecords . map ( record =>
311+ typeof record === 'object' && record != null && '__invalid' in record
312+ ? ( record as InvalidEntry < string > ) . raw
313+ : codec . encode ( record as T ) ,
314+ ) ;
315+ return `${ encoded . join ( '\n' ) } \n` ;
316+ } ) ;
317+
269318 return {
270319 baseName,
271320 walExtension,
@@ -301,6 +350,7 @@ export function setLeaderWal(envVarName: string, profilerID: string): void {
301350
302351// eslint-disable-next-line functional/no-let
303352let shardCount = 0 ;
353+
304354/**
305355 * Generates a human-readable shard ID.
306356 * This ID is unique per process/thread/shard combination and used in the file name.
@@ -354,6 +404,7 @@ export function sortableReadableDateString(timestampMs: string): string {
354404
355405 return `${ yyyy } ${ mm } ${ dd } -${ hh } ${ min } ${ ss } -${ ms } ` ;
356406}
407+
357408/**
358409 * Generates a path to a shard file using human-readable IDs.
359410 * Both groupId and shardId are already in readable date format.
0 commit comments