Changelog
What changed in each release, and what — if anything — you have to do about it. The migration notes Stability & Versioning points at.
Every @nice-code/* package releases in lockstep at one version,
so an entry here covers the whole set. Each release says what changed and, where it matters, what
you have to do — usually nothing.
While we are on 0.x, a 0.MINOR.0 may carry a breaking change. When it does, it is called out
here under Action required. A 0.x.PATCH is fixes only.
0.90.0
Section titled “0.90.0”Plain HTTP now actually works. 🐛 A real bug, fixed — no action needed beyond upgrading.
From 20 June 2026 until this release, a plain (unhandshaked) HTTP pairing was broken end to end:
the plain connector always sent the { k: "act", w } exchange envelope, and the plain endpoint
never unwrapped it. The reply direction was broken the same way. This affected:
serveDurableObject’s defaulthttpFallback: "plain"serveWorker({ secure: false })- any client on
{ carrier: httpCarrier(...), secure: false }
It went unnoticed for two months because the plain endpoint is normally a fallback behind a preferred WebSocket, so nothing exercised it. Secure pairings were never affected.
What changed, all inside createActionFetchHandler’s plain branch (which serveChannel,
serveWorker and serveDurableObject all route through):
- The envelope is decoded with the byte-frozen codec the secure acceptor uses, rather than a
hand-rolled unwrap. An enveloped request is answered with an envelope; a raw action wire is
still accepted and answered raw with its HTTP status, so
curland older peers keep working. - Every reply to an enveloped request is a 200 envelope, refusals included.
Action required: none — your code is already written the way the fix expects. If part of your
estate pins ≤ 0.89.0, use httpFallback: "secure" or a WebSocket-only transport until you upgrade.
Worth knowing, since it was verified while fixing this: a plain client against a secure acceptor
works. secure: false on the connector produces the tokenless envelope, which
httpAcceptorCarrier() accepts at level none — so a read-only public client needs no identity of
its own. See Security Levels and
Cloudflare Durable Objects.
0.89.0
Section titled “0.89.0”Release hygiene — package versions realigned after a mis-sequenced cycle, plus a fix to
@nice-code/commander’s terminal output for process logs. No library behaviour changed.
0.87.0
Section titled “0.87.0”Two consumer-reported issues, both behavioural. ⚠️ One changes when an existing error throws —
read the @nice-code/state entry if you use Store.update.
Connection failures are typed and self-locating
Section titled “Connection failures are typed and self-locating”Dial failures used to flatten to a message string: no endpoint, no discriminator, nothing to branch on but text. All three layers held the evidence and dropped it. Now:
- The HTTP carrier throws with status, content-type and a sanitized body preview on a non-2xx
response, and
endpoint_unreachableon afetchrejection (keeping the platform’s own wording). A deliberate abort is rethrown untouched and never mistaken for an endpoint verdict. - The handshake names what came back when the reply isn’t a protocol frame — which is what
catches a proxy answering
200with a login page. err_nice_transport’sinitialization_failedcontext gainedendpoint(the URL that transport actually dialed) andkind, anEWireConnectFailureKinddiscriminator.- New:
classifyConnectFailure(error)andendpointFromConnectFailure(error).classifyConnectFailureandEWireConnectFailureKindare re-exported from@nice-code/action, so an actions consumer needs no direct@nice-code/wireimport.
Action required: none, but if you were matching on error message text to tell “server down” from
“wrong URL”, replace it with a kind check — that is what it is for. See
Error Handling and the
kind table.
@nice-code/state — the returned-state guard is no longer data-dependent
Section titled “@nice-code/state — the returned-state guard is no longer data-dependent”Store.update is mutate-only: an updater’s return value is ignored, and returning a replacement
while leaving the draft untouched throws rather than silently doing nothing.
The old guard judged this by whether the draft happened to change, which made the throw depend
on the data. store.update((draft) => Object.assign(draft, next)) passed on every run where next
differed from the current state, then threw on the one reset where it was identical — in
production, long after the code was written.
The guard now splits on draft identity: a returned draft always passes, a returned replacement with an untouched draft always throws. Same rule, deterministic outcome.
Action required: none. This only ever turns a spurious throw into a pass. If you added a
defensive braced body ((draft) => { Object.assign(draft, next); }) to work around it, you can keep
it — it stays correct. See Stores.
Before 0.87.0
Section titled “Before 0.87.0”Not recorded here. Use the git tags, or ask — if you hit something that looks like a behaviour change between two earlier versions, we would rather answer it than have you guess.