Skip to content

Commit 408c2e8

Browse files
fahchenclaude
andcommitted
chore(dashboard): bump musubi 0.7.0 → 0.7.1, unwind multi-observer workaround
0.7.1 restores multi-observer mount sharing per the spec report at arbor/spec/cf-dashboard-multi-observer-root.md. The server still dedupes (module, id) at mount time and replies :already_mounted on duplicates, but the reply now carries the existing root_id; the React wrapper layer aliases the second observer to the existing local RootConnection, bumps a refCount, and returns the same StoreProxy. Multiple components that previously crashed with "root already mounted" now share a single proxy. Reverting 541b14e: - InboxNotifier: id "notifier" → "default" - FlowCatalogPage: id "catalog" → "default" - FlowDetailPage: id "detail" → "default" InboxNotifier + InboxPage now share InboxStore("default") again; FlowCatalogPage + FlowDetailPage share FlowCatalogStore("default") again. Both pairs receive the same proxy via 0.7.1's alias path. The "0.7.0 workaround" inline comments are stripped — replaced with the original singleton/refcount comments from the 0.6.1 era. mix.lock hex digest: outer 4a7cdac9...dc0b / inner bd622f15... mix precommit 177/177, dialyzer clean; pnpm typecheck + 172/172 vitest + build + smoke green (single React 19.2.6, WS join, mount stable, remount still receives :already_mounted at the raw-channel level — aliasing happens in the React wrapper); lib/coloured_flow byte-identical. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 541b14e commit 408c2e8

5 files changed

Lines changed: 13 additions & 30 deletions

File tree

