Skip to content

Commit 83c2352

Browse files
committed
Improved documentation and hide unintentionally public lifecycle manager methods
1 parent 36d6f2f commit 83c2352

File tree

8 files changed

+198
-80
lines changed

8 files changed

+198
-80
lines changed

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
"deno.enable": true,
33
"[typescript]": {
44
"editor.defaultFormatter": "denoland.vscode-deno"
5-
}
5+
},
6+
"cSpell.words": [
7+
"healthcheck"
8+
]
69
}

README.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,61 @@
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 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
4+
5+
## Usage
6+
7+
```ts
8+
import { Lifecycle } from '@antman/lifecycle';
9+
10+
if (import.meta.main) {
11+
const lifecycle = new Lifecycle();
12+
lifecycle.all(console.log);
13+
lifecycle.register(db);
14+
lifecycle.register(webserver);
15+
lifecycle.register(outbox);
16+
await lifecycle.start();
17+
}
18+
```
19+
20+
Where each component is defined as a lifecycle component, in one of two ways:
21+
22+
```ts
23+
import { type LifecycleComponent } from '@antman/lifecycle';
24+
import { Pool } from 'pg'
25+
26+
type Db = LifecycleComponent & { pool: Pool };
27+
28+
const { DB_USER, DB_HOST, DB_PASSWORD, DB_PORT } = Deno.env.toObject();
29+
30+
export const db: Db = {
31+
name: 'db',
32+
status: 'pending',
33+
pool: new Pool({
34+
user: DB_USER,
35+
password: DB_HOST,
36+
host: DB_PASSWORD,
37+
port: DB_PORT,
38+
}),
39+
async start() {
40+
// check database connected successfully
41+
await db.pool.query('SELECT 1');
42+
db.status = 'running';
43+
},
44+
async close() {
45+
await db.pool.end();
46+
db.status = 'pending'
47+
}
48+
}
49+
```
50+
51+
or
52+
53+
```ts
54+
class DatabasePool implements LifecycleComponent {
55+
readonly name: 'db';
56+
57+
}
58+
export const db = new DatabasePool();
59+
```
60+
61+
Find more details in the [full documentation](https://jsr.io/@antman/lifecycle/doc)

deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@antman/lifecycle",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"exports": "./mod.ts",
55
"tasks": {
66
"dev": "deno test --watch"
@@ -13,6 +13,7 @@
1313
},
1414
"license": "MIT",
1515
"imports": {
16+
"@antman/bool": "jsr:@antman/bool@^0.1.1",
1617
"@std/async": "jsr:@std/async@^1.0.14"
1718
}
1819
}

deno.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/componentClass.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { delay } from '@std/async/delay';
2+
import { Lifecycle, type LifecycleComponent } from './lifecycle.ts';
3+
import { expect } from 'jsr:@std/expect/expect';
4+
import { setupEvents } from './testUtil.ts';
5+
6+
class DatabasePool implements LifecycleComponent {
7+
readonly name: 'db';
8+
#status: LifecycleComponent['status'];
9+
get status() {
10+
return this.#status;
11+
}
12+
constructor() {
13+
this.name = 'db';
14+
this.#status = 'pending';
15+
}
16+
start() {
17+
return delay(1);
18+
}
19+
close() {
20+
return delay(1);
21+
}
22+
}
23+
24+
Deno.test('Lifecycle component as a class', async () => {
25+
const db = new DatabasePool();
26+
const [actual, actualEvent] = setupEvents();
27+
const lc = new Lifecycle({ healthCheckIntervalMs: 50 });
28+
lc.register(db);
29+
30+
lc.on(...actualEvent('componentStarted'));
31+
lc.on(...actualEvent('componentClosing'));
32+
lc.on(...actualEvent('componentClosed'));
33+
await lc.start();
34+
await lc.close(false);
35+
await delay(2);
36+
expect(actual).toEqual([
37+
'componentStarted db',
38+
'componentClosing db',
39+
'componentClosed db',
40+
]);
41+
});

src/lifecycle.test.ts

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,7 @@
11
import { delay } from 'jsr:@std/async';
22
import { expect } from 'jsr:@std/expect';
3-
import { Lifecycle, type LifecycleComponent } from './lifecycle.ts';
4-
5-
const createChecks = () => {
6-
const checks = {
7-
didStart: false,
8-
didRun: false,
9-
didClosing: false,
10-
didClose: false,
11-
};
12-
const passCheck = (check: keyof typeof checks) => () => (checks[check] = true);
13-
return { checks, passCheck };
14-
};
15-
16-
type EventName =
17-
| 'componentStarted'
18-
| 'componentRestarting'
19-
| 'componentRestarted'
20-
| 'componentClosing'
21-
| 'componentClosed';
22-
type Event = `${EventName} ${string}`;
23-
type Events = Event[];
24-
25-
type MakeEvent = (event: EventName) => Event;
26-
const makeEventFn = (name?: string): MakeEvent => (event) => `${event} ${name}`;
27-
28-
const makeComponent = (c?: Partial<LifecycleComponent>): LifecycleComponent => {
29-
const name = c?.name ?? 'test-component';
30-
const component: LifecycleComponent = {
31-
name,
32-
start: async () => (await delay(1)) ?? (component.status = 'running'),
33-
status: 'pending',
34-
close: () => delay(1),
35-
...c,
36-
};
37-
return component;
38-
};
39-
40-
const makeCrashingComponent = (
41-
c?: Partial<LifecycleComponent>,
42-
crashAfterMs = 10,
43-
): LifecycleComponent => {
44-
const component = makeComponent({
45-
start: async () => {
46-
(await delay(1)) ?? (component.status = 'running');
47-
delay(crashAfterMs).then(() => (component.status = 'crashed'));
48-
},
49-
restart: () => {
50-
component.status = 'running';
51-
return Promise.resolve();
52-
},
53-
...c,
54-
});
55-
return component;
56-
};
57-
58-
type EventTracker = (en: EventName) => [EventName, (cn?: string) => void];
59-
const setupEvents = (): [Events, EventTracker] => {
60-
const actual: Events = [];
61-
const trackActual = (e: Event) => actual.push(e);
62-
return [
63-
actual,
64-
(en: EventName) => [en, (cn?: string) => trackActual(makeEventFn(cn)(en))],
65-
];
66-
};
3+
import { Lifecycle } from './lifecycle.ts';
4+
import { createChecks, makeComponent, makeCrashingComponent, setupEvents } from './testUtil.ts';
675

