@hardlydifficult/daemon 1.0.2 → 1.0.3
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 +177 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,35 @@ Opinionated utilities for long-running Node.js services:
|
|
|
11
11
|
npm install @hardlydifficult/daemon
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { createTeardown, runContinuousLoop } from "@hardlydifficult/daemon";
|
|
18
|
+
|
|
19
|
+
// Graceful shutdown with LIFO cleanup
|
|
20
|
+
const teardown = createTeardown();
|
|
21
|
+
teardown.add(() => console.log("Cleaning up server"));
|
|
22
|
+
teardown.add(() => console.log("Closing database connection"));
|
|
23
|
+
teardown.trapSignals();
|
|
24
|
+
|
|
25
|
+
// Continuous background task with signal-aware sleep
|
|
26
|
+
await runContinuousLoop({
|
|
27
|
+
intervalSeconds: 5,
|
|
28
|
+
runCycle: async (isShutdownRequested) => {
|
|
29
|
+
console.log("Running task...");
|
|
30
|
+
if (isShutdownRequested()) {
|
|
31
|
+
return { stop: true };
|
|
32
|
+
}
|
|
33
|
+
// Perform background work
|
|
34
|
+
return "immediate"; // Run next cycle immediately
|
|
35
|
+
},
|
|
36
|
+
onShutdown: async () => {
|
|
37
|
+
await teardown.run();
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Teardown Management
|
|
15
43
|
|
|
16
44
|
Use `createTeardown()` to register cleanup functions once and execute them from
|
|
17
45
|
every exit path.
|
|
@@ -29,14 +57,62 @@ teardown.trapSignals();
|
|
|
29
57
|
await teardown.run();
|
|
30
58
|
```
|
|
31
59
|
|
|
32
|
-
Behavior
|
|
60
|
+
### Behavior
|
|
61
|
+
|
|
62
|
+
- **LIFO execution**: last added, first run
|
|
63
|
+
- **Idempotent `run()`**: safe to call multiple times
|
|
64
|
+
- **Per-function error isolation**: one failing teardown does not block others
|
|
65
|
+
- **`add()` returns an unregister function**: allowing selective cleanup removal
|
|
66
|
+
|
|
67
|
+
### `createTeardown()`
|
|
68
|
+
|
|
69
|
+
Creates a teardown registry with idempotent resource cleanup.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const teardown = createTeardown();
|
|
73
|
+
|
|
74
|
+
// Register cleanup functions
|
|
75
|
+
const unregister = teardown.add(() => server.stop());
|
|
76
|
+
teardown.add(async () => db.close());
|
|
77
|
+
|
|
78
|
+
// Unregister a specific cleanup
|
|
79
|
+
unregister();
|
|
80
|
+
|
|
81
|
+
// Manually trigger shutdown
|
|
82
|
+
await teardown.run();
|
|
83
|
+
|
|
84
|
+
// Wire SIGTERM/SIGINT handlers
|
|
85
|
+
const untrap = teardown.trapSignals();
|
|
86
|
+
// Later...
|
|
87
|
+
untrap(); // Remove handlers
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Teardown API
|
|
91
|
+
|
|
92
|
+
| Method | Description |
|
|
93
|
+
|--------|-----------|
|
|
94
|
+
| `add(fn)` | Register a cleanup function; returns an unregister function |
|
|
95
|
+
| `run()` | Run all teardown functions in LIFO order (idempotent) |
|
|
96
|
+
| `trapSignals()` | Wire SIGINT/SIGTERM to `run()` then `process.exit(0)`; returns untrap function |
|
|
97
|
+
|
|
98
|
+
### LIFO Execution
|
|
33
99
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
100
|
+
Teardown functions run in reverse registration order:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const teardown = createTeardown();
|
|
104
|
+
teardown.add(() => console.log("First"));
|
|
105
|
+
teardown.add(() => console.log("Second"));
|
|
106
|
+
teardown.add(() => console.log("Third"));
|
|
107
|
+
|
|
108
|
+
await teardown.run();
|
|
109
|
+
// Output:
|
|
110
|
+
// Third
|
|
111
|
+
// Second
|
|
112
|
+
// First
|
|
113
|
+
```
|
|
38
114
|
|
|
39
|
-
## Continuous
|
|
115
|
+
## Continuous Loop Execution
|
|
40
116
|
|
|
41
117
|
Use `runContinuousLoop()` to run work cycles with graceful shutdown, dynamic
|
|
42
118
|
delay control, and configurable error policy.
|
|
@@ -65,7 +141,7 @@ await runContinuousLoop({
|
|
|
65
141
|
});
|
|
66
142
|
```
|
|
67
143
|
|
|
68
|
-
### Cycle
|
|
144
|
+
### Cycle Return Contract
|
|
69
145
|
|
|
70
146
|
`runCycle()` can return:
|
|
71
147
|
|
|
@@ -75,18 +151,18 @@ await runContinuousLoop({
|
|
|
75
151
|
- `{ stop: true }`: stop gracefully after current cycle
|
|
76
152
|
- `{ nextDelayMs: number | "immediate", stop?: true }`: explicit control object
|
|
77
153
|
|
|
78
|
-
### Optional
|
|
154
|
+
### Optional Delay Resolver
|
|
79
155
|
|
|
80
156
|
If your cycle returns domain data, derive schedule policy with
|
|
81
157
|
`getNextDelayMs(result, context)`.
|
|
82
158
|
|
|
83
|
-
### Error
|
|
159
|
+
### Error Handling
|
|
84
160
|
|
|
85
161
|
Use `onCycleError(error, context)` to route to Slack/Sentry and decide whether
|
|
86
162
|
to `"continue"` or `"stop"`. Without this hook, cycle errors are logged and the
|
|
87
163
|
loop continues.
|
|
88
164
|
|
|
89
|
-
### Logger
|
|
165
|
+
### Logger Injection
|
|
90
166
|
|
|
91
167
|
By default, warnings and errors use `console.warn` and `console.error`. Pass
|
|
92
168
|
`logger` to integrate your own logging implementation:
|
|
@@ -97,3 +173,93 @@ const logger = {
|
|
|
97
173
|
error: (message, context) => myLogger.error(message, context),
|
|
98
174
|
};
|
|
99
175
|
```
|
|
176
|
+
|
|
177
|
+
### `runContinuousLoop()` Options
|
|
178
|
+
|
|
179
|
+
| Option | Type | Description |
|
|
180
|
+
|--------|------|-------------|
|
|
181
|
+
| `intervalSeconds` | `number` | Base interval between cycles (converted to ms) |
|
|
182
|
+
| `runCycle` | `(isShutdownRequested: () => boolean) => Promise<...>` | Cycle function with shutdown check |
|
|
183
|
+
| `getNextDelayMs?` | `(...)` => `ContinuousLoopDelay \| undefined` | Derive delay from cycle result |
|
|
184
|
+
| `onCycleError?` | `ContinuousLoopErrorHandler` | Handle cycle errors, return `"continue"` or `"stop"` |
|
|
185
|
+
| `onShutdown?` | `() => void \| Promise<void>` | Cleanup called after shutdown completes |
|
|
186
|
+
| `logger?` | `ContinuousLoopLogger` | Custom logger (defaults to `console`) |
|
|
187
|
+
|
|
188
|
+
### Return Values
|
|
189
|
+
|
|
190
|
+
The `runCycle` function can return:
|
|
191
|
+
- A delay value (`number` ms or `"immediate"`)
|
|
192
|
+
- `{ stop: true }` to gracefully terminate
|
|
193
|
+
- `{ nextDelayMs: ... }` to override delay
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
await runContinuousLoop({
|
|
197
|
+
intervalSeconds: 5,
|
|
198
|
+
runCycle: async () => {
|
|
199
|
+
const data = await fetchData();
|
|
200
|
+
if (!data) {
|
|
201
|
+
return { nextDelayMs: "immediate" }; // Retry immediately
|
|
202
|
+
}
|
|
203
|
+
if (data.done) {
|
|
204
|
+
return { stop: true }; // End loop
|
|
205
|
+
}
|
|
206
|
+
return 2000; // Wait 2 seconds
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Delay Directives
|
|
212
|
+
|
|
213
|
+
| Directive | Description |
|
|
214
|
+
|----------|-------------|
|
|
215
|
+
| `number` | Milliseconds to wait before next cycle |
|
|
216
|
+
| `"immediate"` | Run next cycle without delay |
|
|
217
|
+
| `{ stop: true }` | Stop the loop after current cycle |
|
|
218
|
+
| `{ nextDelayMs: ... }` | Override default or derived delay |
|
|
219
|
+
|
|
220
|
+
### Error Handling Example
|
|
221
|
+
|
|
222
|
+
Cycles errors are caught and handled according to the error policy.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
await runContinuousLoop({
|
|
226
|
+
intervalSeconds: 1,
|
|
227
|
+
runCycle: async () => {
|
|
228
|
+
if (Math.random() > 0.8) {
|
|
229
|
+
throw new Error("Network failure");
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
onCycleError: async (error, context) => {
|
|
233
|
+
console.error(`Cycle ${context.cycleNumber} failed:`, error.message);
|
|
234
|
+
return "continue"; // Keep the loop running
|
|
235
|
+
},
|
|
236
|
+
logger: {
|
|
237
|
+
warn: (msg, ctx) => console.log(`[WARN] ${msg}`, ctx),
|
|
238
|
+
error: (msg, ctx) => console.error(`[ERROR] ${msg}`, ctx),
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
If no `onCycleError` is provided, errors are logged and the loop continues.
|
|
244
|
+
|
|
245
|
+
## Shutdown
|
|
246
|
+
|
|
247
|
+
Signal handlers are automatically registered for `SIGINT` and `SIGTERM`, and removed on completion.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
const loopPromise = runContinuousLoop({
|
|
251
|
+
intervalSeconds: 1,
|
|
252
|
+
runCycle: async (isShutdownRequested) => {
|
|
253
|
+
while (!isShutdownRequested()) {
|
|
254
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
onShutdown: async () => {
|
|
258
|
+
await db.close();
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Later...
|
|
263
|
+
process.kill(process.pid, "SIGTERM"); // Triggers graceful shutdown
|
|
264
|
+
await loopPromise; // Resolves after onShutdown completes
|
|
265
|
+
```
|