@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.
Files changed (2) hide show
  1. package/README.md +177 -11
  2. 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
- ## Teardown management
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
- - LIFO execution (last added, first run)
35
- - Idempotent `run()` (safe to call multiple times)
36
- - Per-function error isolation (one failing teardown does not block others)
37
- - `add()` returns an unregister function
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 loop execution
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 return contract
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 delay resolver
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 handling
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 injection
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
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/daemon",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [