@hardlydifficult/daemon 1.0.0 → 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 +63 -87
- 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,123 +1,99 @@
|
|
|
1
|
-
# @hardlydifficult/
|
|
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
|
|
|
7
10
|
```bash
|
|
8
|
-
npm install @hardlydifficult/
|
|
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 } from "@hardlydifficult/
|
|
20
|
+
import { createTeardown } from "@hardlydifficult/daemon";
|
|
15
21
|
|
|
16
22
|
const teardown = createTeardown();
|
|
17
23
|
teardown.add(() => server.stop());
|
|
18
|
-
teardown.add(() =>
|
|
24
|
+
teardown.add(async () => {
|
|
25
|
+
await db.close();
|
|
26
|
+
});
|
|
19
27
|
teardown.trapSignals();
|
|
20
28
|
|
|
21
|
-
// Any manual exit path:
|
|
22
29
|
await teardown.run();
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
Behavior:
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const teardown = createTeardown();
|
|
33
|
-
```
|
|
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
|
|
34
38
|
|
|
35
|
-
##
|
|
39
|
+
## Continuous loop execution
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
Use `runContinuousLoop()` to run work cycles with graceful shutdown, dynamic
|
|
42
|
+
delay control, and configurable error policy.
|
|
38
43
|
|
|
39
44
|
```typescript
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
import { runContinuousLoop } from "@hardlydifficult/daemon";
|
|
46
|
+
|
|
47
|
+
await runContinuousLoop({
|
|
48
|
+
intervalSeconds: 30,
|
|
49
|
+
async runCycle(isShutdownRequested) {
|
|
50
|
+
if (isShutdownRequested()) {
|
|
51
|
+
return { stop: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const didWork = await syncQueue();
|
|
55
|
+
if (!didWork) {
|
|
56
|
+
return 60_000; // ms
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return "immediate";
|
|
60
|
+
},
|
|
61
|
+
onCycleError(error, context) {
|
|
62
|
+
notifyOps(error, { cycleNumber: context.cycleNumber });
|
|
63
|
+
return "continue"; // or "stop"
|
|
64
|
+
},
|
|
57
65
|
});
|
|
58
|
-
unregister();
|
|
59
|
-
|
|
60
|
-
await teardown.run();
|
|
61
|
-
// Output:
|
|
62
|
-
// Closing database
|
|
63
|
-
// Closing server
|
|
64
66
|
```
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
### Cycle return contract
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
`runCycle()` can return:
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Output: cleanup
|
|
76
|
-
|
|
77
|
-
await teardown.run();
|
|
78
|
-
// No output (idempotent)
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Signal Trapping
|
|
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
|
|
82
77
|
|
|
83
|
-
|
|
78
|
+
### Optional delay resolver
|
|
84
79
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
teardown.add(() => server.stop());
|
|
80
|
+
If your cycle returns domain data, derive schedule policy with
|
|
81
|
+
`getNextDelayMs(result, context)`.
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
const untrap = teardown.trapSignals();
|
|
83
|
+
### Error handling
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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.
|
|
95
88
|
|
|
96
|
-
|
|
89
|
+
### Logger injection
|
|
97
90
|
|
|
98
|
-
|
|
91
|
+
By default, warnings and errors use `console.warn` and `console.error`. Pass
|
|
92
|
+
`logger` to integrate your own logging implementation:
|
|
99
93
|
|
|
100
94
|
```typescript
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
106
|
-
teardown.add(() => {
|
|
107
|
-
console.log("Second cleanup still runs");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await teardown.run();
|
|
111
|
-
// Output: Second cleanup still runs
|
|
95
|
+
const logger = {
|
|
96
|
+
warn: (message, context) => myLogger.warn(message, context),
|
|
97
|
+
error: (message, context) => myLogger.error(message, context),
|
|
98
|
+
};
|
|
112
99
|
```
|
|
113
|
-
|
|
114
|
-
## Behavior Reference
|
|
115
|
-
|
|
116
|
-
| Behavior | Details |
|
|
117
|
-
|----------|---------|
|
|
118
|
-
| **LIFO order** | Teardowns run in reverse registration order (last added runs first) |
|
|
119
|
-
| **Idempotent** | `run()` executes once; subsequent calls are no-ops |
|
|
120
|
-
| **Error resilient** | Each function is wrapped in try/catch; failures don't block remaining teardowns |
|
|
121
|
-
| **Safe unregister** | `add()` returns an unregister function; safe to call multiple times |
|
|
122
|
-
| **Post-run add** | `add()` after `run()` is a silent no-op |
|
|
123
|
-
| **Duplicate safe** | Same function added twice runs twice; unregister only removes its own registration |
|
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":"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"}
|