Skip to content

fix(wire): extract Livewire bridge to /wire subpath addon - #81

Open
zachiler wants to merge 11 commits into
v0.3.0-alphafrom
feature/wire-bridge-extraction
Open

fix(wire): extract Livewire bridge to /wire subpath addon#81
zachiler wants to merge 11 commits into
v0.3.0-alphafrom
feature/wire-bridge-extraction

Conversation

@zachiler

@zachiler zachiler commented Aug 6, 2026

Copy link
Copy Markdown
Member

Wire-bridge extraction (v0.3.0-alpha)

Moves the Livewire integration out of core into a self-contained /wire subpath addon, so the engine stays framework-agnostic and the downstream bridge can evolve on its own. Part of the v0.3.0-alpha decoupling chapter.

What changed

  • New AlpineFlowWire addon (src/wire/index.ts) — registered via Alpine.plugin(AlpineFlowWire), activates through setup(canvas) only when a Livewire $wire proxy is present. Maps flow:* server→client commands to canvas methods and AlpineFlow events to $wire method calls.
  • Relocated the bridge: src/core/wire-bridge.tssrc/wire/bridge.ts (core no longer imports it).
  • /wire subpath is ESM-only: exports entry is types + import (alpineflow-wire.esm.js). The dead default/CDN target and lib/wire.cdn.ts were dropped — WireFlow consumes the .esm.js, never the CDN/require variant. (CDN/no-build support for the structured addons is a deliberate follow-up, not a no.)
  • New public canvas API: selectNodes / selectEdges / setNodeLocked / setNodeHidden (also declared on CanvasContext). The bridge's lock/hide/select composites route through these public methods instead of reaching into internals (TDD'd).
  • Addon lifecycle: setup() may return a cleanup fn, now invoked on canvas destroy.
  • Types: wireEvents removed from core FlowCanvasConfig; re-declared via module augmentation inside the addon so consumers keep the type. Runtime-additive.
  • CHANGELOG: added the v0.3.0-alpha section (breaking bundle change + the public selection API).

Correctness fixes found in review

  • Critical (fixed): client→server forwarding was dead. The addon wrote its on<Event> wrappers to canvas._config, but core's _emit reads callbacks from the closure config (a different object — _config is a one-time stripped copy). Fix: added _liveConfig() to core returning the live closure config (not stored as reactive data, so it stays collab-safe); the addon targets it. Restores the exact pre-extraction behavior.
  • Hardening: no silent version-skew regression. If this addon ever meets an older core lacking _liveConfig() (realistic — WireFlow vendors the bundle as a separate artifact), it now console.warns and skips forwarding rather than silently writing the stale copy and re-introducing the dead-forward bug.
  • Selection semantics: emit-on-actual-change. selectNodes/selectEdges now emit selection-change only when the selection truly changes, at most once per call. Client listeners (and the server, if selection-change is mapped in wireEvents) observe server-driven selections, but re-issuing the same ids is a no-op that does not re-emit — so an idempotent server→client→server round-trip cannot loop. Also collapses the old clear-then-set double emit and skips unknown ids.

Known limitation (documented, not fixed)

wireEvents:{init:…} does not forward — core emits init before _initAddons() runs the wire setup(). Reordering core init is riskier than the edge case warrants (other addons rely on _container/ResizeObserver being ready first). Documented in code.

Tests

  • New real-canvas integration test (wire-events-integration.test.ts) mounts flowCanvas + the wire addon + a $wire and asserts a node-click reaches $wire (fails before the _liveConfig fix, passes after). Plus tests for the version-skew warn path and the selection-change guard.
  • Full suite: 3052 passing, tsc --noEmit clean.

Not in this PR

The WireFlow-side migration (vendor alpineflow-wire.esm.js + default-load it, since the bridge is baseline for WireFlow, not opt-in) happens in the wireflow repo at resync.

zachiler added 11 commits August 5, 2026 20:08
…tegration test

registerWireEvents wrote its on<Event> wrappers to canvas._config, but core's
_emit reads callbacks from the closure config — a different object (the stripped
copy) — so every wireEvents client→server forward was silently dead.

Add _liveConfig() to core returning the live closure config (not stored as
reactive data, so it stays collab-safe) and have the wire addon target it. This
restores the pre-extraction behavior, where the in-core bridge mutated the
closure config directly.

Add a real-canvas integration test (mounts flowCanvas + wire addon + $wire) that
asserts a node-click reaches $wire; the prior fakeCanvas unit tests could not
catch the config-object mismatch. Also fix a stale [wire-bridge] log string and
document the init-ordering and selection round-trip edge cases in code.
Remove lib/wire.cdn.ts and the "default": alpineflow-wire.js export
condition. Nothing built that .js and no consumer used it: WireFlow loads
addons via the .esm.js (import) condition from its vendored dist, never the
CDN/require fallback. Leaves ./wire as types + import, matching how the
structured addons are actually consumed.

CDN support for the structured addons (wire/schema/workflow) is a deliberate
follow-up, tracked separately — not dropped as an idea, just deferred until
after the v0.3.0-alpha extraction ships and is verified.
When core is too old to expose _liveConfig(), the addon previously fell back
to canvas._config — the stripped copy _emit does NOT read — silently
reintroducing the dead client→server forwarding bug. Because WireFlow vendors
AlpineFlow's bundle as a separate artifact, this addon can genuinely meet an
older core, so the failure must be loud. Now: warn and skip event forwarding
(server→client commands still register); never mutate the copy.
selectNodes/selectEdges called deselectAll() (which emits) then emitted again,
so a single call could fire up to two selection-change events, and re-selecting
the same ids still emitted. Route both (and deselectAll) through a shared guard
that clears silently and emits once, only when the resulting selection differs
from the current one.

Beyond removing the redundant/no-op emits, this defuses the wire round-trip: a
server-driven flow:selectNodes that maps selection-change in wireEvents now
forwards the result once, and re-issuing identical ids does not re-emit — so an
idempotent server→client→server path cannot loop.
…tion API

Document the breaking move of the Livewire bridge from core into the /wire
addon (no longer in the vendored bundle; wireEvents type relocated) and the new
public selectNodes/selectEdges/setNodeLocked/setNodeHidden methods with their
emit-on-actual-change semantics.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant