@hardlydifficult/daemon 1.0.4 → 1.0.5
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 +103 -229
- 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
|
+
Graceful shutdown and continuous loop utilities for Node.js daemon processes.
|
|
7
4
|
|
|
8
5
|
## Installation
|
|
9
6
|
|
|
@@ -13,303 +10,180 @@ 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
14
|
import { createTeardown, runContinuousLoop } from "@hardlydifficult/daemon";
|
|
20
15
|
|
|
21
16
|
const teardown = createTeardown();
|
|
22
17
|
|
|
23
|
-
// Register cleanup
|
|
24
|
-
teardown.add(() => console.log("
|
|
25
|
-
teardown.add(() => console.log("
|
|
18
|
+
// Register cleanup functions
|
|
19
|
+
teardown.add(() => console.log("Stopping server"));
|
|
20
|
+
teardown.add(() => console.log("Closing database connection"));
|
|
26
21
|
|
|
27
|
-
//
|
|
22
|
+
// Handle SIGINT/SIGTERM
|
|
28
23
|
teardown.trapSignals();
|
|
29
24
|
|
|
30
|
-
// Run a
|
|
25
|
+
// Run a background task loop
|
|
31
26
|
await runContinuousLoop({
|
|
32
27
|
intervalSeconds: 5,
|
|
33
|
-
async
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
40
|
-
return { stop: true }; // Stop after first cycle for demo
|
|
28
|
+
runCycle: async (isShutdownRequested) => {
|
|
29
|
+
if (isShutdownRequested()) return;
|
|
30
|
+
console.log("Running periodic task...");
|
|
31
|
+
// Simulate work
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
33
|
+
return { stop: false };
|
|
41
34
|
},
|
|
42
35
|
onShutdown: async () => {
|
|
43
|
-
console.log("
|
|
44
|
-
await teardown.run();
|
|
36
|
+
console.log("Shutting down loop");
|
|
45
37
|
}
|
|
46
38
|
});
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Teardown Management
|
|
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
39
|
|
|
40
|
+
// Manual shutdown
|
|
64
41
|
await teardown.run();
|
|
65
42
|
```
|
|
66
43
|
|
|
67
|
-
|
|
44
|
+
## Teardown Management
|
|
45
|
+
|
|
46
|
+
Idempotent resource teardown with signal trapping and LIFO cleanup order.
|
|
68
47
|
|
|
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
|
|
48
|
+
### createTeardown()
|
|
73
49
|
|
|
74
|
-
|
|
50
|
+
Creates a teardown registry for managing cleanup functions.
|
|
75
51
|
|
|
76
|
-
|
|
52
|
+
**Features:**
|
|
53
|
+
- Registers cleanup functions in order; runs them in LIFO order
|
|
54
|
+
- Swallows errors per-function so remaining cleanup still executes
|
|
55
|
+
- Idempotent: calling `run()` multiple times has no additional effect
|
|
56
|
+
- Signal trapping for SIGINT/SIGTERM that calls `run()` then exits
|
|
57
|
+
- Returns an unregister function for individual registrations
|
|
77
58
|
|
|
78
59
|
```typescript
|
|
79
60
|
const teardown = createTeardown();
|
|
80
61
|
|
|
81
|
-
//
|
|
82
|
-
const unregister = teardown.add(() => server.
|
|
83
|
-
teardown.add(
|
|
62
|
+
// Add cleanup functions
|
|
63
|
+
const unregister = teardown.add(() => server.close());
|
|
64
|
+
teardown.add(() => db.close());
|
|
84
65
|
|
|
85
|
-
// Unregister a specific
|
|
66
|
+
// Unregister a specific function if needed
|
|
86
67
|
unregister();
|
|
87
68
|
|
|
88
|
-
//
|
|
89
|
-
|
|
69
|
+
// Handle OS signals
|
|
70
|
+
teardown.trapSignals();
|
|
90
71
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
// Later...
|
|
94
|
-
untrap(); // Remove handlers
|
|
72
|
+
// Run all cleanup
|
|
73
|
+
await teardown.run();
|
|
95
74
|
```
|
|
96
75
|
|
|
97
|
-
#### Teardown
|
|
76
|
+
#### Teardown Interface
|
|
98
77
|
|
|
99
78
|
| Method | Description |
|
|
100
|
-
|
|
101
|
-
| `add(fn)` |
|
|
102
|
-
| `run()` |
|
|
103
|
-
| `trapSignals()` |
|
|
79
|
+
|--------|-------------|
|
|
80
|
+
| `add(fn)` | Registers a cleanup function (sync or async); returns unregister function |
|
|
81
|
+
| `run()` | Runs all registered cleanup functions in LIFO order; idempotent |
|
|
82
|
+
| `trapSignals()` | Attaches SIGINT/SIGTERM handlers; returns untrap function |
|
|
104
83
|
|
|
105
|
-
###
|
|
84
|
+
### Signal Trapping
|
|
106
85
|
|
|
107
|
-
|
|
86
|
+
Signal handlers are attached via `trapSignals()` and call `run()` then `process.exit(0)`.
|
|
108
87
|
|
|
109
88
|
```typescript
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
teardown.add(() => console.log("Third"));
|
|
114
|
-
|
|
115
|
-
await teardown.run();
|
|
116
|
-
// Output:
|
|
117
|
-
// Third
|
|
118
|
-
// Second
|
|
119
|
-
// First
|
|
89
|
+
const untrap = teardown.trapSignals();
|
|
90
|
+
// Later, if needed:
|
|
91
|
+
untrap(); // Remove signal handlers
|
|
120
92
|
```
|
|
121
93
|
|
|
122
94
|
## Continuous Loop Execution
|
|
123
95
|
|
|
124
|
-
|
|
125
|
-
delay control, and configurable error policy.
|
|
96
|
+
Interruptible loop with configurable cycle interval, error handling, and graceful shutdown.
|
|
126
97
|
|
|
127
|
-
|
|
128
|
-
|
|
98
|
+
### runContinuousLoop()
|
|
99
|
+
|
|
100
|
+
Runs a function repeatedly with signal-aware sleep that can be interrupted.
|
|
129
101
|
|
|
102
|
+
```typescript
|
|
130
103
|
await runContinuousLoop({
|
|
131
|
-
intervalSeconds:
|
|
132
|
-
async
|
|
104
|
+
intervalSeconds: 10,
|
|
105
|
+
runCycle: async (isShutdownRequested) => {
|
|
133
106
|
if (isShutdownRequested()) {
|
|
134
107
|
return { stop: true };
|
|
135
108
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return 60_000; // ms
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return "immediate";
|
|
109
|
+
await doWork();
|
|
110
|
+
// Return a delay override
|
|
111
|
+
return 5000; // milliseconds
|
|
143
112
|
},
|
|
144
|
-
onCycleError(error, context) {
|
|
145
|
-
|
|
113
|
+
onCycleError: async (error, context) => {
|
|
114
|
+
console.error(`Cycle ${context.cycleNumber} failed:`, error);
|
|
146
115
|
return "continue"; // or "stop"
|
|
147
116
|
},
|
|
117
|
+
onShutdown: async () => {
|
|
118
|
+
await cleanup();
|
|
119
|
+
}
|
|
148
120
|
});
|
|
149
121
|
```
|
|
150
122
|
|
|
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
|
|
160
|
-
|
|
161
|
-
### Optional Delay Resolver
|
|
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:
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
const logger = {
|
|
179
|
-
warn: (message, context) => myLogger.warn(message, context),
|
|
180
|
-
error: (message, context) => myLogger.error(message, context),
|
|
181
|
-
};
|
|
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`
|
|
123
|
+
#### Loop Lifecycle
|
|
214
124
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
125
|
+
- Runs cycles indefinitely until:
|
|
126
|
+
- `SIGINT` or `SIGTERM` is received
|
|
127
|
+
- `runCycle` returns `{ stop: true }`
|
|
128
|
+
- `onCycleError` returns `"stop"`
|
|
129
|
+
- Each cycle may override the default delay or signal immediate continuation
|
|
231
130
|
|
|
232
|
-
|
|
233
|
-
interface ContinuousLoopCycleContext {
|
|
234
|
-
cycleNumber: number;
|
|
235
|
-
isShutdownRequested: () => boolean;
|
|
236
|
-
}
|
|
237
|
-
```
|
|
131
|
+
### Options
|
|
238
132
|
|
|
239
|
-
|
|
133
|
+
| Option | Type | Description |
|
|
134
|
+
|--------|------|-------------|
|
|
135
|
+
| `intervalSeconds` | `number` | Default delay between cycles (seconds) |
|
|
136
|
+
| `runCycle` | `(isShutdownRequested: () => boolean) => Promise<...>` | Function to run each cycle |
|
|
137
|
+
| `getNextDelayMs?` | `(result, context) => ContinuousLoopDelay` | Derive delay from cycle result |
|
|
138
|
+
| `onCycleError?` | `(error, context) => "continue" \| "stop"` | Handle cycle errors |
|
|
139
|
+
| `onShutdown?` | `() => void \| Promise<void>` | Cleanup after shutdown |
|
|
140
|
+
| `logger?` | `ContinuousLoopLogger` | Optional custom logger |
|
|
240
141
|
|
|
241
|
-
|
|
142
|
+
### Cycle Return Values
|
|
242
143
|
|
|
243
|
-
|
|
144
|
+
Cycles may return:
|
|
145
|
+
- A delay in milliseconds (e.g., `5000`)
|
|
146
|
+
- `"immediate"` to continue without delay
|
|
147
|
+
- `{ stop: true }` to end the loop
|
|
148
|
+
- `{ nextDelayMs: number \| "immediate" }` to override delay
|
|
244
149
|
|
|
245
|
-
|
|
150
|
+
If the cycle returns a domain-specific result, use `getNextDelayMs` to derive the next delay.
|
|
246
151
|
|
|
247
|
-
|
|
152
|
+
### Error Handling
|
|
248
153
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
context: ContinuousLoopErrorContext
|
|
253
|
-
) => ContinuousLoopErrorAction | Promise<ContinuousLoopErrorAction>
|
|
254
|
-
```
|
|
154
|
+
By default:
|
|
155
|
+
- Errors are logged to `console.error`
|
|
156
|
+
- Loop continues to next cycle
|
|
255
157
|
|
|
256
|
-
|
|
158
|
+
Custom error handling via `onCycleError` can override behavior:
|
|
257
159
|
|
|
258
160
|
```typescript
|
|
259
161
|
onCycleError: async (error, context) => {
|
|
260
|
-
|
|
261
|
-
|
|
162
|
+
if (error instanceof FatalError) {
|
|
163
|
+
return "stop"; // Terminate loop
|
|
164
|
+
}
|
|
165
|
+
return "continue"; // Proceed
|
|
262
166
|
}
|
|
263
167
|
```
|
|
264
168
|
|
|
265
|
-
###
|
|
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
|
-
```
|
|
169
|
+
### Context Types
|
|
279
170
|
|
|
280
|
-
|
|
171
|
+
#### ContinuousLoopCycleContext
|
|
281
172
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
};
|
|
173
|
+
| Field | Type | Description |
|
|
174
|
+
|-------|------|-------------|
|
|
175
|
+
| `cycleNumber` | `number` | 1-based cycle number |
|
|
176
|
+
| `isShutdownRequested` | `() => boolean` | Check shutdown status |
|
|
287
177
|
|
|
288
|
-
|
|
289
|
-
intervalSeconds: 10,
|
|
290
|
-
runCycle: () => Promise.resolve(),
|
|
291
|
-
logger
|
|
292
|
-
});
|
|
293
|
-
```
|
|
178
|
+
#### ContinuousLoopErrorContext
|
|
294
179
|
|
|
295
|
-
|
|
180
|
+
Identical to `ContinuousLoopCycleContext`; passed to `onCycleError`.
|
|
296
181
|
|
|
297
|
-
|
|
182
|
+
#### ContinuousLoopLogger
|
|
298
183
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
while (!isShutdownRequested()) {
|
|
304
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
onShutdown: async () => {
|
|
308
|
-
await db.close();
|
|
309
|
-
},
|
|
310
|
-
});
|
|
184
|
+
| Method | Parameters | Description |
|
|
185
|
+
|--------|------------|-------------|
|
|
186
|
+
| `warn(message, context?)` | `string`, `Record<string, unknown>` | Warning log |
|
|
187
|
+
| `error(message, context?)` | `string`, `Record<string, unknown>` | Error log |
|
|
311
188
|
|
|
312
|
-
|
|
313
|
-
process.kill(process.pid, "SIGTERM"); // Triggers graceful shutdown
|
|
314
|
-
await loopPromise; // Resolves after onShutdown completes
|
|
315
|
-
```
|
|
189
|
+
Default logger uses `console.warn`/`console.error`.
|