@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.
Files changed (2) hide show
  1. package/README.md +106 -182
  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
 
@@ -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
- teardown.add(() => console.log("Cleaning up server"));
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
- // Continuous background task with signal-aware sleep
25
+ // Run a background task loop
26
26
  await runContinuousLoop({
27
27
  intervalSeconds: 5,
28
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
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
- await teardown.run();
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
- ### Behavior
44
+ ## Teardown Management
61
45
 
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
46
+ Idempotent resource teardown with signal trapping and LIFO cleanup order.
66
47
 
67
- ### `createTeardown()`
48
+ ### createTeardown()
68
49
 
69
- Creates a teardown registry with idempotent resource cleanup.
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
- // Register cleanup functions
75
- const unregister = teardown.add(() => server.stop());
76
- teardown.add(async () => db.close());
62
+ // Add cleanup functions
63
+ const unregister = teardown.add(() => server.close());
64
+ teardown.add(() => db.close());
77
65
 
78
- // Unregister a specific cleanup
66
+ // Unregister a specific function if needed
79
67
  unregister();
80
68
 
81
- // Manually trigger shutdown
82
- await teardown.run();
69
+ // Handle OS signals
70
+ teardown.trapSignals();
83
71
 
84
- // Wire SIGTERM/SIGINT handlers
85
- const untrap = teardown.trapSignals();
86
- // Later...
87
- untrap(); // Remove handlers
72
+ // Run all cleanup
73
+ await teardown.run();
88
74
  ```
89
75
 
90
- #### Teardown API
76
+ #### Teardown Interface
91
77
 
92
78
  | 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 |
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
- ### LIFO Execution
84
+ ### Signal Trapping
99
85
 
100
- Teardown functions run in reverse registration order:
86
+ Signal handlers are attached via `trapSignals()` and call `run()` then `process.exit(0)`.
101
87
 
102
88
  ```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
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
- Use `runContinuousLoop()` to run work cycles with graceful shutdown, dynamic
118
- delay control, and configurable error policy.
96
+ Interruptible loop with configurable cycle interval, error handling, and graceful shutdown.
119
97
 
120
- ```typescript
121
- import { runContinuousLoop } from "@hardlydifficult/daemon";
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: 30,
125
- async runCycle(isShutdownRequested) {
104
+ intervalSeconds: 10,
105
+ runCycle: async (isShutdownRequested) => {
126
106
  if (isShutdownRequested()) {
127
107
  return { stop: true };
128
108
  }
129
-
130
- const didWork = await syncQueue();
131
- if (!didWork) {
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
- notifyOps(error, { cycleNumber: context.cycleNumber });
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
- ### Cycle Return Contract
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
- ### Error Handling
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
- Use `onCycleError(error, context)` to route to Slack/Sentry and decide whether
162
- to `"continue"` or `"stop"`. Without this hook, cycle errors are logged and the
163
- loop continues.
131
+ ### Options
164
132
 
165
- ### Logger Injection
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
- By default, warnings and errors use `console.warn` and `console.error`. Pass
168
- `logger` to integrate your own logging implementation:
142
+ ### Cycle Return Values
169
143
 
170
- ```typescript
171
- const logger = {
172
- warn: (message, context) => myLogger.warn(message, context),
173
- error: (message, context) => myLogger.error(message, context),
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
- ### `runContinuousLoop()` Options
150
+ If the cycle returns a domain-specific result, use `getNextDelayMs` to derive the next delay.
178
151
 
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`) |
152
+ ### Error Handling
187
153
 
188
- ### Return Values
154
+ By default:
155
+ - Errors are logged to `console.error`
156
+ - Loop continues to next cycle
189
157
 
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
158
+ Custom error handling via `onCycleError` can override behavior:
194
159
 
195
160
  ```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
- });
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
- #### Delay Directives
169
+ ### Context Types
212
170
 
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 |
171
+ #### ContinuousLoopCycleContext
219
172
 
220
- ### Error Handling Example
173
+ | Field | Type | Description |
174
+ |-------|------|-------------|
175
+ | `cycleNumber` | `number` | 1-based cycle number |
176
+ | `isShutdownRequested` | `() => boolean` | Check shutdown status |
221
177
 
222
- Cycles errors are caught and handled according to the error policy.
178
+ #### ContinuousLoopErrorContext
223
179
 
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
- ```
180
+ Identical to `ContinuousLoopCycleContext`; passed to `onCycleError`.
242
181
 
243
- If no `onCycleError` is provided, errors are logged and the loop continues.
182
+ #### ContinuousLoopLogger
244
183
 
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
- });
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
- // Later...
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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/daemon",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [