Skip to content

Commit 5fa985a

Browse files
committed
feat: experimental support for running terminating and shutdown hooks in reverse order
1 parent 48a964a commit 5fa985a

File tree

3 files changed

+277
-23
lines changed

3 files changed

+277
-23
lines changed

src/application.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,10 +650,18 @@ export class Application<ContainerBindings extends Record<any, any>> extends Mac
650650
return
651651
}
652652

653+
const shutdownInReverseOrder = this.experimentalFlags.enabled('shutdownInReverseOrder')
654+
653655
debug('terminating app')
656+
654657
this.#terminating = true
655-
await this.#hooks.runner('terminating').run(this)
656-
await this.#providersManager.shutdown()
658+
if (shutdownInReverseOrder) {
659+
await this.#hooks.runner('terminating').runReverse(this)
660+
} else {
661+
await this.#hooks.runner('terminating').run(this)
662+
}
663+
664+
await this.#providersManager.shutdown(shutdownInReverseOrder)
657665
this.#hooks.clear('terminating')
658666
this.#state = 'terminated'
659667
}

src/managers/providers.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,17 @@ export class ProvidersManager {
204204
/**
205205
* Invoke shutdown method on all the providers
206206
*/
207-
async shutdown() {
208-
for (let provider of this.#providersWithShutdownListeners) {
207+
async shutdown(inReverseOrder: boolean) {
208+
const providersWithShutdownListeners = inReverseOrder
209+
? Array.from(this.#providersWithShutdownListeners).reverse()
210+
: Array.from(this.#providersWithShutdownListeners)
211+
212+
this.#providersWithShutdownListeners = []
213+
214+
for (let provider of providersWithShutdownListeners) {
209215
if (provider.shutdown) {
210216
await provider.shutdown()
211217
}
212218
}
213-
214-
this.#providersWithShutdownListeners = []
215219
}
216220
}

tests/application/providers.spec.ts

Lines changed: 259 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -522,31 +522,60 @@ test.group('Application | providers', (group) => {
522522
constructor(private app) {}
523523
524524
register() {
525-
this.app.container.singleton('route', () => {
525+
this.app.container.singleton('lifecycle', () => {
526526
return {
527527
stack: []
528528
}
529529
})
530530
}
531531
532532
async boot() {
533-
const route = await this.app.container.make('route')
534-
route.stack.push('booted')
533+
const lifecycle = await this.app.container.make('lifecycle')
534+
lifecycle.stack.push('router booted')
535535
}
536536
537537
async start() {
538-
const route = await this.app.container.make('route')
539-
route.stack.push('setup start')
538+
const lifecycle = await this.app.container.make('lifecycle')
539+
lifecycle.stack.push('router setup start')
540540
}
541541
542542
async ready() {
543-
const route = await this.app.container.make('route')
544-
route.stack.push('ready')
543+
const lifecycle = await this.app.container.make('lifecycle')
544+
lifecycle.stack.push('router ready')
545545
}
546546
547547
async shutdown() {
548-
const route = await this.app.container.make('route')
549-
route.stack.push('shutdown')
548+
const lifecycle = await this.app.container.make('lifecycle')
549+
lifecycle.stack.push('router shutdown')
550+
}
551+
}
552+
`
553+
)
554+
555+
await outputFile(
556+
join(BASE_PATH, './app_provider.ts'),
557+
`
558+
export default class AppProvider {
559+
constructor(private app) {}
560+
561+
async boot() {
562+
const lifecycle = await this.app.container.make('lifecycle')
563+
lifecycle.stack.push('app booted')
564+
}
565+
566+
async start() {
567+
const lifecycle = await this.app.container.make('lifecycle')
568+
lifecycle.stack.push('app setup start')
569+
}
570+
571+
async ready() {
572+
const lifecycle = await this.app.container.make('lifecycle')
573+
lifecycle.stack.push('app ready')
574+
}
575+
576+
async shutdown() {
577+
const lifecycle = await this.app.container.make('lifecycle')
578+
lifecycle.stack.push('app shutdown')
550579
}
551580
}
552581
`
@@ -562,19 +591,143 @@ test.group('Application | providers', (group) => {
562591
file: () => import(new URL('./route_provider.js?v=12', BASE_URL).href),
563592
environment: ['web'],
564593
},
594+
{
595+
file: () => import(new URL('./app_provider.js?v=12', BASE_URL).href),
596+
environment: ['web'],
597+
},
565598
],
566599
})
567600

568601
await app.init()
569602
await app.boot()
570603
await app.start(async () => {
571-
const route = await app.container.make('route')
572-
route.stack.push('starting')
604+
const lifecycle = await app.container.make('lifecycle')
605+
lifecycle.stack.push('starting')
573606
})
574607
await app.terminate()
575608

576-
assert.deepEqual(await app.container.make('route'), {
577-
stack: ['booted', 'setup start', 'starting', 'ready', 'shutdown'],
609+
assert.deepEqual(await app.container.make('lifecycle'), {
610+
stack: [
611+
'router booted',
612+
'app booted',
613+
'router setup start',
614+
'app setup start',
615+
'starting',
616+
'router ready',
617+
'app ready',
618+
'router shutdown',
619+
'app shutdown',
620+
],
621+
})
622+
})
623+
624+
test('invoke shutdown hooks in reverse order', async ({ assert }) => {
625+
await outputFile(
626+
join(BASE_PATH, './route_provider.ts'),
627+
`
628+
export default class RouteProvider {
629+
constructor(private app) {}
630+
631+
register() {
632+
this.app.container.singleton('lifecycle', () => {
633+
return {
634+
stack: []
635+
}
636+
})
637+
}
638+
639+
async boot() {
640+
const lifecycle = await this.app.container.make('lifecycle')
641+
lifecycle.stack.push('router booted')
642+
}
643+
644+
async start() {
645+
const lifecycle = await this.app.container.make('lifecycle')
646+
lifecycle.stack.push('router setup start')
647+
}
648+
649+
async ready() {
650+
const lifecycle = await this.app.container.make('lifecycle')
651+
lifecycle.stack.push('router ready')
652+
}
653+
654+
async shutdown() {
655+
const lifecycle = await this.app.container.make('lifecycle')
656+
lifecycle.stack.push('router shutdown')
657+
}
658+
}
659+
`
660+
)
661+
662+
await outputFile(
663+
join(BASE_PATH, './app_provider.ts'),
664+
`
665+
export default class AppProvider {
666+
constructor(private app) {}
667+
668+
async boot() {
669+
const lifecycle = await this.app.container.make('lifecycle')
670+
lifecycle.stack.push('app booted')
671+
}
672+
673+
async start() {
674+
const lifecycle = await this.app.container.make('lifecycle')
675+
lifecycle.stack.push('app setup start')
676+
}
677+
678+
async ready() {
679+
const lifecycle = await this.app.container.make('lifecycle')
680+
lifecycle.stack.push('app ready')
681+
}
682+
683+
async shutdown() {
684+
const lifecycle = await this.app.container.make('lifecycle')
685+
lifecycle.stack.push('app shutdown')
686+
}
687+
}
688+
`
689+
)
690+
691+
const app = new Application(BASE_URL, {
692+
environment: 'web',
693+
})
694+
695+
app.rcContents({
696+
experimental: {
697+
shutdownInReverseOrder: true,
698+
},
699+
providers: [
700+
{
701+
file: () => import(new URL('./route_provider.js?v=13', BASE_URL).href),
702+
environment: ['web'],
703+
},
704+
{
705+
file: () => import(new URL('./app_provider.js?v=13', BASE_URL).href),
706+
environment: ['web'],
707+
},
708+
],
709+
})
710+
711+
await app.init()
712+
await app.boot()
713+
await app.start(async () => {
714+
const lifecycle = await app.container.make('lifecycle')
715+
lifecycle.stack.push('starting')
716+
})
717+
await app.terminate()
718+
719+
assert.deepEqual(await app.container.make('lifecycle'), {
720+
stack: [
721+
'router booted',
722+
'app booted',
723+
'router setup start',
724+
'app setup start',
725+
'starting',
726+
'router ready',
727+
'app ready',
728+
'app shutdown',
729+
'router shutdown',
730+
],
578731
})
579732
})
580733

@@ -623,7 +776,7 @@ test.group('Application | providers', (group) => {
623776
app.rcContents({
624777
providers: [
625778
{
626-
file: () => import(new URL('./route_provider.js?v=13', BASE_URL).href),
779+
file: () => import(new URL('./route_provider.js?v=14', BASE_URL).href),
627780
environment: ['web'],
628781
},
629782
],
@@ -683,7 +836,7 @@ test.group('Application | providers', (group) => {
683836
app.rcContents({
684837
providers: [
685838
{
686-
file: () => import(new URL('./route_provider.js?v=14', BASE_URL).href),
839+
file: () => import(new URL('./route_provider.js?v=15', BASE_URL).href),
687840
environment: ['web'],
688841
},
689842
],
@@ -740,7 +893,7 @@ test.group('Application | providers', (group) => {
740893
app.rcContents({
741894
providers: [
742895
{
743-
file: () => import(new URL('./route_provider.js?v=15', BASE_URL).href),
896+
file: () => import(new URL('./route_provider.js?v=16', BASE_URL).href),
744897
environment: ['web'],
745898
},
746899
],
@@ -767,6 +920,95 @@ test.group('Application | providers', (group) => {
767920
assert.isTrue(app.isTerminated)
768921
})
769922

923+
test('invoke terminating hooks in reverse order', async ({ assert }) => {
924+
await outputFile(
925+
join(BASE_PATH, './route_provider.ts'),
926+
`
927+
export default class RouteProvider {
928+
constructor(private app) {}
929+
930+
register() {
931+
this.app.container.singleton('route', () => {
932+
return {
933+
stack: []
934+
}
935+
})
936+
}
937+
938+
async boot() {
939+
const route = await this.app.container.make('route')
940+
route.stack.push('booted')
941+
}
942+
943+
async start() {
944+
const route = await this.app.container.make('route')
945+
route.stack.push('setup start')
946+
}
947+
948+
async ready() {
949+
const route = await this.app.container.make('route')
950+
route.stack.push('ready')
951+
}
952+
953+
async shutdown() {
954+
const route = await this.app.container.make('route')
955+
route.stack.push('shutdown')
956+
}
957+
}
958+
`
959+
)
960+
961+
const app = new Application(BASE_URL, {
962+
environment: 'web',
963+
})
964+
965+
app.rcContents({
966+
experimental: {
967+
shutdownInReverseOrder: true,
968+
},
969+
providers: [
970+
{
971+
file: () => import(new URL('./route_provider.js?v=17', BASE_URL).href),
972+
environment: ['web'],
973+
},
974+
],
975+
})
976+
977+
await app.init()
978+
await app.boot()
979+
await app.start(async () => {
980+
const route = await app.container.make('route')
981+
route.stack.push('starting')
982+
})
983+
984+
app.terminating(async () => {
985+
const route = await app.container.make('route')
986+
assert.isTrue(app.isTerminating)
987+
route.stack.push('terminating 1')
988+
})
989+
app.terminating(async () => {
990+
const route = await app.container.make('route')
991+
assert.isTrue(app.isTerminating)
992+
route.stack.push('terminating 2')
993+
})
994+
995+
await app.terminate()
996+
997+
assert.deepEqual(await app.container.make('route'), {
998+
stack: [
999+
'booted',
1000+
'setup start',
1001+
'starting',
1002+
'ready',
1003+
'terminating 2',
1004+
'terminating 1',
1005+
'shutdown',
1006+
],
1007+
})
1008+
1009+
assert.isTrue(app.isTerminated)
1010+
})
1011+
7701012
test('terminate after from initiated state', async ({ assert }) => {
7711013
await outputFile(
7721014
join(BASE_PATH, './route_provider.ts'),
@@ -806,7 +1048,7 @@ test.group('Application | providers', (group) => {
8061048
app.rcContents({
8071049
providers: [
8081050
{
809-
file: () => import(new URL('./route_provider.js?v=16', BASE_URL).href),
1051+
file: () => import(new URL('./route_provider.js?v=18', BASE_URL).href),
8101052
environment: ['web'],
8111053
},
8121054
],

0 commit comments

Comments
 (0)