@hardlydifficult/daemon 1.0.4 → 1.0.6
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 +87 -227
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/daemon
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- `createTeardown()` for idempotent cleanup with signal trapping
|
|
6
|
-
- `runContinuousLoop()` for daemon-style cycle execution
|
|
3
|
+
A utility library for building long-running Node.js processes with graceful shutdown and continuous background task execution.
|
|
7
4
|
|
|
8
5
|
## Installation
|
|
9
6
|
|
|
@@ -13,303 +10,166 @@ npm install @hardlydifficult/daemon
|
|
|
13
10
|
|
|
14
11
|
## Quick Start
|
|
15
12
|
|
|
16
|
-
Create a daemon with signal-trapped teardown and a continuous loop:
|
|
17
|
-
|
|
18
13
|
```typescript
|
|
19
|
-
import {
|
|
14
|
+
import { runContinuousLoop, createTeardown } from "@hardlydifficult/daemon";
|
|
20
15
|
|
|
16
|
+
// Setup graceful shutdown
|
|
21
17
|
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
|
|
18
|
+
teardown.add(() => console.log("Shutting down..."));
|
|
28
19
|
teardown.trapSignals();
|
|
29
20
|
|
|
30
|
-
// Run a
|
|
21
|
+
// Run a background task every 5 seconds
|
|
31
22
|
await runContinuousLoop({
|
|
32
23
|
intervalSeconds: 5,
|
|
33
|
-
async
|
|
24
|
+
runCycle: async (isShutdownRequested) => {
|
|
34
25
|
console.log("Running cycle...");
|
|
35
|
-
if (isShutdownRequested()) {
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
// Perform background task
|
|
39
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
40
|
-
return { stop: true }; // Stop after first cycle for demo
|
|
26
|
+
if (isShutdownRequested()) return { stop: true };
|
|
27
|
+
return { nextDelayMs: "immediate" };
|
|
41
28
|
},
|
|
42
29
|
onShutdown: async () => {
|
|
43
|
-
console.log("Shutdown complete");
|
|
44
30
|
await teardown.run();
|
|
45
31
|
}
|
|
46
32
|
});
|
|
47
33
|
```
|
|
48
34
|
|
|
49
|
-
##
|
|
50
|
-
|
|
51
|
-
Use `createTeardown()` to register cleanup functions once and execute them from
|
|
52
|
-
every exit path.
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
import { createTeardown } from "@hardlydifficult/daemon";
|
|
56
|
-
|
|
57
|
-
const teardown = createTeardown();
|
|
58
|
-
teardown.add(() => server.stop());
|
|
59
|
-
teardown.add(async () => {
|
|
60
|
-
await db.close();
|
|
61
|
-
});
|
|
62
|
-
teardown.trapSignals();
|
|
63
|
-
|
|
64
|
-
await teardown.run();
|
|
65
|
-
```
|
|
35
|
+
## Graceful Shutdown with `createTeardown`
|
|
66
36
|
|
|
67
|
-
|
|
37
|
+
Manages resource cleanup with LIFO execution order, signal trapping for SIGINT/SIGTERM, and idempotent teardown behavior.
|
|
68
38
|
|
|
69
|
-
|
|
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
|
|
39
|
+
### Core API
|
|
73
40
|
|
|
74
|
-
|
|
41
|
+
#### `createTeardown(): Teardown`
|
|
75
42
|
|
|
76
|
-
Creates a teardown
|
|
43
|
+
Creates a teardown manager for registering cleanup functions.
|
|
77
44
|
|
|
78
45
|
```typescript
|
|
79
46
|
const teardown = createTeardown();
|
|
80
47
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
teardown.add(
|
|
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
|
-
```
|
|
48
|
+
// Add cleanup functions
|
|
49
|
+
teardown.add(() => server.close());
|
|
50
|
+
teardown.add(() => db.close());
|
|
96
51
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
52
|
+
// Or use async functions
|
|
53
|
+
teardown.add(async () => {
|
|
54
|
+
await flushPendingWrites();
|
|
55
|
+
});
|
|
120
56
|
```
|
|
121
57
|
|
|
122
|
-
|
|
58
|
+
#### `Teardown.add(fn): () => void`
|
|
123
59
|
|
|
124
|
-
|
|
125
|
-
delay control, and configurable error policy.
|
|
60
|
+
Registers a cleanup function. Returns an unregister function for removing it.
|
|
126
61
|
|
|
127
62
|
```typescript
|
|
128
|
-
|
|
63
|
+
const unregister = teardown.add(() => cleanup());
|
|
129
64
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
async runCycle(isShutdownRequested) {
|
|
133
|
-
if (isShutdownRequested()) {
|
|
134
|
-
return { stop: true };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const didWork = await syncQueue();
|
|
138
|
-
if (!didWork) {
|
|
139
|
-
return 60_000; // ms
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return "immediate";
|
|
143
|
-
},
|
|
144
|
-
onCycleError(error, context) {
|
|
145
|
-
notifyOps(error, { cycleNumber: context.cycleNumber });
|
|
146
|
-
return "continue"; // or "stop"
|
|
147
|
-
},
|
|
148
|
-
});
|
|
65
|
+
// Later, remove it
|
|
66
|
+
unregister();
|
|
149
67
|
```
|
|
150
68
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
`runCycle()` can return:
|
|
154
|
-
|
|
155
|
-
- any value/`undefined`: use default `intervalSeconds`
|
|
156
|
-
- `number`: use that delay in milliseconds
|
|
157
|
-
- `"immediate"`: run the next cycle without sleeping
|
|
158
|
-
- `{ stop: true }`: stop gracefully after current cycle
|
|
159
|
-
- `{ nextDelayMs: number | "immediate", stop?: true }`: explicit control object
|
|
69
|
+
#### `Teardown.run(): Promise<void>`
|
|
160
70
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
If your cycle returns domain data, derive schedule policy with
|
|
164
|
-
`getNextDelayMs(result, context)`.
|
|
165
|
-
|
|
166
|
-
### Error Handling
|
|
167
|
-
|
|
168
|
-
Use `onCycleError(error, context)` to route to Slack/Sentry and decide whether
|
|
169
|
-
to `"continue"` or `"stop"`. Without this hook, cycle errors are logged and the
|
|
170
|
-
loop continues.
|
|
171
|
-
|
|
172
|
-
### Logger Injection
|
|
173
|
-
|
|
174
|
-
By default, warnings and errors use `console.warn` and `console.error`. Pass
|
|
175
|
-
`logger` to integrate your own logging implementation:
|
|
71
|
+
Runs all cleanup functions in LIFO order. Safe to call multiple times—subsequent calls are no-ops.
|
|
176
72
|
|
|
177
73
|
```typescript
|
|
178
|
-
|
|
179
|
-
warn: (message, context) => myLogger.warn(message, context),
|
|
180
|
-
error: (message, context) => myLogger.error(message, context),
|
|
181
|
-
};
|
|
74
|
+
await teardown.run(); // Runs last-in-first-out
|
|
182
75
|
```
|
|
183
76
|
|
|
184
|
-
|
|
77
|
+
#### `Teardown.trapSignals(): () => void`
|
|
185
78
|
|
|
186
|
-
|
|
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:**
|
|
79
|
+
Wires SIGINT/SIGTERM to automatically call `run()` then `process.exit(0)`.
|
|
202
80
|
|
|
203
81
|
```typescript
|
|
204
|
-
|
|
205
|
-
// Return raw delay
|
|
206
|
-
return 5000;
|
|
207
|
-
|
|
208
|
-
// Or return control directives
|
|
209
|
-
return { nextDelayMs: "immediate", stop: false };
|
|
210
|
-
}
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
### `ContinuousLoopDelay`
|
|
82
|
+
const untrap = teardown.trapSignals();
|
|
214
83
|
|
|
215
|
-
|
|
216
|
-
|
|
84
|
+
// Later, restore default behavior
|
|
85
|
+
untrap();
|
|
217
86
|
```
|
|
218
87
|
|
|
219
|
-
###
|
|
88
|
+
### Behavior Notes
|
|
220
89
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
stop?: boolean;
|
|
224
|
-
nextDelayMs?: ContinuousLoopDelay;
|
|
225
|
-
}
|
|
226
|
-
```
|
|
90
|
+
- Errors in teardown functions are caught and logged, allowing remaining functions to complete.
|
|
91
|
+
- Signal handlers are added only once per process and cleaned up automatically when untrap() is called.
|
|
227
92
|
|
|
228
|
-
|
|
93
|
+
## Continuous Loop with `runContinuousLoop`
|
|
229
94
|
|
|
230
|
-
|
|
95
|
+
Runs a recurring task in a loop with built-in signal handling, dynamic delays, and configurable error policies.
|
|
231
96
|
|
|
232
|
-
|
|
233
|
-
interface ContinuousLoopCycleContext {
|
|
234
|
-
cycleNumber: number;
|
|
235
|
-
isShutdownRequested: () => boolean;
|
|
236
|
-
}
|
|
237
|
-
```
|
|
97
|
+
### Core Options
|
|
238
98
|
|
|
239
|
-
|
|
99
|
+
| Option | Type | Description |
|
|
100
|
+
|--------|------|-------------|
|
|
101
|
+
| `intervalSeconds` | `number` | Default delay between cycles in seconds |
|
|
102
|
+
| `runCycle` | `() => Promise<RunCycleResult>` | Main function executed per cycle |
|
|
103
|
+
| `getNextDelayMs` | `() => Delay \| undefined` | Optional custom delay resolver |
|
|
104
|
+
| `onCycleError` | `ErrorHandler` | Custom error handling strategy |
|
|
105
|
+
| `onShutdown` | `() => Promise<void>` | Cleanup hook called on shutdown |
|
|
106
|
+
| `logger` | `ContinuousLoopLogger` | Optional logger (defaults to console) |
|
|
240
107
|
|
|
241
|
-
|
|
108
|
+
### Return Values from `runCycle`
|
|
242
109
|
|
|
243
|
-
|
|
110
|
+
You can control loop behavior by returning one of:
|
|
244
111
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
**Signature:**
|
|
112
|
+
- **Delay value**: `number` (ms) or `"immediate"` to skip delay
|
|
113
|
+
- **Control object**: `{ stop?: boolean; nextDelayMs?: Delay }`
|
|
248
114
|
|
|
249
115
|
```typescript
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
116
|
+
await runContinuousLoop({
|
|
117
|
+
intervalSeconds: 10,
|
|
118
|
+
runCycle: async () => {
|
|
119
|
+
// Skip delay after this cycle
|
|
120
|
+
return "immediate";
|
|
121
|
+
}
|
|
122
|
+
});
|
|
254
123
|
```
|
|
255
124
|
|
|
256
|
-
|
|
125
|
+
### Example: Backoff Loop
|
|
257
126
|
|
|
258
127
|
```typescript
|
|
259
|
-
|
|
260
|
-
console.error(`Cycle ${context.cycleNumber} failed: ${error.message}`);
|
|
261
|
-
return "stop"; // or "continue"
|
|
262
|
-
}
|
|
263
|
-
```
|
|
128
|
+
import { runContinuousLoop } from "@hardlydifficult/daemon";
|
|
264
129
|
|
|
265
|
-
|
|
130
|
+
type Result = { backoffMs: number } | { success: true };
|
|
266
131
|
|
|
267
|
-
|
|
268
|
-
|
|
132
|
+
await runContinuousLoop({
|
|
133
|
+
intervalSeconds: 60,
|
|
134
|
+
runCycle: async () => {
|
|
135
|
+
const success = await attemptTask();
|
|
136
|
+
return success
|
|
137
|
+
? { success: true }
|
|
138
|
+
: { backoffMs: Math.min(60_000, Math.random() * 5_000) };
|
|
139
|
+
},
|
|
140
|
+
getNextDelayMs: (result) =>
|
|
141
|
+
"backoffMs" in result ? result.backoffMs : undefined,
|
|
142
|
+
onShutdown: () => console.log("Stopping loop"),
|
|
143
|
+
});
|
|
269
144
|
```
|
|
270
145
|
|
|
271
|
-
###
|
|
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
|
-
```
|
|
146
|
+
### Error Handling
|
|
279
147
|
|
|
280
|
-
|
|
148
|
+
By default, cycle errors are logged to console and the loop continues. Custom error handling:
|
|
281
149
|
|
|
282
150
|
```typescript
|
|
283
|
-
const logger = {
|
|
284
|
-
warn: (msg) => console.warn(`[WARN] ${msg}`),
|
|
285
|
-
error: (msg) => console.error(`[ERROR] ${msg}`)
|
|
286
|
-
};
|
|
287
|
-
|
|
288
151
|
await runContinuousLoop({
|
|
289
|
-
intervalSeconds:
|
|
290
|
-
runCycle: () =>
|
|
291
|
-
|
|
152
|
+
intervalSeconds: 5,
|
|
153
|
+
runCycle: async () => { throw new Error("fail"); },
|
|
154
|
+
onCycleError: async (error, context) => {
|
|
155
|
+
console.error(`Cycle ${context.cycleNumber} failed: ${error.message}`);
|
|
156
|
+
return "stop"; // or "continue"
|
|
157
|
+
}
|
|
292
158
|
});
|
|
293
159
|
```
|
|
294
160
|
|
|
295
|
-
|
|
161
|
+
### Shutdown Signals
|
|
296
162
|
|
|
297
|
-
|
|
163
|
+
The loop responds to `SIGINT` and `SIGTERM` by stopping after the current cycle completes. Use `isShutdownRequested()` inside `runCycle` to abort long-running work:
|
|
298
164
|
|
|
299
165
|
```typescript
|
|
300
|
-
|
|
166
|
+
await runContinuousLoop({
|
|
301
167
|
intervalSeconds: 1,
|
|
302
168
|
runCycle: async (isShutdownRequested) => {
|
|
303
169
|
while (!isShutdownRequested()) {
|
|
304
|
-
await
|
|
170
|
+
await processChunk();
|
|
305
171
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
await db.close();
|
|
309
|
-
},
|
|
172
|
+
return { stop: true };
|
|
173
|
+
}
|
|
310
174
|
});
|
|
311
|
-
|
|
312
|
-
// Later...
|
|
313
|
-
process.kill(process.pid, "SIGTERM"); // Triggers graceful shutdown
|
|
314
|
-
await loopPromise; // Resolves after onShutdown completes
|
|
315
175
|
```
|