Skip to content

1.0.0

Choose a tag to compare

@github-actions github-actions released this 22 Oct 09:55

Added

  • Multi-core serving: serve({ workers }) (a number, or "auto": os.availableParallelism(), at most 16), dusk start --workers <n|auto> and DUSK_WORKERS for the generated server.mjs. The process becomes the primary of a node:cluster pool that serves nothing, forks the workers, waits until each listens (a worker that dies before the pool is ready fails the start), replaces a crashed worker with an exponential delay (500 ms doubling to 30 s), stops with exit code 1 and a clear message when 5 workers crash within 30 s (supervise.maxCrashes, crashWindowMs), kills a worker that does not listen within 30 s, replaces every worker one by one on SIGUSR2 or pool.restart() (the new worker listens before the old one drains, which first stops accepting and answers on its open connections with Connection: close), and on SIGTERM/SIGINT drains the workers for up to gracefulShutdownMs. ServerHandle gains role ("single", "worker", "primary") and pool (PoolControl: workers, pids(), restart(), failed); workerInfo() and markProcessLocalStore(label) are exported from @shadow-js/dusk/node; outside production a worker sends x-dusk-worker. The primary warns once that in-memory stores are per worker (dusk start --workers when a page sets revalidate; serve for every store that calls markProcessLocalStore). Without workers, or with 1, nothing of the pool is loaded and the server behaves as before. tools/load gains --workers.

  • Production observability, opt-in and loaded only when used: the serverTiming handler option (W3C Server-Timing with total, route, loaders, render and your own metrics; on in development, off in production; { header, onTimings }; never replaces a header the response already has; for a streamed page the head carries the time to the shell and onTimings gets the full render time), event.timing.measure / event.timing.add and timing() (@shadow-js/dusk/server) with token-checked names (at most 64 characters, at most 20 per request) and descriptions reduced to safe ASCII, child spans for the loader chain (dusk.loaders, dusk.loader), the endpoint (dusk.page, dusk.api, dusk.rpc, ...), the render and the stream (dusk.render, dusk.page.stream) with OpenTelemetry attribute names (http.response.status_code, http.route, error.type) and recordException, each ended exactly once also on abort and timeout, and the metrics handler option (createCounter().add, createHistogram().record; dusk.server.requests and http.server.request.duration). Without a tracer the request path allocates no span. New types: ServerTimingOptions, RequestTimings, EventTiming, MetricsLike, CounterLike, HistogramLike; RequestEvent gains timing. Guide: Configuring a Dusk app, Observability.

  • pwa() (@shadow-js/dusk/vite, opt-in): after the client build it writes manifest.webmanifest from a typed manifest, an offline page (no script, no inline style) and sw.js, a service worker generated as TypeScript and compiled by the build with no runtime library. The worker precaches the build's hashed assets with a content-hash revision per file, reuses unchanged files from the previous build's cache, answers a failed navigation with an app shell or the offline page, and applies runtime rules per path glob (network-first, cache-first, stale-while-revalidate, with entry and age limits and a network timeout). Caches are named by build id and the other builds' caches are deleted on activate. It handles only same-origin GET requests inside the base, never a request with Authorization or a range, never /_dusk/**, and never keeps a response that is not a plain 200, is no-store or private, varies on cookies, or carries a nonce policy. A new version waits until the page asks.

  • @shadow-js/dusk/features/pwa (opt-in subpath): registerServiceWorker (SSR-safe, scoped, waits for load, not in development unless dev: true), useServiceWorkerUpdate() returning { waiting, update }, and unregisterServiceWorker. Guide: Dusk: installable apps and offline.

  • imageVariants() (@shadow-js/dusk/vite, opt-in): at build time it prepares content-hashed AVIF and WebP files at a width ladder (640, 960, 1280, 1920 by default) for the images of public/ that the source mentions, caches the encoded files between builds, warns once when sharp is missing and serves the originals, and warns about originals over a size budget. <Image> writes a <picture> whose sources point at the prepared files, preloads the best prepared format for a priority image, and keeps the optimizer URLs on the <img> as the fallback.

  • defineFonts gains routes (preload the files of some families on some routes only; fontPreloadsFor picks the tags for a path), report (and FontsOutput.report, formatFontReport): every font file with its size and whether every page preloads it.

  • @shadow-js/dusk/auth (opt-in subpath, no runtime dependency): provider-agnostic sign-in for Auth0, Okta, Keycloak, Google, Microsoft Entra, GitHub, Discord and any OpenID Connect issuer. createOidcClient runs the OAuth 2.1 / OpenID Connect authorization code flow with PKCE (S256), state and nonce kept in the session for ten minutes and one use, discovery with caching, ID token verification with WebCrypto (RS256, PS256, ES256, EdDSA; HS256 only for a confidential client that lists it) through a JWKS cache with key rotation and a refetch limit, checks of iss, aud, azp, exp, nbf, iat, nonce and at_hash, the iss response parameter (mix-up defense), userinfo, refresh token rotation with single-flight, and RP-initiated logout with token revocation. Every call to the provider has a timeout, no redirects and a 1 MiB cap; issuers and endpoints must be https (localhost in development). signIn gives the session a new id; requireUser is a Dusk guard. Presets: google, microsoft, auth0, okta, keycloak, github, discord. Also verifyJwt, createJwksCache, OidcError, isOidcError. Guide: Dusk: sign-in with OAuth and OpenID Connect.

  • Scalable caching: request coalescing (single flight) for the page cache and the data cache with a cap on requests per key (cache.maxWaiters, 1,000) and on keys in flight (cache.maxInFlight, 10,000; maxInFlight of the data cache is now 10,000 too), every request getting its own response or copy of the value, failures shared and not cached, a leaving request not stopping the shared work unless every request left (499 for a page request whose client left; signal of cacheData; cachedFetch fetches with the signal of the shared run), and a render timeout (cache.renderTimeoutMs, 30 s; timeoutMs of the data cache).

  • Stale-while-revalidate: the background refresh of a stale page or value goes to the platform's waitUntil when the request has one, a failed refresh keeps the stale copy, is reported once and backs off (cache.backoffBaseMs 1 s, cache.backoffMaxMs 60 s), pages answer with Age and s-maxage equal to their lifetime, and a render or run that began before a purge of one of its tags does not store its result after it.

  • redisStore(client, options) (@shadow-js/dusk/stores/redis): a page cache store over a five-command Redis client interface (no Redis package), with a version tag, an entry size cap, key prefixes, tag indexes kept as sets (no SCAN), a tag purge that does not lose the index of a concurrent write, and an optional tag index expiry. CacheStore gained an optional purgeTag.

  • runStoreContract(store) and STORE_CONTRACT (@shadow-js/dusk/stores/contract): the scenarios every page cache store passes; the memory store, the KV adapters and redisStore run them.

