@stacksjs/defaults 0.70.206 → 0.70.208
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/TODO.md +21 -0
- package/ai/AGENTS.md +79 -0
- package/ai/README.md +45 -0
- package/ai/claude/launch.json +12 -0
- package/ai/skills/stacks-actions/SKILL.md +128 -0
- package/ai/skills/stacks-ai/SKILL.md +276 -0
- package/ai/skills/stacks-alias/SKILL.md +94 -0
- package/ai/skills/stacks-analytics/SKILL.md +91 -0
- package/ai/skills/stacks-api/SKILL.md +738 -0
- package/ai/skills/stacks-arrays/SKILL.md +277 -0
- package/ai/skills/stacks-auth/SKILL.md +422 -0
- package/ai/skills/stacks-auto-imports/SKILL.md +138 -0
- package/ai/skills/stacks-brandkit/SKILL.md +811 -0
- package/ai/skills/stacks-browse/SKILL.md +127 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +492 -0
- package/ai/skills/stacks-browser/SKILL.md +145 -0
- package/ai/skills/stacks-buddy/SKILL.md +676 -0
- package/ai/skills/stacks-build/SKILL.md +163 -0
- package/ai/skills/stacks-cache/SKILL.md +125 -0
- package/ai/skills/stacks-calendar/SKILL.md +113 -0
- package/ai/skills/stacks-chat/SKILL.md +371 -0
- package/ai/skills/stacks-cli/SKILL.md +112 -0
- package/ai/skills/stacks-cloud/SKILL.md +192 -0
- package/ai/skills/stacks-cms/SKILL.md +217 -0
- package/ai/skills/stacks-collections/SKILL.md +82 -0
- package/ai/skills/stacks-commerce/SKILL.md +114 -0
- package/ai/skills/stacks-composables/SKILL.md +161 -0
- package/ai/skills/stacks-config/SKILL.md +144 -0
- package/ai/skills/stacks-configuration/SKILL.md +154 -0
- package/ai/skills/stacks-cron/SKILL.md +123 -0
- package/ai/skills/stacks-crosswind/SKILL.md +190 -0
- package/ai/skills/stacks-dashboard/SKILL.md +173 -0
- package/ai/skills/stacks-database/SKILL.md +286 -0
- package/ai/skills/stacks-datetime/SKILL.md +269 -0
- package/ai/skills/stacks-dependencies/SKILL.md +111 -0
- package/ai/skills/stacks-deploy/SKILL.md +102 -0
- package/ai/skills/stacks-design-brutalist/SKILL.md +110 -0
- package/ai/skills/stacks-design-minimalist/SKILL.md +115 -0
- package/ai/skills/stacks-design-output/SKILL.md +55 -0
- package/ai/skills/stacks-design-soft/SKILL.md +156 -0
- package/ai/skills/stacks-design-taste/SKILL.md +1012 -0
- package/ai/skills/stacks-design-taste-codex/SKILL.md +294 -0
- package/ai/skills/stacks-desktop/SKILL.md +78 -0
- package/ai/skills/stacks-development/SKILL.md +298 -0
- package/ai/skills/stacks-dns/SKILL.md +149 -0
- package/ai/skills/stacks-docs/SKILL.md +109 -0
- package/ai/skills/stacks-email/SKILL.md +236 -0
- package/ai/skills/stacks-enums/SKILL.md +100 -0
- package/ai/skills/stacks-env/SKILL.md +181 -0
- package/ai/skills/stacks-error-handling/SKILL.md +137 -0
- package/ai/skills/stacks-events/SKILL.md +228 -0
- package/ai/skills/stacks-faker/SKILL.md +158 -0
- package/ai/skills/stacks-git/SKILL.md +158 -0
- package/ai/skills/stacks-guard/SKILL.md +98 -0
- package/ai/skills/stacks-health/SKILL.md +75 -0
- package/ai/skills/stacks-http/SKILL.md +316 -0
- package/ai/skills/stacks-i18n/SKILL.md +198 -0
- package/ai/skills/stacks-image-to-code/SKILL.md +629 -0
- package/ai/skills/stacks-imagegen-mobile/SKILL.md +1474 -0
- package/ai/skills/stacks-imagegen-web/SKILL.md +998 -0
- package/ai/skills/stacks-investigate/SKILL.md +119 -0
- package/ai/skills/stacks-jobs/SKILL.md +136 -0
- package/ai/skills/stacks-lint/SKILL.md +184 -0
- package/ai/skills/stacks-listeners/SKILL.md +99 -0
- package/ai/skills/stacks-logging/SKILL.md +78 -0
- package/ai/skills/stacks-mail/SKILL.md +120 -0
- package/ai/skills/stacks-middleware/SKILL.md +249 -0
- package/ai/skills/stacks-migrations/SKILL.md +105 -0
- package/ai/skills/stacks-models/SKILL.md +267 -0
- package/ai/skills/stacks-new-feature/SKILL.md +206 -0
- package/ai/skills/stacks-notifications/SKILL.md +210 -0
- package/ai/skills/stacks-objects/SKILL.md +205 -0
- package/ai/skills/stacks-office-hours/SKILL.md +134 -0
- package/ai/skills/stacks-orm/SKILL.md +342 -0
- package/ai/skills/stacks-path/SKILL.md +118 -0
- package/ai/skills/stacks-payments/SKILL.md +373 -0
- package/ai/skills/stacks-plan-review/SKILL.md +152 -0
- package/ai/skills/stacks-plugins/SKILL.md +149 -0
- package/ai/skills/stacks-push/SKILL.md +376 -0
- package/ai/skills/stacks-query-builder/SKILL.md +128 -0
- package/ai/skills/stacks-queue/SKILL.md +559 -0
- package/ai/skills/stacks-realtime/SKILL.md +442 -0
- package/ai/skills/stacks-redesign/SKILL.md +182 -0
- package/ai/skills/stacks-registry/SKILL.md +58 -0
- package/ai/skills/stacks-repl/SKILL.md +80 -0
- package/ai/skills/stacks-retro/SKILL.md +122 -0
- package/ai/skills/stacks-review/SKILL.md +135 -0
- package/ai/skills/stacks-router/SKILL.md +231 -0
- package/ai/skills/stacks-routes/SKILL.md +105 -0
- package/ai/skills/stacks-scaffolding/SKILL.md +162 -0
- package/ai/skills/stacks-scheduler/SKILL.md +363 -0
- package/ai/skills/stacks-search-engine/SKILL.md +112 -0
- package/ai/skills/stacks-security/SKILL.md +129 -0
- package/ai/skills/stacks-security-audit/SKILL.md +128 -0
- package/ai/skills/stacks-server/SKILL.md +458 -0
- package/ai/skills/stacks-shell/SKILL.md +62 -0
- package/ai/skills/stacks-slug/SKILL.md +87 -0
- package/ai/skills/stacks-sms/SKILL.md +385 -0
- package/ai/skills/stacks-socials/SKILL.md +303 -0
- package/ai/skills/stacks-storage/SKILL.md +576 -0
- package/ai/skills/stacks-strings/SKILL.md +416 -0
- package/ai/skills/stacks-stx/SKILL.md +176 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +22 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +290 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +4 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +1252 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +289 -0
- package/ai/skills/stacks-technical-diagrams/bunfig.toml +1 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +88 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +75 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +70 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +41 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +62 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +371 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +295 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +91 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +384 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +84 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +257 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +13 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +230 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +88 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +103 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +481 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +68 -0
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +121 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +43 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +221 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +241 -0
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +186 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +332 -0
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +297 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +28 -0
- package/ai/skills/stacks-testing/SKILL.md +129 -0
- package/ai/skills/stacks-tunnel/SKILL.md +125 -0
- package/ai/skills/stacks-types/SKILL.md +178 -0
- package/ai/skills/stacks-ui/SKILL.md +246 -0
- package/ai/skills/stacks-utils/SKILL.md +444 -0
- package/ai/skills/stacks-validation/SKILL.md +371 -0
- package/ai/skills/stacks-whois/SKILL.md +315 -0
- package/assets/world-map-data.ts +121 -0
- package/bootstrap.ts +114 -0
- package/docs/api-examples.md +46 -0
- package/docs/index.md +24 -0
- package/docs/markdown-examples.md +87 -0
- package/functions/analytics.ts +97 -0
- package/functions/api.ts +143 -0
- package/functions/auth.ts +208 -0
- package/functions/billing/payments.ts +149 -0
- package/functions/blog.ts +120 -0
- package/functions/cms/categorizables.ts +107 -0
- package/functions/cms/pages.ts +108 -0
- package/functions/cms/posts.ts +100 -0
- package/functions/cms/taggables.ts +102 -0
- package/functions/commerce/coupons.ts +128 -0
- package/functions/commerce/customers.ts +128 -0
- package/functions/commerce/gift-cards.ts +119 -0
- package/functions/commerce/orders.ts +112 -0
- package/functions/commerce/payments.ts +119 -0
- package/functions/commerce/products/categories.ts +123 -0
- package/functions/commerce/products/manufacturers.ts +123 -0
- package/functions/commerce/products/products.ts +123 -0
- package/functions/commerce/products/reviews.ts +123 -0
- package/functions/commerce/products/units.ts +123 -0
- package/functions/commerce/products/variants.ts +123 -0
- package/functions/commerce/shippings/delivery-routes.ts +123 -0
- package/functions/commerce/shippings/digital-deliveries.ts +119 -0
- package/functions/commerce/shippings/drivers.ts +123 -0
- package/functions/commerce/shippings/license-keys.ts +123 -0
- package/functions/commerce/shippings/shipping-methods.ts +123 -0
- package/functions/commerce/shippings/shipping-rates.ts +119 -0
- package/functions/commerce/shippings/shipping-zones.ts +119 -0
- package/functions/commerce/taxes.ts +119 -0
- package/functions/commerce/waitlist/products.ts +123 -0
- package/functions/commerce/waitlist/restaurants.ts +123 -0
- package/functions/craft.ts +107 -0
- package/functions/dashboard.ts +153 -0
- package/functions/inbox.ts +95 -0
- package/functions/jobs.ts +86 -0
- package/functions/monitoring/errors.ts +295 -0
- package/functions/queue.ts +111 -0
- package/functions/realtime/websockets.ts +96 -0
- package/functions/realtime.ts +52 -0
- package/functions/releases.ts +49 -0
- package/functions/search.ts +126 -0
- package/functions/toasts.ts +95 -0
- package/functions/uploads.ts +216 -0
- package/ide/.gitignore +4 -0
- package/ide/cursor/rules/auto-imports.mdc +574 -0
- package/ide/cursor/rules/code-style.mdc +12 -0
- package/ide/cursor/rules/documentation.mdc +9 -0
- package/ide/cursor/rules/error-handling-validation.mdc +11 -0
- package/ide/cursor/rules/key-conventions.mdc +11 -0
- package/ide/cursor/rules/project-structure.mdc +169 -0
- package/ide/cursor/rules/readme.mdc +318 -0
- package/ide/cursor/rules/syntax-formatting.mdc +9 -0
- package/ide/cursor/rules/testing.mdc +10 -0
- package/ide/cursor/rules/typescript.mdc +9 -0
- package/ide/cursor/rules/ui-styling.mdc +0 -0
- package/ide/dictionary.txt +397 -0
- package/ide/jetbrains/.fleet/settings.json +4 -0
- package/ide/jetbrains/.idea/.gitignore +5 -0
- package/ide/jetbrains/.idea/.name +1 -0
- package/ide/jetbrains/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/ide/jetbrains/.idea/jsLinters/eslint.xml +7 -0
- package/ide/jetbrains/.idea/modules.xml +8 -0
- package/ide/jetbrains/.idea/stacks.iml +22 -0
- package/ide/jetbrains/.idea/vcs.xml +6 -0
- package/ide/jetbrains/.idea/webResources.xml +14 -0
- package/ide/jetbrains/IntelliJ IDEA Global Settings +0 -0
- package/ide/jetbrains/installed.txt +4 -0
- package/ide/jetbrains/options/github-copilot.xml +5 -0
- package/ide/jetbrains/options/nodejs.xml +7 -0
- package/ide/jetbrains/options/templates.xml +124 -0
- package/ide/jetbrains/templates/JavaScript.xml +6 -0
- package/ide/jetbrains/templates/Stacks.xml +8 -0
- package/ide/vscode/.vscode/README.md +23 -0
- package/ide/vscode/.vscode/extensions.json +5 -0
- package/ide/vscode/.vscode/settings.json +176 -0
- package/ide/vscode/.vscode/stacks.code-snippets +181 -0
- package/ide/vscode/LICENSE.md +21 -0
- package/ide/vscode/README.md +36 -0
- package/ide/vscode/logo.png +0 -0
- package/ide/vscode/package.json +79 -0
- package/ide/zed/.zed/settings.json +139 -0
- package/lang/README.md +28 -0
- package/lang/de.yml +14 -0
- package/lang/en.yml +15 -0
- package/package.json +19 -3
- package/project/bootstrap +161 -0
- package/project/buddy +175 -0
- package/project/storage/framework/tsconfig.app.json +50 -0
- package/project/storage/framework/tsconfig.base.json +58 -0
- package/routes/core.ts +78 -0
- package/routes/dashboard-api.ts +233 -0
- package/routes/dashboard.ts +739 -0
- package/routes/email.ts +129 -0
- package/stores/[name].stx +16 -0
- package/stores/git.ts +71 -0
- package/stores/payment.ts +474 -0
- package/stores/queue.ts +43 -0
- package/stores/user.ts +29 -0
- package/stx-components-plugin.ts +25 -0
- package/styles/design-tokens.css +597 -0
- package/types/dashboard.ts +70 -0
- package/types/defaults.ts +438 -0
- package/vcs/github/CODE_OF_CONDUCT.md +83 -0
- package/vcs/github/CONTRIBUTING.md +187 -0
- package/vcs/github/EXPLANATIONS.md +41 -0
- package/vcs/github/FUNDING.yml +1 -0
- package/vcs/github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/vcs/github/ISSUE_TEMPLATE/config.yml +10 -0
- package/vcs/github/ISSUE_TEMPLATE/feature_request.yml +44 -0
- package/vcs/github/SECURITY.md +70 -0
- package/vcs/github/renovate.json +5 -0
- package/vcs/github/stale.yml +14 -0
- package/vcs/github/workflows/README.md +16 -0
- package/vcs/github/workflows/ci.yml +114 -0
- package/vcs/github/workflows/export-size.yml +25 -0
- package/vcs/github/workflows/labeler.yml +13 -0
- package/vcs/github/workflows/release.yml +219 -0
- package/views/[...all].stx +8 -0
- package/views/about.md +3 -0
- package/views/auth/login.stx +1 -0
- package/views/auth/signup.stx +85 -0
- package/views/dashboard/.discovered-models.json +542 -0
- package/views/dashboard/AUDIT.md +904 -0
- package/views/dashboard/[...all].stx +89 -0
- package/views/dashboard/access-tokens/index.stx +177 -0
- package/views/dashboard/actions/index.stx +129 -0
- package/views/dashboard/analytics/blog/index.stx +356 -0
- package/views/dashboard/analytics/browsers/index.stx +263 -0
- package/views/dashboard/analytics/commerce/sales/index.stx +329 -0
- package/views/dashboard/analytics/commerce/web/index.stx +379 -0
- package/views/dashboard/analytics/countries/index.stx +289 -0
- package/views/dashboard/analytics/devices/index.stx +305 -0
- package/views/dashboard/analytics/events/index.stx +536 -0
- package/views/dashboard/analytics/index.stx +88 -0
- package/views/dashboard/analytics/marketing/index.stx +315 -0
- package/views/dashboard/analytics/pages/index.stx +271 -0
- package/views/dashboard/analytics/referrers/index.stx +319 -0
- package/views/dashboard/analytics/web/index.stx +356 -0
- package/views/dashboard/buddy/index.stx +198 -0
- package/views/dashboard/ci/index.stx +462 -0
- package/views/dashboard/cloud/index.stx +452 -0
- package/views/dashboard/commands/index.stx +295 -0
- package/views/dashboard/commerce/categories/index.stx +406 -0
- package/views/dashboard/commerce/coupons/index.stx +485 -0
- package/views/dashboard/commerce/customers/index.stx +410 -0
- package/views/dashboard/commerce/dashboard/index.stx +287 -0
- package/views/dashboard/commerce/delivery/delivery-routes.stx +305 -0
- package/views/dashboard/commerce/delivery/digital-delivery.stx +332 -0
- package/views/dashboard/commerce/delivery/drivers.stx +397 -0
- package/views/dashboard/commerce/delivery/index.stx +18 -0
- package/views/dashboard/commerce/delivery/license-keys.stx +380 -0
- package/views/dashboard/commerce/delivery/shipping-methods.stx +346 -0
- package/views/dashboard/commerce/delivery/shipping-rates.stx +338 -0
- package/views/dashboard/commerce/delivery/shipping-zones.stx +302 -0
- package/views/dashboard/commerce/delivery/tabs.ts +25 -0
- package/views/dashboard/commerce/gift-cards/index.stx +561 -0
- package/views/dashboard/commerce/manufacturers/index.stx +308 -0
- package/views/dashboard/commerce/orders/index.stx +247 -0
- package/views/dashboard/commerce/payments/index.stx +535 -0
- package/views/dashboard/commerce/pos/index.stx +653 -0
- package/views/dashboard/commerce/printers/devices/index.stx +273 -0
- package/views/dashboard/commerce/printers/receipts/index.stx +308 -0
- package/views/dashboard/commerce/products/detail.stx +563 -0
- package/views/dashboard/commerce/products/index.stx +370 -0
- package/views/dashboard/commerce/reviews/index.stx +656 -0
- package/views/dashboard/commerce/reviews/index.ts +0 -0
- package/views/dashboard/commerce/taxes/index.stx +377 -0
- package/views/dashboard/commerce/units/index.stx +317 -0
- package/views/dashboard/commerce/variants/index.stx +319 -0
- package/views/dashboard/commerce/waitlist/products.stx +942 -0
- package/views/dashboard/commerce/waitlist/restaurant.stx +1024 -0
- package/views/dashboard/components/index.stx +330 -0
- package/views/dashboard/composables/index.ts +19 -0
- package/views/dashboard/composables/useChart.ts +77 -0
- package/views/dashboard/composables/useNavigation.ts +99 -0
- package/views/dashboard/composables/useRole.ts +58 -0
- package/views/dashboard/composables/useTheme.ts +74 -0
- package/views/dashboard/content/authors/index.stx +171 -0
- package/views/dashboard/content/blog/index.stx +493 -0
- package/views/dashboard/content/categories/index.stx +169 -0
- package/views/dashboard/content/comments/index.stx +189 -0
- package/views/dashboard/content/dashboard.stx +383 -0
- package/views/dashboard/content/files/index.stx +706 -0
- package/views/dashboard/content/pages/index.stx +174 -0
- package/views/dashboard/content/posts/index.stx +269 -0
- package/views/dashboard/content/seo/index.stx +240 -0
- package/views/dashboard/content/tags/index.stx +167 -0
- package/views/dashboard/data/activity/index.stx +261 -0
- package/views/dashboard/data/dashboard/index.stx +116 -0
- package/views/dashboard/data/subscribers.stx +270 -0
- package/views/dashboard/data/teams.stx +275 -0
- package/views/dashboard/data/users.stx +279 -0
- package/views/dashboard/dependencies/index.stx +178 -0
- package/views/dashboard/deployments/[id].stx +234 -0
- package/views/dashboard/deployments/index.stx +9 -0
- package/views/dashboard/dns/index.stx +163 -0
- package/views/dashboard/environment/index.stx +283 -0
- package/views/dashboard/errors/index.stx +274 -0
- package/views/dashboard/forgot-password.stx +57 -0
- package/views/dashboard/functions/index.stx +159 -0
- package/views/dashboard/health/index.stx +248 -0
- package/views/dashboard/inbox/activity.stx +350 -0
- package/views/dashboard/inbox/index.stx +452 -0
- package/views/dashboard/inbox/settings.stx +290 -0
- package/views/dashboard/index.stx +252 -0
- package/views/dashboard/insights/index.stx +529 -0
- package/views/dashboard/jobs/[id].stx +430 -0
- package/views/dashboard/jobs/history.stx +222 -0
- package/views/dashboard/jobs/index.stx +214 -0
- package/views/dashboard/kanban/[id].stx +899 -0
- package/views/dashboard/kanban/index.stx +260 -0
- package/views/dashboard/layouts/default.stx +565 -0
- package/views/dashboard/layouts/guest.stx +10 -0
- package/views/dashboard/login.stx +48 -0
- package/views/dashboard/logs/index.stx +540 -0
- package/views/dashboard/mailboxes/index.stx +149 -0
- package/views/dashboard/management/permissions/index.stx +490 -0
- package/views/dashboard/marketing/campaigns/index.stx +604 -0
- package/views/dashboard/marketing/lists/index.stx +514 -0
- package/views/dashboard/marketing/reviews/index.stx +475 -0
- package/views/dashboard/marketing/social-posts/index.stx +560 -0
- package/views/dashboard/models/[model].stx +195 -0
- package/views/dashboard/models/index.stx +159 -0
- package/views/dashboard/monitoring/errors/index.stx +415 -0
- package/views/dashboard/notifications/dashboard.stx +383 -0
- package/views/dashboard/notifications/email.stx +336 -0
- package/views/dashboard/notifications/history.stx +373 -0
- package/views/dashboard/notifications/sms.stx +332 -0
- package/views/dashboard/packages/index.stx +195 -0
- package/views/dashboard/queries/[id].stx +381 -0
- package/views/dashboard/queries/history.stx +227 -0
- package/views/dashboard/queries/index.stx +335 -0
- package/views/dashboard/queries/slow.stx +318 -0
- package/views/dashboard/queue/index.stx +427 -0
- package/views/dashboard/realtime/index.stx +429 -0
- package/views/dashboard/register.stx +46 -0
- package/views/dashboard/releases/index.stx +389 -0
- package/views/dashboard/requests/index.stx +462 -0
- package/views/dashboard/serverless/index.stx +475 -0
- package/views/dashboard/servers/[id].stx +299 -0
- package/views/dashboard/servers/index.stx +483 -0
- package/views/dashboard/settings/billing.stx +50 -0
- package/views/dashboard/settings/index.stx +256 -0
- package/views/dashboard/settings/mail.stx +252 -0
- package/views/dashboard/stores/auth.ts +123 -0
- package/views/dashboard/stores/ci.ts +342 -0
- package/views/dashboard/stores/index.ts +129 -0
- package/views/dashboard/stores/kanban.ts +723 -0
- package/views/dashboard/stores/rbac.ts +350 -0
- package/views/dashboard/stx.config.ts +34 -0
- package/views/dashboard/teams/[id].stx +170 -0
- package/views/forum/category.stx +92 -0
- package/views/forum/index.stx +209 -0
- package/views/forum/new-topic.stx +113 -0
- package/views/forum/profile.stx +1 -0
- package/views/forum/topic.stx +143 -0
- package/views/hello-world/[name].stx +47 -0
- package/views/index.stx +21 -0
- package/views/system-tray/index.stx +147 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stacks-queue
|
|
3
|
+
description: Use when working with job queues in a Stacks application — creating jobs, dispatching, workers, batches, failed jobs, queue events, health checks, testing, Redis/database/sync drivers, rate limiting, or scheduled jobs. Covers @stacksjs/queue, config/queue.ts, and app/Jobs/.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
|
+
allowed-tools: Read Edit Write Bash Grep Glob
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Stacks Queue System
|
|
10
|
+
|
|
11
|
+
## Key Paths
|
|
12
|
+
- Queue package: `storage/framework/core/queue/src/`
|
|
13
|
+
- Configuration: `config/queue.ts`
|
|
14
|
+
- Application jobs: `app/Jobs/`
|
|
15
|
+
- Job model: `storage/framework/defaults/app/Models/Job.ts`
|
|
16
|
+
- Failed job model: `storage/framework/defaults/app/Models/FailedJob.ts`
|
|
17
|
+
|
|
18
|
+
## Source Files
|
|
19
|
+
```
|
|
20
|
+
queue/src/
|
|
21
|
+
├── action.ts # Job class with dispatch/dispatchIf/dispatchAfter/dispatchNow
|
|
22
|
+
├── job.ts # JobBuilder fluent API + job() helper + jobBatch() + runJob()
|
|
23
|
+
├── discovery.ts # Job auto-discovery from app/Jobs/ via Bun.Glob
|
|
24
|
+
├── scheduler.ts # Cron-based job scheduling with overlap prevention
|
|
25
|
+
├── worker.ts # Queue worker (database polling & Redis processing)
|
|
26
|
+
├── batch.ts # Job batching (PendingBatch, DispatchedBatch, Batch)
|
|
27
|
+
├── events.ts # QueueEvents emitter, QueueMetrics, WorkerTracker
|
|
28
|
+
├── health.ts # Queue health checks (checkQueueHealth, HTTP handler)
|
|
29
|
+
├── notifications.ts # Failed job notifications (email/slack/discord/webhook)
|
|
30
|
+
├── testing.ts # FakeQueue, QueueTester, runJob, expectJobToFail
|
|
31
|
+
├── utils.ts # storeJob helper
|
|
32
|
+
├── drivers/redis.ts # RedisQueue (bun-queue wrapper), StacksQueueManager
|
|
33
|
+
└── index.ts # Re-exports everything
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Job Class (action.ts)
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
export class Job {
|
|
40
|
+
name: string
|
|
41
|
+
description: string
|
|
42
|
+
action?: string | Function // action name or function
|
|
43
|
+
handle?: Function // direct handler function
|
|
44
|
+
queue?: string // queue name (default: 'default')
|
|
45
|
+
rate?: string // cron expression for scheduling
|
|
46
|
+
tries?: number // max retry attempts
|
|
47
|
+
timeout?: number // timeout in seconds
|
|
48
|
+
backoff?: number | number[] // backoff delay(s) in seconds
|
|
49
|
+
backoffConfig?: { strategy, initialDelay, factor, maxDelay, jitter }
|
|
50
|
+
enabled?: boolean
|
|
51
|
+
|
|
52
|
+
async dispatch(payload?): Promise<void>
|
|
53
|
+
async dispatchIf(condition: boolean, payload?): Promise<void>
|
|
54
|
+
async dispatchUnless(condition: boolean, payload?): Promise<void>
|
|
55
|
+
async dispatchAfter(delaySeconds: number, payload?): Promise<void>
|
|
56
|
+
async dispatchNow(payload?): Promise<void> // bypasses queue, runs immediately
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Dispatch Routing
|
|
61
|
+
1. Checks `isFaked()` -- if testing, dispatches to FakeQueue
|
|
62
|
+
2. `sync` driver: calls `dispatchNow()` (immediate execution)
|
|
63
|
+
3. `redis` driver: creates `RedisQueue`, calls `queue.add()` with bun-queue options
|
|
64
|
+
4. `database` driver: inserts into `jobs` table with JSON payload
|
|
65
|
+
5. Fallback: `dispatchNow()`
|
|
66
|
+
|
|
67
|
+
### dispatchNow() Execution Order
|
|
68
|
+
1. If `handle` is a function: `await this.handle(payload)`
|
|
69
|
+
2. If `action` is a string: `await runAction(this.action)` via `@stacksjs/actions`
|
|
70
|
+
3. If `action` is a function: `await this.action()`
|
|
71
|
+
4. Otherwise: throws error
|
|
72
|
+
|
|
73
|
+
## JobBuilder Fluent API (job.ts)
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { job } from '@stacksjs/queue'
|
|
77
|
+
|
|
78
|
+
await job('SendWelcomeEmail', { email, name })
|
|
79
|
+
.onQueue('emails')
|
|
80
|
+
.delay(60) // seconds
|
|
81
|
+
.tries(5)
|
|
82
|
+
.timeout(30) // seconds
|
|
83
|
+
.backoff([10, 30, 60])
|
|
84
|
+
.withContext({ userId })
|
|
85
|
+
.dispatch()
|
|
86
|
+
|
|
87
|
+
// Conditional dispatch
|
|
88
|
+
await job('ProcessOrder', data).dispatchIf(isValid)
|
|
89
|
+
await job('ProcessOrder', data).dispatchUnless(isDuplicate)
|
|
90
|
+
|
|
91
|
+
// Immediate execution
|
|
92
|
+
await job('SendNotification', data).dispatchNow()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The `job()` helper loads job modules from `app/Jobs/{name}.ts` via `runJob()`.
|
|
96
|
+
|
|
97
|
+
### runJob(name, options)
|
|
98
|
+
Dynamically imports `app/Jobs/{name}.ts` and executes:
|
|
99
|
+
1. `jobConfig.handle(payload)` if handle is a function
|
|
100
|
+
2. `runAction(jobConfig.action)` if action is a string
|
|
101
|
+
3. `jobConfig.action()` if action is a function
|
|
102
|
+
4. `jobConfig(payload, context)` if default export is a function
|
|
103
|
+
|
|
104
|
+
### jobBatch() Helper
|
|
105
|
+
```typescript
|
|
106
|
+
import { jobBatch } from '@stacksjs/queue'
|
|
107
|
+
const batch = await jobBatch([SendEmail, ProcessOrder])
|
|
108
|
+
.name('Onboard User')
|
|
109
|
+
.allowFailures()
|
|
110
|
+
.then(async (b) => console.log('All done!'))
|
|
111
|
+
.dispatch()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Job Batching (batch.ts)
|
|
115
|
+
|
|
116
|
+
### PendingBatch (not yet dispatched)
|
|
117
|
+
```typescript
|
|
118
|
+
const batch = Batch.create([
|
|
119
|
+
{ job: SendEmail, payload: { to: 'a@b.com' } },
|
|
120
|
+
{ job: ProcessImage, payload: { id: 1 } },
|
|
121
|
+
])
|
|
122
|
+
.name('welcome-batch')
|
|
123
|
+
.onQueue('default')
|
|
124
|
+
.allowFailures() // continue processing on failure
|
|
125
|
+
.then(async (batch) => {}) // all succeeded (or allowFailures + all done)
|
|
126
|
+
.catch(async (batch, error) => {}) // first failure
|
|
127
|
+
.finally(async (batch) => {}) // always runs
|
|
128
|
+
.progress(async (batch) => {}) // each job completion
|
|
129
|
+
|
|
130
|
+
const dispatched = await batch.dispatch()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### DispatchedBatch (inspectable)
|
|
134
|
+
```typescript
|
|
135
|
+
const b = await Batch.find(batchId) // returns DispatchedBatch | null
|
|
136
|
+
await b.progress() // 0-100 percentage
|
|
137
|
+
await b.pendingJobs() // count
|
|
138
|
+
await b.failedJobs() // count
|
|
139
|
+
await b.completedJobs() // total - pending
|
|
140
|
+
await b.failedJobIds() // string[]
|
|
141
|
+
await b.finished() // boolean (finished_at !== null)
|
|
142
|
+
await b.cancelled() // boolean
|
|
143
|
+
await b.hasFailures() // boolean
|
|
144
|
+
await b.cancel() // prevents remaining jobs, fires finally callbacks
|
|
145
|
+
await b.add([moreJobs]) // add jobs to running batch
|
|
146
|
+
await b.delete() // remove batch record + callbacks
|
|
147
|
+
await b.fresh() // get fresh BatchRecord from storage
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Batch Static Methods
|
|
151
|
+
```typescript
|
|
152
|
+
Batch.create(jobs): PendingBatch
|
|
153
|
+
await Batch.find(id): DispatchedBatch | null
|
|
154
|
+
await Batch.all(): DispatchedBatch[]
|
|
155
|
+
await Batch.prune(olderThanHours = 24): number // delete finished batches
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Batch Storage
|
|
159
|
+
- **Database**: `job_batches` table with columns: `id`, `name`, `total_jobs`, `pending_jobs`, `failed_jobs`, `failed_job_ids` (JSON), `options` (JSON), `cancelled_at`, `created_at`, `finished_at`
|
|
160
|
+
- **Redis**: hash at `stacks:batch:{id}`, set index at `stacks:batches`
|
|
161
|
+
- Callbacks stored in in-memory `Map` (not serializable -- only work in dispatching process)
|
|
162
|
+
|
|
163
|
+
### Batch Lifecycle
|
|
164
|
+
- On job completion: decrements `pending_jobs`, fires progress callbacks. When `pending_jobs === 0`: fires `then` + `finally` callbacks, emits `batch:completed`
|
|
165
|
+
- On job failure: increments `failed_jobs`, appends to `failed_job_ids`. If `allowFailures` is false: cancels batch immediately. Fires `catch` callbacks, emits `batch:failed`
|
|
166
|
+
- Worker checks `_batchId` in payload to skip cancelled batch jobs
|
|
167
|
+
|
|
168
|
+
## Job Discovery (discovery.ts)
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const jobs = await discoverJobs() // scans app/Jobs/**/*.{ts,js} via Bun.Glob
|
|
172
|
+
const job = getJob('SendWelcomeEmail') // by name from registry
|
|
173
|
+
const scheduled = getScheduledJobs() // jobs with rate/schedule
|
|
174
|
+
const all = getAllJobs()
|
|
175
|
+
await executeJob('SendWelcomeEmail', payload) // execute by name
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Skips `.test.`, `.spec.`, and `index` files. Supports both class-based (has `handle` on prototype) and function-based (object with `handle` method) jobs.
|
|
179
|
+
|
|
180
|
+
### JobRegistry
|
|
181
|
+
Global singleton `jobRegistry`:
|
|
182
|
+
- `register(job)`, `get(name)`, `all()`, `byQueue(queue)`, `scheduled()`, `has(name)`, `clear()`
|
|
183
|
+
|
|
184
|
+
### BackoffConfig
|
|
185
|
+
```typescript
|
|
186
|
+
interface BackoffConfig {
|
|
187
|
+
strategy?: 'fixed' | 'exponential' | 'linear'
|
|
188
|
+
initialDelay?: number
|
|
189
|
+
factor?: number
|
|
190
|
+
maxDelay?: number
|
|
191
|
+
jitter?: { enabled?: boolean, factor?: number, minDelay?: number, maxDelay?: number }
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Job Scheduler (scheduler.ts)
|
|
196
|
+
|
|
197
|
+
Cron-based scheduling with 1-minute check interval.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
await startScheduler({ checkInterval: 60000, timezone: 'UTC', preventOverlapping: true })
|
|
201
|
+
await stopScheduler()
|
|
202
|
+
isSchedulerRunning()
|
|
203
|
+
getSchedulerStatus() // { isRunning, jobCount, jobs: [{ name, schedule, lastRun, nextRun, isRunning }] }
|
|
204
|
+
getRegisteredJobs() // Map<string, ScheduledJobState>
|
|
205
|
+
await triggerJob('SendReport') // manually trigger a scheduled job
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Cron Support
|
|
209
|
+
- Standard 5-part cron: `minute hour day month dayOfWeek`
|
|
210
|
+
- 6-part cron (with seconds): seconds are stripped, runs at minute granularity
|
|
211
|
+
- Predefined: `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`, `@midnight`
|
|
212
|
+
- Every expressions: `Every.minute`, `Every.fiveMinutes`, `Every.hour`, `Every.day`, etc.
|
|
213
|
+
- Supports lists (`1,2,3`), ranges (`1-5`), steps (`*/5`, `1-5/2`), wildcards (`*`)
|
|
214
|
+
|
|
215
|
+
### Overlap Prevention
|
|
216
|
+
- Global `preventOverlapping: true` (default) skips if job is still running
|
|
217
|
+
- Per-job `withoutOverlapping: true` on job config
|
|
218
|
+
|
|
219
|
+
## Queue Worker (worker.ts)
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
await startProcessor('default', { concurrency: 5 }) // start worker
|
|
223
|
+
await stopProcessor() // graceful shutdown
|
|
224
|
+
await executeFailedJobs() // requeue all failed jobs
|
|
225
|
+
await retryFailedJob(id) // requeue specific failed job
|
|
226
|
+
getActiveJobCount() // currently processing count
|
|
227
|
+
isWorkerRunning() // boolean
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Database Worker Details
|
|
231
|
+
- Polls `jobs` table every **1 second** (`sleep(1000)`)
|
|
232
|
+
- Refreshes queue list every **10 seconds** via `getAllQueues()` (SELECT DISTINCT queue FROM jobs)
|
|
233
|
+
- **Atomic claim**: SELECT pending job + UPDATE with CAS pattern (`WHERE reserved_at IS NULL`)
|
|
234
|
+
- Increments `attempts`, sets `reserved_at` to current timestamp
|
|
235
|
+
- If `numUpdatedRows === 0`, another worker claimed it -- tries next
|
|
236
|
+
- On success: DELETE from `jobs`, emit `job:completed`
|
|
237
|
+
- On failure: compare `attempts` vs `maxAttempts` (from payload `options.tries`, default 1)
|
|
238
|
+
- **Exceeded**: move to `failed_jobs` table (uuid, connection, queue, payload, exception, failed_at), emit `job:failed`
|
|
239
|
+
- **Retrying**: release job by setting `reserved_at = null`, `available_at = now + backoff`, emit `job:retrying`
|
|
240
|
+
- Backoff: uses `options.backoff` from payload -- array (indexed by attempt), number (fixed), or 30s default
|
|
241
|
+
- Worker ID: `worker-{pid}-{timestamp}`
|
|
242
|
+
- Handles `SIGINT` / `SIGTERM` for graceful shutdown
|
|
243
|
+
|
|
244
|
+
### Redis Worker Details
|
|
245
|
+
- Uses `RedisQueue.process(concurrency, handler)` from bun-queue
|
|
246
|
+
- Handler receives `BunJob` objects
|
|
247
|
+
- **Throws on failure** for bun-queue's built-in retry handling
|
|
248
|
+
- Keep-alive loop: polls `sleep(1000)` until `workerRunning === false`
|
|
249
|
+
- Closes queue on shutdown
|
|
250
|
+
|
|
251
|
+
### Job Payload Format (database)
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"jobName": "SendWelcomeEmail",
|
|
255
|
+
"payload": { "email": "user@example.com" },
|
|
256
|
+
"options": { "queue": "emails", "tries": 3, "timeout": 30, "backoff": [10, 30, 60] }
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Queue Events (events.ts)
|
|
261
|
+
|
|
262
|
+
### QueueEvents Class
|
|
263
|
+
Custom event emitter (not mitt-based) with `Map<QueueEventType, Set<Handler>>` + wildcard Set.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
const events = getQueueEvents() // global singleton
|
|
267
|
+
const unsub = events.on('job:failed', handler)
|
|
268
|
+
const unsub2 = events.onAny((event, payload) => { ... })
|
|
269
|
+
events.once('job:completed', handler)
|
|
270
|
+
await events.emit('job:completed', payload)
|
|
271
|
+
events.off('job:completed') // remove all handlers for event
|
|
272
|
+
events.removeAllListeners()
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Convenience Functions
|
|
276
|
+
```typescript
|
|
277
|
+
const unsub = onQueueEvent('job:failed', (payload) => { ... })
|
|
278
|
+
const unsub2 = onQueueEvent('*', (event, payload) => { ... })
|
|
279
|
+
await emitQueueEvent('job:completed', { jobId, result, duration })
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Event Types
|
|
283
|
+
`'job:added'` | `'job:processing'` | `'job:completed'` | `'job:failed'` | `'job:retrying'` | `'job:stalled'` | `'job:progress'` | `'queue:paused'` | `'queue:resumed'` | `'queue:error'` | `'worker:started'` | `'worker:stopped'` | `'batch:added'` | `'batch:completed'` | `'batch:failed'`
|
|
284
|
+
|
|
285
|
+
### QueueEventPayload
|
|
286
|
+
```typescript
|
|
287
|
+
interface QueueEventPayload {
|
|
288
|
+
jobId?: string, queueName?: string, jobName?: string, data?: any,
|
|
289
|
+
result?: any, error?: Error, progress?: number, timestamp: number,
|
|
290
|
+
attemptsMade?: number, duration?: number
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Event-Aware Wrapper
|
|
295
|
+
```typescript
|
|
296
|
+
const wrappedHandler = withEvents('emails', originalHandler)
|
|
297
|
+
// Automatically emits job:processing, job:completed, job:failed
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### OnQueueEvent Decorator
|
|
301
|
+
```typescript
|
|
302
|
+
class MyHandler {
|
|
303
|
+
@OnQueueEvent('job:failed')
|
|
304
|
+
handleFailed(payload: QueueEventPayload) { ... }
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### QueueMetrics
|
|
309
|
+
```typescript
|
|
310
|
+
const metrics = getGlobalMetrics() // global singleton
|
|
311
|
+
metrics.getThroughputPerMinute() // completions in last 60 seconds
|
|
312
|
+
metrics.getAverageProcessingTime() // avg duration in last 60 seconds
|
|
313
|
+
metrics.getMetrics() // { counts: {added,completed,failed,processing}, averageDuration, recentErrors, throughputPerMinute }
|
|
314
|
+
metrics.reset()
|
|
315
|
+
metrics.stop() // unsubscribe from events
|
|
316
|
+
```
|
|
317
|
+
Tracks last 1000 completions and last 100 errors in sliding windows.
|
|
318
|
+
|
|
319
|
+
### WorkerTracker
|
|
320
|
+
```typescript
|
|
321
|
+
const tracker = getWorkerTracker() // global singleton (not lazy)
|
|
322
|
+
tracker.register(id, queue)
|
|
323
|
+
tracker.markActive(id) / tracker.markIdle(id)
|
|
324
|
+
tracker.recordCompletion(id) / tracker.recordFailure(id)
|
|
325
|
+
tracker.unregister(id) // marks as 'stopped'
|
|
326
|
+
tracker.getAll(): TrackedWorker[]
|
|
327
|
+
tracker.clear()
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
interface TrackedWorker {
|
|
332
|
+
id: string, status: 'active' | 'idle' | 'stopped', queue: string,
|
|
333
|
+
processedCount: number, failedCount: number,
|
|
334
|
+
lastActivityAt: string, startedAt: string
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Queue Health (health.ts)
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const result = await checkQueueHealth({
|
|
342
|
+
maxPendingWarning: 1000, maxPendingCritical: 5000,
|
|
343
|
+
maxFailedWarning: 10, maxFailedCritical: 100,
|
|
344
|
+
maxJobAgeWarning: 3600, maxJobAgeCritical: 86400, // seconds
|
|
345
|
+
maxErrorRateWarning: 0.1, maxErrorRateCritical: 0.5,
|
|
346
|
+
queues?: ['default', 'emails'] // filter to specific queues
|
|
347
|
+
})
|
|
348
|
+
// result: { status: 'healthy'|'degraded'|'unhealthy', queues, workers, metrics, alerts }
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
- Queries `jobs` and `failed_jobs` tables directly
|
|
352
|
+
- Groups by queue name, calculates pending/processing/delayed/failed counts
|
|
353
|
+
- Oldest job age calculated from `created_at`
|
|
354
|
+
- Error rate = totalFailed / totalJobs
|
|
355
|
+
- Worker statuses from `getWorkerTracker().getAll()`
|
|
356
|
+
- Metrics from `getGlobalMetrics()`
|
|
357
|
+
- Returns real `TrackedWorker` data even if DB query fails
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
const healthy = await isQueueHealthy() // returns boolean
|
|
361
|
+
const handler = createHealthCheckHandler() // HTTP handler (200/207/503)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Failed Job Notifications (notifications.ts)
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
configureFailedJobNotifications({
|
|
368
|
+
channels: ['email', 'slack', 'discord', 'webhook', 'log'],
|
|
369
|
+
email: { to: ['admin@example.com'], from?: string, subject?: string },
|
|
370
|
+
slack: { webhookUrl: '...', channel?: '#alerts', username?: 'Queue Monitor', iconEmoji?: ':warning:' },
|
|
371
|
+
discord: { webhookUrl: '...', username?: 'Queue Monitor', avatarUrl?: string },
|
|
372
|
+
webhook: { url: '...', headers?: Record<string,string>, secret?: 'hmac-key' },
|
|
373
|
+
rateLimit: 100, // max per hour
|
|
374
|
+
batch: true, // batch notifications
|
|
375
|
+
batchInterval: 60000, // ms (default 60s)
|
|
376
|
+
filter: (job) => job.queue !== 'low-priority',
|
|
377
|
+
})
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
- **Email**: uses `@stacksjs/email` (`mail.send()`) with HTML table format
|
|
381
|
+
- **Slack**: sends Block Kit payload (header + sections, max 10 jobs per message)
|
|
382
|
+
- **Discord**: sends embeds (red color, max 10 per message)
|
|
383
|
+
- **Webhook**: JSON payload with HMAC SHA-256 signature in `X-Signature` header (via `crypto.subtle`)
|
|
384
|
+
- **Log**: writes to application log via `@stacksjs/logging`
|
|
385
|
+
- Rate limiter resets every **3,600,000ms** (1 hour)
|
|
386
|
+
- Batch: accumulates jobs, flushes on interval via `setTimeout`
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
const notifier = getFailedJobNotifier() // global singleton
|
|
390
|
+
await notifyJobFailed(jobInfo) // uses global notifier
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Queue Testing (testing.ts)
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
import { fake, restore, runJob as runTestJob, expectJobToFail } from '@stacksjs/queue'
|
|
397
|
+
|
|
398
|
+
const fakeQueue = fake() // sets global FakeQueue singleton
|
|
399
|
+
|
|
400
|
+
// Dispatch goes to fake queue (not real driver)
|
|
401
|
+
await job('SendEmail', data).dispatch()
|
|
402
|
+
await SendWelcomeEmail.dispatch(data)
|
|
403
|
+
|
|
404
|
+
// Assertions (throw on failure)
|
|
405
|
+
fakeQueue.assertDispatched('SendEmail')
|
|
406
|
+
fakeQueue.assertDispatched('SendEmail', (j) => j.data.to === 'test@example.com')
|
|
407
|
+
fakeQueue.assertNotDispatched('ProcessPayment')
|
|
408
|
+
fakeQueue.assertDispatchedTimes('SendEmail', 3)
|
|
409
|
+
fakeQueue.assertNothingDispatched()
|
|
410
|
+
fakeQueue.assertPushed('Reminder')
|
|
411
|
+
fakeQueue.assertPushedWithDelay('Reminder', 3600)
|
|
412
|
+
fakeQueue.assertPushedOn('emails', 'SendEmail')
|
|
413
|
+
|
|
414
|
+
// Run job synchronously for testing
|
|
415
|
+
const result = await runTestJob({ handle: async (data) => 'ok' }, testData)
|
|
416
|
+
|
|
417
|
+
// Assert job failure
|
|
418
|
+
const error = await expectJobToFail(BadJob, data, /expected error/)
|
|
419
|
+
// or: await expectJobToFail(BadJob, data, 'substring match')
|
|
420
|
+
|
|
421
|
+
restore() // clears FakeQueue singleton, re-enables real queue
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### QueueTester Class
|
|
425
|
+
```typescript
|
|
426
|
+
const tester = createQueueTester() // auto-calls fake()
|
|
427
|
+
tester.dispatch('Job', data).assertDispatched('Job').reset()
|
|
428
|
+
tester.cleanup() // calls restore()
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### FakeQueue Internals
|
|
432
|
+
- `dispatchedJobs[]` -- recorded dispatches
|
|
433
|
+
- `pushedJobs[]` -- recorded delayed pushes
|
|
434
|
+
- `processedJobs[]` -- simulated processing results
|
|
435
|
+
- `failedJobs[]` -- simulated failures
|
|
436
|
+
|
|
437
|
+
## Redis Driver (drivers/redis.ts)
|
|
438
|
+
|
|
439
|
+
Wraps `bun-queue`'s `Queue` class with Stacks-compatible API.
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const queue = new RedisQueue<T>('emails', redisConfig)
|
|
443
|
+
await queue.add(data, options) // delay/attempts/priority/timeout/backoff
|
|
444
|
+
queue.process(concurrency, handler) // start processing (one-time call)
|
|
445
|
+
await queue.getJob(jobId)
|
|
446
|
+
await queue.getJobs('waiting' | 'active' | 'completed' | 'failed' | 'delayed')
|
|
447
|
+
await queue.getJobCounts() // { waiting, active, completed, failed, delayed }
|
|
448
|
+
await queue.removeJob(jobId)
|
|
449
|
+
await queue.pause() / queue.resume()
|
|
450
|
+
await queue.empty() / queue.close()
|
|
451
|
+
await queue.getMetrics()
|
|
452
|
+
await queue.ping() // health check
|
|
453
|
+
await queue.scheduleCron({ cron, data, tz?, name? })
|
|
454
|
+
await queue.unscheduleCron(jobId)
|
|
455
|
+
await queue.getDeadLetterJobs()
|
|
456
|
+
await queue.republishDeadLetterJob(jobId)
|
|
457
|
+
await queue.clearDeadLetterQueue()
|
|
458
|
+
await queue.bulkRemove(jobIds): number
|
|
459
|
+
await queue.getClusterInfo()
|
|
460
|
+
queue.isLeader(): boolean // for horizontal scaling
|
|
461
|
+
queue.getQueue(): BunQueue<T> // underlying bun-queue instance
|
|
462
|
+
queue.on(event, handler) // subscribe to bun-queue events
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### StacksQueueManager
|
|
466
|
+
```typescript
|
|
467
|
+
const manager = new StacksQueueManager(connectionConfigs)
|
|
468
|
+
manager.queue('emails') // get or create RedisQueue by name
|
|
469
|
+
manager.setDefaultConnection('high-priority')
|
|
470
|
+
await manager.closeAll()
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### RedisQueue Events
|
|
474
|
+
`jobAdded`, `jobCompleted`, `jobFailed`, `jobProgress`, `jobActive`, `jobStalled`, `jobDelayed`, `ready`, `error`
|
|
475
|
+
|
|
476
|
+
### bun-queue Re-exports
|
|
477
|
+
`batch`, `chain`, `dispatch`, `dispatchAfter`, `dispatchSync`, `getQueueManager`, `setQueueManager`, `QueueManager`
|
|
478
|
+
|
|
479
|
+
## Creating a Job
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
// app/Jobs/SendWelcomeEmail.ts
|
|
483
|
+
import { Job } from '@stacksjs/queue'
|
|
484
|
+
|
|
485
|
+
export default new Job({
|
|
486
|
+
name: 'SendWelcomeEmail',
|
|
487
|
+
description: 'Send welcome email to new user',
|
|
488
|
+
queue: 'emails',
|
|
489
|
+
tries: 3,
|
|
490
|
+
backoff: [10, 30, 60],
|
|
491
|
+
timeout: 30,
|
|
492
|
+
|
|
493
|
+
async handle(payload: { email: string; name: string }) {
|
|
494
|
+
console.log(`Sending to ${payload.email}`)
|
|
495
|
+
return { sent: true }
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Dispatching
|
|
501
|
+
```typescript
|
|
502
|
+
import SendWelcomeEmail from '~/app/Jobs/SendWelcomeEmail'
|
|
503
|
+
|
|
504
|
+
await SendWelcomeEmail.dispatch({ email, name })
|
|
505
|
+
await SendWelcomeEmail.dispatchIf(isNewUser, { email, name })
|
|
506
|
+
await SendWelcomeEmail.dispatchUnless(isDuplicate, { email, name })
|
|
507
|
+
await SendWelcomeEmail.dispatchAfter(60, { email, name }) // 60s delay
|
|
508
|
+
await SendWelcomeEmail.dispatchNow({ email, name }) // bypass queue
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## config/queue.ts
|
|
512
|
+
```typescript
|
|
513
|
+
{
|
|
514
|
+
default: 'sync', // 'sync' | 'database' | 'redis' | 'sqs' | 'memory'
|
|
515
|
+
connections: {
|
|
516
|
+
sync: { driver: 'sync' },
|
|
517
|
+
database: { driver: 'database', table: 'jobs', queue: 'default', retryAfter: 90 },
|
|
518
|
+
redis: {
|
|
519
|
+
driver: 'redis',
|
|
520
|
+
redis: { url, host: 'localhost', port: 6379, password, db: 0 },
|
|
521
|
+
prefix: 'stacks:queue',
|
|
522
|
+
concurrency: 5,
|
|
523
|
+
logLevel: 'info',
|
|
524
|
+
distributedLock: true,
|
|
525
|
+
stalledJobCheckInterval: 30000,
|
|
526
|
+
maxStalledJobRetries: 3,
|
|
527
|
+
limiter: { max: 100, duration: 1000 }, // if QUEUE_RATE_LIMIT_ENABLED
|
|
528
|
+
metrics: { enabled: false, collectInterval: 10000 },
|
|
529
|
+
defaultDeadLetterOptions: { enabled: true, maxRetries: 3, queueSuffix: '-dead-letter' },
|
|
530
|
+
horizontalScaling: { enabled, maxWorkersPerInstance, jobsPerWorker, leaderElection: { heartbeatInterval: 5000, leaderTimeout: 15000 } },
|
|
531
|
+
defaultJobOptions: { attempts: 3, removeOnComplete: true, removeOnFail: false, backoff: { type: 'exponential', delay: 1000 } }
|
|
532
|
+
},
|
|
533
|
+
sqs: { driver: 'sqs', key, secret, prefix, suffix, queue: 'default', region: 'us-east-1' },
|
|
534
|
+
memory: { driver: 'memory', maxSize: 10000 }
|
|
535
|
+
},
|
|
536
|
+
failed: { driver: 'database', table: 'failed_jobs', prefix: 'stacks:failed' },
|
|
537
|
+
batching: { driver: 'redis', prefix: 'stacks:batches' },
|
|
538
|
+
worker: { concurrency: 5, shutdownTimeout: 30000 }
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Gotchas
|
|
543
|
+
- Default driver is `sync` -- jobs run immediately in the same process (no background processing)
|
|
544
|
+
- Database driver polls every 1 second, refreshes queue list every 10 seconds
|
|
545
|
+
- Atomic claim prevents race conditions: SELECT + UPDATE WHERE reserved_at IS NULL (CAS pattern)
|
|
546
|
+
- Backoff supports: fixed number, array of delays (indexed by attempt), or BackoffConfig object with strategy
|
|
547
|
+
- Job batches store callbacks in-memory (`Map`) -- callbacks only work in the dispatching process, not across restarts
|
|
548
|
+
- Redis driver wraps `bun-queue` -- uses `bun-queue` Queue class for actual queue management
|
|
549
|
+
- Failed job notifications use HMAC SHA-256 (via Web Crypto API) for webhook signatures
|
|
550
|
+
- Health check queries `jobs` and `failed_jobs` tables directly for real counts
|
|
551
|
+
- Rate limiter for notifications resets hourly (3,600,000ms)
|
|
552
|
+
- Testing: `fake()` sets global singleton, `restore()` clears it -- affects ALL code in the process
|
|
553
|
+
- Queue events use a custom `QueueEvents` class (Map + Set), not mitt
|
|
554
|
+
- Metrics track last 1000 completions and 100 errors in sliding windows
|
|
555
|
+
- Worker ID format: `worker-{pid}-{timestamp}`
|
|
556
|
+
- The `QUEUE_DRIVER` env var controls which driver is used (reads via `@stacksjs/env`)
|
|
557
|
+
- `dispatchAfter()` on sync driver uses `setTimeout` to delay then executes immediately
|
|
558
|
+
- Failed jobs table: `uuid`, `connection` ('database'), `queue`, `payload`, `exception` (stack trace), `failed_at`
|
|
559
|
+
- `process.on('unhandledRejection')` and `process.on('uncaughtException')` are set at module load in worker.ts
|