@hardlydifficult/daemon 1.0.5 → 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 +103 -117
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hardlydifficult/daemon
2
2
 
3
- Graceful shutdown and continuous loop utilities for Node.js daemon processes.
3
+ A utility library for building long-running Node.js processes with graceful shutdown and continuous background task execution.
4
4
 
5
5
  ## Installation
6
6
 
@@ -11,179 +11,165 @@ npm install @hardlydifficult/daemon
11
11
  ## Quick Start
12
12
 
13
13
  ```typescript
14
- import { createTeardown, runContinuousLoop } from "@hardlydifficult/daemon";
14
+ import { runContinuousLoop, createTeardown } from "@hardlydifficult/daemon";
15
15
 
16
+ // Setup graceful shutdown
16
17
  const teardown = createTeardown();
17
-
18
- // Register cleanup functions
19
- teardown.add(() => console.log("Stopping server"));
20
- teardown.add(() => console.log("Closing database connection"));
21
-
22
- // Handle SIGINT/SIGTERM
18
+ teardown.add(() => console.log("Shutting down..."));
23
19
  teardown.trapSignals();
24
20
 
25
- // Run a background task loop
21
+ // Run a background task every 5 seconds
26
22
  await runContinuousLoop({
27
23
  intervalSeconds: 5,
28
24
  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 };
25
+ console.log("Running cycle...");
26
+ if (isShutdownRequested()) return { stop: true };
27
+ return { nextDelayMs: "immediate" };
34
28
  },
35
29
  onShutdown: async () => {
36
- console.log("Shutting down loop");
30
+ await teardown.run();
37
31
  }
38
32
  });
39
-
40
- // Manual shutdown
41
- await teardown.run();
42
33
  ```
43
34
 
44
- ## Teardown Management
35
+ ## Graceful Shutdown with `createTeardown`
45
36
 
46
- Idempotent resource teardown with signal trapping and LIFO cleanup order.
37
+ Manages resource cleanup with LIFO execution order, signal trapping for SIGINT/SIGTERM, and idempotent teardown behavior.
47
38
 
48
- ### createTeardown()
39
+ ### Core API
49
40
 
50
- Creates a teardown registry for managing cleanup functions.
41
+ #### `createTeardown(): Teardown`
51
42
 
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
43
+ Creates a teardown manager for registering cleanup functions.
58
44
 
59
45
  ```typescript
60
46
  const teardown = createTeardown();
61
47
 
62
48
  // Add cleanup functions
63
- const unregister = teardown.add(() => server.close());
49
+ teardown.add(() => server.close());
64
50
  teardown.add(() => db.close());
65
51
 
66
- // Unregister a specific function if needed
67
- unregister();
52
+ // Or use async functions
53
+ teardown.add(async () => {
54
+ await flushPendingWrites();
55
+ });
56
+ ```
68
57
 
69
- // Handle OS signals
70
- teardown.trapSignals();
58
+ #### `Teardown.add(fn): () => void`
71
59
 
72
- // Run all cleanup
73
- await teardown.run();
60
+ Registers a cleanup function. Returns an unregister function for removing it.
61
+
62
+ ```typescript
63
+ const unregister = teardown.add(() => cleanup());
64
+
65
+ // Later, remove it
66
+ unregister();
74
67
  ```
75
68
 
76
- #### Teardown Interface
69
+ #### `Teardown.run(): Promise<void>`
77
70
 
78
- | Method | Description |
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 |
71
+ Runs all cleanup functions in LIFO order. Safe to call multiple times—subsequent calls are no-ops.
72
+
73
+ ```typescript
74
+ await teardown.run(); // Runs last-in-first-out
75
+ ```
83
76
 
84
- ### Signal Trapping
77
+ #### `Teardown.trapSignals(): () => void`
85
78
 
86
- Signal handlers are attached via `trapSignals()` and call `run()` then `process.exit(0)`.
79
+ Wires SIGINT/SIGTERM to automatically call `run()` then `process.exit(0)`.
87
80
 
