Skip to content

Releases: fahchen/musubi

v0.7.2

04 Jun 22:22
013582a

Choose a tag to compare

Fixed

  • @musubi/reactuseMusubiRootSuspense no 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 .then ran 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 spurious unmount + mount burst 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 (key as a real SharedRootMount field, cancelCleanupTimer / buildMountOptions helpers) with no behavior change (#70).

Full changelog: v0.7.1...v0.7.2

v0.7.1

31 May 06:16
0d74d48

Choose a tag to compare

Fixed

  • Transport / @musubi/client — Restored multi-observer ergonomics
    regressed in 0.7.0. The server's :already_mounted reply on duplicate
    (module, id) now carries the existing root_id; the client aliases
    to its local RootConnection, bumps a local refCount, and shares one
    StoreProxy across all consumers. The last unmount defers 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
    MusubiInconsistencyError instead of being swallowed. Dev-mode warns
    when an alias has different params than the original mount. Wire
    protocol additions: :already_mounted :error reply 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
    .catch so rejecting before any awaiter is observing is safe, and
    the mount push is settled synchronously via a cancelMountPush hook
    so the caller doesn't wait for Phoenix's push timeout. Version-mismatch
    recovery that hits a stale :already_mounted reply (server still
    has our entry after our recovery unmount push 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 awaiting unmount()
    caller through a pendingUnmountResolver rather than hanging it
    forever. The grace timer skips teardown when a concurrent mount for
    the same (module, callerId) is in flight, and
    mountConnectionRoot's finally re-arms teardown for any root left
    orphaned because that pending mount then settled :error instead of
    aliasing. channel.leave() is now called with connectionState.channel
    pre-cleared and inside a try/finally that guarantees roots and
    the runtime entry are dropped even if leave() throws synchronously.
    handleConnectionDisconnect now clears connectionState.roots (was
    only disconnectConnectionState) so a subsequent mount on the
    reconnecting state can't alias to a disconnected entry. Server-side
    unmount-push failures are now logged via console.warn instead of
    bubbling to the consumer's await mounted.unmount() — local state
    is already torn down by then; the release promise resolves cleanly.

v0.7.0

31 May 01:24
87f544f

Choose a tag to compare

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 literal root_id values must
    update. See spec/domains/runtime/features/connection-root-identity.feature (#65).

v0.6.1

30 May 09:26
4216285

Choose a tag to compare

[0.6.1] — 2026-05-30

Fixed

  • TransportMusubi.Transport.Socket.build_connect_socket/2 no
    longer crashes the WebSocket handshake with FunctionClauseError when
    Phoenix's cookie session store delivers connect_info = %{session: nil} (the shape it produces on a cookieless first visit). The handler
    now normalizes nil to %{} before passing the session through to
    Musubi.Socket.put_session/2 (#63).
  • @musubi/react — Drop the react ^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 #525 on the first Suspense render. React
    is now hoisted at the repo root and pinned via pnpm.overrides; the
    package's public peerDependencies (react ^18.2.0 || ^19.0.0) is
    unchanged (#63).
  • @musubi/reactuseMusubiRootSuspense no 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's useId
    claim to a Set<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 while refs > 0 (a committed
    consumer owns the lifecycle). Falls back to "cleanup on channel
    termination" on hosts that lack FinalizationRegistry. (#63).

v0.6.0

27 May 23:55
f3e315e

Choose a tag to compare

Added

  • Musubi.Testing.dispatch_command/4 now accepts a native (atom-keyed, atom-valued) payload and wire-encodes it via Musubi.Wire.to_wire/1 before dispatch, so handle_command/3 receives 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 egress to_wire encoding of command replies (#59).

Full Changelog: v0.5.0...v0.6.0

v0.5.0

27 May 12:36
54d5182

Choose a tag to compare

Changed

  • Command replies are now returned in native Elixir shape (atom keys, structs, atom values), symmetric with render/1; Musubi.Wire.to_wire/1 moves to the transport egress (#59). Revises #57. Client wire contract unchanged. Breaking (Elixir API): tests asserting wire-shaped replies from dispatch_command/3 / command/4 must switch to native shape.

Full Changelog: v0.4.0...v0.5.0

v0.4.0

26 May 14:12
b6dbb45

Choose a tag to compare

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_command hooks and [:musubi, :auth, :deny] telemetry still see the raw reply (atom keys/values).

Added

  • Musubi.Wire support for DateTime/NaiveDateTime/Date/Time (ISO8601) and URI (string) (#57). MapSet, Decimal, and tuples stay unhandled and raise Protocol.UndefinedError — convert first.

Full Changelog: v0.3.0...v0.4.0

v0.3.0

20 May 00:28
67534d7

Choose a tag to compare

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

18 May 13:49
9029422

Choose a tag to compare

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

17 May 08:31
fa94655

Choose a tag to compare

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