@hardlydifficult/daemon 1.0.1 → 1.0.2
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 +59 -74
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runContinuousLoop.d.ts +52 -6
- package/dist/runContinuousLoop.d.ts.map +1 -1
- package/dist/runContinuousLoop.js +107 -7
- package/dist/runContinuousLoop.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# @hardlydifficult/daemon
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
|
@@ -8,107 +11,89 @@ Idempotent resource teardown with signal trapping and LIFO cleanup ordering.
|
|
|
8
11
|
npm install @hardlydifficult/daemon
|
|
9
12
|
```
|
|
10
13
|
|
|
11
|
-
##
|
|
14
|
+
## Teardown management
|
|
15
|
+
|
|
16
|
+
Use `createTeardown()` to register cleanup functions once and execute them from
|
|
17
|
+
every exit path.
|
|
12
18
|
|
|
13
19
|
```typescript
|
|
14
|
-
import { createTeardown
|
|
20
|
+
import { createTeardown } from "@hardlydifficult/daemon";
|
|
15
21
|
|
|
16
|
-
// Register cleanup functions
|
|
17
22
|
const teardown = createTeardown();
|
|
18
|
-
teardown.add(() =>
|
|
19
|
-
teardown.add(() =>
|
|
23
|
+
teardown.add(() => server.stop());
|
|
24
|
+
teardown.add(async () => {
|
|
25
|
+
await db.close();
|
|
26
|
+
});
|
|
20
27
|
teardown.trapSignals();
|
|
21
28
|
|
|
22
|
-
// Run teardown when ready
|
|
23
29
|
await teardown.run();
|
|
24
|
-
// Logs:
|
|
25
|
-
// Closing database
|
|
26
|
-
// Closing server
|
|
27
30
|
```
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
Behavior:
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
- LIFO execution (last added, first run)
|
|
35
|
+
- Idempotent `run()` (safe to call multiple times)
|
|
36
|
+
- Per-function error isolation (one failing teardown does not block others)
|
|
37
|
+
- `add()` returns an unregister function
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
## Continuous loop execution
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
Use `runContinuousLoop()` to run work cycles with graceful shutdown, dynamic
|
|
42
|
+
delay control, and configurable error policy.
|
|
36
43
|
|
|
37
44
|
```typescript
|
|
38
|
-
|
|
39
|
-
```
|
|
45
|
+
import { runContinuousLoop } from "@hardlydifficult/daemon";
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
await runContinuousLoop({
|
|
48
|
+
intervalSeconds: 30,
|
|
49
|
+
async runCycle(isShutdownRequested) {
|
|
50
|
+
if (isShutdownRequested()) {
|
|
51
|
+
return { stop: true };
|
|
52
|
+
}
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
const didWork = await syncQueue();
|
|
55
|
+
if (!didWork) {
|
|
56
|
+
return 60_000; // ms
|
|
57
|
+
}
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
return "immediate";
|
|
60
|
+
},
|
|
61
|
+
onCycleError(error, context) {
|
|
62
|
+
notifyOps(error, { cycleNumber: context.cycleNumber });
|
|
63
|
+
return "continue"; // or "stop"
|
|
64
|
+
},
|
|
49
65
|
});
|
|
50
|
-
|
|
51
|
-
// Unregister before teardown if needed
|
|
52
|
-
unregister();
|
|
53
66
|
```
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Runs all teardown functions in LIFO order. Idempotent — subsequent calls are no-ops.
|
|
58
|
-
|
|
59
|
-
```typescript
|
|
60
|
-
const teardown = createTeardown();
|
|
61
|
-
teardown.add(() => console.log("First"));
|
|
62
|
-
teardown.add(() => console.log("Second"));
|
|
68
|
+
### Cycle return contract
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
// Logs:
|
|
66
|
-
// Second
|
|
67
|
-
// First
|
|
68
|
-
```
|
|
70
|
+
`runCycle()` can return:
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
- any value/`undefined`: use default `intervalSeconds`
|
|
73
|
+
- `number`: use that delay in milliseconds
|
|
74
|
+
- `"immediate"`: run the next cycle without sleeping
|
|
75
|
+
- `{ stop: true }`: stop gracefully after current cycle
|
|
76
|
+
- `{ nextDelayMs: number | "immediate", stop?: true }`: explicit control object
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
### Optional delay resolver
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const untrap = teardown.trapSignals();
|
|
77
|
-
|
|
78
|
-
// Later, to stop trapping signals
|
|
79
|
-
untrap();
|
|
80
|
-
```
|
|
80
|
+
If your cycle returns domain data, derive schedule policy with
|
|
81
|
+
`getNextDelayMs(result, context)`.
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
### Error handling
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
Use `onCycleError(error, context)` to route to Slack/Sentry and decide whether
|
|
86
|
+
to `"continue"` or `"stop"`. Without this hook, cycle errors are logged and the
|
|
87
|
+
loop continues.
|
|
85
88
|
|
|
86
|
-
###
|
|
89
|
+
### Logger injection
|
|
87
90
|
|
|
88
|
-
|
|
91
|
+
By default, warnings and errors use `console.warn` and `console.error`. Pass
|
|
92
|
+
`logger` to integrate your own logging implementation:
|
|
89
93
|
|
|
90
94
|
```typescript
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
runCycle: async (isShutdownRequested) => {
|
|
96
|
-
if (isShutdownRequested()) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// Perform work here
|
|
100
|
-
console.log("Running cycle...");
|
|
101
|
-
},
|
|
102
|
-
onShutdown: async () => {
|
|
103
|
-
console.log("Shutdown complete");
|
|
104
|
-
}
|
|
105
|
-
});
|
|
95
|
+
const logger = {
|
|
96
|
+
warn: (message, context) => myLogger.warn(message, context),
|
|
97
|
+
error: (message, context) => myLogger.error(message, context),
|
|
98
|
+
};
|
|
106
99
|
```
|
|
107
|
-
|
|
108
|
-
#### Options
|
|
109
|
-
|
|
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 |
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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,+
|
|
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
|
|
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
|
-
|
|
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
|
-
* -
|
|
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;
|
|
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
|
-
* -
|
|
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
|
-
|
|
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
|
|
79
|
-
|
|
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
|
-
|
|
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":";AAAA;;;;;GAKG;;
|
|
1
|
+
{"version":3,"file":"runContinuousLoop.js","sourceRoot":"","sources":["../src/runContinuousLoop.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAuNH,8CA0GC;AAjPD,MAAM,aAAa,GAAyB;IAC1C,IAAI,CAAC,OAAO,EAAE,OAAO;QACnB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtB,OAAO;QACT,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IACD,KAAK,CAAC,OAAO,EAAE,OAAO;QACpB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACvB,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAClC,CAAC;CACF,CAAC;AAEF;;;;GAIG;AACH,SAAS,wBAAwB,CAAC,UAAkB;IAIlD,IAAI,OAAO,GAAwB,IAAI,CAAC;IACxC,IAAI,OAAO,GAAyC,IAAI,CAAC;IAEzD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE;QACtC,OAAO,GAAG,CAAC,CAAC;QACZ,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YACxB,OAAO,GAAG,IAAI,CAAC;YACf,OAAO,GAAG,IAAI,CAAC;YACf,CAAC,EAAE,CAAC;QACN,CAAC,EAAE,UAAU,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,GAAS,EAAE;QACxB,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,OAAO,EAAE,CAAC;YACV,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC;AAED,SAAS,gBAAgB,CACvB,OAA4B,EAC5B,MAAyD;IAEzD,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;QAC5B,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAC7C,IAAI,MAAM,KAAK,iBAAiB,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAC1E,CAAC;QACD,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,0DAA0D,CACpE,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,yBAAyB,CAChC,MAAoC;IAEpC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;QACzD,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAClD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,KAAK,GAAG,MAAiC,CAAC;IAChD,MAAM,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC;IAC9B,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,IAAI;QACzB,WAAW,EACT,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,KAAK,WAAW;YAC5D,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,SAAS;KAChB,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,KAAc,EACd,OAAmC,EACnC,YAAoD,EACpD,MAA4B;IAE5B,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAClD,OAAO,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;QACjD,CAAC;QAAC,OAAO,YAAY,EAAE,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE;gBAC1C,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,UAAU,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;gBAClE,YAAY,EACV,YAAY,YAAY,KAAK;oBAC3B,CAAC,CAAC,YAAY,CAAC,OAAO;oBACtB,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;aAC3B,CAAC,CAAC;YACH,OAAO,UAAU,CAAC;QACpB,CAAC;IACH,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE;QAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;KAC9D,CAAC,CAAC;IACH,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACI,KAAK,UAAU,iBAAiB,CACrC,OAAuC;IAEvC,MAAM,EACJ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,UAAU,EACV,MAAM,GAAG,aAAa,GACvB,GAAG,OAAO,CAAC;IAEZ,MAAM,cAAc,GAAG,gBAAgB,CACrC,eAAe,GAAG,IAAI,EACtB,iBAAiB,CAClB,CAAC;IAEF,IAAI,iBAAiB,GAAG,KAAK,CAAC;IAC9B,IAAI,kBAAkB,GAAwB,IAAI,CAAC;IACnD,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,MAAM,cAAc,GAAG,CAAC,MAAc,EAAQ,EAAE;QAC9C,MAAM,CAAC,IAAI,CAAC,YAAY,MAAM,+BAA+B,CAAC,CAAC;QAC/D,iBAAiB,GAAG,IAAI,CAAC;QACzB,IAAI,kBAAkB,KAAK,IAAI,EAAE,CAAC;YAChC,kBAAkB,EAAE,CAAC;YACrB,kBAAkB,GAAG,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,GAAS,EAAE;QAC/B,cAAc,CAAC,QAAQ,CAAC,CAAC;IAC3B,CAAC,CAAC;IACF,MAAM,cAAc,GAAG,GAAS,EAAE;QAChC,cAAc,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;IAEtC,MAAM,mBAAmB,GAAG,GAAY,EAAE,CAAC,iBAAiB,CAAC;IAE7D,MAAM,cAAc,GAAG,GAAY,EAAE,CAAC,CAAC,iBAAiB,CAAC;IAEzD,IAAI,CAAC;QACH,OAAO,cAAc,EAAE,EAAE,CAAC;YACxB,WAAW,IAAI,CAAC,CAAC;YACjB,MAAM,YAAY,GAA+B;gBAC/C,WAAW;gBACX,mBAAmB;aACpB,CAAC;YACF,IAAI,WAAW,GAAwB,cAAc,CAAC;YAEtD,IAAI,CAAC;gBACH,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,mBAAmB,CAAC,CAAC;gBACxD,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;oBACtB,SAAS;gBACX,CAAC;gBAED,MAAM,YAAY,GAAG,yBAAyB,CAAC,WAAW,CAAC,CAAC;gBAC5D,IAAI,YAAY,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;oBAC/B,iBAAiB,GAAG,IAAI,CAAC;oBACzB,SAAS;gBACX,CAAC;gBAED,IAAI,YAAY,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;oBAC3C,WAAW,GAAG,gBAAgB,CAAC,YAAY,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;gBACvE,CAAC;qBAAM,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;oBACxC,MAAM,YAAY,GAAG,cAAc,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;oBAC/D,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;wBAC/B,WAAW,GAAG,gBAAgB,CAAC,YAAY,EAAE,gBAAgB,CAAC,CAAC;oBACjE,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,MAAM,GAAG,MAAM,gBAAgB,CACnC,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,MAAM,CACP,CAAC;gBACF,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;oBACtB,iBAAiB,GAAG,IAAI,CAAC;oBACzB,SAAS;gBACX,CAAC;YACH,CAAC;YAED,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;gBACtB,MAAM;YACR,CAAC;YAED,IAAI,WAAW,KAAK,WAAW,EAAE,CAAC;gBAChC,SAAS;YACX,CAAC;YAED,MAAM,KAAK,GAAG,wBAAwB,CAAC,WAAW,CAAC,CAAC;YACpD,kBAAkB,GAAG,KAAK,CAAC,MAAM,CAAC;YAClC,MAAM,KAAK,CAAC,OAAO,CAAC;YACpB,kBAAkB,GAAG,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;QACvC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,UAAU,EAAE,CAAC;QACrB,CAAC;IACH,CAAC;AACH,CAAC"}
|