From 4d5c5fd70c447b0ffad115c6ded450a117a78eb7 Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 31 Mar 2026 09:47:11 +0800 Subject: [PATCH 1/5] feat(core): add snapshot mode to lifecycle (stop after didLoad phase) Add `snapshot` option to EggCoreOptions and LifecycleOptions. When enabled, lifecycle stops after didLoad phase completes, skipping willReady, didReady, and serverDidReady hooks. Used for V8 startup snapshot construction. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/egg.ts | 7 +++++++ packages/core/src/lifecycle.ts | 12 +++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/core/src/egg.ts b/packages/core/src/egg.ts index 2596923430..f7847f1618 100644 --- a/packages/core/src/egg.ts +++ b/packages/core/src/egg.ts @@ -33,6 +33,12 @@ export interface EggCoreOptions { env?: string; /** Skip lifecycle hooks, only trigger loadMetadata for manifest generation */ metadataOnly?: boolean; + /** + * When true, the application loads metadata only (plugins, configs, extensions, + * services, controllers) without starting servers, timers, or connections. + * Used for V8 startup snapshot construction. + */ + snapshot?: boolean; } export type EggCoreInitOptions = Partial; @@ -191,6 +197,7 @@ export class EggCore extends KoaApplication { baseDir: options.baseDir, app: this, logger: this.console, + snapshot: options.snapshot, }); this.lifecycle.on('error', (err) => this.emit('error', err)); this.lifecycle.on('ready_timeout', (id) => this.emit('ready_timeout', id)); diff --git a/packages/core/src/lifecycle.ts b/packages/core/src/lifecycle.ts index e2e4c8bd8d..74a95b1466 100644 --- a/packages/core/src/lifecycle.ts +++ b/packages/core/src/lifecycle.ts @@ -69,6 +69,12 @@ export interface LifecycleOptions { baseDir: string; app: EggCore; logger: EggConsoleLogger; + /** + * When true, the lifecycle stops after didLoad phase completes. + * willReady, didReady, and serverDidReady hooks are NOT called. + * Used for V8 startup snapshot construction. + */ + snapshot?: boolean; } export type FunWithFullPath = Fun & { fullPath?: string }; @@ -119,7 +125,7 @@ export class Lifecycle extends EventEmitter { }); this.ready((err) => { - if (!this.#metadataOnly) { + if (!this.#metadataOnly && !this.options.snapshot) { void this.triggerDidReady(err); } debug('app ready'); @@ -372,6 +378,10 @@ export class Lifecycle extends EventEmitter { debug('trigger didLoad end'); if (err) { this.ready(err); + } else if (this.options.snapshot) { + // In snapshot mode, stop after didLoad — skip willReady/didReady/serverDidReady + debug('snapshot mode: skipping willReady, marking ready after didLoad'); + this.ready(true); } else { this.triggerWillReady(); } From f43d47ce354bc2976e92c416f5848e0b4b9f8d12 Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 31 Mar 2026 10:38:22 +0800 Subject: [PATCH 2/5] docs(core): clarify snapshot option JSDoc to match actual behavior Update the snapshot option documentation to accurately describe that lifecycle stops after didLoad phase, rather than the misleading "loads metadata only" wording. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/egg.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/egg.ts b/packages/core/src/egg.ts index f7847f1618..d4093bb5a6 100644 --- a/packages/core/src/egg.ts +++ b/packages/core/src/egg.ts @@ -34,8 +34,8 @@ export interface EggCoreOptions { /** Skip lifecycle hooks, only trigger loadMetadata for manifest generation */ metadataOnly?: boolean; /** - * When true, the application loads metadata only (plugins, configs, extensions, - * services, controllers) without starting servers, timers, or connections. + * When true, lifecycle stops after the `didLoad` phase. + * `willReady`, `didReady`, and `serverDidReady` are skipped. * Used for V8 startup snapshot construction. */ snapshot?: boolean; From b425d9b0c73a113f2281a6f3e88da782971f763d Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 31 Mar 2026 10:44:33 +0800 Subject: [PATCH 3/5] test(core): add tests for snapshot mode lifecycle behavior Verify that snapshot mode stops after didLoad and skips willReady/didReady/serverDidReady. Also verify snapshot option propagation from EggCore to Lifecycle. Co-Authored-By: Claude Opus 4.6 --- packages/core/test/snapshot.test.ts | 198 ++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 packages/core/test/snapshot.test.ts diff --git a/packages/core/test/snapshot.test.ts b/packages/core/test/snapshot.test.ts new file mode 100644 index 0000000000..0b4eabde84 --- /dev/null +++ b/packages/core/test/snapshot.test.ts @@ -0,0 +1,198 @@ +import { strict as assert } from 'node:assert'; + +import { describe, it, afterEach } from 'vitest'; + +import { EggCore } from '../src/egg.ts'; +import { Lifecycle } from '../src/lifecycle.ts'; + +describe('test/snapshot.test.ts', () => { + let app: EggCore | undefined; + + afterEach(async () => { + if (app) { + await app.close(); + app = undefined; + } + }); + + describe('Lifecycle snapshot mode', () => { + it('should stop after didLoad and skip willReady/didReady/serverDidReady', async () => { + const callOrder: string[] = []; + + const lifecycle = new Lifecycle({ + baseDir: '.', + app: new EggCore(), + snapshot: true, + }); + + lifecycle.addBootHook( + class Boot { + configWillLoad(): void { + callOrder.push('configWillLoad'); + } + + configDidLoad(): void { + callOrder.push('configDidLoad'); + } + + async didLoad(): Promise { + callOrder.push('didLoad'); + } + + async willReady(): Promise { + callOrder.push('willReady'); + } + + async didReady(): Promise { + callOrder.push('didReady'); + } + + async serverDidReady(): Promise { + callOrder.push('serverDidReady'); + } + }, + ); + + lifecycle.init(); + lifecycle.triggerConfigWillLoad(); + await lifecycle.ready(); + + // configWillLoad, configDidLoad, didLoad should be called + assert.ok(callOrder.includes('configWillLoad'), 'configWillLoad should be called'); + assert.ok(callOrder.includes('configDidLoad'), 'configDidLoad should be called'); + assert.ok(callOrder.includes('didLoad'), 'didLoad should be called'); + + // willReady, didReady, serverDidReady should NOT be called + assert.ok(!callOrder.includes('willReady'), 'willReady should NOT be called in snapshot mode'); + assert.ok(!callOrder.includes('didReady'), 'didReady should NOT be called in snapshot mode'); + assert.ok(!callOrder.includes('serverDidReady'), 'serverDidReady should NOT be called in snapshot mode'); + + await lifecycle.close(); + }); + + it('should call willReady/didReady when snapshot is not set', async () => { + const callOrder: string[] = []; + + const lifecycle = new Lifecycle({ + baseDir: '.', + app: new EggCore(), + // no snapshot option + }); + + lifecycle.addBootHook( + class Boot { + configWillLoad(): void { + callOrder.push('configWillLoad'); + } + + configDidLoad(): void { + callOrder.push('configDidLoad'); + } + + async didLoad(): Promise { + callOrder.push('didLoad'); + } + + async willReady(): Promise { + callOrder.push('willReady'); + } + + async didReady(): Promise { + callOrder.push('didReady'); + } + }, + ); + + lifecycle.init(); + lifecycle.triggerConfigWillLoad(); + await lifecycle.ready(); + + // All hooks should be called in normal mode + assert.ok(callOrder.includes('configWillLoad'), 'configWillLoad should be called'); + assert.ok(callOrder.includes('configDidLoad'), 'configDidLoad should be called'); + assert.ok(callOrder.includes('didLoad'), 'didLoad should be called'); + assert.ok(callOrder.includes('willReady'), 'willReady should be called in normal mode'); + assert.ok(callOrder.includes('didReady'), 'didReady should be called in normal mode'); + + await lifecycle.close(); + }); + + it('should mark ready immediately after didLoad completes in snapshot mode', async () => { + let didLoadCompleted = false; + + const lifecycle = new Lifecycle({ + baseDir: '.', + app: new EggCore(), + snapshot: true, + }); + + lifecycle.addBootHook( + class Boot { + async didLoad(): Promise { + didLoadCompleted = true; + } + }, + ); + + lifecycle.init(); + lifecycle.triggerConfigWillLoad(); + await lifecycle.ready(); + + assert.ok(didLoadCompleted, 'didLoad should have completed'); + + await lifecycle.close(); + }); + }); + + describe('EggCore snapshot option', () => { + it('should pass snapshot option to lifecycle', () => { + app = new EggCore({ snapshot: true }); + assert.equal(app.options.snapshot, true); + assert.equal(app.lifecycle.options.snapshot, true); + }); + + it('should not set snapshot by default', () => { + app = new EggCore(); + assert.equal(app.options.snapshot, undefined); + assert.equal(app.lifecycle.options.snapshot, undefined); + }); + + it('should become ready after didLoad in snapshot mode (EggCore level)', async () => { + const callOrder: string[] = []; + + app = new EggCore({ snapshot: true }); + + app.lifecycle.addBootHook( + class Boot { + configWillLoad(): void { + callOrder.push('configWillLoad'); + } + + configDidLoad(): void { + callOrder.push('configDidLoad'); + } + + async didLoad(): Promise { + callOrder.push('didLoad'); + } + + async willReady(): Promise { + callOrder.push('willReady'); + } + + async didReady(): Promise { + callOrder.push('didReady'); + } + }, + ); + + app.lifecycle.init(); + app.lifecycle.triggerConfigWillLoad(); + await app.ready(); + + assert.ok(callOrder.includes('didLoad'), 'didLoad should be called'); + assert.ok(!callOrder.includes('willReady'), 'willReady should NOT be called'); + assert.ok(!callOrder.includes('didReady'), 'didReady should NOT be called'); + }); + }); +}); From b7b7679c439ecf02f4f5329ba6a55b168e71ea2a Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 31 Mar 2026 15:21:03 +0800 Subject: [PATCH 4/5] refactor(core): move snapshot cutoff from didLoad to configDidLoad MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After configDidLoad, SDKs begin initializing in didLoad hooks — opening connections, starting timers, and creating resources that cannot survive V8 heap serialization. Move the snapshot cutoff earlier: stop right after configDidLoad completes, before triggerDidLoad() is called. This captures all file loading, config merging, and prototype extensions while avoiding non-serializable side effects. The deferred phases (didLoad/willReady/didReady) will run at snapshot restore time instead. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/egg.ts | 7 ++-- packages/core/src/lifecycle.ts | 19 ++++++---- packages/core/test/snapshot.test.ts | 58 +++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/packages/core/src/egg.ts b/packages/core/src/egg.ts index d4093bb5a6..e374956959 100644 --- a/packages/core/src/egg.ts +++ b/packages/core/src/egg.ts @@ -34,9 +34,10 @@ export interface EggCoreOptions { /** Skip lifecycle hooks, only trigger loadMetadata for manifest generation */ metadataOnly?: boolean; /** - * When true, lifecycle stops after the `didLoad` phase. - * `willReady`, `didReady`, and `serverDidReady` are skipped. - * Used for V8 startup snapshot construction. + * When true, lifecycle stops after the `configDidLoad` phase. + * `didLoad`, `willReady`, `didReady`, and `serverDidReady` are skipped. + * Used for V8 startup snapshot construction — later phases typically + * open connections and start timers which are not serializable. */ snapshot?: boolean; } diff --git a/packages/core/src/lifecycle.ts b/packages/core/src/lifecycle.ts index 74a95b1466..e721c96255 100644 --- a/packages/core/src/lifecycle.ts +++ b/packages/core/src/lifecycle.ts @@ -70,9 +70,10 @@ export interface LifecycleOptions { app: EggCore; logger: EggConsoleLogger; /** - * When true, the lifecycle stops after didLoad phase completes. - * willReady, didReady, and serverDidReady hooks are NOT called. - * Used for V8 startup snapshot construction. + * When true, the lifecycle stops after configDidLoad phase completes. + * didLoad, willReady, didReady, and serverDidReady hooks are NOT called. + * Used for V8 startup snapshot construction — didLoad and later phases + * typically open connections and start timers which are not serializable. */ snapshot?: boolean; } @@ -274,6 +275,14 @@ export class Lifecycle extends EventEmitter { } } debug('trigger configDidLoad end'); + if (this.options.snapshot) { + // In snapshot mode, stop after configDidLoad — skip didLoad/willReady/didReady/serverDidReady. + // didLoad hooks typically open connections and start timers which are not serializable + // in V8 startup snapshots. These deferred phases run at snapshot restore time instead. + debug('snapshot mode: skipping didLoad, marking ready after configDidLoad'); + this.ready(true); + return; + } this.triggerDidLoad(); } @@ -378,10 +387,6 @@ export class Lifecycle extends EventEmitter { debug('trigger didLoad end'); if (err) { this.ready(err); - } else if (this.options.snapshot) { - // In snapshot mode, stop after didLoad — skip willReady/didReady/serverDidReady - debug('snapshot mode: skipping willReady, marking ready after didLoad'); - this.ready(true); } else { this.triggerWillReady(); } diff --git a/packages/core/test/snapshot.test.ts b/packages/core/test/snapshot.test.ts index 0b4eabde84..5b33d24f93 100644 --- a/packages/core/test/snapshot.test.ts +++ b/packages/core/test/snapshot.test.ts @@ -16,7 +16,7 @@ describe('test/snapshot.test.ts', () => { }); describe('Lifecycle snapshot mode', () => { - it('should stop after didLoad and skip willReady/didReady/serverDidReady', async () => { + it('should stop after configDidLoad and skip didLoad/willReady/didReady/serverDidReady', async () => { const callOrder: string[] = []; const lifecycle = new Lifecycle({ @@ -57,12 +57,12 @@ describe('test/snapshot.test.ts', () => { lifecycle.triggerConfigWillLoad(); await lifecycle.ready(); - // configWillLoad, configDidLoad, didLoad should be called + // configWillLoad and configDidLoad should be called assert.ok(callOrder.includes('configWillLoad'), 'configWillLoad should be called'); assert.ok(callOrder.includes('configDidLoad'), 'configDidLoad should be called'); - assert.ok(callOrder.includes('didLoad'), 'didLoad should be called'); - // willReady, didReady, serverDidReady should NOT be called + // didLoad, willReady, didReady, serverDidReady should NOT be called + assert.ok(!callOrder.includes('didLoad'), 'didLoad should NOT be called in snapshot mode'); assert.ok(!callOrder.includes('willReady'), 'willReady should NOT be called in snapshot mode'); assert.ok(!callOrder.includes('didReady'), 'didReady should NOT be called in snapshot mode'); assert.ok(!callOrder.includes('serverDidReady'), 'serverDidReady should NOT be called in snapshot mode'); @@ -70,7 +70,7 @@ describe('test/snapshot.test.ts', () => { await lifecycle.close(); }); - it('should call willReady/didReady when snapshot is not set', async () => { + it('should call all lifecycle hooks when snapshot is not set', async () => { const callOrder: string[] = []; const lifecycle = new Lifecycle({ @@ -117,8 +117,9 @@ describe('test/snapshot.test.ts', () => { await lifecycle.close(); }); - it('should mark ready immediately after didLoad completes in snapshot mode', async () => { - let didLoadCompleted = false; + it('should mark ready immediately after configDidLoad in snapshot mode', async () => { + let configDidLoadCompleted = false; + let didLoadCalled = false; const lifecycle = new Lifecycle({ baseDir: '.', @@ -128,8 +129,12 @@ describe('test/snapshot.test.ts', () => { lifecycle.addBootHook( class Boot { + configDidLoad(): void { + configDidLoadCompleted = true; + } + async didLoad(): Promise { - didLoadCompleted = true; + didLoadCalled = true; } }, ); @@ -138,10 +143,40 @@ describe('test/snapshot.test.ts', () => { lifecycle.triggerConfigWillLoad(); await lifecycle.ready(); - assert.ok(didLoadCompleted, 'didLoad should have completed'); + assert.ok(configDidLoadCompleted, 'configDidLoad should have completed'); + assert.ok(!didLoadCalled, 'didLoad should NOT be called in snapshot mode'); await lifecycle.close(); }); + + it('should still register beforeClose hooks in snapshot mode', async () => { + let beforeCloseCalled = false; + + const lifecycle = new Lifecycle({ + baseDir: '.', + app: new EggCore(), + snapshot: true, + }); + + lifecycle.addBootHook( + class Boot { + configDidLoad(): void { + // configDidLoad runs in snapshot mode + } + + async beforeClose(): Promise { + beforeCloseCalled = true; + } + }, + ); + + lifecycle.init(); + lifecycle.triggerConfigWillLoad(); + await lifecycle.ready(); + await lifecycle.close(); + + assert.ok(beforeCloseCalled, 'beforeClose should be called even in snapshot mode'); + }); }); describe('EggCore snapshot option', () => { @@ -157,7 +192,7 @@ describe('test/snapshot.test.ts', () => { assert.equal(app.lifecycle.options.snapshot, undefined); }); - it('should become ready after didLoad in snapshot mode (EggCore level)', async () => { + it('should become ready after configDidLoad in snapshot mode (EggCore level)', async () => { const callOrder: string[] = []; app = new EggCore({ snapshot: true }); @@ -190,7 +225,8 @@ describe('test/snapshot.test.ts', () => { app.lifecycle.triggerConfigWillLoad(); await app.ready(); - assert.ok(callOrder.includes('didLoad'), 'didLoad should be called'); + assert.ok(callOrder.includes('configDidLoad'), 'configDidLoad should be called'); + assert.ok(!callOrder.includes('didLoad'), 'didLoad should NOT be called'); assert.ok(!callOrder.includes('willReady'), 'willReady should NOT be called'); assert.ok(!callOrder.includes('didReady'), 'didReady should NOT be called'); }); From 3235abe590902cae2c9b34d71ac1fa858d4ea0b0 Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 31 Mar 2026 16:07:22 +0800 Subject: [PATCH 5/5] refactor(core): move snapshot cutoff from configDidLoad to configWillLoad The snapshot check was inside triggerConfigDidLoad(), running AFTER all configDidLoad hooks had already executed. SDKs execute during configDidLoad hooks, opening connections and starting timers which are not serializable in V8 startup snapshots. Move the check into triggerConfigWillLoad() so that configDidLoad is never called in snapshot mode. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/egg.ts | 9 ++++--- packages/core/src/lifecycle.ts | 26 +++++++++++--------- packages/core/test/snapshot.test.ts | 38 +++++++++++++++++++---------- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/packages/core/src/egg.ts b/packages/core/src/egg.ts index e374956959..c7cbe18e88 100644 --- a/packages/core/src/egg.ts +++ b/packages/core/src/egg.ts @@ -34,10 +34,11 @@ export interface EggCoreOptions { /** Skip lifecycle hooks, only trigger loadMetadata for manifest generation */ metadataOnly?: boolean; /** - * When true, lifecycle stops after the `configDidLoad` phase. - * `didLoad`, `willReady`, `didReady`, and `serverDidReady` are skipped. - * Used for V8 startup snapshot construction — later phases typically - * open connections and start timers which are not serializable. + * When true, lifecycle stops after the `configWillLoad` phase. + * `configDidLoad`, `didLoad`, `willReady`, `didReady`, and `serverDidReady` + * are skipped. Used for V8 startup snapshot construction — SDKs typically + * execute during `configDidLoad`, opening connections and starting timers + * which are not serializable. Analogous to `metadataOnly` mode. */ snapshot?: boolean; } diff --git a/packages/core/src/lifecycle.ts b/packages/core/src/lifecycle.ts index e721c96255..3facbe3eb5 100644 --- a/packages/core/src/lifecycle.ts +++ b/packages/core/src/lifecycle.ts @@ -70,10 +70,12 @@ export interface LifecycleOptions { app: EggCore; logger: EggConsoleLogger; /** - * When true, the lifecycle stops after configDidLoad phase completes. - * didLoad, willReady, didReady, and serverDidReady hooks are NOT called. - * Used for V8 startup snapshot construction — didLoad and later phases - * typically open connections and start timers which are not serializable. + * When true, the lifecycle stops after configWillLoad phase completes. + * configDidLoad, didLoad, willReady, didReady, and serverDidReady hooks + * are NOT called. Used for V8 startup snapshot construction — SDKs + * typically execute during configDidLoad, opening connections and starting + * timers which are not serializable. The handling is analogous to + * metadataOnly mode: both short-circuit the lifecycle chain early. */ snapshot?: boolean; } @@ -258,6 +260,14 @@ export class Lifecycle extends EventEmitter { } } debug('trigger configWillLoad end'); + if (this.options.snapshot) { + // Snapshot mode: stop AFTER configWillLoad, BEFORE configDidLoad. + // SDKs typically execute during configDidLoad hooks — these open connections + // and start timers which are not serializable in V8 startup snapshots. + debug('snapshot mode: stopping after configWillLoad, skipping configDidLoad and later phases'); + this.ready(true); + return; + } this.triggerConfigDidLoad(); } @@ -275,14 +285,6 @@ export class Lifecycle extends EventEmitter { } } debug('trigger configDidLoad end'); - if (this.options.snapshot) { - // In snapshot mode, stop after configDidLoad — skip didLoad/willReady/didReady/serverDidReady. - // didLoad hooks typically open connections and start timers which are not serializable - // in V8 startup snapshots. These deferred phases run at snapshot restore time instead. - debug('snapshot mode: skipping didLoad, marking ready after configDidLoad'); - this.ready(true); - return; - } this.triggerDidLoad(); } diff --git a/packages/core/test/snapshot.test.ts b/packages/core/test/snapshot.test.ts index 5b33d24f93..e584046f7a 100644 --- a/packages/core/test/snapshot.test.ts +++ b/packages/core/test/snapshot.test.ts @@ -16,7 +16,7 @@ describe('test/snapshot.test.ts', () => { }); describe('Lifecycle snapshot mode', () => { - it('should stop after configDidLoad and skip didLoad/willReady/didReady/serverDidReady', async () => { + it('should stop after configWillLoad and skip configDidLoad/didLoad/willReady/didReady/serverDidReady', async () => { const callOrder: string[] = []; const lifecycle = new Lifecycle({ @@ -57,11 +57,11 @@ describe('test/snapshot.test.ts', () => { lifecycle.triggerConfigWillLoad(); await lifecycle.ready(); - // configWillLoad and configDidLoad should be called + // configWillLoad should be called assert.ok(callOrder.includes('configWillLoad'), 'configWillLoad should be called'); - assert.ok(callOrder.includes('configDidLoad'), 'configDidLoad should be called'); - // didLoad, willReady, didReady, serverDidReady should NOT be called + // configDidLoad and all later hooks should NOT be called + assert.ok(!callOrder.includes('configDidLoad'), 'configDidLoad should NOT be called in snapshot mode'); assert.ok(!callOrder.includes('didLoad'), 'didLoad should NOT be called in snapshot mode'); assert.ok(!callOrder.includes('willReady'), 'willReady should NOT be called in snapshot mode'); assert.ok(!callOrder.includes('didReady'), 'didReady should NOT be called in snapshot mode'); @@ -117,8 +117,9 @@ describe('test/snapshot.test.ts', () => { await lifecycle.close(); }); - it('should mark ready immediately after configDidLoad in snapshot mode', async () => { - let configDidLoadCompleted = false; + it('should mark ready immediately after configWillLoad in snapshot mode', async () => { + let configWillLoadCompleted = false; + let configDidLoadCalled = false; let didLoadCalled = false; const lifecycle = new Lifecycle({ @@ -129,8 +130,12 @@ describe('test/snapshot.test.ts', () => { lifecycle.addBootHook( class Boot { + configWillLoad(): void { + configWillLoadCompleted = true; + } + configDidLoad(): void { - configDidLoadCompleted = true; + configDidLoadCalled = true; } async didLoad(): Promise { @@ -143,13 +148,14 @@ describe('test/snapshot.test.ts', () => { lifecycle.triggerConfigWillLoad(); await lifecycle.ready(); - assert.ok(configDidLoadCompleted, 'configDidLoad should have completed'); + assert.ok(configWillLoadCompleted, 'configWillLoad should have completed'); + assert.ok(!configDidLoadCalled, 'configDidLoad should NOT be called in snapshot mode'); assert.ok(!didLoadCalled, 'didLoad should NOT be called in snapshot mode'); await lifecycle.close(); }); - it('should still register beforeClose hooks in snapshot mode', async () => { + it('should not register beforeClose hooks in snapshot mode (configDidLoad skipped)', async () => { let beforeCloseCalled = false; const lifecycle = new Lifecycle({ @@ -160,8 +166,12 @@ describe('test/snapshot.test.ts', () => { lifecycle.addBootHook( class Boot { + configWillLoad(): void { + // configWillLoad runs in snapshot mode + } + configDidLoad(): void { - // configDidLoad runs in snapshot mode + // configDidLoad is skipped in snapshot mode } async beforeClose(): Promise { @@ -175,7 +185,8 @@ describe('test/snapshot.test.ts', () => { await lifecycle.ready(); await lifecycle.close(); - assert.ok(beforeCloseCalled, 'beforeClose should be called even in snapshot mode'); + // beforeClose is registered during configDidLoad iteration, which is skipped + assert.ok(!beforeCloseCalled, 'beforeClose should NOT be called since configDidLoad is skipped'); }); }); @@ -192,7 +203,7 @@ describe('test/snapshot.test.ts', () => { assert.equal(app.lifecycle.options.snapshot, undefined); }); - it('should become ready after configDidLoad in snapshot mode (EggCore level)', async () => { + it('should become ready after configWillLoad in snapshot mode (EggCore level)', async () => { const callOrder: string[] = []; app = new EggCore({ snapshot: true }); @@ -225,7 +236,8 @@ describe('test/snapshot.test.ts', () => { app.lifecycle.triggerConfigWillLoad(); await app.ready(); - assert.ok(callOrder.includes('configDidLoad'), 'configDidLoad should be called'); + assert.ok(callOrder.includes('configWillLoad'), 'configWillLoad should be called'); + assert.ok(!callOrder.includes('configDidLoad'), 'configDidLoad should NOT be called'); assert.ok(!callOrder.includes('didLoad'), 'didLoad should NOT be called'); assert.ok(!callOrder.includes('willReady'), 'willReady should NOT be called'); assert.ok(!callOrder.includes('didReady'), 'didReady should NOT be called');