Skip to content

Commit b81bec3

Browse files
committed
feat: Add pseudo-functional interface for lifecycle
1 parent 555d248 commit b81bec3

File tree

11 files changed

+383
-50
lines changed

11 files changed

+383
-50
lines changed

README.md

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# Lifecycle Manager
22

3-
Manages the clean startup and shutdown of a process. Register lifecycle components in the order they will be initialized, and then call `start()` on the lifecycle. When the lifecycle manager receives a SIGTERM or if the `close()` method is called, it will begin the shutdown process. The lifecycle manager closes each component in the reverse order of their registration
3+
Manages the clean startup and shutdown of a process. Register lifecycle nodes in the order they will be initialized, and then call `start()` on the lifecycle. When the lifecycle root node receives a SIGTERM or if the `close()` method is called, it will begin the shutdown process. The lifecycle root closes each node in the reverse order of their registration
44

55
## Usage
66

77
```ts
88
import { Lifecycle } from '@antman/lifecycle';
99

1010
if (import.meta.main) {
11-
const lifecycle = new Lifecycle();
11+
const lifecycle = newLifecycleRoot();
1212
lifecycle.all(console.log);
1313
lifecycle.register(db);
1414
lifecycle.register(webserver);
@@ -17,48 +17,48 @@ if (import.meta.main) {
1717
}
1818
```
1919

20-
Where each component is defined as a lifecycle component:
20+
Where each node is defined as a lifecycle node:
2121

2222
```ts
23-
class DatabasePool extends LifecycleComponent {
24-
pool?: Pool;
25-
async start() {
26-
this.pool = new Pool({
27-
user: DB_USER,
28-
password: DB_HOST,
29-
host: DB_PASSWORD,
30-
port: DB_PORT,
31-
});
32-
await this.pool.query('SELECT 1');
23+
export const dbLifecycleNode = newNode(() => {
24+
let pool: Pool | undefined;
25+
return {
26+
name: 'DatabasePool',
27+
async start() {
28+
pool = new Pool({
29+
user: DB_USER,
30+
password: DB_HOST,
31+
host: DB_PASSWORD,
32+
port: DB_PORT,
33+
});
34+
await pool.query('SELECT 1');
35+
},
36+
close: () => pool.end(),
3337
}
34-
async close(){
35-
await this.pool.end();
36-
}
37-
}
38-
export const db = new DatabasePool();
38+
})
3939
```
4040

