Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion packages/plugins/driver-memory/src/memory-driver.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,14 +33,18 @@ export interface InMemoryDriverConfig {
logger?: Logger;
/**
* Persistence configuration. Defaults to `'auto'`.
* - `'auto'` (default) — Auto-detect environment (browser → localStorage, Node.js → file)
* - `'auto'` (default) — Auto-detect environment (browser → localStorage, Node.js → file, serverless → disabled)
* - `'file'` — File-system persistence with defaults (Node.js only)
* - `'local'` — localStorage persistence with defaults (Browser only)
* - `{ type: 'file', path?: string, autoSaveInterval?: number }` — File-system with options
* - `{ type: 'local', key?: string }` — localStorage with options
* - `{ type: 'auto', path?: string, key?: string, autoSaveInterval?: number }` — Auto-detect with options
* - `{ adapter: PersistenceAdapterInterface }` — Custom adapter
* - `false` — Disable persistence (pure in-memory)
*
* ⚠️ In serverless environments (Vercel, AWS Lambda, Netlify, etc.),
* auto mode disables file persistence to prevent silent data loss.
* Use `persistence: false` or supply a custom adapter for serverless deployments.
*/
persistence?: string | false | {
type?: 'file' | 'local' | 'auto';
Expand DownExpand Up@@ -932,10 +936,52 @@ export class InMemoryDriver implements DriverInterface {
return typeof globalThis.localStorage !== 'undefined';
}

/**
* Detect whether the current runtime is a serverless/edge environment.
*
* Checks well-known environment variables set by serverless platforms:
* - `VERCEL` / `VERCEL_ENV` — Vercel Functions / Edge
* - `AWS_LAMBDA_FUNCTION_NAME` — AWS Lambda
* - `NETLIFY` — Netlify Functions
* - `FUNCTIONS_WORKER_RUNTIME` — Azure Functions
* - `K_SERVICE` — Google Cloud Run / Cloud Functions
* - `FUNCTION_TARGET` — Google Cloud Functions (Node.js)
* - `DENO_DEPLOYMENT_ID` — Deno Deploy
*
* Returns `false` when `process` or `process.env` is unavailable
* (e.g. browser or edge runtimes without a Node.js process object).
*/
private isServerlessEnvironment(): boolean {
if (typeof globalThis.process === 'undefined' || !globalThis.process.env) {
return false;
}
const env = globalThis.process.env;
return !!(
env.VERCEL ||
env.VERCEL_ENV ||
env.AWS_LAMBDA_FUNCTION_NAME ||
env.NETLIFY ||
env.FUNCTIONS_WORKER_RUNTIME ||
env.K_SERVICE ||
env.FUNCTION_TARGET ||
env.DENO_DEPLOYMENT_ID
);
}

private static readonly SERVERLESS_PERSISTENCE_WARNING =
'Serverless environment detected — file-system persistence is disabled in auto mode. ' +
'Data will NOT be persisted across function invocations. ' +
'Set persistence: false to silence this warning, or provide a custom adapter ' +
'(e.g. Upstash Redis, Vercel KV) via persistence: { adapter: yourAdapter }.';

/**
* Initialize the persistence adapter based on configuration.
* Defaults to 'auto' when persistence is not specified.
* Use `persistence: false` to explicitly disable persistence.
*
* In serverless environments (Vercel, AWS Lambda, etc.), auto mode disables
* file-system persistence and emits a warning. Use `persistence: false` or
* supply a custom adapter for serverless-safe operation.
*/
private async initPersistence(): Promise<void> {
const persistence = this.config.persistence === undefined ? 'auto' : this.config.persistence;
Expand All@@ -947,6 +993,8 @@ export class InMemoryDriver implements DriverInterface {
const { LocalStoragePersistenceAdapter } = await import('./persistence/local-storage-adapter.js');
this.persistenceAdapter = new LocalStoragePersistenceAdapter();
this.logger.debug('Auto-detected browser environment, using localStorage persistence');
} else if (this.isServerlessEnvironment()) {
this.logger.warn(InMemoryDriver.SERVERLESS_PERSISTENCE_WARNING);
} else {
const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
this.persistenceAdapter = new FileSystemPersistenceAdapter();
Expand All@@ -971,6 +1019,8 @@ export class InMemoryDriver implements DriverInterface {
key: persistence.key,
});
this.logger.debug('Auto-detected browser environment, using localStorage persistence');
} else if (this.isServerlessEnvironment()) {
this.logger.warn(InMemoryDriver.SERVERLESS_PERSISTENCE_WARNING);
} else {
const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
this.persistenceAdapter = new FileSystemPersistenceAdapter({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -212,4 +212,87 @@ describe('InMemoryDriver Persistence', () => {
await driver2.disconnect();
});
});

describe('Serverless Environment Detection', () => {
const serverlessEnvVars = [
'VERCEL',
'VERCEL_ENV',
'AWS_LAMBDA_FUNCTION_NAME',
'NETLIFY',
'FUNCTIONS_WORKER_RUNTIME',
'K_SERVICE',
'FUNCTION_TARGET',
'DENO_DEPLOYMENT_ID',
];

afterEach(() => {
// Clean up all serverless env vars after each test
for (const key of serverlessEnvVars) {
delete process.env[key];
}
});

it('should disable file persistence in auto mode when VERCEL env is set', async () => {
process.env.VERCEL = '1';
const filePath = path.join(TEST_DATA_DIR, 'serverless-test.json');
const driver = new InMemoryDriver({
persistence: { type: 'auto', path: filePath },
});
await driver.connect();
await driver.create('items', { id: '1', name: 'Widget' });
await driver.flush();
await driver.disconnect();

// File should NOT have been created because auto mode skips file persistence in serverless
expect(fs.existsSync(filePath)).toBe(false);
});

it('should disable file persistence in auto shorthand mode when AWS_LAMBDA_FUNCTION_NAME is set', async () => {
process.env.AWS_LAMBDA_FUNCTION_NAME = 'my-function';
const driver = new InMemoryDriver({ persistence: 'auto' });
await driver.connect();
await driver.create('items', { id: '1', name: 'Widget' });

// Should work as pure in-memory without errors
const items = await driver.find('items', { object: 'items' });
expect(items).toHaveLength(1);

await driver.disconnect();
Comment on lines +237 to +260

CopilotAIFeb 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serverless environment tests verify that file persistence is disabled but don't verify that the warning message is actually logged. Consider adding assertions to verify the warning is emitted, for example by using a mock logger or by spying on logger.warn. This would ensure the warning behavior is tested and prevent regressions where the warning might be accidentally removed.

Suggested change
constfilePath=path.join(TEST_DATA_DIR,'serverless-test.json');
constdriver=newInMemoryDriver({
persistence: {type: 'auto',path: filePath},
});
awaitdriver.connect();
awaitdriver.create('items',{id: '1',name: 'Widget'});
awaitdriver.flush();
awaitdriver.disconnect();
// File should NOT have been created because auto mode skips file persistence in serverless
expect(fs.existsSync(filePath)).toBe(false);
});
it('should disable file persistence in auto shorthand mode when AWS_LAMBDA_FUNCTION_NAME is set',async()=>{
process.env.AWS_LAMBDA_FUNCTION_NAME='my-function';
constdriver=newInMemoryDriver({persistence: 'auto'});
awaitdriver.connect();
awaitdriver.create('items',{id: '1',name: 'Widget'});
// Should work as pure in-memory without errors
constitems=awaitdriver.find('items',{object: 'items'});
expect(items).toHaveLength(1);
awaitdriver.disconnect();
constwarnSpy=vi.spyOn(console,'warn').mockImplementation(()=>{});
try{
constfilePath=path.join(TEST_DATA_DIR,'serverless-test.json');
constdriver=newInMemoryDriver({
persistence: {type: 'auto',path: filePath},
});
awaitdriver.connect();
awaitdriver.create('items',{id: '1',name: 'Widget'});
awaitdriver.flush();
awaitdriver.disconnect();
// File should NOT have been created because auto mode skips file persistence in serverless
expect(fs.existsSync(filePath)).toBe(false);
// A warning should be logged when auto persistence is disabled in a serverless environment
expect(warnSpy).toHaveBeenCalled();
}finally{
warnSpy.mockRestore();
}
});
it('should disable file persistence in auto shorthand mode when AWS_LAMBDA_FUNCTION_NAME is set',async()=>{
process.env.AWS_LAMBDA_FUNCTION_NAME='my-function';
constwarnSpy=vi.spyOn(console,'warn').mockImplementation(()=>{});
try{
constdriver=newInMemoryDriver({persistence: 'auto'});
awaitdriver.connect();
awaitdriver.create('items',{id: '1',name: 'Widget'});
// Should work as pure in-memory without errors
constitems=awaitdriver.find('items',{object: 'items'});
expect(items).toHaveLength(1);
awaitdriver.disconnect();
// A warning should be logged when auto persistence is disabled in a serverless environment
expect(warnSpy).toHaveBeenCalled();
}finally{
warnSpy.mockRestore();
}

Copilot uses AI. Check for mistakes.
});

it('should still allow explicit file persistence in serverless if user requests it', async () => {
process.env.VERCEL = '1';
const filePath = path.join(TEST_DATA_DIR, 'explicit-file-serverless.json');
const driver = new InMemoryDriver({
persistence: { type: 'file', path: filePath, autoSaveInterval: 100 },
});
await driver.connect();
await driver.create('items', { id: '1', name: 'Widget' });
await driver.flush();
await driver.disconnect();

// Explicit 'file' type should still create the file even in serverless
expect(fs.existsSync(filePath)).toBe(true);
});

it('should still allow custom adapter in serverless', async () => {
process.env.NETLIFY = 'true';
const stored: Record<string, any[]> = {};
const customAdapter = {
load: async () => Object.keys(stored).length > 0 ? { ...stored } : null,
save: async (db: Record<string, any[]>) => {
for (const [k, v] of Object.entries(db)) { stored[k] = [...v]; }
},
flush: async () => {},
};

const driver = new InMemoryDriver({ persistence: { adapter: customAdapter } });
await driver.connect();
await driver.create('items', { id: '1', name: 'Widget' });
await driver.disconnect();

expect(stored.items).toBeDefined();
expect(stored.items).toHaveLength(1);
});
});
Comment on lines +216 to +297

CopilotAIFeb 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test case for edge runtime environments where both localStorage and serverless environment variables might be present (e.g., Vercel Edge Functions with polyfills). Consider adding a test that sets both a serverless env var and mocks localStorage to verify that browser detection takes precedence and uses localStorage persistence. This would document and protect the expected priority order.

Copilot uses AI. Check for mistakes.
});
21 changes: 18 additions & 3 deletions packages/spec/src/data/driver/memory.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,8 @@ export type PersistenceAdapter = z.infer<typeof PersistenceAdapterSchema>;
* - `file`: Persist to disk file (Node.js only).
* - `local`: Persist to localStorage (Browser only).
* - `auto`: Auto-detect environment and choose the best strategy.
* Uses `localStorage` in browser environments and `file` in Node.js.
* Uses `localStorage` in browser environments, `file` in standard Node.js,
* and disables persistence with a warning in serverless/edge runtimes.
*/
export const PersistenceTypeSchema = z.enum(['file', 'local', 'auto']).describe('Persistence backend type');

Expand DownExpand Up@@ -89,7 +90,14 @@ export type CustomPersistenceConfig = z.infer<typeof CustomPersistenceConfigSche
* Auto-detect persistence configuration.
* Automatically selects the best persistence strategy based on the runtime environment:
* - Browser → localStorage persistence
* - Node.js → File-system persistence
* - Serverless (Vercel, AWS Lambda, etc.) → Persistence disabled with warning
* - Node.js (standard) → File-system persistence
*
* **⚠️ Serverless Warning:** In serverless/edge environments the file system is
* ephemeral or read-only. Auto mode detects this and disables file persistence to
* prevent silent data loss. Use `persistence: false` to silence the warning, or
* supply a custom adapter (e.g. Upstash Redis, Vercel KV) via
* `persistence: { adapter: yourAdapter }`.
*
* Optional overrides allow customizing the file path or localStorage key
* used by the auto-detected adapter.
Expand All@@ -112,7 +120,7 @@ export type AutoPersistenceConfig = z.infer<typeof AutoPersistenceConfigSchema>;
* Supports shorthand strings and detailed object configs:
* - `'file'` — File-system persistence with defaults (Node.js)
* - `'local'` — localStorage persistence with defaults (Browser)
* - `'auto'` — Auto-detect environment (browser → localStorage, Node.js → file)
* - `'auto'` — Auto-detect environment (browser → localStorage, Node.js → file, serverless → disabled)
* - `{ type: 'file', path?: string }` — File-system with custom path
* - `{ type: 'local', key?: string }` — localStorage with custom key
* - `{ type: 'auto', path?: string, key?: string }` — Auto-detect with overrides
Expand DownExpand Up@@ -191,6 +199,11 @@ export const MemoryConfigSchema = z.object({
* - `{ adapter: PersistenceAdapter }`: Custom persistence adapter
* - `false`: Disable persistence (pure in-memory, data lost on disconnect)
*
* **⚠️ Serverless / Edge environments (Vercel, AWS Lambda, Netlify, etc.):**
* Auto mode detects serverless runtimes and disables file persistence to prevent
* silent data loss. Set `persistence: false` to opt-in to pure in-memory mode,
* or supply a custom adapter (e.g. Upstash Redis, Vercel KV) for durable storage.
*
* @example
* // Auto-detect environment (default)
* new InMemoryDriver()
Expand All@@ -200,6 +213,8 @@ export const MemoryConfigSchema = z.object({
* new InMemoryDriver({ persistence: 'local' })
* // Pure memory (no persistence)
* new InMemoryDriver({ persistence: false })
* // Custom adapter for serverless
* new InMemoryDriver({ persistence: { adapter: upstashAdapter } })
*/
persistence: MemoryPersistenceConfigSchema.or(z.literal(false)).default('auto').describe('Persistence configuration (defaults to auto-detect)'),

Expand Down