@forinda/kickjs 9.0.0 → 9.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{application-BnFA6J97.mjs → application-BSDt9vOo.mjs} +3 -3
- package/dist/{application-BnFA6J97.mjs.map → application-BSDt9vOo.mjs.map} +1 -1
- package/dist/{bootstrap-K1PcxSTg.mjs → bootstrap-CJ8ALvif.mjs} +3 -3
- package/dist/{bootstrap-K1PcxSTg.mjs.map → bootstrap-CJ8ALvif.mjs.map} +1 -1
- package/dist/{client-ip-DAnYuS54.mjs → client-ip-BHH-wDR7.mjs} +2 -2
- package/dist/{client-ip-DAnYuS54.mjs.map → client-ip-BHH-wDR7.mjs.map} +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-16yw-Mxk.mjs → config-Dz5GgLKS.mjs} +3 -3
- package/dist/{config-16yw-Mxk.mjs.map → config-Dz5GgLKS.mjs.map} +1 -1
- package/dist/{container-Dw45sa0Y.mjs → container-DM3gPzGt.mjs} +3 -3
- package/dist/{container-Dw45sa0Y.mjs.map → container-DM3gPzGt.mjs.map} +1 -1
- package/dist/{context-DlIdXA6I.mjs → context-DEntnLmq.mjs} +3 -3
- package/dist/{context-DlIdXA6I.mjs.map → context-DEntnLmq.mjs.map} +1 -1
- package/dist/{context-errors-BqOFD7UE.mjs → context-errors-Cb4jKwm4.mjs} +2 -2
- package/dist/{context-errors-BqOFD7UE.mjs.map → context-errors-Cb4jKwm4.mjs.map} +1 -1
- package/dist/{contributor-runner-DLvBA96K.mjs → contributor-runner-Buxk3Ay_.mjs} +3 -3
- package/dist/{contributor-runner-DLvBA96K.mjs.map → contributor-runner-Buxk3Ay_.mjs.map} +1 -1
- package/dist/core/adapter.mjs +1 -1
- package/dist/core/app-module.mjs +1 -1
- package/dist/core/cache.mjs +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/errors.mjs +1 -1
- package/dist/core/interfaces.mjs +1 -1
- package/dist/core/logger.mjs +1 -1
- package/dist/core/path.mjs +1 -1
- package/dist/core/plugin.mjs +1 -1
- package/dist/core/reactivity.mjs +1 -1
- package/dist/{core-CCwlN0Gn.mjs → core-Cv1KjPCj.mjs} +3 -3
- package/dist/{core-CCwlN0Gn.mjs.map → core-Cv1KjPCj.mjs.map} +1 -1
- package/dist/{csrf-B8CGeOlP.mjs → csrf-BgfnRJ_y.mjs} +3 -3
- package/dist/{csrf-B8CGeOlP.mjs.map → csrf-BgfnRJ_y.mjs.map} +1 -1
- package/dist/{decorate-CahyjuWW.mjs → decorate-CnpOo_Rp.mjs} +1 -1
- package/dist/{define-adapter-Ckc2P-wH.mjs → define-adapter-7WlOyHBR.mjs} +2 -2
- package/dist/{define-adapter-Ckc2P-wH.mjs.map → define-adapter-7WlOyHBR.mjs.map} +1 -1
- package/dist/{disposables-DnvT8DXr.mjs → disposables-C2uznWks.mjs} +2 -2
- package/dist/{disposables-DnvT8DXr.mjs.map → disposables-C2uznWks.mjs.map} +1 -1
- package/dist/{env-DMAdjj8Z.mjs → env-Cj-U1sin.mjs} +3 -3
- package/dist/{env-DMAdjj8Z.mjs.map → env-Cj-U1sin.mjs.map} +1 -1
- package/dist/{handler-2CbWK63p.mjs → handler-CnjyOHSw.mjs} +3 -3
- package/dist/{handler-2CbWK63p.mjs.map → handler-CnjyOHSw.mjs.map} +1 -1
- package/dist/http/application.mjs +2 -2
- package/dist/http/bootstrap.mjs +2 -2
- package/dist/http/context.mjs +2 -2
- package/dist/http/middleware/cors.mjs +1 -1
- package/dist/http/middleware/csrf.mjs +2 -2
- package/dist/http/middleware/error-handler.mjs +2 -2
- package/dist/http/middleware/helmet.mjs +1 -1
- package/dist/http/middleware/rate-limit.mjs +2 -2
- package/dist/http/middleware/request-id.mjs +1 -1
- package/dist/http/middleware/request-logger.mjs +2 -2
- package/dist/http/middleware/session.mjs +2 -2
- package/dist/http/middleware/spa.mjs +2 -2
- package/dist/http/middleware/upload.mjs +2 -2
- package/dist/http/middleware/validate.mjs +1 -1
- package/dist/http/middleware/views.mjs +2 -2
- package/dist/http/query/index.mjs +2 -2
- package/dist/http/router-builder.mjs +2 -2
- package/dist/http/runtimes/fastify.mjs +2 -2
- package/dist/http/runtimes/h3-web.mjs +2 -2
- package/dist/http/runtimes/h3.mjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{jobs-C8_ESbx0.mjs → jobs-DIEQILZ8.mjs} +3 -3
- package/dist/{jobs-C8_ESbx0.mjs.map → jobs-DIEQILZ8.mjs.map} +1 -1
- package/dist/{kv-stores-BhUmirED.mjs → kv-stores-D4dmpn6D.mjs} +3 -3
- package/dist/{kv-stores-BhUmirED.mjs.map → kv-stores-D4dmpn6D.mjs.map} +1 -1
- package/dist/{metadata-BQG1MmqL.mjs → metadata-DrCyWXgz.mjs} +2 -2
- package/dist/{metadata-BQG1MmqL.mjs.map → metadata-DrCyWXgz.mjs.map} +1 -1
- package/dist/{module-registry-BXnvRtH9.mjs → module-registry-Bv3NMDX5.mjs} +2 -2
- package/dist/{module-registry-BXnvRtH9.mjs.map → module-registry-Bv3NMDX5.mjs.map} +1 -1
- package/dist/{observers-BI8H_g45.mjs → observers-DzVW_zb7.mjs} +2 -2
- package/dist/{observers-BI8H_g45.mjs.map → observers-DzVW_zb7.mjs.map} +1 -1
- package/dist/{query-CRTeiKUv.mjs → query-BGQdAxkX.mjs} +2 -2
- package/dist/{query-CRTeiKUv.mjs.map → query-BGQdAxkX.mjs.map} +1 -1
- package/dist/{respond-BAxhPjRj.mjs → respond--WlinyCt.mjs} +2 -2
- package/dist/{respond-BAxhPjRj.mjs.map → respond--WlinyCt.mjs.map} +1 -1
- package/dist/{route-flag-GipNvQ4O.mjs → route-flag-DEUYr5Br.mjs} +3 -3
- package/dist/{route-flag-GipNvQ4O.mjs.map → route-flag-DEUYr5Br.mjs.map} +1 -1
- package/dist/{route-policy-DtGR4sar.mjs → route-policy-BmiX7NiW.mjs} +2 -2
- package/dist/{route-policy-DtGR4sar.mjs.map → route-policy-BmiX7NiW.mjs.map} +1 -1
- package/dist/{router-builder-C2nnS0VJ.mjs → router-builder-Cig7o4BH.mjs} +3 -3
- package/dist/{router-builder-C2nnS0VJ.mjs.map → router-builder-Cig7o4BH.mjs.map} +1 -1
- package/dist/{runtime-DGzigyO2.mjs → runtime-C3izjAep.mjs} +3 -3
- package/dist/{runtime-DGzigyO2.mjs.map → runtime-C3izjAep.mjs.map} +1 -1
- package/dist/{upload-config-CcoZp7xA.mjs → upload-config-BhaBjCiB.mjs} +2 -2
- package/dist/{upload-config-CcoZp7xA.mjs.map → upload-config-BhaBjCiB.mjs.map} +1 -1
- package/dist/web.mjs +2 -2
- package/package.json +7 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v9.0.
|
|
2
|
+
* @forinda/kickjs v9.0.2
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{o as reloadEnv}from"./env-
|
|
12
|
-
//# sourceMappingURL=bootstrap-
|
|
11
|
+
import{o as reloadEnv}from"./env-Cj-U1sin.mjs";import{t as Container}from"./container-DM3gPzGt.mjs";import{createLogger}from"./core/logger.mjs";import{t as Application}from"./application-BSDt9vOo.mjs";import{r as reportError}from"./observers-DzVW_zb7.mjs";import{listCronJobs,runCronJob}from"./core/cron.mjs";import{reactive,ref}from"./core/reactivity.mjs";import"./core-Cv1KjPCj.mjs";import{t as defineAdapter}from"./define-adapter-7WlOyHBR.mjs";import{createRequire}from"node:module";import cluster from"node:cluster";import os from"node:os";const log$2=createLogger(`KickCronAdapter`),peerRequire=createRequire(import.meta.url),CRON_WORKER_ENV=`KICK_CRON_WORKER`;function loadCroner(){try{return peerRequire(`croner`).Cron}catch{return}}function isCronProcess(){return!cluster.isWorker||process.env.KICK_CRON_WORKER===`1`}const KickCronAdapter=defineAdapter({name:`KickCronAdapter`,defaults:{enabled:!0},build(options){let schedules=[],inFlight=new Set,jobs=[];return{afterStart({container}){if(options.enabled===!1||!isCronProcess()||(jobs=listCronJobs(container),jobs.length===0))return;let Croner=loadCroner();if(!Croner){log$2.warn(`${jobs.length} @Cron job(s) found but "croner" is not installed, so none will run. Install it: pnpm add croner`);return}let run=(job,trigger)=>{let p=runCronJob(job,container,trigger).catch(()=>{});inFlight.add(p),p.finally(()=>inFlight.delete(p))};for(let job of jobs)schedules.push(new Croner(job.expression,job.timezone?{timezone:job.timezone}:{},()=>run(job,`schedule`))),job.runOnInit&&run(job,`init`);log$2.info(`Scheduled ${jobs.length} @Cron job(s): ${jobs.map(j=>j.name).join(`, `)}`)},async shutdown(){for(let schedule of schedules.splice(0))schedule.stop();await Promise.all(inFlight)},introspect(){return{protocolVersion:1,name:`KickCronAdapter`,kind:`adapter`,state:{jobs:jobs.map(j=>({name:j.name,expression:j.expression,timezone:j.timezone,description:j.description}))}}}}}}),log$1=createLogger(`Cluster`);function isClusterPrimary(){return cluster.isPrimary}function setupClusterPrimary(opts){let numWorkers=opts.workers??os.cpus().length,gracefulTimeout=opts.gracefulShutdownTimeout??5e3,restartDelay=opts.restartDelay??1e3,maxFailures=opts.maxFailures??5,restartWindow=opts.restartWindow??3e4,startupTimeouts=new Map,state=reactive({failureTimestamps:[],shuttingDown:!1}),activeWorkers=ref(0);log$1.info(`Primary ${process.pid} starting ${numWorkers} worker(s)`);let cronWorkerId,forkWorker=()=>{let takesCron=cronWorkerId===void 0,worker=cluster.fork(takesCron?{[CRON_WORKER_ENV]:`1`}:void 0);takesCron&&(cronWorkerId=worker.id)};for(let i=0;i<numWorkers;i++)forkWorker();cluster.on(`fork`,worker=>{let timeout=setTimeout(()=>{log$1.error(`Worker ${worker.process.pid} did not respond within 10s, killing...`),worker.kill()},1e4);startupTimeouts.set(worker.id,timeout),activeWorkers.value++,log$1.debug(`Worker ${worker.process.pid} forked (id=${worker.id})`)}),cluster.on(`listening`,(worker,address)=>{let timeout=startupTimeouts.get(worker.id);timeout&&(clearTimeout(timeout),startupTimeouts.delete(worker.id)),log$1.info(`Worker ${worker.process.pid} listening on ${address.address}:${address.port}`)}),cluster.on(`online`,worker=>{log$1.debug(`Worker ${worker.process.pid} online (id=${worker.id})`)}),cluster.on(`exit`,(worker,code,signal)=>{let timeout=startupTimeouts.get(worker.id);if(timeout&&(clearTimeout(timeout),startupTimeouts.delete(worker.id)),activeWorkers.value--,worker.id===cronWorkerId&&(cronWorkerId=void 0),worker.exitedAfterDisconnect){log$1.info(`Worker ${worker.process.pid} exited gracefully (id=${worker.id})`);return}let now=Date.now();for(;state.failureTimestamps.length>0&&now-state.failureTimestamps[0]>=restartWindow;)state.failureTimestamps.shift();if(state.failureTimestamps.push(now),state.failureTimestamps.length>maxFailures){log$1.error(`${state.failureTimestamps.length} worker failures in ${restartWindow}ms, giving up`),activeWorkers.value===0&&(log$1.error(`All workers have crashed, exiting primary`),process.exit(1));return}log$1.warn(`Worker ${worker.process.pid} died (code=${code}, signal=${signal}), restarting in ${restartDelay}ms... (failures: ${state.failureTimestamps.length}/${maxFailures})`),setTimeout(()=>{activeWorkers.value<numWorkers&&forkWorker()},restartDelay)}),cluster.on(`disconnect`,worker=>{log$1.debug(`Worker ${worker.process.pid} disconnected (id=${worker.id})`)});for(let signal of[`SIGTERM`,`SIGINT`])process.on(signal,()=>{state.shuttingDown||(state.shuttingDown=!0,log$1.info(`Primary received ${signal}, disconnecting workers...`),cluster.disconnect(()=>{log$1.info(`All workers disconnected, exiting primary`),process.exit(0)}),setTimeout(()=>{log$1.warn(`Graceful shutdown timed out, forcing exit`),process.exit(1)},gracefulTimeout).unref())})}function tryReloadEnv(){try{reloadEnv()}catch{}}const log=createLogger(`Process`);async function bootstrap(options){let isViteDev=!!globalThis.__kickjs_httpServer;if(options.cluster&&isViteDev&&log.warn(`Cluster mode is ignored in dev — Vite owns the server. Cluster applies in production (kick start).`),options.cluster&&cluster.isPrimary&&!isViteDev)return setupClusterPrimary(typeof options.cluster==`object`?options.cluster:{}),new Application(options);let g=globalThis,processHooks=options.processHooks??`auto`;if(!g.__kickBootstrapped){if(processHooks!==`manual`&&(process.on(`uncaughtException`,err=>{log.error(err,`Uncaught exception`),reportError(err,{source:`uncaught`}),process.exitCode=1}),process.on(`unhandledRejection`,reason=>{log.error(reason,`Unhandled rejection`),reportError(reason,{source:`unhandled-rejection`}),process.exitCode=1})),processHooks===`auto`&&!g.__kickjs_httpServer)for(let signal of[`SIGINT`,`SIGTERM`])process.on(signal,async()=>{log.info(`Received ${signal}, shutting down...`),g.__app&&await g.__app.shutdown(),process.exit(0)});g.__kickBootstrapped=!0}if(g.__app){log.debug(`HMR rebuild triggered`);try{await g.__app.shutdown({closeServer:!1})}catch(err){log.error(err,`HMR teardown of the previous app failed`)}tryReloadEnv(),Container.reset();let freshApp=new Application(options);return g.__app=freshApp,g.__kickjs_container=freshApp.getContainer(),await freshApp.start(),log.debug(`HMR rebuild complete`),freshApp}let app=new Application(options);return g.__app=app,g.__kickjs_container=app.getContainer(),process.env.KICK_TINKER?(await app.registerOnly(),app):(await app.start(),app)}export{KickCronAdapter as i,isClusterPrimary as n,CRON_WORKER_ENV as r,bootstrap as t};
|
|
12
|
+
//# sourceMappingURL=bootstrap-CJ8ALvif.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap-K1PcxSTg.mjs","names":["log","log"],"sources":["../src/http/cron-adapter.ts","../src/http/cluster.ts","../src/http/bootstrap.ts"],"sourcesContent":["/**\n * `KickCronAdapter()` — runs `@Cron` jobs on the Node server.\n *\n * Named with the `Kick` prefix (adapter name and logger too) so it never\n * collides with an app's own `CronAdapter` — which is also why it is opt-in.\n *\n * Opt-in: `@Cron` only records metadata, and apps that schedule jobs with\n * their own adapter keep doing so — adding this one to such an app would run\n * every job twice. Register it once:\n *\n * ```ts\n * bootstrap({ modules, adapters: [KickCronAdapter()] })\n * ```\n *\n * Timing comes from the optional peer `croner` (`pnpm add croner`), loaded\n * only when there is a job to schedule. Each tick resolves the job's class\n * from the container (the same singletons a request gets) and calls\n * `runCronJob`, which applies `enabled` / `overlap` and reports failures to\n * the error observers.\n *\n * Several processes: in `bootstrap({ cluster })`, only the worker the primary\n * marks with `KICK_CRON_WORKER=1` schedules, so a job runs once per cluster.\n * Several separate instances (replicas behind a load balancer) each schedule\n * every job — turn it off on all but one (`KickCronAdapter({ enabled: false })`),\n * or have the job take a lock.\n *\n * @module @forinda/kickjs/http/cron-adapter\n */\nimport cluster from 'node:cluster'\nimport { createRequire } from 'node:module'\nimport { defineAdapter } from '../core/define-adapter'\nimport { createLogger } from '../core/logger'\nimport { listCronJobs, runCronJob, type CronJob } from '../core/cron'\nimport type { Container } from '../core/container'\n\nconst log = createLogger('KickCronAdapter')\nconst peerRequire = createRequire(import.meta.url)\n\n/** Set by the cluster primary on the one worker that runs `KickCronAdapter` jobs. */\nexport const CRON_WORKER_ENV = 'KICK_CRON_WORKER'\n\nexport interface CronAdapterOptions {\n /** Off for processes that should run no jobs (replicas, one-off scripts). Default `true`. */\n enabled?: boolean\n}\n\ninterface Schedule {\n stop(): void\n}\n\ntype CronerCtor = new (pattern: string, options: { timezone?: string }, fn: () => void) => Schedule\n\nfunction loadCroner(): CronerCtor | undefined {\n try {\n return (peerRequire('croner') as { Cron: CronerCtor }).Cron\n } catch {\n return undefined\n }\n}\n\n/** Whether this process should schedule jobs — one worker per cluster. */\nfunction isCronProcess(): boolean {\n return !cluster.isWorker || process.env[CRON_WORKER_ENV] === '1'\n}\n\nexport const KickCronAdapter = defineAdapter<CronAdapterOptions>({\n name: 'KickCronAdapter',\n defaults: { enabled: true },\n build(options) {\n const schedules: Schedule[] = []\n const inFlight = new Set<Promise<unknown>>()\n let jobs: CronJob[] = []\n\n return {\n afterStart({ container }) {\n if (options.enabled === false || !isCronProcess()) return\n jobs = listCronJobs(container)\n if (jobs.length === 0) return\n\n const Croner = loadCroner()\n if (!Croner) {\n log.warn(\n `${jobs.length} @Cron job(s) found but \"croner\" is not installed, so none will run. ` +\n 'Install it: pnpm add croner',\n )\n return\n }\n\n const run = (job: CronJob, trigger: 'schedule' | 'init') => {\n // Failures are already logged and reported by runCronJob.\n const p = runCronJob(job, container as Container, trigger).catch(() => {})\n inFlight.add(p)\n void p.finally(() => inFlight.delete(p))\n }\n\n for (const job of jobs) {\n schedules.push(\n new Croner(\n job.expression,\n // No croner `name`: croner keeps names in a process-wide registry\n // and throws on a duplicate.\n job.timezone ? { timezone: job.timezone } : {},\n () => run(job, 'schedule'),\n ),\n )\n if (job.runOnInit) run(job, 'init')\n }\n log.info(`Scheduled ${jobs.length} @Cron job(s): ${jobs.map((j) => j.name).join(', ')}`)\n },\n\n /**\n * Stops the schedules, then waits for running jobs so they don't lose\n * their dependencies mid-run. Also runs on every HMR reload, so a reload\n * never leaves a second copy ticking.\n */\n async shutdown() {\n for (const schedule of schedules.splice(0)) schedule.stop()\n await Promise.all(inFlight)\n },\n\n introspect() {\n return {\n protocolVersion: 1,\n name: 'KickCronAdapter',\n kind: 'adapter',\n state: {\n jobs: jobs.map((j) => ({\n name: j.name,\n expression: j.expression,\n timezone: j.timezone,\n description: j.description,\n })),\n },\n } as never\n },\n }\n },\n})\n","import cluster from 'node:cluster'\nimport { CRON_WORKER_ENV } from './cron-adapter'\nimport os from 'node:os'\nimport { createLogger } from '../core'\nimport { reactive, ref } from '../core/reactivity'\n\nconst log = createLogger('Cluster')\n\nexport interface ClusterOptions {\n /** Number of worker processes (default: os.cpus().length) */\n workers?: number\n /** Delay before forcefully killing a worker during shutdown (ms), default: 5000 */\n gracefulShutdownTimeout?: number\n /** Delay between worker restarts (ms), default: 1000 */\n restartDelay?: number\n /** Max failures in restartWindow before refusing to restart, default: 5 */\n maxFailures?: number\n /** Time window for counting failures (ms), default: 30000 */\n restartWindow?: number\n}\n\n/** Returns true if the current process is the cluster primary */\nexport function isClusterPrimary(): boolean {\n return cluster.isPrimary\n}\n\n/**\n * Fork worker processes and manage the cluster lifecycle.\n *\n * The primary process forks `workers` child processes. Each worker\n * shares the same port via Node's built-in `cluster` module (OS\n * round-robin load balancing).\n *\n * Features:\n * - Graceful shutdown with timeout fallback to forceful kill\n * - Auto-restart dead workers (respects voluntary disconnects)\n * - Startup timeout detection (stuck workers)\n * - Crash loop prevention (backoff on rapid failures)\n * - Comprehensive lifecycle logging\n */\nexport function setupClusterPrimary(opts: ClusterOptions): void {\n const numWorkers = opts.workers ?? os.cpus().length\n const gracefulTimeout = opts.gracefulShutdownTimeout ?? 5000\n const restartDelay = opts.restartDelay ?? 1000\n const maxFailures = opts.maxFailures ?? 5\n const restartWindow = opts.restartWindow ?? 30000\n\n const startupTimeouts = new Map<number, NodeJS.Timeout>()\n const state = reactive({\n failureTimestamps: [] as number[],\n shuttingDown: false,\n })\n const activeWorkers = ref(0)\n\n log.info(`Primary ${process.pid} starting ${numWorkers} worker(s)`)\n\n // Exactly one worker runs `KickCronAdapter` jobs, so a job fires once per\n // cluster rather than once per worker. The primary marks it with\n // KICK_CRON_WORKER=1; when it dies, the next fork inherits the role.\n let cronWorkerId: number | undefined\n const forkWorker = (): void => {\n const takesCron = cronWorkerId === undefined\n const worker = cluster.fork(takesCron ? { [CRON_WORKER_ENV]: '1' } : undefined)\n if (takesCron) cronWorkerId = worker.id\n }\n\n // Initial fork\n for (let i = 0; i < numWorkers; i++) {\n forkWorker()\n }\n\n // Detect stuck workers (no 'listening' event within timeout)\n cluster.on('fork', (worker) => {\n const timeout = setTimeout(() => {\n log.error(`Worker ${worker.process.pid} did not respond within 10s, killing...`)\n worker.kill()\n }, 10000)\n\n startupTimeouts.set(worker.id, timeout)\n activeWorkers.value++\n log.debug(`Worker ${worker.process.pid} forked (id=${worker.id})`)\n })\n\n // Clear startup timeout when worker is ready\n cluster.on('listening', (worker, address) => {\n const timeout = startupTimeouts.get(worker.id)\n if (timeout) {\n clearTimeout(timeout)\n startupTimeouts.delete(worker.id)\n }\n log.info(`Worker ${worker.process.pid} listening on ${address.address}:${address.port}`)\n })\n\n // Log when worker comes online\n cluster.on('online', (worker) => {\n log.debug(`Worker ${worker.process.pid} online (id=${worker.id})`)\n })\n\n // Auto-restart dead workers (unless intentionally disconnected)\n cluster.on('exit', (worker, code, signal) => {\n // Clear startup timeout if still pending\n const timeout = startupTimeouts.get(worker.id)\n if (timeout) {\n clearTimeout(timeout)\n startupTimeouts.delete(worker.id)\n }\n\n // Don't restart if worker was intentionally disconnected\n activeWorkers.value--\n if (worker.id === cronWorkerId) cronWorkerId = undefined\n\n if (worker.exitedAfterDisconnect) {\n log.info(`Worker ${worker.process.pid} exited gracefully (id=${worker.id})`)\n return\n }\n\n // Track crash history globally (worker IDs change on restart)\n const now = Date.now()\n // Remove timestamps outside the window, then add the current failure\n while (\n state.failureTimestamps.length > 0 &&\n now - state.failureTimestamps[0] >= restartWindow\n ) {\n state.failureTimestamps.shift()\n }\n state.failureTimestamps.push(now)\n\n // Prevent restart loops\n if (state.failureTimestamps.length > maxFailures) {\n log.error(\n `${state.failureTimestamps.length} worker failures in ${restartWindow}ms, giving up`,\n )\n // Optionally exit the primary if too many workers are crashing\n if (activeWorkers.value === 0) {\n log.error('All workers have crashed, exiting primary')\n process.exit(1)\n }\n return\n }\n\n log.warn(\n `Worker ${worker.process.pid} died (code=${code}, signal=${signal}), restarting in ${restartDelay}ms... (failures: ${state.failureTimestamps.length}/${maxFailures})`,\n )\n\n setTimeout(() => {\n // Only restart if we still need more workers\n if (activeWorkers.value < numWorkers) {\n forkWorker()\n }\n }, restartDelay)\n })\n\n // Log when worker disconnects (can happen before exit)\n cluster.on('disconnect', (worker) => {\n log.debug(`Worker ${worker.process.pid} disconnected (id=${worker.id})`)\n })\n\n // Forward shutdown signals to workers (disconnect so exitedAfterDisconnect is set)\n for (const signal of ['SIGTERM', 'SIGINT'] as const) {\n process.on(signal, () => {\n if (state.shuttingDown) return\n state.shuttingDown = true\n log.info(`Primary received ${signal}, disconnecting workers...`)\n cluster.disconnect(() => {\n log.info('All workers disconnected, exiting primary')\n process.exit(0)\n })\n // Force kill after graceful timeout\n setTimeout(() => {\n log.warn('Graceful shutdown timed out, forcing exit')\n process.exit(1)\n }, gracefulTimeout).unref()\n })\n }\n}\n","import cluster from 'node:cluster'\nimport { reportError } from '../core/observers'\nimport { createLogger, Container } from '../core'\nimport { reloadEnv } from '../config/env'\nimport { Application, type ApplicationOptions } from './application'\nimport { setupClusterPrimary, type ClusterOptions } from './cluster'\n\n/** Reload env from .env file (HMR rebuild) — config now lives in-tree. */\nfunction tryReloadEnv(): void {\n try {\n reloadEnv()\n } catch {\n // Best-effort: never crash bootstrap because env reload failed.\n }\n}\n\nconst log = createLogger('Process')\n\n/**\n * Bootstrap a KickJS application.\n *\n * Creates the Express app, registers modules and adapters, and starts\n * the HTTP server. In dev mode (`kick dev`), the Vite plugin manages\n * the server and HMR — source changes are picked up instantly without\n * losing database connections or WebSocket state.\n *\n * @returns The {@link Application} instance.\n *\n * @example\n * ```ts\n * // src/index.ts\n * import 'reflect-metadata'\n * import { bootstrap } from '@forinda/kickjs'\n * import { modules } from './modules'\n *\n * export const app = await bootstrap({ modules })\n * ```\n */\nexport async function bootstrap(options: ApplicationOptions): Promise<Application> {\n // ── Cluster mode ─────────────────────────────────────────────────────\n // In cluster mode the primary process forks workers and never runs the\n // Express application itself. Only workers proceed past this block.\n // Cluster is disabled in Vite dev mode — Vite owns the HTTP server and\n // forked workers would each create their own Vite instance on separate ports.\n const isViteDev = !!(globalThis as any).__kickjs_httpServer\n if (options.cluster && isViteDev) {\n log.warn(\n 'Cluster mode is ignored in dev — Vite owns the server. Cluster applies in production (kick start).',\n )\n }\n if (options.cluster && cluster.isPrimary && !isViteDev) {\n const clusterOpts: ClusterOptions = typeof options.cluster === 'object' ? options.cluster : {}\n setupClusterPrimary(clusterOpts)\n\n // Return a placeholder — the primary never serves HTTP traffic.\n // The Application is created only so callers get a valid reference.\n return new Application(options)\n }\n\n // Internal: globalThis stores the app instance and Vite's HTTP server\n // reference across HMR rebuilds. When __kickjs_httpServer is set, the\n // Vite plugin owns the port and bootstrap() skips server creation.\n // On subsequent calls (HMR), the Express handler is swapped without\n // restarting the server — preserving DB connections and port bindings.\n const g = globalThis as any\n\n // ── Global error handlers + signal hooks ─────────────────────────────\n // `processHooks` controls whether KickJS registers its own\n // process-level handlers. Defaults to `'auto'` — register everything.\n // Adopters that bring their own observability stack (OpenTelemetry SDK,\n // Sentry, custom shutdown choreographer) often register their own\n // SIGINT/SIGTERM handlers and need to opt out of ours so the two don't\n // race to call `process.exit(0)`. Three modes:\n // - 'auto' (default): register uncaughtException/unhandledRejection\n // loggers + SIGINT/SIGTERM → `app.shutdown()` → `process.exit(0)`.\n // - 'errors-only': register the error loggers; skip signal handlers.\n // Use this when you have your own shutdown choreographer that calls\n // `app.shutdown()` itself.\n // - 'manual': skip everything. Adopter is responsible for both error\n // logging and signal-driven shutdown.\n const processHooks = options.processHooks ?? 'auto'\n\n if (!g.__kickBootstrapped) {\n if (processHooks !== 'manual') {\n // A listener here replaces Node's own handling, which exits with code\n // 1. Keep the failure in the exit code: a boot that throws — a top-level\n // `await bootstrap()` rejecting — must not end the process with 0, or a\n // deploy reads the crash as success.\n process.on('uncaughtException', (err) => {\n log.error(err, 'Uncaught exception')\n reportError(err, { source: 'uncaught' })\n process.exitCode = 1\n })\n\n process.on('unhandledRejection', (reason) => {\n log.error(reason as any, 'Unhandled rejection')\n reportError(reason, { source: 'unhandled-rejection' })\n process.exitCode = 1\n })\n }\n\n if (processHooks === 'auto' && !g.__kickjs_httpServer) {\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.on(signal, async () => {\n log.info(`Received ${signal}, shutting down...`)\n if (g.__app) await g.__app.shutdown()\n process.exit(0)\n })\n }\n }\n\n g.__kickBootstrapped = true\n }\n\n // ── HMR rebuild ──────────────────────────────────────────────────────\n if (g.__app) {\n log.debug('HMR rebuild triggered')\n\n // Tear the PREVIOUS app down before replacing it. Without this the old\n // Application was dropped on the floor still running: its adapters kept\n // their sockets and consumers open, so every save added another set.\n //\n // Observed in the wild as one process holding several consumer-group\n // members — on a single-partition topic only one can hold the assignment,\n // and it was a leaked consumer no longer wired to anything, so jobs\n // stopped being processed. A second socket.io server on the same HTTP\n // server crashed `handleUpgrade()` the same way.\n //\n // `closeServer: false` is load-bearing: the dev HTTP server is shared\n // across rebuilds, so a full shutdown would close the listening socket and\n // kill HMR on the first save.\n //\n // Failures are logged, not thrown — one broken adapter must not leave the\n // dev server wedged with no app at all.\n try {\n await g.__app.shutdown({ closeServer: false })\n } catch (err) {\n log.error(err as Error, 'HMR teardown of the previous app failed')\n }\n\n tryReloadEnv()\n Container.reset()\n\n const freshApp = new Application(options)\n g.__app = freshApp\n g.__kickjs_container = freshApp.getContainer()\n\n await freshApp.start()\n log.debug('HMR rebuild complete')\n return freshApp\n }\n\n // ── First boot ───────────────────────────────────────────────────────\n const app = new Application(options)\n g.__app = app\n g.__kickjs_container = app.getContainer()\n\n if (process.env.KICK_TINKER) {\n await app.registerOnly()\n return app\n }\n\n await app.start()\n\n return app\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bootstrap-CJ8ALvif.mjs","names":["log","log"],"sources":["../src/http/cron-adapter.ts","../src/http/cluster.ts","../src/http/bootstrap.ts"],"sourcesContent":["/**\n * `KickCronAdapter()` — runs `@Cron` jobs on the Node server.\n *\n * Named with the `Kick` prefix (adapter name and logger too) so it never\n * collides with an app's own `CronAdapter` — which is also why it is opt-in.\n *\n * Opt-in: `@Cron` only records metadata, and apps that schedule jobs with\n * their own adapter keep doing so — adding this one to such an app would run\n * every job twice. Register it once:\n *\n * ```ts\n * bootstrap({ modules, adapters: [KickCronAdapter()] })\n * ```\n *\n * Timing comes from the optional peer `croner` (`pnpm add croner`), loaded\n * only when there is a job to schedule. Each tick resolves the job's class\n * from the container (the same singletons a request gets) and calls\n * `runCronJob`, which applies `enabled` / `overlap` and reports failures to\n * the error observers.\n *\n * Several processes: in `bootstrap({ cluster })`, only the worker the primary\n * marks with `KICK_CRON_WORKER=1` schedules, so a job runs once per cluster.\n * Several separate instances (replicas behind a load balancer) each schedule\n * every job — turn it off on all but one (`KickCronAdapter({ enabled: false })`),\n * or have the job take a lock.\n *\n * @module @forinda/kickjs/http/cron-adapter\n */\nimport cluster from 'node:cluster'\nimport { createRequire } from 'node:module'\nimport { defineAdapter } from '../core/define-adapter'\nimport { createLogger } from '../core/logger'\nimport { listCronJobs, runCronJob, type CronJob } from '../core/cron'\nimport type { Container } from '../core/container'\n\nconst log = createLogger('KickCronAdapter')\nconst peerRequire = createRequire(import.meta.url)\n\n/** Set by the cluster primary on the one worker that runs `KickCronAdapter` jobs. */\nexport const CRON_WORKER_ENV = 'KICK_CRON_WORKER'\n\nexport interface CronAdapterOptions {\n /** Off for processes that should run no jobs (replicas, one-off scripts). Default `true`. */\n enabled?: boolean\n}\n\ninterface Schedule {\n stop(): void\n}\n\ntype CronerCtor = new (pattern: string, options: { timezone?: string }, fn: () => void) => Schedule\n\nfunction loadCroner(): CronerCtor | undefined {\n try {\n return (peerRequire('croner') as { Cron: CronerCtor }).Cron\n } catch {\n return undefined\n }\n}\n\n/** Whether this process should schedule jobs — one worker per cluster. */\nfunction isCronProcess(): boolean {\n return !cluster.isWorker || process.env[CRON_WORKER_ENV] === '1'\n}\n\nexport const KickCronAdapter = defineAdapter<CronAdapterOptions>({\n name: 'KickCronAdapter',\n defaults: { enabled: true },\n build(options) {\n const schedules: Schedule[] = []\n const inFlight = new Set<Promise<unknown>>()\n let jobs: CronJob[] = []\n\n return {\n afterStart({ container }) {\n if (options.enabled === false || !isCronProcess()) return\n jobs = listCronJobs(container)\n if (jobs.length === 0) return\n\n const Croner = loadCroner()\n if (!Croner) {\n log.warn(\n `${jobs.length} @Cron job(s) found but \"croner\" is not installed, so none will run. ` +\n 'Install it: pnpm add croner',\n )\n return\n }\n\n const run = (job: CronJob, trigger: 'schedule' | 'init') => {\n // Failures are already logged and reported by runCronJob.\n const p = runCronJob(job, container as Container, trigger).catch(() => {})\n inFlight.add(p)\n void p.finally(() => inFlight.delete(p))\n }\n\n for (const job of jobs) {\n schedules.push(\n new Croner(\n job.expression,\n // No croner `name`: croner keeps names in a process-wide registry\n // and throws on a duplicate.\n job.timezone ? { timezone: job.timezone } : {},\n () => run(job, 'schedule'),\n ),\n )\n if (job.runOnInit) run(job, 'init')\n }\n log.info(`Scheduled ${jobs.length} @Cron job(s): ${jobs.map((j) => j.name).join(', ')}`)\n },\n\n /**\n * Stops the schedules, then waits for running jobs so they don't lose\n * their dependencies mid-run. Also runs on every HMR reload, so a reload\n * never leaves a second copy ticking.\n */\n async shutdown() {\n for (const schedule of schedules.splice(0)) schedule.stop()\n await Promise.all(inFlight)\n },\n\n introspect() {\n return {\n protocolVersion: 1,\n name: 'KickCronAdapter',\n kind: 'adapter',\n state: {\n jobs: jobs.map((j) => ({\n name: j.name,\n expression: j.expression,\n timezone: j.timezone,\n description: j.description,\n })),\n },\n } as never\n },\n }\n },\n})\n","import cluster from 'node:cluster'\nimport { CRON_WORKER_ENV } from './cron-adapter'\nimport os from 'node:os'\nimport { createLogger } from '../core'\nimport { reactive, ref } from '../core/reactivity'\n\nconst log = createLogger('Cluster')\n\nexport interface ClusterOptions {\n /** Number of worker processes (default: os.cpus().length) */\n workers?: number\n /** Delay before forcefully killing a worker during shutdown (ms), default: 5000 */\n gracefulShutdownTimeout?: number\n /** Delay between worker restarts (ms), default: 1000 */\n restartDelay?: number\n /** Max failures in restartWindow before refusing to restart, default: 5 */\n maxFailures?: number\n /** Time window for counting failures (ms), default: 30000 */\n restartWindow?: number\n}\n\n/** Returns true if the current process is the cluster primary */\nexport function isClusterPrimary(): boolean {\n return cluster.isPrimary\n}\n\n/**\n * Fork worker processes and manage the cluster lifecycle.\n *\n * The primary process forks `workers` child processes. Each worker\n * shares the same port via Node's built-in `cluster` module (OS\n * round-robin load balancing).\n *\n * Features:\n * - Graceful shutdown with timeout fallback to forceful kill\n * - Auto-restart dead workers (respects voluntary disconnects)\n * - Startup timeout detection (stuck workers)\n * - Crash loop prevention (backoff on rapid failures)\n * - Comprehensive lifecycle logging\n */\nexport function setupClusterPrimary(opts: ClusterOptions): void {\n const numWorkers = opts.workers ?? os.cpus().length\n const gracefulTimeout = opts.gracefulShutdownTimeout ?? 5000\n const restartDelay = opts.restartDelay ?? 1000\n const maxFailures = opts.maxFailures ?? 5\n const restartWindow = opts.restartWindow ?? 30000\n\n const startupTimeouts = new Map<number, NodeJS.Timeout>()\n const state = reactive({\n failureTimestamps: [] as number[],\n shuttingDown: false,\n })\n const activeWorkers = ref(0)\n\n log.info(`Primary ${process.pid} starting ${numWorkers} worker(s)`)\n\n // Exactly one worker runs `KickCronAdapter` jobs, so a job fires once per\n // cluster rather than once per worker. The primary marks it with\n // KICK_CRON_WORKER=1; when it dies, the next fork inherits the role.\n let cronWorkerId: number | undefined\n const forkWorker = (): void => {\n const takesCron = cronWorkerId === undefined\n const worker = cluster.fork(takesCron ? { [CRON_WORKER_ENV]: '1' } : undefined)\n if (takesCron) cronWorkerId = worker.id\n }\n\n // Initial fork\n for (let i = 0; i < numWorkers; i++) {\n forkWorker()\n }\n\n // Detect stuck workers (no 'listening' event within timeout)\n cluster.on('fork', (worker) => {\n const timeout = setTimeout(() => {\n log.error(`Worker ${worker.process.pid} did not respond within 10s, killing...`)\n worker.kill()\n }, 10000)\n\n startupTimeouts.set(worker.id, timeout)\n activeWorkers.value++\n log.debug(`Worker ${worker.process.pid} forked (id=${worker.id})`)\n })\n\n // Clear startup timeout when worker is ready\n cluster.on('listening', (worker, address) => {\n const timeout = startupTimeouts.get(worker.id)\n if (timeout) {\n clearTimeout(timeout)\n startupTimeouts.delete(worker.id)\n }\n log.info(`Worker ${worker.process.pid} listening on ${address.address}:${address.port}`)\n })\n\n // Log when worker comes online\n cluster.on('online', (worker) => {\n log.debug(`Worker ${worker.process.pid} online (id=${worker.id})`)\n })\n\n // Auto-restart dead workers (unless intentionally disconnected)\n cluster.on('exit', (worker, code, signal) => {\n // Clear startup timeout if still pending\n const timeout = startupTimeouts.get(worker.id)\n if (timeout) {\n clearTimeout(timeout)\n startupTimeouts.delete(worker.id)\n }\n\n // Don't restart if worker was intentionally disconnected\n activeWorkers.value--\n if (worker.id === cronWorkerId) cronWorkerId = undefined\n\n if (worker.exitedAfterDisconnect) {\n log.info(`Worker ${worker.process.pid} exited gracefully (id=${worker.id})`)\n return\n }\n\n // Track crash history globally (worker IDs change on restart)\n const now = Date.now()\n // Remove timestamps outside the window, then add the current failure\n while (\n state.failureTimestamps.length > 0 &&\n now - state.failureTimestamps[0] >= restartWindow\n ) {\n state.failureTimestamps.shift()\n }\n state.failureTimestamps.push(now)\n\n // Prevent restart loops\n if (state.failureTimestamps.length > maxFailures) {\n log.error(\n `${state.failureTimestamps.length} worker failures in ${restartWindow}ms, giving up`,\n )\n // Optionally exit the primary if too many workers are crashing\n if (activeWorkers.value === 0) {\n log.error('All workers have crashed, exiting primary')\n process.exit(1)\n }\n return\n }\n\n log.warn(\n `Worker ${worker.process.pid} died (code=${code}, signal=${signal}), restarting in ${restartDelay}ms... (failures: ${state.failureTimestamps.length}/${maxFailures})`,\n )\n\n setTimeout(() => {\n // Only restart if we still need more workers\n if (activeWorkers.value < numWorkers) {\n forkWorker()\n }\n }, restartDelay)\n })\n\n // Log when worker disconnects (can happen before exit)\n cluster.on('disconnect', (worker) => {\n log.debug(`Worker ${worker.process.pid} disconnected (id=${worker.id})`)\n })\n\n // Forward shutdown signals to workers (disconnect so exitedAfterDisconnect is set)\n for (const signal of ['SIGTERM', 'SIGINT'] as const) {\n process.on(signal, () => {\n if (state.shuttingDown) return\n state.shuttingDown = true\n log.info(`Primary received ${signal}, disconnecting workers...`)\n cluster.disconnect(() => {\n log.info('All workers disconnected, exiting primary')\n process.exit(0)\n })\n // Force kill after graceful timeout\n setTimeout(() => {\n log.warn('Graceful shutdown timed out, forcing exit')\n process.exit(1)\n }, gracefulTimeout).unref()\n })\n }\n}\n","import cluster from 'node:cluster'\nimport { reportError } from '../core/observers'\nimport { createLogger, Container } from '../core'\nimport { reloadEnv } from '../config/env'\nimport { Application, type ApplicationOptions } from './application'\nimport { setupClusterPrimary, type ClusterOptions } from './cluster'\n\n/** Reload env from .env file (HMR rebuild) — config now lives in-tree. */\nfunction tryReloadEnv(): void {\n try {\n reloadEnv()\n } catch {\n // Best-effort: never crash bootstrap because env reload failed.\n }\n}\n\nconst log = createLogger('Process')\n\n/**\n * Bootstrap a KickJS application.\n *\n * Creates the Express app, registers modules and adapters, and starts\n * the HTTP server. In dev mode (`kick dev`), the Vite plugin manages\n * the server and HMR — source changes are picked up instantly without\n * losing database connections or WebSocket state.\n *\n * @returns The {@link Application} instance.\n *\n * @example\n * ```ts\n * // src/index.ts\n * import 'reflect-metadata'\n * import { bootstrap } from '@forinda/kickjs'\n * import { modules } from './modules'\n *\n * export const app = await bootstrap({ modules })\n * ```\n */\nexport async function bootstrap(options: ApplicationOptions): Promise<Application> {\n // ── Cluster mode ─────────────────────────────────────────────────────\n // In cluster mode the primary process forks workers and never runs the\n // Express application itself. Only workers proceed past this block.\n // Cluster is disabled in Vite dev mode — Vite owns the HTTP server and\n // forked workers would each create their own Vite instance on separate ports.\n const isViteDev = !!(globalThis as any).__kickjs_httpServer\n if (options.cluster && isViteDev) {\n log.warn(\n 'Cluster mode is ignored in dev — Vite owns the server. Cluster applies in production (kick start).',\n )\n }\n if (options.cluster && cluster.isPrimary && !isViteDev) {\n const clusterOpts: ClusterOptions = typeof options.cluster === 'object' ? options.cluster : {}\n setupClusterPrimary(clusterOpts)\n\n // Return a placeholder — the primary never serves HTTP traffic.\n // The Application is created only so callers get a valid reference.\n return new Application(options)\n }\n\n // Internal: globalThis stores the app instance and Vite's HTTP server\n // reference across HMR rebuilds. When __kickjs_httpServer is set, the\n // Vite plugin owns the port and bootstrap() skips server creation.\n // On subsequent calls (HMR), the Express handler is swapped without\n // restarting the server — preserving DB connections and port bindings.\n const g = globalThis as any\n\n // ── Global error handlers + signal hooks ─────────────────────────────\n // `processHooks` controls whether KickJS registers its own\n // process-level handlers. Defaults to `'auto'` — register everything.\n // Adopters that bring their own observability stack (OpenTelemetry SDK,\n // Sentry, custom shutdown choreographer) often register their own\n // SIGINT/SIGTERM handlers and need to opt out of ours so the two don't\n // race to call `process.exit(0)`. Three modes:\n // - 'auto' (default): register uncaughtException/unhandledRejection\n // loggers + SIGINT/SIGTERM → `app.shutdown()` → `process.exit(0)`.\n // - 'errors-only': register the error loggers; skip signal handlers.\n // Use this when you have your own shutdown choreographer that calls\n // `app.shutdown()` itself.\n // - 'manual': skip everything. Adopter is responsible for both error\n // logging and signal-driven shutdown.\n const processHooks = options.processHooks ?? 'auto'\n\n if (!g.__kickBootstrapped) {\n if (processHooks !== 'manual') {\n // A listener here replaces Node's own handling, which exits with code\n // 1. Keep the failure in the exit code: a boot that throws — a top-level\n // `await bootstrap()` rejecting — must not end the process with 0, or a\n // deploy reads the crash as success.\n process.on('uncaughtException', (err) => {\n log.error(err, 'Uncaught exception')\n reportError(err, { source: 'uncaught' })\n process.exitCode = 1\n })\n\n process.on('unhandledRejection', (reason) => {\n log.error(reason as any, 'Unhandled rejection')\n reportError(reason, { source: 'unhandled-rejection' })\n process.exitCode = 1\n })\n }\n\n if (processHooks === 'auto' && !g.__kickjs_httpServer) {\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.on(signal, async () => {\n log.info(`Received ${signal}, shutting down...`)\n if (g.__app) await g.__app.shutdown()\n process.exit(0)\n })\n }\n }\n\n g.__kickBootstrapped = true\n }\n\n // ── HMR rebuild ──────────────────────────────────────────────────────\n if (g.__app) {\n log.debug('HMR rebuild triggered')\n\n // Tear the PREVIOUS app down before replacing it. Without this the old\n // Application was dropped on the floor still running: its adapters kept\n // their sockets and consumers open, so every save added another set.\n //\n // Observed in the wild as one process holding several consumer-group\n // members — on a single-partition topic only one can hold the assignment,\n // and it was a leaked consumer no longer wired to anything, so jobs\n // stopped being processed. A second socket.io server on the same HTTP\n // server crashed `handleUpgrade()` the same way.\n //\n // `closeServer: false` is load-bearing: the dev HTTP server is shared\n // across rebuilds, so a full shutdown would close the listening socket and\n // kill HMR on the first save.\n //\n // Failures are logged, not thrown — one broken adapter must not leave the\n // dev server wedged with no app at all.\n try {\n await g.__app.shutdown({ closeServer: false })\n } catch (err) {\n log.error(err as Error, 'HMR teardown of the previous app failed')\n }\n\n tryReloadEnv()\n Container.reset()\n\n const freshApp = new Application(options)\n g.__app = freshApp\n g.__kickjs_container = freshApp.getContainer()\n\n await freshApp.start()\n log.debug('HMR rebuild complete')\n return freshApp\n }\n\n // ── First boot ───────────────────────────────────────────────────────\n const app = new Application(options)\n g.__app = app\n g.__kickjs_container = app.getContainer()\n\n if (process.env.KICK_TINKER) {\n await app.registerOnly()\n return app\n }\n\n await app.start()\n\n return app\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v9.0.
|
|
2
|
+
* @forinda/kickjs v9.0.2
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
11
|
function firstHop(value){return typeof value==`string`&&value.length>0?value.split(`,`)[0].trim():void 0}function headerValue(headers,name){if(headers)return typeof headers.get==`function`?headers.get(name):headers[name]}function resolveClientIp(req){if(typeof req.ip==`string`&&req.ip.length>0)return req.ip;let socketAddress=req.socket?.remoteAddress;if(socketAddress)return socketAddress;let h=req.headers;return firstHop(headerValue(h,`cf-connecting-ip`))??firstHop(headerValue(h,`x-forwarded-for`))??firstHop(headerValue(h,`x-real-ip`))}function resolvePathname(req){if(typeof req.path==`string`&&req.path.length>0)return req.path;let url=req.url;if(!url)return`/`;if(/^[a-z][a-z0-9+.-]*:\/\//i.test(url))try{return new URL(url).pathname||`/`}catch{return`/`}let q=url.indexOf(`?`);return(q===-1?url:url.slice(0,q))||`/`}export{resolvePathname as n,resolveClientIp as t};
|
|
12
|
-
//# sourceMappingURL=client-ip-
|
|
12
|
+
//# sourceMappingURL=client-ip-BHH-wDR7.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-ip-
|
|
1
|
+
{"version":3,"file":"client-ip-BHH-wDR7.mjs","names":[],"sources":["../src/http/client-ip.ts"],"sourcesContent":["/**\n * Client address + pathname resolution shared by everything that runs as\n * connect-style middleware.\n *\n * Fastify and h3 hand middleware the RAW node request, so the Express\n * conveniences (`req.ip`, `req.path`, `req.originalUrl`) are `undefined`\n * there. Reading them directly produced two live bugs:\n *\n * rate-limit → every caller keyed as the `'127.0.0.1'` fallback, i.e. ONE\n * shared bucket, so a single client could exhaust the limit\n * for everyone\n * csrf → `ignorePaths` never matched, so configured exemptions were\n * silently dead\n *\n * Edge-safe: no node imports.\n */\n\n/** The request members these helpers read, from node's shape rather than Express's. */\nexport interface ClientRequestLike {\n url?: string\n /** Express-only convenience. */\n path?: string\n /** Express-only; the full path before router mounting rewrote `url`. */\n originalUrl?: string\n /** Express and Fastify compute this from their trust-proxy settings. */\n ip?: unknown\n /**\n * A plain object on every path the framework currently takes — `WebRequestShim`\n * normalizes a WHATWG `Headers` before the context is built. A `Headers`\n * instance is accepted defensively: it is not index-accessible, so if one ever\n * did arrive, every lookup would return `undefined` SILENTLY and drop each\n * caller into one shared rate-limit bucket.\n */\n headers?: Record<string, string | string[] | undefined> | Headers\n socket?: { remoteAddress?: string }\n}\n\n/** First entry of a possibly comma-separated header value. */\nfunction firstHop(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value.split(',')[0]!.trim() : undefined\n}\n\n/** Read one header from either a plain object or a WHATWG `Headers`. */\nfunction headerValue(\n headers: Record<string, string | string[] | undefined> | Headers | undefined,\n name: string,\n): unknown {\n if (!headers) return undefined\n // `Headers` has no index access, so `headers[name]` silently yields\n // `undefined` rather than the value.\n if (typeof (headers as Headers).get === 'function') return (headers as Headers).get(name)\n return (headers as Record<string, string | string[] | undefined>)[name]\n}\n\n/**\n * The client's IP address, or `undefined` when it cannot be determined.\n *\n * Prefers the address the runtime computed — Express derives `req.ip` from\n * `trust proxy`, Fastify from `trustProxy` — because raw forwarded headers are\n * client-SPOOFABLE wherever nothing normalizes them. h3 takes the same stance:\n * its `getRequestIP` only consults `x-forwarded-for` when explicitly opted in.\n *\n * Order is `req.ip` → `socket.remoteAddress` → forwarded headers. The socket\n * comes BEFORE the headers deliberately: it cannot be spoofed, whereas a direct\n * client can vary `x-forwarded-for` per request to get a fresh rate-limit\n * bucket each time. Headers are consulted only where there is no socket — the\n * web/edge entry, where the platform terminates the connection.\n */\nexport function resolveClientIp(req: ClientRequestLike): string | undefined {\n // 1. What the runtime computed. Express derives `req.ip` from `trust proxy`,\n // Fastify from `trustProxy` — a value vetted against a CONFIGURED proxy,\n // which is the only reason to believe a forwarded header at all.\n if (typeof req.ip === 'string' && req.ip.length > 0) return req.ip\n\n // 2. The socket. Unspoofable, and present on every node runtime.\n //\n // This MUST precede the headers. A raw Fastify / h3 request has no\n // `req.ip`, so consulting `x-forwarded-for` first let a DIRECT client send\n // a different value per request and land in a fresh rate-limit bucket each\n // time — evading the limit entirely. Behind a real proxy the socket is the\n // proxy's address, which buckets clients together; the fix for that is to\n // configure the runtime's trust-proxy setting so step 1 answers, not to\n // trust an unverified header here.\n const socketAddress = req.socket?.remoteAddress\n if (socketAddress) return socketAddress\n\n // 3. Headers, only where there is no socket at all — the web/edge entry,\n // where the platform terminates the connection and these are the only\n // signal available.\n const h = req.headers\n return (\n firstHop(headerValue(h, 'cf-connecting-ip')) ??\n firstHop(headerValue(h, 'x-forwarded-for')) ??\n firstHop(headerValue(h, 'x-real-ip'))\n )\n}\n\n/** Pathname only — `req.url` carries the query string, and `req.path` is Express-only. */\nexport function resolvePathname(req: ClientRequestLike): string {\n if (typeof req.path === 'string' && req.path.length > 0) return req.path\n const url = req.url\n if (!url) return '/'\n // `WebRequestShim` already reduces the WHATWG `Request.url` to path+search,\n // but an ABSOLUTE url would otherwise be returned whole — so a skip prefix\n // like `/health` would never match `https://host/health`.\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(url)) {\n try {\n return new URL(url).pathname || '/'\n } catch {\n return '/'\n }\n }\n const q = url.indexOf('?')\n return (q === -1 ? url : url.slice(0, q)) || '/'\n}\n"],"mappings":";;;;;;;;;;AAsCA,SAAS,SAAS,MAAoC,CACpD,OAAO,OAAO,OAAU,UAAY,MAAM,OAAS,EAAI,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,CAAE,KAAK,EAAI,IAAA,EACvF,CAGA,SAAS,YACP,QACA,KACS,CACJ,WAIL,OADI,OAAQ,QAAoB,KAAQ,WAAoB,QAAoB,IAAI,IAAI,EAChF,QAA0D,KACpE,CAgBA,SAAgB,gBAAgB,IAA4C,CAI1E,GAAI,OAAO,IAAI,IAAO,UAAY,IAAI,GAAG,OAAS,EAAG,OAAO,IAAI,GAWhE,IAAM,cAAgB,IAAI,QAAQ,cAClC,GAAI,cAAe,OAAO,cAK1B,IAAM,EAAI,IAAI,QACd,OACE,SAAS,YAAY,EAAG,kBAAkB,CAAC,GAC3C,SAAS,YAAY,EAAG,iBAAiB,CAAC,GAC1C,SAAS,YAAY,EAAG,WAAW,CAAC,CAExC,CAGA,SAAgB,gBAAgB,IAAgC,CAC9D,GAAI,OAAO,IAAI,MAAS,UAAY,IAAI,KAAK,OAAS,EAAG,OAAO,IAAI,KACpE,IAAM,IAAM,IAAI,IAChB,GAAI,CAAC,IAAK,MAAO,IAIjB,GAAI,2BAA2B,KAAK,GAAG,EACrC,GAAI,CACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC,UAAY,GAClC,MAAQ,CACN,MAAO,GACT,CAEF,IAAM,EAAI,IAAI,QAAQ,GAAG,EACzB,OAAQ,IAAM,GAAK,IAAM,IAAI,MAAM,EAAG,CAAC,IAAM,GAC/C"}
|
package/dist/config/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v9.0.
|
|
2
|
+
* @forinda/kickjs v9.0.2
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{a as loadEnvFromSchema,c as withEnv,i as loadEnv,n as defineEnv,o as reloadEnv,r as getEnv,s as resetEnvCache,t as baseEnvSchema}from"../env-
|
|
11
|
+
import{a as loadEnvFromSchema,c as withEnv,i as loadEnv,n as defineEnv,o as reloadEnv,r as getEnv,s as resetEnvCache,t as baseEnvSchema}from"../env-Cj-U1sin.mjs";import{n as createConfigService,t as ConfigService}from"../config-Dz5GgLKS.mjs";export{ConfigService,baseEnvSchema,createConfigService,defineEnv,getEnv,loadEnv,loadEnvFromSchema,reloadEnv,resetEnvCache,withEnv};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v9.0.
|
|
2
|
+
* @forinda/kickjs v9.0.2
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{i as loadEnv,o as reloadEnv}from"./env-
|
|
12
|
-
//# sourceMappingURL=config-
|
|
11
|
+
import{i as loadEnv,o as reloadEnv}from"./env-Cj-U1sin.mjs";import{Service}from"./core/decorators.mjs";import{t as __decorate}from"./decorate-CnpOo_Rp.mjs";let ConfigService=class{get env(){return loadEnv()}get(key,fallback){return this.env[key]??fallback}getAll(){return Object.freeze({...this.env})}reload(){reloadEnv(),loadEnv()}isProduction(){return this.env.NODE_ENV===`production`}isDevelopment(){return this.env.NODE_ENV===`development`}isTest(){return this.env.NODE_ENV===`test`}};ConfigService=__decorate([Service()],ConfigService);function createConfigService(schema){let SchemaConfigService=class{get env(){return loadEnv(schema)}get(key,fallback){return this.env[key]??fallback}getAll(){return Object.freeze({...this.env})}reload(){reloadEnv(),loadEnv(schema)}isProduction(){return this.env.NODE_ENV===`production`}isDevelopment(){return this.env.NODE_ENV===`development`}isTest(){return this.env.NODE_ENV===`test`}};return SchemaConfigService=__decorate([Service()],SchemaConfigService),SchemaConfigService}export{createConfigService as n,ConfigService as t};
|
|
12
|
+
//# sourceMappingURL=config-Dz5GgLKS.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-
|
|
1
|
+
{"version":3,"file":"config-Dz5GgLKS.mjs","names":[],"sources":["../src/config/config-service.ts"],"sourcesContent":["import type { z } from 'zod'\nimport { Service } from '../core/decorators'\nimport type { EnvKey } from '../core/decorators'\nimport { loadEnv, reloadEnv } from './env'\n\n/**\n * Constraint that locks the `key` argument to known `EnvKey` literals\n * once `kick typegen` has populated `KickEnv`. Mirrors the same trick\n * used by the `@Value` decorator: when `EnvKey` resolves to `never`\n * (no schema yet), accept any string for back-compat; once typegen\n * has run, require a known key.\n */\ntype ConfigKey<K extends string> = [EnvKey] extends [never] ? K : K & EnvKey\n\n/**\n * Resolve the return type for a `ConfigService.get(key)` call. When\n * `KickEnv` is populated and the key is a known one, returns the\n * schema-inferred type. Otherwise falls back to the caller-provided\n * generic `T` (default `any`) so existing call sites keep working.\n */\ntype ConfigValue<K extends string, T> = K extends EnvKey ? KickEnv[K] : T\n\n/**\n * Injectable service for accessing typed environment configuration.\n *\n * Once `kick typegen` has run against your `src/env.ts`, both `get()`\n * and `getAll()` are fully typed against the schema with **no extra\n * setup** — you just inject `ConfigService` and call it:\n *\n * ```ts\n * @Service()\n * class DatabaseService {\n * @Autowired() private readonly config!: ConfigService\n *\n * connect() {\n * const url = this.config.get('DATABASE_URL') // typed string\n * const port = this.config.get('PORT') // typed number\n * // const bad = this.config.get('NOPE') // tsc error\n * }\n * }\n * ```\n *\n * Without typegen the previous `<T = any>` generic still works, so\n * legacy code calling `config.get<string>('DATABASE_URL')` keeps\n * compiling.\n */\n@Service()\nexport class ConfigService {\n /**\n * Read the current parsed env on every access. We deliberately do **not**\n * snapshot in the constructor: if `ConfigService` is instantiated before\n * the user's `defineEnv()` schema has been registered (e.g. DI wires it\n * eagerly, or HMR's `reloadEnv()` cleared the cache between rebuilds),\n * an eager snapshot would freeze the base shape and never see the\n * extended values — even though `@Value()` (which reads lazily through\n * `Container._envResolver`) would resolve them correctly.\n *\n * `loadEnv()` is sticky: once an extended schema has been parsed it\n * keeps returning that schema's values, so this getter is cheap (one\n * cached lookup) and always consistent with `@Value`.\n */\n private get env(): Record<string, any> {\n return loadEnv() as Record<string, any>\n }\n\n /**\n * Get an env variable by key.\n *\n * - When `kick typegen` has run, `key` is constrained to known\n * `KickEnv` keys and the return type is inferred from the schema.\n * - When `KickEnv` is empty (no typegen run yet), any string is\n * accepted and `T` falls back to the explicit generic for\n * back-compat.\n */\n get<K extends string, T = any>(key: ConfigKey<K>): ConfigValue<K, T>\n /** With a fallback, returned when the value is unset (`undefined` or `null`). */\n get<K extends string, T = any>(\n key: ConfigKey<K>,\n fallback: NonNullable<ConfigValue<K, T>>,\n ): NonNullable<ConfigValue<K, T>>\n get(key: string, fallback?: unknown): unknown {\n return this.env[key] ?? fallback\n }\n\n /**\n * Get all env config as a readonly object. When `KickEnv` is\n * populated by typegen, returns `Readonly<KickEnv>` so the caller\n * gets autocomplete on every key.\n */\n getAll(): [EnvKey] extends [never] ? Readonly<Record<string, any>> : Readonly<KickEnv> {\n return Object.freeze({ ...this.env }) as any\n }\n\n /** Reload env from .env file (for HMR / file-watch scenarios) */\n reload(): void {\n reloadEnv()\n // Touch loadEnv() so the resolver + cache are repopulated immediately.\n loadEnv()\n }\n\n isProduction(): boolean {\n return this.env.NODE_ENV === 'production'\n }\n\n isDevelopment(): boolean {\n return this.env.NODE_ENV === 'development'\n }\n\n isTest(): boolean {\n return this.env.NODE_ENV === 'test'\n }\n}\n\n/**\n * Typed config service interface — provides autocomplete for env keys.\n * Use with `createConfigService()` for full type safety.\n */\nexport interface TypedConfigService<TEnv extends Record<string, any>> {\n get<K extends string & keyof TEnv>(key: K): TEnv[K]\n get<K extends string & keyof TEnv>(key: K, fallback: NonNullable<TEnv[K]>): NonNullable<TEnv[K]>\n getAll(): Readonly<TEnv>\n reload(): void\n isProduction(): boolean\n isDevelopment(): boolean\n isTest(): boolean\n}\n\n/**\n * Create a typed ConfigService class bound to a specific Zod env schema.\n * The returned class is DI-injectable via `@Service()` and provides\n * fully typed `get()` with key autocomplete.\n *\n * @example\n * ```ts\n * const envSchema = defineEnv((base) =>\n * base.extend({\n * DATABASE_URL: z.string().url(),\n * JWT_SECRET: z.string().min(32),\n * })\n * )\n *\n * // Create a typed, injectable config service\n * export const AppConfigService = createConfigService(envSchema)\n * export type AppConfigService = InstanceType<typeof AppConfigService>\n *\n * // In a controller or service — inject it:\n * @Controller()\n * class UserController {\n * constructor(@Inject(AppConfigService) private config: AppConfigService) {\n * const dbUrl = config.get('DATABASE_URL') // string — autocompletes!\n * const bad = config.get('NOPE') // TS error\n * }\n * }\n * ```\n */\nexport function createConfigService<T extends z.ZodObject<any>>(schema: T) {\n type TEnv = z.infer<T>\n\n @Service()\n class SchemaConfigService implements TypedConfigService<TEnv> {\n /** See note on `ConfigService.env` — same eager-snapshot hazard. */\n private get env(): TEnv {\n return loadEnv(schema)\n }\n\n get<K extends string & keyof TEnv>(key: K): TEnv[K]\n get<K extends string & keyof TEnv>(key: K, fallback: NonNullable<TEnv[K]>): NonNullable<TEnv[K]>\n get(key: string, fallback?: unknown): unknown {\n return (this.env as Record<string, unknown>)[key] ?? fallback\n }\n\n getAll(): Readonly<TEnv> {\n return Object.freeze({ ...this.env })\n }\n\n reload(): void {\n reloadEnv()\n loadEnv(schema)\n }\n\n isProduction(): boolean {\n return this.env.NODE_ENV === 'production'\n }\n\n isDevelopment(): boolean {\n return this.env.NODE_ENV === 'development'\n }\n\n isTest(): boolean {\n return this.env.NODE_ENV === 'test'\n }\n }\n\n return SchemaConfigService as {\n new (): TypedConfigService<TEnv>\n }\n}\n"],"mappings":";;;;;;;;;;4JA+CO,IAAM,cAAN,KAAoB,CAczB,IAAY,KAA2B,CACrC,OAAO,QAAQ,CACjB,CAiBA,IAAI,IAAa,SAA6B,CAC5C,OAAO,KAAK,IAAI,MAAQ,QAC1B,CAOA,QAAuF,CACrF,OAAO,OAAO,OAAO,CAAE,GAAG,KAAK,GAAI,CAAC,CACtC,CAGA,QAAe,CACb,UAAU,EAEV,QAAQ,CACV,CAEA,cAAwB,CACtB,OAAO,KAAK,IAAI,WAAa,YAC/B,CAEA,eAAyB,CACvB,OAAO,KAAK,IAAI,WAAa,aAC/B,CAEA,QAAkB,CAChB,OAAO,KAAK,IAAI,WAAa,MAC/B,CACF,EAjEC,cAAA,WAAA,CAAA,QAAQ,CAAA,EAAA,aAAA,EA6GT,SAAgB,oBAAgD,OAAW,CAGzE,IACM,oBADN,KAC8D,CAE5D,IAAY,KAAY,CACtB,OAAO,QAAQ,MAAM,CACvB,CAIA,IAAI,IAAa,SAA6B,CAC5C,OAAQ,KAAK,IAAgC,MAAQ,QACvD,CAEA,QAAyB,CACvB,OAAO,OAAO,OAAO,CAAE,GAAG,KAAK,GAAI,CAAC,CACtC,CAEA,QAAe,CACb,UAAU,EACV,QAAQ,MAAM,CAChB,CAEA,cAAwB,CACtB,OAAO,KAAK,IAAI,WAAa,YAC/B,CAEA,eAAyB,CACvB,OAAO,KAAK,IAAI,WAAa,aAC/B,CAEA,QAAkB,CAChB,OAAO,KAAK,IAAI,WAAa,MAC/B,CACF,EAEA,MAnCC,qBAAA,WAAA,CAAA,QAAQ,CAAA,EAAA,mBAAA,EAmCF,mBAGT"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v9.0.
|
|
2
|
+
* @forinda/kickjs v9.0.2
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{METADATA}from"./core/interfaces.mjs";import{createLogger}from"./core/logger.mjs";import{i as getMetaRecord,n as getClassMetaOrUndefined,o as getMethodMetaOrUndefined,r as getMetaMap,t as getClassMeta}from"./metadata-
|
|
11
|
+
import{METADATA}from"./core/interfaces.mjs";import{createLogger}from"./core/logger.mjs";import{i as getMetaRecord,n as getClassMetaOrUndefined,o as getMethodMetaOrUndefined,r as getMetaMap,t as getClassMeta}from"./metadata-DrCyWXgz.mjs";import"reflect-metadata";let impl=null;function _setAssetResolver(fn){impl=fn}function resolveAssetViaSlot(namespace,key){if(!impl)throw Error(`@Asset('${namespace}/${key}'): the asset manager is not loaded. Import it (any import from '@forinda/kickjs' loads it) — note @Asset reads from the filesystem and is not supported on edge runtimes.`);return impl(namespace,key)}var KickError=class extends Error{code;summary;cause;fix;docsUrl;context;constructor(init){super(formatKickError(init,{color:!1})),this.name=`KickError`,this.code=init.code,this.summary=init.summary,init.cause!==void 0&&(this.cause=init.cause),init.fix!==void 0&&(this.fix=init.fix),init.docsUrl!==void 0&&(this.docsUrl=init.docsUrl),init.context!==void 0&&(this.context=init.context)}};function ansiEnabled(){return process.env.NO_COLOR?!1:process.env.FORCE_COLOR?!0:process.stderr?.isTTY===!0}function paint(text,code,enabled){return enabled?`[${code}m${text}[0m`:text}const c={red:(s,enabled)=>paint(s,`31`,enabled),bold:(s,enabled)=>paint(s,`1`,enabled),dim:(s,enabled)=>paint(s,`2`,enabled),cyan:(s,enabled)=>paint(s,`36`,enabled),blue:(s,enabled)=>paint(s,`34`,enabled),underline:(s,enabled)=>paint(s,`4`,enabled)};function formatKickError(err,options={}){let color=options.color??ansiEnabled(),lines=[],code=c.bold(c.red(err.code,color),color);if(lines.push(`${code}: ${err.summary}`),err.cause){lines.push(``,` ${c.bold(c.dim(`Cause:`,color),color)}`);for(let line of err.cause.split(`
|
|
12
12
|
`))lines.push(` ${highlightInline(line,color)}`)}if(err.fix){lines.push(``,` ${c.bold(c.dim(`Fix:`,color),color)}`);for(let line of err.fix.split(`
|
|
13
13
|
`))lines.push(` ${highlightInline(line,color)}`)}return err.docsUrl&&(lines.push(``,` ${c.bold(c.dim(`Docs:`,color),color)}`),lines.push(` ${c.underline(c.blue(err.docsUrl,color),color)}`)),lines.join(`
|
|
14
14
|
`)}function highlightInline(line,color){return color?line.replace(/`([^`]+)`/g,(_,inner)=>`\`${c.cyan(inner,!0)}\``):line}const DOCS_BASE=`https://kickjs.app`;function noProviderError(tokenName){return new KickError({code:`KICK001`,summary:`No provider for ${tokenName}`,cause:`\`${tokenName}\` was requested from the DI container but no binding is registered.
|
|
@@ -129,4 +129,4 @@ requests would hang instead of being answered.`,fix:`Pick one:
|
|
|
129
129
|
|
|
130
130
|
Or remove \`http2\` if TLS is terminated in front of the app.`,docsUrl:`${DOCS_BASE}/guide/http-runtimes#https-and-http-2`})}function tlsCredentialsError(problem){return new KickError({code:`KICK009`,summary:`server.tls is not usable: ${problem}`,cause:`\`bootstrap({ server: { tls } })\` was given TLS options the server cannot use:
|
|
131
131
|
${problem}. Node would start anyway and fail every TLS handshake.`,fix:"Give the server an identity:\n • `key` and `cert` together (PEM, matching each other), or\n • `pfx` (with `passphrase` if it is encrypted), or\n • `SNICallback` / `pskCallback` for per-host or PSK setups.\n\n server: { tls: { key: readFileSync('key.pem'), cert: readFileSync('cert.pem') } }",docsUrl:`${DOCS_BASE}/guide/http-runtimes#https-and-http-2`,context:{problem}})}const INJECTION_TOKEN=Symbol.for(`@kickjs/InjectionToken`);function createToken(name){return Object.freeze({[INJECTION_TOKEN]:!0,name})}function isInjectionToken(value){return typeof value==`object`&&!!value&&value[INJECTION_TOKEN]===!0}const log=createLogger(`Container`);function createReg(fields){return{kind:`unknown`,resolveCount:0,postConstructStatus:`skipped`,dependencies:[],...fields}}function tokenName(token){return typeof token==`symbol`?token.toString():isInjectionToken(token)?token.name:token?.name||String(token)}function getPersistentStore(){return globalThis.__kickjs_persistent||(globalThis.__kickjs_persistent={factories:[],instances:[],resolvedInstances:new Map}),globalThis.__kickjs_persistent}var Container=class Container{static instance;registrations=new Map;resolving=new Set;changeListeners=new Set;pendingChanges=[];notifyTimer=null;static DEBOUNCE_MS=50;static _onReady=null;static _onReset=null;static _onCreate=null;static _envResolver=null;isGlobal=!1;static _requestStoreProvider=null;static getInstance(){return Container.instance||(Container.instance=new Container,Container.instance.isGlobal=!0),Container._onReady&&=(Container._onReady(Container.instance),null),Container.instance}static reset(){Container.instance=new Container,Container.instance.isGlobal=!0,Container._onReset?.(Container.instance);let store=getPersistentStore();for(let{token,factory,scope}of store.factories)if(!Container.instance.has(token)){let name=tokenName(token),existing=store.resolvedInstances.get(name);Container.instance.registrations.set(token,createReg({target:Object,scope,factory,kind:`factory`,instance:existing,resolveCount:+!!existing,postConstructStatus:existing?`completed`:`skipped`}))}for(let{token,instance}of store.instances)Container.instance.has(token)||Container.instance.registrations.set(token,createReg({target:instance.constructor,scope:`singleton`,instance,kind:`instance`,postConstructStatus:`completed`,resolveCount:1}))}static create(){let container=new Container;return Container._onCreate?.(container),container}register(token,target,scope=`singleton`){let kind=getClassMeta(METADATA.CLASS_KIND,target,`unknown`),reg=createReg({target,scope,kind,dependencies:this.extractDependencies(target)});this.registrations.set(token,reg),typeof token==`function`&&token.name&&this.registrations.set(`__hmr__${token.name}`,reg),this.emit(token,`registered`,kind)}registerFactory(token,factory,scope=`singleton`){let store=getPersistentStore(),name=tokenName(token),factoryUnchanged=(this.isGlobal?store.factories.find(r=>tokenName(r.token)===name):void 0)?.factory===factory,existingInstance=this.isGlobal&&factoryUnchanged?store.resolvedInstances.get(name):void 0;if(this.isGlobal&&!factoryUnchanged&&store.resolvedInstances.delete(name),this.registrations.set(token,createReg({target:Object,scope,factory,kind:`factory`,instance:existingInstance,resolveCount:+!!existingInstance,postConstructStatus:existingInstance?`completed`:`skipped`})),this.isGlobal){let idx=store.factories.findIndex(r=>tokenName(r.token)===name);idx>=0?store.factories[idx]={token,factory,scope}:store.factories.push({token,factory,scope})}this.emit(name,`registered`,`factory`)}registerInstance(token,instance){let store=getPersistentStore(),name=tokenName(token);if(this.registrations.set(token,createReg({target:instance.constructor,scope:`singleton`,instance,kind:`instance`,postConstructStatus:`completed`,resolveCount:1,firstResolvedAt:Date.now(),lastResolvedAt:Date.now()})),this.isGlobal){store.resolvedInstances.set(name,instance);let idx=store.instances.findIndex(r=>tokenName(r.token)===name);idx>=0?store.instances[idx]={token,instance}:store.instances.push({token,instance})}this.emit(name,`registered`,`instance`)}has(token){return this.registrations.has(token)}getRegistrations(){let entries=[];for(let[token,reg]of this.registrations)entries.push({token:tokenName(token),scope:reg.scope,kind:reg.kind,instantiated:reg.scope===`singleton`?reg.instance!==void 0:reg.resolveCount>0,resolveCount:reg.resolveCount,lastResolvedAt:reg.lastResolvedAt,firstResolvedAt:reg.firstResolvedAt,resolveDurationMs:reg.resolveDurationMs,postConstructStatus:reg.postConstructStatus,dependencies:reg.dependencies});return entries}resolve(token){let reg=this.registrations.get(token);if(!reg&&typeof token==`function`&&token.name&&(reg=this.registrations.get(`__hmr__${token.name}`)),!reg)throw noProviderError(tokenName(token));if(reg.scope===`singleton`&®.instance!==void 0)return reg.resolveCount++,reg.lastResolvedAt=Date.now(),this.emit(token,`resolved`,reg.kind),reg.instance;if(reg.scope===`request`){if(!Container._requestStoreProvider)throw requestScopeMiddlewareMissingError(tokenName(token));let store=Container._requestStoreProvider();if(!store)throw requestScopeOutsideRequestError(tokenName(token));if(store.values.has(token))return reg.resolveCount++,reg.lastResolvedAt=Date.now(),this.emit(token,`resolved`,reg.kind),store.values.get(token);if(store.instances.has(token))return reg.resolveCount++,reg.lastResolvedAt=Date.now(),this.emit(token,`resolved`,reg.kind),store.instances.get(token);if(reg.factory){let start=performance.now(),instance=reg.factory();return reg.resolveDurationMs=performance.now()-start,reg.resolveCount++,reg.lastResolvedAt=Date.now(),reg.firstResolvedAt||(reg.firstResolvedAt=Date.now()),store.instances.set(token,instance),this.emit(token,`resolved`,reg.kind),instance}let start=performance.now(),instance=this.createInstance(reg);return reg.resolveDurationMs=performance.now()-start,reg.resolveCount++,reg.lastResolvedAt=Date.now(),reg.firstResolvedAt||(reg.firstResolvedAt=Date.now()),store.instances.set(token,instance),this.emit(token,`resolved`,reg.kind),instance}if(reg.factory){let start=performance.now(),instance=reg.factory();return reg.resolveDurationMs=performance.now()-start,reg.resolveCount++,reg.lastResolvedAt=Date.now(),reg.firstResolvedAt||(reg.firstResolvedAt=Date.now()),reg.scope===`singleton`&&(reg.instance=instance,this.isGlobal&&getPersistentStore().resolvedInstances.set(tokenName(token),instance)),this.emit(token,`resolved`,reg.kind),instance}if(this.resolving.has(token)){let chain=[...this.resolving].map(tokenName);throw chain.push(tokenName(token)),Error(`Circular dependency detected: ${chain.join(` -> `)}`)}this.resolving.add(token);try{let start=performance.now(),instance=this.createInstance(reg);return reg.resolveDurationMs=performance.now()-start,reg.resolveCount++,reg.lastResolvedAt=Date.now(),reg.firstResolvedAt||(reg.firstResolvedAt=Date.now()),reg.scope===`singleton`&&(reg.instance=instance),this.emit(token,`resolved`,reg.kind),instance}finally{this.resolving.delete(token)}}bootstrap(){}onChange(callback){return this.changeListeners.add(callback),()=>this.changeListeners.delete(callback)}invalidate(token){let name=tokenName(token),reg=this.registrations.get(token)??this.registrations.get(`__hmr__${name}`);if(reg&&!getPersistentStore().resolvedInstances.has(name)){reg.instance=void 0,reg.resolveCount=0,reg.postConstructStatus=`skipped`,this.emit(name,`invalidated`,reg.kind);for(let[depToken,depReg]of this.registrations)depReg.dependencies.includes(name)&&this.invalidate(depToken)}}emit(rawToken,event,kind){if(this.changeListeners.size===0)return;let token=tokenName(rawToken),existing=this.pendingChanges.find(p=>p.token===token&&p.event===event);existing?existing.timestamp=Date.now():this.pendingChanges.push({token,event,kind,timestamp:Date.now()}),this.notifyTimer&&clearTimeout(this.notifyTimer),this.notifyTimer=setTimeout(()=>{let batch=this.pendingChanges.splice(0);if(batch.length!==0)for(let listener of this.changeListeners)try{listener(batch)}catch{}},Container.DEBOUNCE_MS),this.notifyTimer.unref?.()}flushChanges(){this.notifyTimer&&=(clearTimeout(this.notifyTimer),null);let batch=this.pendingChanges.splice(0);if(batch.length!==0)for(let listener of this.changeListeners)try{listener(batch)}catch{}}buildPlan(reg){let paramTypes=getClassMeta(METADATA.PARAM_TYPES,reg.target,[]),injectTokens=getMetaRecord(METADATA.INJECT,reg.target),maxInjectIndex=Object.keys(injectTokens).length>0?Math.max(...Object.keys(injectTokens).map(Number))+1:0,paramCount=Math.max(paramTypes.length,maxInjectIndex),ctorTokens=[];for(let index=0;index<paramCount;index++){let token=injectTokens[index]||paramTypes[index];if(!token)throw Error(`Cannot resolve constructor parameter at index ${index} of ${tokenName(reg.target)}. No @Inject token and no design:paramtypes metadata (tsx/ts-node don't emit it). Add @Inject(Token) to every constructor parameter.`);if(reg.scope===`singleton`){let depReg=this.registrations.get(token);if(depReg&&depReg.scope===`request`){let parent=tokenName(reg.target),dep=tokenName(token),fix=getClassMetaOrUndefined(METADATA.CLASS_KIND,reg.target)===`controller`?`@Controller() is always SINGLETON, so inject "${dep}" with @Autowired() instead of the constructor.`:`Give "${parent}" TRANSIENT or REQUEST scope (e.g. @Service({ scope: Scope.REQUEST })), or inject "${dep}" with @Autowired() instead of the constructor.`;throw Error(`Cannot inject REQUEST-scoped "${dep}" into SINGLETON "${parent}". Singletons outlive requests. ${fix}`)}}ctorTokens.push(token)}let autowiredProps=getMetaMap(METADATA.AUTOWIRED,reg.target.prototype),autowired=[];for(let[prop,token]of autowiredProps){let resolvedToken=token||getMethodMetaOrUndefined(METADATA.PROPERTY_TYPE,reg.target.prototype,prop);resolvedToken&&autowired.push([prop,resolvedToken])}let values=[...getMetaMap(METADATA.VALUE,reg.target.prototype).entries()],assetProps=getMetaMap(METADATA.ASSET,reg.target.prototype),assets=[];for(let[prop,config]of assetProps){let slashIdx=config.assetKey.indexOf(`/`);if(slashIdx===-1)throw Error(`@Asset('${config.assetKey}'): asset key must include a '/' separator (e.g. 'mails/welcome').`);assets.push([prop,{namespace:config.assetKey.slice(0,slashIdx),key:config.assetKey.slice(slashIdx+1)}])}return{ctorTokens,autowired,values,assets,postConstruct:getClassMetaOrUndefined(METADATA.POST_CONSTRUCT,reg.target.prototype)}}createInstance(reg){let plan=reg.plan??=this.buildPlan(reg),args=[];for(let token of plan.ctorTokens)args.push(this.resolve(token));let instance=new reg.target(...args);this.injectProperties(instance,plan);let postConstruct=plan.postConstruct;if(postConstruct&&typeof instance[postConstruct]==`function`)try{let result=instance[postConstruct]();result&&typeof result.then==`function`?(reg.postConstructStatus=`pending`,result.then(()=>{reg.postConstructStatus=`completed`}).catch(()=>{reg.postConstructStatus=`failed`}),log.warn(`@PostConstruct on ${tokenName(reg.target)}.${String(postConstruct)}() returned a Promise but the container is synchronous. The async operation will not be awaited.`)):reg.postConstructStatus=`completed`}catch(err){reg.postConstructStatus=`failed`,log.error(err,`@PostConstruct on ${tokenName(reg.target)}.${String(postConstruct)}() threw an error`)}return instance}extractDependencies(target){let paramTypes=getClassMeta(METADATA.PARAM_TYPES,target,[]),injectTokens=getMetaRecord(METADATA.INJECT,target),ctorDeps=paramTypes.map((type,index)=>tokenName(injectTokens[index]||type)),autowiredProps=getMetaMap(METADATA.AUTOWIRED,target.prototype),propDeps=[];for(let[prop,token]of autowiredProps){let resolvedToken=token||getMethodMetaOrUndefined(METADATA.PROPERTY_TYPE,target.prototype,prop);resolvedToken&&propDeps.push(tokenName(resolvedToken))}return[...new Set([...ctorDeps,...propDeps])]}injectProperties(instance,plan){for(let[prop,resolvedToken]of plan.autowired)Object.defineProperty(instance,prop,{get:()=>{let value=this.resolve(resolvedToken);return this.registrations.get(resolvedToken)?.scope===`singleton`&&Object.defineProperty(instance,prop,{value,enumerable:!0,configurable:!0,writable:!1}),value},enumerable:!0,configurable:!0});for(let[prop,config]of plan.values)Object.defineProperty(instance,prop,{get(){if(Container._envResolver){let val=Container._envResolver(config.envKey);if(val!==void 0)return val}let val=process.env[config.envKey];if(val!==void 0)return val;if(config.defaultValue!==void 0)return config.defaultValue;throw envValueMissingError(config.envKey)},enumerable:!0,configurable:!0});for(let[prop,{namespace,key}]of plan.assets)Object.defineProperty(instance,prop,{get(){return resolveAssetViaSlot(namespace,key)},enumerable:!0,configurable:!0})}};export{isInjectionToken as a,http2UnsupportedRuntimeError as c,KickError as d,formatKickError as f,createToken as i,moduleRouteMissingControllerError as l,tokenName as n,duplicateRouteError as o,_setAssetResolver as p,INJECTION_TOKEN as r,http2RequiresTlsError as s,Container as t,tlsCredentialsError as u};
|
|
132
|
-
//# sourceMappingURL=container-
|
|
132
|
+
//# sourceMappingURL=container-DM3gPzGt.mjs.map
|