create-nexwire 4.2.5 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -12
- package/express/.env +1 -1
- package/express/package.json +1 -1
- package/express/src/config/database.ts +48 -29
- package/express/src/config/index.ts +83 -83
- package/express/src/config/openapi.ts +6 -24
- package/express/src/config/queue.ts +30 -30
- package/express/src/config/validate.ts +50 -40
- package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/express/src/framework/cache/cache.ts +40 -8
- package/express/src/framework/circuit-breaker/cb.ts +88 -60
- package/express/src/framework/database/connection.ts +52 -27
- package/express/src/framework/events/dispatcher.ts +61 -49
- package/express/src/framework/facade.ts +47 -3
- package/express/src/framework/http/app.ts +28 -22
- package/express/src/framework/http/logger.ts +37 -17
- package/express/src/framework/http/openapi.ts +9 -11
- package/express/src/framework/maker-cli/runtime/core.mjs +1 -2
- package/express/src/framework/modules/routes.ts +26 -21
- package/express/src/framework/notification/channels/broadcast.ts +15 -0
- package/express/src/framework/notification/channels/mail.ts +42 -0
- package/express/src/framework/notification/contracts.ts +29 -0
- package/express/src/framework/notification/index.ts +19 -94
- package/express/src/framework/notification/registry.ts +15 -0
- package/express/src/framework/providers/adapter.ts +27 -17
- package/express/src/framework/queue/queue.ts +43 -44
- package/express/src/framework/realtime/broadcast.ts +29 -5
- package/express/src/framework/realtime/socket.ts +149 -119
- package/express/src/framework/redis/client.ts +148 -151
- package/express/src/framework/runtime/server-adapter.ts +39 -34
- package/express/src/framework/scheduler/lock.ts +128 -116
- package/express/src/framework/server.ts +33 -49
- package/express/src/framework/session/session.ts +69 -14
- package/express/src/framework/storage/storage.ts +744 -785
- package/express/src/framework/support/mail.ts +15 -7
- package/express/src/middlewares/auth-middleware.ts +1 -2
- package/express/src/middlewares/role-middleware.ts +3 -13
- package/express/src/modules/auth/routes/index.ts +1 -2
- package/express/src/providers/notification.ts +4 -0
- package/express/src/resources/src/components/Switch.vue +60 -65
- package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
- package/express/src/resources/src/pages/auth/forgetPassword.vue +23 -30
- package/express/src/resources/src/pages/auth/login.vue +25 -28
- package/express/src/resources/src/pages/auth/register.vue +35 -39
- package/express/src/resources/src/pages/auth/resetPassword.vue +29 -37
- package/express/src/resources/src/pages/auth/verifyEmail.vue +15 -19
- package/express/src/resources/src/plugins/gum.ts +519 -203
- package/express/src/resources/src/plugins/pulse.ts +157 -147
- package/express/src/resources/src/stores/auth.ts +9 -6
- package/express/src/resources/vite.config.ts +54 -43
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/express/src/storage/logs/app.log +0 -0
- package/express/src/storage/logs/fatal.log +0 -0
- package/express/src/types/global.d.ts +32 -26
- package/fastify/.dockerignore +14 -0
- package/fastify/.env +44 -0
- package/fastify/.env.example +42 -0
- package/fastify/.vscode/extensions.json +6 -0
- package/fastify/.vscode/settings.json +37 -0
- package/fastify/LICENSE +21 -0
- package/fastify/README.md +232 -0
- package/fastify/biome.json +99 -0
- package/fastify/drizzle.config.ts +25 -0
- package/fastify/gitignore-stub +31 -0
- package/fastify/package.json +130 -0
- package/fastify/public/.gitkeep +0 -0
- package/fastify/src/config/app.ts +41 -0
- package/fastify/src/config/auth.ts +12 -0
- package/fastify/src/config/cache.ts +16 -0
- package/fastify/src/config/cookie.ts +15 -0
- package/fastify/src/config/cors.ts +12 -0
- package/fastify/src/config/database.ts +48 -0
- package/fastify/src/config/index.ts +83 -0
- package/fastify/src/config/jwt.ts +19 -0
- package/fastify/src/config/logging.ts +13 -0
- package/fastify/src/config/mail.ts +24 -0
- package/fastify/src/config/openapi.ts +36 -0
- package/fastify/src/config/queue.ts +30 -0
- package/fastify/src/config/rateLimit.ts +17 -0
- package/fastify/src/config/realtime.ts +15 -0
- package/fastify/src/config/redis.ts +18 -0
- package/fastify/src/config/security.ts +44 -0
- package/fastify/src/config/session.ts +17 -0
- package/fastify/src/config/storage.ts +22 -0
- package/fastify/src/config/validate.ts +50 -0
- package/fastify/src/database/migrations/sqlite/0000_init.sql +48 -0
- package/fastify/src/database/migrations/sqlite/meta/0000_snapshot.json +324 -0
- package/fastify/src/database/migrations/sqlite/meta/_journal.json +13 -0
- package/fastify/src/database/schema.ts +2 -0
- package/fastify/src/env.ts +76 -0
- package/fastify/src/framework/cache/cache.ts +114 -0
- package/fastify/src/framework/circuit-breaker/cb.ts +88 -0
- package/fastify/src/framework/database/app-schema.ts +37 -0
- package/fastify/src/framework/database/connection.ts +223 -0
- package/fastify/src/framework/database/migrate-hooks.ts +87 -0
- package/fastify/src/framework/database/paginate.ts +226 -0
- package/fastify/src/framework/database/schema.ts +20 -0
- package/fastify/src/framework/database/seed.ts +152 -0
- package/fastify/src/framework/events/dispatcher.ts +61 -0
- package/fastify/src/framework/facade.ts +96 -0
- package/fastify/src/framework/http/app.ts +176 -0
- package/fastify/src/framework/http/cors.ts +13 -0
- package/fastify/src/framework/http/fastify.d.ts +13 -0
- package/fastify/src/framework/http/logger.ts +110 -0
- package/fastify/src/framework/http/metrics.ts +22 -0
- package/fastify/src/framework/http/openapi.ts +48 -0
- package/fastify/src/framework/http/ratelimiter.ts +47 -0
- package/fastify/src/framework/http/security.ts +25 -0
- package/fastify/src/framework/http/static.ts +83 -0
- package/fastify/src/framework/http/validation.ts +49 -0
- package/fastify/src/framework/kernel.ts +43 -0
- package/fastify/src/framework/maker-cli/db/core.mjs +291 -0
- package/fastify/src/framework/maker-cli/db/index.mjs +204 -0
- package/fastify/src/framework/maker-cli/deploy/core.mjs +1046 -0
- package/fastify/src/framework/maker-cli/deploy/index.mjs +91 -0
- package/fastify/src/framework/maker-cli/index.mjs +65 -0
- package/fastify/src/framework/maker-cli/middleware/core.mjs +41 -0
- package/fastify/src/framework/maker-cli/middleware/index.mjs +17 -0
- package/fastify/src/framework/maker-cli/module/core.mjs +1066 -0
- package/fastify/src/framework/maker-cli/module/index.mjs +211 -0
- package/fastify/src/framework/maker-cli/provider/core.mjs +49 -0
- package/fastify/src/framework/maker-cli/provider/index.mjs +12 -0
- package/fastify/src/framework/maker-cli/runtime/build-ui.mjs +16 -0
- package/fastify/src/framework/maker-cli/runtime/core.mjs +560 -0
- package/fastify/src/framework/maker-cli/runtime/index.mjs +120 -0
- package/fastify/src/framework/maker-cli/runtime/redis-commander-patch.cjs +20 -0
- package/fastify/src/framework/maker-cli/stubs/controller/openapi.ts.stub +74 -0
- package/fastify/src/framework/maker-cli/stubs/controller/plain.ts.stub +77 -0
- package/fastify/src/framework/maker-cli/stubs/controller/standalone.ts.stub +51 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +52 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +55 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +51 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +55 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/README.stub +59 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +45 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +45 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +45 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +68 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +68 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +67 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +41 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/README.stub +78 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +120 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +133 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +131 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +11 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +16 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +13 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +14 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +7 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +54 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +70 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +24 -0
- package/fastify/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
- package/fastify/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
- package/fastify/src/framework/maker-cli/stubs/job/name.ts.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
- package/fastify/src/framework/maker-cli/stubs/middleware/name.ts.stub +15 -0
- package/fastify/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +10 -0
- package/fastify/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +10 -0
- package/fastify/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +10 -0
- package/fastify/src/framework/maker-cli/stubs/notification/controller.ts.stub +79 -0
- package/fastify/src/framework/maker-cli/stubs/notification/job.ts.stub +11 -0
- package/fastify/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
- package/fastify/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
- package/fastify/src/framework/maker-cli/stubs/notification/route.api.ts.stub +71 -0
- package/fastify/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +15 -0
- package/fastify/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +26 -0
- package/fastify/src/framework/maker-cli/stubs/notification/schema.ts.stub +26 -0
- package/fastify/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
- package/fastify/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
- package/fastify/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
- package/fastify/src/framework/maker-cli/stubs/route/api.ts.stub +101 -0
- package/fastify/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -0
- package/fastify/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +120 -0
- package/fastify/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +50 -0
- package/fastify/src/framework/maker-cli/stubs/schedule/name.ts.stub +36 -0
- package/fastify/src/framework/maker-cli/stubs/schema/name.plain.ts.stub +14 -0
- package/fastify/src/framework/maker-cli/stubs/schema/name.ts.stub +33 -0
- package/fastify/src/framework/maker-cli/stubs/seeder/name.ts.stub +18 -0
- package/fastify/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
- package/fastify/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
- package/fastify/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
- package/fastify/src/framework/maker-cli/stubs/test/unit.ts.stub +22 -0
- package/fastify/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
- package/fastify/src/framework/maker-cli/utils/env-config.mjs +44 -0
- package/fastify/src/framework/maker-cli/utils/env-db.mjs +61 -0
- package/fastify/src/framework/maker-cli/utils/file-ops.mjs +30 -0
- package/fastify/src/framework/maker-cli/utils/flags.mjs +14 -0
- package/fastify/src/framework/maker-cli/utils/help.mjs +379 -0
- package/fastify/src/framework/maker-cli/utils/naming.mjs +119 -0
- package/fastify/src/framework/maker-cli/utils/process.mjs +52 -0
- package/fastify/src/framework/maker-cli/utils/project-commands.mjs +201 -0
- package/fastify/src/framework/modules/discover.ts +57 -0
- package/fastify/src/framework/modules/routes.ts +31 -0
- package/fastify/src/framework/notification/channels/broadcast.ts +15 -0
- package/fastify/src/framework/notification/channels/mail.ts +42 -0
- package/fastify/src/framework/notification/contracts.ts +29 -0
- package/fastify/src/framework/notification/index.ts +59 -0
- package/fastify/src/framework/notification/registry.ts +15 -0
- package/fastify/src/framework/providers/adapter.ts +149 -0
- package/fastify/src/framework/providers/contracts.ts +126 -0
- package/fastify/src/framework/providers/loader.ts +105 -0
- package/fastify/src/framework/providers/registry.ts +88 -0
- package/fastify/src/framework/queue/clear.ts +22 -0
- package/fastify/src/framework/queue/queue.ts +401 -0
- package/fastify/src/framework/queue/ui.ts +194 -0
- package/fastify/src/framework/queue/worker.ts +61 -0
- package/fastify/src/framework/realtime/broadcast.ts +70 -0
- package/fastify/src/framework/realtime/index.ts +2 -0
- package/fastify/src/framework/realtime/socket-cookie.ts +72 -0
- package/fastify/src/framework/realtime/socket.ts +149 -0
- package/fastify/src/framework/realtime/types.ts +6 -0
- package/fastify/src/framework/realtime/ui.ts +16 -0
- package/fastify/src/framework/redis/client.ts +148 -0
- package/fastify/src/framework/runtime/runtime.ts +23 -0
- package/fastify/src/framework/runtime/server-adapter.ts +33 -0
- package/fastify/src/framework/scheduler/lock.ts +128 -0
- package/fastify/src/framework/scheduler/run.ts +48 -0
- package/fastify/src/framework/scheduler/scheduler.ts +155 -0
- package/fastify/src/framework/server.ts +162 -0
- package/fastify/src/framework/session/session.ts +160 -0
- package/fastify/src/framework/storage/storage.ts +744 -0
- package/fastify/src/framework/support/cookie.ts +71 -0
- package/fastify/src/framework/support/jwt.ts +33 -0
- package/fastify/src/framework/support/lifecycle.ts +35 -0
- package/fastify/src/framework/support/logger.ts +148 -0
- package/fastify/src/framework/support/mail.ts +56 -0
- package/fastify/src/framework/support/password.ts +23 -0
- package/fastify/src/framework/support/status-codes.ts +75 -0
- package/fastify/src/framework/support/upload.ts +181 -0
- package/fastify/src/framework/support/url.ts +41 -0
- package/fastify/src/middlewares/auth-middleware.ts +134 -0
- package/fastify/src/middlewares/role-middleware.ts +19 -0
- package/fastify/src/modules/auth/__tests__/forgetpass.test.ts +200 -0
- package/fastify/src/modules/auth/__tests__/login.test.ts +171 -0
- package/fastify/src/modules/auth/__tests__/logout.test.ts +60 -0
- package/fastify/src/modules/auth/__tests__/logoutalldevices.test.ts +49 -0
- package/fastify/src/modules/auth/__tests__/me.test.ts +91 -0
- package/fastify/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
- package/fastify/src/modules/auth/__tests__/register.test.ts +291 -0
- package/fastify/src/modules/auth/__tests__/resetpass.test.ts +206 -0
- package/fastify/src/modules/auth/__tests__/verifyemail.test.ts +183 -0
- package/fastify/src/modules/auth/controllers/auth.ts +252 -0
- package/fastify/src/modules/auth/controllers/role.ts +35 -0
- package/fastify/src/modules/auth/database/models/role.ts +14 -0
- package/fastify/src/modules/auth/database/models/user.ts +46 -0
- package/fastify/src/modules/auth/database/seeders/role.ts +18 -0
- package/fastify/src/modules/auth/database/seeders/user.ts +35 -0
- package/fastify/src/modules/auth/facade.ts +12 -0
- package/fastify/src/modules/auth/helpers/auth.ts +115 -0
- package/fastify/src/modules/auth/jobs/forgetpass.ts +20 -0
- package/fastify/src/modules/auth/jobs/registeruser.ts +23 -0
- package/fastify/src/modules/auth/jobs/verifyemail.ts +18 -0
- package/fastify/src/modules/auth/routes/index.ts +185 -0
- package/fastify/src/modules/auth/routes/role.ts +56 -0
- package/fastify/src/modules/auth/schemas/auth.ts +116 -0
- package/fastify/src/modules/auth/services/forgetpassword.ts +66 -0
- package/fastify/src/modules/auth/services/login.ts +63 -0
- package/fastify/src/modules/auth/services/logout.ts +25 -0
- package/fastify/src/modules/auth/services/logoutalldevices.ts +19 -0
- package/fastify/src/modules/auth/services/me.ts +31 -0
- package/fastify/src/modules/auth/services/refreshtoken.ts +110 -0
- package/fastify/src/modules/auth/services/register.ts +141 -0
- package/fastify/src/modules/auth/services/resetpassword.ts +69 -0
- package/fastify/src/modules/auth/services/verifyemail.ts +77 -0
- package/fastify/src/modules/auth/types/auth.ts +31 -0
- package/fastify/src/providers/README.md +107 -0
- package/fastify/src/providers/notification.ts +4 -0
- package/fastify/src/resources/index.html +17 -0
- package/fastify/src/resources/src/App.vue +5 -0
- package/fastify/src/resources/src/assets/css/styles.css +14761 -0
- package/fastify/src/resources/src/assets/css/styles.css.map +1 -0
- package/fastify/src/resources/src/assets/fonts/public-sans/public-sans-latin-ext.woff2 +0 -0
- package/fastify/src/resources/src/assets/fonts/public-sans/public-sans-latin.woff2 +0 -0
- package/fastify/src/resources/src/assets/images/favicon/favicon.ico +0 -0
- package/fastify/src/resources/src/assets/images/logo.png +0 -0
- package/fastify/src/resources/src/assets/scss/custom.css +217 -0
- package/fastify/src/resources/src/assets/scss/custom.css.map +1 -0
- package/fastify/src/resources/src/assets/scss/custom.scss +1346 -0
- package/fastify/src/resources/src/components/Button.vue +32 -0
- package/fastify/src/resources/src/components/Checkbox.vue +32 -0
- package/fastify/src/resources/src/components/Datepicker.vue +106 -0
- package/fastify/src/resources/src/components/FeatureButton.vue +53 -0
- package/fastify/src/resources/src/components/FloatButton.vue +33 -0
- package/fastify/src/resources/src/components/Href.vue +32 -0
- package/fastify/src/resources/src/components/Input.vue +243 -0
- package/fastify/src/resources/src/components/InputGroup.vue +147 -0
- package/fastify/src/resources/src/components/InputPasswordToggle.vue +229 -0
- package/fastify/src/resources/src/components/Modal.vue +71 -0
- package/fastify/src/resources/src/components/Pagebar.vue +26 -0
- package/fastify/src/resources/src/components/Refresh.vue +19 -0
- package/fastify/src/resources/src/components/Select.vue +384 -0
- package/fastify/src/resources/src/components/Spinner.vue +42 -0
- package/fastify/src/resources/src/components/Switch.vue +60 -0
- package/fastify/src/resources/src/components/TextArea.vue +117 -0
- package/fastify/src/resources/src/components/Toast.vue +56 -0
- package/fastify/src/resources/src/components/datatable/DataTableSkeleton.vue +98 -0
- package/fastify/src/resources/src/components/datatable/Pagination.vue +147 -0
- package/fastify/src/resources/src/components/datatable/SelectOpption.vue +64 -0
- package/fastify/src/resources/src/components/datatable/index.vue +199 -0
- package/fastify/src/resources/src/composables/useAuth.ts +52 -0
- package/fastify/src/resources/src/helpers/assets.ts +51 -0
- package/fastify/src/resources/src/helpers/nformatter.ts +10 -0
- package/fastify/src/resources/src/helpers/utils.ts +75 -0
- package/fastify/src/resources/src/layouts/AuthLayout.vue +20 -0
- package/fastify/src/resources/src/layouts/Layout/Footer.vue +23 -0
- package/fastify/src/resources/src/layouts/Layout/Header.vue +124 -0
- package/fastify/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
- package/fastify/src/resources/src/layouts/Layout/index.vue +87 -0
- package/fastify/src/resources/src/main.ts +28 -0
- package/fastify/src/resources/src/pages/auth/forgetPassword.vue +76 -0
- package/fastify/src/resources/src/pages/auth/login.vue +96 -0
- package/fastify/src/resources/src/pages/auth/register.vue +129 -0
- package/fastify/src/resources/src/pages/auth/resetPassword.vue +110 -0
- package/fastify/src/resources/src/pages/auth/verifyEmail.vue +64 -0
- package/fastify/src/resources/src/pages/dashboard/index.vue +66 -0
- package/fastify/src/resources/src/plugins/axios.ts +31 -0
- package/fastify/src/resources/src/plugins/browserDetect.ts +55 -0
- package/fastify/src/resources/src/plugins/dialog.ts +216 -0
- package/fastify/src/resources/src/plugins/gum.ts +683 -0
- package/fastify/src/resources/src/plugins/pulse.ts +157 -0
- package/fastify/src/resources/src/plugins/routeProgress.ts +87 -0
- package/fastify/src/resources/src/router/index.ts +85 -0
- package/fastify/src/resources/src/stores/admin-ui.ts +148 -0
- package/fastify/src/resources/src/stores/auth.ts +187 -0
- package/fastify/src/resources/tsconfig.json +19 -0
- package/fastify/src/resources/vite.config.ts +64 -0
- package/fastify/src/storage/app/private/.gitkeep +0 -0
- package/fastify/src/storage/app/public/.gitkeep +0 -0
- package/fastify/src/storage/app/tmp/generated/.gitkeep +0 -0
- package/fastify/src/storage/database/nexwire.sqlite +0 -0
- package/fastify/src/storage/logs/.gitkeep +0 -0
- package/fastify/src/storage/logs/app.log +0 -0
- package/fastify/src/storage/logs/fatal.log +0 -0
- package/fastify/src/storage/tmp/.gitkeep +0 -0
- package/fastify/src/storage/trash/modules/.gitkeep +0 -0
- package/fastify/src/types/global.d.ts +32 -0
- package/fastify/tsconfig.json +19 -0
- package/fastify/vitest.config.ts +38 -0
- package/hono/.env +1 -1
- package/hono/package.json +1 -1
- package/hono/src/config/database.ts +48 -29
- package/hono/src/config/index.ts +83 -83
- package/hono/src/config/openapi.ts +6 -24
- package/hono/src/config/queue.ts +30 -30
- package/hono/src/config/validate.ts +50 -40
- package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/hono/src/framework/cache/cache.ts +40 -8
- package/hono/src/framework/circuit-breaker/cb.ts +88 -60
- package/hono/src/framework/database/connection.ts +52 -27
- package/hono/src/framework/events/dispatcher.ts +61 -49
- package/hono/src/framework/facade.ts +39 -1
- package/hono/src/framework/http/logger.ts +86 -81
- package/hono/src/framework/http/openapi.ts +7 -9
- package/hono/src/framework/maker-cli/runtime/core.mjs +1 -2
- package/hono/src/framework/notification/channels/broadcast.ts +15 -0
- package/hono/src/framework/notification/channels/mail.ts +42 -0
- package/hono/src/framework/notification/contracts.ts +29 -0
- package/hono/src/framework/notification/index.ts +19 -94
- package/hono/src/framework/notification/registry.ts +15 -0
- package/hono/src/framework/providers/adapter.ts +27 -17
- package/hono/src/framework/queue/queue.ts +43 -44
- package/hono/src/framework/realtime/broadcast.ts +29 -5
- package/hono/src/framework/realtime/socket.ts +217 -196
- package/hono/src/framework/redis/client.ts +148 -151
- package/hono/src/framework/scheduler/lock.ts +128 -116
- package/hono/src/framework/server.ts +27 -29
- package/hono/src/framework/session/session.ts +62 -14
- package/hono/src/framework/storage/storage.ts +744 -785
- package/hono/src/framework/support/mail.ts +56 -48
- package/hono/src/middlewares/auth-middleware.ts +1 -2
- package/hono/src/middlewares/role-middleware.ts +3 -13
- package/hono/src/modules/auth/routes/index.ts +1 -2
- package/hono/src/providers/notification.ts +4 -0
- package/hono/src/resources/src/components/Switch.vue +60 -65
- package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
- package/hono/src/resources/src/pages/auth/forgetPassword.vue +23 -29
- package/hono/src/resources/src/pages/auth/login.vue +26 -29
- package/hono/src/resources/src/pages/auth/register.vue +36 -40
- package/hono/src/resources/src/pages/auth/resetPassword.vue +29 -37
- package/hono/src/resources/src/pages/auth/verifyEmail.vue +15 -19
- package/hono/src/resources/src/plugins/gum.ts +519 -203
- package/hono/src/resources/src/plugins/pulse.ts +157 -147
- package/hono/src/resources/src/stores/auth.ts +9 -6
- package/hono/src/resources/vite.config.ts +54 -43
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/hono/src/storage/logs/app.log +0 -0
- package/hono/src/storage/logs/fatal.log +0 -0
- package/hono/src/types/global.d.ts +32 -26
- package/package.json +4 -3
- package/src/index.js +143 -15
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { type Job, type JobsOptions, Queue, QueueEvents, Worker } from "bullmq";
|
|
2
|
+
import { type DurableContext, type DurableJob, DurableWorker, RedisStateStore } from "bullmq-durable";
|
|
3
|
+
import { queueConfig, redisConfig } from "@/config/index.js";
|
|
4
|
+
import { discoverModuleFiles, importFile } from "@/framework/modules/discover.js";
|
|
5
|
+
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
6
|
+
import { logger } from "@/framework/support/logger.js";
|
|
7
|
+
|
|
8
|
+
export type QueueJob = {
|
|
9
|
+
name: string;
|
|
10
|
+
data: any;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type QueueHandler = (job: Job) => Promise<any>;
|
|
14
|
+
export type DurableQueueHandler = (job: DurableJob, ctx: DurableContext) => Promise<any>;
|
|
15
|
+
export type AnyQueueHandler = QueueHandler | DurableQueueHandler;
|
|
16
|
+
|
|
17
|
+
const queues = new Map<string, Queue>();
|
|
18
|
+
const events = new Map<string, QueueEvents>();
|
|
19
|
+
const handlers = new Map<string, QueueHandler>();
|
|
20
|
+
const durableHandlers = new Map<string, DurableQueueHandler>();
|
|
21
|
+
const workers: Worker[] = [];
|
|
22
|
+
/**
|
|
23
|
+
* Queue names this process already has a worker for. Each BullMQ `Worker` holds
|
|
24
|
+
* its own blocking Redis connection, so starting the runtime twice for one queue
|
|
25
|
+
* would silently double that connection for no benefit. Cleared by
|
|
26
|
+
* `stopQueueRuntime` so a later boot in the same process still works.
|
|
27
|
+
*/
|
|
28
|
+
const workedQueues = new Set<string>();
|
|
29
|
+
let durableStateStore: RedisStateStore | null = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Why: Jobs already reported as dropped, so that one unreachable queue cannot
|
|
33
|
+
* flood the log from a request hot path.
|
|
34
|
+
* When: A call to queueJob finds no Redis.
|
|
35
|
+
* Where: Queue facade module scope.
|
|
36
|
+
* How: Keyed by queue + job name, because that pair is what identifies a queued
|
|
37
|
+
* job - two queues running the same job name are two separate failures and
|
|
38
|
+
* must each be reported once.
|
|
39
|
+
*/
|
|
40
|
+
const droppedJobs = new Set<string>();
|
|
41
|
+
|
|
42
|
+
function queuePrefix() {
|
|
43
|
+
return queueConfig.prefix;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function key(queue: string, job: string) {
|
|
47
|
+
return `${queue}:${job}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Why: BullMQ manages its own connections. We pass the URL string,
|
|
52
|
+
* not the shared client, so BullMQ doesn't crash if the shared client dies.
|
|
53
|
+
*/
|
|
54
|
+
function connectionOptions() {
|
|
55
|
+
return { url: redisConfig.url };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Why: Registers a job handler for a queue + job name.
|
|
60
|
+
* When: Module job files are loaded during boot.
|
|
61
|
+
* Where: Module job definition files under `src/modules/<module>/jobs`.
|
|
62
|
+
* How: Stores handler in an in-memory lookup key. Pass `{ durable: true }` to
|
|
63
|
+
* register a checkpointed handler `(job, ctx)` powered by bullmq-durable;
|
|
64
|
+
* such a handler can split its work into `ctx.step(...)` checkpoints that
|
|
65
|
+
* survive crashes/restarts instead of re-running from scratch.
|
|
66
|
+
*/
|
|
67
|
+
export function shouldQueue(job: string, queue: string, handler: AnyQueueHandler, options: { durable?: boolean } = {}) {
|
|
68
|
+
const target = key(queue || "default", job);
|
|
69
|
+
if (options.durable) {
|
|
70
|
+
durableHandlers.set(target, handler as DurableQueueHandler);
|
|
71
|
+
} else {
|
|
72
|
+
handlers.set(target, handler as QueueHandler);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Why: Gets or lazily creates a BullMQ queue instance.
|
|
78
|
+
* When: Enqueueing jobs or ensuring queue resources.
|
|
79
|
+
* Where: Queue facade and dispatcher.
|
|
80
|
+
* How: Reuses shared Redis connection and memoized queue map.
|
|
81
|
+
*/
|
|
82
|
+
export function getQueue(queue = "default") {
|
|
83
|
+
// Check if Redis is supposed to be enabled
|
|
84
|
+
if (!redisConfig.enabled) return null;
|
|
85
|
+
|
|
86
|
+
if (!queues.has(queue)) {
|
|
87
|
+
queues.set(
|
|
88
|
+
queue,
|
|
89
|
+
new Queue(queue, {
|
|
90
|
+
connection: connectionOptions(), // Let BullMQ handle its own connection
|
|
91
|
+
prefix: queuePrefix(),
|
|
92
|
+
defaultJobOptions: {
|
|
93
|
+
attempts: 3,
|
|
94
|
+
backoff: { type: "exponential", delay: 3000 },
|
|
95
|
+
removeOnComplete: 1000,
|
|
96
|
+
removeOnFail: 5000
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return queues.get(queue)!;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Why: Provides the shared Redis-backed state store for durable queues.
|
|
107
|
+
* When: Durable queue/worker construction.
|
|
108
|
+
* Where: Queue facade internals.
|
|
109
|
+
* How: Creates one `RedisStateStore` reusing the shared Redis client so durable
|
|
110
|
+
* state lives in Redis. Note the store opens its own client on first use -
|
|
111
|
+
* bullmq-durable does this so that merely constructing a store costs no
|
|
112
|
+
* connection - which is why `stopQueueRuntime` must call `close()` on it.
|
|
113
|
+
*/
|
|
114
|
+
function durableStateStoreIfReady() {
|
|
115
|
+
const client = redisClientIfReady();
|
|
116
|
+
if (!client) return null;
|
|
117
|
+
|
|
118
|
+
if (!durableStateStore) {
|
|
119
|
+
durableStateStore = new RedisStateStore({ connection: client as never, prefix: queueConfig.durablePrefix });
|
|
120
|
+
}
|
|
121
|
+
return durableStateStore;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Why: Facade-friendly alias for getQueue.
|
|
126
|
+
* When: Consumers want fluent queue access.
|
|
127
|
+
* Where: Framework facade/API usage.
|
|
128
|
+
* How: Delegates directly to getQueue.
|
|
129
|
+
*/
|
|
130
|
+
export function queue(queueName = "default") {
|
|
131
|
+
return getQueue(queueName);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Why: Ensures configured queues exist before UI/runtime usage.
|
|
136
|
+
* When: Queue dashboard setup or startup warmup.
|
|
137
|
+
* Where: Queue UI/bootstrap code.
|
|
138
|
+
* How: Iterates names and resolves queue instances.
|
|
139
|
+
*/
|
|
140
|
+
export function ensureQueues(queueNames: string[]) {
|
|
141
|
+
for (const queueName of queueNames) {
|
|
142
|
+
getQueue(queueName);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Why: Returns all created queue instances.
|
|
148
|
+
* When: Queue introspection and dashboard binding.
|
|
149
|
+
* Where: Queue dashboard setup code.
|
|
150
|
+
* How: Converts internal queue map to array.
|
|
151
|
+
*/
|
|
152
|
+
export function getAllQueues() {
|
|
153
|
+
return Array.from(queues.values());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Why: Explains that a job was lost instead of letting it vanish.
|
|
158
|
+
* When: queueJob is called and no Redis connection is ready.
|
|
159
|
+
* Where: Called from queueJob.
|
|
160
|
+
* How: Warns once per queue + job. The message names the job and its queue,
|
|
161
|
+
* says plainly that it never ran, and gives both the Redis fix and the
|
|
162
|
+
* in-process alternative, so a caller reading only this line knows what
|
|
163
|
+
* to do without opening framework source.
|
|
164
|
+
*/
|
|
165
|
+
function warnDroppedJob(queueName: string, job: string, delay?: number) {
|
|
166
|
+
const id = key(queueName, job);
|
|
167
|
+
if (droppedJobs.has(id)) return;
|
|
168
|
+
droppedJobs.add(id);
|
|
169
|
+
|
|
170
|
+
const schedule = delay ? ` It was due in ${delay}s, so it was never even scheduled.` : "";
|
|
171
|
+
console.warn(
|
|
172
|
+
`[queue] Dropped job "${job}" on queue "${queueName}". Redis is not available, so this job was never queued and never ran.${schedule} ` +
|
|
173
|
+
`Enable REDIS in .env and run a queue worker, or call dispatchCommand("${job}", payload) without { async: true } to run it in-process now.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Why: Adds a job to BullMQ with project defaults.
|
|
179
|
+
* When: Commands/events need background execution.
|
|
180
|
+
* Where: Dispatcher and feature modules.
|
|
181
|
+
* How: Resolves queue then calls add with retry/backoff options. When Redis is
|
|
182
|
+
* absent there is nowhere to put the job, so it returns null and says so -
|
|
183
|
+
* previously it returned null with no output at all, which meant silently
|
|
184
|
+
* losing the work.
|
|
185
|
+
*/
|
|
186
|
+
export async function queueJob(
|
|
187
|
+
job: string,
|
|
188
|
+
data: any,
|
|
189
|
+
options: {
|
|
190
|
+
queue?: string;
|
|
191
|
+
delay?: number;
|
|
192
|
+
attempts?: number;
|
|
193
|
+
jobId?: string;
|
|
194
|
+
priority?: number;
|
|
195
|
+
removeOnComplete?: JobsOptions["removeOnComplete"];
|
|
196
|
+
removeOnFail?: JobsOptions["removeOnFail"];
|
|
197
|
+
backoff?: JobsOptions["backoff"];
|
|
198
|
+
} = {}
|
|
199
|
+
) {
|
|
200
|
+
const queueName = options.queue || "default";
|
|
201
|
+
const queue = getQueue(queueName);
|
|
202
|
+
if (!queue) {
|
|
203
|
+
warnDroppedJob(queueName, job, options.delay);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
return await queue.add(job, data, {
|
|
209
|
+
delay: options.delay ? options.delay * 1000 : 0,
|
|
210
|
+
attempts: options.attempts ?? 3,
|
|
211
|
+
jobId: options.jobId,
|
|
212
|
+
priority: options.priority,
|
|
213
|
+
removeOnComplete: options.removeOnComplete ?? 1000,
|
|
214
|
+
removeOnFail: options.removeOnFail ?? 5000,
|
|
215
|
+
backoff: options.backoff ?? { type: "exponential", delay: 3000 }
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
logger.error(`Failed to queue job ${job}`, { error });
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Why: Loads module job files so handlers are registered.
|
|
225
|
+
* When: Worker start and explicit queue boot flows.
|
|
226
|
+
* Where: Worker/runtime bootstrap.
|
|
227
|
+
* How: Discovers jobs glob and imports each file.
|
|
228
|
+
*/
|
|
229
|
+
export async function bootQueueJobs() {
|
|
230
|
+
const files = await discoverModuleFiles("**/jobs/*.{ts,js}");
|
|
231
|
+
for (const file of files) await importFile(file);
|
|
232
|
+
return files.length;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Why: Deletes phantom queues whose names are neither configured nor worked by
|
|
237
|
+
* this process.
|
|
238
|
+
* When: Queue worker boot, after workers start.
|
|
239
|
+
* Where: Worker runtime bootstrap.
|
|
240
|
+
* How: Scans Redis for `{prefix}*:meta` keys, and for each queue name that is
|
|
241
|
+
* not in queueConfig.queues nor in the queues this worker was launched
|
|
242
|
+
* with (--queue), obliterates it only when it holds no live jobs
|
|
243
|
+
* (waiting/active/delayed/paused/prioritized/waiting-children). Historical
|
|
244
|
+
* completed/failed entries are ignored so leftover history cannot keep a
|
|
245
|
+
* dead queue alive. Recreates nothing permanent: BullMQ re-creates a
|
|
246
|
+
* queue's keys on demand if any code path still dispatches to it.
|
|
247
|
+
*/
|
|
248
|
+
export async function pruneStaleQueues(protectedQueues: string[] = []) {
|
|
249
|
+
if (!queueConfig.autoPruneQueues || !redisConfig.enabled) return;
|
|
250
|
+
|
|
251
|
+
const client = redisClientIfReady();
|
|
252
|
+
if (!client) return;
|
|
253
|
+
|
|
254
|
+
const configured = new Set([...queueConfig.queues, ...protectedQueues]);
|
|
255
|
+
const prefix = `${queuePrefix()}:`;
|
|
256
|
+
const candidates = new Set<string>();
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
let cursor = "0";
|
|
260
|
+
do {
|
|
261
|
+
const [nextCursor, keys] = await client.scan(cursor, "MATCH", `${prefix}*:meta`, "COUNT", 100);
|
|
262
|
+
cursor = nextCursor;
|
|
263
|
+
for (const key of keys) {
|
|
264
|
+
const name = key.slice(prefix.length, -5);
|
|
265
|
+
if (name && !configured.has(name)) candidates.add(name);
|
|
266
|
+
}
|
|
267
|
+
} while (cursor !== "0");
|
|
268
|
+
|
|
269
|
+
for (const name of candidates) {
|
|
270
|
+
try {
|
|
271
|
+
const queue = getQueue(name);
|
|
272
|
+
if (!queue) continue;
|
|
273
|
+
|
|
274
|
+
const counts = await queue.getJobCounts("waiting", "active", "delayed", "paused", "prioritized", "waiting-children");
|
|
275
|
+
const live = Object.values(counts).reduce((sum, n) => sum + (Number(n) || 0), 0);
|
|
276
|
+
if (live !== 0) continue;
|
|
277
|
+
|
|
278
|
+
await queue.obliterate({ force: true });
|
|
279
|
+
logger.info(`Pruned stale queue "${name}"`);
|
|
280
|
+
} catch (error: any) {
|
|
281
|
+
logger.warn(`Could not prune stale queue "${name}": ${error?.message || error}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (error: any) {
|
|
285
|
+
logger.warn(`Could not scan queues for stale cleanup: ${error?.message || error}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Why: Starts BullMQ workers for selected queues.
|
|
291
|
+
* When: Queue worker process boots.
|
|
292
|
+
* Where: Worker runtime entrypoint.
|
|
293
|
+
* How: Boots handlers, creates QueueEvents, and creates workers.
|
|
294
|
+
*/
|
|
295
|
+
export async function startQueueWorker(queueNames = ["default"]) {
|
|
296
|
+
await bootQueueJobs();
|
|
297
|
+
if (!redisConfig.enabled) throw new Error("Redis is required for queue workers");
|
|
298
|
+
|
|
299
|
+
for (const queueName of queueNames) {
|
|
300
|
+
if (workedQueues.has(queueName)) continue;
|
|
301
|
+
workedQueues.add(queueName);
|
|
302
|
+
|
|
303
|
+
if (!events.has(queueName)) {
|
|
304
|
+
// Let BullMQ manage its own connection for events
|
|
305
|
+
events.set(queueName, new QueueEvents(queueName, { connection: connectionOptions(), prefix: queuePrefix() }));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const durable = [...durableHandlers.keys()].some((k) => k.startsWith(`${queueName}:`));
|
|
309
|
+
|
|
310
|
+
if (durable) {
|
|
311
|
+
// Durable worker still needs a shared client for its state store in this specific lib
|
|
312
|
+
const store = durableStateStoreIfReady();
|
|
313
|
+
if (!store) throw new Error("Redis is required for durable queue workers");
|
|
314
|
+
|
|
315
|
+
const map: Record<string, AnyQueueHandler> = {};
|
|
316
|
+
for (const [k, h] of handlers) {
|
|
317
|
+
if (k.startsWith(`${queueName}:`)) map[k.slice(queueName.length + 1)] = h;
|
|
318
|
+
}
|
|
319
|
+
for (const [k, h] of durableHandlers) {
|
|
320
|
+
if (k.startsWith(`${queueName}:`)) map[k.slice(queueName.length + 1)] = h;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const worker = new DurableWorker(queueName, map as never, {
|
|
324
|
+
connection: connectionOptions(),
|
|
325
|
+
prefix: queuePrefix(),
|
|
326
|
+
stateStore: store,
|
|
327
|
+
concurrency: 10
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
worker.on("completed", (job) => logger.info(`Processed: ${job.name} (${queueName})`));
|
|
331
|
+
worker.on("failed", (job, error) => logger.warn(`Failed: ${job?.name ?? "unknown"} (${queueName}) - ${error.message}`));
|
|
332
|
+
|
|
333
|
+
workers.push(worker as unknown as Worker);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const worker = new Worker(
|
|
338
|
+
queueName,
|
|
339
|
+
async (job) => {
|
|
340
|
+
logger.info(`Processing: ${job.name} (${queueName})`);
|
|
341
|
+
const handler = handlers.get(key(queueName, job.name));
|
|
342
|
+
if (!handler) throw new Error(`No handler registered for ${queueName}:${job.name}`);
|
|
343
|
+
return await handler(job);
|
|
344
|
+
},
|
|
345
|
+
{ connection: connectionOptions(), prefix: queuePrefix(), concurrency: 10 }
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
worker.on("completed", (job) => logger.info(`Processed: ${job.name} (${queueName})`));
|
|
349
|
+
worker.on("failed", (job, error) => logger.warn(`Failed: ${job?.name ?? "unknown"} (${queueName}) - ${error.message}`));
|
|
350
|
+
|
|
351
|
+
workers.push(worker);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Why: Hard-clears ALL queue-system state for reset workflows.
|
|
357
|
+
* When: Local cleanup and maintenance commands.
|
|
358
|
+
* Where: `queue:clear` command.
|
|
359
|
+
* How: Wipes every BullMQ queue key (including jobs, job schedulers, and
|
|
360
|
+
* repeat keys for queues not listed in `queues`, e.g. `demo`), plus the
|
|
361
|
+
* durable key space, by scanning each prefix and deleting in batches.
|
|
362
|
+
* Cache/session/rate-limit keys are intentionally left alone.
|
|
363
|
+
*/
|
|
364
|
+
export async function clearQueue() {
|
|
365
|
+
if (!redisConfig.enabled) return;
|
|
366
|
+
const client = redisClientIfReady();
|
|
367
|
+
if (!client) return;
|
|
368
|
+
|
|
369
|
+
for (const prefix of [queuePrefix(), queueConfig.durablePrefix]) {
|
|
370
|
+
const stream = client.scanStream({ match: `${prefix}*`, count: 100 });
|
|
371
|
+
for await (const keys of stream) {
|
|
372
|
+
if (keys.length) await client.del(keys);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Why: Gracefully stops all workers/events/queues.
|
|
379
|
+
* When: Process shutdown.
|
|
380
|
+
* Where: Server/worker lifecycle hooks.
|
|
381
|
+
* How: Closes resources with Promise.allSettled and clears registries. The
|
|
382
|
+
* durable state store is closed too: it owns a Redis client of its own, and
|
|
383
|
+
* dropping the reference without closing it would leak that connection on
|
|
384
|
+
* every restart until Redis runs out of clients.
|
|
385
|
+
*/
|
|
386
|
+
export async function stopQueueRuntime() {
|
|
387
|
+
await Promise.allSettled(workers.map((worker) => worker.close()));
|
|
388
|
+
workers.length = 0;
|
|
389
|
+
workedQueues.clear();
|
|
390
|
+
|
|
391
|
+
await Promise.allSettled(Array.from(events.values()).map((queueEvents) => queueEvents.close()));
|
|
392
|
+
events.clear();
|
|
393
|
+
|
|
394
|
+
await Promise.allSettled(Array.from(queues.values()).map((queue) => queue.close()));
|
|
395
|
+
queues.clear();
|
|
396
|
+
|
|
397
|
+
if (durableStateStore) {
|
|
398
|
+
await Promise.allSettled([durableStateStore.close()]);
|
|
399
|
+
durableStateStore = null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createBullBoard } from "@bull-board/api";
|
|
2
|
+
import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
|
|
3
|
+
import { FastifyAdapter } from "@bull-board/fastify";
|
|
4
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
5
|
+
import { queueConfig, redisConfig } from "@/config/index.js";
|
|
6
|
+
import { ensureQueues, getQueue } from "@/framework/queue/queue.js";
|
|
7
|
+
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
8
|
+
import { cookie } from "@/framework/support/cookie.js";
|
|
9
|
+
import { jwt } from "@/framework/support/jwt.js";
|
|
10
|
+
import { parseCsvOrFallback } from "@/framework/support/lifecycle.js";
|
|
11
|
+
|
|
12
|
+
const BASE_PATH = queueConfig.queueUi || "/queues";
|
|
13
|
+
|
|
14
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
15
|
+
|
|
16
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
17
|
+
let syncInFlight = false;
|
|
18
|
+
|
|
19
|
+
const dashboardQueueNames = new Set<string>();
|
|
20
|
+
|
|
21
|
+
const serverAdapter = new FastifyAdapter();
|
|
22
|
+
serverAdapter.setBasePath(BASE_PATH);
|
|
23
|
+
|
|
24
|
+
const bullBoard = createBullBoard({
|
|
25
|
+
queues: [],
|
|
26
|
+
serverAdapter,
|
|
27
|
+
options: {
|
|
28
|
+
uiConfig: {
|
|
29
|
+
boardTitle: "Queue Dashboard"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export function stopQueueDashboard() {
|
|
35
|
+
if (pollTimer) {
|
|
36
|
+
clearInterval(pollTimer);
|
|
37
|
+
pollTimer = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function refreshDashboardQueues() {
|
|
42
|
+
if (syncInFlight) return;
|
|
43
|
+
syncInFlight = true;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const client = redisClientIfReady();
|
|
47
|
+
if (!client) return;
|
|
48
|
+
|
|
49
|
+
const discovered = (await discoverQueueNames()).sort();
|
|
50
|
+
ensureQueues(discovered);
|
|
51
|
+
|
|
52
|
+
for (const name of discovered) {
|
|
53
|
+
if (dashboardQueueNames.has(name)) continue;
|
|
54
|
+
const queue = getQueue(name);
|
|
55
|
+
if (!queue) continue;
|
|
56
|
+
bullBoard.addQueue(new BullMQAdapter(queue));
|
|
57
|
+
dashboardQueueNames.add(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const name of Array.from(dashboardQueueNames)) {
|
|
61
|
+
if (!discovered.includes(name)) {
|
|
62
|
+
bullBoard.removeQueue(name);
|
|
63
|
+
dashboardQueueNames.delete(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Keep the previous queue list if a refresh fails.
|
|
68
|
+
} finally {
|
|
69
|
+
syncInFlight = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function queuePrefix() {
|
|
74
|
+
return queueConfig.prefix;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function allowedQueueDashboardEmails() {
|
|
78
|
+
const emails = parseCsvOrFallback(queueConfig.allowedEmails, []);
|
|
79
|
+
return new Set(emails.map((email) => email.toLowerCase()));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function dashboardAuth() {
|
|
83
|
+
const allowedEmails = allowedQueueDashboardEmails();
|
|
84
|
+
|
|
85
|
+
return async (request: FastifyRequest, reply: FastifyReply) => {
|
|
86
|
+
/**
|
|
87
|
+
* Why: The email allowlist is the ONLY gate on the queue dashboard UI.
|
|
88
|
+
* When: No `allowedEmails` configured → the dashboard is open to everyone
|
|
89
|
+
* in both dev and production (no login required). When configured,
|
|
90
|
+
* only the listed email(s) pass — everyone else is blocked.
|
|
91
|
+
* Where: BullMQ dashboard route hook.
|
|
92
|
+
* How: An empty allowlist short-circuits straight through; otherwise the
|
|
93
|
+
* requester's email (from the validated access token) must be in the
|
|
94
|
+
* set. This keeps BullMQ workers (mail delivery etc.) always running
|
|
95
|
+
* and gating strictly UI-only.
|
|
96
|
+
*/
|
|
97
|
+
if (allowedEmails.size === 0) return;
|
|
98
|
+
|
|
99
|
+
const rawToken = await cookie.getAuth(request);
|
|
100
|
+
if (!rawToken) {
|
|
101
|
+
return reply.code(401).send({ message: "Unauthorized" });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const payload = await jwt.verifyToken(rawToken, "access");
|
|
105
|
+
if (!payload) {
|
|
106
|
+
return reply.code(401).send({ message: "Unauthorized" });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const normalized = payload as Record<string, unknown>;
|
|
110
|
+
const email = String(normalized.email ?? "")
|
|
111
|
+
.trim()
|
|
112
|
+
.toLowerCase();
|
|
113
|
+
|
|
114
|
+
if (!allowedEmails.has(email)) {
|
|
115
|
+
return reply.code(401).send({ message: "Unauthorized" });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function discoverQueueNames(): Promise<string[]> {
|
|
121
|
+
const client = redisClientIfReady();
|
|
122
|
+
if (!client) return [];
|
|
123
|
+
|
|
124
|
+
const prefix = `${queuePrefix()}:`;
|
|
125
|
+
const names = new Set<string>();
|
|
126
|
+
let cursor = 0;
|
|
127
|
+
|
|
128
|
+
do {
|
|
129
|
+
const [nextCursor, keys] = await client.scan(cursor, "MATCH", `${prefix}*:meta`, "COUNT", 100);
|
|
130
|
+
cursor = Number(nextCursor);
|
|
131
|
+
for (const key of keys) {
|
|
132
|
+
const name = key.slice(prefix.length, -5);
|
|
133
|
+
if (name) names.add(name);
|
|
134
|
+
}
|
|
135
|
+
} while (cursor !== 0);
|
|
136
|
+
|
|
137
|
+
return Array.from(names);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function unavailableHtml() {
|
|
141
|
+
return `
|
|
142
|
+
<!doctype html>
|
|
143
|
+
<html>
|
|
144
|
+
<head>
|
|
145
|
+
<title>Queue Dashboard Unavailable</title>
|
|
146
|
+
<style>
|
|
147
|
+
body { font-family: system-ui, sans-serif; padding: 48px; text-align: center; }
|
|
148
|
+
h1 { color: #dc2626; }
|
|
149
|
+
</style>
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<h1>Queue Dashboard Unavailable</h1>
|
|
153
|
+
<p>Redis is not connected. Check the Redis URL in src/config/redis.ts and make sure Redis is running.</p>
|
|
154
|
+
<p>Redis URL: ${redisConfig.url}</p>
|
|
155
|
+
</body>
|
|
156
|
+
</html>
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Why: Mounts the BullMQ dashboard, or a fallback page when Redis is down.
|
|
162
|
+
* When: Kernel boots after queue jobs are registered.
|
|
163
|
+
* Where: Framework startup.
|
|
164
|
+
* How: Registers the BullBoard Fastify adapter at `BASE_PATH`, wrapped in a
|
|
165
|
+
* scope that applies the email allowlist hook before any dashboard route.
|
|
166
|
+
*/
|
|
167
|
+
export async function setupQueueDashboard() {
|
|
168
|
+
if (!redisClientIfReady()) {
|
|
169
|
+
const route = (app: FastifyInstance) => {
|
|
170
|
+
const handler = async (_request: FastifyRequest, reply: FastifyReply) => reply.code(200).type("text/html").send(unavailableHtml());
|
|
171
|
+
app.get(BASE_PATH, { schema: { hide: true } }, handler);
|
|
172
|
+
app.get(`${BASE_PATH}/*`, { schema: { hide: true } }, handler);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return { basePath: BASE_PATH, enabled: false, route };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await refreshDashboardQueues();
|
|
179
|
+
if (!pollTimer) {
|
|
180
|
+
pollTimer = setInterval(refreshDashboardQueues, POLL_INTERVAL_MS);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const route = async (app: FastifyInstance) => {
|
|
184
|
+
await app.register(
|
|
185
|
+
async (scope) => {
|
|
186
|
+
scope.addHook("onRequest", dashboardAuth());
|
|
187
|
+
await scope.register(serverAdapter.registerPlugin());
|
|
188
|
+
},
|
|
189
|
+
{ prefix: BASE_PATH }
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return { basePath: BASE_PATH, enabled: true, route };
|
|
194
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { queueConfig } from "@/config/index.js";
|
|
2
|
+
import { closeDatabase, initDatabase } from "@/framework/database/connection.js";
|
|
3
|
+
import { pruneStaleQueues, startQueueWorker as startQueueWorkers, stopQueueRuntime } from "@/framework/queue/queue.js";
|
|
4
|
+
import { closeRedis, initRedis, redisError, redisReady } from "@/framework/redis/client.js";
|
|
5
|
+
import { parseCsvOrFallback, registerShutdownSignals, type ShutdownSignal } from "@/framework/support/lifecycle.js";
|
|
6
|
+
import { logger } from "@/framework/support/logger.js";
|
|
7
|
+
|
|
8
|
+
export interface WorkerHandle {
|
|
9
|
+
queues: string[];
|
|
10
|
+
shutdown: (signal: ShutdownSignal) => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Why: Starts the queue worker and wires DB/Redis shutdown.
|
|
15
|
+
* When: CLI `queue:work` command or E2E test bootstrap.
|
|
16
|
+
* Where: Worker entrypoint.
|
|
17
|
+
* How: Parses --queue= args, inits DB/Redis, starts workers, registers signals.
|
|
18
|
+
* When called as the main module, also prints the startup banner.
|
|
19
|
+
*/
|
|
20
|
+
export async function startQueueWorkerRuntime(queues: string[] = []): Promise<WorkerHandle> {
|
|
21
|
+
if (!queues.length) {
|
|
22
|
+
const queuesArg = process.argv.find((arg) => arg.startsWith("--queue="));
|
|
23
|
+
const queueNames = queuesArg?.split("=")[1];
|
|
24
|
+
queues = parseCsvOrFallback(queueNames, queueConfig.queues);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await initDatabase();
|
|
28
|
+
await initRedis();
|
|
29
|
+
if (!redisReady()) {
|
|
30
|
+
logger.error("Queue worker cannot start because Redis is unavailable", {
|
|
31
|
+
error: redisError()
|
|
32
|
+
});
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await startQueueWorkers(queues);
|
|
37
|
+
await pruneStaleQueues(queues);
|
|
38
|
+
console.log(`Queue worker started: ${queues.join(", ")}`);
|
|
39
|
+
|
|
40
|
+
let shuttingDown = false;
|
|
41
|
+
|
|
42
|
+
async function shutdown(signal: ShutdownSignal) {
|
|
43
|
+
if (shuttingDown) return;
|
|
44
|
+
shuttingDown = true;
|
|
45
|
+
|
|
46
|
+
logger.info("Queue worker shutdown signal received", { signal });
|
|
47
|
+
await Promise.allSettled([stopQueueRuntime(), closeDatabase()]);
|
|
48
|
+
await closeRedis();
|
|
49
|
+
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerShutdownSignals(shutdown);
|
|
54
|
+
|
|
55
|
+
return { queues, shutdown };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isMain = process.argv[1]?.endsWith("worker.ts") || process.argv[1]?.endsWith("worker.js");
|
|
59
|
+
if (isMain) {
|
|
60
|
+
await startQueueWorkerRuntime();
|
|
61
|
+
}
|