Releases: fahchen/musubi
Releases · fahchen/musubi
v0.7.2
Fixed
@musubi/react—useMusubiRootSuspenseno longer double-mounts a root over the wire on react-router v7 SPA navigation. When the last render-phase claimer dropped while the mount was still in flight, the orphan sweep chained teardown inline on the mount promise; that.thenran in the same microtask flush as the mount settle — ahead of React's MessageChannel-scheduled resumed Suspense render — so the resumed render found no shared entry and allocated a fresh mount, producing a spuriousunmount+mountburst right after the first patch. Teardown is now deferred two macrotask hops past the settle (React's render-phase then commit-phase) so the resumed render re-claims the entry first and the existing ref / claimer guards bail. Also tidied the shared-mount bookkeeping (keyas a realSharedRootMountfield,cancelCleanupTimer/buildMountOptionshelpers) with no behavior change (#70).
Full changelog: v0.7.1...v0.7.2
v0.7.1
Fixed
- Transport /
@musubi/client— Restored multi-observer ergonomics
regressed in 0.7.0. The server's:already_mountedreply on duplicate
(module, id)now carries the existingroot_id; the client aliases
to its localRootConnection, bumps a local refCount, and shares one
StoreProxyacross all consumers. The lastunmountdefers the server
push via a brief grace timer so a route-swap remount within the same
React commit batch cancels the teardown. Out-of-sync state (server
reports mounted, client has no record) surfaces as
MusubiInconsistencyErrorinstead of being swallowed. Dev-mode warns
when an alias has differentparamsthan the original mount. Wire
protocol additions::already_mounted:errorreply payload now
carries"root_id". The 0.7.0 cross-module isolation
("<module>:<caller-id>"composite root_id) is unchanged (#67). @musubi/client— Hardened the mount / unmount / disconnect
interplay against several real edge cases (#68). Mid-mount disconnect
no longer surfaces as an unhandled rejection: the in-flight
tentative's initial-patch waiter is now shielded by a pre-attached
.catchso rejecting before any awaiter is observing is safe, and
the mount push is settled synchronously via acancelMountPushhook
so the caller doesn't wait for Phoenix's push timeout. Version-mismatch
recovery that hits a stale:already_mountedreply (server still
has our entry after our recoveryunmountpush failed to land) no
longer hangs forever waiting for an initial patch the server won't
re-emit — it logs and force-cascades a full
disconnectConnectionState(channel left + runtime entry removed)
so consumers see a clean tear-down. Grace-timer cancellation
(alias-remount, disconnect) now settles the awaitingunmount()
caller through apendingUnmountResolverrather than hanging it
forever. The grace timer skips teardown when a concurrent mount for
the same(module, callerId)is in flight, and
mountConnectionRoot'sfinallyre-arms teardown for any root left
orphaned because that pending mount then settled:errorinstead of
aliasing.channel.leave()is now called withconnectionState.channel
pre-cleared and inside atry/finallythat guaranteesrootsand
the runtime entry are dropped even ifleave()throws synchronously.
handleConnectionDisconnectnow clearsconnectionState.roots(was
onlydisconnectConnectionState) so a subsequent mount on the
reconnecting state can't alias to a disconnected entry. Server-side
unmount-push failures are now logged viaconsole.warninstead of
bubbling to the consumer'sawait mounted.unmount()— local state
is already torn down by then; the release promise resolves cleanly.
v0.7.0
Changed (breaking — wire protocol)
- Transport /
@musubi/client— Connection roots are now identified
by(module, caller id); the server composes and assigns the wire
root_id, which the client treats as opaque. Fixes silent state
corruption when two roots shared a caller id. Duplicate(module, id)
on one connection is rejected with:already_mounted. Client-side
dedup removed. Tooling that pinned literalroot_idvalues must
update. Seespec/domains/runtime/features/connection-root-identity.feature(#65).
v0.6.1
[0.6.1] — 2026-05-30
Fixed
- Transport —
Musubi.Transport.Socket.build_connect_socket/2no
longer crashes the WebSocket handshake withFunctionClauseErrorwhen
Phoenix's cookie session store deliversconnect_info = %{session: nil}(the shape it produces on a cookieless first visit). The handler
now normalizesnilto%{}before passing the session through to
Musubi.Socket.put_session/2(#63). @musubi/react— Drop thereact ^18.3.0/react-dom ^18.3.0
devDependencies that were causing pnpm-workspace consumers on React
19 to ship two React copies in their production bundle and crash
with minified React error#525on the first Suspense render. React
is now hoisted at the repo root and pinned viapnpm.overrides; the
package's publicpeerDependencies(react ^18.2.0 || ^19.0.0) is
unchanged (#63).@musubi/react—useMusubiRootSuspenseno longer wedges
Suspense in an infinite mount/unmount loop under React 19. The
previous timer-based orphan sweep raced React 19's
MessageChannel-scheduled commit and tore the mount entry down before
any consumer could claim it. The cleanup path is now a
FinalizationRegistry-backed safety net: each render-phase mount
allocates a fresh unregister token and adds the fiber'suseId
claim to aSet<claimerId>on the shared entry. The finalizer
fires only after React releases the discarded fiber, drops this
fiber's claim, and bails while the set is non-empty (other sibling
consumers still hold the entry) or whilerefs > 0(a committed
consumer owns the lifecycle). Falls back to "cleanup on channel
termination" on hosts that lackFinalizationRegistry. (#63).
v0.6.0
Added
Musubi.Testing.dispatch_command/4now accepts a native (atom-keyed, atom-valued) payload and wire-encodes it viaMusubi.Wire.to_wire/1before dispatch, sohandle_command/3receives the same string-keyed map a real client delivers (#61). Tests can write%{by: 3}instead of%{"by" => 3}; the encode is idempotent on existing string-keyed payloads, so this is non-breaking. Symmetric with the egressto_wireencoding of command replies (#59).
Full Changelog: v0.5.0...v0.6.0
v0.5.0
Changed
- Command replies are now returned in native Elixir shape (atom keys, structs, atom values), symmetric with
render/1;Musubi.Wire.to_wire/1moves to the transport egress (#59). Revises #57. Client wire contract unchanged. Breaking (Elixir API): tests asserting wire-shaped replies fromdispatch_command/3/command/4must switch to native shape.
Full Changelog: v0.4.0...v0.5.0
v0.4.0
Changed
- Command replies now serialize through
Musubi.Wire(#57). Replies match the wire shape the client receives (string keys, stringified atoms), and schema validation runs against that form — fixing atom-valued and nested reply-field validation.:after_commandhooks and[:musubi, :auth, :deny]telemetry still see the raw reply (atom keys/values).
Added
Musubi.Wiresupport forDateTime/NaiveDateTime/Date/Time(ISO8601) andURI(string) (#57).MapSet,Decimal, and tuples stay unhandled and raiseProtocol.UndefinedError— convert first.
Full Changelog: v0.3.0...v0.4.0
v0.3.0
What's Changed
- fix(examples): declare cart_page command reply types by @fahchen in #51
- docs: show Phoenix endpoint socket wiring in README by @fahchen in #52
- feat(dsl)!: block-form command DSL + reply validation by @fahchen in #53
- feat: file uploads — DSL, transport, wire ops, client/React surface by @fahchen in #54
- chore(release): bump to 0.3.0 + add CHANGELOG.md by @fahchen in #55
Full Changelog: v0.2.0...v0.3.0
v0.2.0
What's Changed
- ci: matrix on Elixir/OTP/Phoenix; upgrade actions to Node 24 native by @fahchen in #46
- feat(client+react): createMusubi factory for one-time store-type binding by @fahchen in #47
- feat(react): structured command errors + useMusubiCommand mutation shape by @fahchen in #48
- feat(react): Suspense + Provider socket prop + polish by @fahchen in #49
- docs: client API v2 sweep — factory, errors, suspense, provider, keyOf by @fahchen in #50
Full Changelog: v0.1.0...v0.2.0
v0.1.0
What's Changed
- docs: add PRD for BEAM hierarchical store runtime by @fahchen in #1
- chore: bootstrap project tooling by @fahchen in #2
- feat(m1): Arbor.Socket + assigns + attr macro (Track B) by @fahchen in #4
- docs: add AGENTS.md by @fahchen in #6
- feat(m1): typed_structor plugins + DSL macros (Track A) by @fahchen in #3
- docs: add Copilot review playbook and PR template by @fahchen in #7
- feat(m1): Page Runtime + hooks + Store Registry (Track C) by @fahchen in #5
- docs: rebrand stream_async as LV-parity; align spec with M1 implementation by @fahchen in #8
- feat(m2): render contract + resolver + validation (Tracks A+B combined) by @fahchen in #11
- feat(m3): command pipeline by @fahchen in #12
- feat(m4): replication, streams, transport adapter by @fahchen in #13
- feat(m5): async lifecycle by @fahchen in #14
- feat(m6): codegen + telemetry + channel + bench by @fahchen in #19
- feat(m6): example apps + persistence pattern guide by @fahchen in #17
- refactor(socket): encapsulate changed access via any_changed?/1 by @fahchen in #20
- refactor(store): rename to_state → render + Arbor.Store behaviour by @fahchen in #21
- feat(streams): tag every wire stream op with its owning store_id by @fahchen in #22
- feat(client): @arbor/client — pure-TS Arbor wire consumer by @fahchen in #23
- feat(react): @arbor/react — React adapter on top of @arbor/client by @fahchen in #24
- feat(examples): wire cart_page + chat_room examples end-to-end by @fahchen in #25
- feat(arbor_ts): emit @arbor/client augmentation + adopt generic store typing across packages + examples by @fahchen in #26
- fix: chrome-test bugs — auto socket.connect + stable empty stream + jason dep by @fahchen in #27
- feat(client): land connectStore contract and type-only codegen by @fahchen in #28
- feat(transport): add connection-scoped root stores by @fahchen in #31
- docs: prepare release documentation by @fahchen in #32
- feat(streams): explicit stream markers and async stream wire shape by @fahchen in #33
- feat(examples/chat_room): stream_async demo + client duplicate-mount fix by @fahchen in #34
- feat: child-targeted commands demo + per-example mix aliases by @fahchen in #35
- refactor: LV-style facade for Arbor.Store + assign_new/3 + update/3 rename by @fahchen in #36
- feat: Arbor.Testing harness, cold-VM fix, JS source-export packages by @fahchen in #37
- fix(reconciler): deep-tree leaf dirty detection + prune-safe reuse by @fahchen in #40
- perf(reconciler): check parent assign value equality before changed-key intersection by @fahchen in #43
- perf(page/server): skip Jsonpatch when wire root is structurally equal by @fahchen in #38
- perf(client): invalidate snapshotCache by op path instead of clearing by @fahchen in #39
- perf(resolver): stitch cached child wire_state into parent wire output by @fahchen in #41
- perf(resolver): skip root render/1 when root socket is unchanged by @fahchen in #42
- chore: rename Arbor → Musubi by @fahchen in #44
- ci: add publish workflow; drop ritual dev dep by @fahchen in #45
New Contributors
Full Changelog: https://github.com/fahchen/musubi/commits/v0.1.0