4141
Find more details in the [full documentation](https://jsr.io/@antman/lifecycle/doc)
4242

4343
## Nested Lifecycles
4444

45-
Sometimes, a lifecycle component needs to manage a subset of LifecycleComponents and their lifecycles. Every instance of LifecycleComponent also provides a registerChildComponent method and startChildComponents & closeChildComponents methods. Use these to register child lifecycle components, then start and close them during the startup and shutdown of the parent component.
45+
Sometimes, a lifecycle node needs to manage a subset of LifecycleNodes and their lifecycles. Every instance of LifecycleNode also provides a registerChildNode method and startChildNodes & closeChildNodes methods. Use these to register child lifecycle nodes, then start and close them during the startup and shutdown of the parent node.
4646

4747
For example:
4848

4949
```ts
50-
const parentComponent = new (class ParentComponent extends LifecycleComponent {
50+
const parentNode = newNode((internals) => ({
5151
async start(){
52-
this.registerChildComponent(childOne)
53-
this.registerChildComponent(childTwo)
54-
await this.startChildComponents()
52+
internals.registerChildNode(childOne)
53+
internals.registerChildNode(childTwo)
54+
await internals.startChildNodes()
5555
}
5656
async close(){
57-
await this.closeChildComponents();
57+
await internals.closeChildNodes();
5858
}
59-
})();
59+
}));
6060

61-
const lifecycle = new Lifecycle();
62-
lifecycle.register(parentComponent)
61+
const lifecycle = newLifecycleRoot();
62+
lifecycle.register(parentNode)
6363
await lifecycle.start();
6464
```

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@antman/lifecycle",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"exports": "./mod.ts",
55
"tasks": {
66
"dev": "deno test --watch"

mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export * from './src/lifecycle.ts';
22
export * from './src/LifecycleComponent.ts';
3+
export * from './src/makeLifecycle.ts';
4+
export * from './src/makeLifecycleNode.ts';

src/EventMap.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Status } from './Status.ts';
2+
3+
export type EventMap = Record<Status, []> & {
4+
componentStarted: [string];
5+
componentClosing: [string];
6+
componentClosed: [string];
7+
};

src/Status.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type Status = (typeof statuses)[number];
2+
export const statuses = [
3+
'pending',
4+
'starting',
5+
'running',
6+
'closing',
7+
'closed',
8+
] as const;

src/componentEvents.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const componentEvents = [
2+
'componentStarted',
3+
'componentClosing',
4+
'componentClosed',
5+
] as const;
6+
export type ComponentEvent = (typeof componentEvents)[number];
7+
export const isComponentEvent = (e: string | undefined): e is ComponentEvent =>
8+
componentEvents.includes(e as ComponentEvent);

src/lifecycle.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,10 @@
11
import { isDefined } from '@antman/bool';
22
import { EventEmitter } from 'node:events';
33
import type { LifecycleComponent } from './LifecycleComponent.ts';
4+
import type { EventMap } from './EventMap.ts';
5+
import { type Status, statuses } from './Status.ts';
6+
import { componentEvents, isComponentEvent } from './componentEvents.ts';
47

5-
const statuses = [
6-
'pending',
7-
'starting',
8-
'running',
9-
'closing',
10-
'closed',
11-
] as const;
12-
type Status = (typeof statuses)[number];
13-
14-
const componentEvents = [
15-
'componentStarted',
16-
'componentClosing',
17-
'componentClosed',
18-
] as const;
19-
type ComponentEvent = (typeof componentEvents)[number];
20-
const isComponentEvent = (e: string | undefined): e is ComponentEvent =>
21-
componentEvents.includes(e as ComponentEvent);
22-
type EventMap = Record<Status, []> & {
23-
componentStarted: [string];
24-
componentClosing: [string];
25-
componentClosed: [string];
26-
};
278
/**
289
* Manages the clean startup and shutdown of a process and its components.
2910
*

src/lifecycleNode.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { delay } from '@std/async/delay';
2+
import { expect } from '@std/expect/expect';
3+
import { setupEvents } from './testUtil.ts';
4+
import { newNode } from './makeLifecycleNode.ts';
5+
import { newLifecycleRoot } from './makeLifecycle.ts';
6+
7+
const stubAsync = () => delay(1);
8+
9+
Deno.test('Lifecycle maker functions', async () => {
10+
const [actual, actualEvent] = setupEvents();
11+
const lc = newLifecycleRoot();
12+
lc.register({ name: 'DatabasePool', start: stubAsync, close: stubAsync });
13+
14+
lc.on(...actualEvent('componentStarted'));
15+
lc.on(...actualEvent('componentClosing'));
16+
lc.on(...actualEvent('componentClosed'));
17+
await lc.start();
18+
await lc.close(false);
19+
await delay(2);
20+
expect(actual).toEqual([
21+
'componentStarted DatabasePool',
22+
'componentClosing DatabasePool',
23+
'componentClosed DatabasePool',
24+
]);
25+
});
26+
27+
const events: string[] = [];
28+
const parent = newNode((internals) => ({
29+
name: 'ParentComponent',
30+
async start() {
31+
events.push('parent.starting');
32+
internals.registerChildNode({
33+
name: 'childOne',
34+
async start() {
35+
events.push('childOne.starting');
36+
await delay(1);
37+
events.push('childOne.started');
38+
delay(1);
39+
},
40+
async close() {
41+
events.push('childOne.closing');
42+
await delay(1);
43+
events.push('childOne.closed');
44+
},
45+
});
46+
internals.registerChildNode({
47+
name: 'childTwo',
48+
async start() {
49+
events.push('childTwo.starting');
50+
await delay(1);
51+
events.push('childTwo.started');
52+
delay(1);
53+
},
54+
async close() {
55+
events.push('childTwo.closing');
56+
await delay(1);
57+
events.push('childTwo.closed');
58+
},
59+
});
60+
await internals.startChildNodes();
61+
events.push('parent.started');
62+
},
63+
async close() {
64+
events.push('parent.closing');
65+
await internals.closeChildNodes();
66+
events.push('parent.closed');
67+
},
68+
}));
69+
70+
Deno.test('lifecycle node manages child nodes', async () => {
71+
events.splice(0, events.length);
72+
const lc = newLifecycleRoot();
73+
lc.on('componentStarted', (name) => events.push(`componentStarted ${name}`));
74+
lc.on('componentClosed', (name) => events.push(`componentClosed ${name}`));
75+
lc.register(parent);
76+
await lc.start();
77+
await lc.close(false);
78+
await delay(2);
79+
expect(events).toEqual([
80+
'parent.starting',
81+
'childOne.starting',
82+
'childOne.started',
83+
'childTwo.starting',
84+
'childTwo.started',
85+
'parent.started',
86+
'componentStarted ParentComponent',
87+
'parent.closing',
88+
'childTwo.closing',
89+
'childTwo.closed',
90+
'childOne.closing',
91+
'childOne.closed',
92+
'parent.closed',
93+
'componentClosed ParentComponent',
94+
]);
95+
});

src/lifecycleRoot.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { delay } from '@std/async';
2+
import { expect } from '@std/expect';
3+
import { createChecks, setupEvents } from './testUtil.ts';
4+
import { newLifecycleRoot } from './makeLifecycle.ts';
5+
6+
const stubAsync = () => delay(1);
7+
const makeTestNode = (name: string) => ({ name, start: stubAsync, close: stubAsync });
8+
9+
Deno.test('LifecycleRoot', async ({ step }) => {
10+
await step('can start & close a lifecycle', async () => {
11+
const { checks, passCheck } = createChecks();
12+
const lc = newLifecycleRoot();
13+
lc.on('starting', passCheck('didStart'));
14+
lc.on('running', passCheck('didRun'));
15+
lc.on('closing', passCheck('didClosing'));
16+
lc.on('closed', passCheck('didClose'));
17+
await lc.start();
18+
await lc.close(false);
19+
expect(checks).toEqual({
20+
didStart: true,
21+
didRun: true,
22+
didClosing: true,
23+
didClose: true,
24+
});
25+
});
26+
await step('start and close lifecycle components', async () => {
27+
const [actual, actualEvent] = setupEvents();
28+
const lc = newLifecycleRoot();
29+
lc.register(makeTestNode('TestComponentOne'));
30+
lc.register(makeTestNode('TestComponentTwo'));
31+
lc.register(makeTestNode('TestComponentThree'));
32+
33+
lc.on(...actualEvent('componentStarted'));
34+
lc.on(...actualEvent('componentClosing'));
35+
lc.on(...actualEvent('componentClosed'));
36+
await lc.start();
37+
await lc.close(false);
38+
await delay(2);
39+
expect(actual).toEqual([
40+
'componentStarted TestComponentOne',
41+
'componentStarted TestComponentTwo',
42+
'componentStarted TestComponentThree',
43+
'componentClosing TestComponentThree',
44+
'componentClosed TestComponentThree',
45+
'componentClosing TestComponentTwo',
46+
'componentClosed TestComponentTwo',
47+
'componentClosing TestComponentOne',
48+
'componentClosed TestComponentOne',
49+
]);
50+
});
51+
});

0 commit comments

Comments
 (0)