686
Deno.test('LifeCycle', async ({ step }) => {
697
await step('can start & close a lifecycle', async () => {

src/lifecycle.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
import { isDefined } from '@antman/bool';
12
import { delay } from '@std/async/delay';
23
import { EventEmitter } from 'node:events';
34

4-
export type ComponentStatus = 'pending' | 'running' | 'crashed';
5-
type Opt = {
5+
type ComponentStatus = 'pending' | 'running' | 'crashed';
6+
/**
7+
* Define the options for the lifecycle manager
8+
*/
9+
export type LifecycleOptions = {
610
/**
711
* Define the frequency of component health check cycles. Note: This is the interval in which the lifecycle manager will begin polling each component's status -- the interval begins once each component has returned its status. Components returning their status in a promise can delay subsequent health checks.
812
*/
9-
healthCheckIntervalMs: number;
13+
healthCheckIntervalMs?: number;
1014
};
11-
type Options = Partial<Opt>;
1215
const statuses = [
1316
'pending',
1417
'starting',
@@ -27,7 +30,7 @@ export type LifecycleComponent = {
2730
/**
2831
* Provide a name to identify the component in events emitted by LifecycleManager -- useful for logging
2932
*/
30-
name: string;
33+
readonly name: string;
3134
/**
3235
* The status property will be used as a healthcheck; if the component status becomes 'crashed', the lifecycle manager will call `restart` if it exists, or `start` if it doesn't
3336
*/
@@ -45,7 +48,6 @@ export type LifecycleComponent = {
4548
*/
4649
close(): Promise<unknown>;
4750
};
48-
4951
const defaultOptions = { healthCheckIntervalMs: 600 };
5052
const componentEvents = [
5153
'componentStarted',
@@ -54,6 +56,9 @@ const componentEvents = [
5456
'componentRestarting',
5557
'componentRestarted',
5658
] as const;
59+
type ComponentEvent = (typeof componentEvents)[number];
60+
const isComponentEvent = (e: string | undefined): e is ComponentEvent =>
61+
componentEvents.includes(e as ComponentEvent);
5762
type EventMap = Record<Status | 'healthChecked', []> & {
5863
componentStarted: [string];
5964
componentClosing: [string];
@@ -62,7 +67,7 @@ type EventMap = Record<Status | 'healthChecked', []> & {
6267
componentRestarted: [string];
6368
};
6469
/**
65-
* Manages the clean startup and shutdown of a process
70+
* Manages the clean startup and shutdown of a process and its components.
6671
*
6772
* 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
6873
*/
@@ -77,7 +82,9 @@ export class Lifecycle {
7782
this.#emit(v);
7883
}
7984
#emit(event: keyof EventMap, name?: string): void {
80-
this.#emitter.emit(event, name);
85+
(isComponentEvent(event) && isDefined(name))
86+
? this.#emitter.emit(event, name)
87+
: this.#emitter.emit(event);
8188
}
8289
/**
8390
* Provide a callback for events emitted by lifecycle manager
@@ -97,7 +104,7 @@ export class Lifecycle {
97104
this.#emitter.on(event, (name: string) => cb(event, name))
98105
);
99106
}
100-
constructor(opt: Options = defaultOptions) {
107+
constructor(opt: LifecycleOptions = defaultOptions) {
101108
const { healthCheckIntervalMs } = { ...defaultOptions, ...opt };
102109
this.#status = 'pending';
103110
this.#components = [];
@@ -130,7 +137,7 @@ export class Lifecycle {
130137
public start = async (): Promise<void> => {
131138
this.#setStatus('starting');
132139
for (const component of this.#components) {
133-
await this.startComponent(component);
140+
await this.#startComponent(component);
134141
}
135142
Deno.addSignalListener('SIGTERM', this.close);
136143
(async () => {
@@ -165,7 +172,7 @@ export class Lifecycle {
165172
this.#setStatus('closed');
166173
shouldExit && Deno.exit(0);
167174
};
168-
private async startComponent(component: LifecycleComponent): Promise<void> {
175+
async #startComponent(component: LifecycleComponent): Promise<void> {
169176
await component.start();
170177
this.#emit('componentStarted', component.name);
171178
}
@@ -174,14 +181,14 @@ export class Lifecycle {
174181
await component.close();
175182
this.#emit('componentClosed', component.name);
176183
}
177-
private async restartComponent(component: LifecycleComponent): Promise<void> {
184+
async #restartComponent(component: LifecycleComponent): Promise<void> {
178185
this.#emit('componentRestarting', component.name);
179186
await (component.restart ?? component.start)();
180187
this.#emit('componentRestarted', component.name);
181188
}
182189
async #checkComponentHealth(): Promise<void> {
183190
for (const component of this.#components) {
184-
if (await hasCrashed(component)) await this.restartComponent(component);
191+
if (await hasCrashed(component)) await this.#restartComponent(component);
185192
}
186193
this.#emit('healthChecked');
187194
}

0 commit comments

Comments
 (0)