@hardlydifficult/daemon 1.0.4 → 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.
Files changed (2) hide show
  1. package/README.md +87 -227
  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
+ A utility library for building long-running Node.js processes with graceful shutdown and continuous background task execution.
7
4
 
8
5
  ## Installation
9
6
 
@@ -13,303 +10,166 @@ 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
- import { createTeardown, runContinuousLoop } from "@hardlydifficult/daemon";
14
+ import { runContinuousLoop, createTeardown } from "@hardlydifficult/daemon";
20
15
 
16
+ // Setup graceful shutdown
21
17
  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
18
+ teardown.add(() => console.log("Shutting down..."));
28
19
  teardown.trapSignals();
29
20
 
30
- // Run a continuous loop
21
+ // Run a background task every 5 seconds
31
22
  await runContinuousLoop({
32
23
  intervalSeconds: 5,
33
- async runCycle(isShutdownRequested) {
24
+ runCycle: async (isShutdownRequested) => {
34
25
  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
26
+ if (isShutdownRequested()) return { stop: true };
27
+ return { nextDelayMs: "immediate" };
41
28
  },
42
29
  onShutdown: async () => {
43
- console.log("Shutdown complete");
44
30
  await teardown.run();
45
31
  }
46
32
  });
47
33
  ```
48
34
 
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
-
64
- await teardown.run();
65
- ```
35
+ ## Graceful Shutdown with `createTeardown`
66
36
 
67
- ### Behavior
37
+ Manages resource cleanup with LIFO execution order, signal trapping for SIGINT/SIGTERM, and idempotent teardown behavior.
68
38
 
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
39
+ ### Core API
73
40
 
74
- ### `createTeardown()`
41
+ #### `createTeardown(): Teardown`
75
42
 
76
- Creates a teardown registry with idempotent resource cleanup.
43
+ Creates a teardown manager for registering cleanup functions.
77
44
 
78
45
  ```typescript
79
46
  const teardown = createTeardown();
80
47
 
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
- ```
48
+ // Add cleanup functions
49
+ teardown.add(() => server.close());
50
+ teardown.add(() => db.close());
96
51
 
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
52
+ // Or use async functions
53
+ teardown.add(async () => {
54
+ await flushPendingWrites();
55
+ });
120
56
  ```
121
57
 
122
- ## Continuous Loop Execution
58
+ #### `Teardown.add(fn): () => void`
123
59
 
124
- Use `runContinuousLoop()` to run work cycles with graceful shutdown, dynamic
125
- delay control, and configurable error policy.
60
+ Registers a cleanup function. Returns an unregister function for removing it.
126
61
 
127
62
  ```typescript
128
- import { runContinuousLoop } from "@hardlydifficult/daemon";
63
+ const unregister = teardown.add(() => cleanup());
129
64
 
130
- await runContinuousLoop({
131
- intervalSeconds: 30,
132
- async runCycle(isShutdownRequested) {
133
- if (isShutdownRequested()) {
134
- return { stop: true };
135
- }
136
-
137
- const didWork = await syncQueue();
138
- if (!didWork) {
139
- return 60_000; // ms
140
- }
141
-
142
- return "immediate";
143
- },
144
- onCycleError(error, context) {
145
- notifyOps(error, { cycleNumber: context.cycleNumber });
146
- return "continue"; // or "stop"
147
- },
148
- });
65
+ // Later, remove it
66
+ unregister();
149
67
  ```
150
68
 
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
69
+ #### `Teardown.run(): Promise<void>`
160
70
 
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:
71
+ Runs all cleanup functions in LIFO order. Safe to call multiple times—subsequent calls are no-ops.
176
72
 
177
73
  ```typescript
178
- const logger = {
179
- warn: (message, context) => myLogger.warn(message, context),
180
- error: (message, context) => myLogger.error(message, context),
181
- };
74
+ await teardown.run(); // Runs last-in-first-out
182
75
  ```
183
76
 
184
- ### `runContinuousLoop()` Options
77
+ #### `Teardown.trapSignals(): () => void`
185
78
 
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:**
79
+ Wires SIGINT/SIGTERM to automatically call `run()` then `process.exit(0)`.
202
80
 
203
81
  ```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`
82
+ const untrap = teardown.trapSignals();
214
83
 
215
- ```typescript
216
- type ContinuousLoopDelay = number | "immediate"
84
+ // Later, restore default behavior
85
+ untrap();
217
86
  ```
218
87
 
219
- ### `ContinuousLoopCycleControl`
88
+ ### Behavior Notes
220
89
 
221
- ```typescript
222
- interface ContinuousLoopCycleControl {
223
- stop?: boolean;
224
- nextDelayMs?: ContinuousLoopDelay;
225
- }
226
- ```
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.
227
92
 
228
- ### `ContinuousLoopCycleContext`
93
+ ## Continuous Loop with `runContinuousLoop`
229
94
 
230
- Provides context to cycle and delay resolver functions.
95
+ Runs a recurring task in a loop with built-in signal handling, dynamic delays, and configurable error policies.
231
96
 
232
- ```typescript
233
- interface ContinuousLoopCycleContext {
234
- cycleNumber: number;
235
- isShutdownRequested: () => boolean;
236
- }
237
- ```
97
+ ### Core Options
238
98
 
239
- ### `ContinuousLoopErrorContext`
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) |
240
107
 
241
- Same as `ContinuousLoopCycleContext`.
108
+ ### Return Values from `runCycle`
242
109
 
243
- ### `ContinuousLoopErrorHandler`
110
+ You can control loop behavior by returning one of:
244
111
 
245
- Handles errors and returns `"stop"` or `"continue"`.
246
-
247
- **Signature:**
112
+ - **Delay value**: `number` (ms) or `"immediate"` to skip delay
113
+ - **Control object**: `{ stop?: boolean; nextDelayMs?: Delay }`
248
114
 
249
115
  ```typescript
250
- type ContinuousLoopErrorHandler = (
251
- error: unknown,
252
- context: ContinuousLoopErrorContext
253
- ) => ContinuousLoopErrorAction | Promise<ContinuousLoopErrorAction>
116
+ await runContinuousLoop({
117
+ intervalSeconds: 10,
118
+ runCycle: async () => {
119
+ // Skip delay after this cycle
120
+ return "immediate";
121
+ }
122
+ });
254
123
  ```
255
124
 
256
- **Example:**
125
+ ### Example: Backoff Loop
257
126
 
258
127
  ```typescript
259
- onCycleError: async (error, context) => {
260
- console.error(`Cycle ${context.cycleNumber} failed: ${error.message}`);
261
- return "stop"; // or "continue"
262
- }
263
- ```
128
+ import { runContinuousLoop } from "@hardlydifficult/daemon";
264
129
 
265
- ### `ContinuousLoopErrorAction`
130
+ type Result = { backoffMs: number } | { success: true };
266
131
 
267
- ```typescript
268
- type ContinuousLoopErrorAction = "continue" | "stop"
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
+ });
269
144
  ```
270
145
 
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
- ```
146
+ ### Error Handling
279
147
 
