payment-kit 1.27.2 → 1.28.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 (184) 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 +10 -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/integrations/arcblock/nft.ts +6 -2
  7. package/api/src/integrations/arcblock/stake.ts +3 -2
  8. package/api/src/integrations/arcblock/token.ts +4 -4
  9. package/api/src/integrations/blocklet/notification.ts +1 -1
  10. package/api/src/integrations/ethereum/tx.ts +29 -0
  11. package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
  12. package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
  13. package/api/src/integrations/stripe/resource.ts +8 -0
  14. package/api/src/libs/audit.ts +32 -16
  15. package/api/src/libs/auth.ts +49 -2
  16. package/api/src/libs/chain-error.ts +31 -0
  17. package/api/src/libs/error.ts +15 -0
  18. package/api/src/libs/event.ts +42 -1
  19. package/api/src/libs/invoice.ts +69 -34
  20. package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
  21. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
  22. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
  23. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
  24. package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
  25. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
  26. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
  27. package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
  28. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
  29. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
  30. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
  31. package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
  32. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
  33. package/api/src/libs/pagination.ts +14 -9
  34. package/api/src/libs/payment.ts +25 -10
  35. package/api/src/libs/session.ts +1 -1
  36. package/api/src/libs/timing.ts +35 -0
  37. package/api/src/libs/util.ts +16 -15
  38. package/api/src/libs/wallet-migration.ts +72 -53
  39. package/api/src/queues/auto-recharge.ts +1 -1
  40. package/api/src/queues/credit-consume.ts +94 -12
  41. package/api/src/queues/credit-grant.ts +4 -0
  42. package/api/src/queues/event.ts +14 -2
  43. package/api/src/queues/invoice.ts +1 -0
  44. package/api/src/queues/payment.ts +83 -15
  45. package/api/src/queues/refund.ts +84 -71
  46. package/api/src/queues/subscription.ts +1 -0
  47. package/api/src/routes/checkout-sessions.ts +82 -43
  48. package/api/src/routes/connect/change-payment.ts +2 -0
  49. package/api/src/routes/connect/change-plan.ts +2 -0
  50. package/api/src/routes/connect/pay.ts +12 -3
  51. package/api/src/routes/connect/setup.ts +3 -1
  52. package/api/src/routes/connect/shared.ts +52 -39
  53. package/api/src/routes/connect/subscribe.ts +4 -1
  54. package/api/src/routes/credit-grants.ts +25 -17
  55. package/api/src/routes/donations.ts +2 -2
  56. package/api/src/routes/meter-events.ts +16 -6
  57. package/api/src/routes/payment-links.ts +1 -1
  58. package/api/src/routes/payment-methods.ts +1 -1
  59. package/api/src/routes/settings.ts +1 -1
  60. package/api/src/routes/tax-rates.ts +1 -1
  61. package/api/src/store/models/customer.ts +23 -1
  62. package/api/src/store/models/payment-method.ts +4 -0
  63. package/api/src/store/models/price.ts +23 -14
  64. package/api/tests/libs/wallet-migration.spec.ts +4 -4
  65. package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
  66. package/api/tests/queues/credit-consume.spec.ts +8 -4
  67. package/api/tests/routes/credit-grants.spec.ts +1 -0
  68. package/blocklet.yml +1 -1
  69. package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
  70. package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
  71. package/cloudflare/README.md +499 -0
  72. package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
  73. package/cloudflare/build.ts +151 -0
  74. package/cloudflare/did-connect-auth.ts +527 -0
  75. package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
  76. package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
  77. package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
  78. package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
  79. package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
  80. package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
  81. package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
  82. package/cloudflare/frontend-shims/js-sdk.ts +43 -0
  83. package/cloudflare/frontend-shims/mime-types.ts +46 -0
  84. package/cloudflare/frontend-shims/session.ts +24 -0
  85. package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
  86. package/cloudflare/index.html +40 -0
  87. package/cloudflare/migrate-to-d1.js +252 -0
  88. package/cloudflare/migrations/0001_initial_schema.sql +82 -0
  89. package/cloudflare/migrations/0002_indexes.sql +75 -0
  90. package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
  91. package/cloudflare/run-build.js +390 -0
  92. package/cloudflare/scripts/test-decrypt.js +102 -0
  93. package/cloudflare/shims/arcblock-ws.ts +20 -0
  94. package/cloudflare/shims/axios-http-adapter.ts +4 -0
  95. package/cloudflare/shims/axios-lite.ts +117 -0
  96. package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
  97. package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
  98. package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
  99. package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
  100. package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
  101. package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
  102. package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
  103. package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
  104. package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
  105. package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
  106. package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
  107. package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
  108. package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
  109. package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
  110. package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
  111. package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
  112. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
  113. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
  114. package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
  115. package/cloudflare/shims/cookie-parser.ts +3 -0
  116. package/cloudflare/shims/cors.ts +21 -0
  117. package/cloudflare/shims/cron.ts +189 -0
  118. package/cloudflare/shims/crypto-js-warn.ts +7 -0
  119. package/cloudflare/shims/did-space-js.ts +17 -0
  120. package/cloudflare/shims/did-space.ts +11 -0
  121. package/cloudflare/shims/error.ts +18 -0
  122. package/cloudflare/shims/express-compat/index.ts +80 -0
  123. package/cloudflare/shims/express-compat/types.ts +41 -0
  124. package/cloudflare/shims/fastq.ts +105 -0
  125. package/cloudflare/shims/lock.ts +115 -0
  126. package/cloudflare/shims/mime-types.ts +56 -0
  127. package/cloudflare/shims/nedb-storage.ts +9 -0
  128. package/cloudflare/shims/node-child-process.ts +9 -0
  129. package/cloudflare/shims/node-fs.ts +20 -0
  130. package/cloudflare/shims/node-http.ts +13 -0
  131. package/cloudflare/shims/node-https.ts +4 -0
  132. package/cloudflare/shims/node-misc.ts +15 -0
  133. package/cloudflare/shims/node-net.ts +8 -0
  134. package/cloudflare/shims/node-os.ts +14 -0
  135. package/cloudflare/shims/node-tty.ts +8 -0
  136. package/cloudflare/shims/node-zlib.ts +17 -0
  137. package/cloudflare/shims/noop.ts +26 -0
  138. package/cloudflare/shims/payment-vendor.ts +14 -0
  139. package/cloudflare/shims/querystring.ts +12 -0
  140. package/cloudflare/shims/queue.ts +585 -0
  141. package/cloudflare/shims/rolldown-runtime.ts +43 -0
  142. package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
  143. package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
  144. package/cloudflare/shims/sequelize-d1/index.ts +34 -0
  145. package/cloudflare/shims/sequelize-d1/model.ts +1157 -0
  146. package/cloudflare/shims/sequelize-d1/operators.ts +293 -0
  147. package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
  148. package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
  149. package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
  150. package/cloudflare/shims/sequelize-d1/types.ts +35 -0
  151. package/cloudflare/shims/stripe-cf.ts +29 -0
  152. package/cloudflare/shims/ws-lite.ts +103 -0
  153. package/cloudflare/shims/xss.ts +3 -0
  154. package/cloudflare/tests/shims/cron.spec.ts +210 -0
  155. package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
  156. package/cloudflare/vite.config.ts +162 -0
  157. package/cloudflare/worker.ts +1553 -0
  158. package/cloudflare/wrangler.json +63 -0
  159. package/cloudflare/wrangler.jsonc +69 -0
  160. package/cloudflare/wrangler.staging.json +66 -0
  161. package/cloudflare/wrangler.toml +28 -0
  162. package/jest.config.js +4 -12
  163. package/package.json +26 -22
  164. package/src/app.tsx +62 -4
  165. package/src/components/customer/link.tsx +9 -13
  166. package/src/components/customer/notification-preference.tsx +3 -2
  167. package/src/components/filter-toolbar.tsx +4 -0
  168. package/src/components/invoice/list.tsx +9 -1
  169. package/src/components/invoice-pdf/utils.ts +2 -1
  170. package/src/components/layout/admin.tsx +39 -5
  171. package/src/components/layout/user-cf.tsx +77 -0
  172. package/src/components/payment-intent/actions.tsx +23 -3
  173. package/src/components/safe-did-address.tsx +75 -0
  174. package/src/libs/patch-user-card.ts +25 -0
  175. package/src/libs/util.ts +5 -7
  176. package/src/pages/admin/billing/meter-events/index.tsx +4 -0
  177. package/src/pages/admin/customers/customers/detail.tsx +2 -2
  178. package/src/pages/admin/customers/customers/index.tsx +2 -2
  179. package/src/pages/admin/overview.tsx +3 -1
  180. package/src/pages/customer/subscription/detail.tsx +4 -4
  181. package/tsconfig.api.json +1 -6
  182. package/tsconfig.json +3 -4
  183. package/tsconfig.types.json +2 -1
  184. package/vite.config.ts +6 -1
