@@ -6,6 +6,20 @@ import { Logger, createLogger } from '@objectstack/core';
66import { Query , Aggregator } from 'mingo' ;
77import { getValueByPath } from './memory-matcher.js' ;
88
9+ /**
10+ * Persistence adapter interface.
11+ * Matches the PersistenceAdapterSchema contract from @objectstack/spec.
12+ */
13+ export interface PersistenceAdapterInterface {
14+ load ( ) : Promise < Record < string , any [ ] > | null > ;
15+ save ( db : Record < string , any [ ] > ) : Promise < void > ;
16+ flush ( ) : Promise < void > ;
17+ /** Optional: Start periodic auto-save (used by FileSystemPersistenceAdapter). */
18+ startAutoSave ?( ) : void ;
19+ /** Optional: Stop auto-save timer and flush pending writes. */
20+ stopAutoSave ?( ) : Promise < void > ;
21+ }
22+
923/**
1024 * Configuration options for the InMemory driver.
1125 * Aligned with @objectstack/spec MemoryConfigSchema.
@@ -17,6 +31,21 @@ export interface InMemoryDriverConfig {
1731 strictMode ?: boolean ;
1832 /** Optional: Logger instance */
1933 logger ?: Logger ;
34+ /**
35+ * Optional persistence configuration.
36+ * - `'file'` — File-system persistence with defaults (Node.js only)
37+ * - `'local'` — localStorage persistence with defaults (Browser only)
38+ * - `{ type: 'file', path?: string, autoSaveInterval?: number }` — File-system with options
39+ * - `{ type: 'local', key?: string }` — localStorage with options
40+ * - `{ adapter: PersistenceAdapterInterface }` — Custom adapter
41+ */
42+ persistence ?: string | {
43+ type ?: 'file' | 'local' ;
44+ path ?: string ;
45+ key ?: string ;
46+ autoSaveInterval ?: number ;
47+ adapter ?: PersistenceAdapterInterface ;
48+ } ;
2049}
2150
2251/**
@@ -51,6 +80,7 @@ export class InMemoryDriver implements DriverInterface {
5180 private logger : Logger ;
5281 private idCounters : Map < string , number > = new Map ( ) ;
5382 private transactions : Map < string , MemoryTransaction > = new Map ( ) ;
83+ private persistenceAdapter : PersistenceAdapterInterface | null = null ;
5484
5585 constructor ( config ?: InMemoryDriverConfig ) {
5686 this . config = config || { } ;
@@ -100,6 +130,37 @@ export class InMemoryDriver implements DriverInterface {
100130 // ===================================
101131
102132 async connect ( ) {
133+ // Initialize persistence adapter if configured
134+ await this . initPersistence ( ) ;
135+
136+ // Load persisted data if available
137+ if ( this . persistenceAdapter ) {
138+ const persisted = await this . persistenceAdapter . load ( ) ;
139+ if ( persisted ) {
140+ for ( const [ objectName , records ] of Object . entries ( persisted ) ) {
141+ this . db [ objectName ] = records ;
142+ // Update ID counters based on persisted data
143+ for ( const record of records ) {
144+ if ( record . id && typeof record . id === 'string' ) {
145+ // ID format: {objectName}-{timestamp}-{counter}
146+ const parts = record . id . split ( '-' ) ;
147+ const lastPart = parts [ parts . length - 1 ] ;
148+ const counter = parseInt ( lastPart , 10 ) ;
149+ if ( ! isNaN ( counter ) ) {
150+ const current = this . idCounters . get ( objectName ) || 0 ;
151+ if ( counter > current ) {
152+ this . idCounters . set ( objectName , counter ) ;
153+ }
154+ }
155+ }
156+ }
157+ }
158+ this . logger . info ( 'InMemory Database restored from persistence' , {
159+ tables : Object . keys ( persisted ) . length ,
160+ } ) ;
161+ }
162+ }
163+
103164 // Load initial data if provided
104165 if ( this . config . initialData ) {
105166 for ( const [ objectName , records ] of Object . entries ( this . config . initialData ) ) {
@@ -115,9 +176,22 @@ export class InMemoryDriver implements DriverInterface {
115176 } else {
116177 this . logger . info ( 'InMemory Database Connected (Virtual)' ) ;
117178 }
179+
180+ // Start auto-save if using file adapter
181+ if ( this . persistenceAdapter ?. startAutoSave ) {
182+ this . persistenceAdapter . startAutoSave ( ) ;
183+ }
118184 }
119185
120186 async disconnect ( ) {
187+ // Stop auto-save and flush pending writes
188+ if ( this . persistenceAdapter ) {
189+ if ( this . persistenceAdapter . stopAutoSave ) {
190+ await this . persistenceAdapter . stopAutoSave ( ) ;
191+ }
192+ await this . persistenceAdapter . flush ( ) ;
193+ }
194+
121195 const tableCount = Object . keys ( this . db ) . length ;
122196 const recordCount = Object . values ( this . db ) . reduce ( ( sum , table ) => sum + table . length , 0 ) ;
123197
@@ -226,6 +300,7 @@ export class InMemoryDriver implements DriverInterface {
226300 } ;
227301
228302 table . push ( newRecord ) ;
303+ this . markDirty ( ) ;
229304 this . logger . debug ( 'Record created' , { object, id : newRecord . id , tableSize : table . length } ) ;
230305 return { ...newRecord } ;
231306 }
@@ -253,6 +328,7 @@ export class InMemoryDriver implements DriverInterface {
253328 } ;
254329
255330 table [ index ] = updatedRecord ;
331+ this . markDirty ( ) ;
256332 this . logger . debug ( 'Record updated' , { object, id } ) ;
257333 return { ...updatedRecord } ;
258334 }
@@ -293,6 +369,7 @@ export class InMemoryDriver implements DriverInterface {
293369 }
294370
295371 table . splice ( index , 1 ) ;
372+ this . markDirty ( ) ;
296373 this . logger . debug ( 'Record deleted' , { object, id, tableSize : table . length } ) ;
297374 return true ;
298375 }
@@ -350,6 +427,7 @@ export class InMemoryDriver implements DriverInterface {
350427 }
351428 }
352429
430+ if ( count > 0 ) this . markDirty ( ) ;
353431 this . logger . debug ( 'UpdateMany completed' , { object, count } ) ;
354432 return { count } ;
355433 }
@@ -377,6 +455,7 @@ export class InMemoryDriver implements DriverInterface {
377455 }
378456
379457 const count = initialLength - this . db [ object ] . length ;
458+ if ( count > 0 ) this . markDirty ( ) ;
380459 this . logger . debug ( 'DeleteMany completed' , { object, count } ) ;
381460 return { count } ;
382461 }
@@ -435,6 +514,7 @@ export class InMemoryDriver implements DriverInterface {
435514 // Restore the snapshot
436515 this . db = tx . snapshot ;
437516 this . transactions . delete ( txId ) ;
517+ this . markDirty ( ) ;
438518 this . logger . debug ( 'Transaction rolled back' , { txId } ) ;
439519 }
440520
@@ -448,6 +528,7 @@ export class InMemoryDriver implements DriverInterface {
448528 async clear ( ) {
449529 this . db = { } ;
450530 this . idCounters . clear ( ) ;
531+ this . markDirty ( ) ;
451532 this . logger . debug ( 'All data cleared' ) ;
452533 }
453534
@@ -818,4 +899,65 @@ export class InMemoryDriver implements DriverInterface {
818899 const timestamp = Date . now ( ) ;
819900 return `${ key } -${ timestamp } -${ counter } ` ;
820901 }
902+
903+ // ===================================
904+ // Persistence
905+ // ===================================
906+
907+ /**
908+ * Mark the database as dirty, triggering persistence save.
909+ */
910+ private markDirty ( ) : void {
911+ if ( this . persistenceAdapter ) {
912+ this . persistenceAdapter . save ( this . db ) ;
913+ }
914+ }
915+
916+ /**
917+ * Flush pending persistence writes to ensure data is safely stored.
918+ */
919+ async flush ( ) : Promise < void > {
920+ if ( this . persistenceAdapter ) {
921+ await this . persistenceAdapter . flush ( ) ;
922+ }
923+ }
924+
925+ /**
926+ * Initialize the persistence adapter based on configuration.
927+ */
928+ private async initPersistence ( ) : Promise < void > {
929+ const persistence = this . config . persistence ;
930+ if ( ! persistence ) return ;
931+
932+ if ( typeof persistence === 'string' ) {
933+ if ( persistence === 'file' ) {
934+ const { FileSystemPersistenceAdapter } = await import ( './persistence/file-adapter.js' ) ;
935+ this . persistenceAdapter = new FileSystemPersistenceAdapter ( ) ;
936+ } else if ( persistence === 'local' ) {
937+ const { LocalStoragePersistenceAdapter } = await import ( './persistence/local-storage-adapter.js' ) ;
938+ this . persistenceAdapter = new LocalStoragePersistenceAdapter ( ) ;
939+ } else {
940+ throw new Error ( `Unknown persistence type: "${ persistence } ". Use 'file' or 'local'.` ) ;
941+ }
942+ } else if ( 'adapter' in persistence && persistence . adapter ) {
943+ this . persistenceAdapter = persistence . adapter ;
944+ } else if ( 'type' in persistence ) {
945+ if ( persistence . type === 'file' ) {
946+ const { FileSystemPersistenceAdapter } = await import ( './persistence/file-adapter.js' ) ;
947+ this . persistenceAdapter = new FileSystemPersistenceAdapter ( {
948+ path : persistence . path ,
949+ autoSaveInterval : persistence . autoSaveInterval ,
950+ } ) ;
951+ } else if ( persistence . type === 'local' ) {
952+ const { LocalStoragePersistenceAdapter } = await import ( './persistence/local-storage-adapter.js' ) ;
953+ this . persistenceAdapter = new LocalStoragePersistenceAdapter ( {
954+ key : persistence . key ,
955+ } ) ;
956+ }
957+ }
958+
959+ if ( this . persistenceAdapter ) {
960+ this . logger . debug ( 'Persistence adapter initialized' ) ;
961+ }
962+ }
821963}
0 commit comments