@hardlydifficult/daemon 1.0.2 → 1.0.4

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 +227 -11
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -11,7 +11,42 @@ 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
+ Create a daemon with signal-trapped teardown and a continuous loop:
17
+
18
+ ```typescript
19
+ import { createTeardown, runContinuousLoop } from "@hardlydifficult/daemon";
20
+
21
+ 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
28
+ teardown.trapSignals();
29
+
30
+ // Run a continuous loop
31
+ await runContinuousLoop({
32
+ intervalSeconds: 5,
33
+ async runCycle(isShutdownRequested) {
34
+ console.log("Running cycle...");
35
+ if (isShutdownRequested()) {
36
+ return { stop: true };
37
+ }
38
+ // Perform background task
39
+ await new Promise(resolve => setTimeout(resolve, 1000));
40
+ return { stop: true }; // Stop after first cycle for demo
41
+ },
42
+ onShutdown: async () => {
43
+ console.log("Shutdown complete");
44
+ await teardown.run();
45
+ }
46
+ });
47
+ ```
48
+
49
+ ## Teardown Management
15
50
 
16
51
  Use `createTeardown()` to register cleanup functions once and execute them from
17
52
  every exit path.
@@ -29,14 +64,62 @@ teardown.trapSignals();
29
64
  await teardown.run();
30
65
  ```
31
66
 
32
- Behavior:
67
+ ### Behavior
68
+
69
+ - **LIFO execution**: last added, first run
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
33
73
 
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
74
+ ### `createTeardown()`
38
75
 
39
- ## Continuous loop execution
76
+ Creates a teardown registry with idempotent resource cleanup.
77
+
78
+ ```typescript
79
+ const teardown = createTeardown();
80
+
81
+ // Register cleanup functions
82
+ const unregister = teardown.add(() => server.stop());
83
+ teardown.add(async () => db.close());
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
+ ```
96
+
97
+ #### Teardown API
98
+
99
+ | Method | Description |
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
120
+ ```
121
+
122
+ ## Continuous Loop Execution
40
123
 
41
124
  Use `runContinuousLoop()` to run work cycles with graceful shutdown, dynamic
42
125
  delay control, and configurable error policy.
@@ -65,7 +148,7 @@ await runContinuousLoop({
65
148
  });
66
149
  ```
67
150
 
68
- ### Cycle return contract
151
+ ### Cycle Return Contract
69
152
 
70
153
  `runCycle()` can return:
71
154
 
@@ -75,18 +158,18 @@ await runContinuousLoop({
75
158
  - `{ stop: true }`: stop gracefully after current cycle
76
159
  - `{ nextDelayMs: number | "immediate", stop?: true }`: explicit control object
77
160
 
78
- ### Optional delay resolver
161
+ ### Optional Delay Resolver
79
162
 
80
163
  If your cycle returns domain data, derive schedule policy with
81
164
  `getNextDelayMs(result, context)`.
82
165
 
83
- ### Error handling
166
+ ### Error Handling
84
167
 
85
168
  Use `onCycleError(error, context)` to route to Slack/Sentry and decide whether
86
169
  to `"continue"` or `"stop"`. Without this hook, cycle errors are logged and the
87
170
  loop continues.
88
171
 
89
- ### Logger injection
172
+ ### Logger Injection
90
173
 
91
174
  By default, warnings and errors use `console.warn` and `console.error`. Pass
92
175
  `logger` to integrate your own logging implementation:
@@ -97,3 +180,136 @@ const logger = {
97
180
  error: (message, context) => myLogger.error(message, context),
98
181
  };
99
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`
214
+
215
+ ```typescript
216
+ type ContinuousLoopDelay = number | "immediate"
217
+ ```
218
+
219
+ ### `ContinuousLoopCycleControl`
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.
231
+
232
+ ```typescript
233
+ interface ContinuousLoopCycleContext {
234
+ cycleNumber: number;
235
+ isShutdownRequested: () => boolean;
236
+ }
237
+ ```
238
+
239
+ ### `ContinuousLoopErrorContext`
240
+
241
+ Same as `ContinuousLoopCycleContext`.
242
+
243
+ ### `ContinuousLoopErrorHandler`
244
+
245
+ Handles errors and returns `"stop"` or `"continue"`.
246
+
247
+ **Signature:**
248
+
249
+ ```typescript
250
+ type ContinuousLoopErrorHandler = (
251
+ error: unknown,
252
+ context: ContinuousLoopErrorContext
253
+ ) => ContinuousLoopErrorAction | Promise<ContinuousLoopErrorAction>
254
+ ```
255
+
256
+ **Example:**
257
+
258
+ ```typescript
259
+ onCycleError: async (error, context) => {
260
+ console.error(`Cycle ${context.cycleNumber} failed: ${error.message}`);
261
+ return "stop"; // or "continue"
262
+ }
263
+ ```
264
+
265
+ ### `ContinuousLoopErrorAction`
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
+ ```
279
+
280
+ **Example:**
281
+
282
+ ```typescript
283
+ const logger = {
284
+ warn: (msg) => console.warn(`[WARN] ${msg}`),
285
+ error: (msg) => console.error(`[ERROR] ${msg}`)
286
+ };
287
+
288
+ await runContinuousLoop({
289
+ intervalSeconds: 10,
290
+ runCycle: () => Promise.resolve(),
291
+ logger
292
+ });
293
+ ```
294
+
295
+ ## Shutdown
296
+
297
+ Signal handlers are automatically registered for `SIGINT` and `SIGTERM`, and removed on completion.
298
+
299
+ ```typescript
300
+ const loopPromise = runContinuousLoop({
301
+ intervalSeconds: 1,
302
+ runCycle: async (isShutdownRequested) => {
303
+ while (!isShutdownRequested()) {
304
+ await new Promise((resolve) => setTimeout(resolve, 100));
305
+ }
306
+ },
307
+ onShutdown: async () => {
308
+ await db.close();
309
+ },
310
+ });
311
+
312
+ // Later...
313
+ process.kill(process.pid, "SIGTERM"); // Triggers graceful shutdown
314
+ await loopPromise; // Resolves after onShutdown completes
315
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/daemon",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [