[{"data":1,"prerenderedAt":2724},["ShallowReactive",2],{"navigation":3,"docs-path-index":340,"-blog-announcing-1-4":446,"-blog-announcing-1-4-surround":2713},[4],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":64},"Docs","i-lucide-book-marked","\u002Fdocs","docs",[10,30,65,293,318],{"title":11,"path":12,"stem":13,"children":14,"icon":29},"Getting Started","\u002Fdocs\u002Fgetting-started","docs\u002F1.getting-started\u002F1.index",[15,17,21,25],{"title":16,"path":12,"stem":13},"Introduction",{"title":18,"path":19,"stem":20},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","docs\u002F1.getting-started\u002F2.installation",{"title":22,"path":23,"stem":24},"Examples","\u002Fdocs\u002Fgetting-started\u002Fexamples","docs\u002F1.getting-started\u002F3.examples",{"title":26,"path":27,"stem":28},"Project Generator","\u002Fdocs\u002Fgetting-started\u002Fproject-generator","docs\u002F1.getting-started\u002F4.project-generator","i-lucide-rocket",{"title":31,"icon":32,"path":33,"stem":34,"children":35,"page":64},"Core features","i-lucide-square-function","\u002Fdocs\u002Fcore-features","docs\u002F2.core-features",[36,40,44,48,52,56,60],{"title":37,"path":38,"stem":39},"Configuration","\u002Fdocs\u002Fcore-features\u002Fconfiguration","docs\u002F2.core-features\u002F0.configuration",{"title":41,"path":42,"stem":43},"Dependency Injection","\u002Fdocs\u002Fcore-features\u002Fcontainer","docs\u002F2.core-features\u002F1.container",{"title":45,"path":46,"stem":47},"Controllers","\u002Fdocs\u002Fcore-features\u002Fcontrollers","docs\u002F2.core-features\u002F2.controllers",{"title":49,"path":50,"stem":51},"Middlewares","\u002Fdocs\u002Fcore-features\u002Fmiddlewares","docs\u002F2.core-features\u002F3.middlewares",{"title":53,"path":54,"stem":55},"Validation","\u002Fdocs\u002Fcore-features\u002Fvalidation","docs\u002F2.core-features\u002F4.validation",{"title":57,"path":58,"stem":59},"Introspection","\u002Fdocs\u002Fcore-features\u002Fintrospection","docs\u002F2.core-features\u002F5.introspection",{"title":61,"path":62,"stem":63},"Health Checks","\u002Fdocs\u002Fcore-features\u002Fhealth","docs\u002F2.core-features\u002F6.health",false,{"title":66,"icon":67,"defaultOpen":68,"path":69,"stem":70,"children":71,"page":64},"Modules","i-lucide-boxes",true,"\u002Fdocs\u002Fmodules","docs\u002F3.modules",[72,89,100,116,135,154,177,191,206,214,229,240,255,274,282],{"title":73,"icon":64,"defaultOpen":64,"path":74,"stem":75,"children":76,"page":64},"Auth","\u002Fdocs\u002Fmodules\u002Fauth","docs\u002F3.modules\u002F1.auth",[77,81,85],{"title":78,"path":79,"stem":80},"Overview","\u002Fdocs\u002Fmodules\u002Fauth\u002Foverview","docs\u002F3.modules\u002F1.auth\u002F0.overview",{"title":82,"path":83,"stem":84},"Decorators","\u002Fdocs\u002Fmodules\u002Fauth\u002Fdecorators","docs\u002F3.modules\u002F1.auth\u002F1.decorators",{"title":86,"path":87,"stem":88},"API Reference","\u002Fdocs\u002Fmodules\u002Fauth\u002Fapi","docs\u002F3.modules\u002F1.auth\u002F2.api",{"title":90,"icon":64,"defaultOpen":64,"path":91,"stem":92,"children":93,"page":64},"Devtools","\u002Fdocs\u002Fmodules\u002Fdevtools","docs\u002F3.modules\u002F10.devtools",[94,97],{"title":78,"path":95,"stem":96},"\u002Fdocs\u002Fmodules\u002Fdevtools\u002Foverview","docs\u002F3.modules\u002F10.devtools\u002F0.overview",{"title":37,"path":98,"stem":99},"\u002Fdocs\u002Fmodules\u002Fdevtools\u002Fconfiguration","docs\u002F3.modules\u002F10.devtools\u002F1.configuration",{"title":101,"icon":64,"defaultOpen":64,"path":102,"stem":103,"children":104,"page":64},"Telemetry","\u002Fdocs\u002Fmodules\u002Ftelemetry","docs\u002F3.modules\u002F11.telemetry",[105,108,112],{"title":78,"path":106,"stem":107},"\u002Fdocs\u002Fmodules\u002Ftelemetry\u002Foverview","docs\u002F3.modules\u002F11.telemetry\u002F0.overview",{"title":109,"path":110,"stem":111},"API","\u002Fdocs\u002Fmodules\u002Ftelemetry\u002Fapi","docs\u002F3.modules\u002F11.telemetry\u002F1.api",{"title":113,"path":114,"stem":115},"Instrumentation","\u002Fdocs\u002Fmodules\u002Ftelemetry\u002Finstrumentation","docs\u002F3.modules\u002F11.telemetry\u002F2.instrumentation",{"title":117,"icon":64,"defaultOpen":64,"path":118,"stem":119,"children":120,"page":64},"Queue","\u002Fdocs\u002Fmodules\u002Fqueue","docs\u002F3.modules\u002F12.queue",[121,124,128,132],{"title":78,"path":122,"stem":123},"\u002Fdocs\u002Fmodules\u002Fqueue\u002Foverview","docs\u002F3.modules\u002F12.queue\u002F0.overview",{"title":125,"path":126,"stem":127},"Consumers","\u002Fdocs\u002Fmodules\u002Fqueue\u002Fconsumers","docs\u002F3.modules\u002F12.queue\u002F1.consumers",{"title":129,"path":130,"stem":131},"Strategies","\u002Fdocs\u002Fmodules\u002Fqueue\u002Fstrategies","docs\u002F3.modules\u002F12.queue\u002F2.strategies",{"title":109,"path":133,"stem":134},"\u002Fdocs\u002Fmodules\u002Fqueue\u002Fapi","docs\u002F3.modules\u002F12.queue\u002F3.api",{"title":136,"icon":64,"defaultOpen":64,"path":137,"stem":138,"children":139,"page":64},"Scheduler","\u002Fdocs\u002Fmodules\u002Fscheduler","docs\u002F3.modules\u002F13.scheduler",[140,143,147,151],{"title":78,"path":141,"stem":142},"\u002Fdocs\u002Fmodules\u002Fscheduler\u002Foverview","docs\u002F3.modules\u002F13.scheduler\u002F0.overview",{"title":144,"path":145,"stem":146},"Jobs","\u002Fdocs\u002Fmodules\u002Fscheduler\u002Fjobs","docs\u002F3.modules\u002F13.scheduler\u002F1.jobs",{"title":148,"path":149,"stem":150},"Clustering","\u002Fdocs\u002Fmodules\u002Fscheduler\u002Fclustering","docs\u002F3.modules\u002F13.scheduler\u002F2.clustering",{"title":109,"path":152,"stem":153},"\u002Fdocs\u002Fmodules\u002Fscheduler\u002Fapi","docs\u002F3.modules\u002F13.scheduler\u002F3.api",{"title":155,"icon":64,"defaultOpen":64,"path":156,"stem":157,"children":158,"page":64},"Testing","\u002Fdocs\u002Fmodules\u002Ftesting","docs\u002F3.modules\u002F14.testing",[159,162,166,170,174],{"title":78,"path":160,"stem":161},"\u002Fdocs\u002Fmodules\u002Ftesting\u002Foverview","docs\u002F3.modules\u002F14.testing\u002F0.overview",{"title":163,"path":164,"stem":165},"Requests","\u002Fdocs\u002Fmodules\u002Ftesting\u002Frequests","docs\u002F3.modules\u002F14.testing\u002F1.requests",{"title":167,"path":168,"stem":169},"Dependencies","\u002Fdocs\u002Fmodules\u002Ftesting\u002Fdependencies","docs\u002F3.modules\u002F14.testing\u002F2.dependencies",{"title":171,"path":172,"stem":173},"Extensions","\u002Fdocs\u002Fmodules\u002Ftesting\u002Fextensions","docs\u002F3.modules\u002F14.testing\u002F3.extensions",{"title":109,"path":175,"stem":176},"\u002Fdocs\u002Fmodules\u002Ftesting\u002Fapi","docs\u002F3.modules\u002F14.testing\u002F4.api",{"title":178,"icon":64,"defaultOpen":64,"path":179,"stem":180,"children":181,"page":64},"Client","\u002Fdocs\u002Fmodules\u002Fclient","docs\u002F3.modules\u002F15.client",[182,185,188],{"title":78,"path":183,"stem":184},"\u002Fdocs\u002Fmodules\u002Fclient\u002Foverview","docs\u002F3.modules\u002F15.client\u002F0.overview",{"title":163,"path":186,"stem":187},"\u002Fdocs\u002Fmodules\u002Fclient\u002Frequests","docs\u002F3.modules\u002F15.client\u002F1.requests",{"title":109,"path":189,"stem":190},"\u002Fdocs\u002Fmodules\u002Fclient\u002Fapi","docs\u002F3.modules\u002F15.client\u002F2.api",{"title":192,"icon":64,"defaultOpen":64,"path":193,"stem":194,"children":195,"page":64},"Logger","\u002Fdocs\u002Fmodules\u002Flogger","docs\u002F3.modules\u002F2.logger",[196,199,203],{"title":78,"path":197,"stem":198},"\u002Fdocs\u002Fmodules\u002Flogger\u002Foverview","docs\u002F3.modules\u002F2.logger\u002F0.overview",{"title":200,"path":201,"stem":202},"Drains & Adapters","\u002Fdocs\u002Fmodules\u002Flogger\u002Fdrivers","docs\u002F3.modules\u002F2.logger\u002F1.drivers",{"title":109,"path":204,"stem":205},"\u002Fdocs\u002Fmodules\u002Flogger\u002Fapi","docs\u002F3.modules\u002F2.logger\u002F2.api",{"title":207,"icon":64,"defaultOpen":64,"path":208,"stem":209,"children":210,"page":64},"Serverless","\u002Fdocs\u002Fmodules\u002Fserverless","docs\u002F3.modules\u002F3.serverless",[211],{"title":78,"path":212,"stem":213},"\u002Fdocs\u002Fmodules\u002Fserverless\u002Foverview","docs\u002F3.modules\u002F3.serverless\u002F0.overview",{"title":215,"icon":64,"defaultOpen":64,"path":216,"stem":217,"children":218,"page":64},"Storage","\u002Fdocs\u002Fmodules\u002Fstorage","docs\u002F3.modules\u002F4.storage",[219,222,226],{"title":78,"path":220,"stem":221},"\u002Fdocs\u002Fmodules\u002Fstorage\u002Foverview","docs\u002F3.modules\u002F4.storage\u002F0.overview",{"title":223,"path":224,"stem":225},"Drivers","\u002Fdocs\u002Fmodules\u002Fstorage\u002Fdrivers","docs\u002F3.modules\u002F4.storage\u002F1.drivers",{"title":109,"path":227,"stem":228},"\u002Fdocs\u002Fmodules\u002Fstorage\u002Fapi","docs\u002F3.modules\u002F4.storage\u002F2.api",{"title":230,"icon":64,"defaultOpen":64,"path":231,"stem":232,"children":233,"page":64},"Web Sockets","\u002Fdocs\u002Fmodules\u002Fweb-sockets","docs\u002F3.modules\u002F5.web-sockets",[234,237],{"title":78,"path":235,"stem":236},"\u002Fdocs\u002Fmodules\u002Fweb-sockets\u002Foverview","docs\u002F3.modules\u002F5.web-sockets\u002F0.overview",{"title":109,"path":238,"stem":239},"\u002Fdocs\u002Fmodules\u002Fweb-sockets\u002Fapi","docs\u002F3.modules\u002F5.web-sockets\u002F1.api",{"title":241,"icon":64,"defaultOpen":64,"path":242,"stem":243,"children":244,"page":64},"Schema","\u002Fdocs\u002Fmodules\u002Fschema","docs\u002F3.modules\u002F6.schema",[245,248,251],{"title":78,"path":246,"stem":247},"\u002Fdocs\u002Fmodules\u002Fschema\u002Foverview","docs\u002F3.modules\u002F6.schema\u002F0.overview",{"title":82,"path":249,"stem":250},"\u002Fdocs\u002Fmodules\u002Fschema\u002Fdecorators","docs\u002F3.modules\u002F6.schema\u002F1.decorators",{"title":252,"path":253,"stem":254},"Scalar","\u002Fdocs\u002Fmodules\u002Fschema\u002Fscalar","docs\u002F3.modules\u002F6.schema\u002F2.scalar",{"title":256,"icon":64,"defaultOpen":64,"path":257,"stem":258,"children":259,"page":64},"Nitro","\u002Fdocs\u002Fmodules\u002Fnitro","docs\u002F3.modules\u002F7.nitro",[260,263,266,270],{"title":78,"path":261,"stem":262},"\u002Fdocs\u002Fmodules\u002Fnitro\u002Foverview","docs\u002F3.modules\u002F7.nitro\u002F0.overview",{"title":215,"path":264,"stem":265},"\u002Fdocs\u002Fmodules\u002Fnitro\u002Fstorage","docs\u002F3.modules\u002F7.nitro\u002F1.storage",{"title":267,"path":268,"stem":269},"File-Based Routes","\u002Fdocs\u002Fmodules\u002Fnitro\u002Ffile-based-routes","docs\u002F3.modules\u002F7.nitro\u002F2.file-based-routes",{"title":271,"path":272,"stem":273},"Middleware","\u002Fdocs\u002Fmodules\u002Fnitro\u002Fmiddleware","docs\u002F3.modules\u002F7.nitro\u002F3.middleware",{"title":275,"icon":64,"defaultOpen":64,"path":276,"stem":277,"children":278,"page":64},"Vite","\u002Fdocs\u002Fmodules\u002Fvite","docs\u002F3.modules\u002F8.vite",[279],{"title":78,"path":280,"stem":281},"\u002Fdocs\u002Fmodules\u002Fvite\u002Foverview","docs\u002F3.modules\u002F8.vite\u002F0.overview",{"title":283,"icon":64,"defaultOpen":64,"path":284,"stem":285,"children":286,"page":64},"Cache","\u002Fdocs\u002Fmodules\u002Fcache","docs\u002F3.modules\u002F9.cache",[287,290],{"title":78,"path":288,"stem":289},"\u002Fdocs\u002Fmodules\u002Fcache\u002Foverview","docs\u002F3.modules\u002F9.cache\u002F0.overview",{"title":109,"path":291,"stem":292},"\u002Fdocs\u002Fmodules\u002Fcache\u002Fapi","docs\u002F3.modules\u002F9.cache\u002F1.api",{"title":294,"icon":295,"path":296,"stem":297,"children":298,"page":64},"Deployment","i-lucide-cloud-upload","\u002Fdocs\u002Fdeployment","docs\u002F4.deployment",[299,302,306,310,314],{"title":78,"path":300,"stem":301},"\u002Fdocs\u002Fdeployment\u002Foverview","docs\u002F4.deployment\u002F0.overview",{"title":303,"path":304,"stem":305},"Vercel","\u002Fdocs\u002Fdeployment\u002Fvercel","docs\u002F4.deployment\u002F1.vercel",{"title":307,"path":308,"stem":309},"AWS Lambda","\u002Fdocs\u002Fdeployment\u002Faws-lambda","docs\u002F4.deployment\u002F2.aws-lambda",{"title":311,"path":312,"stem":313},"Azure Functions","\u002Fdocs\u002Fdeployment\u002Fazure-functions","docs\u002F4.deployment\u002F3.azure-functions",{"title":315,"path":316,"stem":317},"Node.js Server","\u002Fdocs\u002Fdeployment\u002Fnode-server","docs\u002F4.deployment\u002F4.node-server",{"title":319,"icon":320,"path":321,"stem":322,"children":323,"page":64},"Advanced","i-lucide-brain","\u002Fdocs\u002Fadvanced","docs\u002F99.advanced",[324,328,332,336],{"title":325,"path":326,"stem":327},"Custom Decorator","\u002Fdocs\u002Fadvanced\u002Fcustom-decorator","docs\u002F99.advanced\u002F1.custom-decorator",{"title":329,"path":330,"stem":331},"Plugins","\u002Fdocs\u002Fadvanced\u002Fcustom-plugin","docs\u002F99.advanced\u002F2.custom-plugin",{"title":333,"path":334,"stem":335},"Custom CLI Command","\u002Fdocs\u002Fadvanced\u002Fcustom-cli-command","docs\u002F99.advanced\u002F3.custom-cli-command",{"title":337,"path":338,"stem":339},"Writing a Recipe","\u002Fdocs\u002Fadvanced\u002Fwriting-a-recipe","docs\u002F99.advanced\u002F4.writing-a-recipe",[341,343,345,346,347,348,349,351,352,353,354,355,356,357,358,360,362,363,364,365,367,368,369,371,372,373,374,376,377,378,379,380,382,383,384,385,386,388,389,390,391,392,393,395,396,397,398,400,401,402,403,405,406,408,409,410,411,413,414,415,417,418,419,420,422,423,424,425,426,428,429,431,432,433,435,436,437,438,439,440,442,443,444,445],{"path":342},"\u002Fdocs\u002F.navigation",{"path":344},"\u002Fdocs\u002Fgetting-started\u002F.navigation",{"path":12},{"path":19},{"path":23},{"path":27},{"path":350},"\u002Fdocs\u002Fcore-features\u002F.navigation",{"path":38},{"path":42},{"path":46},{"path":50},{"path":54},{"path":58},{"path":62},{"path":359},"\u002Fdocs\u002Fmodules\u002F.navigation",{"path":361},"\u002Fdocs\u002Fmodules\u002Fauth\u002F.navigation",{"path":79},{"path":83},{"path":87},{"path":366},"\u002Fdocs\u002Fmodules\u002Fdevtools\u002F.navigation",{"path":95},{"path":98},{"path":370},"\u002Fdocs\u002Fmodules\u002Ftelemetry\u002F.navigation",{"path":106},{"path":110},{"path":114},{"path":375},"\u002Fdocs\u002Fmodules\u002Fqueue\u002F.navigation",{"path":122},{"path":126},{"path":130},{"path":133},{"path":381},"\u002Fdocs\u002Fmodules\u002Fscheduler\u002F.navigation",{"path":141},{"path":145},{"path":149},{"path":152},{"path":387},"\u002Fdocs\u002Fmodules\u002Ftesting\u002F.navigation",{"path":160},{"path":164},{"path":168},{"path":172},{"path":175},{"path":394},"\u002Fdocs\u002Fmodules\u002Fclient\u002F.navigation",{"path":183},{"path":186},{"path":189},{"path":399},"\u002Fdocs\u002Fmodules\u002Flogger\u002F.navigation",{"path":197},{"path":201},{"path":204},{"path":404},"\u002Fdocs\u002Fmodules\u002Fserverless\u002F.navigation",{"path":212},{"path":407},"\u002Fdocs\u002Fmodules\u002Fstorage\u002F.navigation",{"path":220},{"path":224},{"path":227},{"path":412},"\u002Fdocs\u002Fmodules\u002Fweb-sockets\u002F.navigation",{"path":235},{"path":238},{"path":416},"\u002Fdocs\u002Fmodules\u002Fschema\u002F.navigation",{"path":246},{"path":249},{"path":253},{"path":421},"\u002Fdocs\u002Fmodules\u002Fnitro\u002F.navigation",{"path":261},{"path":264},{"path":268},{"path":272},{"path":427},"\u002Fdocs\u002Fmodules\u002Fvite\u002F.navigation",{"path":280},{"path":430},"\u002Fdocs\u002Fmodules\u002Fcache\u002F.navigation",{"path":288},{"path":291},{"path":434},"\u002Fdocs\u002Fdeployment\u002F.navigation",{"path":300},{"path":304},{"path":308},{"path":312},{"path":316},{"path":441},"\u002Fdocs\u002Fadvanced\u002F.navigation",{"path":326},{"path":330},{"path":334},{"path":338},{"id":447,"title":448,"author":449,"body":455,"category":2703,"date":2704,"description":2705,"extension":2706,"image":2707,"meta":2708,"minRead":687,"navigation":68,"path":2709,"seo":2710,"shortImage":2707,"stem":2711,"__hash__":2712},"blog\u002Fblog\u002F4.announcing-1.4.md","Announcing Vercube 1.4",{"name":450,"avatar":451,"to":454,"twitter":450},"OskarLebuda",{"src":452,"alt":453},"https:\u002F\u002Fgithub.com\u002FOskarLebuda.png","Oskar Lebuda","https:\u002F\u002Fgithub.com\u002FOskarLebuda",{"type":456,"value":457,"toc":2695},"minimark",[458,485,489,510,1003,1006,1201,1219,1234,1249,1252,1317,1334,1337,1350,1679,1692,1709,1712,1755,1770,1901,1904,1923,1927,1942,2006,2017,2095,2106,2114,2118,2124,2226,2237,2248,2407,2418,2422,2466,2473,2480,2484,2493,2519,2522,2549,2552,2663,2671,2674,2691],[459,460,461,465,466,476,477,484],"p",{},[462,463,464],"strong",{},"Vercube 1.4 is out, and it is the biggest release so far."," Two new packages: ",[467,468,472],"a",{"href":469,"rel":470},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@vercube\u002Fscheduler",[471],"nofollow",[473,474,475],"code",{},"@vercube\u002Fscheduler"," runs cron and interval jobs once per occurrence however many replicas you scale to, and ",[467,478,481],{"href":479,"rel":480},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@vercube\u002Ftesting",[471],[473,482,483],{},"@vercube\u002Ftesting"," boots your real application for a test without opening a port. Under them, the application learned to shut down properly, and a long list of places where a request was handled almost right are now handled right.",[486,487,136],"h2",{"id":488},"scheduler",[459,490,491,492,495,496,501,502,505,506,509],{},"The problem a scheduler inside a web app has to solve is not the cron expression. It is that the app runs as four pods, and a nightly billing job written as a ",[473,493,494],{},"setInterval"," runs four times. ",[467,497,499],{"href":469,"rel":498},[471],[473,500,475],{}," separates ",[462,503,504],{},"when"," something runs from ",[462,507,508],{},"who"," runs it: every replica computes the same schedule from the same code, and a shared store decides which one takes each occurrence.",[511,512,518],"pre",{"className":513,"code":514,"filename":515,"language":516,"meta":517,"style":517},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { Inject } from '@vercube\u002Fdi';\nimport { Cron, Every } from '@vercube\u002Fscheduler';\nimport { InvoiceService } from '..\u002Fservices\u002FInvoiceService';\nimport type { SchedulerTypes } from '@vercube\u002Fscheduler';\n\nexport class BillingJobs {\n  @Inject(InvoiceService)\n  private gInvoices!: InvoiceService;\n\n  \u002F\u002F once per day across the whole cluster, on Warsaw wall clock time\n  @Cron('0 3 * * *', { name: 'billing.close-day', timezone: 'Europe\u002FWarsaw' })\n  public async closeDay(ctx: SchedulerTypes.JobContext): Promise\u003Cvoid> {\n    for (const batch of await this.gInvoices.openBatches()) {\n      ctx.signal.throwIfAborted();\n      await this.gInvoices.close(batch, { idempotencyKey: ctx.occurrenceId });\n    }\n  }\n\n  \u002F\u002F every replica clears its own cache\n  @Every('5m', { scope: 'instance' })\n  public flushLocalCache(): void {\n    this.gInvoices.clearCache();\n  }\n}\n","src\u002Fjobs\u002FBillingJobs.ts","ts","",[473,519,520,556,584,607,632,638,655,668,685,690,697,751,796,834,855,899,905,911,916,922,959,975,992,997],{"__ignoreMap":517},[521,522,525,529,533,537,540,543,546,550,553],"span",{"class":523,"line":524},"line",1,[521,526,528],{"class":527},"s7zQu","import",[521,530,532],{"class":531},"sMK4o"," {",[521,534,536],{"class":535},"sTEyZ"," Inject",[521,538,539],{"class":531}," }",[521,541,542],{"class":527}," from",[521,544,545],{"class":531}," '",[521,547,549],{"class":548},"sfazB","@vercube\u002Fdi",[521,551,552],{"class":531},"'",[521,554,555],{"class":531},";\n",[521,557,559,561,563,566,569,572,574,576,578,580,582],{"class":523,"line":558},2,[521,560,528],{"class":527},[521,562,532],{"class":531},[521,564,565],{"class":535}," Cron",[521,567,568],{"class":531},",",[521,570,571],{"class":535}," Every",[521,573,539],{"class":531},[521,575,542],{"class":527},[521,577,545],{"class":531},[521,579,475],{"class":548},[521,581,552],{"class":531},[521,583,555],{"class":531},[521,585,587,589,591,594,596,598,600,603,605],{"class":523,"line":586},3,[521,588,528],{"class":527},[521,590,532],{"class":531},[521,592,593],{"class":535}," InvoiceService",[521,595,539],{"class":531},[521,597,542],{"class":527},[521,599,545],{"class":531},[521,601,602],{"class":548},"..\u002Fservices\u002FInvoiceService",[521,604,552],{"class":531},[521,606,555],{"class":531},[521,608,610,612,615,617,620,622,624,626,628,630],{"class":523,"line":609},4,[521,611,528],{"class":527},[521,613,614],{"class":527}," type",[521,616,532],{"class":531},[521,618,619],{"class":535}," SchedulerTypes",[521,621,539],{"class":531},[521,623,542],{"class":527},[521,625,545],{"class":531},[521,627,475],{"class":548},[521,629,552],{"class":531},[521,631,555],{"class":531},[521,633,635],{"class":523,"line":634},5,[521,636,637],{"emptyLinePlaceholder":68},"\n",[521,639,641,644,648,652],{"class":523,"line":640},6,[521,642,643],{"class":527},"export",[521,645,647],{"class":646},"spNyl"," class",[521,649,651],{"class":650},"sBMFI"," BillingJobs",[521,653,654],{"class":531}," {\n",[521,656,658,661,665],{"class":523,"line":657},7,[521,659,660],{"class":531},"  @",[521,662,664],{"class":663},"s2Zo4","Inject",[521,666,667],{"class":535},"(InvoiceService)\n",[521,669,671,674,678,681,683],{"class":523,"line":670},8,[521,672,673],{"class":646},"  private",[521,675,677],{"class":676},"swJcz"," gInvoices",[521,679,680],{"class":531},"!:",[521,682,593],{"class":650},[521,684,555],{"class":531},[521,686,688],{"class":523,"line":687},9,[521,689,637],{"emptyLinePlaceholder":68},[521,691,693],{"class":523,"line":692},10,[521,694,696],{"class":695},"sHwdD","  \u002F\u002F once per day across the whole cluster, on Warsaw wall clock time\n",[521,698,700,702,705,708,710,713,715,717,719,722,725,727,730,732,734,737,739,741,744,746,748],{"class":523,"line":699},11,[521,701,660],{"class":531},[521,703,704],{"class":663},"Cron",[521,706,707],{"class":535},"(",[521,709,552],{"class":531},[521,711,712],{"class":548},"0 3 * * *",[521,714,552],{"class":531},[521,716,568],{"class":531},[521,718,532],{"class":531},[521,720,721],{"class":676}," name",[521,723,724],{"class":531},":",[521,726,545],{"class":531},[521,728,729],{"class":548},"billing.close-day",[521,731,552],{"class":531},[521,733,568],{"class":531},[521,735,736],{"class":676}," timezone",[521,738,724],{"class":531},[521,740,545],{"class":531},[521,742,743],{"class":548},"Europe\u002FWarsaw",[521,745,552],{"class":531},[521,747,539],{"class":531},[521,749,750],{"class":535},")\n",[521,752,754,757,760,763,765,769,771,773,776,779,782,785,788,791,794],{"class":523,"line":753},12,[521,755,756],{"class":646},"  public",[521,758,759],{"class":646}," async",[521,761,762],{"class":676}," closeDay",[521,764,707],{"class":531},[521,766,768],{"class":767},"sHdIc","ctx",[521,770,724],{"class":531},[521,772,619],{"class":650},[521,774,775],{"class":531},".",[521,777,778],{"class":650},"JobContext",[521,780,781],{"class":531},"):",[521,783,784],{"class":650}," Promise",[521,786,787],{"class":531},"\u003C",[521,789,790],{"class":650},"void",[521,792,793],{"class":531},">",[521,795,654],{"class":531},[521,797,799,802,805,808,811,814,817,820,823,825,828,831],{"class":523,"line":798},13,[521,800,801],{"class":527},"    for",[521,803,804],{"class":676}," (",[521,806,807],{"class":646},"const",[521,809,810],{"class":535}," batch",[521,812,813],{"class":531}," of",[521,815,816],{"class":527}," await",[521,818,819],{"class":531}," this.",[521,821,822],{"class":535},"gInvoices",[521,824,775],{"class":531},[521,826,827],{"class":663},"openBatches",[521,829,830],{"class":676},"()) ",[521,832,833],{"class":531},"{\n",[521,835,837,840,842,845,847,850,853],{"class":523,"line":836},14,[521,838,839],{"class":535},"      ctx",[521,841,775],{"class":531},[521,843,844],{"class":535},"signal",[521,846,775],{"class":531},[521,848,849],{"class":663},"throwIfAborted",[521,851,852],{"class":676},"()",[521,854,555],{"class":531},[521,856,858,861,863,865,867,870,872,875,877,879,882,884,887,889,892,894,897],{"class":523,"line":857},15,[521,859,860],{"class":527},"      await",[521,862,819],{"class":531},[521,864,822],{"class":535},[521,866,775],{"class":531},[521,868,869],{"class":663},"close",[521,871,707],{"class":676},[521,873,874],{"class":535},"batch",[521,876,568],{"class":531},[521,878,532],{"class":531},[521,880,881],{"class":676}," idempotencyKey",[521,883,724],{"class":531},[521,885,886],{"class":535}," ctx",[521,888,775],{"class":531},[521,890,891],{"class":535},"occurrenceId",[521,893,539],{"class":531},[521,895,896],{"class":676},")",[521,898,555],{"class":531},[521,900,902],{"class":523,"line":901},16,[521,903,904],{"class":531},"    }\n",[521,906,908],{"class":523,"line":907},17,[521,909,910],{"class":531},"  }\n",[521,912,914],{"class":523,"line":913},18,[521,915,637],{"emptyLinePlaceholder":68},[521,917,919],{"class":523,"line":918},19,[521,920,921],{"class":695},"  \u002F\u002F every replica clears its own cache\n",[521,923,925,927,930,932,934,937,939,941,943,946,948,950,953,955,957],{"class":523,"line":924},20,[521,926,660],{"class":531},[521,928,929],{"class":663},"Every",[521,931,707],{"class":535},[521,933,552],{"class":531},[521,935,936],{"class":548},"5m",[521,938,552],{"class":531},[521,940,568],{"class":531},[521,942,532],{"class":531},[521,944,945],{"class":676}," scope",[521,947,724],{"class":531},[521,949,545],{"class":531},[521,951,952],{"class":548},"instance",[521,954,552],{"class":531},[521,956,539],{"class":531},[521,958,750],{"class":535},[521,960,962,964,967,970,973],{"class":523,"line":961},21,[521,963,756],{"class":646},[521,965,966],{"class":676}," flushLocalCache",[521,968,969],{"class":531},"():",[521,971,972],{"class":650}," void",[521,974,654],{"class":531},[521,976,978,981,983,985,988,990],{"class":523,"line":977},22,[521,979,980],{"class":531},"    this.",[521,982,822],{"class":535},[521,984,775],{"class":531},[521,986,987],{"class":663},"clearCache",[521,989,852],{"class":676},[521,991,555],{"class":531},[521,993,995],{"class":523,"line":994},23,[521,996,910],{"class":531},[521,998,1000],{"class":523,"line":999},24,[521,1001,1002],{"class":531},"}\n",[459,1004,1005],{},"A job class is an ordinary container service, bound where you bind your controllers. The plugin takes the store:",[511,1007,1010],{"className":513,"code":1008,"filename":1009,"language":516,"meta":517,"style":517},"import { defineConfig } from '@vercube\u002Fcore';\nimport { defineSchedulerStore, SchedulerPlugin } from '@vercube\u002Fscheduler';\nimport { RedisStore } from '@vercube\u002Fscheduler\u002Fstores\u002FRedisStore';\n\nexport default defineConfig({\n  plugins: [\n    [SchedulerPlugin, {\n      store: defineSchedulerStore({ store: RedisStore, initOptions: { url: process.env.REDIS_URL } }),\n    }],\n  ],\n});\n","vercube.config.ts",[473,1011,1012,1034,1060,1082,1086,1099,1109,1118,1176,1186,1193],{"__ignoreMap":517},[521,1013,1014,1016,1018,1021,1023,1025,1027,1030,1032],{"class":523,"line":524},[521,1015,528],{"class":527},[521,1017,532],{"class":531},[521,1019,1020],{"class":535}," defineConfig",[521,1022,539],{"class":531},[521,1024,542],{"class":527},[521,1026,545],{"class":531},[521,1028,1029],{"class":548},"@vercube\u002Fcore",[521,1031,552],{"class":531},[521,1033,555],{"class":531},[521,1035,1036,1038,1040,1043,1045,1048,1050,1052,1054,1056,1058],{"class":523,"line":558},[521,1037,528],{"class":527},[521,1039,532],{"class":531},[521,1041,1042],{"class":535}," defineSchedulerStore",[521,1044,568],{"class":531},[521,1046,1047],{"class":535}," SchedulerPlugin",[521,1049,539],{"class":531},[521,1051,542],{"class":527},[521,1053,545],{"class":531},[521,1055,475],{"class":548},[521,1057,552],{"class":531},[521,1059,555],{"class":531},[521,1061,1062,1064,1066,1069,1071,1073,1075,1078,1080],{"class":523,"line":586},[521,1063,528],{"class":527},[521,1065,532],{"class":531},[521,1067,1068],{"class":535}," RedisStore",[521,1070,539],{"class":531},[521,1072,542],{"class":527},[521,1074,545],{"class":531},[521,1076,1077],{"class":548},"@vercube\u002Fscheduler\u002Fstores\u002FRedisStore",[521,1079,552],{"class":531},[521,1081,555],{"class":531},[521,1083,1084],{"class":523,"line":609},[521,1085,637],{"emptyLinePlaceholder":68},[521,1087,1088,1090,1093,1095,1097],{"class":523,"line":634},[521,1089,643],{"class":527},[521,1091,1092],{"class":527}," default",[521,1094,1020],{"class":663},[521,1096,707],{"class":535},[521,1098,833],{"class":531},[521,1100,1101,1104,1106],{"class":523,"line":640},[521,1102,1103],{"class":676},"  plugins",[521,1105,724],{"class":531},[521,1107,1108],{"class":535}," [\n",[521,1110,1111,1114,1116],{"class":523,"line":657},[521,1112,1113],{"class":535},"    [SchedulerPlugin",[521,1115,568],{"class":531},[521,1117,654],{"class":531},[521,1119,1120,1123,1125,1127,1129,1132,1135,1137,1139,1141,1144,1146,1148,1151,1153,1156,1158,1161,1163,1166,1169,1171,1173],{"class":523,"line":670},[521,1121,1122],{"class":676},"      store",[521,1124,724],{"class":531},[521,1126,1042],{"class":663},[521,1128,707],{"class":535},[521,1130,1131],{"class":531},"{",[521,1133,1134],{"class":676}," store",[521,1136,724],{"class":531},[521,1138,1068],{"class":535},[521,1140,568],{"class":531},[521,1142,1143],{"class":676}," initOptions",[521,1145,724],{"class":531},[521,1147,532],{"class":531},[521,1149,1150],{"class":676}," url",[521,1152,724],{"class":531},[521,1154,1155],{"class":535}," process",[521,1157,775],{"class":531},[521,1159,1160],{"class":535},"env",[521,1162,775],{"class":531},[521,1164,1165],{"class":535},"REDIS_URL ",[521,1167,1168],{"class":531},"}",[521,1170,539],{"class":531},[521,1172,896],{"class":535},[521,1174,1175],{"class":531},",\n",[521,1177,1178,1181,1184],{"class":523,"line":687},[521,1179,1180],{"class":531},"    }",[521,1182,1183],{"class":535},"]",[521,1185,1175],{"class":531},[521,1187,1188,1191],{"class":523,"line":692},[521,1189,1190],{"class":535},"  ]",[521,1192,1175],{"class":531},[521,1194,1195,1197,1199],{"class":523,"line":699},[521,1196,1168],{"class":531},[521,1198,896],{"class":535},[521,1200,555],{"class":531},[459,1202,1203,1206,1207,1210,1211,1214,1215,1218],{},[473,1204,1205],{},"RedisStore"," claims an occurrence with ",[473,1208,1209],{},"SET NX PX"," and touches a single key per operation, so Redis Cluster works without hash tags. ",[473,1212,1213],{},"PostgresStore"," keeps claims in two tables and computes every expiry with the database's ",[473,1216,1217],{},"now()",", so pods whose clocks drift apart still agree on when a claim expires. I left advisory locks out on purpose: they belong to a session, and that breaks behind a connection pool.",[459,1220,1221,1222,1225,1226,1229,1230,1233],{},"The decision I care about most is what happens without a shared store. ",[473,1223,1224],{},"MemoryStore"," only coordinates the process it lives in, and with ",[473,1227,1228],{},"NODE_ENV=production"," a cluster job on it ",[462,1231,1232],{},"refuses to start"," rather than quietly running on every pod. The failure you get is a startup error, not a customer charged four times.",[459,1235,1236,1237,1240,1241,1244,1245,1248],{},"The guarantee is worth stating precisely, because \"exactly once\" is not something a scheduler can promise. A cluster job runs at most once per occurrence in normal operation, and at least once as long as one replica is up when it is due. The edge is a lost lock: when the heartbeat cannot extend it, ",[473,1238,1239],{},"ctx.signal"," is aborted with reason ",[473,1242,1243],{},"lock-lost",", since another replica may take over. That is why the example uses ",[473,1246,1247],{},"ctx.occurrenceId"," as an idempotency key and checks the signal between batches.",[459,1250,1251],{},"The rest is what you would otherwise build around it:",[1253,1254,1255,1266,1278,1295],"ul",{},[1256,1257,1258,1261,1262,1265],"li",{},[462,1259,1260],{},"Misfires."," ",[473,1263,1264],{},"misfire: 'run-once'"," catches up on the latest occurrence nobody ran, for example while every pod was restarting, through the normal claim, so twenty pods starting together run it once.",[1256,1267,1268,1271,1272,1277],{},[462,1269,1270],{},"Queue targets."," For heavy work, the replica that wins the occurrence only enqueues it to ",[467,1273,1274],{"href":122},[473,1275,1276],{},"@vercube\u002Fqueue",", and the queue's attempts and backoff apply.",[1256,1279,1280,1283,1284,1287,1288,1291,1292,1294],{},[462,1281,1282],{},"External triggers."," On platforms without a long-lived process, ",[473,1285,1286],{},"vercube scheduler run billing.close-day"," fires a job from a Kubernetes ",[473,1289,1290],{},"CronJob",", and still claims the occurrence, so a ",[473,1293,1290],{}," that fires twice runs it once.",[1256,1296,1297,1300,1301,1304,1305,1308,1309,1312,1313,1316],{},[462,1298,1299],{},"Telemetry and devtools."," Every run is a ",[473,1302,1303],{},"scheduler.run"," span. One ",[473,1306,1307],{},"completed"," and N-1 ",[473,1310,1311],{},"skipped-claimed"," per occurrence in ",[473,1314,1315],{},"vercube.scheduler.runs"," is what proves exactly one replica ran it, and devtools shows the jobs, their next runs and the recent outcomes.",[459,1318,1319,1320,1325,1326,1325,1330,775],{},"Read more: ",[462,1321,1322],{},[467,1323,1324],{"href":141},"Scheduler overview"," · ",[462,1327,1328],{},[467,1329,144],{"href":145},[462,1331,1332],{},[467,1333,148],{"href":149},[486,1335,155],{"id":1336},"testing",[459,1338,1339,1340,1343,1344,1349],{},"Until now, testing a Vercube app meant choosing between booting the whole thing on a port, with its config file and ",[473,1341,1342],{},".env",", or unit testing services and hoping the controller, the middleware and the validation in between behaved. ",[467,1345,1347],{"href":479,"rel":1346},[471],[473,1348,483],{}," boots the real thing without the parts that make tests slow and leaky.",[511,1351,1354],{"className":513,"code":1352,"filename":1353,"language":516,"meta":517,"style":517},"import { createTestApp } from '@vercube\u002Ftesting';\nimport { expect, it } from 'vitest';\n\nit('returns a user', async () => {\n  await using app = await createTestApp({\n    controllers: [UserController],\n    providers: [UserService],\n    overrides: [[UserRepository, { findById: async (id) => ({ id, name: 'Ada' }) }]],\n  });\n\n  const res = await app.get('\u002Fusers\u002F1').expect(200);\n\n  expect(await res.json()).toEqual({ id: '1', name: 'Ada' });\n});\n","test\u002Fusers.test.ts",[473,1355,1356,1377,1404,1408,1434,1453,1470,1486,1551,1560,1564,1609,1613,1671],{"__ignoreMap":517},[521,1357,1358,1360,1362,1365,1367,1369,1371,1373,1375],{"class":523,"line":524},[521,1359,528],{"class":527},[521,1361,532],{"class":531},[521,1363,1364],{"class":535}," createTestApp",[521,1366,539],{"class":531},[521,1368,542],{"class":527},[521,1370,545],{"class":531},[521,1372,483],{"class":548},[521,1374,552],{"class":531},[521,1376,555],{"class":531},[521,1378,1379,1381,1383,1386,1388,1391,1393,1395,1397,1400,1402],{"class":523,"line":558},[521,1380,528],{"class":527},[521,1382,532],{"class":531},[521,1384,1385],{"class":535}," expect",[521,1387,568],{"class":531},[521,1389,1390],{"class":535}," it",[521,1392,539],{"class":531},[521,1394,542],{"class":527},[521,1396,545],{"class":531},[521,1398,1399],{"class":548},"vitest",[521,1401,552],{"class":531},[521,1403,555],{"class":531},[521,1405,1406],{"class":523,"line":586},[521,1407,637],{"emptyLinePlaceholder":68},[521,1409,1410,1413,1415,1417,1420,1422,1424,1426,1429,1432],{"class":523,"line":609},[521,1411,1412],{"class":663},"it",[521,1414,707],{"class":535},[521,1416,552],{"class":531},[521,1418,1419],{"class":548},"returns a user",[521,1421,552],{"class":531},[521,1423,568],{"class":531},[521,1425,759],{"class":646},[521,1427,1428],{"class":531}," ()",[521,1430,1431],{"class":646}," =>",[521,1433,654],{"class":531},[521,1435,1436,1439,1442,1445,1447,1449,1451],{"class":523,"line":634},[521,1437,1438],{"class":646},"  await using",[521,1440,1441],{"class":535}," app",[521,1443,1444],{"class":531}," =",[521,1446,816],{"class":527},[521,1448,1364],{"class":663},[521,1450,707],{"class":676},[521,1452,833],{"class":531},[521,1454,1455,1458,1460,1463,1466,1468],{"class":523,"line":640},[521,1456,1457],{"class":676},"    controllers",[521,1459,724],{"class":531},[521,1461,1462],{"class":676}," [",[521,1464,1465],{"class":535},"UserController",[521,1467,1183],{"class":676},[521,1469,1175],{"class":531},[521,1471,1472,1475,1477,1479,1482,1484],{"class":523,"line":657},[521,1473,1474],{"class":676},"    providers",[521,1476,724],{"class":531},[521,1478,1462],{"class":676},[521,1480,1481],{"class":535},"UserService",[521,1483,1183],{"class":676},[521,1485,1175],{"class":531},[521,1487,1488,1491,1493,1496,1499,1501,1503,1506,1508,1510,1512,1515,1517,1519,1521,1523,1526,1528,1530,1532,1534,1537,1539,1541,1544,1546,1549],{"class":523,"line":670},[521,1489,1490],{"class":676},"    overrides",[521,1492,724],{"class":531},[521,1494,1495],{"class":676}," [[",[521,1497,1498],{"class":535},"UserRepository",[521,1500,568],{"class":531},[521,1502,532],{"class":531},[521,1504,1505],{"class":663}," findById",[521,1507,724],{"class":531},[521,1509,759],{"class":646},[521,1511,804],{"class":531},[521,1513,1514],{"class":767},"id",[521,1516,896],{"class":531},[521,1518,1431],{"class":646},[521,1520,804],{"class":676},[521,1522,1131],{"class":531},[521,1524,1525],{"class":535}," id",[521,1527,568],{"class":531},[521,1529,721],{"class":676},[521,1531,724],{"class":531},[521,1533,545],{"class":531},[521,1535,1536],{"class":548},"Ada",[521,1538,552],{"class":531},[521,1540,539],{"class":531},[521,1542,1543],{"class":676},") ",[521,1545,1168],{"class":531},[521,1547,1548],{"class":676},"]]",[521,1550,1175],{"class":531},[521,1552,1553,1556,1558],{"class":523,"line":687},[521,1554,1555],{"class":531},"  }",[521,1557,896],{"class":676},[521,1559,555],{"class":531},[521,1561,1562],{"class":523,"line":692},[521,1563,637],{"emptyLinePlaceholder":68},[521,1565,1566,1569,1572,1574,1576,1578,1580,1583,1585,1587,1590,1592,1594,1596,1599,1601,1605,1607],{"class":523,"line":699},[521,1567,1568],{"class":646},"  const",[521,1570,1571],{"class":535}," res",[521,1573,1444],{"class":531},[521,1575,816],{"class":527},[521,1577,1441],{"class":535},[521,1579,775],{"class":531},[521,1581,1582],{"class":663},"get",[521,1584,707],{"class":676},[521,1586,552],{"class":531},[521,1588,1589],{"class":548},"\u002Fusers\u002F1",[521,1591,552],{"class":531},[521,1593,896],{"class":676},[521,1595,775],{"class":531},[521,1597,1598],{"class":663},"expect",[521,1600,707],{"class":676},[521,1602,1604],{"class":1603},"sbssI","200",[521,1606,896],{"class":676},[521,1608,555],{"class":531},[521,1610,1611],{"class":523,"line":753},[521,1612,637],{"emptyLinePlaceholder":68},[521,1614,1615,1618,1620,1623,1625,1627,1630,1633,1635,1638,1640,1642,1644,1646,1648,1651,1653,1655,1657,1659,1661,1663,1665,1667,1669],{"class":523,"line":798},[521,1616,1617],{"class":663},"  expect",[521,1619,707],{"class":676},[521,1621,1622],{"class":527},"await",[521,1624,1571],{"class":535},[521,1626,775],{"class":531},[521,1628,1629],{"class":663},"json",[521,1631,1632],{"class":676},"())",[521,1634,775],{"class":531},[521,1636,1637],{"class":663},"toEqual",[521,1639,707],{"class":676},[521,1641,1131],{"class":531},[521,1643,1525],{"class":676},[521,1645,724],{"class":531},[521,1647,545],{"class":531},[521,1649,1650],{"class":548},"1",[521,1652,552],{"class":531},[521,1654,568],{"class":531},[521,1656,721],{"class":676},[521,1658,724],{"class":531},[521,1660,545],{"class":531},[521,1662,1536],{"class":548},[521,1664,552],{"class":531},[521,1666,539],{"class":531},[521,1668,896],{"class":676},[521,1670,555],{"class":531},[521,1672,1673,1675,1677],{"class":523,"line":836},[521,1674,1168],{"class":531},[521,1676,896],{"class":535},[521,1678,555],{"class":531},[459,1680,1681,1682,1685,1686,1688,1689,1691],{},"Controllers, dependency injection, middleware and validation are real. Requests go through ",[473,1683,1684],{},"app.fetch()",", so no port is opened, and ",[473,1687,1009],{}," and ",[473,1690,1342],{}," are not read unless you ask for them. Every app gets its own container, router and configuration, so test files do not see each other's overrides.",[459,1693,1694,1695,1697,1698,1701,1702,1688,1705,1708],{},"It does not depend on a test runner. The status and header assertions are built in, everything else is your runner's ",[473,1696,1598],{},", and the same test runs under Vitest, ",[473,1699,1700],{},"bun test",", ",[473,1703,1704],{},"deno test",[473,1706,1707],{},"node --test"," with only the import changed. Mocks are whatever you already use.",[459,1710,1711],{},"A few details that came from writing the framework's own tests with it:",[1253,1713,1714,1720,1736,1746],{},[1256,1715,1716,1719],{},[462,1717,1718],{},"Overrides win over plugins."," They are applied after every plugin, so a fake replaces what a plugin bound.",[1256,1721,1722,1728,1729,1732,1733,775],{},[462,1723,1724,1727],{},[473,1725,1726],{},"stub()"," names what you forgot."," A partial fake usually fails with ",[473,1730,1731],{},"undefined is not a function"," far from the cause. A stub fails with ",[473,1734,1735],{},"UserRepository.save is not stubbed",[1256,1737,1738,1741,1742,1745],{},[462,1739,1740],{},"Failures are readable."," A failed ",[473,1743,1744],{},".expect()"," prints the request and the response, with credentials withheld.",[1256,1747,1748,1261,1751,1754],{},[462,1749,1750],{},"Logs are assertable.",[473,1752,1753],{},"app.logs.find('error', \u002Forder not found\u002F)"," sees everything the app logged, including what the framework logged for an unhandled error.",[459,1756,1757,1758,1761,1762,1765,1766,1769],{},"Extensions cover the modules that are awkward to fake by hand. ",[473,1759,1760],{},"testAuth()"," lets a request run as a user with ",[473,1763,1764],{},".as({ id: '1', roles: ['admin'] })",", and the scheduler ships a ",[473,1767,1768],{},"ManualClock",", so a test of the 03:00 billing job advances time by a minute instead of waiting for it:",[511,1771,1773],{"className":513,"code":1772,"language":516,"meta":517,"style":517},"import { withManualClock } from '@vercube\u002Fscheduler\u002Ftesting';\n\nconst time = withManualClock('2026-01-01T02:59:00Z');\nconst app = await createTestApp({ providers: [BillingJobs], extensions: [time] });\n\nawait time.clock.advance('1m');\n",[473,1774,1775,1797,1801,1826,1867,1871],{"__ignoreMap":517},[521,1776,1777,1779,1781,1784,1786,1788,1790,1793,1795],{"class":523,"line":524},[521,1778,528],{"class":527},[521,1780,532],{"class":531},[521,1782,1783],{"class":535}," withManualClock",[521,1785,539],{"class":531},[521,1787,542],{"class":527},[521,1789,545],{"class":531},[521,1791,1792],{"class":548},"@vercube\u002Fscheduler\u002Ftesting",[521,1794,552],{"class":531},[521,1796,555],{"class":531},[521,1798,1799],{"class":523,"line":558},[521,1800,637],{"emptyLinePlaceholder":68},[521,1802,1803,1805,1808,1811,1813,1815,1817,1820,1822,1824],{"class":523,"line":586},[521,1804,807],{"class":646},[521,1806,1807],{"class":535}," time ",[521,1809,1810],{"class":531},"=",[521,1812,1783],{"class":663},[521,1814,707],{"class":535},[521,1816,552],{"class":531},[521,1818,1819],{"class":548},"2026-01-01T02:59:00Z",[521,1821,552],{"class":531},[521,1823,896],{"class":535},[521,1825,555],{"class":531},[521,1827,1828,1830,1833,1835,1837,1839,1841,1843,1846,1848,1851,1853,1856,1858,1861,1863,1865],{"class":523,"line":609},[521,1829,807],{"class":646},[521,1831,1832],{"class":535}," app ",[521,1834,1810],{"class":531},[521,1836,816],{"class":527},[521,1838,1364],{"class":663},[521,1840,707],{"class":535},[521,1842,1131],{"class":531},[521,1844,1845],{"class":676}," providers",[521,1847,724],{"class":531},[521,1849,1850],{"class":535}," [BillingJobs]",[521,1852,568],{"class":531},[521,1854,1855],{"class":676}," extensions",[521,1857,724],{"class":531},[521,1859,1860],{"class":535}," [time] ",[521,1862,1168],{"class":531},[521,1864,896],{"class":535},[521,1866,555],{"class":531},[521,1868,1869],{"class":523,"line":634},[521,1870,637],{"emptyLinePlaceholder":68},[521,1872,1873,1875,1878,1880,1883,1885,1888,1890,1892,1895,1897,1899],{"class":523,"line":640},[521,1874,1622],{"class":527},[521,1876,1877],{"class":535}," time",[521,1879,775],{"class":531},[521,1881,1882],{"class":535},"clock",[521,1884,775],{"class":531},[521,1886,1887],{"class":663},"advance",[521,1889,707],{"class":535},[521,1891,552],{"class":531},[521,1893,1894],{"class":548},"1m",[521,1896,552],{"class":531},[521,1898,896],{"class":535},[521,1900,555],{"class":531},[459,1902,1903],{},"The module tests in the Vercube repository now run through this package, which is the best argument I have that it holds up.",[459,1905,1319,1906,1325,1911,1325,1915,1325,1919,775],{},[462,1907,1908],{},[467,1909,1910],{"href":160},"Testing overview",[462,1912,1913],{},[467,1914,163],{"href":164},[462,1916,1917],{},[467,1918,167],{"href":168},[462,1920,1921],{},[467,1922,171],{"href":172},[486,1924,1926],{"id":1925},"shutting-down-properly","Shutting down properly",[459,1928,1929,1930,1933,1934,1937,1938,1941],{},"Both packages above needed something core did not have: a way to stop. ",[473,1931,1932],{},"app.close()"," now stops the server from accepting connections, waits for the requests in flight, then runs every callback registered with ",[473,1935,1936],{},"app.onClose()",", last registered first. The scheduler and the queue register their own close there, so on ",[473,1939,1940],{},"SIGTERM"," the server stops first, then running jobs drain, then the process exits.",[511,1943,1945],{"className":513,"code":1944,"filename":1009,"language":516,"meta":517,"style":517},"export default defineConfig({\n  shutdown: {\n    handleSignals: true,\n    timeout: 25_000,\n  },\n});\n",[473,1946,1947,1959,1968,1981,1993,1998],{"__ignoreMap":517},[521,1948,1949,1951,1953,1955,1957],{"class":523,"line":524},[521,1950,643],{"class":527},[521,1952,1092],{"class":527},[521,1954,1020],{"class":663},[521,1956,707],{"class":535},[521,1958,833],{"class":531},[521,1960,1961,1964,1966],{"class":523,"line":558},[521,1962,1963],{"class":676},"  shutdown",[521,1965,724],{"class":531},[521,1967,654],{"class":531},[521,1969,1970,1973,1975,1979],{"class":523,"line":586},[521,1971,1972],{"class":676},"    handleSignals",[521,1974,724],{"class":531},[521,1976,1978],{"class":1977},"sfNiH"," true",[521,1980,1175],{"class":531},[521,1982,1983,1986,1988,1991],{"class":523,"line":609},[521,1984,1985],{"class":676},"    timeout",[521,1987,724],{"class":531},[521,1989,1990],{"class":1603}," 25_000",[521,1992,1175],{"class":531},[521,1994,1995],{"class":523,"line":634},[521,1996,1997],{"class":531},"  },\n",[521,1999,2000,2002,2004],{"class":523,"line":640},[521,2001,1168],{"class":531},[521,2003,896],{"class":535},[521,2005,555],{"class":531},[459,2007,2008,2009,2012,2013,2016],{},"Your own resources use the same hook, and ",[473,2010,2011],{},"app.onStart()"," is the counterpart: callbacks run before the port is bound, and one that throws fails ",[473,2014,2015],{},"listen()",", so a database that cannot connect stops the deploy instead of serving errors.",[511,2018,2020],{"className":513,"code":2019,"language":516,"meta":517,"style":517},"app.onStart(() => pool.connect());\napp.onClose(() => pool.end());\n\nawait app.listen();\n",[473,2021,2022,2050,2076,2080],{"__ignoreMap":517},[521,2023,2024,2027,2029,2032,2034,2036,2038,2041,2043,2046,2048],{"class":523,"line":524},[521,2025,2026],{"class":535},"app",[521,2028,775],{"class":531},[521,2030,2031],{"class":663},"onStart",[521,2033,707],{"class":535},[521,2035,852],{"class":531},[521,2037,1431],{"class":646},[521,2039,2040],{"class":535}," pool",[521,2042,775],{"class":531},[521,2044,2045],{"class":663},"connect",[521,2047,1632],{"class":535},[521,2049,555],{"class":531},[521,2051,2052,2054,2056,2059,2061,2063,2065,2067,2069,2072,2074],{"class":523,"line":558},[521,2053,2026],{"class":535},[521,2055,775],{"class":531},[521,2057,2058],{"class":663},"onClose",[521,2060,707],{"class":535},[521,2062,852],{"class":531},[521,2064,1431],{"class":646},[521,2066,2040],{"class":535},[521,2068,775],{"class":531},[521,2070,2071],{"class":663},"end",[521,2073,1632],{"class":535},[521,2075,555],{"class":531},[521,2077,2078],{"class":523,"line":586},[521,2079,637],{"emptyLinePlaceholder":68},[521,2081,2082,2084,2086,2088,2091,2093],{"class":523,"line":609},[521,2083,1622],{"class":527},[521,2085,1441],{"class":535},[521,2087,775],{"class":531},[521,2089,2090],{"class":663},"listen",[521,2092,852],{"class":535},[521,2094,555],{"class":531},[459,2096,2097,2098,2101,2102,2105],{},"Keep ",[473,2099,2100],{},"timeout"," below the pod's ",[473,2103,2104],{},"terminationGracePeriodSeconds",", which is 30 seconds by default, or Kubernetes kills the process before the callbacks run.",[459,2107,1319,2108,775],{},[462,2109,2110],{},[467,2111,2113],{"href":2112},"\u002Fdocs\u002Fcore-features\u002Fconfiguration#graceful-shutdown","Graceful shutdown",[486,2115,2117],{"id":2116},"thirty-fixes-to-how-a-request-is-handled","Thirty fixes to how a request is handled",[459,2119,2120,2121,2123],{},"Writing ",[473,2122,483],{}," meant writing a lot of tests against the request path, and a lot of them found something. None of these was a crash. Each was a request handled almost right, which is the kind of bug that survives longest. The ones you are most likely to have met:",[1253,2125,2126,2146,2168,2182,2188,2201,2213],{},[1256,2127,2128,2131,2132,1701,2135,2138,2139,2141,2142,2145],{},[462,2129,2130],{},"Global middlewares run everywhere."," They now run for ",[473,2133,2134],{},"404",[473,2136,2137],{},"405",", static files and CORS preflight too, so CORS and security headers reach responses no route produced. A ",[473,2140,2137],{}," carries an ",[473,2143,2144],{},"Allow"," header.",[1256,2147,2148,2151,2152,2155,2156,2159,2160,2163,2164,2167],{},[462,2149,2150],{},"The body is parsed by content type",", after the before middlewares, so an auth middleware rejects a request with ",[473,2153,2154],{},"401"," before its body is read. JSON, form data and text are parsed. Anything else is ",[473,2157,2158],{},"415",", and a body over ",[473,2161,2162],{},"server.bodyLimit"," (1 MiB by default) is ",[473,2165,2166],{},"413"," before it is read.",[1256,2169,2170,2173,2174,2177,2178,2181],{},[462,2171,2172],{},"Errors are JSON",", with a ",[473,2175,2176],{},"content-type"," that says so, and an error only sets the status when it is an error status. A thrown error carrying ",[473,2179,2180],{},"status: 200"," used to answer with a 200.",[1256,2183,2184,2187],{},[462,2185,2186],{},"Middlewares run in the order you wrote them",", and app-wide ones run before route ones at equal priority.",[1256,2189,2190,2193,2194,2197,2198,775],{},[462,2191,2192],{},"Repeated query keys",": ",[473,2195,2196],{},"?tag=a&tag=b"," resolves to the first value. Ask for all of them with ",[473,2199,2200],{},"array: true",[1256,2202,2203,2206,2207,2209,2210,2212],{},[462,2204,2205],{},"Headers set by middleware survive errors",", so a ",[473,2208,2154],{}," from an auth middleware reaches the browser as a ",[473,2211,2154],{}," and not as a CORS failure.",[1256,2214,2215,2221,2222,2225],{},[462,2216,2217,2220],{},[473,2218,2219],{},"app.listen()"," decides whether to listen",", instead of the ",[473,2223,2224],{},"import.meta.main"," guard our examples used. PM2 imports the entry through a wrapper, so with the guard the server never started there, and nothing said so.",[459,2227,2228,2229,2232,2233,2236],{},"The modules got the same treatment. The queue honours each handler's attempts and backoff and fixes AMQP priority, Kafka consumes every topic of a mount with a single group member, storage fails on a mount that does not exist, WebSockets report errors to the peer, the serverless adapter stops corrupting ",[473,2230,2231],{},"Set-Cookie",", and ",[473,2234,2235],{},"@User()"," fails closed.",[459,2238,2239,2240,2243,2244,2247],{},"Two small features fell out of it. ",[473,2241,2242],{},"@Param"," takes a ",[473,2245,2246],{},"validationSchema",", so path parameters are validated and coerced like query parameters:",[511,2249,2251],{"className":513,"code":2250,"language":516,"meta":517,"style":517},"@Get('\u002F:userId\u002Fposts\u002F:page')\ngetPosts(\n  @Param({ name: 'userId', validationSchema: z.string().uuid() }) userId: string,\n  @Param({ name: 'page', validationSchema: z.coerce.number().int().min(1) }) page: number,\n) {}\n",[473,2252,2253,2272,2280,2334,2400],{"__ignoreMap":517},[521,2254,2255,2258,2261,2263,2265,2268,2270],{"class":523,"line":524},[521,2256,2257],{"class":531},"@",[521,2259,2260],{"class":663},"Get",[521,2262,707],{"class":535},[521,2264,552],{"class":531},[521,2266,2267],{"class":548},"\u002F:userId\u002Fposts\u002F:page",[521,2269,552],{"class":531},[521,2271,750],{"class":535},[521,2273,2274,2277],{"class":523,"line":558},[521,2275,2276],{"class":663},"getPosts",[521,2278,2279],{"class":535},"(\n",[521,2281,2282,2284,2287,2289,2291,2293,2295,2297,2300,2302,2304,2307,2309,2312,2314,2317,2319,2321,2324,2327,2329,2332],{"class":523,"line":586},[521,2283,660],{"class":535},[521,2285,2286],{"class":663},"Param",[521,2288,707],{"class":535},[521,2290,1131],{"class":531},[521,2292,721],{"class":676},[521,2294,724],{"class":531},[521,2296,545],{"class":531},[521,2298,2299],{"class":548},"userId",[521,2301,552],{"class":531},[521,2303,568],{"class":531},[521,2305,2306],{"class":676}," validationSchema",[521,2308,724],{"class":531},[521,2310,2311],{"class":535}," z",[521,2313,775],{"class":531},[521,2315,2316],{"class":663},"string",[521,2318,852],{"class":535},[521,2320,775],{"class":531},[521,2322,2323],{"class":663},"uuid",[521,2325,2326],{"class":535},"() ",[521,2328,1168],{"class":531},[521,2330,2331],{"class":535},") userId: string",[521,2333,1175],{"class":531},[521,2335,2336,2338,2340,2342,2344,2346,2348,2350,2353,2355,2357,2359,2361,2363,2365,2368,2370,2373,2375,2377,2380,2382,2384,2387,2389,2391,2393,2395,2398],{"class":523,"line":609},[521,2337,660],{"class":535},[521,2339,2286],{"class":663},[521,2341,707],{"class":535},[521,2343,1131],{"class":531},[521,2345,721],{"class":676},[521,2347,724],{"class":531},[521,2349,545],{"class":531},[521,2351,2352],{"class":548},"page",[521,2354,552],{"class":531},[521,2356,568],{"class":531},[521,2358,2306],{"class":676},[521,2360,724],{"class":531},[521,2362,2311],{"class":535},[521,2364,775],{"class":531},[521,2366,2367],{"class":535},"coerce",[521,2369,775],{"class":531},[521,2371,2372],{"class":663},"number",[521,2374,852],{"class":535},[521,2376,775],{"class":531},[521,2378,2379],{"class":663},"int",[521,2381,852],{"class":535},[521,2383,775],{"class":531},[521,2385,2386],{"class":663},"min",[521,2388,707],{"class":535},[521,2390,1650],{"class":1603},[521,2392,1543],{"class":535},[521,2394,1168],{"class":531},[521,2396,2397],{"class":535},") page: number",[521,2399,1175],{"class":531},[521,2401,2402,2404],{"class":523,"line":634},[521,2403,1543],{"class":535},[521,2405,2406],{"class":531},"{}\n",[459,2408,2409,2410,2413,2414,2417],{},"And ",[473,2411,2412],{},"@MultipartFormData()"," parses uploads into strings and ",[473,2415,2416],{},"File","s.",[486,2419,2421],{"id":2420},"openapi-from-what-the-route-already-says","OpenAPI from what the route already says",[459,2423,2424,2427,2428,2431,2432,2435,2436,1701,2439,1701,2442,1688,2445,2448,2449,2452,2453,2456,2457,2459,2460,2463,2464,775],{},[473,2425,2426],{},"@vercube\u002Fschema"," used to document what ",[473,2429,2430],{},"@Schema"," told it. It now derives what the route already declares: ",[473,2433,2434],{},"responses"," from ",[473,2437,2438],{},"@Status",[473,2440,2441],{},"@Redirect",[473,2443,2444],{},"@SetHeader",[473,2446,2447],{},"@Sse",", the ",[473,2450,2451],{},"operationId"," and tags from the controller, and a security requirement for routes behind ",[473,2454,2455],{},"@Auth()",". Path parameters are documented with their schema now that ",[473,2458,2242],{}," has one, and Valibot and ArkType schemas are converted through Standard JSON Schema. With ",[473,2461,2462],{},"includeAllRoutes: true"," every route is documented, not only the ones with ",[473,2465,2430],{},[459,2467,2468,2469,2472],{},"A schema that cannot be converted is left out with a warning, and a route that fails to generate is skipped, so one bad route no longer takes ",[473,2470,2471],{},"\u002F_schema\u002F"," down with it.",[459,2474,1319,2475,775],{},[462,2476,2477],{},[467,2478,2479],{"href":246},"Schema overview",[486,2481,2483],{"id":2482},"upgrade","Upgrade",[459,2485,2486,2487,2490,2491,724],{},"Every ",[473,2488,2489],{},"@vercube\u002F*"," package is released together and has to be on the same version, so update all of them at once rather than only ",[473,2492,1029],{},[511,2494,2498],{"className":2495,"code":2496,"language":2497,"meta":517,"style":517},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","pnpm up \"@vercube\u002F*\" --latest\n","bash",[473,2499,2500],{"__ignoreMap":517},[521,2501,2502,2505,2508,2511,2513,2516],{"class":523,"line":524},[521,2503,2504],{"class":650},"pnpm",[521,2506,2507],{"class":548}," up",[521,2509,2510],{"class":531}," \"",[521,2512,2489],{"class":548},[521,2514,2515],{"class":531},"\"",[521,2517,2518],{"class":548}," --latest\n",[459,2520,2521],{},"Then add the new packages at the same version:",[511,2523,2525],{"className":2495,"code":2524,"language":2497,"meta":517,"style":517},"pnpm add @vercube\u002Fscheduler@1.4.0\npnpm add -D @vercube\u002Ftesting@1.4.0\n",[473,2526,2527,2537],{"__ignoreMap":517},[521,2528,2529,2531,2534],{"class":523,"line":524},[521,2530,2504],{"class":650},[521,2532,2533],{"class":548}," add",[521,2535,2536],{"class":548}," @vercube\u002Fscheduler@1.4.0\n",[521,2538,2539,2541,2543,2546],{"class":523,"line":558},[521,2540,2504],{"class":650},[521,2542,2533],{"class":548},[521,2544,2545],{"class":548}," -D",[521,2547,2548],{"class":548}," @vercube\u002Ftesting@1.4.0\n",[459,2550,2551],{},"Most applications upgrade without touching anything. A few behaviours changed on purpose, and they are worth a search through your code:",[1253,2553,2554,2608,2626,2635,2647],{},[1256,2555,2556,2570,2571,2573,2574,1688,2577,2580,2581,2584,2585,2588,2589,2592,2593,2596,2597,2600,2601,2604,2605,2607],{},[462,2557,2558,2559,2562,2563,2566,2567,775],{},"A missing ",[473,2560,2561],{},"@QueryParam"," is ",[473,2564,2565],{},"undefined",", not ",[473,2568,2569],{},"null"," This is the one breaking change. ",[473,2572,2569],{}," broke ",[473,2575,2576],{},".optional()",[473,2578,2579],{},".default()",", and with ",[473,2582,2583],{},"z.coerce.number()"," a missing ",[473,2586,2587],{},"limit"," silently became ",[473,2590,2591],{},"0",". Code that checks ",[473,2594,2595],{},"=== null"," should use ",[473,2598,2599],{},"== null",", and a schema with ",[473,2602,2603],{},".nullable()"," but no ",[473,2606,2576],{}," now rejects a missing parameter.",[1256,2609,2610,2616,2617,2619,2620,2622,2623,2625],{},[462,2611,2612,2615],{},[473,2613,2614],{},"@Body()"," rejects content types it cannot parse"," with ",[473,2618,2158],{},", and bodies over 1 MiB with ",[473,2621,2166],{},". Raise ",[473,2624,2162],{}," if you accept larger ones.",[1256,2627,2628,2631,2632,2634],{},[462,2629,2630],{},"A repeated query key resolves to its first value."," Use ",[473,2633,2200],{}," where you expect several.",[1256,2636,2637,2643,2644,2646],{},[462,2638,2639,2640,2642],{},"A ",[473,2641,2242],{}," name that is not in the route path fails at registration",", rather than resolving to ",[473,2645,2565],{}," on every request.",[1256,2648,2649,2658,2659,2662],{},[462,2650,2651,2652,2655,2656,775],{},"Remove an ",[473,2653,2654],{},"if (import.meta.main)"," guard around ",[473,2657,2219],{}," It is no longer needed, and ",[473,2660,2661],{},"VERCUBE_LISTEN=0"," is how tools that import your entry keep it from listening.",[459,2664,2665,2666,775],{},"The full list of changes is in the ",[467,2667,2670],{"href":2668,"rel":2669},"https:\u002F\u002Fvercube.dev\u002Fchangelog",[471],"changelog",[2672,2673],"hr",{},[459,2675,2676,2677,2679,2680,2685,2686,775],{},"I would like to hear how the scheduler holds up on real clusters, and which test runner people actually use with ",[473,2678,483],{},". Issues and discussions are on ",[467,2681,2684],{"href":2682,"rel":2683},"https:\u002F\u002Fgithub.com\u002Fvercube\u002Fvercube",[471],"GitHub",", and I am on ",[467,2687,2690],{"href":2688,"rel":2689},"https:\u002F\u002Fx.com\u002FOskarLebuda",[471],"X",[2692,2693,2694],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":517,"searchDepth":524,"depth":558,"links":2696},[2697,2698,2699,2700,2701,2702],{"id":488,"depth":558,"text":136},{"id":1336,"depth":558,"text":155},{"id":1925,"depth":558,"text":1926},{"id":2116,"depth":558,"text":2117},{"id":2420,"depth":558,"text":2421},{"id":2482,"depth":558,"text":2483},"Release","2026-10-30","Scheduled jobs that run once across any number of replicas, a testing package that boots your real app without a port, graceful shutdown, and more than thirty fixes to how a request is handled.","md",null,{},"\u002Fblog\u002Fannouncing-1.4",{"title":448,"description":2705},"blog\u002F4.announcing-1.4","CtSWnfHwuGcl19qdhwMFFQUjQfEvsguUnMJjuJlx7H8",[2714,2719],{"title":2715,"path":2716,"stem":2717,"description":2718,"children":-1},"Announcing Vercube 1.3","\u002Fblog\u002Fannouncing-1.3","blog\u002F3.announcing-1.3","A cheaper request path, a background job module with four transports, devtools rebuilt as a telemetry consumer, and OpenTelemetry across the whole framework.",{"title":2720,"path":2721,"stem":2722,"description":2723,"children":-1},"Announcing Vercube 1.5","\u002Fblog\u002Fannouncing-1.5","blog\u002F5.announcing-1.5","Vercube 1.5 adds a typed API client generated from your controllers, typed errors, health probes, a project generator with modules and a new site.",1794396667893]