reqon-dsl 0.2.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 (388) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/.claude/skills/api-integration.md +125 -0
  3. package/.claude/skills/database-schema.md +51 -0
  4. package/.claude/skills/dsl-design.md +80 -0
  5. package/.claude/skills/property-testing.md +143 -0
  6. package/.claude/skills/reqon/SKILL.md +44 -0
  7. package/.claude/skills/reqon/references/examples.md +206 -0
  8. package/.claude/skills/reqon/references/syntax.md +263 -0
  9. package/.claude/skills/vscode-extension.md +113 -0
  10. package/.github/dependabot.yml +32 -0
  11. package/.github/pull_request_template.md +21 -0
  12. package/.github/workflows/ci.yml +174 -0
  13. package/.github/workflows/release.yml +73 -0
  14. package/CLAUDE.md +72 -0
  15. package/CONTRIBUTING.md +161 -0
  16. package/README.md +235 -0
  17. package/TODO.md +51 -0
  18. package/dist/ast/index.d.ts +1 -0
  19. package/dist/ast/index.js +1 -0
  20. package/dist/ast/nodes.d.ts +237 -0
  21. package/dist/ast/nodes.js +12 -0
  22. package/dist/auth/auth.test.d.ts +1 -0
  23. package/dist/auth/auth.test.js +255 -0
  24. package/dist/auth/circuit-breaker.d.ts +115 -0
  25. package/dist/auth/circuit-breaker.js +267 -0
  26. package/dist/auth/credentials.d.ts +91 -0
  27. package/dist/auth/credentials.js +169 -0
  28. package/dist/auth/index.d.ts +5 -0
  29. package/dist/auth/index.js +8 -0
  30. package/dist/auth/oauth2-provider.d.ts +41 -0
  31. package/dist/auth/oauth2-provider.js +131 -0
  32. package/dist/auth/rate-limiter.d.ts +61 -0
  33. package/dist/auth/rate-limiter.js +380 -0
  34. package/dist/auth/token-store.d.ts +30 -0
  35. package/dist/auth/token-store.js +148 -0
  36. package/dist/auth/types.d.ts +142 -0
  37. package/dist/auth/types.js +1 -0
  38. package/dist/cli.d.ts +2 -0
  39. package/dist/cli.js +270 -0
  40. package/dist/errors/errors.test.d.ts +1 -0
  41. package/dist/errors/errors.test.js +165 -0
  42. package/dist/errors/index.d.ts +83 -0
  43. package/dist/errors/index.js +159 -0
  44. package/dist/execution/execution.test.d.ts +1 -0
  45. package/dist/execution/execution.test.js +246 -0
  46. package/dist/execution/index.d.ts +4 -0
  47. package/dist/execution/index.js +2 -0
  48. package/dist/execution/state.d.ts +136 -0
  49. package/dist/execution/state.js +82 -0
  50. package/dist/execution/store.d.ts +52 -0
  51. package/dist/execution/store.js +120 -0
  52. package/dist/index.d.ts +27 -0
  53. package/dist/index.js +57 -0
  54. package/dist/integration.test.d.ts +1 -0
  55. package/dist/integration.test.js +168 -0
  56. package/dist/interpreter/context.d.ts +15 -0
  57. package/dist/interpreter/context.js +29 -0
  58. package/dist/interpreter/evaluator.d.ts +5 -0
  59. package/dist/interpreter/evaluator.js +223 -0
  60. package/dist/interpreter/evaluator.test.d.ts +1 -0
  61. package/dist/interpreter/evaluator.test.js +512 -0
  62. package/dist/interpreter/executor.d.ts +131 -0
  63. package/dist/interpreter/executor.js +663 -0
  64. package/dist/interpreter/fetch-handler.d.ts +43 -0
  65. package/dist/interpreter/fetch-handler.js +203 -0
  66. package/dist/interpreter/http.d.ts +57 -0
  67. package/dist/interpreter/http.js +210 -0
  68. package/dist/interpreter/http.test.d.ts +1 -0
  69. package/dist/interpreter/http.test.js +299 -0
  70. package/dist/interpreter/index.d.ts +7 -0
  71. package/dist/interpreter/index.js +7 -0
  72. package/dist/interpreter/pagination.d.ts +63 -0
  73. package/dist/interpreter/pagination.js +155 -0
  74. package/dist/interpreter/progress.test.d.ts +1 -0
  75. package/dist/interpreter/progress.test.js +216 -0
  76. package/dist/interpreter/schema-matcher.d.ts +16 -0
  77. package/dist/interpreter/schema-matcher.js +136 -0
  78. package/dist/interpreter/schema-matcher.test.d.ts +1 -0
  79. package/dist/interpreter/schema-matcher.test.js +122 -0
  80. package/dist/interpreter/signals.d.ts +57 -0
  81. package/dist/interpreter/signals.js +73 -0
  82. package/dist/interpreter/step-handlers/for-handler.d.ts +17 -0
  83. package/dist/interpreter/step-handlers/for-handler.js +51 -0
  84. package/dist/interpreter/step-handlers/index.d.ts +8 -0
  85. package/dist/interpreter/step-handlers/index.js +8 -0
  86. package/dist/interpreter/step-handlers/map-handler.d.ts +10 -0
  87. package/dist/interpreter/step-handlers/map-handler.js +20 -0
  88. package/dist/interpreter/step-handlers/match-handler.d.ts +27 -0
  89. package/dist/interpreter/step-handlers/match-handler.js +61 -0
  90. package/dist/interpreter/step-handlers/store-handler.d.ts +13 -0
  91. package/dist/interpreter/step-handlers/store-handler.js +66 -0
  92. package/dist/interpreter/step-handlers/types.d.ts +15 -0
  93. package/dist/interpreter/step-handlers/types.js +1 -0
  94. package/dist/interpreter/step-handlers/validate-handler.d.ts +10 -0
  95. package/dist/interpreter/step-handlers/validate-handler.js +26 -0
  96. package/dist/interpreter/step-handlers/webhook-handler.d.ts +36 -0
  97. package/dist/interpreter/step-handlers/webhook-handler.js +104 -0
  98. package/dist/lexer/index.d.ts +10 -0
  99. package/dist/lexer/index.js +12 -0
  100. package/dist/lexer/lexer.d.ts +24 -0
  101. package/dist/lexer/lexer.js +264 -0
  102. package/dist/lexer/lexer.test.d.ts +1 -0
  103. package/dist/lexer/lexer.test.js +259 -0
  104. package/dist/lexer/tokens.d.ts +69 -0
  105. package/dist/lexer/tokens.js +146 -0
  106. package/dist/loader/index.d.ts +36 -0
  107. package/dist/loader/index.js +220 -0
  108. package/dist/loader/loader.test.d.ts +1 -0
  109. package/dist/loader/loader.test.js +287 -0
  110. package/dist/oas/index.d.ts +4 -0
  111. package/dist/oas/index.js +2 -0
  112. package/dist/oas/loader.d.ts +21 -0
  113. package/dist/oas/loader.js +82 -0
  114. package/dist/oas/oas.test.d.ts +1 -0
  115. package/dist/oas/oas.test.js +218 -0
  116. package/dist/oas/validator.d.ts +12 -0
  117. package/dist/oas/validator.js +227 -0
  118. package/dist/parser/base.d.ts +33 -0
  119. package/dist/parser/base.js +97 -0
  120. package/dist/parser/expressions.d.ts +27 -0
  121. package/dist/parser/expressions.js +248 -0
  122. package/dist/parser/expressions.test.d.ts +1 -0
  123. package/dist/parser/expressions.test.js +378 -0
  124. package/dist/parser/index.d.ts +3 -0
  125. package/dist/parser/index.js +3 -0
  126. package/dist/parser/match.test.d.ts +1 -0
  127. package/dist/parser/match.test.js +254 -0
  128. package/dist/parser/parser.d.ts +68 -0
  129. package/dist/parser/parser.js +1229 -0
  130. package/dist/parser/parser.test.d.ts +1 -0
  131. package/dist/parser/parser.test.js +333 -0
  132. package/dist/parser/schedule.test.d.ts +1 -0
  133. package/dist/parser/schedule.test.js +241 -0
  134. package/dist/plugin.d.ts +35 -0
  135. package/dist/plugin.js +68 -0
  136. package/dist/scheduler/cron-parser.d.ts +32 -0
  137. package/dist/scheduler/cron-parser.js +198 -0
  138. package/dist/scheduler/cron-parser.test.d.ts +1 -0
  139. package/dist/scheduler/cron-parser.test.js +188 -0
  140. package/dist/scheduler/index.d.ts +3 -0
  141. package/dist/scheduler/index.js +2 -0
  142. package/dist/scheduler/scheduler.d.ts +81 -0
  143. package/dist/scheduler/scheduler.js +376 -0
  144. package/dist/scheduler/types.d.ts +65 -0
  145. package/dist/scheduler/types.js +1 -0
  146. package/dist/stores/factory.d.ts +36 -0
  147. package/dist/stores/factory.js +73 -0
  148. package/dist/stores/file.d.ts +60 -0
  149. package/dist/stores/file.js +173 -0
  150. package/dist/stores/file.test.d.ts +1 -0
  151. package/dist/stores/file.test.js +165 -0
  152. package/dist/stores/index.d.ts +6 -0
  153. package/dist/stores/index.js +5 -0
  154. package/dist/stores/memory.d.ts +19 -0
  155. package/dist/stores/memory.js +51 -0
  156. package/dist/stores/memory.test.d.ts +1 -0
  157. package/dist/stores/memory.test.js +157 -0
  158. package/dist/stores/postgrest.d.ts +55 -0
  159. package/dist/stores/postgrest.js +217 -0
  160. package/dist/stores/stores.test.d.ts +1 -0
  161. package/dist/stores/stores.test.js +158 -0
  162. package/dist/stores/types.d.ts +31 -0
  163. package/dist/stores/types.js +26 -0
  164. package/dist/sync/index.d.ts +4 -0
  165. package/dist/sync/index.js +2 -0
  166. package/dist/sync/state.d.ts +69 -0
  167. package/dist/sync/state.js +66 -0
  168. package/dist/sync/store.d.ts +49 -0
  169. package/dist/sync/store.js +93 -0
  170. package/dist/sync/sync.test.d.ts +1 -0
  171. package/dist/sync/sync.test.js +221 -0
  172. package/dist/utils/async.d.ts +7 -0
  173. package/dist/utils/async.js +9 -0
  174. package/dist/utils/file.d.ts +38 -0
  175. package/dist/utils/file.js +92 -0
  176. package/dist/utils/index.d.ts +4 -0
  177. package/dist/utils/index.js +4 -0
  178. package/dist/utils/logger.d.ts +34 -0
  179. package/dist/utils/logger.js +39 -0
  180. package/dist/utils/path.d.ts +12 -0
  181. package/dist/utils/path.js +41 -0
  182. package/dist/webhook/index.d.ts +8 -0
  183. package/dist/webhook/index.js +7 -0
  184. package/dist/webhook/server.d.ts +84 -0
  185. package/dist/webhook/server.js +319 -0
  186. package/dist/webhook/store.d.ts +67 -0
  187. package/dist/webhook/store.js +193 -0
  188. package/dist/webhook/types.d.ts +88 -0
  189. package/dist/webhook/types.js +6 -0
  190. package/docusaurus/README.md +41 -0
  191. package/docusaurus/docs/advanced/execution-state.md +283 -0
  192. package/docusaurus/docs/advanced/extending-reqon.md +388 -0
  193. package/docusaurus/docs/advanced/multi-file-missions.md +250 -0
  194. package/docusaurus/docs/advanced/parallel-execution.md +353 -0
  195. package/docusaurus/docs/api-reference.md +443 -0
  196. package/docusaurus/docs/authentication/api-key.md +339 -0
  197. package/docusaurus/docs/authentication/basic.md +276 -0
  198. package/docusaurus/docs/authentication/bearer.md +282 -0
  199. package/docusaurus/docs/authentication/oauth2.md +317 -0
  200. package/docusaurus/docs/authentication/overview.md +251 -0
  201. package/docusaurus/docs/cli.md +229 -0
  202. package/docusaurus/docs/core-concepts/actions.md +286 -0
  203. package/docusaurus/docs/core-concepts/missions.md +264 -0
  204. package/docusaurus/docs/core-concepts/schemas.md +353 -0
  205. package/docusaurus/docs/core-concepts/sources.md +339 -0
  206. package/docusaurus/docs/core-concepts/stores.md +332 -0
  207. package/docusaurus/docs/dsl-syntax/expressions.md +361 -0
  208. package/docusaurus/docs/dsl-syntax/fetch.md +293 -0
  209. package/docusaurus/docs/dsl-syntax/for-loops.md +324 -0
  210. package/docusaurus/docs/dsl-syntax/map.md +345 -0
  211. package/docusaurus/docs/dsl-syntax/match.md +387 -0
  212. package/docusaurus/docs/dsl-syntax/pipelines.md +397 -0
  213. package/docusaurus/docs/dsl-syntax/validate.md +401 -0
  214. package/docusaurus/docs/error-handling/dead-letter-queues.md +399 -0
  215. package/docusaurus/docs/error-handling/flow-control.md +337 -0
  216. package/docusaurus/docs/error-handling/retry-strategies.md +368 -0
  217. package/docusaurus/docs/examples.md +488 -0
  218. package/docusaurus/docs/getting-started.md +256 -0
  219. package/docusaurus/docs/http/circuit-breaker.md +401 -0
  220. package/docusaurus/docs/http/incremental-sync.md +394 -0
  221. package/docusaurus/docs/http/pagination.md +361 -0
  222. package/docusaurus/docs/http/rate-limiting.md +383 -0
  223. package/docusaurus/docs/http/requests.md +328 -0
  224. package/docusaurus/docs/http/retry.md +402 -0
  225. package/docusaurus/docs/intro.md +90 -0
  226. package/docusaurus/docs/openapi/loading-specs.md +305 -0
  227. package/docusaurus/docs/openapi/operation-calls.md +314 -0
  228. package/docusaurus/docs/openapi/overview.md +212 -0
  229. package/docusaurus/docs/openapi/response-validation.md +344 -0
  230. package/docusaurus/docs/scheduling/cron.md +305 -0
  231. package/docusaurus/docs/scheduling/daemon-mode.md +317 -0
  232. package/docusaurus/docs/scheduling/intervals.md +289 -0
  233. package/docusaurus/docs/scheduling/overview.md +231 -0
  234. package/docusaurus/docs/stores/custom-adapters.md +376 -0
  235. package/docusaurus/docs/stores/file.md +236 -0
  236. package/docusaurus/docs/stores/memory.md +193 -0
  237. package/docusaurus/docs/stores/overview.md +274 -0
  238. package/docusaurus/docs/stores/postgrest.md +316 -0
  239. package/docusaurus/docusaurus.config.ts +148 -0
  240. package/docusaurus/package-lock.json +18029 -0
  241. package/docusaurus/package.json +47 -0
  242. package/docusaurus/sidebars.ts +155 -0
  243. package/docusaurus/src/components/HomepageFeatures/index.tsx +105 -0
  244. package/docusaurus/src/components/HomepageFeatures/styles.module.css +12 -0
  245. package/docusaurus/src/css/custom.css +169 -0
  246. package/docusaurus/src/pages/index.module.css +48 -0
  247. package/docusaurus/src/pages/index.tsx +110 -0
  248. package/docusaurus/src/pages/markdown-page.md +7 -0
  249. package/docusaurus/static/.nojekyll +0 -0
  250. package/docusaurus/static/img/docusaurus-social-card.jpg +0 -0
  251. package/docusaurus/static/img/docusaurus.png +0 -0
  252. package/docusaurus/static/img/favicon.ico +0 -0
  253. package/docusaurus/static/img/logo.svg +10 -0
  254. package/docusaurus/static/img/undraw_docusaurus_mountain.svg +171 -0
  255. package/docusaurus/static/img/undraw_docusaurus_react.svg +170 -0
  256. package/docusaurus/static/img/undraw_docusaurus_tree.svg +40 -0
  257. package/docusaurus/tsconfig.json +8 -0
  258. package/examples/README.md +112 -0
  259. package/examples/error-handling/README.md +150 -0
  260. package/examples/error-handling/payment-processor.vague +287 -0
  261. package/examples/github-sync/README.md +74 -0
  262. package/examples/github-sync/fetch-issues.vague +47 -0
  263. package/examples/github-sync/fetch-prs.vague +40 -0
  264. package/examples/github-sync/mission.vague +101 -0
  265. package/examples/github-sync/normalize.vague +70 -0
  266. package/examples/jsonplaceholder/README.md +28 -0
  267. package/examples/jsonplaceholder/posts.vague +48 -0
  268. package/examples/petstore/README.md +35 -0
  269. package/examples/petstore/openapi.yaml +97 -0
  270. package/examples/petstore/sync.vague +52 -0
  271. package/examples/temporal-comparison/README.md +297 -0
  272. package/examples/temporal-comparison/reconciliation.vague +355 -0
  273. package/examples/temporal-comparison/temporal/activities/index.ts +8 -0
  274. package/examples/temporal-comparison/temporal/activities/shipstation.ts +225 -0
  275. package/examples/temporal-comparison/temporal/activities/shopify.ts +257 -0
  276. package/examples/temporal-comparison/temporal/activities/storage.ts +198 -0
  277. package/examples/temporal-comparison/temporal/activities/stripe.ts +169 -0
  278. package/examples/temporal-comparison/temporal/activities/validation.ts +205 -0
  279. package/examples/temporal-comparison/temporal/client/schedule.ts +218 -0
  280. package/examples/temporal-comparison/temporal/config/retry.ts +63 -0
  281. package/examples/temporal-comparison/temporal/types/index.ts +129 -0
  282. package/examples/temporal-comparison/temporal/workers/main.ts +130 -0
  283. package/examples/temporal-comparison/temporal/workflows/orderReconciliation.ts +262 -0
  284. package/examples/xero/README.md +88 -0
  285. package/examples/xero/invoices.vague +189 -0
  286. package/package.json +40 -0
  287. package/src/api-integration.test.ts +954 -0
  288. package/src/ast/index.ts +1 -0
  289. package/src/ast/nodes.ts +310 -0
  290. package/src/auth/auth.test.ts +326 -0
  291. package/src/auth/circuit-breaker.test.ts +390 -0
  292. package/src/auth/circuit-breaker.ts +379 -0
  293. package/src/auth/credentials.test.ts +273 -0
  294. package/src/auth/credentials.ts +246 -0
  295. package/src/auth/index.ts +40 -0
  296. package/src/auth/oauth2-provider.ts +177 -0
  297. package/src/auth/rate-limiter.ts +459 -0
  298. package/src/auth/token-store.ts +177 -0
  299. package/src/auth/types.ts +159 -0
  300. package/src/benchmark/e2e.bench.ts +288 -0
  301. package/src/benchmark/evaluator.bench.ts +331 -0
  302. package/src/benchmark/fixtures.ts +295 -0
  303. package/src/benchmark/index.ts +108 -0
  304. package/src/benchmark/lexer.bench.ts +69 -0
  305. package/src/benchmark/parser.bench.ts +103 -0
  306. package/src/benchmark/resilience.bench.ts +193 -0
  307. package/src/benchmark/store.bench.ts +147 -0
  308. package/src/benchmark/utils.ts +230 -0
  309. package/src/cli.ts +313 -0
  310. package/src/errors/errors.test.ts +234 -0
  311. package/src/errors/index.ts +223 -0
  312. package/src/execution/execution.test.ts +307 -0
  313. package/src/execution/index.ts +21 -0
  314. package/src/execution/state.ts +207 -0
  315. package/src/execution/store.ts +188 -0
  316. package/src/index.ts +169 -0
  317. package/src/integration.test.ts +192 -0
  318. package/src/interpreter/context.ts +57 -0
  319. package/src/interpreter/evaluator.test.ts +796 -0
  320. package/src/interpreter/evaluator.ts +245 -0
  321. package/src/interpreter/executor.ts +946 -0
  322. package/src/interpreter/fetch-handler.ts +302 -0
  323. package/src/interpreter/http.test.ts +423 -0
  324. package/src/interpreter/http.ts +308 -0
  325. package/src/interpreter/index.ts +32 -0
  326. package/src/interpreter/pagination.ts +207 -0
  327. package/src/interpreter/progress.test.ts +276 -0
  328. package/src/interpreter/schema-matcher.test.ts +160 -0
  329. package/src/interpreter/schema-matcher.ts +168 -0
  330. package/src/interpreter/signals.ts +73 -0
  331. package/src/interpreter/step-handlers/for-handler.ts +65 -0
  332. package/src/interpreter/step-handlers/index.ts +17 -0
  333. package/src/interpreter/step-handlers/map-handler.ts +24 -0
  334. package/src/interpreter/step-handlers/match-handler.ts +101 -0
  335. package/src/interpreter/step-handlers/store-handler.ts +78 -0
  336. package/src/interpreter/step-handlers/types.ts +17 -0
  337. package/src/interpreter/step-handlers/validate-handler.ts +30 -0
  338. package/src/interpreter/step-handlers/webhook-handler.ts +142 -0
  339. package/src/lexer/index.ts +18 -0
  340. package/src/lexer/lexer.test.ts +316 -0
  341. package/src/lexer/tokens.ts +179 -0
  342. package/src/loader/index.ts +288 -0
  343. package/src/loader/loader.test.ts +360 -0
  344. package/src/oas/index.ts +4 -0
  345. package/src/oas/loader.ts +126 -0
  346. package/src/oas/oas.test.ts +254 -0
  347. package/src/oas/validator.ts +299 -0
  348. package/src/parser/base.ts +124 -0
  349. package/src/parser/expressions.test.ts +525 -0
  350. package/src/parser/expressions.ts +314 -0
  351. package/src/parser/index.ts +3 -0
  352. package/src/parser/match.test.ts +296 -0
  353. package/src/parser/parser.test.ts +739 -0
  354. package/src/parser/parser.ts +1469 -0
  355. package/src/parser/schedule.test.ts +287 -0
  356. package/src/parser/webhook.test.ts +248 -0
  357. package/src/plugin.ts +83 -0
  358. package/src/scheduler/cron-parser.test.ts +236 -0
  359. package/src/scheduler/cron-parser.ts +236 -0
  360. package/src/scheduler/index.ts +10 -0
  361. package/src/scheduler/scheduler.ts +443 -0
  362. package/src/scheduler/types.ts +71 -0
  363. package/src/stores/factory.ts +104 -0
  364. package/src/stores/file.test.ts +276 -0
  365. package/src/stores/file.ts +211 -0
  366. package/src/stores/index.ts +6 -0
  367. package/src/stores/memory.test.ts +238 -0
  368. package/src/stores/memory.ts +63 -0
  369. package/src/stores/postgrest.test.ts +488 -0
  370. package/src/stores/postgrest.ts +263 -0
  371. package/src/stores/stores.test.ts +197 -0
  372. package/src/stores/types.ts +58 -0
  373. package/src/sync/index.ts +16 -0
  374. package/src/sync/state.ts +126 -0
  375. package/src/sync/store.ts +139 -0
  376. package/src/sync/sync.test.ts +271 -0
  377. package/src/utils/async.ts +10 -0
  378. package/src/utils/file.ts +106 -0
  379. package/src/utils/index.ts +14 -0
  380. package/src/utils/logger.ts +53 -0
  381. package/src/utils/path.ts +47 -0
  382. package/src/webhook/index.ts +15 -0
  383. package/src/webhook/server.test.ts +253 -0
  384. package/src/webhook/server.ts +389 -0
  385. package/src/webhook/store.ts +239 -0
  386. package/src/webhook/types.ts +93 -0
  387. package/tsconfig.json +17 -0
  388. package/vitest.config.ts +39 -0