Changed

  • The page cache key is hashKey([host, path, query, cookies]) (the query key format); the KV page cache adapter stores a key or tag longer than 512 characters under its digest. createKeyValueCacheStore().tags() lists only keys whose entry still exists and carries the tag.

  • --json on every command of the dusk bin, in one documented contract (schema 1: schema, command, ok, data, warnings and, on failure, error: { code, message } with a stable DUSK_ code). build, doctor, completion, --help and --version print one document; dev and start print JSON Lines (ready with the address, request, stopped). Stdout carries only JSON, logs go to stderr, nothing is asked, a refused command line and an uncaught exception are documents too, and the exit codes are unchanged. The build summary is the data of dusk build --json and the doctor checks are data.checks; the shell completion scripts offer the flag.

  • File routes, server functions, API routes, middleware and request security.

  • Server rendering, streaming, prerendering, client hydration, metadata, assets and deployment adapters.

  • Server HTML applies matched route stylesheets; client navigation inherits the server URL policy.

  • Native Bun, Deno and Cloudflare WebSocket bridges share guarded upgrades, message limits and untracked callbacks.

  • WebSocket cleanup releases connection slots on failed closes and bounds heartbeat timers; Deno graceful shutdown waits for native close handshakes up to its configured deadline.

  • Node layouts declare their WebSocket and static-file middleware support; Vercel and Netlify edge targets declare explicit-event request context. The deploy guide checks its generated capability table against adapter factories.

  • serve({ compress }) takes level, types (replaces the default compressible list) and filter(request, response), which skips compression unless it returns true, so responses that reflect a secret can stay out of compression (BREACH). Options are validated when the server starts: an unknown name, a number out of range or a types or filter of the wrong kind throws with the option's name. zstd is negotiated by q-value after brotli and before gzip where node:zlib has it at run time (dynamic responses and .zst files next to a static file); Node 22.12 keeps brotli and gzip.

  • The Node server sends 103 Early Hints to a browser navigation (Sec-Fetch-Mode: navigate, HTTP/1.1) to a page before the loaders run: the stylesheets, entry script and matched routes' preloads from the build, same-origin only, at most 8 (serve({ earlyHints }), on by default). The final response keeps the same Link header on every adapter. Adapters declare earlyHints in their capabilities (Node layout and Tarout), and the deploy guide's generated capability table has an Early Hints column.

  • Typed streaming server functions deliver AsyncIterable items with codec preservation, consumer backpressure, bounded frames/items/duration, cancellation and safe terminal errors. The compiler emits an opt-in client stub; useStream owns disposal, reactive inputs and explicit reconnects.

Fixed

  • Guard combinators stop after cancellation, including asynchronous alternatives; iterator callbacks and validation retain their request context.
  • Streaming validation does not read inference-only schema metadata or access schemas before guards pass.
  • A rolling restart of a worker pool no longer loses a connection: the primary stops handing a worker connections before the worker closes its listener (worker.disconnect()), and the worker answers that with its soft shutdown, so no idle connection is cut either. A connection handed over in the gap used to get no answer at all.
  • Vite 7.0 and newer are supported next to Vite 8: the plugin picks rollupOptions or rolldownOptions, esbuild or Oxc, by what the installed Vite exports (src/vite/compat.ts).
  • build.budgets.enforce: with true, dusk build fails (exit code 1, DUSK_BUDGET_EXCEEDED in --json) when a page is over its first-load budget, after the summary is printed. The default stays a warning.
  • imageVariants() also prepares the images the code imports (import hero from "./hero.jpg"), keyed by the URL the bundler wrote (the CDN URL with assetOrigin). configureImageVariants adds to the images it already has when called again.
  • subsetFonts (@shadow-js/dusk/vite): cuts local font files into slices by Unicode range (FONT_SUBSETS: latin, latin-ext, vietnamese, cyrillic, greek, arabic, hebrew, devanagari, or your own) as WOFF2 files under public/, with the unicode-range of each, for defineFonts. subset-font is an optional peer.