280
- **Example:**
148
+ By default, cycle errors are logged to console and the loop continues. Custom error handling:
281
149
 
282
150
  ```typescript
283
- const logger = {
284
- warn: (msg) => console.warn(`[WARN] ${msg}`),
285
- error: (msg) => console.error(`[ERROR] ${msg}`)
286
- };
287
-
288
151
  await runContinuousLoop({
289
- intervalSeconds: 10,
290
- runCycle: () => Promise.resolve(),
291
- logger
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"
157
+ }
292
158
  });
293
159
  ```
294
160
 
295
- ## Shutdown
161
+ ### Shutdown Signals
296
162
 
297
- Signal handlers are automatically registered for `SIGINT` and `SIGTERM`, and removed on completion.
163
+ The loop responds to `SIGINT` and `SIGTERM` by stopping after the current cycle completes. Use `isShutdownRequested()` inside `runCycle` to abort long-running work:
298
164
 
299
165
  ```typescript
300
- const loopPromise = runContinuousLoop({
166
+ await runContinuousLoop({
301
167
  intervalSeconds: 1,
302
168
  runCycle: async (isShutdownRequested) => {
303
169
  while (!isShutdownRequested()) {
304
- await new Promise((resolve) => setTimeout(resolve, 100));
170
+ await processChunk();
305
171
  }
306
- },
307
- onShutdown: async () => {
308
- await db.close();
309
- },
172
+ return { stop: true };
173
+ }
310
174
  });
311
-
312
- // Later...
313
- process.kill(process.pid, "SIGTERM"); // Triggers graceful shutdown
314
- await loopPromise; // Resolves after onShutdown completes
315
175
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/daemon",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [