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,70 @@
|
|
|
1
|
+
import { env } from "@/env.js";
|
|
2
|
+
import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
|
|
3
|
+
import { socketServer } from "@/framework/realtime/socket.js";
|
|
4
|
+
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
5
|
+
import { logger } from "@/framework/support/logger.js";
|
|
6
|
+
|
|
7
|
+
export type BroadcastOptions = {
|
|
8
|
+
users?: Array<string | number>;
|
|
9
|
+
roles?: string[];
|
|
10
|
+
rooms?: string[];
|
|
11
|
+
auth?: boolean;
|
|
12
|
+
all?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Circuit breaker for the pub/sub relay (used by workers that don't have Socket.IO)
|
|
16
|
+
const broadcastBreaker = createCircuitBreaker({
|
|
17
|
+
name: "Realtime Broadcast Relay",
|
|
18
|
+
failureThreshold: 3,
|
|
19
|
+
resetTimeout: 30000
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Why: Emits realtime events to targeted audiences.
|
|
24
|
+
* When: Domain events need websocket fan-out.
|
|
25
|
+
* Where: Event dispatcher and module jobs.
|
|
26
|
+
* How: Emits via Socket.IO if available; otherwise relays through Redis pub/sub
|
|
27
|
+
* so the main process (which has Socket.IO) can broadcast it.
|
|
28
|
+
*/
|
|
29
|
+
function emitViaIo(event: string, payload: any, options: BroadcastOptions) {
|
|
30
|
+
const io = socketServer();
|
|
31
|
+
if (!io) return;
|
|
32
|
+
|
|
33
|
+
if (options.all) io.emit(event, payload);
|
|
34
|
+
if (options.auth) io.to("auth").emit(event, payload);
|
|
35
|
+
|
|
36
|
+
for (const user of options.users || []) io.to(`user:${user}`).emit(event, payload);
|
|
37
|
+
for (const role of options.roles || []) io.to(`role:${role}`).emit(event, payload);
|
|
38
|
+
for (const room of options.rooms || []) io.to(room).emit(event, payload);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function broadcast(event: string, payload: any, options: BroadcastOptions = {}) {
|
|
42
|
+
const io = socketServer();
|
|
43
|
+
|
|
44
|
+
// If this process has Socket.IO (the main API server), emit directly
|
|
45
|
+
if (io) {
|
|
46
|
+
try {
|
|
47
|
+
emitViaIo(event, payload, options);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error("Failed to broadcast event via Socket.IO", { error, event });
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If this is a background worker, relay through Redis
|
|
55
|
+
const redis = redisClientIfReady();
|
|
56
|
+
if (!redis) return;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await broadcastBreaker.exec(async () => {
|
|
60
|
+
const channel = `${env.REDIS_PREFIX}:broadcast`;
|
|
61
|
+
await redis.publish(channel, JSON.stringify({ event, payload, options }));
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (isCircuitBreakerError(error)) {
|
|
65
|
+
logger.warn("Broadcast relay skipped: Circuit breaker is open");
|
|
66
|
+
} else {
|
|
67
|
+
logger.error("Failed to relay broadcast via Redis", { error, event });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { unsign } from "@fastify/cookie";
|
|
2
|
+
import type { Socket } from "socket.io";
|
|
3
|
+
import { cookieConfig } from "@/config/index.js";
|
|
4
|
+
import type { RealtimeAuthContext } from "@/framework/realtime/types.js";
|
|
5
|
+
import { jwt } from "@/framework/support/jwt.js";
|
|
6
|
+
|
|
7
|
+
export function unauthenticatedRealtimeAuth(): RealtimeAuthContext {
|
|
8
|
+
return { isAuthenticated: false, userId: null, roles: [], payload: null };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseCookieHeader(header: string | undefined) {
|
|
12
|
+
if (!header) return {} as Record<string, string>;
|
|
13
|
+
|
|
14
|
+
const cookies: Record<string, string> = {};
|
|
15
|
+
for (const part of header.split(";")) {
|
|
16
|
+
const [rawName, ...rawValue] = part.trim().split("=");
|
|
17
|
+
if (!rawName || rawValue.length === 0) continue;
|
|
18
|
+
cookies[rawName] = decodeURIComponent(rawValue.join("="));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return cookies;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function userIdFromPayload(payload: Record<string, unknown>) {
|
|
25
|
+
const candidate = payload.userId ?? payload.id ?? payload.sub;
|
|
26
|
+
if (candidate === null || candidate === undefined) return null;
|
|
27
|
+
return String(candidate);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rolesFromPayload(payload: Record<string, unknown>) {
|
|
31
|
+
const direct = payload.roles;
|
|
32
|
+
if (Array.isArray(direct)) return direct.map((role) => String(role));
|
|
33
|
+
|
|
34
|
+
const single = payload.role;
|
|
35
|
+
if (single === null || single === undefined) return [];
|
|
36
|
+
return [String(single)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Why: Resolves auth context for websocket connections from cookies.
|
|
41
|
+
* When: Socket.IO handshake middleware executes.
|
|
42
|
+
* Where: Realtime server auth pipeline.
|
|
43
|
+
* How: Parses access cookie, verifies its signature with @fastify/cookie, then
|
|
44
|
+
* verifies the JWT and normalizes user/role claims.
|
|
45
|
+
*/
|
|
46
|
+
export async function authFromSocketHandshake(socket: Socket): Promise<RealtimeAuthContext> {
|
|
47
|
+
const cookies = parseCookieHeader(socket.handshake.headers.cookie);
|
|
48
|
+
const rawToken = cookies[`${cookieConfig.name}_access`];
|
|
49
|
+
|
|
50
|
+
if (!rawToken) {
|
|
51
|
+
return unauthenticatedRealtimeAuth();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const unsigned = unsign(rawToken, cookieConfig.secret);
|
|
55
|
+
if (!unsigned.valid) {
|
|
56
|
+
return unauthenticatedRealtimeAuth();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const payload = await jwt.verifyToken(unsigned.value, "access");
|
|
60
|
+
if (!payload) {
|
|
61
|
+
return unauthenticatedRealtimeAuth();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const normalizedPayload = payload as Record<string, unknown>;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
isAuthenticated: true,
|
|
68
|
+
userId: userIdFromPayload(normalizedPayload),
|
|
69
|
+
roles: rolesFromPayload(normalizedPayload),
|
|
70
|
+
payload: normalizedPayload
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createAdapter } from "@socket.io/redis-adapter";
|
|
2
|
+
import { Server as SocketIOServer } from "socket.io";
|
|
3
|
+
import { appConfig, realtimeConfig, redisConfig } from "@/config/index.js";
|
|
4
|
+
import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
|
|
5
|
+
import { authFromSocketHandshake, unauthenticatedRealtimeAuth } from "@/framework/realtime/socket-cookie.js";
|
|
6
|
+
import type { RealtimeAuthContext } from "@/framework/realtime/types.js";
|
|
7
|
+
import { redisClientIfReady } from "@/framework/redis/client.js";
|
|
8
|
+
import { logger } from "@/framework/support/logger.js";
|
|
9
|
+
|
|
10
|
+
export const WS_PATH = "/socket.io";
|
|
11
|
+
|
|
12
|
+
// Create a breaker for the Socket.IO Redis adapter
|
|
13
|
+
const socketAdapterBreaker = createCircuitBreaker({
|
|
14
|
+
name: "Socket.IO Redis Adapter",
|
|
15
|
+
failureThreshold: 3,
|
|
16
|
+
resetTimeout: 30000
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export type RealtimeServeWebSocket = Record<string, never>;
|
|
20
|
+
|
|
21
|
+
export type RealtimeInit = {
|
|
22
|
+
io: SocketIOServer;
|
|
23
|
+
websocketServe: RealtimeServeWebSocket;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let io: SocketIOServer | null = null;
|
|
27
|
+
let adapterSubClient: ReturnType<typeof redisClientIfReady> | null = null;
|
|
28
|
+
|
|
29
|
+
function socketAllowedOrigins() {
|
|
30
|
+
const origins = [appConfig.url, appConfig.frontendUrl].filter(Boolean) as string[];
|
|
31
|
+
if (realtimeConfig.enabled) origins.push("https://admin.socket.io");
|
|
32
|
+
return [...new Set(origins)];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function attachRedisAdapter() {
|
|
36
|
+
if (!redisConfig.enabled) return;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await socketAdapterBreaker.exec(async () => {
|
|
40
|
+
const pubClient = redisClientIfReady();
|
|
41
|
+
if (!pubClient) throw new Error("Redis client not ready");
|
|
42
|
+
|
|
43
|
+
// Socket.IO requires a duplicate connection for subscriptions
|
|
44
|
+
adapterSubClient = pubClient.duplicate();
|
|
45
|
+
await adapterSubClient.connect();
|
|
46
|
+
|
|
47
|
+
io?.adapter(createAdapter(pubClient, adapterSubClient));
|
|
48
|
+
logger.debug("Socket.IO Redis adapter attached successfully");
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (isCircuitBreakerError(error)) {
|
|
52
|
+
logger.warn("Socket.IO Redis adapter skipped due to circuit breaker");
|
|
53
|
+
} else {
|
|
54
|
+
logger.error("Failed to attach Socket.IO Redis adapter", { error });
|
|
55
|
+
}
|
|
56
|
+
// Fallback: Socket.IO will automatically use its default in-memory adapter.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function registerAuthAndRooms() {
|
|
61
|
+
io?.use(async (socket, next) => {
|
|
62
|
+
const auth = await authFromSocketHandshake(socket);
|
|
63
|
+
socket.data.auth = auth as RealtimeAuthContext;
|
|
64
|
+
|
|
65
|
+
if (auth.isAuthenticated) {
|
|
66
|
+
socket.join("auth");
|
|
67
|
+
if (auth.userId) socket.join(`user:${auth.userId}`);
|
|
68
|
+
for (const role of auth.roles) socket.join(`role:${role}`);
|
|
69
|
+
} else {
|
|
70
|
+
socket.join("guest");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
next();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
io?.on("connection", (socket) => {
|
|
77
|
+
const auth = (socket.data.auth || unauthenticatedRealtimeAuth()) as RealtimeAuthContext;
|
|
78
|
+
|
|
79
|
+
logger.debug("Socket connected", {
|
|
80
|
+
socketId: socket.id,
|
|
81
|
+
authenticated: auth.isAuthenticated,
|
|
82
|
+
userId: auth.userId
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
socket.on("join", (room) => socket.join(String(room)));
|
|
86
|
+
socket.on("disconnect", (reason) => {
|
|
87
|
+
logger.info("Socket disconnected", {
|
|
88
|
+
socketId: socket.id,
|
|
89
|
+
reason,
|
|
90
|
+
authenticated: auth.isAuthenticated,
|
|
91
|
+
userId: auth.userId
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Why: Boots the singleton Socket.IO server for Fastify.
|
|
99
|
+
* When: Fastify HTTP server starts and realtime is enabled.
|
|
100
|
+
* Where: Framework server lifecycle.
|
|
101
|
+
* How: Attaches Socket.IO directly to Fastify's underlying Node httpServer.
|
|
102
|
+
*/
|
|
103
|
+
export async function initRealtime(target: { httpServer: any }): Promise<RealtimeInit | null> {
|
|
104
|
+
if (!realtimeConfig.enabled) {
|
|
105
|
+
logger.debug("Socket.IO disabled (disabled in config)");
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (io) return { io, websocketServe: {} };
|
|
110
|
+
|
|
111
|
+
// Fastify exposes the native Node.js httpServer as `app.server`
|
|
112
|
+
io = new SocketIOServer(target.httpServer, {
|
|
113
|
+
path: WS_PATH, // Keep the same path as the Hono and Express engines
|
|
114
|
+
cors: { origin: socketAllowedOrigins(), credentials: true }
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await attachRedisAdapter();
|
|
118
|
+
registerAuthAndRooms();
|
|
119
|
+
|
|
120
|
+
logger.debug(`Socket.IO enabled at ${WS_PATH}`);
|
|
121
|
+
return { io, websocketServe: {} };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function socketServer() {
|
|
125
|
+
return io;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function ioServer() {
|
|
129
|
+
return io;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function closeRealtime() {
|
|
133
|
+
if (!io) return;
|
|
134
|
+
|
|
135
|
+
await new Promise<void>((resolve) => {
|
|
136
|
+
io?.close(() => resolve());
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (adapterSubClient) {
|
|
140
|
+
try {
|
|
141
|
+
await adapterSubClient.quit();
|
|
142
|
+
} catch {
|
|
143
|
+
adapterSubClient.disconnect();
|
|
144
|
+
}
|
|
145
|
+
adapterSubClient = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
io = null;
|
|
149
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { instrument } from "@socket.io/admin-ui";
|
|
2
|
+
import type { Server as SocketIOServer } from "socket.io";
|
|
3
|
+
import { appConfig, realtimeConfig } from "@/config/index.js";
|
|
4
|
+
|
|
5
|
+
export function setupSocketAdminUI(io: SocketIOServer | null) {
|
|
6
|
+
if (!io || !realtimeConfig.enabled || appConfig.environment === "production") {
|
|
7
|
+
return { enabled: false };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
instrument(io, {
|
|
11
|
+
auth: false,
|
|
12
|
+
mode: "development"
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return { enabled: true };
|
|
16
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
import { redisConfig } from "@/config/index.js";
|
|
3
|
+
import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
|
|
4
|
+
import { logger } from "@/framework/support/logger.js";
|
|
5
|
+
|
|
6
|
+
export type redis = Redis;
|
|
7
|
+
|
|
8
|
+
let client: redis | null = null;
|
|
9
|
+
let ready = false;
|
|
10
|
+
let lastError: string | null = null;
|
|
11
|
+
let redisBreaker: any | null = null;
|
|
12
|
+
|
|
13
|
+
if (redisConfig.enabled) {
|
|
14
|
+
redisBreaker = createCircuitBreaker({ name: "Redis", failureThreshold: 3, resetTimeout: 30000 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeRedisUrl() {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(redisConfig.url);
|
|
20
|
+
if (url.password) url.password = "***";
|
|
21
|
+
return url.toString();
|
|
22
|
+
} catch {
|
|
23
|
+
return redisConfig.url;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Why: Initializes shared Redis connection for cache/queue/events/session.
|
|
29
|
+
* When: Runtime bootstrap before Redis-backed features are used.
|
|
30
|
+
* Where: Server, worker, scheduler startup paths.
|
|
31
|
+
* How: Connects once, tracks readiness, and degrades gracefully if unavailable.
|
|
32
|
+
*/
|
|
33
|
+
export async function initRedis() {
|
|
34
|
+
if (client) return client;
|
|
35
|
+
|
|
36
|
+
if (!redisConfig.enabled) {
|
|
37
|
+
ready = false;
|
|
38
|
+
lastError = "redis disabled in config";
|
|
39
|
+
logger.debug("Redis disabled; cache, session storage, queue, events, and cockpit unavailable");
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return await redisBreaker.exec(async () => {
|
|
45
|
+
client = new Redis(redisConfig.url, {
|
|
46
|
+
lazyConnect: true,
|
|
47
|
+
enableReadyCheck: true,
|
|
48
|
+
maxRetriesPerRequest: 0,
|
|
49
|
+
retryStrategy: () => null
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
client.on("error", (error) => {
|
|
53
|
+
lastError = error.message;
|
|
54
|
+
if (ready) logger.warn("Redis connection error", { redisUrl: safeRedisUrl(), error: error.message });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await client.connect();
|
|
58
|
+
await client.ping();
|
|
59
|
+
|
|
60
|
+
ready = true;
|
|
61
|
+
logger.debug("Redis connected successfully");
|
|
62
|
+
return client;
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Use the functional guard
|
|
66
|
+
if (isCircuitBreakerError(error)) {
|
|
67
|
+
lastError = error.message;
|
|
68
|
+
logger.warn("Redis initialization blocked by circuit breaker");
|
|
69
|
+
} else {
|
|
70
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
71
|
+
logger.warn(`Redis unavailable; continuing without Redis features (${lastError})`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
client?.disconnect();
|
|
75
|
+
client = null;
|
|
76
|
+
ready = false;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Why: Returns the current Redis client instance.
|
|
83
|
+
* When: Internal helpers need direct Redis operations.
|
|
84
|
+
* Where: Cache/session/queue/events modules.
|
|
85
|
+
* How: Returns nullable singleton without creating a new connection.
|
|
86
|
+
*/
|
|
87
|
+
export function redis() {
|
|
88
|
+
return client;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Why: Indicates whether Redis is connected and usable.
|
|
93
|
+
* When: Features need to branch on Redis availability.
|
|
94
|
+
* Where: Cache/session/queue/event internals.
|
|
95
|
+
* How: Combines readiness flag with client presence.
|
|
96
|
+
*/
|
|
97
|
+
export function redisReady() {
|
|
98
|
+
return ready && client !== null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Why: Provides a single ready-checked Redis client accessor.
|
|
103
|
+
* When: Redis-backed modules need quick availability guard.
|
|
104
|
+
* Where: Cache/session/queue/events/BullBoard internals.
|
|
105
|
+
* How: Returns connected client or null when Redis is unavailable.
|
|
106
|
+
*/
|
|
107
|
+
export function redisClientIfReady() {
|
|
108
|
+
return redisReady() ? client : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Why: Exposes last Redis error for diagnostics.
|
|
113
|
+
* When: Startup or health logs need failure detail.
|
|
114
|
+
* Where: Runtime status output.
|
|
115
|
+
* How: Returns cached error message string.
|
|
116
|
+
*/
|
|
117
|
+
export function redisError() {
|
|
118
|
+
return lastError;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Why: Returns the Redis circuit breaker instance or null when disabled.
|
|
123
|
+
* When: Health checks and diagnostics need breaker state.
|
|
124
|
+
* Where: Runtime status and monitoring paths.
|
|
125
|
+
* How: Returns the breaker if Redis is enabled, null otherwise.
|
|
126
|
+
*/
|
|
127
|
+
export function getRedisBreaker() {
|
|
128
|
+
return redisBreaker;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Why: Closes Redis connection during graceful shutdown.
|
|
133
|
+
* When: Process stop hooks for server/worker/scheduler.
|
|
134
|
+
* Where: Runtime lifecycle files.
|
|
135
|
+
* How: Attempts quit, falls back to disconnect, then resets state.
|
|
136
|
+
*/
|
|
137
|
+
export async function closeRedis() {
|
|
138
|
+
if (!client) return;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await client.quit();
|
|
142
|
+
} catch {
|
|
143
|
+
client.disconnect();
|
|
144
|
+
} finally {
|
|
145
|
+
client = null;
|
|
146
|
+
ready = false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type Runtime = "node" | "bun" | "unknown";
|
|
2
|
+
|
|
3
|
+
const runtimeGlobals = globalThis as Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Why: Select the correct driver/flavor when a runtime has specialized packages.
|
|
7
|
+
* When: Loading DB clients, web servers, and queues that vary per runtime.
|
|
8
|
+
* Where: Framework bootstrap and driver resolution layers.
|
|
9
|
+
* How: Inspects global runtime markers (Bun) and falls back to Node/unknown.
|
|
10
|
+
*/
|
|
11
|
+
export function runtime(): Runtime {
|
|
12
|
+
if (runtimeGlobals.Bun !== undefined) return "bun";
|
|
13
|
+
if (typeof process !== "undefined" && process.versions?.node) return "node";
|
|
14
|
+
return "unknown";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isBun(): boolean {
|
|
18
|
+
return runtime() === "bun";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isNode(): boolean {
|
|
22
|
+
return runtime() === "node";
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AddressInfo } from "node:net";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
import { closeRealtime, initRealtime } from "@/framework/realtime/socket.js";
|
|
4
|
+
|
|
5
|
+
export type HttpServerHandle = {
|
|
6
|
+
address(): string | AddressInfo | null;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
native: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function startNode(app: FastifyInstance, port: number): Promise<HttpServerHandle> {
|
|
12
|
+
// 1. Attach Socket.IO BEFORE starting the server
|
|
13
|
+
await initRealtime({ httpServer: app.server });
|
|
14
|
+
|
|
15
|
+
// 2. Start listening. Fastify defaults to localhost; bind all interfaces to
|
|
16
|
+
// match Express's `server.listen(port)` behaviour.
|
|
17
|
+
await app.listen({ port, host: "0.0.0.0" });
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
native: app.server,
|
|
21
|
+
address: () => app.server.address(),
|
|
22
|
+
close: async () => {
|
|
23
|
+
// 3. Close Socket.IO safely, then the HTTP server
|
|
24
|
+
await closeRealtime();
|
|
25
|
+
await app.close();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function startHttpServer(app: FastifyInstance, port: number): Promise<HttpServerHandle> {
|
|
31
|
+
// Fastify runs natively on Node.js; its `.server` is the underlying http.Server.
|
|
32
|
+
return startNode(app, port);
|
|
33
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { redisConfig } from "@/config/index.js";
|
|
3
|
+
import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
|
|
4
|
+
import { databaseDialect, databasePool } from "@/framework/database/connection.js";
|
|
5
|
+
import { redis, redisReady } from "@/framework/redis/client.js";
|
|
6
|
+
import { logger } from "@/framework/support/logger.js";
|
|
7
|
+
|
|
8
|
+
type LockOptions = { ttlMs?: number; tableName?: string };
|
|
9
|
+
|
|
10
|
+
// Circuit breaker for the Redis Lock
|
|
11
|
+
const lockBreaker = createCircuitBreaker({ name: "Redis Lock", failureThreshold: 3, resetTimeout: 30000 });
|
|
12
|
+
|
|
13
|
+
function table(name?: string) {
|
|
14
|
+
return name || "scheduler_locks";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function ensureTable(tableName?: string) {
|
|
18
|
+
const pool = databasePool();
|
|
19
|
+
const dialect = databaseDialect();
|
|
20
|
+
const lockTable = table(tableName);
|
|
21
|
+
|
|
22
|
+
if (dialect === "mysql") {
|
|
23
|
+
await pool.query(
|
|
24
|
+
`CREATE TABLE IF NOT EXISTS \`${lockTable}\` (name VARCHAR(191) PRIMARY KEY, owner VARCHAR(191) NOT NULL, expires_at BIGINT NOT NULL)`
|
|
25
|
+
);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (dialect === "postgresql") {
|
|
30
|
+
await pool.unsafe(
|
|
31
|
+
`CREATE TABLE IF NOT EXISTS "${lockTable}" (name VARCHAR(191) PRIMARY KEY, owner VARCHAR(191) NOT NULL, expires_at BIGINT NOT NULL)`
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await pool.execute(`CREATE TABLE IF NOT EXISTS "${lockTable}" (name TEXT PRIMARY KEY, owner TEXT NOT NULL, expires_at INTEGER NOT NULL)`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function acquireDbLock(name: string, owner: string, ttlMs: number, tableName?: string) {
|
|
40
|
+
await ensureTable(tableName);
|
|
41
|
+
const pool = databasePool();
|
|
42
|
+
const dialect = databaseDialect();
|
|
43
|
+
const lockTable = table(tableName);
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const expiresAt = now + ttlMs;
|
|
46
|
+
|
|
47
|
+
if (dialect === "postgresql") {
|
|
48
|
+
const rows = await pool.unsafe(
|
|
49
|
+
`INSERT INTO "${lockTable}" (name, owner, expires_at) VALUES ($1, $2, $3) ON CONFLICT (name) DO UPDATE SET owner = EXCLUDED.owner, expires_at = EXCLUDED.expires_at WHERE "${lockTable}".expires_at < $4 RETURNING owner`,
|
|
50
|
+
[name, owner, expiresAt, now]
|
|
51
|
+
);
|
|
52
|
+
return rows?.[0]?.owner === owner;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (dialect === "mysql") {
|
|
56
|
+
await pool.query(
|
|
57
|
+
`INSERT INTO \`${lockTable}\` (name, owner, expires_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE owner = IF(expires_at < ?, VALUES(owner), owner), expires_at = IF(expires_at < ?, VALUES(expires_at), expires_at)`,
|
|
58
|
+
[name, owner, expiresAt, now, now]
|
|
59
|
+
);
|
|
60
|
+
const [rows] = await pool.query(`SELECT owner FROM \`${lockTable}\` WHERE name = ?`, [name]);
|
|
61
|
+
return rows?.[0]?.owner === owner;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await pool.execute({
|
|
65
|
+
sql: `INSERT INTO "${lockTable}" (name, owner, expires_at) VALUES (?, ?, ?) ON CONFLICT(name) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at WHERE "${lockTable}".expires_at < ?`,
|
|
66
|
+
args: [name, owner, expiresAt, now]
|
|
67
|
+
});
|
|
68
|
+
const result = await pool.execute({
|
|
69
|
+
sql: `SELECT owner FROM "${lockTable}" WHERE name = ?`,
|
|
70
|
+
args: [name]
|
|
71
|
+
});
|
|
72
|
+
return result.rows?.[0]?.owner === owner;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function releaseDbLock(name: string, owner: string, tableName?: string) {
|
|
76
|
+
const pool = databasePool();
|
|
77
|
+
const dialect = databaseDialect();
|
|
78
|
+
const lockTable = table(tableName);
|
|
79
|
+
|
|
80
|
+
if (dialect === "postgresql") return await pool.unsafe(`DELETE FROM "${lockTable}" WHERE name = $1 AND owner = $2`, [name, owner]);
|
|
81
|
+
if (dialect === "mysql") return await pool.query(`DELETE FROM \`${lockTable}\` WHERE name = ? AND owner = ?`, [name, owner]);
|
|
82
|
+
await pool.execute({
|
|
83
|
+
sql: `DELETE FROM "${lockTable}" WHERE name = ? AND owner = ?`,
|
|
84
|
+
args: [name, owner]
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function runWithLock<T>(name: string, handler: () => Promise<T>, options: LockOptions = {}) {
|
|
89
|
+
const owner = randomUUID();
|
|
90
|
+
const ttlMs = options.ttlMs || 60_000;
|
|
91
|
+
const client = redis();
|
|
92
|
+
|
|
93
|
+
if (redisReady() && client) {
|
|
94
|
+
try {
|
|
95
|
+
return await lockBreaker.exec(async () => {
|
|
96
|
+
const key = `${redisConfig.prefix}:lock:${name}`;
|
|
97
|
+
const ok = await client.set(key, owner, "PX", ttlMs, "NX");
|
|
98
|
+
if (!ok) return { ran: false, backend: "redis" as const };
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const result = await handler();
|
|
102
|
+
return { ran: true, backend: "redis" as const, result };
|
|
103
|
+
} finally {
|
|
104
|
+
await client.del(key);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// If the circuit breaker is open, we log it and fall back to DB lock
|
|
109
|
+
if (isCircuitBreakerError(error)) {
|
|
110
|
+
logger.warn(`Redis lock skipped due to circuit breaker for ${name}; falling back to DB`);
|
|
111
|
+
} else {
|
|
112
|
+
logger.error(`Redis lock error for ${name}; falling back to DB`, { error });
|
|
113
|
+
}
|
|
114
|
+
// Fall through to DB lock
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// DB Lock Fallback
|
|
119
|
+
const ok = await acquireDbLock(name, owner, ttlMs, options.tableName);
|
|
120
|
+
if (!ok) return { ran: false, backend: "db" as const };
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await handler();
|
|
124
|
+
return { ran: true, backend: "db" as const, result };
|
|
125
|
+
} finally {
|
|
126
|
+
await releaseDbLock(name, owner, options.tableName);
|
|
127
|
+
}
|
|
128
|
+
}
|