@hardlydifficult/daemon 1.0.1 → 1.0.3

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.
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # @hardlydifficult/daemon
2
2
 
3
- Idempotent resource teardown with signal trapping and LIFO cleanup ordering.
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
4
7
 
5
8
  ## Installation
6
9
 
@@ -13,102 +16,250 @@ npm install @hardlydifficult/daemon
13
16
  ```typescript
14
17
  import { createTeardown, runContinuousLoop } from "@hardlydifficult/daemon";
15
18
 
16
- // Register cleanup functions
19
+ // Graceful shutdown with LIFO cleanup
17
20
  const teardown = createTeardown();
18
- teardown.add(() => console.log("Closing server"));
19
- teardown.add(() => console.log("Closing database"));
21
+ teardown.add(() => console.log("Cleaning up server"));
22
+ teardown.add(() => console.log("Closing database connection"));
20
23
  teardown.trapSignals();
21
24
 
22
- // Run teardown when ready
23
- await teardown.run();
24
- // Logs:
25
- // Closing database
26
- // Closing server
25
+ // Continuous background task with signal-aware sleep
26
+ await runContinuousLoop({
27
+ intervalSeconds: 5,
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
35
+ },
36
+ onShutdown: async () => {
37
+ await teardown.run();
38
+ },
39
+ });
27
40
  ```
28
41
 
29
42
  ## Teardown Management
30
43
 
31
- Resource cleanup registry with idempotent execution and LIFO ordering.
32
-
33
- ### `createTeardown()`
34
-
35
- Creates a teardown registry for managing resource cleanup.
44
+ Use `createTeardown()` to register cleanup functions once and execute them from
45
+ every exit path.
36
46
 
37
47
  ```typescript
48
+ import { createTeardown } from "@hardlydifficult/daemon";
49
+
38
50
  const teardown = createTeardown();
51
+ teardown.add(() => server.stop());
52
+ teardown.add(async () => {
53
+ await db.close();
54
+ });
55
+ teardown.trapSignals();
56
+
57
+ await teardown.run();
39
58
  ```
40
59
 
41
- #### `.add(fn: () => void | Promise<void>): () => void`
60
+ ### Behavior
42
61
 
43
- Registers a teardown function. Returns an unregister function.
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
66
+
67
+ ### `createTeardown()`
68
+
69
+ Creates a teardown registry with idempotent resource cleanup.
44
70
 
45
71
  ```typescript
46
72
  const teardown = createTeardown();
47
- const unregister = teardown.add(async () => {
48
- await server.close();
49
- });
50
73
 
51
- // Unregister before teardown if needed
74
+ // Register cleanup functions
75
+ const unregister = teardown.add(() => server.stop());
76
+ teardown.add(async () => db.close());
77
+
78
+ // Unregister a specific cleanup
52
79
  unregister();
80
+
81
+ // Manually trigger shutdown
82
+ await teardown.run();
83
+
84
+ // Wire SIGTERM/SIGINT handlers
85
+ const untrap = teardown.trapSignals();
86
+ // Later...
87
+ untrap(); // Remove handlers
53
88
  ```
54
89
 
55
- #### `.run(): Promise<void>`
90
+ #### Teardown API
91
+
92
+ | 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 |
56
97
 
57
- Runs all teardown functions in LIFO order. Idempotent — subsequent calls are no-ops.
98
+ ### LIFO Execution
99
+
100
+ Teardown functions run in reverse registration order:
58
101
 
59
102
  ```typescript
60
103
  const teardown = createTeardown();
61
104
  teardown.add(() => console.log("First"));
62
105
  teardown.add(() => console.log("Second"));
106
+ teardown.add(() => console.log("Third"));
63
107
 
64
108
  await teardown.run();
65
- // Logs:
109
+ // Output:
110
+ // Third
66
111
  // Second
67
112
  // First
68
113
  ```
69
114
 
