@hardlydifficult/daemon 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +227 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,42 @@ Opinionated utilities for long-running Node.js services:
|
|
|
11
11
|
npm install @hardlydifficult/daemon
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
Create a daemon with signal-trapped teardown and a continuous loop:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createTeardown, runContinuousLoop } from "@hardlydifficult/daemon";
|
|
20
|
+
|
|
21
|
+
const teardown = createTeardown();
|
|
22
|
+
|
|
23
|
+
// Register cleanup for resources
|
|
24
|
+
teardown.add(() => console.log("Cleanup: closing server"));
|
|
25
|
+
teardown.add(() => console.log("Cleanup: disconnecting database"));
|
|
26
|
+
|
|
27
|
+
// Trap SIGINT/SIGTERM
|
|
28
|
+
teardown.trapSignals();
|
|
29
|
+
|
|
30
|
+
// Run a continuous loop
|
|
31
|
+
await runContinuousLoop({
|
|
32
|
+
intervalSeconds: 5,
|
|
33
|
+
async runCycle(isShutdownRequested) {
|
|
34
|
+
console.log("Running cycle...");
|
|
35
|
+
if (isShutdownRequested()) {
|
|
36
|
+
return { stop: true };
|
|
37
|
+
}
|
|
38
|
+
// Perform background task
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
40
|
+
return { stop: true }; // Stop after first cycle for demo
|
|
41
|
+
},
|
|
42
|
+
onShutdown: async () => {
|
|
43
|
+
console.log("Shutdown complete");
|
|
44
|
+
await teardown.run();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Teardown Management
|
|
15
50
|
|
|
16
51
|
Use `createTeardown()` to register cleanup functions once and execute them from
|
|
17
52
|
every exit path.
|
|
@@ -29,14 +64,62 @@ teardown.trapSignals();
|
|
|
29
64
|
await teardown.run();
|
|
30
65
|
```
|
|
31
66
|
|
|
32
|
-
Behavior
|
|
67
|
+
### Behavior
|
|
68
|
+
|
|
69
|
+
- **LIFO execution**: last added, first run
|
|
70
|
+
- **Idempotent `run()`**: safe to call multiple times
|
|
71
|
+
- **Per-function error isolation**: one failing teardown does not block others
|
|
72
|
+
- **`add()` returns an unregister function**: allowing selective cleanup removal
|
|
33
73
|
|
|
34
|
-
|
|
35
|
-
- Idempotent `run()` (safe to call multiple times)
|
|
36
|
-
- Per-function error isolation (one failing teardown does not block others)
|
|
37
|
-
- `add()` returns an unregister function
|
|
74
|
+
### `createTeardown()`
|
|
38
75
|
|
|
39
|
-
|
|
76
|
+
Creates a teardown registry with idempotent resource cleanup.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const teardown = createTeardown();
|
|
80
|
+
|
|
81
|
+
// Register cleanup functions
|
|
82
|
+
const unregister = teardown.add(() => server.stop());
|
|
83
|
+
teardown.add(async () => db.close());
|
|
84
|
+
|
|
85
|
+
// Unregister a specific cleanup
|
|
86
|
+
unregister();
|
|
87
|
+
|
|
88
|
+
// Manually trigger shutdown
|
|
89
|
+
await teardown.run();
|
|
90
|
+
|
|
91
|
+
// Wire SIGTERM/SIGINT handlers
|
|
92
|
+
const untrap = teardown.trapSignals();
|
|
93
|
+
// Later...
|
|
94
|
+
untrap(); // Remove handlers
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Teardown API
|
|
98
|
+
|
|
99
|
+
| Method | Description |
|
|
100
|
+
|--------|-----------|
|
|
101
|
+
| `add(fn)` | Register a cleanup function; returns an unregister function |
|
|
102
|
+
| `run()` | Run all teardown functions in LIFO order (idempotent) |
|
|
103
|
+
| `trapSignals()` | Wire SIGINT/SIGTERM to `run()` then `process.exit(0)`; returns untrap function |
|
|
104
|
+
|
|
105
|
+
### LIFO Execution
|
|
106
|
+
|
|
107
|
+
Teardown functions run in reverse registration order:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const teardown = createTeardown();
|
|
111
|
+
teardown.add(() => console.log("First"));
|
|
112
|
+
teardown.add(() => console.log("Second"));
|
|
113
|
+
teardown.add(() => console.log("Third"));
|
|
114
|
+
|
|
115
|
+
await teardown.run();
|
|
116
|
+
// Output:
|
|
117
|
+
// Third
|
|
118
|
+
// Second
|
|
119
|
+
// First
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Continuous Loop Execution
|
|
40
123
|
|
|
41
124
|
Use `runContinuousLoop()` to run work cycles with graceful shutdown, dynamic
|
|
42
125
|
delay control, and configurable error policy.
|
|
@@ -65,7 +148,7 @@ await runContinuousLoop({
|
|
|
65
148
|
});
|
|
66
149
|
```
|
|
67
150
|
|
|
68
|
-
### Cycle
|
|
151
|
+
### Cycle Return Contract
|
|
69
152
|
|
|
70
153
|
`runCycle()` can return:
|
|
71
154
|
|
|
@@ -75,18 +158,18 @@ await runContinuousLoop({
|
|
|
75
158
|
- `{ stop: true }`: stop gracefully after current cycle
|
|
76
159
|
- `{ nextDelayMs: number | "immediate", stop?: true }`: explicit control object
|
|
77
160
|
|
|
78
|
-
### Optional
|
|
161
|
+
### Optional Delay Resolver
|
|
79
162
|
|
|
80
163
|
If your cycle returns domain data, derive schedule policy with
|
|
81
164
|
`getNextDelayMs(result, context)`.
|
|
82
165
|
|
|
83
|
-
### Error
|
|
166
|
+
### Error Handling
|
|
84
167
|
|
|
85
168
|
Use `onCycleError(error, context)` to route to Slack/Sentry and decide whether
|
|
86
169
|
to `"continue"` or `"stop"`. Without this hook, cycle errors are logged and the
|
|
87
170
|
loop continues.
|
|
88
171
|
|
|
89
|
-
### Logger
|
|
172
|
+
### Logger Injection
|
|
90
173
|
|
|
91
174
|
By default, warnings and errors use `console.warn` and `console.error`. Pass
|
|
92
175
|
`logger` to integrate your own logging implementation:
|
|
@@ -97,3 +180,136 @@ const logger = {
|
|
|
97
180
|
error: (message, context) => myLogger.error(message, context),
|
|
98
181
|
};
|
|
99
182
|
```
|
|
183
|
+
|
|
184
|
+
### `runContinuousLoop()` Options
|
|
185
|
+
|
|
186
|
+
| Option | Type | Default | Description |
|
|
187
|
+
|--------|------|---------|-------------|
|
|
188
|
+
| `intervalSeconds` | `number` | — | Interval between cycles in seconds |
|
|
189
|
+
| `runCycle` | `Function` | — | Callback for each cycle |
|
|
190
|
+
| `getNextDelayMs?` | `Function` | — | Derive delay from cycle result |
|
|
191
|
+
| `onCycleError?` | `Function` | — | Handle cycle errors |
|
|
192
|
+
| `onShutdown?` | `Function` | — | Cleanup on shutdown |
|
|
193
|
+
| `logger?` | `ContinuousLoopLogger` | `console` | Logger for warnings/errors |
|
|
194
|
+
|
|
195
|
+
### `ContinuousLoopRunCycleResult`
|
|
196
|
+
|
|
197
|
+
The return type supports:
|
|
198
|
+
- Raw delay (`number` or `"immediate"`)
|
|
199
|
+
- Control object: `{ stop?: boolean; nextDelayMs?: ContinuousLoopDelay }`
|
|
200
|
+
|
|
201
|
+
**Example:**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
async runCycle() {
|
|
205
|
+
// Return raw delay
|
|
206
|
+
return 5000;
|
|
207
|
+
|
|
208
|
+
// Or return control directives
|
|
209
|
+
return { nextDelayMs: "immediate", stop: false };
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### `ContinuousLoopDelay`
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
type ContinuousLoopDelay = number | "immediate"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `ContinuousLoopCycleControl`
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
interface ContinuousLoopCycleControl {
|
|
223
|
+
stop?: boolean;
|
|
224
|
+
nextDelayMs?: ContinuousLoopDelay;
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `ContinuousLoopCycleContext`
|
|
229
|
+
|
|
230
|
+
Provides context to cycle and delay resolver functions.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
interface ContinuousLoopCycleContext {
|
|
234
|
+
cycleNumber: number;
|
|
235
|
+
isShutdownRequested: () => boolean;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### `ContinuousLoopErrorContext`
|
|
240
|
+
|
|
241
|
+
Same as `ContinuousLoopCycleContext`.
|
|
242
|
+
|
|
243
|
+
### `ContinuousLoopErrorHandler`
|
|
244
|
+
|
|
245
|
+
Handles errors and returns `"stop"` or `"continue"`.
|
|
246
|
+
|
|
247
|
+
**Signature:**
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
type ContinuousLoopErrorHandler = (
|
|
251
|
+
error: unknown,
|
|
252
|
+
context: ContinuousLoopErrorContext
|
|
253
|
+
) => ContinuousLoopErrorAction | Promise<ContinuousLoopErrorAction>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Example:**
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
onCycleError: async (error, context) => {
|
|
260
|
+
console.error(`Cycle ${context.cycleNumber} failed: ${error.message}`);
|
|
261
|
+
return "stop"; // or "continue"
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### `ContinuousLoopErrorAction`
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
type ContinuousLoopErrorAction = "continue" | "stop"
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### `ContinuousLoopLogger`
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
interface ContinuousLoopLogger {
|
|
275
|
+
warn(message: string, context?: Readonly<Record<string, unknown>>): void;
|
|
276
|
+
error(message: string, context?: Readonly<Record<string, unknown>>): void;
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Example:**
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const logger = {
|
|
284
|
+
warn: (msg) => console.warn(`[WARN] ${msg}`),
|
|
285
|
+
error: (msg) => console.error(`[ERROR] ${msg}`)
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
await runContinuousLoop({
|
|
289
|
+
intervalSeconds: 10,
|
|
290
|
+
runCycle: () => Promise.resolve(),
|
|
291
|
+
logger
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Shutdown
|
|
296
|
+
|
|
297
|
+
Signal handlers are automatically registered for `SIGINT` and `SIGTERM`, and removed on completion.
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const loopPromise = runContinuousLoop({
|
|
301
|
+
intervalSeconds: 1,
|
|
302
|
+
runCycle: async (isShutdownRequested) => {
|
|
303
|
+
while (!isShutdownRequested()) {
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
onShutdown: async () => {
|
|
308
|
+
await db.close();
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Later...
|
|
313
|
+
process.kill(process.pid, "SIGTERM"); // Triggers graceful shutdown
|
|
314
|
+
await loopPromise; // Resolves after onShutdown completes
|
|
315
|
+
```
|