Skip to content

Commit b5f4ade

Browse files
Copilothotlong
andcommitted
feat: add auto-detect persistence strategy for memory driver
Adds 'auto' mode to PersistenceTypeSchema that detects browser vs Node.js environment at runtime and automatically selects the best persistence strategy: - Browser → localStorage persistence - Node.js → file-system persistence Changes: - spec: Add 'auto' to PersistenceTypeSchema enum - spec: Add AutoPersistenceConfigSchema with optional overrides - spec: Update MemoryPersistenceConfigSchema union to include auto - driver: Add isBrowserEnvironment() detection method - driver: Handle 'auto' in initPersistence() for both shorthand and object config - tests: Add schema validation tests for auto persistence - tests: Add driver integration tests for auto environment detection Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 2a95cfe commit b5f4ade

4 files changed

Lines changed: 198 additions & 5 deletions

File tree

packages/plugins/driver-memory/src/memory-driver.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ export interface InMemoryDriverConfig {
3535
* Optional persistence configuration.
3636
* - `'file'` — File-system persistence with defaults (Node.js only)
3737
* - `'local'` — localStorage persistence with defaults (Browser only)
38+
* - `'auto'` — Auto-detect environment (browser → localStorage, Node.js → file)
3839
* - `{ type: 'file', path?: string, autoSaveInterval?: number }` — File-system with options
3940
* - `{ type: 'local', key?: string }` — localStorage with options
41+
* - `{ type: 'auto', path?: string, key?: string, autoSaveInterval?: number }` — Auto-detect with options
4042
* - `{ adapter: PersistenceAdapterInterface }` — Custom adapter
4143
*/
4244
persistence?: string | {
43-
type?: 'file' | 'local';
45+
type?: 'file' | 'local' | 'auto';
4446
path?: string;
4547
key?: string;
4648
autoSaveInterval?: number;
@@ -922,6 +924,13 @@ export class InMemoryDriver implements DriverInterface {
922924
}
923925
}
924926

927+
/**
928+
* Detect whether the current runtime is a browser environment.
929+
*/
930+
private isBrowserEnvironment(): boolean {
931+
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
932+
}
933+
925934
/**
926935
* Initialize the persistence adapter based on configuration.
927936
*/
@@ -930,19 +939,44 @@ export class InMemoryDriver implements DriverInterface {
930939
if (!persistence) return;
931940

932941
if (typeof persistence === 'string') {
933-
if (persistence === 'file') {
942+
if (persistence === 'auto') {
943+
if (this.isBrowserEnvironment()) {
944+
const { LocalStoragePersistenceAdapter } = await import('./persistence/local-storage-adapter.js');
945+
this.persistenceAdapter = new LocalStoragePersistenceAdapter();
946+
this.logger.debug('Auto-detected browser environment, using localStorage persistence');
947+
} else {
948+
const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
949+
this.persistenceAdapter = new FileSystemPersistenceAdapter();
950+
this.logger.debug('Auto-detected Node.js environment, using file persistence');
951+
}
952+
} else if (persistence === 'file') {
934953
const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
935954
this.persistenceAdapter = new FileSystemPersistenceAdapter();
936955
} else if (persistence === 'local') {
937956
const { LocalStoragePersistenceAdapter } = await import('./persistence/local-storage-adapter.js');
938957
this.persistenceAdapter = new LocalStoragePersistenceAdapter();
939958
} else {
940-
throw new Error(`Unknown persistence type: "${persistence}". Use 'file' or 'local'.`);
959+
throw new Error(`Unknown persistence type: "${persistence}". Use 'file', 'local', or 'auto'.`);
941960
}
942961
} else if ('adapter' in persistence && persistence.adapter) {
943962
this.persistenceAdapter = persistence.adapter;
944963
} else if ('type' in persistence) {
945-
if (persistence.type === 'file') {
964+
if (persistence.type === 'auto') {
965+
if (this.isBrowserEnvironment()) {
966+
const { LocalStoragePersistenceAdapter } = await import('./persistence/local-storage-adapter.js');
967+
this.persistenceAdapter = new LocalStoragePersistenceAdapter({
968+
key: persistence.key,
969+
});
970+
this.logger.debug('Auto-detected browser environment, using localStorage persistence');
971+
} else {
972+
const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
973+
this.persistenceAdapter = new FileSystemPersistenceAdapter({
974+
path: persistence.path,
975+
autoSaveInterval: persistence.autoSaveInterval,
976+
});
977+
this.logger.debug('Auto-detected Node.js environment, using file persistence');
978+
}
979+
} else if (persistence.type === 'file') {
946980
const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
947981
this.persistenceAdapter = new FileSystemPersistenceAdapter({
948982
path: persistence.path,

packages/plugins/driver-memory/src/persistence/persistence.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,40 @@ describe('InMemoryDriver Persistence', () => {
155155
});
156156
});
157157

158+
describe('Auto Persistence', () => {
159+
it('should auto-detect Node.js environment and use file persistence with shorthand', async () => {
160+
// In Node.js, 'auto' should select file persistence
161+
const driver = new InMemoryDriver({ persistence: 'auto' });
162+
await driver.connect();
163+
await driver.create('items', { id: '1', name: 'Widget' });
164+
await driver.disconnect();
165+
});
166+
167+
it('should auto-detect Node.js environment and use file persistence with object config', async () => {
168+
const filePath = path.join(TEST_DATA_DIR, 'auto-test-db.json');
169+
const driver1 = new InMemoryDriver({
170+
persistence: { type: 'auto', path: filePath, autoSaveInterval: 100 },
171+
});
172+
await driver1.connect();
173+
await driver1.create('users', { id: '1', name: 'Alice' });
174+
await driver1.flush();
175+
await driver1.disconnect();
176+
177+
// Verify file was created (Node.js env selects file adapter)
178+
expect(fs.existsSync(filePath)).toBe(true);
179+
180+
// Restore from file
181+
const driver2 = new InMemoryDriver({
182+
persistence: { type: 'auto', path: filePath, autoSaveInterval: 100 },
183+
});
184+
await driver2.connect();
185+
const users = await driver2.find('users', { object: 'users' });
186+
expect(users).toHaveLength(1);
187+
expect(users[0].name).toBe('Alice');
188+
await driver2.disconnect();
189+
});
190+
});
191+
158192
describe('Bulk Operations with Persistence', () => {
159193
it('should persist bulk creates', async () => {
160194
const driver1 = new InMemoryDriver({

packages/spec/src/data/driver/memory.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
LocalStoragePersistenceConfigSchema,
99
CustomPersistenceConfigSchema,
1010
PersistenceAdapterSchema,
11+
AutoPersistenceConfigSchema,
1112
} from './memory.zod';
1213

1314
describe('MemoryConfigSchema', () => {
@@ -63,6 +64,14 @@ describe('MemoryConfigSchema', () => {
6364
expect(config.persistence).toBe('local');
6465
});
6566

67+
it('should accept persistence shorthand "auto"', () => {
68+
const config = MemoryConfigSchema.parse({
69+
persistence: 'auto',
70+
});
71+
72+
expect(config.persistence).toBe('auto');
73+
});
74+
6675
it('should accept persistence with file object config', () => {
6776
const config = MemoryConfigSchema.parse({
6877
persistence: {
@@ -104,6 +113,34 @@ describe('MemoryConfigSchema', () => {
104113
expect(p.key).toBe('myapp:db');
105114
});
106115

116+
it('should accept persistence with auto object config', () => {
117+
const config = MemoryConfigSchema.parse({
118+
persistence: {
119+
type: 'auto',
120+
path: '/var/data/memory.json',
121+
key: 'myapp:db',
122+
autoSaveInterval: 5000,
123+
},
124+
});
125+
126+
const p = config.persistence as { type: 'auto'; path?: string; key?: string; autoSaveInterval?: number };
127+
expect(p.type).toBe('auto');
128+
expect(p.path).toBe('/var/data/memory.json');
129+
expect(p.key).toBe('myapp:db');
130+
expect(p.autoSaveInterval).toBe(5000);
131+
});
132+
133+
it('should accept auto persistence without overrides', () => {
134+
const config = MemoryConfigSchema.parse({
135+
persistence: {
136+
type: 'auto',
137+
},
138+
});
139+
140+
const p = config.persistence as { type: 'auto' };
141+
expect(p.type).toBe('auto');
142+
});
143+
107144
it('should accept persistence with custom adapter', () => {
108145
const mockAdapter = {
109146
load: async () => null,
@@ -205,6 +242,10 @@ describe('PersistenceTypeSchema', () => {
205242
expect(PersistenceTypeSchema.parse('local')).toBe('local');
206243
});
207244

245+
it('should accept auto type', () => {
246+
expect(PersistenceTypeSchema.parse('auto')).toBe('auto');
247+
});
248+
208249
it('should reject invalid type', () => {
209250
expect(() => PersistenceTypeSchema.parse('indexeddb')).toThrow();
210251
});
@@ -279,6 +320,40 @@ describe('CustomPersistenceConfigSchema', () => {
279320
});
280321
});
281322

323+
describe('AutoPersistenceConfigSchema', () => {
324+
it('should accept minimal auto config', () => {
325+
const config = AutoPersistenceConfigSchema.parse({
326+
type: 'auto',
327+
});
328+
329+
expect(config.type).toBe('auto');
330+
expect(config.path).toBeUndefined();
331+
expect(config.key).toBeUndefined();
332+
expect(config.autoSaveInterval).toBeUndefined();
333+
});
334+
335+
it('should accept auto config with all overrides', () => {
336+
const config = AutoPersistenceConfigSchema.parse({
337+
type: 'auto',
338+
path: '/data/store.json',
339+
key: 'myapp:db',
340+
autoSaveInterval: 5000,
341+
});
342+
343+
expect(config.type).toBe('auto');
344+
expect(config.path).toBe('/data/store.json');
345+
expect(config.key).toBe('myapp:db');
346+
expect(config.autoSaveInterval).toBe(5000);
347+
});
348+
349+
it('should reject auto config with invalid autoSaveInterval', () => {
350+
expect(() => AutoPersistenceConfigSchema.parse({
351+
type: 'auto',
352+
autoSaveInterval: 50, // Below minimum of 100
353+
})).toThrow();
354+
});
355+
});
356+
282357
describe('MemoryPersistenceConfigSchema', () => {
283358
it('should accept shorthand "file"', () => {
284359
const config = MemoryPersistenceConfigSchema.parse('file');
@@ -290,6 +365,11 @@ describe('MemoryPersistenceConfigSchema', () => {
290365
expect(config).toBe('local');
291366
});
292367

368+
it('should accept shorthand "auto"', () => {
369+
const config = MemoryPersistenceConfigSchema.parse('auto');
370+
expect(config).toBe('auto');
371+
});
372+
293373
it('should accept file object config', () => {
294374
const config = MemoryPersistenceConfigSchema.parse({
295375
type: 'file',
@@ -306,6 +386,15 @@ describe('MemoryPersistenceConfigSchema', () => {
306386
expect(config).toEqual({ type: 'local', key: 'myapp:db' });
307387
});
308388

389+
it('should accept auto object config', () => {
390+
const config = MemoryPersistenceConfigSchema.parse({
391+
type: 'auto',
392+
path: '/data/store.json',
393+
key: 'myapp:db',
394+
});
395+
expect(config).toEqual({ type: 'auto', path: '/data/store.json', key: 'myapp:db' });
396+
});
397+
309398
it('should accept custom adapter', () => {
310399
const adapter = {
311400
load: async () => null,

packages/spec/src/data/driver/memory.zod.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ export type PersistenceAdapter = z.infer<typeof PersistenceAdapterSchema>;
4242
* Persistence type enum.
4343
* - `file`: Persist to disk file (Node.js only).
4444
* - `local`: Persist to localStorage (Browser only).
45+
* - `auto`: Auto-detect environment and choose the best strategy.
46+
* Uses `localStorage` in browser environments and `file` in Node.js.
4547
*/
46-
export const PersistenceTypeSchema = z.enum(['file', 'local']).describe('Persistence backend type');
48+
export const PersistenceTypeSchema = z.enum(['file', 'local', 'auto']).describe('Persistence backend type');
4749

4850
export type PersistenceType = z.infer<typeof PersistenceTypeSchema>;
4951

@@ -83,23 +85,52 @@ export const CustomPersistenceConfigSchema = z.object({
8385

8486
export type CustomPersistenceConfig = z.infer<typeof CustomPersistenceConfigSchema>;
8587

88+
/**
89+
* Auto-detect persistence configuration.
90+
* Automatically selects the best persistence strategy based on the runtime environment:
91+
* - Browser → localStorage persistence
92+
* - Node.js → File-system persistence
93+
*
94+
* Optional overrides allow customizing the file path or localStorage key
95+
* used by the auto-detected adapter.
96+
*/
97+
export const AutoPersistenceConfigSchema = z.object({
98+
type: z.literal('auto'),
99+
/** File path override when running in Node.js. */
100+
path: z.string().optional().describe('File path override for Node.js environments'),
101+
/** Auto-save interval override when running in Node.js. Default: 2000ms. */
102+
autoSaveInterval: z.number().min(100).optional().describe('Auto-save interval override for Node.js environments'),
103+
/** localStorage key override when running in a browser. */
104+
key: z.string().optional().describe('localStorage key override for browser environments'),
105+
}).describe('Auto-detect persistence configuration');
106+
107+
export type AutoPersistenceConfig = z.infer<typeof AutoPersistenceConfigSchema>;
108+
86109
/**
87110
* Unified persistence configuration.
88111
*
89112
* Supports shorthand strings and detailed object configs:
90113
* - `'file'` — File-system persistence with defaults (Node.js)
91114
* - `'local'` — localStorage persistence with defaults (Browser)
115+
* - `'auto'` — Auto-detect environment (browser → localStorage, Node.js → file)
92116
* - `{ type: 'file', path?: string }` — File-system with custom path
93117
* - `{ type: 'local', key?: string }` — localStorage with custom key
118+
* - `{ type: 'auto', path?: string, key?: string }` — Auto-detect with overrides
94119
* - `{ adapter: PersistenceAdapter }` — Custom adapter
95120
*
96121
* @example
122+
* // Auto-detect environment
123+
* persistence: 'auto'
124+
*
97125
* // Node.js with defaults
98126
* persistence: 'file'
99127
*
100128
* // Browser with defaults
101129
* persistence: 'local'
102130
*
131+
* // Auto-detect with overrides
132+
* persistence: { type: 'auto', path: '/var/data/memory.json', key: 'myapp:db' }
133+
*
103134
* // Custom file path
104135
* persistence: { type: 'file', path: '/var/data/memory.json' }
105136
*
@@ -110,6 +141,7 @@ export const MemoryPersistenceConfigSchema = z.union([
110141
PersistenceTypeSchema,
111142
FilePersistenceConfigSchema,
112143
LocalStoragePersistenceConfigSchema,
144+
AutoPersistenceConfigSchema,
113145
CustomPersistenceConfigSchema,
114146
]).describe('Persistence configuration for the memory driver');
115147

@@ -149,13 +181,17 @@ export const MemoryConfigSchema = z.object({
149181
* Optional persistence configuration.
150182
* When configured, the memory store automatically saves and restores data.
151183
*
184+
* - `'auto'`: Auto-detect environment (browser → localStorage, Node.js → file)
152185
* - `'file'`: Persist to disk file (Node.js only, default path: `.objectstack/data/memory-driver.json`)
153186
* - `'local'`: Persist to localStorage (Browser only, default key: `objectstack:memory-db`)
187+
* - `{ type: 'auto', path?: string, key?: string }`: Auto-detect with overrides
154188
* - `{ type: 'file', path?: string }`: File-system with custom path
155189
* - `{ type: 'local', key?: string }`: localStorage with custom key
156190
* - `{ adapter: PersistenceAdapter }`: Custom persistence adapter
157191
*
158192
* @example
193+
* // Auto-detect environment
194+
* new InMemoryDriver({ persistence: 'auto' })
159195
* // Node.js
160196
* new InMemoryDriver({ persistence: 'file' })
161197
* // Browser

0 commit comments

Comments
 (0)