payment-kit 1.29.1 → 1.29.2
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.
- package/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +36 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +27 -24
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +99 -38
- package/api/src/libs/context.ts +78 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +81 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +50 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +259 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +73 -0
- package/api/src/middlewares/hono/csrf.ts +72 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +214 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +17 -12
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +12 -4
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +41 -28
- package/api/src/queues/payout.ts +9 -5
- package/api/src/queues/refund.ts +18 -12
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +667 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +66 -22
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +236 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/README.md +26 -6
- package/cloudflare/build.ts +28 -13
- package/cloudflare/did-connect-auth.ts +0 -217
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +10 -56
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
- package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
- package/cloudflare/worker.ts +204 -433
- package/cloudflare/wrangler.local-e2e.jsonc +26 -0
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/env.d.ts +13 -1
- package/tsconfig.json +1 -1
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
package/api/src/libs/lock.ts
CHANGED
|
@@ -1,53 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
// Phase 8 (W2-1a): lock facade over the locks slot driver.
|
|
2
|
+
//
|
|
3
|
+
// Call sites keep using `getLock(name)` unchanged. Behind the facade:
|
|
4
|
+
// - the active locks driver is injectable (`setLocksDriver`) — default is the
|
|
5
|
+
// in-process memory driver (Blocklet Server / Node). The worker injects the
|
|
6
|
+
// D1 driver through the locks slot.
|
|
7
|
+
// - lock names are tenant-prefixed (W1 §2.2) by default. Per-resource locks
|
|
8
|
+
// are tenant-scoped; process-level singleton guards (queue start guards,
|
|
9
|
+
// recovery) opt out with `{ scope: 'global' }`. In single-tenant mode the
|
|
10
|
+
// prefix is the constant deployment app DID, so lock identity is unchanged.
|
|
11
|
+
|
|
12
|
+
import { getInstanceDid } from './context';
|
|
13
|
+
import {
|
|
14
|
+
createMemoryLocksDriver,
|
|
15
|
+
scopedLockName,
|
|
16
|
+
MemoryLock,
|
|
17
|
+
type LockHandle,
|
|
18
|
+
type LocksDriver,
|
|
19
|
+
type LockScope,
|
|
20
|
+
} from './drivers';
|
|
21
|
+
|
|
22
|
+
let activeDriver: LocksDriver = createMemoryLocksDriver();
|
|
23
|
+
|
|
24
|
+
/** Inject the locks driver (worker wires the D1 driver here). */
|
|
25
|
+
export function setLocksDriver(driver: LocksDriver): void {
|
|
26
|
+
activeDriver = driver;
|
|
27
|
+
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.locked = true;
|
|
30
|
-
resolve(true);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
}
|
|
29
|
+
export function getLocksDriver(): LocksDriver {
|
|
30
|
+
return activeDriver;
|
|
31
|
+
}
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
33
|
+
export interface GetLockOptions {
|
|
34
|
+
ttl?: number;
|
|
35
|
+
/** 'tenant' (default) prefixes the lock name with the current tenant; 'global' keeps the bare name. */
|
|
36
|
+
scope?: LockScope;
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Acquire a handle to a named lock. Tenant-scoped by default; resolving the
|
|
41
|
+
* tenant fails closed in multi-tenant mode when no context is present (matching
|
|
42
|
+
* the scoped-query helpers from Phase 3).
|
|
43
|
+
*/
|
|
44
|
+
export function getLock(name: string, options: GetLockOptions = {}): LockHandle {
|
|
45
|
+
if (!name || typeof name !== 'string') {
|
|
46
|
+
throw new Error('getLock: a non-empty lock name is required');
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return lock;
|
|
48
|
+
const scope: LockScope = options.scope ?? 'tenant';
|
|
49
|
+
const instanceDid = scope === 'tenant' ? getInstanceDid() : null;
|
|
50
|
+
const resolvedName = scopedLockName(name, instanceDid, scope);
|
|
51
|
+
return activeDriver.getLock(resolvedName, options.ttl !== undefined ? { ttl: options.ttl } : undefined);
|
|
53
52
|
}
|
|
53
|
+
|
|
54
|
+
// `Lock` is the in-process memory lock class — kept as a public export so the
|
|
55
|
+
// standalone constructor form (`new Lock(name)`) and `LockHandle` annotations
|
|
56
|
+
// both keep working.
|
|
57
|
+
export { MemoryLock as Lock };
|
package/api/src/libs/logger.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
// Phase 13b: lazy logging boundary.
|
|
2
|
+
//
|
|
3
|
+
// @blocklet/logger throws at REQUIRE time when BLOCKLET_LOG_DIR is absent
|
|
4
|
+
// ("valid BLOCKLET_LOG_DIR env is required by logger"). That made importing the
|
|
5
|
+
// core — and therefore createEmbeddedPaymentService / rpc.entitlements.check,
|
|
6
|
+
// which transitively import this module — demand a blocklet log env even for a
|
|
7
|
+
// bare host (arc embedding before its blocklet runtime is wired). Defer the
|
|
8
|
+
// require to first use and fall back to console when no blocklet log env exists.
|
|
9
|
+
// The blocklet server (BLOCKLET_LOG_DIR set) and the CF worker (aliased shim)
|
|
10
|
+
// still get the real logger on first call — behavior unchanged for them.
|
|
2
11
|
|
|
3
12
|
interface Logger {
|
|
4
13
|
debug: (...args: any[]) => void;
|
|
@@ -7,14 +16,45 @@ interface Logger {
|
|
|
7
16
|
warn: (...args: any[]) => void;
|
|
8
17
|
}
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
const instance = createLogger(label || '');
|
|
12
|
-
return instance;
|
|
13
|
-
};
|
|
19
|
+
let resolved: Logger | undefined;
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
function resolveLogger(): Logger {
|
|
22
|
+
if (resolved) return resolved;
|
|
23
|
+
try {
|
|
24
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
25
|
+
const createLogger = require('@blocklet/logger');
|
|
26
|
+
resolved = createLogger('app') as Logger;
|
|
27
|
+
} catch {
|
|
28
|
+
// bare host without a blocklet log env — console fallback.
|
|
29
|
+
/* eslint-disable no-console */
|
|
30
|
+
resolved = {
|
|
31
|
+
debug: (...args: any[]) => console.debug(...args),
|
|
32
|
+
info: (...args: any[]) => console.info(...args),
|
|
33
|
+
error: (...args: any[]) => console.error(...args),
|
|
34
|
+
warn: (...args: any[]) => console.warn(...args),
|
|
35
|
+
};
|
|
36
|
+
/* eslint-enable no-console */
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const logger: Logger = {
|
|
42
|
+
debug: (...args: any[]) => resolveLogger().debug(...args),
|
|
43
|
+
info: (...args: any[]) => resolveLogger().info(...args),
|
|
44
|
+
error: (...args: any[]) => resolveLogger().error(...args),
|
|
45
|
+
warn: (...args: any[]) => resolveLogger().warn(...args),
|
|
46
|
+
};
|
|
16
47
|
|
|
17
48
|
export default logger;
|
|
18
49
|
|
|
19
|
-
|
|
20
|
-
|
|
50
|
+
// Express access logger — also lazy. A bare host that never mounts the node
|
|
51
|
+
// handler (where setupAccessLogger runs) never needs the blocklet log env.
|
|
52
|
+
export function setupAccessLogger(app: any): void {
|
|
53
|
+
try {
|
|
54
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
55
|
+
const createLogger = require('@blocklet/logger');
|
|
56
|
+
createLogger.setupAccessLogger?.(app);
|
|
57
|
+
} catch {
|
|
58
|
+
// bare host: no access logging
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import BlockletNotification from '@blocklet/sdk/service/notification';
|
|
2
2
|
|
|
3
3
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './template/base';
|
|
4
4
|
import { CheckoutSession, CreditGrant, Invoice, Meter, MeterEvent, Subscription } from '../../store/models';
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
3
|
import { withQuery } from 'ufo';
|
|
4
4
|
import { BN } from '@ocap/util';
|
|
5
|
+
import { creditLowBalanceThresholdPercentage } from '../../env';
|
|
5
6
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
6
7
|
import { translate } from '../../../locales';
|
|
7
8
|
import { Customer, PaymentCurrency, CreditGrant } from '../../../store/models';
|
|
@@ -35,7 +36,7 @@ export class CustomerCreditLowBalanceEmailTemplate implements BaseEmailTemplate<
|
|
|
35
36
|
* @returns threshold percentage (default: 10)
|
|
36
37
|
*/
|
|
37
38
|
static getLowBalanceThresholdPercent(): number {
|
|
38
|
-
return
|
|
39
|
+
return creditLowBalanceThresholdPercentage();
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import { getUrl } from '@blocklet/sdk';
|
|
3
|
+
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
4
4
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
5
5
|
import { translate } from '../../../locales';
|
|
6
6
|
import { CheckoutSession, Customer, PaymentLink, PaymentMethod, Payout } from '../../../store/models';
|
|
@@ -4,7 +4,7 @@ import isEmpty from 'lodash/isEmpty';
|
|
|
4
4
|
import pWaitFor from 'p-wait-for';
|
|
5
5
|
import type { LiteralUnion } from 'type-fest';
|
|
6
6
|
|
|
7
|
-
import { getUrl } from '@blocklet/sdk';
|
|
7
|
+
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
8
8
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
9
9
|
import { translate } from '../../../locales';
|
|
10
10
|
import {
|
package/api/src/libs/payout.ts
CHANGED
|
@@ -6,15 +6,91 @@ import fastq from 'fastq';
|
|
|
6
6
|
import { nanoid } from 'nanoid';
|
|
7
7
|
|
|
8
8
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
9
|
+
import { isTestEnv } from '../env';
|
|
9
10
|
import logger from '../logger';
|
|
10
|
-
import {
|
|
11
|
+
import { context, withTenant } from '../context';
|
|
12
|
+
import {
|
|
13
|
+
TENANT_CONTEXT_MISSING,
|
|
14
|
+
TENANT_MISMATCH,
|
|
15
|
+
TenantError,
|
|
16
|
+
assertValidInstanceDid,
|
|
17
|
+
getDefaultInstanceDid,
|
|
18
|
+
getTenantMode,
|
|
19
|
+
resolveRowTenant,
|
|
20
|
+
} from '../tenant';
|
|
21
|
+
import { tryWithTimeout } from '../util';
|
|
11
22
|
import dayjs from '../dayjs';
|
|
12
23
|
import createQueueStore from './store';
|
|
24
|
+
import { registerQueue, getQueueRuntimeMode, trackPending } from './runtime';
|
|
13
25
|
import { Job } from '../../store/models/job';
|
|
14
26
|
import { sequelize } from '../../store/sequelize';
|
|
15
27
|
|
|
16
28
|
const CANCELLED = '__CANCELLED__';
|
|
17
|
-
|
|
29
|
+
// lazy so the injected config (set after import) is honored at call time
|
|
30
|
+
const minDelay = (): number => (isTestEnv() ? 2 : 8);
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Phase 5 (W1-4a): generic queue tenant layer.
|
|
34
|
+
// Every queue gets this uniformly — see docs/arc-integration/planning/w1-w2/
|
|
35
|
+
// queue-matrix.md for the per-queue conclusions built on top of it.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stamp the active tenant into a job payload at enqueue time.
|
|
40
|
+
* - payload already carries instance_did: validated (and in multi mode it
|
|
41
|
+
* must be a legal DID — a forged/illegal value is refused at the gate)
|
|
42
|
+
* - otherwise: the context tenant (single mode = app DID); multi mode
|
|
43
|
+
* without context -> the push itself is rejected (fail-closed)
|
|
44
|
+
*/
|
|
45
|
+
function injectJobTenant(job: any): any {
|
|
46
|
+
if (!job || typeof job !== 'object') return job;
|
|
47
|
+
if (job.instance_did) {
|
|
48
|
+
assertValidInstanceDid(job.instance_did);
|
|
49
|
+
return job;
|
|
50
|
+
}
|
|
51
|
+
return { ...job, instance_did: context.getInstanceDid() };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run a job handler inside its payload tenant. Legacy jobs persisted before
|
|
56
|
+
* Phase 5 have no instance_did: single mode falls back to the deployment
|
|
57
|
+
* default, multi mode refuses permanently (non-retryable + structured alert).
|
|
58
|
+
*/
|
|
59
|
+
function runJobWithTenant<T>(job: any, onJob: (job: T) => Promise<any>): Promise<any> {
|
|
60
|
+
const tenant = job?.instance_did;
|
|
61
|
+
if (tenant) {
|
|
62
|
+
return withTenant(tenant, () => onJob(job));
|
|
63
|
+
}
|
|
64
|
+
if (getTenantMode() === 'single') {
|
|
65
|
+
return withTenant(getDefaultInstanceDid(), () => onJob(job));
|
|
66
|
+
}
|
|
67
|
+
const err = new TenantError(TENANT_CONTEXT_MISSING, 'legacy job without tenant refused in multi mode');
|
|
68
|
+
(err as any).nonRetryable = true;
|
|
69
|
+
logger.error('[queue] legacy job without tenant refused', { code: TENANT_CONTEXT_MISSING, job });
|
|
70
|
+
return Promise.reject(err);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Handler-entry invariant for object-bound queues: the object loaded by id
|
|
75
|
+
* must belong to the payload tenant the handler is running under. A forged
|
|
76
|
+
* payload (tenant A, object of B) dies here permanently — nothing executes.
|
|
77
|
+
*/
|
|
78
|
+
export function assertJobObjectTenant(row: { instance_did?: string | null } | null | undefined): void {
|
|
79
|
+
if (!row) return; // absent objects are the handler's own no-op/warn path
|
|
80
|
+
const rowTenant = resolveRowTenant(row);
|
|
81
|
+
const jobTenant = context.getInstanceDid();
|
|
82
|
+
if (rowTenant !== jobTenant) {
|
|
83
|
+
// Phase 4 (W1-3): emit a structured violation alert BEFORE throwing, so a
|
|
84
|
+
// forged/mixed cross-tenant job is observable in logs (same dedicated code
|
|
85
|
+
// as the W1 §4.1 event-rejection path) even if the queue swallows the
|
|
86
|
+
// thrown error. Loading the object cross-tenant (systemFindByPk) is what
|
|
87
|
+
// makes this violation visible instead of folding into a scoped null.
|
|
88
|
+
logger.error('TENANT_VIOLATION', { code: TENANT_MISMATCH, rowTenant, jobTenant });
|
|
89
|
+
const err = new TenantError(TENANT_MISMATCH, `job object belongs to ${rowTenant} but payload says ${jobTenant}`);
|
|
90
|
+
(err as any).nonRetryable = true;
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
18
94
|
|
|
19
95
|
type QueueOptions<T> = {
|
|
20
96
|
id?: (job: T) => string;
|
|
@@ -46,6 +122,18 @@ type PushParams<T> = {
|
|
|
46
122
|
delay?: number; // in seconds
|
|
47
123
|
runAt?: number; // unix timestamp in seconds
|
|
48
124
|
skipDuplicateCheck?: boolean; // Q1: skip addJob's findOne when caller guarantees no duplicate
|
|
125
|
+
/**
|
|
126
|
+
* Internal: re-delivery of a row already persisted in the jobs table
|
|
127
|
+
* (startup/scheduled recovery). Skips the push-side tenant gate so legacy
|
|
128
|
+
* pre-tenant rows reach runJobWithTenant, which owns the legacy strategy
|
|
129
|
+
* (single -> default tenant, multi -> structured non-retryable refusal).
|
|
130
|
+
*
|
|
131
|
+
* NEVER set this from application code: it bypasses the enqueue tenant
|
|
132
|
+
* gate. Object-bound handlers are still protected by
|
|
133
|
+
* assertJobObjectTenant, but system-operation queues (no object load)
|
|
134
|
+
* would run under whatever tenant the payload claims.
|
|
135
|
+
*/
|
|
136
|
+
fromStore?: boolean;
|
|
49
137
|
};
|
|
50
138
|
|
|
51
139
|
export default function createQueue<T = any>({ name, onJob, options = defaults }: QueueParams<T>) {
|
|
@@ -98,7 +186,7 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
98
186
|
}
|
|
99
187
|
|
|
100
188
|
try {
|
|
101
|
-
const result = await tryWithTimeout(() =>
|
|
189
|
+
const result = await tryWithTimeout(() => runJobWithTenant(job, onJob), maxTimeout);
|
|
102
190
|
logger.info('job finished', { id, result });
|
|
103
191
|
cb(null, result);
|
|
104
192
|
} catch (err) {
|
|
@@ -108,7 +196,15 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
108
196
|
// @ts-ignore
|
|
109
197
|
}, concurrency);
|
|
110
198
|
|
|
111
|
-
const push = ({
|
|
199
|
+
const push = ({
|
|
200
|
+
job: rawJob,
|
|
201
|
+
id,
|
|
202
|
+
persist = true,
|
|
203
|
+
delay,
|
|
204
|
+
runAt,
|
|
205
|
+
skipDuplicateCheck = false,
|
|
206
|
+
fromStore = false,
|
|
207
|
+
}: PushParams<T>) => {
|
|
112
208
|
const jobEvents = new EventEmitter();
|
|
113
209
|
const emit = (e: string, data: any) => {
|
|
114
210
|
queueEvents.emit(e, data);
|
|
@@ -116,20 +212,24 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
116
212
|
};
|
|
117
213
|
const now = dayjs().unix();
|
|
118
214
|
|
|
119
|
-
if (!
|
|
215
|
+
if (!rawJob) {
|
|
120
216
|
throw new Error('Can not queue empty job');
|
|
121
217
|
}
|
|
122
218
|
|
|
219
|
+
// fail-closed gate: every NEW payload carries its tenant from here on;
|
|
220
|
+
// store re-deliveries pass through so the execution-side legacy strategy applies
|
|
221
|
+
const job = (fromStore ? rawJob : injectJobTenant(rawJob)) as T;
|
|
222
|
+
|
|
123
223
|
const jobId = getJobId(id, job);
|
|
124
224
|
|
|
125
|
-
if ((delay && delay >=
|
|
225
|
+
if ((delay && delay >= minDelay()) || (runAt && runAt > now)) {
|
|
126
226
|
if (!enableScheduledJob) {
|
|
127
227
|
throw new Error('Must set options.enableScheduledJob to true to run delay jobs');
|
|
128
228
|
}
|
|
129
229
|
|
|
130
230
|
// 这里不是精确的 delay, 延迟的时间太短没有意义,所以这里限制了最小 delay
|
|
131
|
-
if (delay && delay <
|
|
132
|
-
throw new Error(`minimum delay is ${
|
|
231
|
+
if (delay && delay < minDelay()) {
|
|
232
|
+
throw new Error(`minimum delay is ${minDelay()}s`);
|
|
133
233
|
}
|
|
134
234
|
|
|
135
235
|
const attrs: { delay?: number; will_run_at?: number } = {};
|
|
@@ -221,12 +321,31 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
221
321
|
queue.push({ id: jobId, job, persist }, onJobComplete);
|
|
222
322
|
});
|
|
223
323
|
|
|
324
|
+
// Phase 12b: workerd flush. Track this immediate execution so a frozen-
|
|
325
|
+
// isolate host can drain it before returning the response — otherwise the
|
|
326
|
+
// fire-and-forget setImmediate/fastq work is dropped when the isolate is
|
|
327
|
+
// torn down. Tracked ONLY on a workerd host: zero overhead and no extra
|
|
328
|
+
// listeners on a long-lived node process.
|
|
329
|
+
let resolveSettled: () => void = () => {};
|
|
330
|
+
if (getQueueRuntimeMode() === 'workerd') {
|
|
331
|
+
const settled = new Promise<void>((resolve) => {
|
|
332
|
+
resolveSettled = resolve;
|
|
333
|
+
});
|
|
334
|
+
jobEvents.on('finished', resolveSettled);
|
|
335
|
+
jobEvents.on('failed', resolveSettled);
|
|
336
|
+
jobEvents.on('cancelled', resolveSettled);
|
|
337
|
+
trackPending(settled);
|
|
338
|
+
}
|
|
339
|
+
|
|
224
340
|
if (persist) {
|
|
225
341
|
store
|
|
226
342
|
.addJob(jobId, job, {}, skipDuplicateCheck)
|
|
227
343
|
.then(queueJob)
|
|
228
344
|
.catch((err) => {
|
|
229
345
|
logger.error('Can not add job to store', { error: err });
|
|
346
|
+
// never enqueued (e.g. duplicate id) → emits nothing; release the
|
|
347
|
+
// workerd flush tracker so flushQueueWork() cannot hang on it.
|
|
348
|
+
resolveSettled();
|
|
230
349
|
});
|
|
231
350
|
} else {
|
|
232
351
|
queueJob();
|
|
@@ -246,7 +365,11 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
246
365
|
|
|
247
366
|
const job = push(params);
|
|
248
367
|
job.on('finished', (data: { id: string; job: T; result: any }) => resolve(data));
|
|
249
|
-
|
|
368
|
+
// Phase 12b: the engine emits 'cancelled' (two L); the old 'canceled'
|
|
369
|
+
// listener never fired, so a cancelled job left pushAndWait hanging
|
|
370
|
+
// forever — and the CF queue() consumer now runs through pushAndWait,
|
|
371
|
+
// where that hang would stall the queue batch until CF kills it.
|
|
372
|
+
job.on('cancelled', (data: { id: string; job: T }) => resolve(data));
|
|
250
373
|
job.on('failed', (data: { id: string; job: T; error: Error }) => reject(data));
|
|
251
374
|
} catch (err) {
|
|
252
375
|
reject(err);
|
|
@@ -296,60 +419,128 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
296
419
|
return updatedJob;
|
|
297
420
|
};
|
|
298
421
|
|
|
299
|
-
// Populate the queue on startup
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
422
|
+
// Populate the queue on startup. Phase 12b: this is a long-lived-process
|
|
423
|
+
// recovery — re-queue persisted immediate rows on boot. A workerd host spins
|
|
424
|
+
// up a fresh isolate per request and MUST NOT re-run every persisted row on
|
|
425
|
+
// each cold start; it drives due-job re-dispatch through dispatchDueJobs()
|
|
426
|
+
// from scheduled() instead. So the boot recovery is node-mode only.
|
|
427
|
+
if (getQueueRuntimeMode() === 'node') {
|
|
428
|
+
process.nextTick(async () => {
|
|
429
|
+
try {
|
|
430
|
+
if (!Job.isInitialized()) {
|
|
431
|
+
Job.initialize(sequelize);
|
|
432
|
+
}
|
|
433
|
+
const jobs = await store.getJobs();
|
|
434
|
+
jobs.forEach((x) => {
|
|
435
|
+
if (x.job && x.id) {
|
|
436
|
+
try {
|
|
437
|
+
push({ job: x.job, id: x.id, persist: false, fromStore: true });
|
|
438
|
+
} catch (err: any) {
|
|
439
|
+
// one bad row must never break recovery of the rest
|
|
440
|
+
logger.error('failed to re-queue stored job', { id: x.id, code: err?.code, message: err?.message });
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
logger.info('skip invalid job from db', { job: x });
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
// eslint-disable-next-line no-shadow
|
|
447
|
+
} catch (err) {
|
|
448
|
+
console.error(err);
|
|
449
|
+
logger.error(`Can not load existing ${name} jobs`, { error: err });
|
|
304
450
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Re-deliver this queue's due delayed rows once — the body shared by the node
|
|
455
|
+
// loop() (timer-driven) and the host dispatchDueJobs() (CF scheduled()-driven).
|
|
456
|
+
// Same cancel-then-replay-from-store path either way, so worker and node agree
|
|
457
|
+
// on exactly how a due delayed job runs.
|
|
458
|
+
const redispatchDue = async (): Promise<{ dispatched: number; failed: number }> => {
|
|
459
|
+
let dispatched = 0;
|
|
460
|
+
let failed = 0;
|
|
461
|
+
if (enableScheduledJob !== true) return { dispatched, failed };
|
|
462
|
+
const jobs = await store.getScheduledJobs();
|
|
463
|
+
for (const x of jobs) {
|
|
464
|
+
if (x.job && x.id) {
|
|
465
|
+
// fix: https://github.com/blocklet/payment-kit/issues/287
|
|
466
|
+
// Intentional cancel-then-replay: marking the row cancelled=true keeps
|
|
467
|
+
// the NEXT due-poll (loop tick / dispatchDueJobs) from re-picking it
|
|
468
|
+
// while this run is in flight; the immediate re-push below clears the
|
|
469
|
+
// in-memory cancel flag for THIS execution, and onJobComplete deletes
|
|
470
|
+
// the row on success. A reader watching raw DB state mid-dispatch will
|
|
471
|
+
// briefly see cancelled=true — that is the de-dupe latch, not an error.
|
|
472
|
+
// eslint-disable-next-line no-await-in-loop
|
|
473
|
+
await cancel(x.id);
|
|
474
|
+
logger.info('reschedule delayed or scheduled job', { id: x.id, job: x.job });
|
|
475
|
+
try {
|
|
476
|
+
push({ job: x.job, id: x.id, persist: false, fromStore: true });
|
|
477
|
+
dispatched += 1;
|
|
478
|
+
} catch (err: any) {
|
|
479
|
+
failed += 1;
|
|
480
|
+
logger.error('failed to reschedule stored job', { id: x.id, code: err?.code, message: err?.message });
|
|
311
481
|
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.error(err);
|
|
316
|
-
logger.error(`Can not load existing ${name} jobs`, { error: err });
|
|
482
|
+
} else {
|
|
483
|
+
logger.info('skip invalid job from db', { job: x });
|
|
484
|
+
}
|
|
317
485
|
}
|
|
318
|
-
|
|
486
|
+
return { dispatched, failed };
|
|
487
|
+
};
|
|
319
488
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
489
|
+
// D2 teardown: the node poll loop is cancelable. `stopLoop()` flips the flag
|
|
490
|
+
// AND clears the pending sleep timer (so the process has no dangling handle on
|
|
491
|
+
// stop — the spec's "active handles 归零") and resolves the in-flight sleep so
|
|
492
|
+
// the loop observes the flag and returns immediately.
|
|
493
|
+
let loopStopped = false;
|
|
494
|
+
let loopTimer: ReturnType<typeof setTimeout> | null = null;
|
|
495
|
+
let loopWake: (() => void) | null = null;
|
|
496
|
+
const cancelableSleep = (ms: number): Promise<void> =>
|
|
497
|
+
new Promise<void>((resolve) => {
|
|
498
|
+
loopWake = resolve;
|
|
499
|
+
loopTimer = setTimeout(() => {
|
|
500
|
+
loopTimer = null;
|
|
501
|
+
loopWake = null;
|
|
502
|
+
resolve();
|
|
503
|
+
}, ms);
|
|
504
|
+
});
|
|
505
|
+
const stopLoop = (): void => {
|
|
506
|
+
loopStopped = true;
|
|
507
|
+
if (loopTimer) {
|
|
508
|
+
clearTimeout(loopTimer);
|
|
509
|
+
loopTimer = null;
|
|
510
|
+
}
|
|
511
|
+
if (loopWake) {
|
|
512
|
+
const wake = loopWake;
|
|
513
|
+
loopWake = null;
|
|
514
|
+
wake();
|
|
515
|
+
}
|
|
516
|
+
};
|
|
325
517
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
518
|
+
// The node poll loop. Disabled on a workerd host: a frozen isolate cannot run
|
|
519
|
+
// a background timer, so the host calls dispatchDueJobs() from scheduled()
|
|
520
|
+
// instead (which runs redispatchDue() above — the same code path).
|
|
521
|
+
const loop = async () => {
|
|
522
|
+
if (enableScheduledJob !== true) return;
|
|
523
|
+
if (getQueueRuntimeMode() !== 'node') return;
|
|
524
|
+
while (!loopStopped) {
|
|
525
|
+
// eslint-disable-next-line no-await-in-loop
|
|
526
|
+
await cancelableSleep((minDelay() * 1000) / 2);
|
|
527
|
+
// stopped during the sleep (teardown) — exit before doing any work.
|
|
528
|
+
if (loopStopped) return;
|
|
529
|
+
// mode can flip after assembly (a host that sets workerd late); stop then.
|
|
530
|
+
if (getQueueRuntimeMode() !== 'node') return;
|
|
531
|
+
try {
|
|
532
|
+
// eslint-disable-next-line no-await-in-loop
|
|
533
|
+
await redispatchDue();
|
|
534
|
+
} catch (err) {
|
|
535
|
+
console.error(err);
|
|
536
|
+
logger.error(`Can not load scheduled ${name} jobs`, { error: err });
|
|
346
537
|
}
|
|
347
538
|
}
|
|
348
539
|
};
|
|
349
540
|
|
|
350
541
|
loop();
|
|
351
542
|
|
|
352
|
-
|
|
543
|
+
const queueInstance = Object.assign(queueEvents, {
|
|
353
544
|
store,
|
|
354
545
|
push,
|
|
355
546
|
pushAndWait,
|
|
@@ -361,6 +552,8 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
361
552
|
delete: deleteJob,
|
|
362
553
|
cancel,
|
|
363
554
|
update: updateJob,
|
|
555
|
+
/** D2 teardown: stop this queue's node poll loop (no-op if not scheduled). */
|
|
556
|
+
stop: stopLoop,
|
|
364
557
|
options: {
|
|
365
558
|
concurrency,
|
|
366
559
|
maxRetries,
|
|
@@ -369,4 +562,18 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
369
562
|
enableScheduledJob,
|
|
370
563
|
},
|
|
371
564
|
});
|
|
565
|
+
|
|
566
|
+
// Phase 12b: register into the host-facing queue runtime surface so the CF
|
|
567
|
+
// worker can look the handle up by name (queue() consumer) and drive due
|
|
568
|
+
// re-dispatch (scheduled()) through the service/slot boundary — no more
|
|
569
|
+
// direct cloudflare/shims/queue.ts imports.
|
|
570
|
+
registerQueue({
|
|
571
|
+
name,
|
|
572
|
+
enableScheduledJob: enableScheduledJob === true,
|
|
573
|
+
handle: queueInstance,
|
|
574
|
+
redispatchDue,
|
|
575
|
+
stop: stopLoop,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return queueInstance;
|
|
372
579
|
}
|