payment-kit 1.27.2 → 1.29.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 (241) hide show
  1. package/__blocklet__.js +37 -0
  2. package/api/ocap-1.30-subpath-shims.d.ts +35 -0
  3. package/api/src/crons/index.ts +32 -0
  4. package/api/src/crons/metering-subscription-detection.ts +12 -14
  5. package/api/src/crons/overdue-detection.ts +51 -74
  6. package/api/src/crons/retry-pending-events.ts +58 -0
  7. package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
  8. package/api/src/integrations/app-store/client.ts +369 -0
  9. package/api/src/integrations/app-store/handlers/index.ts +46 -0
  10. package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
  11. package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
  12. package/api/src/integrations/app-store/notification-routing.ts +18 -0
  13. package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
  14. package/api/src/integrations/arcblock/nft.ts +6 -2
  15. package/api/src/integrations/arcblock/stake.ts +3 -2
  16. package/api/src/integrations/arcblock/token.ts +4 -4
  17. package/api/src/integrations/blocklet/notification.ts +1 -1
  18. package/api/src/integrations/ethereum/tx.ts +29 -0
  19. package/api/src/integrations/google-play/client.ts +276 -0
  20. package/api/src/integrations/google-play/handlers/index.ts +69 -0
  21. package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
  22. package/api/src/integrations/google-play/handlers/voided.ts +106 -0
  23. package/api/src/integrations/google-play/setup.ts +43 -0
  24. package/api/src/integrations/google-play/verify.ts +251 -0
  25. package/api/src/integrations/iap-reconcile.ts +415 -0
  26. package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
  27. package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
  28. package/api/src/integrations/stripe/resource.ts +8 -0
  29. package/api/src/libs/audit.ts +70 -24
  30. package/api/src/libs/auth.ts +49 -2
  31. package/api/src/libs/chain-error.ts +31 -0
  32. package/api/src/libs/entitlement.ts +399 -0
  33. package/api/src/libs/env.ts +2 -0
  34. package/api/src/libs/error.ts +15 -0
  35. package/api/src/libs/event.ts +42 -1
  36. package/api/src/libs/invoice.ts +69 -34
  37. package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
  38. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
  39. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
  40. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
  41. package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
  42. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
  43. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
  44. package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
  45. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
  46. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
  47. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
  48. package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
  49. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
  50. package/api/src/libs/pagination.ts +14 -9
  51. package/api/src/libs/payment.ts +25 -10
  52. package/api/src/libs/security.ts +51 -0
  53. package/api/src/libs/session.ts +1 -1
  54. package/api/src/libs/subscription.ts +13 -1
  55. package/api/src/libs/timing.ts +35 -0
  56. package/api/src/libs/util.ts +29 -15
  57. package/api/src/libs/wallet-migration.ts +72 -53
  58. package/api/src/queues/auto-recharge.ts +1 -1
  59. package/api/src/queues/credit-consume.ts +94 -12
  60. package/api/src/queues/credit-grant.ts +4 -0
  61. package/api/src/queues/event.ts +39 -21
  62. package/api/src/queues/invoice.ts +1 -0
  63. package/api/src/queues/payment.ts +83 -15
  64. package/api/src/queues/refund.ts +84 -71
  65. package/api/src/queues/subscription.ts +1 -0
  66. package/api/src/queues/webhook.ts +12 -2
  67. package/api/src/routes/checkout-sessions.ts +82 -43
  68. package/api/src/routes/connect/change-payment.ts +2 -0
  69. package/api/src/routes/connect/change-plan.ts +2 -0
  70. package/api/src/routes/connect/pay.ts +12 -3
  71. package/api/src/routes/connect/setup.ts +3 -1
  72. package/api/src/routes/connect/shared.ts +52 -39
  73. package/api/src/routes/connect/subscribe.ts +4 -1
  74. package/api/src/routes/credit-grants.ts +25 -17
  75. package/api/src/routes/donations.ts +2 -2
  76. package/api/src/routes/entitlements.ts +105 -0
  77. package/api/src/routes/events.ts +2 -2
  78. package/api/src/routes/index.ts +12 -2
  79. package/api/src/routes/integrations/app-store.ts +267 -0
  80. package/api/src/routes/integrations/google-play.ts +324 -0
  81. package/api/src/routes/meter-events.ts +16 -6
  82. package/api/src/routes/payment-links.ts +1 -1
  83. package/api/src/routes/payment-methods.ts +131 -1
  84. package/api/src/routes/settings.ts +1 -1
  85. package/api/src/routes/tax-rates.ts +1 -1
  86. package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
  87. package/api/src/store/models/customer.ts +37 -1
  88. package/api/src/store/models/entitlement-grant.ts +118 -0
  89. package/api/src/store/models/entitlement-product.ts +48 -0
  90. package/api/src/store/models/entitlement.ts +86 -0
  91. package/api/src/store/models/index.ts +9 -0
  92. package/api/src/store/models/invoice.ts +20 -0
  93. package/api/src/store/models/payment-method.ts +66 -1
  94. package/api/src/store/models/price.ts +23 -14
  95. package/api/src/store/models/refund.ts +10 -0
  96. package/api/src/store/models/subscription.ts +14 -0
  97. package/api/src/store/models/types.ts +32 -0
  98. package/api/tests/integrations/app-store/client.spec.ts +335 -0
  99. package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
  100. package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
  101. package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
  102. package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
  103. package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
  104. package/api/tests/integrations/google-play/verify.spec.ts +215 -0
  105. package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
  106. package/api/tests/libs/entitlement.spec.ts +347 -0
  107. package/api/tests/libs/wallet-migration.spec.ts +4 -4
  108. package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
  109. package/api/tests/queues/credit-consume.spec.ts +8 -4
  110. package/api/tests/routes/credit-grants.spec.ts +1 -0
  111. package/blocklet.yml +1 -1
  112. package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
  113. package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
  114. package/cloudflare/README.md +499 -0
  115. package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
  116. package/cloudflare/build.ts +151 -0
  117. package/cloudflare/did-connect-auth.ts +527 -0
  118. package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
  119. package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
  120. package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
  121. package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
  122. package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
  123. package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
  124. package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
  125. package/cloudflare/frontend-shims/js-sdk.ts +43 -0
  126. package/cloudflare/frontend-shims/mime-types.ts +46 -0
  127. package/cloudflare/frontend-shims/session.ts +24 -0
  128. package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
  129. package/cloudflare/index.html +40 -0
  130. package/cloudflare/migrate-to-d1.js +252 -0
  131. package/cloudflare/migrations/0001_initial_schema.sql +82 -0
  132. package/cloudflare/migrations/0002_indexes.sql +75 -0
  133. package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
  134. package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
  135. package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
  136. package/cloudflare/run-build.js +391 -0
  137. package/cloudflare/scripts/test-decrypt.js +102 -0
  138. package/cloudflare/shims/arcblock-ws.ts +20 -0
  139. package/cloudflare/shims/axios-http-adapter.ts +4 -0
  140. package/cloudflare/shims/axios-lite.ts +117 -0
  141. package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
  142. package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
  143. package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
  144. package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
  145. package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
  146. package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
  147. package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
  148. package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
  149. package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
  150. package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
  151. package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
  152. package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
  153. package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
  154. package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
  155. package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
  156. package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
  157. package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
  158. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
  159. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
  160. package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
  161. package/cloudflare/shims/cookie-parser.ts +3 -0
  162. package/cloudflare/shims/cors.ts +21 -0
  163. package/cloudflare/shims/cron.ts +189 -0
  164. package/cloudflare/shims/crypto-js-warn.ts +7 -0
  165. package/cloudflare/shims/did-space-js.ts +17 -0
  166. package/cloudflare/shims/did-space.ts +11 -0
  167. package/cloudflare/shims/error.ts +18 -0
  168. package/cloudflare/shims/express-compat/index.ts +80 -0
  169. package/cloudflare/shims/express-compat/types.ts +41 -0
  170. package/cloudflare/shims/fastq.ts +105 -0
  171. package/cloudflare/shims/lock.ts +115 -0
  172. package/cloudflare/shims/mime-types.ts +56 -0
  173. package/cloudflare/shims/nedb-storage.ts +9 -0
  174. package/cloudflare/shims/node-child-process.ts +9 -0
  175. package/cloudflare/shims/node-fs.ts +20 -0
  176. package/cloudflare/shims/node-http.ts +13 -0
  177. package/cloudflare/shims/node-https.ts +4 -0
  178. package/cloudflare/shims/node-misc.ts +15 -0
  179. package/cloudflare/shims/node-net.ts +8 -0
  180. package/cloudflare/shims/node-os.ts +14 -0
  181. package/cloudflare/shims/node-tty.ts +8 -0
  182. package/cloudflare/shims/node-zlib.ts +17 -0
  183. package/cloudflare/shims/noop.ts +26 -0
  184. package/cloudflare/shims/payment-vendor.ts +14 -0
  185. package/cloudflare/shims/querystring.ts +12 -0
  186. package/cloudflare/shims/queue.ts +611 -0
  187. package/cloudflare/shims/rolldown-runtime.ts +43 -0
  188. package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
  189. package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
  190. package/cloudflare/shims/sequelize-d1/index.ts +34 -0
  191. package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
  192. package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
  193. package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
  194. package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
  195. package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
  196. package/cloudflare/shims/sequelize-d1/types.ts +35 -0
  197. package/cloudflare/shims/stripe-cf.ts +29 -0
  198. package/cloudflare/shims/ws-lite.ts +103 -0
  199. package/cloudflare/shims/xss.ts +3 -0
  200. package/cloudflare/tests/shims/cron.spec.ts +210 -0
  201. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
  202. package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
  203. package/cloudflare/vite.config.ts +162 -0
  204. package/cloudflare/worker.ts +1608 -0
  205. package/cloudflare/wrangler.json +63 -0
  206. package/cloudflare/wrangler.jsonc +75 -0
  207. package/cloudflare/wrangler.staging.json +67 -0
  208. package/cloudflare/wrangler.toml +28 -0
  209. package/jest.config.js +4 -12
  210. package/package.json +30 -22
  211. package/scripts/seed-google-play.ts +79 -0
  212. package/src/app.tsx +62 -4
  213. package/src/components/customer/link.tsx +9 -13
  214. package/src/components/customer/notification-preference.tsx +3 -2
  215. package/src/components/filter-toolbar.tsx +4 -0
  216. package/src/components/invoice/list.tsx +9 -1
  217. package/src/components/invoice-pdf/utils.ts +2 -1
  218. package/src/components/layout/admin.tsx +39 -5
  219. package/src/components/layout/user-cf.tsx +77 -0
  220. package/src/components/payment-intent/actions.tsx +23 -3
  221. package/src/components/payment-method/app-store.tsx +103 -0
  222. package/src/components/payment-method/form.tsx +7 -1
  223. package/src/components/payment-method/google-play.tsx +85 -0
  224. package/src/components/safe-did-address.tsx +75 -0
  225. package/src/components/subscription/list.tsx +20 -0
  226. package/src/libs/patch-user-card.ts +25 -0
  227. package/src/libs/util.ts +5 -7
  228. package/src/locales/en.tsx +63 -0
  229. package/src/locales/zh.tsx +63 -0
  230. package/src/pages/admin/billing/meter-events/index.tsx +4 -0
  231. package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
  232. package/src/pages/admin/customers/customers/detail.tsx +8 -2
  233. package/src/pages/admin/customers/customers/index.tsx +2 -2
  234. package/src/pages/admin/overview.tsx +3 -1
  235. package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
  236. package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
  237. package/src/pages/customer/subscription/detail.tsx +4 -4
  238. package/tsconfig.api.json +1 -6
  239. package/tsconfig.json +3 -4
  240. package/tsconfig.types.json +2 -1
  241. package/vite.config.ts +6 -1