dashboard/mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ defmodule ColouredFlowDashboard.MixProject do
2929
defp deps do
3030
[
3131
{:coloured_flow, path: ".."},
32-
{:musubi, "~> 0.7.0"},
32+
{:musubi, "~> 0.7.1"},
3333
{:phoenix, "~> 1.8.5"},
3434
{:phoenix_ecto, "~> 4.5"},
3535
{:ecto_sql, "~> 3.13"},

dashboard/mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
1616
"jsonpatch": {:hex, :jsonpatch, "2.3.1", "49c380f458debbd2bc6e256daeab1081dc89624288f3d77ea83952229388d316", [:make, :mix], [], "hexpm", "06c3e4fff3574cc54d335041f6322fe1b72756e396dd472615ce350d3dd5e758"},
1717
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
18-
"musubi": {:hex, :musubi, "0.7.0", "1ad3af8ac77f47448ea77309940ec056f1a68c645602e5d522802e8981b1a552", [:mix], [{:jsonpatch, "~> 2.2", [hex: :jsonpatch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.3 and < 2.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:typed_structor, "~> 0.6.1", [hex: :typed_structor, repo: "hexpm", optional: false]}], "hexpm", "f1d31d567a0a31bd10569c13241848a4b517c725bc55757b8c9b6d717e87a224"},
18+
"musubi": {:hex, :musubi, "0.7.1", "bd622f15d3c79774452ae13008d23f07b5480db3afa851915e80af162908b98e", [:mix], [{:jsonpatch, "~> 2.2", [hex: :jsonpatch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.3 and < 2.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:typed_structor, "~> 0.6.1", [hex: :typed_structor, repo: "hexpm", optional: false]}], "hexpm", "4a7cdac9754747e37af84eceb1f1bea75ae57e441842555b44fc9114ad14dc0b"},
1919
"phoenix": {:hex, :phoenix, "1.8.7", "d8d755b4ff4b449f610223dd706b4ae64155cb720d3dc09c706c079ecea189e4", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "47352f72d6ab31009ef77516b1b3a14745be97b54061fd458031b9d8294869d5"},
2020
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
2121
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},

dashboard/ui/src/components/InboxNotifier.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,12 @@ type InboxProxy = NonNullable<Extract<InboxRootMount, { status: "ready" }>["stor
1717
* the moment a new workitem appears on the live inbox stream. The toast
1818
* links back to `/`. The dot clears when the inbox page is visited.
1919
*
20-
* Mounts InboxStore under a *distinct* caller id ("notifier") so that the
21-
* RootLayout-level notifier and the route-level InboxPage hold independent
22-
* server-side roots. Musubi 0.7 rejects `(module, id)` duplicates on one
23-
* connection with `:already_mounted`, and React 19's render/effect timing
24-
* cannot reliably keep the @musubi/react `pendingRootMounts` dedup honest
25-
* across Suspense retries + route swaps. Two roots per connection is the
26-
* cheap, race-free option. DO NOT collapse this id back to "default" — that
27-
* collides with InboxPage and crashes navigation.
20+
* Backed by a refcounted `useMusubiRoot` mount that the inbox page also
21+
* subscribes to — Musubi's shared-mount table guarantees a single server-side
22+
* root regardless of how many React subscribers exist.
2823
*/
2924
export default function InboxNotifier() {
30-
const root = useMusubiRoot({ module: INBOX_STORE, id: "notifier" })
25+
const root = useMusubiRoot({ module: INBOX_STORE, id: "default" })
3126
if (root.status !== "ready") return null
3227
return <InboxNotifierBody inbox={root.store} />
3328
}

dashboard/ui/src/routes/FlowCatalogPage.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,9 @@ export default function FlowCatalogPage() {
4747
}
4848

4949
function CatalogRoot() {
50-
// Distinct caller id from FlowDetailPage's "detail" id. Musubi 0.7 rejects
51-
// `(module, id)` duplicates on one connection with `:already_mounted`, and
52-
// a /flows → /flows/:id swap can race the @musubi/react pendingRootMounts
53-
// dedup (old useEffect cleanup vs. new render-phase ensureRootMount). Two
54-
// FlowCatalogStore roots per connection is the cheap, race-free option.
55-
// DO NOT collapse this id back to "default" — that collides with
56-
// FlowDetailPage and crashes navigation.
5750
const catalog = useMusubiRootSuspense({
5851
module: FLOW_CATALOG_STORE,
59-
id: "catalog"
52+
id: "default"
6053
})
6154

6255
return <CatalogContent catalog={catalog} />

dashboard/ui/src/routes/FlowDetailPage.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,14 @@ export default function FlowDetailPage() {
4848
}
4949

5050
function DetailRoot({ flowId }: { flowId: string }) {
51-
// Mounts FlowCatalogStore under a *distinct* caller id ("detail") from
52-
// FlowCatalogPage's "catalog" id. Musubi 0.7 rejects `(module, id)`
53-
// duplicates on one connection with `:already_mounted`, and the
54-
// /flows ↔ /flows/:id swap can race the @musubi/react pendingRootMounts
55-
// dedup. Two FlowCatalogStore roots per connection is the cheap,
56-
// race-free option — the stream carries lightweight FlowSummary rows so
57-
// the duplicate is cheap on the server. The heavy NetDiagram + full
58-
// enactments list is fetched on demand via :fetch_flow_detail. DO NOT
59-
// collapse this id back to "default" — that collides with FlowCatalogPage
60-
// and crashes navigation.
51+
// Reuses the FlowCatalogStore singleton root (per Requirements: "/flows,
52+
// /flows/:module → FlowCatalogStore"). The stream carries lightweight
53+
// FlowSummary rows; the heavy NetDiagram + full enactments list is
54+
// fetched on demand via :fetch_flow_detail so the catalog payload stays
55+
// bounded as history accumulates.
6156
const catalog = useMusubiRootSuspense({
6257
module: FLOW_CATALOG_STORE,
63-
id: "detail"
58+
id: "default"
6459
})
6560
return <DetailContent catalog={catalog} flowId={flowId} />
6661
}

0 commit comments

Comments
 (0)