70
- #### `.trapSignals(): () => void`
115
+ ## Continuous Loop Execution
71
116
 
72
- Wires SIGTERM/SIGINT handlers to run teardown and exit. Returns an untrap function to remove handlers.
117
+ Use `runContinuousLoop()` to run work cycles with graceful shutdown, dynamic
118
+ delay control, and configurable error policy.
73
119
 
74
120
  ```typescript
75
- const teardown = createTeardown();
76
- const untrap = teardown.trapSignals();
121
+ import { runContinuousLoop } from "@hardlydifficult/daemon";
122
+
123
+ await runContinuousLoop({
124
+ intervalSeconds: 30,
125
+ async runCycle(isShutdownRequested) {
126
+ if (isShutdownRequested()) {
127
+ return { stop: true };
128
+ }
77
129
 
78
- // Later, to stop trapping signals
79
- untrap();
130
+ const didWork = await syncQueue();
131
+ if (!didWork) {
132
+ return 60_000; // ms
133
+ }
134
+
135
+ return "immediate";
136
+ },
137
+ onCycleError(error, context) {
138
+ notifyOps(error, { cycleNumber: context.cycleNumber });
139
+ return "continue"; // or "stop"
140
+ },
141
+ });
80
142
  ```
81
143
 
82
- ## Continuous Loop Execution
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
83
155
 
84
- Runs cyclic tasks with graceful shutdown support.
156
+ If your cycle returns domain data, derive schedule policy with
157
+ `getNextDelayMs(result, context)`.
85
158
 
86
- ### `runContinuousLoop(options: ContinuousLoopOptions): Promise<void>`
159
+ ### Error Handling
87
160
 
88
- Executes a cycle function repeatedly with interruptible sleep and signal handling.
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.
164
+
165
+ ### Logger Injection
166
+
167
+ By default, warnings and errors use `console.warn` and `console.error`. Pass
168
+ `logger` to integrate your own logging implementation:
89
169
 
90
170
  ```typescript
91
- import { runContinuousLoop } from "@hardlydifficult/daemon";
171
+ const logger = {
172
+ warn: (message, context) => myLogger.warn(message, context),
173
+ error: (message, context) => myLogger.error(message, context),
174
+ };
175
+ ```
176
+
177
+ ### `runContinuousLoop()` Options
92
178
 
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`) |
187
+
188
+ ### Return Values
189
+
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
194
+
195
+ ```typescript
93
196
  await runContinuousLoop({
94
197
  intervalSeconds: 5,
95
- runCycle: async (isShutdownRequested) => {
96
- if (isShutdownRequested()) {
97
- return;
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
98
205
  }
99
- // Perform work here
100
- console.log("Running cycle...");
206
+ return 2000; // Wait 2 seconds
207
+ },
208
+ });
209
+ ```
210
+
211
+ #### Delay Directives
212
+
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 |
219
+
220
+ ### Error Handling Example
221
+
222
+ Cycles errors are caught and handled according to the error policy.
223
+
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),
101
239
  },
102
- onShutdown: async () => {
103
- console.log("Shutdown complete");
104
- }
105
240
  });
106
241
  ```
107
242
 
108
- #### Options
243
+ If no `onCycleError` is provided, errors are logged and the loop continues.
244
+
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
+ });
109
261
 
