diff --git a/packages/plugins/driver-memory/src/memory-driver.ts b/packages/plugins/driver-memory/src/memory-driver.ts index 61e1a1aa29..dadfddcd22 100644 --- a/packages/plugins/driver-memory/src/memory-driver.ts +++ b/packages/plugins/driver-memory/src/memory-driver.ts @@ -33,7 +33,7 @@ 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 @@ -41,6 +41,10 @@ export interface InMemoryDriverConfig { * - `{ 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'; @@ -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 { const persistence = this.config.persistence === undefined ? 'auto' : this.config.persistence; @@ -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(); @@ -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({ diff --git a/packages/plugins/driver-memory/src/persistence/persistence.test.ts b/packages/plugins/driver-memory/src/persistence/persistence.test.ts index 1c24da1eea..073ebd3775 100644 --- a/packages/plugins/driver-memory/src/persistence/persistence.test.ts +++ b/packages/plugins/driver-memory/src/persistence/persistence.test.ts @@ -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(); + }); + + 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 = {}; + const customAdapter = { + load: async () => Object.keys(stored).length > 0 ? { ...stored } : null, + save: async (db: Record) => { + 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); + }); + }); }); diff --git a/packages/spec/src/data/driver/memory.zod.ts b/packages/spec/src/data/driver/memory.zod.ts index c714499fad..c323c1b6ea 100644 --- a/packages/spec/src/data/driver/memory.zod.ts +++ b/packages/spec/src/data/driver/memory.zod.ts @@ -43,7 +43,8 @@ export type PersistenceAdapter = z.infer; * - `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'); @@ -89,7 +90,14 @@ export type CustomPersistenceConfig = z.infer; * 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 @@ -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() @@ -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)'),