@hardlydifficult/daemon 1.0.3 → 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 +106 -182
- 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
|
|
|
@@ -16,250 +13,177 @@ npm install @hardlydifficult/daemon
|
|
|
16
13
|
```typescript
|
|
17
14
|
import { createTeardown, runContinuousLoop } from "@hardlydifficult/daemon";
|
|
18
15
|
|
|
19
|
-
// Graceful shutdown with LIFO cleanup
|
|
20
16
|
const teardown = createTeardown();
|
|
21
|
-
|
|
17
|
+
|
|
18
|
+
// Register cleanup functions
|
|
19
|
+
teardown.add(() => console.log("Stopping server"));
|
|
22
20
|
teardown.add(() => console.log("Closing database connection"));
|
|
21
|
+
|
|
22
|
+
// Handle SIGINT/SIGTERM
|
|
23
23
|
teardown.trapSignals();
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// Run a background task loop
|
|
26
26
|
await runContinuousLoop({
|
|
27
27
|
intervalSeconds: 5,
|
|
28
28
|
runCycle: async (isShutdownRequested) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return "immediate"; // Run next cycle immediately
|
|
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 };
|
|
35
34
|
},
|
|
36
35
|
onShutdown: async () => {
|
|
37
|
-
|
|
38
|
-
}
|
|
36
|
+
console.log("Shutting down loop");
|
|
37
|
+
}
|
|
39
38
|
});
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Teardown Management
|
|
43
|
-
|
|
44
|
-
Use `createTeardown()` to register cleanup functions once and execute them from
|
|
45
|
-
every exit path.
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
import { createTeardown } from "@hardlydifficult/daemon";
|
|
49
|
-
|
|
50
|
-
const teardown = createTeardown();
|
|
51
|
-
teardown.add(() => server.stop());
|
|
52
|
-
teardown.add(async () => {
|
|
53
|
-
await db.close();
|
|
54
|
-
});
|
|
55
|
-
teardown.trapSignals();
|
|
56
39
|
|
|
40
|
+
// Manual shutdown
|
|
57
41
|
await teardown.run();
|
|
58
42
|
```
|
|
59
43
|
|
|
60
|
-
|
|
44
|
+
## Teardown Management
|
|
61
45
|
|
|
62
|
-
|
|
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
|
|
46
|
+
Idempotent resource teardown with signal trapping and LIFO cleanup order.
|
|
66
47
|
|
|
67
|
-
###
|
|
48
|
+
### createTeardown()
|
|
68
49
|
|
|
69
|
-
Creates a teardown registry
|
|
50
|
+
Creates a teardown registry for managing cleanup functions.
|
|
51
|
+
|
|
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
|
|
70
58
|
|
|
71
59
|
```typescript
|
|
72
60
|
const teardown = createTeardown();
|
|
73
61
|
|
|
74
|
-
//
|
|
75
|
-
const unregister = teardown.add(() => server.
|
|
76
|
-
teardown.add(
|
|
62
|
+
// Add cleanup functions
|
|
63
|
+
const unregister = teardown.add(() => server.close());
|
|
64
|
+
teardown.add(() => db.close());
|
|
77
65
|
|
|
78
|
-
// Unregister a specific
|
|
66
|
+
// Unregister a specific function if needed
|
|
79
67
|
unregister();
|
|
80
68
|
|
|
81
|
-
//
|
|
82
|
-
|
|
69
|
+
// Handle OS signals
|
|
70
|
+
teardown.trapSignals();
|
|
83
71
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
// Later...
|
|
87
|
-
untrap(); // Remove handlers
|
|
72
|
+
// Run all cleanup
|
|
73
|
+
await teardown.run();
|
|
88
74
|
```
|
|
89
75
|
|
|
90
|
-
#### Teardown
|
|
76
|
+
#### Teardown Interface
|
|
91
77
|
|
|
92
78
|
| Method | Description |
|
|
93
|
-
|
|
94
|
-
| `add(fn)` |
|
|
95
|
-
| `run()` |
|
|
96
|
-
| `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 |
|
|
97
83
|
|
|
98
|
-
###
|
|
84
|
+
### Signal Trapping
|
|
99
85
|
|
|
100
|
-
|
|
86
|
+
Signal handlers are attached via `trapSignals()` and call `run()` then `process.exit(0)`.
|
|
101
87
|
|
|
102
88
|
```typescript
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
teardown.add(() => console.log("Third"));
|
|
107
|
-
|
|
108
|
-
await teardown.run();
|
|
109
|
-
// Output:
|
|
110
|
-
// Third
|
|
111
|
-
// Second
|
|
112
|
-
// First
|
|
89
|
+
const untrap = teardown.trapSignals();
|
|
90
|
+
// Later, if needed:
|
|
91
|
+
untrap(); // Remove signal handlers
|
|
113
92
|
```
|
|
114
93
|
|
|
115
94
|
## Continuous Loop Execution
|
|
116
95
|
|
|
117
|
-
|
|
118
|
-
delay control, and configurable error policy.
|
|
96
|
+
Interruptible loop with configurable cycle interval, error handling, and graceful shutdown.
|
|
119
97
|
|
|
120
|
-
|
|
121
|
-
|
|
98
|
+
### runContinuousLoop()
|
|
99
|
+
|
|
100
|
+
Runs a function repeatedly with signal-aware sleep that can be interrupted.
|
|
122
101
|
|
|
102
|
+
```typescript
|
|
123
103
|
await runContinuousLoop({
|
|
124
|
-
intervalSeconds:
|
|
125
|
-
async
|
|
104
|
+
intervalSeconds: 10,
|
|
105
|
+
runCycle: async (isShutdownRequested) => {
|
|
126
106
|
if (isShutdownRequested()) {
|
|
127
107
|
return { stop: true };
|
|
128
108
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return 60_000; // ms
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return "immediate";
|
|
109
|
+
await doWork();
|
|
110
|
+
// Return a delay override
|
|
111
|
+
return 5000; // milliseconds
|
|
136
112
|
},
|
|
137
|
-
onCycleError(error, context) {
|
|
138
|
-
|
|
113
|
+
onCycleError: async (error, context) => {
|
|
114
|
+
console.error(`Cycle ${context.cycleNumber} failed:`, error);
|
|
139
115
|
return "continue"; // or "stop"
|
|
140
116
|
},
|
|
117
|
+
onShutdown: async () => {
|
|
118
|
+
await cleanup();
|
|
119
|
+
}
|
|
141
120
|
});
|
|
142
121
|
```
|
|
143
122
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`runCycle()` can return:
|
|
147
|
-
|
|
148
|
-
- any value/`undefined`: use default `intervalSeconds`
|
|
149
|
-
- `number`: use that delay in milliseconds
|
|
150
|
-
- `"immediate"`: run the next cycle without sleeping
|
|
151
|
-
- `{ stop: true }`: stop gracefully after current cycle
|
|
152
|
-
- `{ nextDelayMs: number | "immediate", stop?: true }`: explicit control object
|
|
153
|
-
|
|
154
|
-
### Optional Delay Resolver
|
|
155
|
-
|
|
156
|
-
If your cycle returns domain data, derive schedule policy with
|
|
157
|
-
`getNextDelayMs(result, context)`.
|
|
123
|
+
#### Loop Lifecycle
|
|
158
124
|
|
|
159
|
-
|
|
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
|
|
160
130
|
|
|
161
|
-
|
|
162
|
-
to `"continue"` or `"stop"`. Without this hook, cycle errors are logged and the
|
|
163
|
-
loop continues.
|
|
131
|
+
### Options
|
|
164
132
|
|
|
165
|
-
|
|
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 |
|
|
166
141
|
|
|
167
|
-
|
|
168
|
-
`logger` to integrate your own logging implementation:
|
|
142
|
+
### Cycle Return Values
|
|
169
143
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
```
|
|
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
|
|
176
149
|
|
|
177
|
-
|
|
150
|
+
If the cycle returns a domain-specific result, use `getNextDelayMs` to derive the next delay.
|
|
178
151
|
|
|
179
|
-
|
|
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`) |
|
|
152
|
+
### Error Handling
|
|
187
153
|
|
|
188
|
-
|
|
154
|
+
By default:
|
|
155
|
+
- Errors are logged to `console.error`
|
|
156
|
+
- Loop continues to next cycle
|
|
189
157
|
|
|
190
|
-
|
|
191
|
-
- A delay value (`number` ms or `"immediate"`)
|
|
192
|
-
- `{ stop: true }` to gracefully terminate
|
|
193
|
-
- `{ nextDelayMs: ... }` to override delay
|
|
158
|
+
Custom error handling via `onCycleError` can override behavior:
|
|
194
159
|
|
|
195
160
|
```typescript
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
if (data.done) {
|
|
204
|
-
return { stop: true }; // End loop
|
|
205
|
-
}
|
|
206
|
-
return 2000; // Wait 2 seconds
|
|
207
|
-
},
|
|
208
|
-
});
|
|
161
|
+
onCycleError: async (error, context) => {
|
|
162
|
+
if (error instanceof FatalError) {
|
|
163
|
+
return "stop"; // Terminate loop
|
|
164
|
+
}
|
|
165
|
+
return "continue"; // Proceed
|
|
166
|
+
}
|
|
209
167
|
```
|
|
210
168
|
|
|
211
|
-
|
|
169
|
+
### Context Types
|
|
212
170
|
|
|
213
|
-
|
|
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 |
|
|
171
|
+
#### ContinuousLoopCycleContext
|
|
219
172
|
|
|
220
|
-
|
|
173
|
+
| Field | Type | Description |
|
|
174
|
+
|-------|------|-------------|
|
|
175
|
+
| `cycleNumber` | `number` | 1-based cycle number |
|
|
176
|
+
| `isShutdownRequested` | `() => boolean` | Check shutdown status |
|
|
221
177
|
|
|
222
|
-
|
|
178
|
+
#### ContinuousLoopErrorContext
|
|
223
179
|
|
|
224
|
-
|
|
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
|
-
```
|
|
180
|
+
Identical to `ContinuousLoopCycleContext`; passed to `onCycleError`.
|
|
242
181
|
|
|
243
|
-
|
|
182
|
+
#### ContinuousLoopLogger
|
|
244
183
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
});
|
|
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 |
|
|
261
188
|
|
|
262
|
-
|
|
263
|
-
process.kill(process.pid, "SIGTERM"); // Triggers graceful shutdown
|
|
264
|
-
await loopPromise; // Resolves after onShutdown completes
|
|
265
|
-
```
|
|
189
|
+
Default logger uses `console.warn`/`console.error`.
|