@@ -0,0 +1,585 @@
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 — job is safe in D1, cron will dispatch
413
+ }
414
+ } else {
415
+ // No CF Queue binding — execute inline (Blocklet Server compatibility)
416
+ try {
417
+ const data = await executeJob(jobId, job, persist);
418
+ emit('finished', data);
419
+ } catch (err: any) {
420
+ emit('failed', { id: jobId, job, error: err });
421
+ }
422
+ }
423
+ } else {
424
+ // === Delayed job ===
425
+ // Only persist to D1 with will_run_at. Cron dispatches when due.
426
+ // NOT sent to CF Queue — allows cancel/replace via D1 delete.
427
+ if (persist) {
428
+ const attrs: any = { delay: delaySeconds, will_run_at: Date.now() + delaySeconds * 1000 };
429
+ try {
430
+ await store.addJob(jobId, job, attrs, skipDuplicateCheck);
431
+ } catch (err: any) {
432
+ const isDuplicate = err?.code === 'JOB_DUPLICATE' ||
433
+ err?.message?.includes('UNIQUE constraint failed') ||
434
+ err?.message?.includes('SQLITE_CONSTRAINT');
435
+ if (isDuplicate) {
436
+ return;
437
+ }
438
+ console.error(`[queue:${name}] addJob error for delayed job`, err?.message);
439
+ return;
440
+ }
441
+ }
442
+ emit('queued', { id: jobId, job, persist });
443
+ }
444
+ };
445
+
446
+ const promise = enqueue()
447
+ .catch((err) => console.error(`[queue:${name}] push error:`, err?.message || err));
448
+
449
+ // Register promise to keep Worker alive
450
+ const isHttp = (globalThis as any).__cfHttpContext__;
451
+ if (isHttp && _waitUntil) {
452
+ _waitUntil(promise);
453
+ } else {
454
+ _pendingPushJobs.push(promise);
455
+ if (_waitUntil) {
456
+ _waitUntil(promise);
457
+ }
458
+ }
459
+
460
+ (jobEvents as any).id = jobId;
461
+ return jobEvents;
462
+ };
463
+
464
+ // pushAndWait always runs inline (caller needs the result synchronously)
465
+ const pushAndWait = async (params: PushParams<T>) => {
466
+ if (!params.job) {
467
+ throw new Error('Can not queue empty job');
468
+ }
469
+
470
+ const jobId = getJobId(params.id, params.job);
471
+ const persist = params.persist !== false;
472
+
473
+ if (persist) {
474
+ try {
475
+ await store.addJob(jobId, params.job, {}, params.skipDuplicateCheck || false);
476
+ } catch (err: any) {
477
+ const isDuplicate = err?.code === 'JOB_DUPLICATE' ||
478
+ err?.message?.includes('UNIQUE constraint failed') ||
479
+ err?.message?.includes('SQLITE_CONSTRAINT');
480
+ if (isDuplicate) {
481
+ console.log(`[queue:${name}] pushAndWait duplicate job`, jobId);
482
+ } else {
483
+ throw err;
484
+ }
485
+ }
486
+ }
487
+
488
+ try {
489
+ const data = await executeJob(jobId, params.job, persist);
490
+ queueEvents.emit('finished', data);
491
+ return data;
492
+ } catch (err: any) {
493
+ queueEvents.emit('failed', { id: jobId, job: params.job, error: err });
494
+ throw { id: jobId, job: params.job, error: err };
495
+ }
496
+ };
497
+
498
+ const cancel = async (id: string) => {
499
+ try {
500
+ const doc = await store.updateJob(id, { cancelled: true });
501
+ return doc ? doc.job : null;
502
+ } catch (_e) {
503
+ return null;
504
+ }
505
+ };
506
+
507
+ const getJob = async (id: string) => {
508
+ try {
509
+ const doc = await store.getJob(id);
510
+ return doc ? doc.job : null;
511
+ } catch (_e) {
512
+ return null;
513
+ }
514
+ };
515
+
516
+ const deleteJob = async (id: string, knownExists: boolean = false): Promise<boolean> => {
517
+ if (!knownExists) {
518
+ const exists = await getJob(id);
519
+ if (!exists) return false;
520
+ }
521
+ try {
522
+ await store.deleteJob(id);
523
+ return true;
524
+ } catch (_e) {
525
+ try { await cancel(id); } catch (_e2) { /* ignore */ }
526
+ return false;
527
+ }
528
+ };
529
+
530
+ const updateJob = async (id: string, updates: any) => {
531
+ return store.updateJob(id, updates);
532
+ };
533
+
534
+ const queueInstance = Object.assign(queueEvents, {
535
+ store,
536
+ push,
537
+ pushAndWait,
538
+ drain: (_cb: any) => {},
539
+ empty: (_cb: any) => {},
540
+ saturated: (_cb: any) => {},
541
+ error: (_cb: any) => {},
542
+ get: getJob,
543
+ delete: deleteJob,
544
+ cancel,
545
+ update: updateJob,
546
+ executeJob,
547
+ options: {
548
+ concurrency,
549
+ maxRetries,
550
+ maxTimeout,
551
+ retryDelay,
552
+ enableScheduledJob,
553
+ },
554
+ });
555
+
556
+ // Register handler for CF Queue consumer dispatch
557
+ _handlers.set(name, { onJob, executeJob, store, cancel });
558
+
559
+ // Register for scheduled job dispatch (cron picks up due delayed jobs)
560
+ if (enableScheduledJob) {
561
+ _scheduledQueues.set(name, { store, executeJob, cancel, name });
562
+ }
563
+
564
+ return queueInstance;
565
+ }
566
+
567
+ // Exported for unit tests; not part of the public queue API.
568
+ export const __test__ = {
569
+ resetHandlers() {
570
+ _handlers.clear();
571
+ _scheduledQueues.clear();
572
+ _pendingPushJobs.length = 0;
573
+ _trackedTimers.length = 0;
574
+ },
575
+ registerForTest(name: string, overrides: Partial<RegisteredHandler>) {
576
+ const noop = async () => undefined;
577
+ const store = createQueueStore(name);
578
+ _handlers.set(name, {
579
+ onJob: overrides.onJob ?? noop,
580
+ executeJob: overrides.executeJob ?? (noop as any),
581
+ cancel: overrides.cancel ?? (noop as any),
582
+ store: overrides.store ?? store,
583
+ });
584
+ },
585
+ };
@@ -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
+ };
@@ -0,0 +1,46 @@
1
+ // Sequelize helper functions: literal(), fn(), col()
2
+ // Used sparingly in Payment Kit (~7 places)
3
+
4
+ export function literal(sql: string) {
5
+ return { __literal: true, sql };
6
+ }
7
+
8
+ export function fn(name: string, ...args: any[]) {
9
+ return { __fn: true, name, args };
10
+ }
11
+
12
+ export function col(name: string) {
13
+ return { __col: true, name };
14
+ }
15
+
16
+ // Convert helpers to SQL string
17
+ export function helperToSQL(value: any): string {
18
+ if (!value || typeof value !== 'object') return String(value);
19
+ if (value.__literal) return value.sql;
20
+ if (value.__col) {
21
+ // Handle dotted col names
22
+ if (value.name.includes('.')) return `"${value.name.replace('.', '"."')}"`;
23
+ return `"${value.name}"`;
24
+ }
25
+ if (value.__fn) {
26
+ const args = value.args.map((a: any) => {
27
+ if (a === null || a === undefined) return 'NULL';
28
+ if (typeof a === 'string') {
29
+ // String args in fn() are usually column names (e.g., fn('COUNT', col('id')))
30
+ // But literal strings passed to fn should be quoted differently
31
+ // Convention: use col() for column refs; bare strings are treated as column names too
32
+ return `"${a}"`;
33
+ }
34
+ if (typeof a === 'number') return String(a);
35
+ return helperToSQL(a);
36
+ });
37
+ return `${value.name}(${args.join(', ')})`;
38
+ }
39
+ // Sequelize.where() wrapper
40
+ if (value.__sequelizeWhere) {
41
+ const lhsSQL = helperToSQL(value.lhs);
42
+ const rhsSQL = value.rhs === null ? 'NULL' : '?';
43
+ return `${lhsSQL} = ${rhsSQL}`;
44
+ }
45
+ return String(value);
46
+ }