@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.
Files changed (2) hide show
  1. package/README.md +103 -229
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,9 +1,6 @@
1
1
  # @hardlydifficult/daemon
2
2
 
3
- Opinionated utilities for long-running Node.js services:
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 for resources
24
- teardown.add(() => console.log("Cleanup: closing server"));
25
- teardown.add(() => console.log("Cleanup: disconnecting database"));
18
+ // Register cleanup functions
19
+ teardown.add(() => console.log("Stopping server"));
20
+ teardown.add(() => console.log("Closing database connection"));
26
21
 
27
- // Trap SIGINT/SIGTERM
22
+ // Handle SIGINT/SIGTERM
28
23
  teardown.trapSignals();
29
24
 
30
- // Run a continuous loop
25
+ // Run a background task loop
31
26
  await runContinuousLoop({
32
27
  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
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("Shutdown complete");
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
- ### Behavior
44
+ ## Teardown Management
45
+
46
+ Idempotent resource teardown with signal trapping and LIFO cleanup order.
68
47
 
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
48
+ ### createTeardown()
73
49
 
74
- ### `createTeardown()`
50
+ Creates a teardown registry for managing cleanup functions.
75
51
 
76
- Creates a teardown registry with idempotent resource cleanup.
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
- // Register cleanup functions
82
- const unregister = teardown.add(() => server.stop());
83
- teardown.add(async () => db.close());
62
+ // Add cleanup functions
63
+ const unregister = teardown.add(() => server.close());
64
+ teardown.add(() => db.close());
84
65
 
85
- // Unregister a specific cleanup
66
+ // Unregister a specific function if needed
86
67
  unregister();
87
68
 
88
- // Manually trigger shutdown
89
- await teardown.run();
69
+ // Handle OS signals
70
+ teardown.trapSignals();
90
71
 
91
- // Wire SIGTERM/SIGINT handlers
92
- const untrap = teardown.trapSignals();
93
- // Later...
94
- untrap(); // Remove handlers
72
+ // Run all cleanup
73
+ await teardown.run();
95
74
  ```
96
75
 
97
- #### Teardown API
76
+ #### Teardown Interface
98
77
 
99
78
  | 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 |
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
- ### LIFO Execution
84
+ ### Signal Trapping
106
85
 
107
- Teardown functions run in reverse registration order:
86
+ Signal handlers are attached via `trapSignals()` and call `run()` then `process.exit(0)`.
108
87
 
109
88
  ```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
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
- Use `runContinuousLoop()` to run work cycles with graceful shutdown, dynamic
125
- delay control, and configurable error policy.
96
+ Interruptible loop with configurable cycle interval, error handling, and graceful shutdown.
126
97
 
127
- ```typescript
128
- import { runContinuousLoop } from "@hardlydifficult/daemon";
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: 30,
132
- async runCycle(isShutdownRequested) {
104
+ intervalSeconds: 10,
105
+ runCycle: async (isShutdownRequested) => {
133
106
  if (isShutdownRequested()) {
134
107
  return { stop: true };
135
108
  }
136
-
137
- const didWork = await syncQueue();
138
- if (!didWork) {
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
- notifyOps(error, { cycleNumber: context.cycleNumber });
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
- ### Cycle Return Contract
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
- ```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.
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
- ```typescript
233
- interface ContinuousLoopCycleContext {
234
- cycleNumber: number;
235
- isShutdownRequested: () => boolean;
236
- }
237
- ```
131
+ ### Options
238
132
 
239
- ### `ContinuousLoopErrorContext`
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
- Same as `ContinuousLoopCycleContext`.
142
+ ### Cycle Return Values
242
143
 
243
- ### `ContinuousLoopErrorHandler`
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
- Handles errors and returns `"stop"` or `"continue"`.
150
+ If the cycle returns a domain-specific result, use `getNextDelayMs` to derive the next delay.
246
151
 
247
- **Signature:**
152
+ ### Error Handling
248
153
 
249
- ```typescript
250
- type ContinuousLoopErrorHandler = (
251
- error: unknown,
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
- **Example:**
158
+ Custom error handling via `onCycleError` can override behavior:
257
159
 
258
160
  ```typescript
259
161
  onCycleError: async (error, context) => {
260
- console.error(`Cycle ${context.cycleNumber} failed: ${error.message}`);
261
- return "stop"; // or "continue"
162
+ if (error instanceof FatalError) {
163
+ return "stop"; // Terminate loop
164
+ }
165
+ return "continue"; // Proceed
262
166
  }
263
167
  ```
264
168
 
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
- ```
169
+ ### Context Types
279
170
 
280
- **Example:**
171
+ #### ContinuousLoopCycleContext
281
172
 
282
- ```typescript
283
- const logger = {
284
- warn: (msg) => console.warn(`[WARN] ${msg}`),
285
- error: (msg) => console.error(`[ERROR] ${msg}`)
286
- };
173
+ | Field | Type | Description |
174
+ |-------|------|-------------|
175
+ | `cycleNumber` | `number` | 1-based cycle number |
176
+ | `isShutdownRequested` | `() => boolean` | Check shutdown status |
287
177
 
288
- await runContinuousLoop({
289
- intervalSeconds: 10,
290
- runCycle: () => Promise.resolve(),
291
- logger
292
- });
293
- ```
178
+ #### ContinuousLoopErrorContext
294
179
 
295
- ## Shutdown
180
+ Identical to `ContinuousLoopCycleContext`; passed to `onCycleError`.
296
181
 
297
- Signal handlers are automatically registered for `SIGINT` and `SIGTERM`, and removed on completion.
182
+ #### ContinuousLoopLogger
298
183
 
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
- });
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
- // Later...
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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/daemon",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [