You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
ess-ui/1: a renderer-neutral UI document format (schemas/ui/ess-ui.schema.yaml) with
shells, pages, sections, composites, primitives, widgets, reads, commands, live channels and
state placement per node. ess ui load loads a document, ess ui check reports findings by
canonical node path (ess-ui-check/1, with --model against an ESS specification), ess ui docs renders the format reference from its schema, ess ui run --tui runs a document
in the terminal against its fixtures, and ess generate ui --target react writes a Vite +
React + TypeScript project. Example: examples/partner-portal/.
The system crate's SystemEvent has name() -> &'static str: the qualified name the
specification declares each variant's event under. Generated for every variant, including
events a generated binding delivery reacts to or escalates into.
Where a component is reached_by: network, the server's wire module has encode_system_event(&SystemEvent) -> String, covering every SystemEvent variant and writing {"event": "<qualified name>", "payload": {…}} — the same envelope a command's answer lists a
published event in. In --layout crate it is server::wire::encode_system_event, behind the server feature. A runner names and encodes what the system's log carries without keeping an
event table of its own.
System::take_published() -> Vec<SystemEvent> (Rust) and System.TakePublished() (Go) take
every event the pump has already delivered off the log. A pump returns with every logged event
delivered — a binding whose attempt stopped holds the event in its own held-back list, not on
the log — and events published since the last pump stay for the next one. Where a binding's
delivery is generated, take_invocations() / TakeInvocations()
take the record of what the bindings invoked. A shell that pumps itself calls them to keep a
long-running process bounded.
server::http::Request has headers: Vec<(String, String)>: every request header in arrival
order, the name lower-cased and the value trimmed, a repeated name kept twice. Routing does not
read them; a shell reads authorization there to authenticate the caller before dispatch. http::read keeps at most http::MAX_HEADERS (100) headers and answers 431 beyond that.
The Go target's server package has an encodeEvent… encoder for every generated event.
Changed
Every surface that answers a command now answers with published: every event the outcome
published, in publication order, each {"event": "<qualified name>", "payload": {…}}. That is
the Rust HTTP surface, each surface's transport-free handle, and the Go HTTP surface; the web
bridge's run already listed them in that shape. The created identity of a creating command is
in its event's payload. An outcome that publishes nothing answers "published": []. The other
members (outcome, error, payload) are unchanged. Before this the Rust and Go servers'
answers never carried the events; only the component-level wire::encode_outcome_* listed them.
The OpenAPI projection declares it. Every command response schema requires published: one prefixItems entry per event the branch emits, in emit order, each {event: <const qualified name>, payload: {$ref: <Event>.Event}}, with items: false, so the same events in another
order or one of them twice is not the branch's answer; a branch that emits nothing has at most
zero items. Each emitted event's payload schema is added to components.schemas as <qualified name>.Event. The wording "published to consumers rather than returned here" is
replaced: the events are published to consumers and listed under published.
501 now has two meanings, and the contract declares it. Every command operation in the
OpenAPI projection, and in the contract a generated server serves, gains a 501 response
({"refused": <text>}): the realization is unfinished. Either a port the command runs reported
an unmet obligation — as before — or, new, the command's effect was committed and delivering what this command published to a binding failed; then refused begins delivering what the command published, and each binding whose delivery failed keeps its event and is attempted again on a
later pump, while no other binding receives it twice. An event an earlier command left
undelivered never makes a later command's answer 501. A client must not retry a 501:
after a failed delivery a retry performs the command a second time. The Rust and Go servers,
and handle's entry::Refused::Unmet, answer it the same way. The response-status set of every
command grows by 501, so a client generated from an earlier document sees a new status.
This is a wire change for existing clients. Every command answer gains a required member,
also for models whose commands publish nothing. A client that reads members by name is
unaffected. A client generated from or validating against an earlier OpenAPI document, whose
response schemas are additionalProperties: false, rejects the new answers until it is
regenerated from the new document.
The served dispatch — the HTTP route and handle, Rust and Go — pumps after every command, so
every binding reacting to what it published is delivered before the answer, and then takes the
delivered events and the binding record off the system. A long-running generated server no
longer grows its log, its binding record or a component's outbox with every request. An
in-process caller that read System::published() after dispatch or handle now finds it
empty: the command's events are in the answer, and events a binding's command published in turn
were delivered and taken. The log is taken whatever the pump answered. When delivering this
command's events fails the command's effect stands and the answer is the unmet obligation
(501, entry::Refused::Unmet); the binding that failed holds the event for a later pump.
The served functions (serve, dispatch, handle) are bounded by what System::pump needs —
each component's behaviours by its own port, and the system's obligations where it has any —
instead of every component's behaviours by the served component's port.
Breaking for a caller that builds a Request: server::http::Request gains the public
field headers: Vec<(String, String)>. A shell that builds a Request with a struct literal
(to call dispatch without a socket) no longer compiles until it supplies headers (for
example headers: Vec::new()). A shell that only receives Requests from http::read is
unaffected.
A command whose contract requires no body — one with no input, or whose every input field is
optional — is answered for a POST with no body or an empty (whitespace-only) body, as its
input {}, by the Rust and the Go server alike. Before, such a request was refused 400 "the
body is not JSON" although the contract declares no required requestBody. The document and
the servers read that fact from one function, ess_gen::http::body_required.
The generated Go server answers one request at a time against the system: the served dispatch
holds one package-level lock from reading the input through the port call, Pump and TakePublished. net/http serves each connection on its own goroutine, and concurrent commands
on one shared log could livelock Pump or race the realization's state. Requests still read
their bodies concurrently.
The pump — Rust System::pump, Go System.Pump — tracks delivery per binding, with a
held-back list per binding. Every binding reacting to an event gets one attempt at it, and the
pump passes the event once each has had its attempt, so an event that keeps failing no longer
blocks every event after it. A binding whose attempt an unmet obligation stopped holds the event
in its own held-back list when it declares at_least_once; so does a declared refusal its on_failure: retry answers, which before held the event for every binding. Each pump attempts
the held events again for their own binding alone, after the events published since; one that
fails again stays held and does not fail the pump. pump's Err / Pump's result is the first
failure among the events it delivered for the first time — what was published since the last
pump and what delivering that published — so a served answer is 501 only when delivering its
own command's events failed. Before, an unmet obligation stopped the whole pump at the event:
every later pump stopped there again, every later served command was answered 501, and the
bindings beside the failing one either lost the event or were delivered it again.
The pump never delivers an occurrence again to a binding that already ran. An at_most_once
binding runs once per occurrence: its stopped attempt is reported and not repeated, unless its
own on_failure: retry puts the attempt back, and redeliver / Redeliver — which runs every at_least_once binding again — leaves it out. A failed selection ends that binding's attempt,
not the event's: the other reacting bindings still run, and it is not held back, because its
policy was applied.
The web bridge runs the same Rust pump after every command, so it too keeps answering after an
event that keeps failing: the command whose event stuck is refused unmet-obligation, and
every command after it comes back with its own outcome.
ess verify diff no longer reports system/<system>/unclassified-changed beside an added or
regrouped aggregate view: aggregation is compared as grouping-changed and field-aggregate-changed and is no longer left in the residual (#256).
ess import openapi reads a closed tuple (prefixItems with items: false, or maxItems: 0),
so the contracts ess generate now writes, whose published list is one, import without
refusals.