@hardlydifficult/daemon 1.0.5 → 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 +103 -117
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/daemon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A utility library for building long-running Node.js processes with graceful shutdown and continuous background task execution.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -11,179 +11,165 @@ npm install @hardlydifficult/daemon
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import {
|
|
14
|
+
import { runContinuousLoop, createTeardown } from "@hardlydifficult/daemon";
|
|
15
15
|
|
|
16
|
+
// Setup graceful shutdown
|
|
16
17
|
const teardown = createTeardown();
|
|
17
|
-
|
|
18
|
-
// Register cleanup functions
|
|
19
|
-
teardown.add(() => console.log("Stopping server"));
|
|
20
|
-
teardown.add(() => console.log("Closing database connection"));
|
|
21
|
-
|
|
22
|
-
// Handle SIGINT/SIGTERM
|
|
18
|
+
teardown.add(() => console.log("Shutting down..."));
|
|
23
19
|
teardown.trapSignals();
|
|
24
20
|
|
|
25
|
-
// Run a background task
|
|
21
|
+
// Run a background task every 5 seconds
|
|
26
22
|
await runContinuousLoop({
|
|
27
23
|
intervalSeconds: 5,
|
|
28
24
|
runCycle: async (isShutdownRequested) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
33
|
-
return { stop: false };
|
|
25
|
+
console.log("Running cycle...");
|
|
26
|
+
if (isShutdownRequested()) return { stop: true };
|
|
27
|
+
return { nextDelayMs: "immediate" };
|
|
34
28
|
},
|
|
35
29
|
onShutdown: async () => {
|
|
36
|
-
|
|
30
|
+
await teardown.run();
|
|
37
31
|
}
|
|
38
32
|
});
|
|
39
|
-
|
|
40
|
-
// Manual shutdown
|
|
41
|
-
await teardown.run();
|
|
42
33
|
```
|
|
43
34
|
|
|
44
|
-
##
|
|
35
|
+
## Graceful Shutdown with `createTeardown`
|
|
45
36
|
|
|
46
|
-
|
|
37
|
+
Manages resource cleanup with LIFO execution order, signal trapping for SIGINT/SIGTERM, and idempotent teardown behavior.
|
|
47
38
|
|
|
48
|
-
###
|
|
39
|
+
### Core API
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
#### `createTeardown(): Teardown`
|
|
51
42
|
|
|
52
|
-
|
|
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
|
|
43
|
+
Creates a teardown manager for registering cleanup functions.
|
|
58
44
|
|
|
59
45
|
```typescript
|
|
60
46
|
const teardown = createTeardown();
|
|
61
47
|
|
|
62
48
|
// Add cleanup functions
|
|
63
|
-
|
|
49
|
+
teardown.add(() => server.close());
|
|
64
50
|
teardown.add(() => db.close());
|
|
65
51
|
|
|
66
|
-
//
|
|
67
|
-
|
|
52
|
+
// Or use async functions
|
|
53
|
+
teardown.add(async () => {
|
|
54
|
+
await flushPendingWrites();
|
|
55
|
+
});
|
|
56
|
+
```
|
|
68
57
|
|
|
69
|
-
|
|
70
|
-
teardown.trapSignals();
|
|
58
|
+
#### `Teardown.add(fn): () => void`
|
|
71
59
|
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
Registers a cleanup function. Returns an unregister function for removing it.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const unregister = teardown.add(() => cleanup());
|
|
64
|
+
|
|
65
|
+
// Later, remove it
|
|
66
|
+
unregister();
|
|
74
67
|
```
|
|
75
68
|
|
|
76
|
-
#### Teardown
|
|
69
|
+
#### `Teardown.run(): Promise<void>`
|
|
77
70
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
Runs all cleanup functions in LIFO order. Safe to call multiple times—subsequent calls are no-ops.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
await teardown.run(); // Runs last-in-first-out
|
|
75
|
+
```
|
|
83
76
|
|
|
84
|
-
|
|
77
|
+
#### `Teardown.trapSignals(): () => void`
|
|
85
78
|
|
|
86
|
-
|
|
79
|
+
Wires SIGINT/SIGTERM to automatically call `run()` then `process.exit(0)`.
|
|
87
80
|
|
|
88
81
|
```typescript
|
|
89
82
|
const untrap = teardown.trapSignals();
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
|
|
84
|
+
// Later, restore default behavior
|
|
85
|
+
untrap();
|
|
92
86
|
```
|
|
93
87
|
|
|
94
|
-
|
|
88
|
+
### Behavior Notes
|
|
89
|
+
|
|
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.
|
|
92
|
+
|
|
93
|
+
## Continuous Loop with `runContinuousLoop`
|
|
94
|
+
|
|
95
|
+
Runs a recurring task in a loop with built-in signal handling, dynamic delays, and configurable error policies.
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
### Core Options
|
|
98
|
+
|
|
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) |
|
|
97
107
|
|
|
98
|
-
###
|
|
108
|
+
### Return Values from `runCycle`
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
You can control loop behavior by returning one of:
|
|
111
|
+
|
|
112
|
+
- **Delay value**: `number` (ms) or `"immediate"` to skip delay
|
|
113
|
+
- **Control object**: `{ stop?: boolean; nextDelayMs?: Delay }`
|
|
101
114
|
|
|
102
115
|
```typescript
|
|
103
116
|
await runContinuousLoop({
|
|
104
117
|
intervalSeconds: 10,
|
|
105
|
-
runCycle: async (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
await doWork();
|
|
110
|
-
// Return a delay override
|
|
111
|
-
return 5000; // milliseconds
|
|
112
|
-
},
|
|
113
|
-
onCycleError: async (error, context) => {
|
|
114
|
-
console.error(`Cycle ${context.cycleNumber} failed:`, error);
|
|
115
|
-
return "continue"; // or "stop"
|
|
116
|
-
},
|
|
117
|
-
onShutdown: async () => {
|
|
118
|
-
await cleanup();
|
|
118
|
+
runCycle: async () => {
|
|
119
|
+
// Skip delay after this cycle
|
|
120
|
+
return "immediate";
|
|
119
121
|
}
|
|
120
122
|
});
|
|
121
123
|
```
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
125
|
+
### Example: Backoff Loop
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
|
|
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 |
|
|
141
|
-
|
|
142
|
-
### Cycle Return Values
|
|
127
|
+
```typescript
|
|
128
|
+
import { runContinuousLoop } from "@hardlydifficult/daemon";
|
|
143
129
|
|
|
144
|
-
|
|
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
|
|
130
|
+
type Result = { backoffMs: number } | { success: true };
|
|
149
131
|
|
|
150
|
-
|
|
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
|
+
});
|
|
144
|
+
```
|
|
151
145
|
|
|
152
146
|
### Error Handling
|
|
153
147
|
|
|
154
|
-
By default:
|
|
155
|
-
- Errors are logged to `console.error`
|
|
156
|
-
- Loop continues to next cycle
|
|
157
|
-
|
|
158
|
-
Custom error handling via `onCycleError` can override behavior:
|
|
148
|
+
By default, cycle errors are logged to console and the loop continues. Custom error handling:
|
|
159
149
|
|
|
160
150
|
```typescript
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
151
|
+
await runContinuousLoop({
|
|
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"
|
|
164
157
|
}
|
|
165
|
-
|
|
166
|
-
}
|
|
158
|
+
});
|
|
167
159
|
```
|
|
168
160
|
|
|
169
|
-
###
|
|
170
|
-
|
|
171
|
-
#### ContinuousLoopCycleContext
|
|
172
|
-
|
|
173
|
-
| Field | Type | Description |
|
|
174
|
-
|-------|------|-------------|
|
|
175
|
-
| `cycleNumber` | `number` | 1-based cycle number |
|
|
176
|
-
| `isShutdownRequested` | `() => boolean` | Check shutdown status |
|
|
177
|
-
|
|
178
|
-
#### ContinuousLoopErrorContext
|
|
179
|
-
|
|
180
|
-
Identical to `ContinuousLoopCycleContext`; passed to `onCycleError`.
|
|
181
|
-
|
|
182
|
-
#### ContinuousLoopLogger
|
|
161
|
+
### Shutdown Signals
|
|
183
162
|
|
|
184
|
-
|
|
185
|
-
|--------|------------|-------------|
|
|
186
|
-
| `warn(message, context?)` | `string`, `Record<string, unknown>` | Warning log |
|
|
187
|
-
| `error(message, context?)` | `string`, `Record<string, unknown>` | Error log |
|
|
163
|
+
The loop responds to `SIGINT` and `SIGTERM` by stopping after the current cycle completes. Use `isShutdownRequested()` inside `runCycle` to abort long-running work:
|
|
188
164
|
|
|
189
|
-
|
|
165
|
+
```typescript
|
|
166
|
+
await runContinuousLoop({
|
|
167
|
+
intervalSeconds: 1,
|
|
168
|
+
runCycle: async (isShutdownRequested) => {
|
|
169
|
+
while (!isShutdownRequested()) {
|
|
170
|
+
await processChunk();
|
|
171
|
+
}
|
|
172
|
+
return { stop: true };
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
```
|