Uh oh!
There was an error while loading. Please reload this page.
fix: auto-detect serverless runtime in memory-driver, skip file persistence - #827
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…th warning In serverless environments (Vercel, AWS Lambda, Netlify, Azure Functions, Google Cloud Functions, Deno Deploy), auto persistence mode now disables file-system persistence and emits a warning instead of silently choosing an adapter that will lose data. - Add isServerlessEnvironment() detection via well-known env vars - Auto mode: browser → localStorage, serverless → disabled+warn, Node.js → file - Explicit 'file' or custom adapter still works in serverless (user's choice) - Update schema JSDoc to document serverless behavior - Add 4 new persistence tests for serverless scenarios Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Address code review feedback: - Extract duplicated warning message to static SERVERLESS_PERSISTENCE_WARNING constant - Add JSDoc note about process/env unavailability in non-Node.js runtimes Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Pull request overview
This pull request fixes a critical data loss bug in the memory-driver where the default persistence: 'auto' configuration would silently use file-system persistence in serverless environments (Vercel, AWS Lambda, etc.), leading to complete data loss due to ephemeral/read-only filesystems.
Changes:
- Adds serverless runtime detection by checking well-known environment variables from major platforms
- Updates auto-persistence logic to disable file persistence with a warning in serverless environments while browser and explicit configurations remain unaffected
- Enhances documentation across schema and implementation to clearly warn about serverless behavior and provide actionable alternatives
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/spec/src/data/driver/memory.zod.ts | Updated JSDoc for PersistenceTypeSchema, AutoPersistenceConfigSchema, and MemoryConfigSchema.persistence to document serverless behavior and provide clear guidance on alternatives |
| packages/plugins/driver-memory/src/memory-driver.ts | Added isServerlessEnvironment() detection method and updated initPersistence() to check serverless environment and emit warning instead of using file adapter in auto mode |
| packages/plugins/driver-memory/src/persistence/persistence.test.ts | Added comprehensive test suite for serverless environment detection covering auto mode (object and shorthand), explicit file mode, and custom adapters in serverless context |
| 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(); |
There was a problem hiding this comment.
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.
| 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(); | |
| } |
| 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<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); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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.
persistence: 'auto'(the default) silently selects the file-system adapter in serverless environments where the filesystem is ephemeral or read-only, causing complete data loss with no warning.Changes
isServerlessEnvironment()checks well-known env vars (VERCEL,AWS_LAMBDA_FUNCTION_NAME,NETLIFY,FUNCTIONS_WORKER_RUNTIME,K_SERVICE,FUNCTION_TARGET,DENO_DEPLOYMENT_ID)logger.warn(), Node.js → file. Explicitpersistence: 'file'or custom adapter are unaffected.AutoPersistenceConfigSchema,MemoryConfigSchema.persistence, andPersistenceTypeSchemaupdated to document serverless behaviorVERCEL, auto shorthand underAWS_LAMBDA, explicit file still works in serverless, custom adapter still works in serverlessOriginal prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.