revali_core 3.2.0
revali_core: ^3.2.0 copied to clipboard
Classes to share between Revali and Revali Annotations
CHANGELOG #
3.2.0 | 08.15.26 #
Features #
- Add
IsolateIdentity.scopeName, the one line aMessageBrokerneeds to avoid naming every worker the same thing. The framework cannot do this for an implementation — it never sees the name, because the implementation builds it — soRedisBrokerbeing correct did nothing for a broker written elsewhere, andrevali_corepublished the isolate index while saying nothing about the obligation that comes with it.scopeNameis now that obligation in one call, documented onMessageBrokerandAppConfig.createBroker()where an implementer is actually looking. It leaves the parent (index0) untouched rather than suffixing it-0, so upgrading an app that never spawns workers does not rename its consumer and strand whatever was pending under the old name. - Document on
AppConfig.createBroker()that it runs in every isolate rather than once for the process. That is what makes a name-keyed broker collide with itself, and nothing said so at the place an app author overrides it.
3.2.0 | 08.15.26 #
Features #
- Add
IsolateIdentity.scopeName, the one line aMessageBrokerneeds to avoid naming every worker the same thing. The framework cannot do this for an implementation — it never sees the name, because the implementation builds it — soRedisBrokerbeing correct did nothing for a broker written elsewhere, andrevali_corepublished the isolate index while saying nothing about the obligation that comes with it.scopeNameis now that obligation in one call, documented onMessageBrokerandAppConfig.createBroker()where an implementer is actually looking. It leaves the parent (index0) untouched rather than suffixing it-0, so upgrading an app that never spawns workers does not rename its consumer and strand whatever was pending under the old name. - Document on
AppConfig.createBroker()that it runs in every isolate rather than once for the process. That is what makes a name-keyed broker collide with itself, and nothing said so at the place an app author overrides it.
3.1.0 | 08.15.26 #
Features #
- Add health probes to
AppConfig.HealthSettings(exposed asAppConfig.health) configures a liveness path (/healthz) and a readiness path (/readyz), a list ofHealthChecks consulted by readiness, and a per-checkcheckTimeout. Either path can be set tonull, or the whole thing disabled withconst HealthSettings.disabled(). - Liveness and readiness answer different questions, and the split is deliberate: liveness failing tells an orchestrator to restart the process, so it keeps returning
200during a graceful shutdown, while readiness flips to503. Failing liveness mid-drain would kill exactly the in-flight requests the drain exists to protect. Liveness also runs no checks — consulting a database there turns one database blip into a restart storm. - Add
AppConfig.drainDelay(defaultDuration.zero, so existing behaviour is unchanged). Closing the listening socket is invisible to a load balancer: it keeps routing until its own readiness probe fails, and every request it sends in the meantime hits a closed socket. This is the window in which readiness reports503while the server can still serve. Behind a load balancer, set it longer than the probe's period times its failure threshold, and keepdrainDelay + shutdownTimeoutunder the platform's kill grace period. - Add
TraceContext, ambient for the whole of a request. A request id that only exists as a header dies at the first hop — a call the handler makes to another service opens a fresh, uncorrelated request, and the two services' logs cannot be joined afterwards.TraceContext.currentcarries the request id, W3Ctraceparent/tracestateand a mutablebaggagemap, reachable from anywhere inside the request without being threaded through.outboundHeaders()produces the headers to forward. Nothing forwards them automatically: what counts as a trusted peer is the app's call, not the framework's. traceparentis propagated, never invented. It is carried verbatim when the caller sent one and left absent when they did not — a fabricated one is worse than none, since a collector will stitch it into the wrong trace. This deliberately stops short of span lifecycles, samplers and exporters.baggageis encoded and parsed in the W3C header format, with keys and values percent-encoded so an unescaped,or=cannot silently split one entry into two. Malformed inbound entries are skipped rather than throwing, since they arrive from another service.- Add
Env, a runtime reader for the process environment:require,string,integer,booleananduri, each with an explicit fallback. A variable set to the empty string counts as unset, because orchestrators and CI routinely inject empty values for variables nobody configured. A value that is present but unparseable throws rather than falling back — someone set it on purpose, and quietly ignoring it is how an app ends up listening on a port nothing routes to. Takes an explicit map in tests, so a suite never mutates the real environment. - Add
AppConfig.fromEnv, taking host and port from the environment at startup. Two defaults differ fromAppConfig.defaultAppdeliberately: the host is0.0.0.0rather thanlocalhost, since a server bound to loopback inside a container refuses every request from outside while looking perfectly healthy; and the port comes fromPORT, which is how Cloud Run, Heroku, Render and Fly assign one. It is notconst— it reads the environment, which is only knowable at runtime — so an app using it cannot have aconstconstructor either. That is the point: a port baked in at compile time cannot be changed by the platform running the image. - Add
HttpError, a failure that survives the hop. A status code alone tells a caller that something went wrong, not what: two different 404s are indistinguishable to the service calling you, so its only options are to give up or to match on a human-readable message that was never meant to be an API.HttpErrorcarries a stablecodealongside the status, plus amessagefor humans and optional machine-readabledetails, and serialises to{"error": {...}}— mirroring the{"data": ...}wrapper successful responses already use. Named constructors cover the usual statuses. Throwing it is opt-in: the framework's existing plain-text default responses are unchanged, so clients reading them today keep working. - Add the messaging contract:
MessageBroker,BrokerMessage,BrokerSubscriptionandConsumerRegistry, plus anInMemoryBrokerfor tests and local development. Revali does not run a broker — like a database it is infrastructure you deploy, and this is the client side. Implementations live in their own packages so the framework never picks a winner between brokers whose delivery and ordering guarantees genuinely differ. ConsumerRegistrygives each message what a request already gets: its ownTraceContext, seeded from the message headers, so an event published during a request stays on that request's trace when it is handled minutes later in another process; its ownRequestScopedDIscope, disposed when the message ends; and a place in shutdown. Draining pauses subscriptions before waiting, rather than cancelling — cancelling abandons messages mid-handler, and on an at-least-once broker every one of them is then redelivered as a duplicate nobody needed.- Add
AppConfig.createBroker(), returning null by default. This is what makes messaging opt-in: an app that supplies no broker registers no consumers even when handlers are annotated, and the broker it does supply is drained and closed as part of shutdown. - Add
IsolateIdentity, which says which isolate of the app the caller is running in —index,workerCount, andisWorkerderived from the index rather than stored, so the two cannot disagree. WithAppConfig.workersabove 1 the same program runs several times over and nothing in it could tell the copies apart, which matters to anything that identifies itself to an external system by name: every isolate picks the same one.createBroker()is exactly that case and takes no arguments, so it could not have been handed the answer — an ambient fact is what reaches code that has no parameter to receive it, and widening that signature would have been a breaking change to every app that overrides it. Statics in Dart are per-isolate, so each isolate genuinely holds its own copy with nothing shared and no race to guard: that is not a caveat about the mechanism, it is the mechanism, and it is the same reason the generated server's private worker flag works. Unset it describes the parent of a single-isolate app, so a unit test or an app that never spawns workers observes something true having configured nothing, and there is no null to handle.
3.0.0 | 08.13.26 #
Breaking Changes #
Observer.seetakes oneObservedRequestinstead of(Request, Future<Response>). Migration is mechanical:see(request, response)becomessee(observed), withobserved.requestandobserved.responsein place of the parameters, andobserved.summarynewly available. One interface rather than two — an observer that only wants the finished picture awaitsobserved.summaryinstead of implementing a second type.- Remove the deprecated
DIregistration methods.registerInstance<T>andregister<T>are gone fromDI,DIImpl,DIHandler, andRequestScopedDI; useregisterSingleton<T>andregisterFactory<T>/registerLazySingleton<T>instead. TheFactory<T>typedef is unchanged.
Features #
- Add
RequestSummaryandObservedRequest, giving observers what they could not previously reach: how a request turned out.Observer.seenow takes a singleObservedRequestcarrying therequest(available immediately) plus futures for theresponseand asummary— method, path, matched route path, status, duration and error. Label metrics withroutePath(/api/users/:id) rather thanpath(/api/users/42); that is the difference between one time series and one per id.seealso accepts aFutureOrreturn, so an observer that reports immediately need not beasync. - Add
CompressionSettings, exposed asAppConfig.compression. Responses are gzipped by default for clients that sendAccept-Encoding: gzip, above a 1 KB threshold and only for text-shaped mime types. UseCompressionSettings.disabled()when a CDN or reverse proxy already compresses. - Add request-scoped dependencies.
DI.registerRequestScoped<T>buildsTonce per request and shares it for the rest of that request, with nothing shared between requests — the missing middle betweenregisterSingleton(whole process) andregisterFactory(every resolution). Instances implementing the newDisposableinterface are released when the request ends, in reverse creation order, whether it succeeded or threw. Resolving a request-scoped type outside a request throws rather than silently handing back an undisposed instance shared with nobody.RequestScopedDIis now a working per-request container rather than a stub: it caches what it builds, tracks it for disposal, and finds registrations through theDIHandlerwrapper via the newRequestScopedRegistryinterface. - Add graceful-shutdown configuration to
AppConfig:handleShutdownSignals(defaulttrue) to opt out of signal handling,shutdownTimeout(default 15s) to bound how long in-flight requests are awaited, and anonServerStoppedhook that runs once they have drained so the app can release databases, consumers and file handles.
Fixes #
- Add
SetCookies.headerValues(), returning one formattedSet-Cookieline per cookie instead of an invalid comma/semicolon-joined line (RFC 6265 §4.1.1). Publishedrevali_router4.0.2 already calls this method against theSetCookiesinterface, so any project resolvingrevali_core2.0.0 alongside it fails to compile. - Reflect
https(nothttp) in the "Serving at ..." startup log line when TLS is enabled via--cert/--keyorAppConfig.secure.
2.0.1 | 08.07.26 #
Fixes #
- Add
SetCookies.headerValues(), returning one formattedSet-Cookieline per cookie instead of an invalid comma/semicolon-joined line (RFC 6265 §4.1.1). Publishedrevali_router4.0.2 already calls this method against theSetCookiesinterface, so any project resolvingrevali_core2.0.0 alongside it fails to compile. - Reflect
https(nothttp) in the "Serving at ..." startup log line when TLS is enabled via--cert/--keyorAppConfig.secure.
2.0.0 | 08.04.26 #
Breaking Changes #
- Merge
revali_router_coreinto this package (that package is deprecated).Request,Response,Guard,Middleware,Interceptor,ExceptionCatcher,Observer,CombineComponents,ResponseHandler,Meta,MetaScope,Reflect,RouteEntry,TrustedProxy,Cookies,Headers,Body, and related types now live here. - Move
AllowOrigins,PreventHeaders, andExpectHeadershere fromrevali_annotations(still re-exported from there for compatibility).
Features #
- Add
AppConfig.workersfor multi-isolate serving (shared: truewhen > 1). - Add
AppConfig.backlogto control theHttpServer.bindlisten backlog.
1.6.0 | 06.17.26 #
Features #
- Add
RequestScopedDIfor request-scoped dependency injection, installable via a request wrapper andZone.
1.5.0 | 04.28.26 #
Features #
- Add
runStartuponAppConfigso apps can wrap async server startup (bind, DI, routes); the default implementation forwards to the providedstartcallback unchanged.
1.3.0 | 04.09.25 #
Features #
- Add
registerLazySingletonandregisterFactorymethods toDIinterface- This is to support
factories, so that dependencies can be re-created each time they are resolved
- This is to support
Future BREAKING Changes #
DI.registerwill be removed in favor ofregisterLazySingletonandregisterFactoryDI.registerInstancewill be removed in favor ofregisterSingleton
1.1.0 | 12.11.24 #
Features #
- Abstract
DIclass to support creating own instances ofDI - Create
DIHandlerto override dependency registry during server startup - Add
initializeDImethod to support creating own instances ofDI
1.0.0 | 11.14.24 #
- Initial Release