@gobing-ai/ts-infra 0.4.13 → 0.4.15

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.
@@ -2,6 +2,8 @@
2
2
  * Node.js scheduler adapter using a simple setInterval-based approach.
3
3
  * No external cron library dependency — cron expressions are parsed minimally.
4
4
  */
5
+
6
+ import { settleWithin } from '../internals/drain';
5
7
  import { getLogger } from '../logger';
6
8
  import {
7
9
  getSchedulerJobDuration,
@@ -44,15 +46,39 @@ interface ScheduledEntry {
44
46
  timer?: ReturnType<typeof setInterval>;
45
47
  }
46
48
 
49
+ /** Constructor options for {@link NodeSchedulerAdapter}. */
50
+ export interface NodeSchedulerAdapterConfig {
51
+ /**
52
+ * Upper bound (ms) on how long `stop()` waits for an in-flight tick to settle.
53
+ * A hung action is abandoned at this deadline so it cannot block shutdown.
54
+ * Defaults to 30000 (matching `DBQueueConsumer`).
55
+ */
56
+ readonly drainTimeoutMs?: number;
57
+ }
58
+
47
59
  /**
48
60
  * Scheduler adapter for Node.js using a setInterval-based approach.
49
61
  * No external cron library dependency — cron expressions are parsed minimally.
62
+ *
63
+ * `stop()` drains in-flight ticks, bounded by `drainTimeoutMs` (ADR-024): an
64
+ * action already running when `stop()` is called is awaited rather than torn
65
+ * down, which would leave a half-written row or a half-flushed batch.
50
66
  */
51
67
  export class NodeSchedulerAdapter implements SchedulerAdapter {
52
68
  private readonly entries: ScheduledEntry[] = [];
69
+ private readonly drainTimeoutMs: number;
53
70
  private running = false;
71
+ private readonly inflight = new Set<Promise<void>>();
54
72
 
55
- constructor() {}
73
+ constructor(config: NodeSchedulerAdapterConfig = {}) {
74
+ const { drainTimeoutMs } = config;
75
+ if (drainTimeoutMs !== undefined && (!Number.isFinite(drainTimeoutMs) || drainTimeoutMs < 0)) {
76
+ throw new RangeError(
77
+ `NodeSchedulerAdapter drainTimeoutMs must be a non-negative finite number; received ${drainTimeoutMs}`,
78
+ );
79
+ }
80
+ this.drainTimeoutMs = drainTimeoutMs ?? 30_000;
81
+ }
56
82
 
57
83
  register(cron: string, action: ScheduledAction): void {
58
84
  this.entries.push({ cron, action });
@@ -81,13 +107,32 @@ export class NodeSchedulerAdapter implements SchedulerAdapter {
81
107
  entry.timer = undefined;
82
108
  }
83
109
  }
110
+
111
+ // Drain in-flight ticks, bounded by a shared absolute deadline. clearInterval
112
+ // cancels future ticks but not one already executing; without this wait a tick
113
+ // mid-action when stop() is called would keep running after stop() resolves,
114
+ // tearing down a half-written row or half-flushed batch (ADR-024).
115
+ const deadline = Date.now() + this.drainTimeoutMs;
116
+ for (const p of [...this.inflight]) {
117
+ await settleWithin(p, deadline);
118
+ }
84
119
  }
85
120
 
86
121
  private startEntry(entry: ScheduledEntry): void {
87
122
  if (entry.timer) return;
88
123
 
89
124
  const interval = parseInterval(entry.cron);
90
- entry.timer = setInterval(this._onScheduledTick.bind(this, entry), interval);
125
+ // setInterval discards the async handler's returned promise, so capture it
126
+ // here for stop() to drain. _onScheduledTick never rejects (try/catch),
127
+ // so the cleanup runs on both paths with no unhandled-rejection risk.
128
+ entry.timer = setInterval(() => {
129
+ const tick = this._onScheduledTick(entry);
130
+ this.inflight.add(tick);
131
+ const cleanup = (): void => {
132
+ this.inflight.delete(tick);
133
+ };
134
+ tick.then(cleanup, cleanup);
135
+ }, interval);
91
136
  }
92
137
 
93
138
  private async _onScheduledTick(entry: ScheduledEntry): Promise<void> {
@@ -5,7 +5,13 @@
5
5
  /** Signature for scheduled action handlers. */
6
6
  export type ScheduledAction = () => Promise<void>;
7
7
 
8
- /** Abstract scheduler interface — implementations for Node (node-cron) and Cloudflare. */
8
+ /**
9
+ * Abstract scheduler interface — implementations for Node and Cloudflare.
10
+ *
11
+ * `stop()` cancels future ticks and drains any currently-executing tick,
12
+ * bounded by an implementation-configured timeout (see ADR-024). A stuck
13
+ * action is abandoned at the deadline rather than blocking shutdown forever.
14
+ */
9
15
  export interface SchedulerAdapter {
10
16
  register(cron: string, action: ScheduledAction): void;
11
17
  start(): Promise<void>;
@@ -1 +1 @@
1
- export { NodeSchedulerAdapter } from './scheduler/node';
1
+ export { NodeSchedulerAdapter, type NodeSchedulerAdapterConfig } from './scheduler/node';