110
- | Name | Type | Description |
111
- |-------------------|---------------------------------|------------------------------------------------|
112
- | `intervalSeconds` | `number` | Interval between cycles in seconds |
113
- | `runCycle` | `(isShutdownRequested: () => boolean) => Promise<unknown>` | Callback to run each cycle (shutdown check provided) |
114
- | `onShutdown?` | `() => Promise<void>` | Optional cleanup callback after shutdown |
262
+ // Later...
263
+ process.kill(process.pid, "SIGTERM"); // Triggers graceful shutdown
264
+ await loopPromise; // Resolves after onShutdown completes
265
+ ```
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { createTeardown, type Teardown } from "./createTeardown.js";
2
- export { runContinuousLoop, type ContinuousLoopOptions, } from "./runContinuousLoop.js";
2
+ export { runContinuousLoop, type ContinuousLoopCycleContext, type ContinuousLoopCycleControl, type ContinuousLoopDelay, type ContinuousLoopErrorAction, type ContinuousLoopErrorHandler, type ContinuousLoopErrorContext, type ContinuousLoopLogger, type ContinuousLoopOptions, type ContinuousLoopRunCycleResult, } from "./runContinuousLoop.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EACL,iBAAiB,EACjB,KAAK,qBAAqB,GAC3B,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EACL,iBAAiB,EACjB,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,EAC/B,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,GAClC,MAAM,wBAAwB,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,yDAAoE;AAA3D,mHAAA,cAAc,OAAA;AACvB,+DAGgC;AAF9B,yHAAA,iBAAiB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,yDAAoE;AAA3D,mHAAA,cAAc,OAAA;AACvB,+DAWgC;AAV9B,yHAAA,iBAAiB,OAAA"}
@@ -4,19 +4,62 @@
4
4
  * This module provides interruptible sleep and continuous loop functionality with proper signal handling for
5
5
  * SIGINT/SIGTERM.
6
6
  */
7
+ /** Delay directive returned by runCycle/getNextDelayMs. */
8
+ export type ContinuousLoopDelay = number | "immediate";
9
+ /** Context provided to each cycle and delay resolver. */
10
+ export interface ContinuousLoopCycleContext {
11
+ /** 1-based cycle number for the current loop iteration */
12
+ cycleNumber: number;
13
+ /** Function to check if shutdown has been requested */
14
+ isShutdownRequested: () => boolean;
15
+ }
16
+ /** Context provided to cycle error handling callbacks. */
17
+ export type ContinuousLoopErrorContext = ContinuousLoopCycleContext;
18
+ /** Action returned by onCycleError to control loop behavior. */
19
+ export type ContinuousLoopErrorAction = "continue" | "stop";
20
+ type ContinuousLoopErrorDecision = ContinuousLoopErrorAction | undefined;
21
+ /** Callback used to process cycle errors and decide loop policy. */
22
+ export type ContinuousLoopErrorHandler = (error: unknown, context: ContinuousLoopErrorContext) => ContinuousLoopErrorDecision | Promise<ContinuousLoopErrorDecision>;
23
+ /** Optional control directives that can be returned from runCycle. */
24
+ export interface ContinuousLoopCycleControl {
25
+ /** Stop the loop after this cycle completes. */
26
+ stop?: boolean;
27
+ /** Override delay before the next cycle. */
28
+ nextDelayMs?: ContinuousLoopDelay;
29
+ }
30
+ /** Minimal logger interface compatible with @hardlydifficult/logger. */
31
+ export interface ContinuousLoopLogger {
32
+ warn(message: string, context?: Readonly<Record<string, unknown>>): void;
33
+ error(message: string, context?: Readonly<Record<string, unknown>>): void;
34
+ }
35
+ /** Supported return shape from runCycle. */
36
+ export type ContinuousLoopRunCycleResult<TResult = unknown> = TResult | ContinuousLoopDelay | ContinuousLoopCycleControl;
7
37
  /** Options for running a continuous loop */
8
- export interface ContinuousLoopOptions {
38
+ export interface ContinuousLoopOptions<TResult = unknown> {
9
39
  /** Interval between cycles in seconds */
10
40
  intervalSeconds: number;
11
41
  /**
12
42
  * Callback to run on each cycle.
13
43
  *
14
44
  * @param isShutdownRequested - Function to check if shutdown has been requested during the cycle
15
- * @returns Promise that resolves when the cycle is complete (return value is ignored)
45
+ * @returns Promise resolving to cycle result and optional control directives
46
+ */
47
+ runCycle: (isShutdownRequested: () => boolean) => Promise<ContinuousLoopRunCycleResult<TResult>>;
48
+ /**
49
+ * Optional hook to derive the next delay from a runCycle result.
50
+ * Used only when runCycle does not directly return a delay directive.
51
+ */
52
+ getNextDelayMs?: (result: ContinuousLoopRunCycleResult<TResult>, context: ContinuousLoopCycleContext) => ContinuousLoopDelay | undefined;
53
+ /**
54
+ * Optional error callback for cycle failures.
55
+ *
56
+ * Return "stop" to end the loop, otherwise it will continue.
16
57
  */
17
- runCycle: (isShutdownRequested: () => boolean) => Promise<unknown>;
58
+ onCycleError?: ContinuousLoopErrorHandler;
18
59
  /** Optional callback for cleanup on shutdown */
19
- onShutdown?: () => Promise<void>;
60
+ onShutdown?: () => void | Promise<void>;
61
+ /** Optional logger (defaults to console warn/error) */
62
+ logger?: ContinuousLoopLogger;
20
63
  }
21
64
  /**
22
65
  * Run a function in a continuous loop with graceful shutdown support.
@@ -25,10 +68,13 @@ export interface ContinuousLoopOptions {
25
68
  *
26
69
  * - Interruptible sleep that responds immediately to SIGINT/SIGTERM
27
70
  * - Proper signal handler cleanup to prevent listener accumulation
28
- * - Continues to next cycle even if current cycle fails
71
+ * - Per-cycle delay control via return value or getNextDelayMs
72
+ * - Graceful stop signaling from runCycle ({ stop: true })
73
+ * - Configurable error policy via onCycleError
29
74
  * - Passes shutdown check callback to runCycle for in-cycle interruption
30
75
  *
31
76
  * @param options - Configuration for the continuous loop
32
77
  */
33
- export declare function runContinuousLoop(options: ContinuousLoopOptions): Promise<void>;
78
+ export declare function runContinuousLoop<TResult = unknown>(options: ContinuousLoopOptions<TResult>): Promise<void>;
79
+ export {};
34
80
  //# sourceMappingURL=runContinuousLoop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runContinuousLoop.d.ts","sourceRoot":"","sources":["../src/runContinuousLoop.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,4CAA4C;AAC5C,MAAM,WAAW,qBAAqB;IACpC,yCAAyC;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,mBAAmB,EAAE,MAAM,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAqCD;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAuDf"}
1
+ {"version":3,"file":"runContinuousLoop.d.ts","sourceRoot":"","sources":["../src/runContinuousLoop.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,2DAA2D;AAC3D,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,WAAW,CAAC;AAEvD,yDAAyD;AACzD,MAAM,WAAW,0BAA0B;IACzC,0DAA0D;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,mBAAmB,EAAE,MAAM,OAAO,CAAC;CACpC;AAED,0DAA0D;AAC1D,MAAM,MAAM,0BAA0B,GAAG,0BAA0B,CAAC;AAEpE,gEAAgE;AAChE,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,MAAM,CAAC;AAE5D,KAAK,2BAA2B,GAAG,yBAAyB,GAAG,SAAS,CAAC;AAEzE,oEAAoE;AACpE,MAAM,MAAM,0BAA0B,GAAG,CACvC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,0BAA0B,KAChC,2BAA2B,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC;AAExE,sEAAsE;AACtE,MAAM,WAAW,0BAA0B;IACzC,gDAAgD;IAChD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4CAA4C;IAC5C,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC;AAED,wEAAwE;AACxE,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IACzE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAC3E;AAED,4CAA4C;AAC5C,MAAM,MAAM,4BAA4B,CAAC,OAAO,GAAG,OAAO,IACtD,OAAO,GACP,mBAAmB,GACnB,0BAA0B,CAAC;AAE/B,4CAA4C;AAC5C,MAAM,WAAW,qBAAqB,CAAC,OAAO,GAAG,OAAO;IACtD,yCAAyC;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,EAAE,CACR,mBAAmB,EAAE,MAAM,OAAO,KAC/B,OAAO,CAAC,4BAA4B,CAAC,OAAO,CAAC,CAAC,CAAC;IACpD;;;OAGG;IACH,cAAc,CAAC,EAAE,CACf,MAAM,EAAE,4BAA4B,CAAC,OAAO,CAAC,EAC7C,OAAO,EAAE,0BAA0B,KAChC,mBAAmB,GAAG,SAAS,CAAC;IACrC;;;;OAIG;IACH,YAAY,CAAC,EAAE,0BAA0B,CAAC;IAC1C,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,uDAAuD;IACvD,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AA2HD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,GAAG,OAAO,EACvD,OAAO,EAAE,qBAAqB,CAAC,OAAO,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAwGf"}
@@ -7,6 +7,22 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.runContinuousLoop = runContinuousLoop;
10
+ const defaultLogger = {
11
+ warn(message, context) {
12
+ if (context === undefined) {
13
+ console.warn(message);
14
+ return;
15
+ }
16
+ console.warn(message, context);
17
+ },
18
+ error(message, context) {
19
+ if (context === undefined) {
20
+ console.error(message);
21
+ return;
22
+ }
23
+ console.error(message, context);
24
+ },
25
+ };
10
26
  /**
11
27
  * Creates an interruptible sleep that can be woken early by calling the returned cancel function.
12
28
  *
@@ -35,6 +51,57 @@ function createInterruptibleSleep(durationMs) {
35
51
  };
36
52
  return { promise, cancel };
37
53
  }
54
+ function normalizeDelayMs(delayMs, source) {
55
+ if (delayMs === "immediate") {
56
+ return delayMs;
57
+ }
58
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
59
+ if (source === "intervalSeconds") {
60
+ throw new Error("intervalSeconds must be a non-negative finite number");
61
+ }
62
+ throw new Error(`${source} must return a non-negative finite number or "immediate"`);
63
+ }
64
+ return delayMs;
65
+ }
66
+ function getControlFromCycleResult(result) {
67
+ if (typeof result === "number" || result === "immediate") {
68
+ return { nextDelayMs: result };
69
+ }
70
+ if (typeof result !== "object" || result === null) {
71
+ return {};
72
+ }
73
+ const value = result;
74
+ const { nextDelayMs } = value;
75
+ return {
76
+ stop: value.stop === true,
77
+ nextDelayMs: typeof nextDelayMs === "number" || nextDelayMs === "immediate"
78
+ ? nextDelayMs
79
+ : undefined,
80
+ };
81
+ }
82
+ async function handleCycleError(error, context, onCycleError, logger) {
83
+ if (onCycleError !== undefined) {
84
+ try {
85
+ const action = await onCycleError(error, context);
86
+ return action === "stop" ? "stop" : "continue";
87
+ }
88
+ catch (handlerError) {
89
+ logger.error("onCycleError handler failed", {
90
+ cycleNumber: context.cycleNumber,
91
+ cycleError: error instanceof Error ? error.message : String(error),
92
+ handlerError: handlerError instanceof Error
93
+ ? handlerError.message
94
+ : String(handlerError),
95
+ });
96
+ return "continue";
97
+ }
98
+ }
99
+ logger.error("Cycle error", {
100
+ cycleNumber: context.cycleNumber,
101
+ error: error instanceof Error ? error.message : String(error),
102
+ });
103
+ return "continue";
104
+ }
38
105
  /**
39
106
  * Run a function in a continuous loop with graceful shutdown support.
40
107
  *
@@ -42,17 +109,21 @@ function createInterruptibleSleep(durationMs) {
42
109
  *
43
110
  * - Interruptible sleep that responds immediately to SIGINT/SIGTERM
44
111
  * - Proper signal handler cleanup to prevent listener accumulation
45
- * - Continues to next cycle even if current cycle fails
112
+ * - Per-cycle delay control via return value or getNextDelayMs
113
+ * - Graceful stop signaling from runCycle ({ stop: true })
114
+ * - Configurable error policy via onCycleError
46
115
  * - Passes shutdown check callback to runCycle for in-cycle interruption
47
116
  *
48
117
  * @param options - Configuration for the continuous loop
49
118
  */
50
119
  async function runContinuousLoop(options) {
51
- const { intervalSeconds, runCycle, onShutdown } = options;
120
+ const { intervalSeconds, runCycle, getNextDelayMs, onCycleError, onShutdown, logger = defaultLogger, } = options;
121
+ const defaultDelayMs = normalizeDelayMs(intervalSeconds * 1000, "intervalSeconds");
52
122
  let shutdownRequested = false;
53
123
  let cancelCurrentSleep = null;
124
+ let cycleNumber = 0;
54
125
  const handleShutdown = (signal) => {
55
- console.warn(`Received ${signal}, shutting down gracefully...`);
126
+ logger.warn(`Received ${signal}, shutting down gracefully...`);
56
127
  shutdownRequested = true;
57
128
  if (cancelCurrentSleep !== null) {
58
129
  cancelCurrentSleep();
@@ -71,17 +142,46 @@ async function runContinuousLoop(options) {
71
142
  const shouldContinue = () => !shutdownRequested;
72
143
  try {
73
144
  while (shouldContinue()) {
145
+ cycleNumber += 1;
146
+ const cycleContext = {
147
+ cycleNumber,
148
+ isShutdownRequested,
149
+ };
150
+ let nextDelayMs = defaultDelayMs;
74
151
  try {
75
- await runCycle(isShutdownRequested);
152
+ const cycleResult = await runCycle(isShutdownRequested);
153
+ if (!shouldContinue()) {
154
+ continue;
155
+ }
156
+ const cycleControl = getControlFromCycleResult(cycleResult);
157
+ if (cycleControl.stop === true) {
158
+ shutdownRequested = true;
159
+ continue;
160
+ }
161
+ if (cycleControl.nextDelayMs !== undefined) {
162
+ nextDelayMs = normalizeDelayMs(cycleControl.nextDelayMs, "runCycle");
163
+ }
164
+ else if (getNextDelayMs !== undefined) {
165
+ const derivedDelay = getNextDelayMs(cycleResult, cycleContext);
166
+ if (derivedDelay !== undefined) {
167
+ nextDelayMs = normalizeDelayMs(derivedDelay, "getNextDelayMs");
168
+ }
169
+ }
76
170
  }
77
171
  catch (error) {
78
- const errorMessage = error instanceof Error ? error.message : String(error);
79
- console.error(`Cycle error: ${errorMessage}`);
172
+ const action = await handleCycleError(error, cycleContext, onCycleError, logger);
173
+ if (action === "stop") {
174
+ shutdownRequested = true;
175
+ continue;
176
+ }
80
177
  }
81
178
  if (!shouldContinue()) {
82
179
  break;
83
180
  }
84
- const sleep = createInterruptibleSleep(intervalSeconds * 1000);
181
+ if (nextDelayMs === "immediate") {
182
+ continue;
183
+ }
184
+ const sleep = createInterruptibleSleep(nextDelayMs);
85
185
  cancelCurrentSleep = sleep.cancel;
86
186
  await sleep.promise;
87
187
  cancelCurrentSleep = null;
@@ -1 +1 @@
1
- {"version":3,"file":"runContinuousLoop.js","sourceRoot":"","sources":["../src/runContinuousLoop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runContinuousLoop.js","sourceRoot":"","sources":["../src/runContinuousLoop.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/daemon",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [