feat: defer AsyncLocalStorage creation for v8 startup snapshots (#1946)
## Summary - Defer `AsyncLocalStorage` creation when `v8.startupSnapshot.isBuildingSnapshot()` is true, making Koa compatible with Node.js startup snapshots - Register a `v8.startupSnapshot.addDeserializeCallback` to properly initialize `ctxStorage` after snapshot restoration - Extract `getAsyncLocalStorage()` helper to consolidate creation logic ## Test plan - [x] All 429 existing tests pass with 0 failures - [x] `currentContext` tests verify both `asyncLocalStorage: true` and custom `AsyncLocalStorage` instance paths work correctly - [x] Normal (non-snapshot) code path is unchanged — `v8.startupSnapshot?.isBuildingSnapshot?.()` returns `undefined` in regular execution 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('node:test')
|
||||
const { describe, it, beforeEach, afterEach } = require('node:test')
|
||||
const v8 = require('node:v8')
|
||||
const request = require('supertest')
|
||||
const assert = require('node:assert/strict')
|
||||
const Koa = require('../..')
|
||||
@@ -113,4 +114,85 @@ describe('app.currentContext', () => {
|
||||
await request(app.callback()).get('/').expect('ok')
|
||||
assert(app.currentContext === undefined)
|
||||
})
|
||||
|
||||
describe('v8 startup snapshot', () => {
|
||||
let originalStartupSnapshot
|
||||
|
||||
beforeEach(() => {
|
||||
originalStartupSnapshot = v8.startupSnapshot
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
v8.startupSnapshot = originalStartupSnapshot
|
||||
})
|
||||
|
||||
it('should defer AsyncLocalStorage creation when building snapshot', () => {
|
||||
let deserializeCallback
|
||||
v8.startupSnapshot = {
|
||||
isBuildingSnapshot: () => true,
|
||||
addDeserializeCallback: (cb, data) => {
|
||||
deserializeCallback = { cb, data }
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Koa({ asyncLocalStorage: true })
|
||||
assert.strictEqual(app.ctxStorage, null)
|
||||
assert(deserializeCallback, 'deserialize callback should be registered')
|
||||
|
||||
// simulate snapshot deserialization
|
||||
deserializeCallback.cb(deserializeCallback.data)
|
||||
assert(app.ctxStorage instanceof AsyncLocalStorage)
|
||||
})
|
||||
|
||||
it('should defer with custom AsyncLocalStorage when building snapshot', () => {
|
||||
const customStorage = new AsyncLocalStorage()
|
||||
let deserializeCallback
|
||||
v8.startupSnapshot = {
|
||||
isBuildingSnapshot: () => true,
|
||||
addDeserializeCallback: (cb, data) => {
|
||||
deserializeCallback = { cb, data }
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Koa({ asyncLocalStorage: customStorage })
|
||||
assert.strictEqual(app.ctxStorage, null)
|
||||
|
||||
// simulate snapshot deserialization
|
||||
deserializeCallback.cb(deserializeCallback.data)
|
||||
assert(app.ctxStorage instanceof AsyncLocalStorage)
|
||||
assert.strictEqual(app.ctxStorage, customStorage)
|
||||
})
|
||||
|
||||
it('should work normally after deserialization', async () => {
|
||||
let deserializeCallback
|
||||
v8.startupSnapshot = {
|
||||
isBuildingSnapshot: () => true,
|
||||
addDeserializeCallback: (cb, data) => {
|
||||
deserializeCallback = { cb, data }
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Koa({ asyncLocalStorage: true })
|
||||
|
||||
// simulate snapshot deserialization
|
||||
deserializeCallback.cb(deserializeCallback.data)
|
||||
|
||||
app.use(async ctx => {
|
||||
assert(ctx === app.currentContext)
|
||||
ctx.body = 'ok'
|
||||
})
|
||||
|
||||
await request(app.callback()).get('/').expect('ok')
|
||||
assert(app.currentContext === undefined)
|
||||
})
|
||||
|
||||
it('should not defer when not building snapshot', () => {
|
||||
v8.startupSnapshot = {
|
||||
isBuildingSnapshot: () => false
|
||||
}
|
||||
|
||||
const app = new Koa({ asyncLocalStorage: true })
|
||||
assert(app.ctxStorage instanceof AsyncLocalStorage)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
const util = require('node:util')
|
||||
const v8 = require('node:v8')
|
||||
const debug = util.debuglog('koa:application')
|
||||
const Emitter = require('node:events')
|
||||
const Stream = require('node:stream')
|
||||
@@ -40,6 +41,13 @@ const only = require('./only.js')
|
||||
* Inherits from `Emitter.prototype`.
|
||||
*/
|
||||
|
||||
function getAsyncLocalStorage (options) {
|
||||
if (options.asyncLocalStorage instanceof AsyncLocalStorage) {
|
||||
return options.asyncLocalStorage
|
||||
}
|
||||
return new AsyncLocalStorage()
|
||||
}
|
||||
|
||||
module.exports = class Application extends Emitter {
|
||||
/**
|
||||
* Initialize a new `Application`.
|
||||
@@ -81,10 +89,13 @@ module.exports = class Application extends Emitter {
|
||||
this[util.inspect.custom] = this.inspect
|
||||
}
|
||||
if (options.asyncLocalStorage) {
|
||||
if (options.asyncLocalStorage instanceof AsyncLocalStorage) {
|
||||
this.ctxStorage = options.asyncLocalStorage
|
||||
if (v8.startupSnapshot?.isBuildingSnapshot?.()) {
|
||||
this.ctxStorage = null
|
||||
v8.startupSnapshot.addDeserializeCallback(({ app, options }) => {
|
||||
app.ctxStorage = getAsyncLocalStorage(options)
|
||||
}, { app: this, options })
|
||||
} else {
|
||||
this.ctxStorage = new AsyncLocalStorage()
|
||||
this.ctxStorage = getAsyncLocalStorage(options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user