@@ -0,0 +1,236 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ parseCronExpression,
4
+ getNextCronRun,
5
+ intervalToMs,
6
+ getNextRunTime,
7
+ shouldRunNow,
8
+ } from './cron-parser.js';
9
+ import type { ScheduleDefinition, IntervalSchedule } from '../ast/nodes.js';
10
+
11
+ describe('parseCronExpression', () => {
12
+ it('should parse simple cron expressions', () => {
13
+ const schedule = parseCronExpression('0 * * * *');
14
+ expect(schedule.minute).toEqual([0]);
15
+ expect(schedule.hour).toHaveLength(24); // All hours
16
+ expect(schedule.dayOfMonth).toHaveLength(31);
17
+ expect(schedule.month).toHaveLength(12);
18
+ expect(schedule.dayOfWeek).toHaveLength(7);
19
+ });
20
+
21
+ it('should parse specific values', () => {
22
+ const schedule = parseCronExpression('30 9 15 6 1');
23
+ expect(schedule.minute).toEqual([30]);
24
+ expect(schedule.hour).toEqual([9]);
25
+ expect(schedule.dayOfMonth).toEqual([15]);
26
+ expect(schedule.month).toEqual([6]);
27
+ expect(schedule.dayOfWeek).toEqual([1]);
28
+ });
29
+
30
+ it('should parse ranges', () => {
31
+ const schedule = parseCronExpression('0-5 9-17 * * 1-5');
32
+ expect(schedule.minute).toEqual([0, 1, 2, 3, 4, 5]);
33
+ expect(schedule.hour).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]);
34
+ expect(schedule.dayOfWeek).toEqual([1, 2, 3, 4, 5]);
35
+ });
36
+
37
+ it('should parse step values', () => {
38
+ const schedule = parseCronExpression('*/15 */6 * * *');
39
+ expect(schedule.minute).toEqual([0, 15, 30, 45]);
40
+ expect(schedule.hour).toEqual([0, 6, 12, 18]);
41
+ });
42
+
43
+ it('should parse lists', () => {
44
+ const schedule = parseCronExpression('0,30 9,12,18 * * *');
45
+ expect(schedule.minute).toEqual([0, 30]);
46
+ expect(schedule.hour).toEqual([9, 12, 18]);
47
+ });
48
+
49
+ it('should parse combined expressions', () => {
50
+ const schedule = parseCronExpression('0 9-17/2 * * 1-5');
51
+ expect(schedule.minute).toEqual([0]);
52
+ expect(schedule.hour).toEqual([9, 11, 13, 15, 17]);
53
+ expect(schedule.dayOfWeek).toEqual([1, 2, 3, 4, 5]);
54
+ });
55
+
56
+ it('should throw on invalid cron expression', () => {
57
+ expect(() => parseCronExpression('0 * *')).toThrow('expected 5 fields');
58
+ });
59
+
60
+ it('should throw on out of range values', () => {
61
+ expect(() => parseCronExpression('60 * * * *')).toThrow('out of range');
62
+ expect(() => parseCronExpression('* 25 * * *')).toThrow('out of range');
63
+ });
64
+ });
65
+
66
+ describe('getNextCronRun', () => {
67
+ it('should calculate next run for every minute', () => {
68
+ const schedule = parseCronExpression('* * * * *');
69
+ const now = new Date('2025-01-20T10:30:00Z');
70
+ const next = getNextCronRun(schedule, now);
71
+
72
+ expect(next.getMinutes()).toBe(31);
73
+ expect(next.getHours()).toBe(10);
74
+ });
75
+
76
+ it('should calculate next run for specific time', () => {
77
+ const schedule = parseCronExpression('0 9 * * *');
78
+ const now = new Date('2025-01-20T10:30:00Z');
79
+ const next = getNextCronRun(schedule, now);
80
+
81
+ // Should be 9:00 the next day
82
+ expect(next.getMinutes()).toBe(0);
83
+ expect(next.getHours()).toBe(9);
84
+ expect(next.getDate()).toBe(21);
85
+ });
86
+
87
+ it('should handle hourly schedules', () => {
88
+ const schedule = parseCronExpression('0 * * * *');
89
+ const now = new Date('2025-01-20T10:30:00Z');
90
+ const next = getNextCronRun(schedule, now);
91
+
92
+ expect(next.getMinutes()).toBe(0);
93
+ expect(next.getHours()).toBe(11);
94
+ });
95
+
96
+ it('should handle day of week constraints', () => {
97
+ const schedule = parseCronExpression('0 9 * * 1'); // Monday only
98
+ const now = new Date('2025-01-20T10:30:00Z'); // This is a Monday
99
+ const next = getNextCronRun(schedule, now);
100
+
101
+ // Should be next Monday at 9:00
102
+ expect(next.getDay()).toBe(1); // Monday
103
+ expect(next.getHours()).toBe(9);
104
+ expect(next.getMinutes()).toBe(0);
105
+ });
106
+ });
107
+
108
+ describe('intervalToMs', () => {
109
+ it('should convert seconds to ms', () => {
110
+ const interval: IntervalSchedule = { value: 30, unit: 'seconds' };
111
+ expect(intervalToMs(interval)).toBe(30_000);
112
+ });
113
+
114
+ it('should convert minutes to ms', () => {
115
+ const interval: IntervalSchedule = { value: 5, unit: 'minutes' };
116
+ expect(intervalToMs(interval)).toBe(5 * 60 * 1000);
117
+ });
118
+
119
+ it('should convert hours to ms', () => {
120
+ const interval: IntervalSchedule = { value: 6, unit: 'hours' };
121
+ expect(intervalToMs(interval)).toBe(6 * 60 * 60 * 1000);
122
+ });
123
+
124
+ it('should convert days to ms', () => {
125
+ const interval: IntervalSchedule = { value: 1, unit: 'days' };
126
+ expect(intervalToMs(interval)).toBe(24 * 60 * 60 * 1000);
127
+ });
128
+
129
+ it('should convert weeks to ms', () => {
130
+ const interval: IntervalSchedule = { value: 1, unit: 'weeks' };
131
+ expect(intervalToMs(interval)).toBe(7 * 24 * 60 * 60 * 1000);
132
+ });
133
+ });
134
+
135
+ describe('getNextRunTime', () => {
136
+ it('should calculate next run for interval schedule', () => {
137
+ const schedule: ScheduleDefinition = {
138
+ type: 'ScheduleDefinition',
139
+ scheduleType: 'interval',
140
+ interval: { value: 6, unit: 'hours' },
141
+ };
142
+
143
+ const now = new Date('2025-01-20T10:00:00Z');
144
+ const next = getNextRunTime(schedule, now);
145
+
146
+ expect(next).not.toBeNull();
147
+ expect(next!.getTime()).toBe(now.getTime() + 6 * 60 * 60 * 1000);
148
+ });
149
+
150
+ it('should calculate next run for cron schedule', () => {
151
+ const schedule: ScheduleDefinition = {
152
+ type: 'ScheduleDefinition',
153
+ scheduleType: 'cron',
154
+ cronExpression: '0 */6 * * *',
155
+ };
156
+
157
+ const now = new Date('2025-01-20T10:30:00Z');
158
+ const next = getNextRunTime(schedule, now);
159
+
160
+ expect(next).not.toBeNull();
161
+ expect(next!.getMinutes()).toBe(0);
162
+ expect(next!.getHours()).toBe(12); // Next 6-hour mark
163
+ });
164
+
165
+ it('should calculate next run for one-time schedule', () => {
166
+ const schedule: ScheduleDefinition = {
167
+ type: 'ScheduleDefinition',
168
+ scheduleType: 'once',
169
+ runAt: '2025-01-25T15:00:00Z',
170
+ };
171
+
172
+ const now = new Date('2025-01-20T10:00:00Z');
173
+ const next = getNextRunTime(schedule, now);
174
+
175
+ expect(next).not.toBeNull();
176
+ expect(next!.toISOString()).toBe('2025-01-25T15:00:00.000Z');
177
+ });
178
+
179
+ it('should return null for past one-time schedule', () => {
180
+ const schedule: ScheduleDefinition = {
181
+ type: 'ScheduleDefinition',
182
+ scheduleType: 'once',
183
+ runAt: '2025-01-15T15:00:00Z',
184
+ };
185
+
186
+ const now = new Date('2025-01-20T10:00:00Z');
187
+ const next = getNextRunTime(schedule, now);
188
+
189
+ expect(next).toBeNull();
190
+ });
191
+ });
192
+
193
+ describe('shouldRunNow', () => {
194
+ it('should return true for interval schedule that is due', () => {
195
+ const schedule: ScheduleDefinition = {
196
+ type: 'ScheduleDefinition',
197
+ scheduleType: 'interval',
198
+ interval: { value: 1, unit: 'hours' },
199
+ };
200
+
201
+ const lastRun = new Date(Date.now() - 61 * 60 * 1000); // 61 minutes ago
202
+ expect(shouldRunNow(schedule, lastRun)).toBe(true);
203
+ });
204
+
205
+ it('should return false for interval schedule not yet due', () => {
206
+ const schedule: ScheduleDefinition = {
207
+ type: 'ScheduleDefinition',
208
+ scheduleType: 'interval',
209
+ interval: { value: 1, unit: 'hours' },
210
+ };
211
+
212
+ const lastRun = new Date(Date.now() - 30 * 60 * 1000); // 30 minutes ago
213
+ expect(shouldRunNow(schedule, lastRun)).toBe(false);
214
+ });
215
+
216
+ it('should return true for first run of interval schedule', () => {
217
+ const schedule: ScheduleDefinition = {
218
+ type: 'ScheduleDefinition',
219
+ scheduleType: 'interval',
220
+ interval: { value: 1, unit: 'hours' },
221
+ };
222
+
223
+ expect(shouldRunNow(schedule, undefined)).toBe(true);
224
+ });
225
+
226
+ it('should return false for one-time schedule that already ran', () => {
227
+ const schedule: ScheduleDefinition = {
228
+ type: 'ScheduleDefinition',
229
+ scheduleType: 'once',
230
+ runAt: '2025-01-15T15:00:00Z',
231
+ };
232
+
233
+ const lastRun = new Date('2025-01-15T15:00:00Z');
234
+ expect(shouldRunNow(schedule, lastRun)).toBe(false);
235
+ });
236
+ });
@@ -0,0 +1,236 @@
1
+ import type { ScheduleDefinition, IntervalSchedule } from '../ast/nodes.js';
2
+
3
+ /**
4
+ * Parse a cron expression and calculate the next run time
5
+ *
6
+ * Cron format: "minute hour day-of-month month day-of-week"
7
+ * Supports: numbers, ranges (1-5), steps (*​/5), lists (1,3,5), and wildcards (*)
8
+ */
9
+ export function parseCronExpression(expression: string): CronSchedule {
10
+ const parts = expression.trim().split(/\s+/);
11
+
12
+ if (parts.length !== 5) {
13
+ throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`);
14
+ }
15
+
16
+ return {
17
+ minute: parseField(parts[0], 0, 59),
18
+ hour: parseField(parts[1], 0, 23),
19
+ dayOfMonth: parseField(parts[2], 1, 31),
20
+ month: parseField(parts[3], 1, 12),
21
+ dayOfWeek: parseField(parts[4], 0, 6), // 0 = Sunday
22
+ };
23
+ }
24
+
25
+ interface CronSchedule {
26
+ minute: number[];
27
+ hour: number[];
28
+ dayOfMonth: number[];
29
+ month: number[];
30
+ dayOfWeek: number[];
31
+ }
32
+
33
+ function parseField(field: string, min: number, max: number): number[] {
34
+ const values: Set<number> = new Set();
35
+
36
+ for (const part of field.split(',')) {
37
+ if (part === '*') {
38
+ // All values
39
+ for (let i = min; i <= max; i++) {
40
+ values.add(i);
41
+ }
42
+ } else if (part.includes('/')) {
43
+ // Step values (e.g., */5 or 1-10/2)
44
+ const [range, stepStr] = part.split('/');
45
+ const step = parseInt(stepStr, 10);
46
+
47
+ let start = min;
48
+ let end = max;
49
+
50
+ if (range !== '*') {
51
+ if (range.includes('-')) {
52
+ const [rangeStart, rangeEnd] = range.split('-').map((n) => parseInt(n, 10));
53
+ start = rangeStart;
54
+ end = rangeEnd;
55
+ } else {
56
+ start = parseInt(range, 10);
57
+ }
58
+ }
59
+
60
+ for (let i = start; i <= end; i += step) {
61
+ values.add(i);
62
+ }
63
+ } else if (part.includes('-')) {
64
+ // Range (e.g., 1-5)
65
+ const [start, end] = part.split('-').map((n) => parseInt(n, 10));
66
+ for (let i = start; i <= end; i++) {
67
+ values.add(i);
68
+ }
69
+ } else {
70
+ // Single value
71
+ values.add(parseInt(part, 10));
72
+ }
73
+ }
74
+
75
+ // Validate all values are in range
76
+ for (const value of values) {
77
+ if (value < min || value > max) {
78
+ throw new Error(`Cron field value ${value} out of range [${min}, ${max}]`);
79
+ }
80
+ }
81
+
82
+ return Array.from(values).sort((a, b) => a - b);
83
+ }
84
+
85
+ /**
86
+ * Calculate the next run time for a cron schedule
87
+ */
88
+ export function getNextCronRun(schedule: CronSchedule, after: Date = new Date()): Date {
89
+ const next = new Date(after);
90
+ next.setSeconds(0, 0);
91
+ next.setMinutes(next.getMinutes() + 1); // Start from next minute
92
+
93
+ // Try up to 4 years to find a match
94
+ const maxIterations = 4 * 366 * 24 * 60;
95
+
96
+ for (let i = 0; i < maxIterations; i++) {
97
+ // Check month
98
+ if (!schedule.month.includes(next.getMonth() + 1)) {
99
+ // Move to first day of next matching month
100
+ next.setMonth(next.getMonth() + 1);
101
+ next.setDate(1);
102
+ next.setHours(0, 0, 0, 0);
103
+ continue;
104
+ }
105
+
106
+ // Check day of month
107
+ if (!schedule.dayOfMonth.includes(next.getDate())) {
108
+ next.setDate(next.getDate() + 1);
109
+ next.setHours(0, 0, 0, 0);
110
+ continue;
111
+ }
112
+
113
+ // Check day of week
114
+ if (!schedule.dayOfWeek.includes(next.getDay())) {
115
+ next.setDate(next.getDate() + 1);
116
+ next.setHours(0, 0, 0, 0);
117
+ continue;
118
+ }
119
+
120
+ // Check hour
121
+ if (!schedule.hour.includes(next.getHours())) {
122
+ next.setHours(next.getHours() + 1);
123
+ next.setMinutes(0, 0, 0);
124
+ continue;
125
+ }
126
+
127
+ // Check minute
128
+ if (!schedule.minute.includes(next.getMinutes())) {
129
+ next.setMinutes(next.getMinutes() + 1);
130
+ continue;
131
+ }
132
+
133
+ // Found a match!
134
+ return next;
135
+ }
136
+
137
+ throw new Error('Could not find next cron run time within 4 years');
138
+ }
139
+
140
+ /**
141
+ * Convert interval schedule to milliseconds
142
+ */
143
+ export function intervalToMs(interval: IntervalSchedule): number {
144
+ const multipliers: Record<IntervalSchedule['unit'], number> = {
145
+ seconds: 1000,
146
+ minutes: 60 * 1000,
147
+ hours: 60 * 60 * 1000,
148
+ days: 24 * 60 * 60 * 1000,
149
+ weeks: 7 * 24 * 60 * 60 * 1000,
150
+ };
151
+
152
+ return interval.value * multipliers[interval.unit];
153
+ }
154
+
155
+ /**
156
+ * Calculate next run time based on schedule definition
157
+ */
158
+ export function getNextRunTime(schedule: ScheduleDefinition, after: Date = new Date()): Date | null {
159
+ switch (schedule.scheduleType) {
160
+ case 'interval': {
161
+ if (!schedule.interval) {
162
+ throw new Error('Interval schedule missing interval configuration');
163
+ }
164
+ const intervalMs = intervalToMs(schedule.interval);
165
+ return new Date(after.getTime() + intervalMs);
166
+ }
167
+
168
+ case 'cron': {
169
+ if (!schedule.cronExpression) {
170
+ throw new Error('Cron schedule missing cron expression');
171
+ }
172
+ const cronSchedule = parseCronExpression(schedule.cronExpression);
173
+ return getNextCronRun(cronSchedule, after);
174
+ }
175
+
176
+ case 'once': {
177
+ if (!schedule.runAt) {
178
+ throw new Error('One-time schedule missing runAt datetime');
179
+ }
180
+ const runAt = new Date(schedule.runAt);
181
+ // If the scheduled time is in the past, return null (job should not run)
182
+ if (runAt <= after) {
183
+ return null;
184
+ }
185
+ return runAt;
186
+ }
187
+
188
+ default:
189
+ throw new Error(`Unknown schedule type: ${schedule.scheduleType}`);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Check if a schedule should run now (within the check interval)
195
+ */
196
+ export function shouldRunNow(
197
+ schedule: ScheduleDefinition,
198
+ lastRun: Date | undefined,
199
+ checkIntervalMs: number = 1000
200
+ ): boolean {
201
+ const now = new Date();
202
+
203
+ switch (schedule.scheduleType) {
204
+ case 'interval': {
205
+ if (!lastRun) return true; // Never run before, run now
206
+ if (!schedule.interval) return false;
207
+
208
+ const intervalMs = intervalToMs(schedule.interval);
209
+ const elapsed = now.getTime() - lastRun.getTime();
210
+ return elapsed >= intervalMs;
211
+ }
212
+
213
+ case 'cron': {
214
+ if (!schedule.cronExpression) return false;
215
+
216
+ const cronSchedule = parseCronExpression(schedule.cronExpression);
217
+ const nextRun = getNextCronRun(cronSchedule, lastRun ?? new Date(0));
218
+
219
+ // Check if we're within the check interval of the next run time
220
+ const diff = Math.abs(now.getTime() - nextRun.getTime());
221
+ return diff <= checkIntervalMs;
222
+ }
223
+
224
+ case 'once': {
225
+ if (!schedule.runAt) return false;
226
+ if (lastRun) return false; // Already ran
227
+
228
+ const runAt = new Date(schedule.runAt);
229
+ const diff = Math.abs(now.getTime() - runAt.getTime());
230
+ return diff <= checkIntervalMs;
231
+ }
232
+
233
+ default:
234
+ return false;
235
+ }
236
+ }
@@ -0,0 +1,10 @@
1
+ export { Scheduler } from './scheduler.js';
2
+ export { parseCronExpression, getNextRunTime, intervalToMs, shouldRunNow } from './cron-parser.js';
3
+ export type {
4
+ ScheduledJob,
5
+ SchedulerState,
6
+ ScheduleEvent,
7
+ SchedulerCallbacks,
8
+ SchedulerConfig,
9
+ ScheduledMission,
10
+ } from './types.js';