@emmvish/stable-infra 1.0.0

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 (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2520 -0
  3. package/dist/constants/index.d.ts +15 -0
  4. package/dist/constants/index.d.ts.map +1 -0
  5. package/dist/constants/index.js +136 -0
  6. package/dist/constants/index.js.map +1 -0
  7. package/dist/core/index.d.ts +8 -0
  8. package/dist/core/index.d.ts.map +1 -0
  9. package/dist/core/index.js +8 -0
  10. package/dist/core/index.js.map +1 -0
  11. package/dist/core/stable-api-gateway.d.ts +4 -0
  12. package/dist/core/stable-api-gateway.d.ts.map +1 -0
  13. package/dist/core/stable-api-gateway.js +146 -0
  14. package/dist/core/stable-api-gateway.js.map +1 -0
  15. package/dist/core/stable-buffer.d.ts +23 -0
  16. package/dist/core/stable-buffer.d.ts.map +1 -0
  17. package/dist/core/stable-buffer.js +127 -0
  18. package/dist/core/stable-buffer.js.map +1 -0
  19. package/dist/core/stable-function.d.ts +11 -0
  20. package/dist/core/stable-function.d.ts.map +1 -0
  21. package/dist/core/stable-function.js +355 -0
  22. package/dist/core/stable-function.js.map +1 -0
  23. package/dist/core/stable-request.d.ts +3 -0
  24. package/dist/core/stable-request.d.ts.map +1 -0
  25. package/dist/core/stable-request.js +313 -0
  26. package/dist/core/stable-request.js.map +1 -0
  27. package/dist/core/stable-scheduler.d.ts +71 -0
  28. package/dist/core/stable-scheduler.d.ts.map +1 -0
  29. package/dist/core/stable-scheduler.js +782 -0
  30. package/dist/core/stable-scheduler.js.map +1 -0
  31. package/dist/core/stable-workflow-graph.d.ts +3 -0
  32. package/dist/core/stable-workflow-graph.d.ts.map +1 -0
  33. package/dist/core/stable-workflow-graph.js +5 -0
  34. package/dist/core/stable-workflow-graph.js.map +1 -0
  35. package/dist/core/stable-workflow.d.ts +3 -0
  36. package/dist/core/stable-workflow.d.ts.map +1 -0
  37. package/dist/core/stable-workflow.js +374 -0
  38. package/dist/core/stable-workflow.js.map +1 -0
  39. package/dist/enums/index.d.ts +84 -0
  40. package/dist/enums/index.d.ts.map +1 -0
  41. package/dist/enums/index.js +99 -0
  42. package/dist/enums/index.js.map +1 -0
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +4 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/stable-runner/index.d.ts +2 -0
  48. package/dist/stable-runner/index.d.ts.map +1 -0
  49. package/dist/stable-runner/index.js +324 -0
  50. package/dist/stable-runner/index.js.map +1 -0
  51. package/dist/types/index.d.ts +1482 -0
  52. package/dist/types/index.d.ts.map +1 -0
  53. package/dist/types/index.js +2 -0
  54. package/dist/types/index.js.map +1 -0
  55. package/dist/utilities/buffer-utils.d.ts +4 -0
  56. package/dist/utilities/buffer-utils.d.ts.map +1 -0
  57. package/dist/utilities/buffer-utils.js +13 -0
  58. package/dist/utilities/buffer-utils.js.map +1 -0
  59. package/dist/utilities/cache-manager.d.ts +54 -0
  60. package/dist/utilities/cache-manager.d.ts.map +1 -0
  61. package/dist/utilities/cache-manager.js +288 -0
  62. package/dist/utilities/cache-manager.js.map +1 -0
  63. package/dist/utilities/circuit-breaker.d.ts +78 -0
  64. package/dist/utilities/circuit-breaker.d.ts.map +1 -0
  65. package/dist/utilities/circuit-breaker.js +350 -0
  66. package/dist/utilities/circuit-breaker.js.map +1 -0
  67. package/dist/utilities/concurrency-limiter.d.ts +46 -0
  68. package/dist/utilities/concurrency-limiter.d.ts.map +1 -0
  69. package/dist/utilities/concurrency-limiter.js +172 -0
  70. package/dist/utilities/concurrency-limiter.js.map +1 -0
  71. package/dist/utilities/delay.d.ts +2 -0
  72. package/dist/utilities/delay.d.ts.map +1 -0
  73. package/dist/utilities/delay.js +8 -0
  74. package/dist/utilities/delay.js.map +1 -0
  75. package/dist/utilities/execute-branch-workflow.d.ts +3 -0
  76. package/dist/utilities/execute-branch-workflow.d.ts.map +1 -0
  77. package/dist/utilities/execute-branch-workflow.js +736 -0
  78. package/dist/utilities/execute-branch-workflow.js.map +1 -0
  79. package/dist/utilities/execute-concurrently.d.ts +3 -0
  80. package/dist/utilities/execute-concurrently.d.ts.map +1 -0
  81. package/dist/utilities/execute-concurrently.js +258 -0
  82. package/dist/utilities/execute-concurrently.js.map +1 -0
  83. package/dist/utilities/execute-gateway-item.d.ts +6 -0
  84. package/dist/utilities/execute-gateway-item.d.ts.map +1 -0
  85. package/dist/utilities/execute-gateway-item.js +129 -0
  86. package/dist/utilities/execute-gateway-item.js.map +1 -0
  87. package/dist/utilities/execute-non-linear-workflow.d.ts +3 -0
  88. package/dist/utilities/execute-non-linear-workflow.d.ts.map +1 -0
  89. package/dist/utilities/execute-non-linear-workflow.js +486 -0
  90. package/dist/utilities/execute-non-linear-workflow.js.map +1 -0
  91. package/dist/utilities/execute-phase.d.ts +3 -0
  92. package/dist/utilities/execute-phase.d.ts.map +1 -0
  93. package/dist/utilities/execute-phase.js +132 -0
  94. package/dist/utilities/execute-phase.js.map +1 -0
  95. package/dist/utilities/execute-sequentially.d.ts +3 -0
  96. package/dist/utilities/execute-sequentially.d.ts.map +1 -0
  97. package/dist/utilities/execute-sequentially.js +49 -0
  98. package/dist/utilities/execute-sequentially.js.map +1 -0
  99. package/dist/utilities/execute-with-persistence.d.ts +3 -0
  100. package/dist/utilities/execute-with-persistence.d.ts.map +1 -0
  101. package/dist/utilities/execute-with-persistence.js +60 -0
  102. package/dist/utilities/execute-with-persistence.js.map +1 -0
  103. package/dist/utilities/execute-with-timeout.d.ts +6 -0
  104. package/dist/utilities/execute-with-timeout.d.ts.map +1 -0
  105. package/dist/utilities/execute-with-timeout.js +28 -0
  106. package/dist/utilities/execute-with-timeout.js.map +1 -0
  107. package/dist/utilities/execute-workflow-graph.d.ts +3 -0
  108. package/dist/utilities/execute-workflow-graph.d.ts.map +1 -0
  109. package/dist/utilities/execute-workflow-graph.js +439 -0
  110. package/dist/utilities/execute-workflow-graph.js.map +1 -0
  111. package/dist/utilities/extract-common-request-config-options.d.ts +3 -0
  112. package/dist/utilities/extract-common-request-config-options.d.ts.map +1 -0
  113. package/dist/utilities/extract-common-request-config-options.js +12 -0
  114. package/dist/utilities/extract-common-request-config-options.js.map +1 -0
  115. package/dist/utilities/fn-exec.d.ts +3 -0
  116. package/dist/utilities/fn-exec.d.ts.map +1 -0
  117. package/dist/utilities/fn-exec.js +66 -0
  118. package/dist/utilities/fn-exec.js.map +1 -0
  119. package/dist/utilities/format-log-context.d.ts +3 -0
  120. package/dist/utilities/format-log-context.d.ts.map +1 -0
  121. package/dist/utilities/format-log-context.js +19 -0
  122. package/dist/utilities/format-log-context.js.map +1 -0
  123. package/dist/utilities/function-cache-manager.d.ts +32 -0
  124. package/dist/utilities/function-cache-manager.d.ts.map +1 -0
  125. package/dist/utilities/function-cache-manager.js +172 -0
  126. package/dist/utilities/function-cache-manager.js.map +1 -0
  127. package/dist/utilities/generate-axios-request-config.d.ts +12 -0
  128. package/dist/utilities/generate-axios-request-config.d.ts.map +1 -0
  129. package/dist/utilities/generate-axios-request-config.js +14 -0
  130. package/dist/utilities/generate-axios-request-config.js.map +1 -0
  131. package/dist/utilities/get-new-delay-time.d.ts +3 -0
  132. package/dist/utilities/get-new-delay-time.d.ts.map +1 -0
  133. package/dist/utilities/get-new-delay-time.js +26 -0
  134. package/dist/utilities/get-new-delay-time.js.map +1 -0
  135. package/dist/utilities/hash-utils.d.ts +4 -0
  136. package/dist/utilities/hash-utils.d.ts.map +1 -0
  137. package/dist/utilities/hash-utils.js +24 -0
  138. package/dist/utilities/hash-utils.js.map +1 -0
  139. package/dist/utilities/index.d.ts +37 -0
  140. package/dist/utilities/index.d.ts.map +1 -0
  141. package/dist/utilities/index.js +37 -0
  142. package/dist/utilities/index.js.map +1 -0
  143. package/dist/utilities/infrastructure-persistence.d.ts +14 -0
  144. package/dist/utilities/infrastructure-persistence.d.ts.map +1 -0
  145. package/dist/utilities/infrastructure-persistence.js +87 -0
  146. package/dist/utilities/infrastructure-persistence.js.map +1 -0
  147. package/dist/utilities/is-retryable-error.d.ts +4 -0
  148. package/dist/utilities/is-retryable-error.d.ts.map +1 -0
  149. package/dist/utilities/is-retryable-error.js +22 -0
  150. package/dist/utilities/is-retryable-error.js.map +1 -0
  151. package/dist/utilities/metrics-aggregator.d.ts +19 -0
  152. package/dist/utilities/metrics-aggregator.d.ts.map +1 -0
  153. package/dist/utilities/metrics-aggregator.js +300 -0
  154. package/dist/utilities/metrics-aggregator.js.map +1 -0
  155. package/dist/utilities/metrics-validator.d.ts +98 -0
  156. package/dist/utilities/metrics-validator.d.ts.map +1 -0
  157. package/dist/utilities/metrics-validator.js +324 -0
  158. package/dist/utilities/metrics-validator.js.map +1 -0
  159. package/dist/utilities/prepare-api-function-options.d.ts +3 -0
  160. package/dist/utilities/prepare-api-function-options.d.ts.map +1 -0
  161. package/dist/utilities/prepare-api-function-options.js +51 -0
  162. package/dist/utilities/prepare-api-function-options.js.map +1 -0
  163. package/dist/utilities/prepare-api-request-data.d.ts +3 -0
  164. package/dist/utilities/prepare-api-request-data.d.ts.map +1 -0
  165. package/dist/utilities/prepare-api-request-data.js +15 -0
  166. package/dist/utilities/prepare-api-request-data.js.map +1 -0
  167. package/dist/utilities/prepare-api-request-options.d.ts +3 -0
  168. package/dist/utilities/prepare-api-request-options.d.ts.map +1 -0
  169. package/dist/utilities/prepare-api-request-options.js +22 -0
  170. package/dist/utilities/prepare-api-request-options.js.map +1 -0
  171. package/dist/utilities/rate-limiter.d.ts +49 -0
  172. package/dist/utilities/rate-limiter.d.ts.map +1 -0
  173. package/dist/utilities/rate-limiter.js +197 -0
  174. package/dist/utilities/rate-limiter.js.map +1 -0
  175. package/dist/utilities/req-fn.d.ts +4 -0
  176. package/dist/utilities/req-fn.d.ts.map +1 -0
  177. package/dist/utilities/req-fn.js +94 -0
  178. package/dist/utilities/req-fn.js.map +1 -0
  179. package/dist/utilities/safely-execute-unknown-function.d.ts +2 -0
  180. package/dist/utilities/safely-execute-unknown-function.d.ts.map +1 -0
  181. package/dist/utilities/safely-execute-unknown-function.js +8 -0
  182. package/dist/utilities/safely-execute-unknown-function.js.map +1 -0
  183. package/dist/utilities/safely-stringify.d.ts +4 -0
  184. package/dist/utilities/safely-stringify.d.ts.map +1 -0
  185. package/dist/utilities/safely-stringify.js +13 -0
  186. package/dist/utilities/safely-stringify.js.map +1 -0
  187. package/dist/utilities/stable-buffer-replay.d.ts +3 -0
  188. package/dist/utilities/stable-buffer-replay.d.ts.map +1 -0
  189. package/dist/utilities/stable-buffer-replay.js +94 -0
  190. package/dist/utilities/stable-buffer-replay.js.map +1 -0
  191. package/dist/utilities/validate-trial-mode-probabilities.d.ts +3 -0
  192. package/dist/utilities/validate-trial-mode-probabilities.d.ts.map +1 -0
  193. package/dist/utilities/validate-trial-mode-probabilities.js +13 -0
  194. package/dist/utilities/validate-trial-mode-probabilities.js.map +1 -0
  195. package/dist/utilities/validate-workflow-graph.d.ts +7 -0
  196. package/dist/utilities/validate-workflow-graph.d.ts.map +1 -0
  197. package/dist/utilities/validate-workflow-graph.js +235 -0
  198. package/dist/utilities/validate-workflow-graph.js.map +1 -0
  199. package/dist/utilities/workflow-graph-builder.d.ts +37 -0
  200. package/dist/utilities/workflow-graph-builder.d.ts.map +1 -0
  201. package/dist/utilities/workflow-graph-builder.js +225 -0
  202. package/dist/utilities/workflow-graph-builder.js.map +1 -0
  203. package/package.json +63 -0
@@ -0,0 +1,782 @@
1
+ import { ScheduleTypes } from '../enums/index.js';
2
+ import { isStableBuffer, MetricsValidator } from '../utilities/index.js';
3
+ export class StableScheduler {
4
+ config;
5
+ handler;
6
+ jobs = new Map();
7
+ queue = [];
8
+ queued = new Set();
9
+ timer = null;
10
+ persistTimer = null;
11
+ persistQueued = false;
12
+ runningCount = 0;
13
+ completed = 0;
14
+ failed = 0;
15
+ dropped = 0;
16
+ sequence = 0;
17
+ schedulerStartTime = null;
18
+ totalExecutionTimeMs = 0;
19
+ totalQueueDelayMs = 0;
20
+ constructor(config, handler) {
21
+ this.config = {
22
+ maxParallel: config.maxParallel ?? 2,
23
+ tickIntervalMs: config.tickIntervalMs ?? 500,
24
+ queueLimit: config.queueLimit ?? 1000,
25
+ timezone: config.timezone,
26
+ persistence: {
27
+ enabled: config.persistence?.enabled ?? false,
28
+ saveState: config.persistence?.saveState,
29
+ loadState: config.persistence?.loadState,
30
+ persistenceDebounceMs: config.persistence?.persistenceDebounceMs
31
+ },
32
+ retry: config.retry,
33
+ executionTimeoutMs: config.executionTimeoutMs,
34
+ metricsGuardrails: config.metricsGuardrails,
35
+ sharedBuffer: config.sharedBuffer,
36
+ sharedInfrastructure: config.sharedInfrastructure,
37
+ loadTransactionLogs: config.loadTransactionLogs
38
+ };
39
+ this.handler = handler;
40
+ }
41
+ addJobs(jobs) {
42
+ jobs.forEach((job) => this.addJob(job));
43
+ void this.persistStateIfEnabled();
44
+ }
45
+ setJobs(jobs) {
46
+ this.stop();
47
+ this.jobs.clear();
48
+ this.queue.length = 0;
49
+ this.queued.clear();
50
+ this.runningCount = 0;
51
+ this.completed = 0;
52
+ this.failed = 0;
53
+ this.dropped = 0;
54
+ this.addJobs(jobs);
55
+ this.start();
56
+ void this.persistStateIfEnabled();
57
+ }
58
+ addJob(job) {
59
+ const id = job.id ?? this.createId('job');
60
+ const schedule = job.schedule;
61
+ const now = Date.now();
62
+ const { nextRunAt, runOnce, remainingTimestamps } = this.initializeSchedule(schedule, now);
63
+ const scheduledJob = {
64
+ id,
65
+ job: { ...job, id },
66
+ schedule,
67
+ nextRunAt,
68
+ lastRunAt: null,
69
+ remainingTimestamps,
70
+ runOnce,
71
+ isRunning: false,
72
+ retryAttempts: 0
73
+ };
74
+ this.jobs.set(id, scheduledJob);
75
+ void this.persistStateIfEnabled();
76
+ return id;
77
+ }
78
+ start() {
79
+ if (this.timer) {
80
+ return;
81
+ }
82
+ if (!this.schedulerStartTime) {
83
+ this.schedulerStartTime = Date.now();
84
+ }
85
+ this.timer = setInterval(() => this.tick(), Math.max(50, this.config.tickIntervalMs));
86
+ this.tick();
87
+ }
88
+ stop() {
89
+ if (this.timer) {
90
+ clearInterval(this.timer);
91
+ this.timer = null;
92
+ }
93
+ }
94
+ tick() {
95
+ const now = Date.now();
96
+ let stateChanged = false;
97
+ for (const [id, job] of this.jobs.entries()) {
98
+ if (job.isRunning || this.queued.has(id) || job.nextRunAt === null) {
99
+ continue;
100
+ }
101
+ if (job.nextRunAt <= now) {
102
+ if (this.queue.length >= this.config.queueLimit) {
103
+ this.dropped += 1;
104
+ continue;
105
+ }
106
+ this.queue.push(id);
107
+ this.queued.add(id);
108
+ stateChanged = true;
109
+ }
110
+ }
111
+ while (this.runningCount < this.config.maxParallel && this.queue.length > 0) {
112
+ const id = this.queue.shift();
113
+ if (!id)
114
+ break;
115
+ this.queued.delete(id);
116
+ const job = this.jobs.get(id);
117
+ if (!job)
118
+ continue;
119
+ this.dispatch(job);
120
+ stateChanged = true;
121
+ }
122
+ if (stateChanged) {
123
+ void this.persistStateIfEnabled();
124
+ }
125
+ }
126
+ getStats() {
127
+ return {
128
+ queued: this.queue.length,
129
+ running: this.runningCount,
130
+ completed: this.completed,
131
+ failed: this.failed,
132
+ dropped: this.dropped,
133
+ totalJobs: this.jobs.size
134
+ };
135
+ }
136
+ getSharedInfrastructure() {
137
+ return this.config.sharedInfrastructure;
138
+ }
139
+ getInfrastructureMetrics() {
140
+ const infra = this.config.sharedInfrastructure;
141
+ if (!infra) {
142
+ return {};
143
+ }
144
+ return {
145
+ ...(infra.circuitBreaker ? { circuitBreaker: infra.circuitBreaker.getState() } : {}),
146
+ ...(infra.rateLimiter ? { rateLimiter: infra.rateLimiter.getState() } : {}),
147
+ ...(infra.concurrencyLimiter ? { concurrencyLimiter: infra.concurrencyLimiter.getState() } : {}),
148
+ ...(infra.cacheManager ? { cacheManager: infra.cacheManager.getStats() } : {})
149
+ };
150
+ }
151
+ getMetrics() {
152
+ const totalRuns = this.completed + this.failed;
153
+ const startedAt = this.schedulerStartTime ?? Date.now();
154
+ const elapsedMs = Math.max(0, Date.now() - startedAt);
155
+ const successRate = totalRuns > 0 ? (this.completed / totalRuns) * 100 : 0;
156
+ const failureRate = totalRuns > 0 ? (this.failed / totalRuns) * 100 : 0;
157
+ const throughput = elapsedMs > 0 ? totalRuns / (elapsedMs / 1000) : 0;
158
+ const averageExecutionTime = totalRuns > 0 ? this.totalExecutionTimeMs / totalRuns : 0;
159
+ const averageQueueDelay = totalRuns > 0 ? this.totalQueueDelayMs / totalRuns : 0;
160
+ const infra = this.config.sharedInfrastructure;
161
+ const infrastructureMetrics = this.buildInfrastructureMetrics(infra);
162
+ const metrics = {
163
+ totalJobs: this.jobs.size,
164
+ queued: this.queue.length,
165
+ running: this.runningCount,
166
+ completed: this.completed,
167
+ failed: this.failed,
168
+ dropped: this.dropped,
169
+ totalRuns,
170
+ successRate,
171
+ failureRate,
172
+ throughput,
173
+ averageExecutionTime,
174
+ averageQueueDelay,
175
+ startedAt: this.schedulerStartTime ? new Date(this.schedulerStartTime).toISOString() : undefined,
176
+ lastUpdated: new Date().toISOString(),
177
+ ...(Object.keys(infrastructureMetrics).length > 0 ? { infrastructure: infrastructureMetrics } : {})
178
+ };
179
+ if (!this.config.metricsGuardrails) {
180
+ return { metrics };
181
+ }
182
+ const schedulerValidation = MetricsValidator.validateSchedulerMetrics(metrics, this.config.metricsGuardrails);
183
+ const infraValidation = this.validateInfrastructureMetrics(infrastructureMetrics);
184
+ const combinedAnomalies = [
185
+ ...schedulerValidation.anomalies,
186
+ ...infraValidation.anomalies
187
+ ];
188
+ return {
189
+ metrics,
190
+ validation: {
191
+ isValid: combinedAnomalies.length === 0,
192
+ anomalies: combinedAnomalies,
193
+ validatedAt: new Date().toISOString()
194
+ }
195
+ };
196
+ }
197
+ buildInfrastructureMetrics(infra) {
198
+ const result = {};
199
+ if (!infra)
200
+ return result;
201
+ if (infra.circuitBreaker) {
202
+ const cbState = infra.circuitBreaker.getState();
203
+ result.circuitBreaker = {
204
+ state: cbState.state,
205
+ totalRequests: cbState.totalRequests,
206
+ failedRequests: cbState.failedRequests,
207
+ successfulRequests: cbState.successfulRequests,
208
+ failurePercentage: cbState.failurePercentage
209
+ };
210
+ }
211
+ if (infra.rateLimiter) {
212
+ const rlState = infra.rateLimiter.getState();
213
+ result.rateLimiter = {
214
+ totalRequests: rlState.totalRequests,
215
+ throttledRequests: rlState.throttledRequests,
216
+ throttleRate: rlState.throttleRate,
217
+ queueLength: rlState.queueLength,
218
+ averageQueueWaitTime: rlState.averageQueueWaitTime
219
+ };
220
+ }
221
+ if (infra.concurrencyLimiter) {
222
+ const clState = infra.concurrencyLimiter.getState();
223
+ result.concurrencyLimiter = {
224
+ totalRequests: clState.totalRequests,
225
+ completedRequests: clState.completedRequests,
226
+ queuedRequests: clState.queuedRequests,
227
+ queueLength: clState.queueLength,
228
+ averageQueueWaitTime: clState.averageQueueWaitTime,
229
+ utilizationPercentage: clState.utilizationPercentage
230
+ };
231
+ }
232
+ if (infra.cacheManager) {
233
+ const cacheStats = infra.cacheManager.getStats();
234
+ result.cacheManager = {
235
+ hits: cacheStats.hits,
236
+ misses: cacheStats.misses,
237
+ hitRate: cacheStats.hitRate,
238
+ missRate: cacheStats.missRate,
239
+ utilizationPercentage: cacheStats.utilizationPercentage,
240
+ evictions: cacheStats.evictions
241
+ };
242
+ }
243
+ return result;
244
+ }
245
+ validateInfrastructureMetrics(infraMetrics) {
246
+ if (!this.config.metricsGuardrails?.infrastructure || Object.keys(infraMetrics).length === 0) {
247
+ return { isValid: true, anomalies: [], validatedAt: new Date().toISOString() };
248
+ }
249
+ const transformedMetrics = {};
250
+ if (infraMetrics.circuitBreaker) {
251
+ transformedMetrics.circuitBreaker = {
252
+ failureRate: infraMetrics.circuitBreaker.failurePercentage,
253
+ totalRequests: infraMetrics.circuitBreaker.totalRequests,
254
+ failedRequests: infraMetrics.circuitBreaker.failedRequests
255
+ };
256
+ }
257
+ if (infraMetrics.cacheManager) {
258
+ transformedMetrics.cache = {
259
+ hitRate: infraMetrics.cacheManager.hitRate,
260
+ missRate: infraMetrics.cacheManager.missRate,
261
+ utilizationPercentage: infraMetrics.cacheManager.utilizationPercentage,
262
+ evictionRate: infraMetrics.cacheManager.evictions > 0 ?
263
+ (infraMetrics.cacheManager.evictions / (infraMetrics.cacheManager.hits + infraMetrics.cacheManager.misses)) * 100 : 0
264
+ };
265
+ }
266
+ if (infraMetrics.rateLimiter) {
267
+ transformedMetrics.rateLimiter = {
268
+ throttleRate: infraMetrics.rateLimiter.throttleRate,
269
+ queueLength: infraMetrics.rateLimiter.queueLength,
270
+ averageQueueWaitTime: infraMetrics.rateLimiter.averageQueueWaitTime
271
+ };
272
+ }
273
+ if (infraMetrics.concurrencyLimiter) {
274
+ transformedMetrics.concurrencyLimiter = {
275
+ utilizationPercentage: infraMetrics.concurrencyLimiter.utilizationPercentage,
276
+ queueLength: infraMetrics.concurrencyLimiter.queueLength,
277
+ averageQueueWaitTime: infraMetrics.concurrencyLimiter.averageQueueWaitTime
278
+ };
279
+ }
280
+ return MetricsValidator.validateInfrastructureMetrics(transformedMetrics, this.config.metricsGuardrails);
281
+ }
282
+ getState() {
283
+ const sharedBufferSnapshot = isStableBuffer(this.config.sharedBuffer)
284
+ ? this.config.sharedBuffer.read()
285
+ : this.config.sharedBuffer;
286
+ return {
287
+ jobs: Array.from(this.jobs.values()).map((job) => ({
288
+ id: job.id,
289
+ job: job.job,
290
+ schedule: job.schedule,
291
+ nextRunAt: job.nextRunAt,
292
+ lastRunAt: job.lastRunAt,
293
+ remainingTimestamps: job.remainingTimestamps ? [...job.remainingTimestamps] : null,
294
+ runOnce: job.runOnce,
295
+ isRunning: job.isRunning,
296
+ retryAttempts: job.retryAttempts
297
+ })),
298
+ queue: [...this.queue],
299
+ stats: {
300
+ completed: this.completed,
301
+ failed: this.failed,
302
+ dropped: this.dropped,
303
+ sequence: this.sequence
304
+ },
305
+ sharedBuffer: sharedBufferSnapshot
306
+ };
307
+ }
308
+ async restoreState(state) {
309
+ let resolvedState = state;
310
+ if (!resolvedState && this.config.persistence.loadState) {
311
+ resolvedState = await this.config.persistence.loadState();
312
+ }
313
+ if (!resolvedState) {
314
+ return false;
315
+ }
316
+ this.stop();
317
+ this.jobs.clear();
318
+ this.queue.length = 0;
319
+ this.queued.clear();
320
+ this.runningCount = 0;
321
+ this.completed = resolvedState.stats.completed;
322
+ this.failed = resolvedState.stats.failed;
323
+ this.dropped = resolvedState.stats.dropped;
324
+ this.sequence = resolvedState.stats.sequence;
325
+ if (resolvedState.sharedBuffer !== undefined) {
326
+ if (isStableBuffer(this.config.sharedBuffer)) {
327
+ this.config.sharedBuffer.setState(resolvedState.sharedBuffer);
328
+ }
329
+ else {
330
+ this.config.sharedBuffer = resolvedState.sharedBuffer;
331
+ }
332
+ }
333
+ resolvedState.jobs.forEach((jobState) => {
334
+ const restored = {
335
+ id: jobState.id,
336
+ job: jobState.job,
337
+ schedule: jobState.schedule,
338
+ nextRunAt: jobState.nextRunAt,
339
+ lastRunAt: jobState.lastRunAt,
340
+ remainingTimestamps: jobState.remainingTimestamps ? [...jobState.remainingTimestamps] : null,
341
+ runOnce: jobState.runOnce,
342
+ isRunning: false,
343
+ retryAttempts: jobState.retryAttempts ?? 0
344
+ };
345
+ this.jobs.set(jobState.id, restored);
346
+ });
347
+ resolvedState.queue.forEach((id) => {
348
+ if (this.jobs.has(id)) {
349
+ this.queue.push(id);
350
+ this.queued.add(id);
351
+ }
352
+ });
353
+ this.tick();
354
+ void this.persistStateIfEnabled();
355
+ return true;
356
+ }
357
+ dispatch(job) {
358
+ this.runningCount += 1;
359
+ job.isRunning = true;
360
+ if (job.runOnce) {
361
+ job.nextRunAt = null;
362
+ }
363
+ void this.persistStateIfEnabled();
364
+ const startedAt = Date.now();
365
+ const scheduledAtMs = job.nextRunAt ?? startedAt;
366
+ const queueDelay = Math.max(0, startedAt - scheduledAtMs);
367
+ this.totalQueueDelayMs += queueDelay;
368
+ const sharedInfra = this.config.sharedInfrastructure;
369
+ let transactionLogs;
370
+ const createBaseContext = () => ({
371
+ runId: this.createId('run'),
372
+ jobId: job.id,
373
+ scheduledAt: new Date(job.nextRunAt ?? startedAt).toISOString(),
374
+ startedAt: new Date(startedAt).toISOString(),
375
+ schedule: job.schedule,
376
+ ...(sharedInfra ? { sharedInfrastructure: sharedInfra } : {}),
377
+ ...(transactionLogs ? { transactionLogs } : {})
378
+ });
379
+ const retryConfig = this.getRetryConfig(job);
380
+ if (retryConfig) {
381
+ job.retryAttempts += 1;
382
+ }
383
+ let jobError = null;
384
+ const executeHandler = async () => {
385
+ if (this.config.loadTransactionLogs) {
386
+ try {
387
+ transactionLogs = await this.config.loadTransactionLogs({});
388
+ }
389
+ catch (e) {
390
+ console.error(`stable-infra: Failed to load transaction logs: ${e.message}`);
391
+ }
392
+ }
393
+ const baseContext = createBaseContext();
394
+ if (sharedInfra?.circuitBreaker) {
395
+ const canExecute = await sharedInfra.circuitBreaker.canExecute();
396
+ if (!canExecute) {
397
+ throw new Error('Circuit breaker is open');
398
+ }
399
+ }
400
+ if (sharedInfra?.rateLimiter) {
401
+ await sharedInfra.rateLimiter.execute(async () => { });
402
+ }
403
+ const runHandler = async () => {
404
+ if (isStableBuffer(this.config.sharedBuffer)) {
405
+ return this.config.sharedBuffer.run((bufferState) => this.handler(job.job, { ...baseContext, sharedBuffer: bufferState }));
406
+ }
407
+ else {
408
+ return this.handler(job.job, {
409
+ ...baseContext,
410
+ ...(this.config.sharedBuffer !== undefined
411
+ ? { sharedBuffer: this.config.sharedBuffer }
412
+ : {})
413
+ });
414
+ }
415
+ };
416
+ if (sharedInfra?.concurrencyLimiter) {
417
+ return sharedInfra.concurrencyLimiter.execute(runHandler);
418
+ }
419
+ return runHandler();
420
+ };
421
+ let handlerPromise;
422
+ if (sharedInfra?.circuitBreaker || sharedInfra?.rateLimiter || sharedInfra?.concurrencyLimiter || this.config.loadTransactionLogs) {
423
+ handlerPromise = executeHandler();
424
+ }
425
+ else if (isStableBuffer(this.config.sharedBuffer)) {
426
+ const baseContext = createBaseContext();
427
+ handlerPromise = this.config.sharedBuffer.run((bufferState) => this.handler(job.job, { ...baseContext, sharedBuffer: bufferState }));
428
+ }
429
+ else {
430
+ try {
431
+ const baseContext = createBaseContext();
432
+ const result = this.handler(job.job, {
433
+ ...baseContext,
434
+ ...(this.config.sharedBuffer !== undefined
435
+ ? { sharedBuffer: this.config.sharedBuffer }
436
+ : {})
437
+ });
438
+ handlerPromise = Promise.resolve(result);
439
+ }
440
+ catch (error) {
441
+ handlerPromise = Promise.reject(error);
442
+ }
443
+ }
444
+ const executionPromise = this.withTimeout(handlerPromise, this.getExecutionTimeoutMs(job));
445
+ void executionPromise
446
+ .then(() => {
447
+ this.completed += 1;
448
+ job.retryAttempts = 0;
449
+ if (sharedInfra?.circuitBreaker) {
450
+ sharedInfra.circuitBreaker.recordSuccess();
451
+ }
452
+ })
453
+ .catch((error) => {
454
+ this.failed += 1;
455
+ jobError = error;
456
+ if (sharedInfra?.circuitBreaker) {
457
+ sharedInfra.circuitBreaker.recordFailure();
458
+ }
459
+ })
460
+ .finally(() => {
461
+ const scheduledRetry = this.scheduleRetryIfEnabled(job, startedAt, jobError);
462
+ const executionTime = Date.now() - startedAt;
463
+ this.totalExecutionTimeMs += Math.max(0, executionTime);
464
+ job.isRunning = false;
465
+ job.lastRunAt = startedAt;
466
+ this.runningCount -= 1;
467
+ if (!scheduledRetry) {
468
+ job.retryAttempts = 0;
469
+ this.updateNextRun(job, startedAt);
470
+ }
471
+ this.tick();
472
+ void this.persistStateIfEnabled();
473
+ });
474
+ }
475
+ getRetryConfig(job) {
476
+ return job.job.retry ?? this.config.retry;
477
+ }
478
+ getExecutionTimeoutMs(job) {
479
+ return job.job.executionTimeoutMs ?? this.config.executionTimeoutMs;
480
+ }
481
+ scheduleRetryIfEnabled(job, startedAt, error) {
482
+ if (!error) {
483
+ return false;
484
+ }
485
+ const retryConfig = this.getRetryConfig(job);
486
+ if (!retryConfig) {
487
+ return false;
488
+ }
489
+ const maxAttempts = retryConfig.maxAttempts ?? 1;
490
+ if (maxAttempts <= 1 || job.retryAttempts >= maxAttempts) {
491
+ return false;
492
+ }
493
+ const baseDelay = retryConfig.delayMs ?? 1000;
494
+ const backoff = retryConfig.backoffMultiplier ?? 1;
495
+ const calculatedDelay = baseDelay * Math.pow(backoff, Math.max(job.retryAttempts - 1, 0));
496
+ const delay = retryConfig.maxDelayMs ? Math.min(calculatedDelay, retryConfig.maxDelayMs) : calculatedDelay;
497
+ job.nextRunAt = startedAt + Math.max(0, delay);
498
+ return true;
499
+ }
500
+ withTimeout(promise, timeoutMs) {
501
+ if (!timeoutMs || timeoutMs <= 0) {
502
+ return promise;
503
+ }
504
+ return new Promise((resolve, reject) => {
505
+ const timeoutId = setTimeout(() => {
506
+ reject(new Error(`Scheduler job timed out after ${timeoutMs}ms`));
507
+ }, timeoutMs);
508
+ promise
509
+ .then((value) => {
510
+ clearTimeout(timeoutId);
511
+ resolve(value);
512
+ })
513
+ .catch((error) => {
514
+ clearTimeout(timeoutId);
515
+ reject(error);
516
+ });
517
+ });
518
+ }
519
+ initializeSchedule(schedule, now) {
520
+ if (!schedule) {
521
+ return { nextRunAt: now, runOnce: true, remainingTimestamps: null };
522
+ }
523
+ if (schedule.type === ScheduleTypes.INTERVAL) {
524
+ const startAt = this.parseTimestamp(schedule.startAt);
525
+ if (startAt !== null && startAt > now) {
526
+ return { nextRunAt: startAt, runOnce: false, remainingTimestamps: null };
527
+ }
528
+ return { nextRunAt: now, runOnce: false, remainingTimestamps: null };
529
+ }
530
+ if (schedule.type === ScheduleTypes.CRON) {
531
+ const nextRunAt = this.getNextCronTime(schedule.expression, now, schedule.timezone);
532
+ return { nextRunAt, runOnce: false, remainingTimestamps: null };
533
+ }
534
+ if (schedule.type === ScheduleTypes.TIMESTAMP) {
535
+ const at = this.parseTimestamp(schedule.at);
536
+ return { nextRunAt: at, runOnce: true, remainingTimestamps: null };
537
+ }
538
+ const timestamps = schedule.at
539
+ .map((value) => this.parseTimestamp(value))
540
+ .filter((value) => value !== null)
541
+ .sort((a, b) => a - b);
542
+ const nextRunAt = timestamps.length > 0 ? timestamps[0] : null;
543
+ return { nextRunAt, runOnce: false, remainingTimestamps: timestamps };
544
+ }
545
+ updateNextRun(job, lastRunAt) {
546
+ const schedule = job.schedule;
547
+ if (!schedule) {
548
+ job.nextRunAt = job.runOnce ? null : lastRunAt;
549
+ return;
550
+ }
551
+ if (schedule.type === ScheduleTypes.INTERVAL) {
552
+ job.nextRunAt = lastRunAt + schedule.everyMs;
553
+ return;
554
+ }
555
+ if (schedule.type === ScheduleTypes.CRON) {
556
+ job.nextRunAt = this.getNextCronTime(schedule.expression, lastRunAt, schedule.timezone);
557
+ return;
558
+ }
559
+ if (schedule.type === ScheduleTypes.TIMESTAMP) {
560
+ job.nextRunAt = null;
561
+ return;
562
+ }
563
+ const remaining = job.remainingTimestamps ?? [];
564
+ while (remaining.length > 0 && remaining[0] <= lastRunAt) {
565
+ remaining.shift();
566
+ }
567
+ job.remainingTimestamps = remaining;
568
+ job.nextRunAt = remaining.length > 0 ? remaining[0] : null;
569
+ }
570
+ parseTimestamp(value) {
571
+ if (typeof value === 'number') {
572
+ return Number.isFinite(value) ? value : null;
573
+ }
574
+ if (typeof value === 'string') {
575
+ const parsed = Date.parse(value);
576
+ return Number.isNaN(parsed) ? null : parsed;
577
+ }
578
+ return null;
579
+ }
580
+ getNextCronTime(expression, fromMs, timezone) {
581
+ const fields = expression.trim().split(/\s+/);
582
+ if (fields.length < 5 || fields.length > 6) {
583
+ return null;
584
+ }
585
+ const hasSeconds = fields.length === 6;
586
+ const [secField, minField, hourField, dayField, monthField, dowField] = hasSeconds
587
+ ? fields
588
+ : ['0', ...fields];
589
+ const seconds = this.parseCronField(secField, 0, 59, true);
590
+ const minutes = this.parseCronField(minField, 0, 59, true);
591
+ const hours = this.parseCronField(hourField, 0, 23, true);
592
+ const days = this.parseCronField(dayField, 1, 31, true);
593
+ const months = this.parseCronField(monthField, 1, 12, true);
594
+ const dows = this.parseCronField(dowField, 0, 6, true);
595
+ if (!seconds || !minutes || !hours || !days || !months || !dows) {
596
+ return null;
597
+ }
598
+ const maxIterations = 366 * 24 * 60 * 60;
599
+ let candidate = new Date(fromMs + 1000);
600
+ for (let i = 0; i < maxIterations; i += 1) {
601
+ const candidateDate = candidate;
602
+ const parts = this.getCronDateParts(candidateDate, timezone);
603
+ if (!parts) {
604
+ return null;
605
+ }
606
+ const match = seconds.has(parts.second) &&
607
+ minutes.has(parts.minute) &&
608
+ hours.has(parts.hour) &&
609
+ days.has(parts.day) &&
610
+ months.has(parts.month) &&
611
+ dows.has(parts.dow);
612
+ if (match) {
613
+ return candidateDate.getTime();
614
+ }
615
+ candidate = new Date(candidateDate.getTime() + 1000);
616
+ }
617
+ return null;
618
+ }
619
+ parseCronField(field, min, max, strict) {
620
+ const values = new Set();
621
+ const segments = field.split(',');
622
+ let hasValidSegment = false;
623
+ segments.forEach((segment) => {
624
+ const trimmed = segment.trim();
625
+ if (!trimmed)
626
+ return;
627
+ const [rangePart, stepPart] = trimmed.split('/');
628
+ if (stepPart !== undefined && !this.isValidInteger(stepPart)) {
629
+ return;
630
+ }
631
+ const step = stepPart ? Number(stepPart) : 1;
632
+ const safeStep = Number.isFinite(step) && step > 0 ? step : null;
633
+ if (!safeStep) {
634
+ return;
635
+ }
636
+ let rangeStart;
637
+ let rangeEnd;
638
+ if (rangePart === '*') {
639
+ rangeStart = min;
640
+ rangeEnd = max;
641
+ }
642
+ else if (rangePart.includes('-')) {
643
+ const [startRaw, endRaw] = rangePart.split('-');
644
+ if (!this.isValidInteger(startRaw) || !this.isValidInteger(endRaw)) {
645
+ return;
646
+ }
647
+ rangeStart = Number(startRaw);
648
+ rangeEnd = Number(endRaw);
649
+ }
650
+ else {
651
+ if (!this.isValidInteger(rangePart)) {
652
+ return;
653
+ }
654
+ rangeStart = Number(rangePart);
655
+ rangeEnd = rangeStart;
656
+ }
657
+ if (rangeStart < min || rangeEnd > max || rangeStart > rangeEnd) {
658
+ return;
659
+ }
660
+ for (let value = rangeStart; value <= rangeEnd; value += safeStep) {
661
+ values.add(value);
662
+ }
663
+ hasValidSegment = true;
664
+ });
665
+ if (values.size === 0) {
666
+ if (strict) {
667
+ return null;
668
+ }
669
+ for (let value = min; value <= max; value += 1) {
670
+ values.add(value);
671
+ }
672
+ }
673
+ return values;
674
+ }
675
+ isValidInteger(value) {
676
+ return /^\d+$/.test(value);
677
+ }
678
+ getCronDateParts(date, timezone) {
679
+ if (!timezone) {
680
+ return {
681
+ second: date.getSeconds(),
682
+ minute: date.getMinutes(),
683
+ hour: date.getHours(),
684
+ day: date.getDate(),
685
+ month: date.getMonth() + 1,
686
+ dow: date.getDay()
687
+ };
688
+ }
689
+ try {
690
+ const formatter = new Intl.DateTimeFormat('en-US', {
691
+ timeZone: timezone,
692
+ hour12: false,
693
+ weekday: 'short',
694
+ year: 'numeric',
695
+ month: '2-digit',
696
+ day: '2-digit',
697
+ hour: '2-digit',
698
+ minute: '2-digit',
699
+ second: '2-digit'
700
+ });
701
+ const parts = formatter.formatToParts(date);
702
+ const partMap = new Map(parts.map((part) => [part.type, part.value]));
703
+ const month = Number(partMap.get('month'));
704
+ const day = Number(partMap.get('day'));
705
+ const hour = Number(partMap.get('hour'));
706
+ const minute = Number(partMap.get('minute'));
707
+ const second = Number(partMap.get('second'));
708
+ const weekday = partMap.get('weekday');
709
+ if (Number.isNaN(month) ||
710
+ Number.isNaN(day) ||
711
+ Number.isNaN(hour) ||
712
+ Number.isNaN(minute) ||
713
+ Number.isNaN(second) ||
714
+ !weekday) {
715
+ return null;
716
+ }
717
+ const weekdayIndex = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(weekday);
718
+ if (weekdayIndex === -1) {
719
+ return null;
720
+ }
721
+ return {
722
+ second,
723
+ minute,
724
+ hour,
725
+ day,
726
+ month,
727
+ dow: weekdayIndex
728
+ };
729
+ }
730
+ catch {
731
+ return null;
732
+ }
733
+ }
734
+ createId(prefix) {
735
+ return `${prefix}-${this.generateUuid()}-${Date.now()}`;
736
+ }
737
+ generateUuid() {
738
+ if (typeof globalThis.crypto?.randomUUID === 'function') {
739
+ return globalThis.crypto.randomUUID();
740
+ }
741
+ const bytes = new Uint8Array(16);
742
+ if (typeof globalThis.crypto?.getRandomValues === 'function') {
743
+ globalThis.crypto.getRandomValues(bytes);
744
+ }
745
+ else {
746
+ for (let i = 0; i < bytes.length; i += 1) {
747
+ bytes[i] = Math.floor(Math.random() * 256);
748
+ }
749
+ }
750
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
751
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
752
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));
753
+ return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex
754
+ .slice(6, 8)
755
+ .join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;
756
+ }
757
+ async persistStateIfEnabled() {
758
+ if (!this.config.persistence.enabled || !this.config.persistence.saveState) {
759
+ return;
760
+ }
761
+ const debounceMs = this.config.persistence?.persistenceDebounceMs ?? 0;
762
+ if (debounceMs > 0) {
763
+ if (this.persistTimer) {
764
+ this.persistQueued = true;
765
+ return;
766
+ }
767
+ this.persistQueued = false;
768
+ this.persistTimer = setTimeout(async () => {
769
+ this.persistTimer = null;
770
+ const state = this.getState();
771
+ await this.config.persistence.saveState?.(state);
772
+ if (this.persistQueued) {
773
+ void this.persistStateIfEnabled();
774
+ }
775
+ }, debounceMs);
776
+ return;
777
+ }
778
+ const state = this.getState();
779
+ await this.config.persistence.saveState(state);
780
+ }
781
+ }
782
+ //# sourceMappingURL=stable-scheduler.js.map