Current section
19 Versions
Jump to
Current section
19 Versions
Compare versions
9
files changed
+546
additions
-1034
deletions
| @@ -11,6 +11,45 @@ not in lockstep yet; entries note which surface they affect. | |
| 11 11 | |
| 12 12 | ## [Unreleased] |
| 13 13 | |
| 14 | + ## [0.12.0] — 2026-06-27 |
| 15 | + |
| 16 | + ### Fixed |
| 17 | + |
| 18 | + - **`musubi` / `@musubi/client`** — A WebSocket reconnect no longer leaves a |
| 19 | + root store permanently unrecoverable (`Command "…" failed: Store is not |
| 20 | + connected` until a full page reload). The whole connection multiplexed every |
| 21 | + root over one `musubi:connection` topic/channel; on a drop, Phoenix's |
| 22 | + automatic rejoin of the (never-`leave()`d) channel collided on that single |
| 23 | + topic with the new channel the recovery path opened, Phoenix closed the |
| 24 | + duplicate join, the state machine derailed, `mount` was never re-sent, and the |
| 25 | + server's `terminate/2` `stop_root` was never reversed. Each root store now |
| 26 | + owns its own channel, so recovery rides Phoenix's per-channel rejoin with no |
| 27 | + collision (see Changed). |
| 28 | + |
| 29 | + ### Changed |
| 30 | + |
| 31 | + - **`musubi` / `@musubi/client`** — **Breaking wire protocol.** Each root store |
| 32 | + now gets its own channel on topic `musubi:connection:<root_id>` instead of one |
| 33 | + shared `musubi:connection` channel multiplexing all roots by `root_id`. Join |
| 34 | + carries `{module, id, params}` and **is** the mount (the join reply returns the |
| 35 | + canonical `root_id`); leaving the channel (client `leave()` or a transport |
| 36 | + drop) **is** the unmount and stops that one root via `terminate/2`. Reconnect |
| 37 | + recovery now reuses Phoenix's built-in per-channel rejoin: the |
| 38 | + `join().receive("ok")` hook re-fires on every rejoin and rebuilds the root, |
| 39 | + keeping the last-good snapshot until the server's fresh initial patch swaps it |
| 40 | + in. The `command` / `allow_upload` / `cancel_upload` / `upload_progress` / |
| 41 | + `upload_error` payloads no longer carry a `root_id` (one root per channel). |
| 42 | + Client and server must be upgraded together. |
| 43 | + |
| 44 | + ### Removed |
| 45 | + |
| 46 | + - **`@musubi/client`** — `SocketLike.onOpen` is gone (added in 0.11.0 to drive |
| 47 | + the socket-level reopen handler). Recovery is now per-channel, so the |
| 48 | + socket-level `onOpen`/`handleSocketReopen`/`reestablishConnectionRoots` |
| 49 | + machinery — and the `mount` / `unmount` / `already_mounted` channel messages — |
| 50 | + were removed. Custom `SocketLike` implementations need only `connect` and |
| 51 | + `channel`. |
| 52 | + |
| 14 53 | ## [0.11.1] — 2026-06-25 |
| 15 54 | |
| 16 55 | ### Fixed |
| @@ -388,7 +427,8 @@ Initial public release of the Musubi runtime (then `Arbor`): | |
| 388 427 | - TypeScript client and React adapter that materialize the diff stream |
| 389 428 | into immutable snapshots. |
| 390 429 | |
| 391 | - [Unreleased]: https://github.com/fahchen/musubi/compare/v0.11.1...HEAD |
| 430 | + [Unreleased]: https://github.com/fahchen/musubi/compare/v0.12.0...HEAD |
| 431 | + [0.12.0]: https://github.com/fahchen/musubi/compare/v0.11.1...v0.12.0 |
| 392 432 | [0.11.1]: https://github.com/fahchen/musubi/compare/v0.11.0...v0.11.1 |
| 393 433 | [0.11.0]: https://github.com/fahchen/musubi/compare/v0.10.0...v0.11.0 |
| 394 434 | [0.10.0]: https://github.com/fahchen/musubi/compare/v0.9.2...v0.10.0 |
| @@ -34,7 +34,7 @@ Add Musubi to your Phoenix application: | |
| 34 34 | ```elixir |
| 35 35 | def deps do |
| 36 36 | [ |
| 37 | - {:musubi, "~> 0.11.1"} |
| 37 | + {:musubi, "~> 0.12.0"} |
| 38 38 | ] |
| 39 39 | end |
| 40 40 | ``` |
| @@ -10,7 +10,7 @@ In the Phoenix app: | |
| 10 10 | ```elixir |
| 11 11 | def deps do |
| 12 12 | [ |
| 13 | - {:musubi, "~> 0.11.1"} |
| 13 | + {:musubi, "~> 0.12.0"} |
| 14 14 | ] |
| 15 15 | end |
| 16 16 | ``` |
| @@ -71,15 +71,17 @@ transport params from the client. Use it for: | |
| 71 71 | - long-lived assigns shared by every Musubi connection and mounted root |
| 72 72 | - rejecting the socket with `:error` or `{:error, :unauthorized}` |
| 73 73 | |
| 74 | - `handle_join/2` runs once when the Musubi connection channel joins. It receives |
| 75 | - the connection join params, not root mount params. Use it for: |
| 74 | + `handle_join/2` runs once per root channel (each root store joins its own |
| 75 | + channel). It receives that root's mount params (`module` / `id` / `params`) and |
| 76 | + runs just before the root's `mount/2`. Use it for: |
| 76 77 | |
| 77 | - - workspace or tenant scope checks |
| 78 | - - connection-level authorization |
| 79 | - - setting assigns shared by every root mounted on that Musubi connection |
| 78 | + - per-root authorization (reject the join with `:error` / `{:error, :unauthorized}`) |
| 79 | + - workspace or tenant scope checks keyed off the mount params |
| 80 | + - setting assigns for the root mounted on that channel |
| 80 81 | |
| 81 | - Root store `mount/2` runs once for each mounted root. It receives the root's |
| 82 | - own mount params: |
| 82 | + For assigns shared by *every* root, prefer `handle_connect/2` (socket-level, |
| 83 | + runs once per socket). Root store `mount/2` then runs for the root, receiving the |
| 84 | + same mount params: |
| 83 85 | |
| 84 86 | ```elixir |
| 85 87 | @impl Musubi.Store |
| @@ -102,7 +104,7 @@ Use these scopes consistently: | |
| 102 104 | | connect params | `new Socket("/socket", params: ...)` | Phoenix socket | auth tokens and transport credentials | |
| 103 105 | | connect info | Phoenix endpoint `connect_info` | Phoenix socket | session, peer data, headers configured by the endpoint | |
| 104 106 | | session | `connect_info[:session]` | Musubi socket | browser session data shared by mounted roots | |
| 105 | - | join params | `connect(socket, { topic: ... })` channel join payload | Musubi connection | connection-wide scopes | |
| 107 | + | join params | per-root channel join payload (`{ module, id, params }`, sent by `mountStore`) | one root store | seen by `handle_join/2`; identical to that root's mount params | |
| 106 108 | | mount params | `connection.mountStore({ module, id, params })` | one root store | business identity such as `poll_id` or `cart_id` | |
| 107 109 | |
| 108 110 | Child stores do not receive `mount/2`. They run `init/1` and can still read |
| @@ -1,6 +1,6 @@ | |
| 1 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/fahchen/musubi">>}]}. |
| 2 2 | {<<"name">>,<<"musubi">>}. |
| 3 | - {<<"version">>,<<"0.11.1">>}. |
| 3 | + {<<"version">>,<<"0.12.0">>}. |
| 4 4 | {<<"description">>, |
| 5 5 | <<"Server-authoritative, page-scoped runtime library for Elixir/Phoenix applications.">>}. |
| 6 6 | {<<"elixir">>,<<"~> 1.18">>}. |
Loading more files…