@@ -0,0 +1,611 @@
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
+ };
@@ -0,0 +1,43 @@
1
+ // Shim for */esm/_virtual/rolldown_runtime.mjs
2
+ // CF Workers bundle doesn't support createRequire(import.meta.url).
3
+ // We statically import modules that __require() needs and serve them.
4
+
5
+ import * as googleProtobuf from 'google-protobuf';
6
+ import * as googleProtobufAnyPb from 'google-protobuf/google/protobuf/any_pb';
7
+ import * as googleProtobufTimestampPb from 'google-protobuf/google/protobuf/timestamp_pb';
8
+ import _debugImport from 'debug';
9
+
10
+ const debugFactory = typeof _debugImport === 'function' ? _debugImport : (_debugImport as any).default || (() => () => {});
11
+
12
+ const _modules: Record<string, any> = {
13
+ 'google-protobuf': googleProtobuf,
14
+ 'google-protobuf/google/protobuf/any_pb': googleProtobufAnyPb,
15
+ 'google-protobuf/google/protobuf/timestamp_pb': googleProtobufTimestampPb,
16
+ 'debug': debugFactory,
17
+ };
18
+
19
+ const __commonJSMin = (cb: any, mod?: any) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
20
+
21
+ const __require = (mod: string) => {
22
+ if (_modules[mod]) return _modules[mod];
23
+ if (typeof globalThis.require === 'function') {
24
+ try { return globalThis.require(mod); } catch {}
25
+ }
26
+ console.warn('[rolldown-runtime] Cannot require:', mod);
27
+ return {};
28
+ };
29
+
30
+ // Matches rolldown's __exportAll(gettersMap, symbols?) → frozen module object
31
+ const __defProp = Object.defineProperty;
32
+ const __exportAll = (all: Record<string, () => any>, symbols?: boolean) => {
33
+ const target: any = {};
34
+ for (const name in all) {
35
+ __defProp(target, name, { get: all[name], enumerable: true });
36
+ }
37
+ if (symbols) {
38
+ __defProp(target, Symbol.toStringTag, { value: 'Module' });
39
+ }
40
+ return target;
41
+ };
42
+
43
+ export { __commonJSMin, __require, __exportAll };
@@ -0,0 +1,24 @@
1
+ // DataTypes stub — used in Model.init() for schema registration
2
+ // In D1 shim, we collect these to know column types but don't create tables at runtime
3
+ // (tables are created via D1 migrations)
4
+
5
+ export const DataTypes = {
6
+ STRING: (len?: number) => ({ type: 'TEXT', length: len }),
7
+ TEXT: { type: 'TEXT' },
8
+ INTEGER: { type: 'INTEGER' },
9
+ BIGINT: { type: 'INTEGER' },
10
+ FLOAT: { type: 'REAL' },
11
+ DOUBLE: { type: 'REAL' },
12
+ BOOLEAN: { type: 'INTEGER' },
13
+ DATE: { type: 'TEXT' },
14
+ DATEONLY: { type: 'TEXT' },
15
+ JSON: { type: 'TEXT' },
16
+ JSONB: { type: 'TEXT' },
17
+ DECIMAL: (precision?: number, scale?: number) => ({ type: 'REAL', precision, scale }),
18
+ BLOB: { type: 'BLOB' },
19
+ ENUM: (...values: string[]) => ({ type: 'TEXT', values }),
20
+ ARRAY: (itemType: any) => ({ type: 'TEXT', itemType }),
21
+ VIRTUAL: { type: 'VIRTUAL' },
22
+ UUID: { type: 'TEXT' },
23
+ NOW: 'CF_NOW_PLACEHOLDER',
24
+ };