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