88
81
  ```typescript
89
82
  const untrap = teardown.trapSignals();
90
- // Later, if needed:
91
- untrap(); // Remove signal handlers
83
+
84
+ // Later, restore default behavior
85
+ untrap();
92
86
  ```
93
87
 
94
- ## Continuous Loop Execution
88
+ ### Behavior Notes
89
+
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.
92
+
93
+ ## Continuous Loop with `runContinuousLoop`
94
+
95
+ Runs a recurring task in a loop with built-in signal handling, dynamic delays, and configurable error policies.
95
96
 
96
- Interruptible loop with configurable cycle interval, error handling, and graceful shutdown.
97
+ ### Core Options
98
+
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) |
97
107
 
98
- ### runContinuousLoop()
108
+ ### Return Values from `runCycle`
99
109
 
100
- Runs a function repeatedly with signal-aware sleep that can be interrupted.
110
+ You can control loop behavior by returning one of:
111
+
112
+ - **Delay value**: `number` (ms) or `"immediate"` to skip delay
113
+ - **Control object**: `{ stop?: boolean; nextDelayMs?: Delay }`
101
114
 
102
115
  ```typescript
103
116
  await runContinuousLoop({
104
117
  intervalSeconds: 10,
105
- runCycle: async (isShutdownRequested) => {
106
- if (isShutdownRequested()) {
107
- return { stop: true };
108
- }
109
- await doWork();
110
- // Return a delay override
111
- return 5000; // milliseconds
112
- },
113
- onCycleError: async (error, context) => {
114
- console.error(`Cycle ${context.cycleNumber} failed:`, error);
115
- return "continue"; // or "stop"
116
- },
117
- onShutdown: async () => {
118
- await cleanup();
118
+ runCycle: async () => {
119
+ // Skip delay after this cycle
120
+ return "immediate";
119
121
  }
120
122
  });
121
123
  ```
122
124
 
123
- #### Loop Lifecycle
124
-
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
125
+ ### Example: Backoff Loop
130
126
 
131
- ### Options
132
-
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 |
141
-
142
- ### Cycle Return Values
127
+ ```typescript
128
+ import { runContinuousLoop } from "@hardlydifficult/daemon";
143
129
 
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
130
+ type Result = { backoffMs: number } | { success: true };
149
131
 
150
- If the cycle returns a domain-specific result, use `getNextDelayMs` to derive the next delay.
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
+ });
144
+ ```
151
145
 
152
146
  ### Error Handling
153
147
 
154
- By default:
155
- - Errors are logged to `console.error`
156
- - Loop continues to next cycle
157
-
158
- Custom error handling via `onCycleError` can override behavior:
148
+ By default, cycle errors are logged to console and the loop continues. Custom error handling:
159
149
 
160
150
  ```typescript
161
- onCycleError: async (error, context) => {
162
- if (error instanceof FatalError) {
163
- return "stop"; // Terminate loop
151
+ await runContinuousLoop({
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"
164
157
  }
165
- return "continue"; // Proceed
166
- }
158
+ });
167
159
  ```
168
160
 
169
- ### Context Types
170
-
171
- #### ContinuousLoopCycleContext
172
-
173
- | Field | Type | Description |
174
- |-------|------|-------------|
175
- | `cycleNumber` | `number` | 1-based cycle number |
176
- | `isShutdownRequested` | `() => boolean` | Check shutdown status |
177
-
178
- #### ContinuousLoopErrorContext
179
-
180
- Identical to `ContinuousLoopCycleContext`; passed to `onCycleError`.
181
-
182
- #### ContinuousLoopLogger
161
+ ### Shutdown Signals
183
162
 
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 |
163
+ The loop responds to `SIGINT` and `SIGTERM` by stopping after the current cycle completes. Use `isShutdownRequested()` inside `runCycle` to abort long-running work:
188
164
 
189
- Default logger uses `console.warn`/`console.error`.
165
+ ```typescript
166
+ await runContinuousLoop({
167
+ intervalSeconds: 1,
168
+ runCycle: async (isShutdownRequested) => {
169
+ while (!isShutdownRequested()) {
170
+ await processChunk();
171
+ }
172
+ return { stop: true };
173
+ }
174
+ });
175
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/daemon",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [