payment-kit 1.29.1 → 1.29.3

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 (343) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +47 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +41 -37
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/crons/tenant-fanout.ts +82 -0
  12. package/api/src/host-node/did-connect-runtime-node.ts +33 -0
  13. package/api/src/host-node/serve-static-arc.ts +68 -0
  14. package/api/src/host-node/serve-static.ts +41 -0
  15. package/api/src/index.ts +22 -161
  16. package/api/src/integrations/app-store/client.ts +3 -4
  17. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  18. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  19. package/api/src/integrations/arcblock/token.ts +21 -7
  20. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  21. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  22. package/api/src/integrations/google-play/verify.ts +3 -2
  23. package/api/src/integrations/iap-reconcile.ts +3 -5
  24. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  25. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  26. package/api/src/libs/archive/query.ts +19 -0
  27. package/api/src/libs/audit.ts +61 -4
  28. package/api/src/libs/auth.ts +247 -47
  29. package/api/src/libs/context.ts +89 -1
  30. package/api/src/libs/currency.ts +2 -2
  31. package/api/src/libs/dayjs.ts +8 -2
  32. package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
  33. package/api/src/libs/did-connect/tenant-identity.ts +221 -0
  34. package/api/src/libs/drivers/auth-storage.ts +118 -0
  35. package/api/src/libs/drivers/cron.ts +264 -0
  36. package/api/src/libs/drivers/db.ts +170 -0
  37. package/api/src/libs/drivers/identity.ts +142 -0
  38. package/api/src/libs/drivers/index.ts +40 -0
  39. package/api/src/libs/drivers/locks.ts +226 -0
  40. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  41. package/api/src/libs/drivers/queue.ts +104 -0
  42. package/api/src/libs/drivers/secrets.ts +194 -0
  43. package/api/src/libs/env.ts +170 -54
  44. package/api/src/libs/exchange-rate/service.ts +7 -6
  45. package/api/src/libs/http-fetch-adapter.ts +60 -0
  46. package/api/src/libs/invoice.ts +1 -1
  47. package/api/src/libs/lock.ts +51 -47
  48. package/api/src/libs/logger.ts +48 -8
  49. package/api/src/libs/notification/index.ts +1 -1
  50. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  51. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  52. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  53. package/api/src/libs/overdraft-protection.ts +1 -1
  54. package/api/src/libs/payout.ts +1 -1
  55. package/api/src/libs/queue/index.ts +271 -52
  56. package/api/src/libs/queue/runtime.ts +175 -0
  57. package/api/src/libs/resource.ts +3 -3
  58. package/api/src/libs/secrets.ts +38 -0
  59. package/api/src/libs/session.ts +3 -2
  60. package/api/src/libs/subscription.ts +5 -5
  61. package/api/src/libs/tenant.ts +92 -0
  62. package/api/src/libs/url.ts +3 -3
  63. package/api/src/libs/util.ts +21 -13
  64. package/api/src/middlewares/hono/cdn.ts +63 -0
  65. package/api/src/middlewares/hono/context.ts +80 -0
  66. package/api/src/middlewares/hono/csrf.ts +83 -0
  67. package/api/src/middlewares/hono/fallback.ts +194 -0
  68. package/api/src/middlewares/hono/pipeline.ts +73 -0
  69. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  70. package/api/src/middlewares/hono/resource.ts +63 -0
  71. package/api/src/middlewares/hono/security.ts +209 -0
  72. package/api/src/middlewares/hono/session.ts +114 -0
  73. package/api/src/middlewares/hono/xss.ts +61 -0
  74. package/api/src/queues/auto-recharge.ts +12 -10
  75. package/api/src/queues/checkout-session.ts +38 -21
  76. package/api/src/queues/credit-consume.ts +40 -36
  77. package/api/src/queues/credit-grant.ts +25 -18
  78. package/api/src/queues/credit-reconciliation.ts +7 -5
  79. package/api/src/queues/discount-status.ts +9 -6
  80. package/api/src/queues/event.ts +41 -11
  81. package/api/src/queues/exchange-rate-health.ts +49 -30
  82. package/api/src/queues/invoice.ts +18 -15
  83. package/api/src/queues/notification.ts +14 -7
  84. package/api/src/queues/payment.ts +64 -37
  85. package/api/src/queues/payout.ts +37 -21
  86. package/api/src/queues/refund.ts +36 -18
  87. package/api/src/queues/subscription.ts +83 -53
  88. package/api/src/queues/token-transfer.ts +15 -10
  89. package/api/src/queues/usage-record.ts +8 -5
  90. package/api/src/queues/vendors/commission.ts +7 -5
  91. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  92. package/api/src/queues/vendors/fulfillment.ts +4 -2
  93. package/api/src/queues/vendors/return-processor.ts +5 -3
  94. package/api/src/queues/vendors/return-scanner.ts +5 -4
  95. package/api/src/queues/vendors/status-check.ts +10 -7
  96. package/api/src/queues/webhook.ts +60 -32
  97. package/api/src/routes/connect/shared.ts +1 -2
  98. package/api/src/routes/connect/subscribe.ts +3 -3
  99. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  100. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  101. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  102. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  103. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  104. package/api/src/routes/hono/credit-tokens.ts +43 -0
  105. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  106. package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
  107. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  108. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  109. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  110. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  111. package/api/src/routes/hono/exchange-rates.ts +77 -0
  112. package/api/src/routes/hono/index.ts +115 -0
  113. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  114. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  115. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  116. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  117. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  118. package/api/src/routes/hono/meters.ts +288 -0
  119. package/api/src/routes/hono/passports.ts +73 -0
  120. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  121. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  122. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  123. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  124. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  125. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  126. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  127. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  128. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  129. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  130. package/api/src/routes/hono/redirect.ts +24 -0
  131. package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
  132. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  133. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  134. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  135. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  136. package/api/src/routes/hono/tool.ts +69 -0
  137. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  138. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  139. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  140. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  141. package/api/src/service.ts +814 -0
  142. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  143. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  144. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  145. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  146. package/api/src/store/models/auto-recharge-config.ts +22 -10
  147. package/api/src/store/models/checkout-session.ts +15 -14
  148. package/api/src/store/models/coupon.ts +29 -20
  149. package/api/src/store/models/credit-grant.ts +38 -29
  150. package/api/src/store/models/credit-transaction.ts +32 -21
  151. package/api/src/store/models/customer.ts +19 -17
  152. package/api/src/store/models/discount.ts +11 -2
  153. package/api/src/store/models/entitlement-grant.ts +21 -9
  154. package/api/src/store/models/entitlement-product.ts +21 -9
  155. package/api/src/store/models/entitlement.ts +19 -10
  156. package/api/src/store/models/event.ts +18 -9
  157. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  158. package/api/src/store/models/invoice-item.ts +18 -9
  159. package/api/src/store/models/invoice.ts +16 -8
  160. package/api/src/store/models/meter-event.ts +27 -9
  161. package/api/src/store/models/meter.ts +31 -22
  162. package/api/src/store/models/payment-currency.ts +25 -8
  163. package/api/src/store/models/payment-intent.ts +15 -6
  164. package/api/src/store/models/payment-link.ts +15 -6
  165. package/api/src/store/models/payment-method.ts +38 -22
  166. package/api/src/store/models/payment-stat.ts +18 -9
  167. package/api/src/store/models/payout.ts +15 -6
  168. package/api/src/store/models/price-quote.ts +17 -8
  169. package/api/src/store/models/price.ts +24 -12
  170. package/api/src/store/models/pricing-table.ts +29 -20
  171. package/api/src/store/models/product-vendor.ts +20 -10
  172. package/api/src/store/models/product.ts +15 -6
  173. package/api/src/store/models/promotion-code.ts +14 -6
  174. package/api/src/store/models/refund.ts +15 -6
  175. package/api/src/store/models/revenue-snapshot.ts +21 -9
  176. package/api/src/store/models/setting.ts +18 -9
  177. package/api/src/store/models/setup-intent.ts +36 -27
  178. package/api/src/store/models/subscription-item.ts +21 -9
  179. package/api/src/store/models/subscription-schedule.ts +21 -9
  180. package/api/src/store/models/subscription.ts +21 -10
  181. package/api/src/store/models/tax-rate.ts +29 -21
  182. package/api/src/store/models/usage-record.ts +11 -2
  183. package/api/src/store/models/webhook-attempt.ts +18 -9
  184. package/api/src/store/models/webhook-endpoint.ts +18 -9
  185. package/api/src/store/scoped-core.ts +55 -0
  186. package/api/src/store/scoped.ts +247 -0
  187. package/api/src/store/sequelize.ts +82 -23
  188. package/api/src/store/sql-migrations.ts +20 -0
  189. package/api/src/store/tenant-backfill.ts +260 -0
  190. package/api/src/store/tenant-model.ts +124 -0
  191. package/api/src/store/tenant-tables.ts +50 -0
  192. package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
  193. package/api/tests/crons/tenant-fanout.spec.ts +158 -0
  194. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  195. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  196. package/api/tests/fixtures/core-env-violation.ts +10 -0
  197. package/api/tests/fixtures/host-read-violation.ts +19 -0
  198. package/api/tests/fixtures/tenants.ts +4 -0
  199. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  200. package/api/tests/libs/archive-query.spec.ts +26 -0
  201. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  202. package/api/tests/libs/context.spec.ts +204 -0
  203. package/api/tests/libs/core-config.spec.ts +115 -0
  204. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  205. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  206. package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
  207. package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
  208. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  209. package/api/tests/libs/scoped.spec.ts +222 -0
  210. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  211. package/api/tests/libs/service-host.spec.ts +37 -0
  212. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  213. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  214. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  215. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  216. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  217. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  218. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  219. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  220. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  221. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  222. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  223. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  224. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  225. package/api/tests/models/tenant-columns.spec.ts +161 -0
  226. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  227. package/api/tests/queues/credit-consume.spec.ts +8 -1
  228. package/api/tests/queues/event-tenant.spec.ts +292 -0
  229. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  230. package/api/tests/queues/queue-parity.spec.ts +249 -0
  231. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  232. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  233. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  234. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  235. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  236. package/api/tests/service/collapse.spec.ts +96 -0
  237. package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
  238. package/api/tests/service/fail-closed-http.spec.ts +79 -0
  239. package/api/tests/service/static-arc-handler.spec.ts +101 -0
  240. package/api/tests/service/static-externalized.spec.ts +48 -0
  241. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  242. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  243. package/api/tests/store/tenant-model.spec.ts +162 -0
  244. package/api/tests/store/tenant-residual.spec.ts +196 -0
  245. package/api/third.d.ts +4 -0
  246. package/blocklet.yml +1 -1
  247. package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
  248. package/cloudflare/README.md +34 -27
  249. package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
  250. package/cloudflare/build.ts +33 -13
  251. package/cloudflare/cf-adapter.ts +419 -0
  252. package/cloudflare/did-connect-runtime.ts +96 -0
  253. package/cloudflare/did-connect-token-storage.ts +151 -0
  254. package/cloudflare/esbuild-cf-config.cjs +407 -0
  255. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  256. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  257. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  258. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  259. package/cloudflare/queue-runtime-mode.ts +13 -0
  260. package/cloudflare/run-build.js +33 -403
  261. package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
  262. package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
  263. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  264. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  265. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  266. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  267. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  268. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  269. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
  270. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
  271. package/cloudflare/shims/cron.ts +38 -158
  272. package/cloudflare/shims/events.ts +124 -0
  273. package/cloudflare/shims/fastq.ts +15 -1
  274. package/cloudflare/shims/nedb-storage.ts +16 -8
  275. package/cloudflare/shims/xss.ts +8 -0
  276. package/cloudflare/tenant-middleware.ts +36 -0
  277. package/cloudflare/tests/cf-adapter.spec.ts +244 -0
  278. package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
  279. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  280. package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
  281. package/cloudflare/vite.config.ts +53 -45
  282. package/cloudflare/worker.ts +261 -448
  283. package/cloudflare/wrangler.json +0 -6
  284. package/cloudflare/wrangler.jsonc +0 -6
  285. package/cloudflare/wrangler.local-e2e.jsonc +25 -0
  286. package/cloudflare/wrangler.staging.json +0 -6
  287. package/jest.config.js +3 -1
  288. package/package.json +33 -38
  289. package/scripts/bootstrap-inject.ts +166 -0
  290. package/scripts/core-env-whitelist.json +1 -0
  291. package/scripts/e2e-12b-runtime.ts +149 -0
  292. package/scripts/e2e-core-config.ts +125 -0
  293. package/scripts/e2e-d1-tenancy.ts +116 -0
  294. package/scripts/e2e-d2-cron-queue.ts +139 -0
  295. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  296. package/scripts/e2e-hono-s2.ts +125 -0
  297. package/scripts/e2e-hono-s3e.ts +135 -0
  298. package/scripts/e2e-hono-s4.ts +114 -0
  299. package/scripts/e2e-migration-contract.ts +100 -0
  300. package/scripts/e2e-s0.ts +61 -0
  301. package/scripts/e2e-s1.ts +107 -0
  302. package/scripts/e2e-s2.ts +178 -0
  303. package/scripts/e2e-s3.ts +110 -0
  304. package/scripts/e2e-s4.ts +191 -0
  305. package/scripts/e2e-s5.ts +139 -0
  306. package/scripts/e2e-s6.ts +127 -0
  307. package/scripts/e2e-tenant-model.ts +119 -0
  308. package/scripts/e2e-tenant-worker.ts +199 -0
  309. package/scripts/gen-sql-migrations.js +46 -0
  310. package/scripts/phase8-codemod.js +219 -0
  311. package/scripts/phase9a-env-getters-codemod.js +82 -0
  312. package/scripts/scan-core-env.js +109 -0
  313. package/scripts/scan-tenant-queries.js +235 -0
  314. package/scripts/schema-drift-guard.ts +210 -0
  315. package/scripts/tenant-scan-whitelist.json +1 -0
  316. package/src/app.tsx +2 -1
  317. package/src/env.d.ts +13 -1
  318. package/src/libs/service-host.ts +13 -0
  319. package/tsconfig.json +1 -1
  320. package/vite.arc.config.ts +159 -0
  321. package/api/src/libs/did-space.ts +0 -235
  322. package/api/src/libs/middleware.ts +0 -50
  323. package/api/src/libs/security.ts +0 -192
  324. package/api/src/queues/space.ts +0 -662
  325. package/api/src/routes/credit-tokens.ts +0 -38
  326. package/api/src/routes/exchange-rates.ts +0 -87
  327. package/api/src/routes/index.ts +0 -142
  328. package/api/src/routes/integrations/stripe.ts +0 -61
  329. package/api/src/routes/meters.ts +0 -274
  330. package/api/src/routes/passports.ts +0 -68
  331. package/api/src/routes/redirect.ts +0 -20
  332. package/api/src/routes/tool.ts +0 -65
  333. package/api/src/routes/webhook-endpoints.ts +0 -126
  334. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  335. package/cloudflare/did-connect-auth.ts +0 -527
  336. package/cloudflare/shims/did-space-js.ts +0 -17
  337. package/cloudflare/shims/did-space.ts +0 -11
  338. package/cloudflare/shims/express-compat/index.ts +0 -80
  339. package/cloudflare/shims/express-compat/types.ts +0 -41
  340. package/cloudflare/shims/lock.ts +0 -115
  341. package/cloudflare/shims/queue.ts +0 -611
  342. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  343. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -1,611 +0,0 @@
1
- // CF Workers queue shim — replaces libs/queue/index.ts
2
- //
3
- // Architecture (mirrors Blocklet Server's original design):
4
- //
5
- // D1 jobs table = scheduler (source of truth for all jobs)
6
- // CF Queue = executor (replaces fastq — concurrent execution engine)
7
- // Cron = dispatcher (replaces loop() — polls D1 for due jobs)
8
- //
9
- // Flow:
10
- // push(job, immediate) → D1 addJob + CF Queue send → consumer executes → D1 deleteJob
11
- // push(job, delayed) → D1 addJob only → cron dispatches when due → CF Queue → execute
12
- // pushAndWait(job) → D1 addJob + inline execute (caller awaits result)
13
- // cancel/delete → D1 deleteJob (delayed jobs never reach Queue until due)
14
- // CF Queue 429 → job stays in D1, cron retries next cycle
15
- //
16
- // This preserves the original Blocklet Server semantics:
17
- // fastq.push(job) → CF Queue send (immediate execution with concurrency control)
18
- // store.addJob() → D1 jobs table (persistence + scheduling)
19
- // loop() polling → cron every minute (dispatches due delayed jobs)
20
-
21
- import EventEmitter from 'events';
22
- import { nanoid } from 'nanoid';
23
- import { Op } from 'sequelize';
24
-
25
- import { Job, TJob } from '../../api/src/store/models/job';
26
- import createQueueStore from '../../api/src/libs/queue/store';
27
-
28
- // --- CF Queue binding ---
29
- let _cfQueue: any | null = null;
30
-
31
- export function setCFQueue(queue: any) {
32
- _cfQueue = queue;
33
- }
34
-
35
- // --- Handler registry ---
36
- type RegisteredHandler = {
37
- onJob: (job: any) => Promise<any>;
38
- executeJob: (jobId: string, job: any, persist: boolean) => Promise<any>;
39
- store: ReturnType<typeof createQueueStore>;
40
- cancel: (id: string) => Promise<any>;
41
- };
42
- const _handlers = new Map<string, RegisteredHandler>();
43
-
44
- export function getHandler(queueName: string): RegisteredHandler | undefined {
45
- return _handlers.get(queueName);
46
- }
47
-
48
- export function getAllHandlerNames(): string[] {
49
- return Array.from(_handlers.keys());
50
- }
51
-
52
- // --- waitUntil for keeping async work alive ---
53
- let _waitUntil: ((promise: Promise<any>) => void) | null = null;
54
-
55
- export function setWaitUntil(fn: (promise: Promise<any>) => void) {
56
- _waitUntil = fn;
57
- }
58
-
59
- // Pending promises — await before returning response
60
- const _pendingPushJobs: Promise<any>[] = [];
61
- (globalThis as any).__cfPendingJobs__ = _pendingPushJobs;
62
-
63
- // --- Request-time timer tracking ---
64
- // Business code uses setTimeout for batching (e.g. addToBatch's 3-second window).
65
- // In CF Workers, these timers fire after the response is sent — too late.
66
- // We intercept setTimeout during request handling, and flush pending timer
67
- // callbacks in flushPendingJobs before the response returns.
68
-
69
- type TrackedTimer = { fn: Function; args: any[]; cleared: boolean; realId: any };
70
- const _trackedTimers: TrackedTimer[] = [];
71
-
72
- const _prevSetTimeout = globalThis.setTimeout;
73
- const _prevClearTimeout = globalThis.clearTimeout;
74
-
75
- (globalThis as any).setTimeout = function(fn: any, delay?: number, ...args: any[]) {
76
- if (typeof fn !== 'function') {
77
- return (_prevSetTimeout as any)(fn, delay, ...args);
78
- }
79
-
80
- const entry: TrackedTimer = { fn, args, cleared: false, realId: null };
81
-
82
- entry.realId = (_prevSetTimeout as any)((...a: any[]) => {
83
- if (!entry.cleared) {
84
- entry.cleared = true;
85
- fn(...a);
86
- }
87
- }, delay, ...args);
88
-
89
- _trackedTimers.push(entry);
90
- return entry.realId;
91
- };
92
-
93
- (globalThis as any).clearTimeout = function(id: any) {
94
- for (const entry of _trackedTimers) {
95
- if (entry.realId === id || (entry.realId && typeof entry.realId === 'object' &&
96
- id && typeof id === 'object' && entry.realId[Symbol.toPrimitive]?.() === id[Symbol.toPrimitive]?.())) {
97
- entry.cleared = true;
98
- }
99
- }
100
- return (_prevClearTimeout as any)(id);
101
- };
102
-
103
- function flushTrackedTimers() {
104
- const pending = _trackedTimers.splice(0);
105
-
106
- const origSetHas = Set.prototype.has;
107
- if (_cfQueue) {
108
- Set.prototype.has = function() { return false; };
109
- }
110
-
111
- for (const entry of pending) {
112
- if (!entry.cleared) {
113
- entry.cleared = true;
114
- try { (_prevClearTimeout as any)(entry.realId); } catch (_e) { /* ignore */ }
115
- try { entry.fn(...entry.args); } catch (e) { console.error('[queue] timer flush error:', e); }
116
- }
117
- }
118
-
119
- if (_cfQueue) {
120
- Set.prototype.has = origSetHas;
121
- }
122
- }
123
-
124
- export async function flushPendingJobs() {
125
- const MAX_ITERATIONS = 10;
126
- for (let i = 0; i < MAX_ITERATIONS; i++) {
127
- flushTrackedTimers();
128
-
129
- if (_pendingPushJobs.length === 0) break;
130
- const batch = _pendingPushJobs.splice(0);
131
- await Promise.allSettled(batch);
132
- }
133
- }
134
-
135
- // --- Scheduled jobs: dispatch due jobs from D1 to CF Queue ---
136
- type ScheduledQueueEntry = {
137
- store: ReturnType<typeof createQueueStore>;
138
- executeJob: (jobId: string, job: any, persist: boolean) => Promise<any>;
139
- cancel: (id: string) => Promise<any>;
140
- name: string;
141
- };
142
- const _scheduledQueues = new Map<string, ScheduledQueueEntry>();
143
-
144
- /**
145
- * Dispatch all due delayed jobs from D1 to CF Queue for execution.
146
- * Called from CF Cron Trigger (every minute).
147
- *
148
- * This replaces the original loop() that polled every 4 seconds.
149
- * Jobs with will_run_at <= now are picked up, cancelled in D1 (to prevent
150
- * re-dispatch next cycle), then sent to CF Queue for execution.
151
- * If CF Queue is unavailable, execute inline as fallback.
152
- */
153
- export async function runAllScheduledJobs(): Promise<{ dispatched: number; failed: number; queues: string[] }> {
154
- let totalDispatched = 0;
155
- let totalFailed = 0;
156
- const queuesProcessed: string[] = [];
157
-
158
- // Dispatch helper: send job to CF Queue, or execute inline if unavailable
159
- //
160
- // Aligns with the original Blocklet Server semantics: the scheduler removes the
161
- // D1 row BEFORE onJob runs, so fire-and-forget re-pushes inside onJob (e.g.
162
- // handlePayment → paymentQueue.push({ runAt }) after a payment failure) can
163
- // freely INSERT a new scheduled row with the same id without hitting the
164
- // store.addJob duplicate check.
165
- //
166
- // The message carries persist:false so the consumer does NOT delete the row
167
- // again after onJob completes — otherwise it would wipe out the freshly
168
- // re-scheduled row inserted by the running handler.
169
- const dispatchJob = async (
170
- queueName: string,
171
- entry: { executeJob: Function; cancel: Function; store: ReturnType<typeof createQueueStore> },
172
- jobRecord: any,
173
- ) => {
174
- // Delete the old row up-front. Match the original loop() invariant:
175
- // once a scheduled row is picked up, it should no longer exist as a
176
- // "pending scheduled" row in D1 — the handler is now responsible for
177
- // its fate (success → nothing to clean up; failure → re-push writes a
178
- // brand new row).
179
- try {
180
- await entry.store.deleteJob(jobRecord.id);
181
- } catch (_e) {
182
- /* ignore — best effort */
183
- }
184
-
185
- if (_cfQueue) {
186
- try {
187
- await _cfQueue.send({ queueName, jobId: jobRecord.id, job: jobRecord.job, persist: false });
188
- } catch (sendErr: any) {
189
- // CF Queue unavailable → run inline with persist:false
190
- // (row was already deleted above; don't try to delete again)
191
- await entry.executeJob(jobRecord.id, jobRecord.job, false);
192
- }
193
- } else {
194
- await entry.executeJob(jobRecord.id, jobRecord.job, false);
195
- }
196
- };
197
-
198
- // Single aggregated D1 query across ALL registered queues. Replaces the
199
- // previous per-queue loop (16 queues × 2 queries = 32 serial round-trips,
200
- // ~2s wall time per scheduled tick) that consistently tripped CF Workers'
201
- // scheduled event time budget and produced ~1,440 exceededCpu outcomes/day.
202
- //
203
- // Shape: one findAll covering
204
- // (a) delayed jobs that are due — delay != -1 AND will_run_at <= now
205
- // (b) immediate jobs stuck in D1 — delay == -1 AND created_at < now-2min
206
- // Results are grouped by queue name and dispatched via the existing
207
- // per-queue dispatchJob path, preserving original behavior.
208
- const queueNames = Array.from(_handlers.keys());
209
- if (queueNames.length === 0) {
210
- return { dispatched: 0, failed: 0, queues: [] };
211
- }
212
-
213
- const now = Date.now();
214
- // SQLite / D1 stores DATE columns as ISO strings. The raw D1 bind layer
215
- // rejects Date objects with "Type 'object' not supported", so we pass the
216
- // serialized string here instead of a native Date.
217
- const twoMinAgoISO = new Date(now - 2 * 60 * 1000).toISOString();
218
-
219
- let allJobs: TJob[] = [];
220
- try {
221
- allJobs = (await Job.findAll({
222
- where: {
223
- queue: { [Op.in]: queueNames },
224
- cancelled: false,
225
- [Op.or]: [
226
- { delay: { [Op.not]: -1 }, will_run_at: { [Op.lte]: now } },
227
- { delay: -1, created_at: { [Op.lt]: twoMinAgoISO } },
228
- ],
229
- },
230
- order: [['created_at', 'ASC']],
231
- transaction: null,
232
- })) as unknown as TJob[];
233
- } catch (err: any) {
234
- console.error('[queue:dispatch] Aggregated D1 scan failed:', err?.message || err);
235
- return { dispatched: 0, failed: 0, queues: [] };
236
- }
237
-
238
- if (allJobs.length === 0) {
239
- return { dispatched: 0, failed: 0, queues: [] };
240
- }
241
-
242
- // Group by queue, preserving created_at ASC order from the SQL ORDER BY.
243
- const jobsByQueue = new Map<string, TJob[]>();
244
- for (const job of allJobs) {
245
- if (!job.job || !job.id) continue;
246
- const arr = jobsByQueue.get(job.queue) ?? [];
247
- arr.push(job);
248
- jobsByQueue.set(job.queue, arr);
249
- }
250
-
251
- for (const [queueName, jobs] of jobsByQueue) {
252
- const entry = _handlers.get(queueName);
253
- if (!entry || jobs.length === 0) continue;
254
-
255
- queuesProcessed.push(queueName);
256
-
257
- for (const jobRecord of jobs) {
258
- try {
259
- await dispatchJob(queueName, entry, jobRecord);
260
- totalDispatched++;
261
- } catch (err: any) {
262
- totalFailed++;
263
- console.error(`[queue:dispatch] Failed ${queueName}/${jobRecord.id}:`, err?.message || err);
264
- }
265
- }
266
- }
267
-
268
- return { dispatched: totalDispatched, failed: totalFailed, queues: queuesProcessed };
269
- }
270
-
271
- // --- Queue factory ---
272
-
273
- type QueueOptions<T> = {
274
- id?: (job: T) => string;
275
- concurrency?: number;
276
- maxRetries?: number;
277
- maxTimeout?: number;
278
- retryDelay?: number;
279
- enableScheduledJob?: boolean;
280
- };
281
-
282
- type QueueParams<T> = {
283
- name: string;
284
- onJob: (job: T) => Promise<any>;
285
- options?: QueueOptions<T>;
286
- };
287
-
288
- const defaults: QueueOptions<any> = {
289
- concurrency: 1,
290
- maxRetries: 1,
291
- maxTimeout: 24 * 60 * 60 * 1000,
292
- retryDelay: 0,
293
- enableScheduledJob: false,
294
- };
295
-
296
- type PushParams<T> = {
297
- job: T;
298
- id?: string;
299
- persist?: boolean;
300
- delay?: number;
301
- runAt?: number;
302
- skipDuplicateCheck?: boolean;
303
- };
304
-
305
- export default function createQueue<T = any>({ name, onJob, options = defaults }: QueueParams<T>) {
306
- const store = createQueueStore(name);
307
- const { concurrency, maxRetries, maxTimeout, retryDelay, enableScheduledJob } = Object.assign({}, defaults, options);
308
-
309
- const getJobId = (id: string | undefined, job: any) => id || (options?.id ? options.id(job) : nanoid()) || nanoid();
310
-
311
- const queueEvents = new EventEmitter();
312
-
313
- // Execute the job directly (used by pushAndWait and queue consumer)
314
- const executeJob = async (jobId: string, job: T, persist: boolean): Promise<{ id: string; job: T; result: any }> => {
315
- let retryCount = 0;
316
-
317
- // eslint-disable-next-line no-constant-condition
318
- while (true) {
319
- try {
320
- console.log(`[queue:${name}] executing job`, jobId);
321
- const result = await onJob(job);
322
- console.log(`[queue:${name}] job finished`, jobId);
323
-
324
- if (persist) {
325
- try { await store.deleteJob(jobId); } catch (_e) { /* ignore */ }
326
- }
327
-
328
- return { id: jobId, job, result };
329
- } catch (err: any) {
330
- console.error(`[queue:${name}] job error`, jobId, err?.message || err);
331
-
332
- if (err?.nonRetryable === true) {
333
- if (persist) {
334
- try { await store.deleteJob(jobId); } catch (_e) { /* ignore */ }
335
- }
336
- throw err;
337
- }
338
-
339
- retryCount++;
340
- if (retryCount >= maxRetries!) {
341
- if (persist) {
342
- try { await store.deleteJob(jobId); } catch (_e) { /* ignore */ }
343
- }
344
- throw err;
345
- }
346
-
347
- console.log(`[queue:${name}] retrying job`, jobId, `attempt ${retryCount}/${maxRetries}`);
348
- queueEvents.emit('retry', { id: jobId, job });
349
- if (retryDelay && retryDelay > 0) {
350
- await new Promise((r) => (_prevSetTimeout as any)(r, retryDelay));
351
- }
352
- }
353
- }
354
- };
355
-
356
- // Send job to CF Queue for execution (replaces fastq.push)
357
- const sendToCFQueue = async (jobId: string, job: T) => {
358
- const message = { queueName: name, jobId, job };
359
- await _cfQueue.send(message);
360
- };
361
-
362
- const push = ({ job, id, persist = true, delay, runAt, skipDuplicateCheck = false }: PushParams<T>) => {
363
- const jobEvents = new EventEmitter();
364
- const emit = (e: string, data: any) => {
365
- queueEvents.emit(e, data);
366
- jobEvents.emit(e, data);
367
- };
368
-
369
- if (!job) {
370
- throw new Error('Can not queue empty job');
371
- }
372
-
373
- const jobId = getJobId(id, job);
374
-
375
- // Calculate delay in seconds
376
- let delaySeconds = 0;
377
- if (delay) {
378
- delaySeconds = delay;
379
- } else if (runAt) {
380
- delaySeconds = Math.max(0, Math.floor((runAt as number) - Date.now() / 1000));
381
- }
382
-
383
- const isImmediate = delaySeconds <= 0;
384
-
385
- const enqueue = async () => {
386
- if (isImmediate) {
387
- // === Immediate job ===
388
- // 1. Persist to D1 (source of truth)
389
- // 2. Send to CF Queue for execution (like fastq.push)
390
- // 3. If CF Queue fails → job stays in D1, cron will dispatch
391
- if (persist) {
392
- try {
393
- await store.addJob(jobId, job, {}, skipDuplicateCheck);
394
- } catch (err: any) {
395
- const isDuplicate = err?.code === 'JOB_DUPLICATE' ||
396
- err?.message?.includes('UNIQUE constraint failed') ||
397
- err?.message?.includes('SQLITE_CONSTRAINT');
398
- if (isDuplicate) {
399
- return;
400
- }
401
- console.error(`[queue:${name}] addJob error`, err?.message);
402
- return;
403
- }
404
- }
405
-
406
- emit('queued', { id: jobId, job, persist });
407
-
408
- if (_cfQueue) {
409
- try {
410
- await sendToCFQueue(jobId, job);
411
- } catch (err: any) {
412
- // CF Queue unavailable (429 Too Many Requests, transport down, etc.)
413
- // Fall through to inline execution: the worker context already has
414
- // its own CPU budget (HTTP handler: 30s; scheduled handler: 30s),
415
- // and a tightly-throttled CF Queue + a backed-up cron dispatcher
416
- // means immediate jobs would otherwise sit in D1 indefinitely.
417
- // The D1 row persists either way, so a crash here still lets a
418
- // later cron tick (or a retry) pick it up.
419
- console.warn(`[queue:${name}] CF Queue send failed (${err?.message || err}); executing inline`);
420
- try {
421
- // Pass persist=false so executeJob does NOT delete the D1 row on a
422
- // terminal failure — that would wipe the only durable backup (esp.
423
- // for maxRetries:0 queues like webhookQueue) and the cron dispatcher
424
- // could never retry it. Delete the row ONLY on success here.
425
- // (PR #1381 re-review P1.)
426
- const data = await executeJob(jobId, job, false);
427
- if (persist) {
428
- try {
429
- await store.deleteJob(jobId);
430
- } catch (_e) {
431
- /* ignore — duplicate delete is harmless */
432
- }
433
- }
434
- emit('finished', data);
435
- } catch (execErr: any) {
436
- // D1 row intact → a later cron tick / retry can pick it up.
437
- emit('failed', { id: jobId, job, error: execErr });
438
- }
439
- }
440
- } else {
441
- // No CF Queue binding — execute inline (Blocklet Server compatibility)
442
- try {
443
- const data = await executeJob(jobId, job, persist);
444
- emit('finished', data);
445
- } catch (err: any) {
446
- emit('failed', { id: jobId, job, error: err });
447
- }
448
- }
449
- } else {
450
- // === Delayed job ===
451
- // Only persist to D1 with will_run_at. Cron dispatches when due.
452
- // NOT sent to CF Queue — allows cancel/replace via D1 delete.
453
- if (persist) {
454
- const attrs: any = { delay: delaySeconds, will_run_at: Date.now() + delaySeconds * 1000 };
455
- try {
456
- await store.addJob(jobId, job, attrs, skipDuplicateCheck);
457
- } catch (err: any) {
458
- const isDuplicate = err?.code === 'JOB_DUPLICATE' ||
459
- err?.message?.includes('UNIQUE constraint failed') ||
460
- err?.message?.includes('SQLITE_CONSTRAINT');
461
- if (isDuplicate) {
462
- return;
463
- }
464
- console.error(`[queue:${name}] addJob error for delayed job`, err?.message);
465
- return;
466
- }
467
- }
468
- emit('queued', { id: jobId, job, persist });
469
- }
470
- };
471
-
472
- const promise = enqueue()
473
- .catch((err) => console.error(`[queue:${name}] push error:`, err?.message || err));
474
-
475
- // Register promise to keep Worker alive
476
- const isHttp = (globalThis as any).__cfHttpContext__;
477
- if (isHttp && _waitUntil) {
478
- _waitUntil(promise);
479
- } else {
480
- _pendingPushJobs.push(promise);
481
- if (_waitUntil) {
482
- _waitUntil(promise);
483
- }
484
- }
485
-
486
- (jobEvents as any).id = jobId;
487
- return jobEvents;
488
- };
489
-
490
- // pushAndWait always runs inline (caller needs the result synchronously)
491
- const pushAndWait = async (params: PushParams<T>) => {
492
- if (!params.job) {
493
- throw new Error('Can not queue empty job');
494
- }
495
-
496
- const jobId = getJobId(params.id, params.job);
497
- const persist = params.persist !== false;
498
-
499
- if (persist) {
500
- try {
501
- await store.addJob(jobId, params.job, {}, params.skipDuplicateCheck || false);
502
- } catch (err: any) {
503
- const isDuplicate = err?.code === 'JOB_DUPLICATE' ||
504
- err?.message?.includes('UNIQUE constraint failed') ||
505
- err?.message?.includes('SQLITE_CONSTRAINT');
506
- if (isDuplicate) {
507
- console.log(`[queue:${name}] pushAndWait duplicate job`, jobId);
508
- } else {
509
- throw err;
510
- }
511
- }
512
- }
513
-
514
- try {
515
- const data = await executeJob(jobId, params.job, persist);
516
- queueEvents.emit('finished', data);
517
- return data;
518
- } catch (err: any) {
519
- queueEvents.emit('failed', { id: jobId, job: params.job, error: err });
520
- throw { id: jobId, job: params.job, error: err };
521
- }
522
- };
523
-
524
- const cancel = async (id: string) => {
525
- try {
526
- const doc = await store.updateJob(id, { cancelled: true });
527
- return doc ? doc.job : null;
528
- } catch (_e) {
529
- return null;
530
- }
531
- };
532
-
533
- const getJob = async (id: string) => {
534
- try {
535
- const doc = await store.getJob(id);
536
- return doc ? doc.job : null;
537
- } catch (_e) {
538
- return null;
539
- }
540
- };
541
-
542
- const deleteJob = async (id: string, knownExists: boolean = false): Promise<boolean> => {
543
- if (!knownExists) {
544
- const exists = await getJob(id);
545
- if (!exists) return false;
546
- }
547
- try {
548
- await store.deleteJob(id);
549
- return true;
550
- } catch (_e) {
551
- try { await cancel(id); } catch (_e2) { /* ignore */ }
552
- return false;
553
- }
554
- };
555
-
556
- const updateJob = async (id: string, updates: any) => {
557
- return store.updateJob(id, updates);
558
- };
559
-
560
- const queueInstance = Object.assign(queueEvents, {
561
- store,
562
- push,
563
- pushAndWait,
564
- drain: (_cb: any) => {},
565
- empty: (_cb: any) => {},
566
- saturated: (_cb: any) => {},
567
- error: (_cb: any) => {},
568
- get: getJob,
569
- delete: deleteJob,
570
- cancel,
571
- update: updateJob,
572
- executeJob,
573
- options: {
574
- concurrency,
575
- maxRetries,
576
- maxTimeout,
577
- retryDelay,
578
- enableScheduledJob,
579
- },
580
- });
581
-
582
- // Register handler for CF Queue consumer dispatch
583
- _handlers.set(name, { onJob, executeJob, store, cancel });
584
-
585
- // Register for scheduled job dispatch (cron picks up due delayed jobs)
586
- if (enableScheduledJob) {
587
- _scheduledQueues.set(name, { store, executeJob, cancel, name });
588
- }
589
-
590
- return queueInstance;
591
- }
592
-
593
- // Exported for unit tests; not part of the public queue API.
594
- export const __test__ = {
595
- resetHandlers() {
596
- _handlers.clear();
597
- _scheduledQueues.clear();
598
- _pendingPushJobs.length = 0;
599
- _trackedTimers.length = 0;
600
- },
601
- registerForTest(name: string, overrides: Partial<RegisteredHandler>) {
602
- const noop = async () => undefined;
603
- const store = createQueueStore(name);
604
- _handlers.set(name, {
605
- onJob: overrides.onJob ?? noop,
606
- executeJob: overrides.executeJob ?? (noop as any),
607
- cancel: overrides.cancel ?? (noop as any),
608
- store: overrides.store ?? store,
609
- });
610
- },
611
- };
@@ -1,87 +0,0 @@
1
- // Regression guard for PR #1381 review P1: on Cloudflare the queue shim only
2
- // writes a DELAYED job to D1 (with will_run_at, later dispatched by the cron)
3
- // when it is persisted. A delayed job pushed with persist:false is silently
4
- // dropped — which is exactly how the webhook retry ladder lost deliveries after
5
- // the first failure. These tests pin that invariant so callers always persist
6
- // delayed retries.
7
-
8
- // A single shared store mock so we can assert what the shim wrote.
9
- const mockStore = {
10
- addJob: jest.fn().mockResolvedValue(undefined),
11
- deleteJob: jest.fn().mockResolvedValue(true),
12
- getJob: jest.fn(),
13
- getJobs: jest.fn(),
14
- getScheduledJobs: jest.fn(),
15
- updateJob: jest.fn(),
16
- isCancelled: jest.fn().mockResolvedValue(false),
17
- findJobs: jest.fn(),
18
- };
19
-
20
- jest.mock('../../../api/src/store/models/job', () => ({ Job: { findAll: jest.fn() } }));
21
- jest.mock('../../../api/src/libs/queue/store', () => ({
22
- __esModule: true,
23
- default: jest.fn(() => mockStore),
24
- }));
25
-
26
- import createQueue, { setCFQueue, flushPendingJobs } from '../../shims/queue';
27
-
28
- const futureRunAt = () => Math.floor(Date.now() / 1000) + 600; // +10 min → delayed
29
-
30
- describe('shim queue — delayed job persistence (PR #1381 P1)', () => {
31
- beforeEach(() => {
32
- jest.clearAllMocks();
33
- setCFQueue(null); // no CF Queue binding; delayed path is independent of it anyway
34
- });
35
-
36
- it('persists a delayed job to D1 (with will_run_at) when persist=true', async () => {
37
- const q = createQueue({ name: 'webhook-persist-true', onJob: jest.fn() });
38
- q.push({ id: 'j1', job: { v: 1 }, runAt: futureRunAt(), persist: true });
39
- await flushPendingJobs();
40
-
41
- expect(mockStore.addJob).toHaveBeenCalledTimes(1);
42
- const attrs = mockStore.addJob.mock.calls[0]![2];
43
- expect(attrs.will_run_at).toBeGreaterThan(Date.now());
44
- expect(attrs.delay).toBeGreaterThan(0);
45
- });
46
-
47
- it('DROPS a delayed job (no D1 write) when persist=false — the lost-retry bug class', async () => {
48
- const q = createQueue({ name: 'webhook-persist-false', onJob: jest.fn() });
49
- q.push({ id: 'j2', job: { v: 2 }, runAt: futureRunAt(), persist: false });
50
- await flushPendingJobs();
51
-
52
- expect(mockStore.addJob).not.toHaveBeenCalled();
53
- });
54
- });
55
-
56
- describe('shim queue — inline fallback keeps the D1 backup on failure (PR #1381 re-review P1)', () => {
57
- beforeEach(() => {
58
- jest.clearAllMocks();
59
- });
60
-
61
- it('does NOT delete the persisted row when CF Queue send fails AND the inline handler fails', async () => {
62
- setCFQueue({ send: jest.fn().mockRejectedValue(new Error('429 Too Many Requests')) } as any);
63
- const onJob = jest.fn().mockRejectedValue(new Error('handler boom'));
64
- const q = createQueue({ name: 'webhook-fallback-fail', onJob, options: { maxRetries: 1 } });
65
-
66
- q.push({ id: 'wj1', job: { v: 1 }, persist: true }); // immediate
67
- await flushPendingJobs();
68
-
69
- expect(mockStore.addJob).toHaveBeenCalledTimes(1); // durable backup written
70
- expect(onJob).toHaveBeenCalled(); // inline attempt ran
71
- expect(mockStore.deleteJob).not.toHaveBeenCalled(); // backup retained → cron can retry
72
- setCFQueue(null);
73
- });
74
-
75
- it('deletes the persisted row when the inline fallback SUCCEEDS', async () => {
76
- setCFQueue({ send: jest.fn().mockRejectedValue(new Error('429')) } as any);
77
- const onJob = jest.fn().mockResolvedValue(undefined);
78
- const q = createQueue({ name: 'webhook-fallback-ok', onJob, options: { maxRetries: 1 } });
79
-
80
- q.push({ id: 'wj2', job: { v: 2 }, persist: true });
81
- await flushPendingJobs();
82
-
83
- expect(onJob).toHaveBeenCalled();
84
- expect(mockStore.deleteJob).toHaveBeenCalledWith('wj2'); // deleted only on success
85
- setCFQueue(null);
86
- });
87
- });