@emmvish/stable-request 2.8.5 → 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 (135) 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/enums/index.d.ts +0 -37
  12. package/dist/enums/index.d.ts.map +1 -1
  13. package/dist/enums/index.js +0 -43
  14. package/dist/enums/index.js.map +1 -1
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +18 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/types/index.d.ts +94 -1157
  20. package/dist/types/index.d.ts.map +1 -1
  21. package/dist/utilities/index.d.ts +0 -18
  22. package/dist/utilities/index.d.ts.map +1 -1
  23. package/dist/utilities/index.js +0 -18
  24. package/dist/utilities/index.js.map +1 -1
  25. package/dist/utilities/infrastructure-persistence.d.ts +0 -1
  26. package/dist/utilities/infrastructure-persistence.d.ts.map +1 -1
  27. package/dist/utilities/infrastructure-persistence.js +12 -15
  28. package/dist/utilities/infrastructure-persistence.js.map +1 -1
  29. package/dist/utilities/metrics-aggregator.d.ts +2 -13
  30. package/dist/utilities/metrics-aggregator.d.ts.map +1 -1
  31. package/dist/utilities/metrics-aggregator.js +9 -251
  32. package/dist/utilities/metrics-aggregator.js.map +1 -1
  33. package/dist/utilities/metrics-validator.d.ts +6 -76
  34. package/dist/utilities/metrics-validator.d.ts.map +1 -1
  35. package/dist/utilities/metrics-validator.js +12 -181
  36. package/dist/utilities/metrics-validator.js.map +1 -1
  37. package/dist/utilities/validate-trial-mode-probabilities.js +2 -2
  38. package/dist/utilities/validate-trial-mode-probabilities.js.map +1 -1
  39. package/package.json +20 -24
  40. package/dist/core/stable-api-gateway.d.ts +0 -4
  41. package/dist/core/stable-api-gateway.d.ts.map +0 -1
  42. package/dist/core/stable-api-gateway.js +0 -146
  43. package/dist/core/stable-api-gateway.js.map +0 -1
  44. package/dist/core/stable-function.d.ts +0 -11
  45. package/dist/core/stable-function.d.ts.map +0 -1
  46. package/dist/core/stable-function.js +0 -355
  47. package/dist/core/stable-function.js.map +0 -1
  48. package/dist/core/stable-scheduler.d.ts +0 -71
  49. package/dist/core/stable-scheduler.d.ts.map +0 -1
  50. package/dist/core/stable-scheduler.js +0 -782
  51. package/dist/core/stable-scheduler.js.map +0 -1
  52. package/dist/core/stable-workflow-graph.d.ts +0 -3
  53. package/dist/core/stable-workflow-graph.d.ts.map +0 -1
  54. package/dist/core/stable-workflow-graph.js +0 -5
  55. package/dist/core/stable-workflow-graph.js.map +0 -1
  56. package/dist/core/stable-workflow.d.ts +0 -3
  57. package/dist/core/stable-workflow.d.ts.map +0 -1
  58. package/dist/core/stable-workflow.js +0 -374
  59. package/dist/core/stable-workflow.js.map +0 -1
  60. package/dist/stable-runner/index.d.ts +0 -2
  61. package/dist/stable-runner/index.d.ts.map +0 -1
  62. package/dist/stable-runner/index.js +0 -324
  63. package/dist/stable-runner/index.js.map +0 -1
  64. package/dist/utilities/concurrency-limiter.d.ts +0 -46
  65. package/dist/utilities/concurrency-limiter.d.ts.map +0 -1
  66. package/dist/utilities/concurrency-limiter.js +0 -172
  67. package/dist/utilities/concurrency-limiter.js.map +0 -1
  68. package/dist/utilities/execute-branch-workflow.d.ts +0 -3
  69. package/dist/utilities/execute-branch-workflow.d.ts.map +0 -1
  70. package/dist/utilities/execute-branch-workflow.js +0 -736
  71. package/dist/utilities/execute-branch-workflow.js.map +0 -1
  72. package/dist/utilities/execute-concurrently.d.ts +0 -3
  73. package/dist/utilities/execute-concurrently.d.ts.map +0 -1
  74. package/dist/utilities/execute-concurrently.js +0 -258
  75. package/dist/utilities/execute-concurrently.js.map +0 -1
  76. package/dist/utilities/execute-gateway-item.d.ts +0 -6
  77. package/dist/utilities/execute-gateway-item.d.ts.map +0 -1
  78. package/dist/utilities/execute-gateway-item.js +0 -129
  79. package/dist/utilities/execute-gateway-item.js.map +0 -1
  80. package/dist/utilities/execute-non-linear-workflow.d.ts +0 -3
  81. package/dist/utilities/execute-non-linear-workflow.d.ts.map +0 -1
  82. package/dist/utilities/execute-non-linear-workflow.js +0 -486
  83. package/dist/utilities/execute-non-linear-workflow.js.map +0 -1
  84. package/dist/utilities/execute-phase.d.ts +0 -3
  85. package/dist/utilities/execute-phase.d.ts.map +0 -1
  86. package/dist/utilities/execute-phase.js +0 -132
  87. package/dist/utilities/execute-phase.js.map +0 -1
  88. package/dist/utilities/execute-sequentially.d.ts +0 -3
  89. package/dist/utilities/execute-sequentially.d.ts.map +0 -1
  90. package/dist/utilities/execute-sequentially.js +0 -49
  91. package/dist/utilities/execute-sequentially.js.map +0 -1
  92. package/dist/utilities/execute-with-timeout.d.ts +0 -6
  93. package/dist/utilities/execute-with-timeout.d.ts.map +0 -1
  94. package/dist/utilities/execute-with-timeout.js +0 -28
  95. package/dist/utilities/execute-with-timeout.js.map +0 -1
  96. package/dist/utilities/execute-workflow-graph.d.ts +0 -3
  97. package/dist/utilities/execute-workflow-graph.d.ts.map +0 -1
  98. package/dist/utilities/execute-workflow-graph.js +0 -439
  99. package/dist/utilities/execute-workflow-graph.js.map +0 -1
  100. package/dist/utilities/extract-common-request-config-options.d.ts +0 -3
  101. package/dist/utilities/extract-common-request-config-options.d.ts.map +0 -1
  102. package/dist/utilities/extract-common-request-config-options.js +0 -12
  103. package/dist/utilities/extract-common-request-config-options.js.map +0 -1
  104. package/dist/utilities/fn-exec.d.ts +0 -3
  105. package/dist/utilities/fn-exec.d.ts.map +0 -1
  106. package/dist/utilities/fn-exec.js +0 -66
  107. package/dist/utilities/fn-exec.js.map +0 -1
  108. package/dist/utilities/function-cache-manager.d.ts +0 -32
  109. package/dist/utilities/function-cache-manager.d.ts.map +0 -1
  110. package/dist/utilities/function-cache-manager.js +0 -172
  111. package/dist/utilities/function-cache-manager.js.map +0 -1
  112. package/dist/utilities/prepare-api-function-options.d.ts +0 -3
  113. package/dist/utilities/prepare-api-function-options.d.ts.map +0 -1
  114. package/dist/utilities/prepare-api-function-options.js +0 -51
  115. package/dist/utilities/prepare-api-function-options.js.map +0 -1
  116. package/dist/utilities/prepare-api-request-data.d.ts +0 -3
  117. package/dist/utilities/prepare-api-request-data.d.ts.map +0 -1
  118. package/dist/utilities/prepare-api-request-data.js +0 -15
  119. package/dist/utilities/prepare-api-request-data.js.map +0 -1
  120. package/dist/utilities/prepare-api-request-options.d.ts +0 -3
  121. package/dist/utilities/prepare-api-request-options.d.ts.map +0 -1
  122. package/dist/utilities/prepare-api-request-options.js +0 -22
  123. package/dist/utilities/prepare-api-request-options.js.map +0 -1
  124. package/dist/utilities/rate-limiter.d.ts +0 -49
  125. package/dist/utilities/rate-limiter.d.ts.map +0 -1
  126. package/dist/utilities/rate-limiter.js +0 -197
  127. package/dist/utilities/rate-limiter.js.map +0 -1
  128. package/dist/utilities/validate-workflow-graph.d.ts +0 -7
  129. package/dist/utilities/validate-workflow-graph.d.ts.map +0 -1
  130. package/dist/utilities/validate-workflow-graph.js +0 -235
  131. package/dist/utilities/validate-workflow-graph.js.map +0 -1
  132. package/dist/utilities/workflow-graph-builder.d.ts +0 -37
  133. package/dist/utilities/workflow-graph-builder.d.ts.map +0 -1
  134. package/dist/utilities/workflow-graph-builder.js +0 -225
  135. package/dist/utilities/workflow-graph-builder.js.map +0 -1
@@ -1,782 +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
- 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-request: 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