Use the new channel registry for runs
Nobody has claimed this yet.
- Dominant language
- Elixir
- Stars
- 296
- Forks
- 86
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 50
Description
Multiple stores manage Phoenix channel connections with their own lifecycle
logic. When switching contexts quickly (version switch, run switch, job switch),
race conditions cause UI flickering and stale data.
Symptoms
- Version switch flickering: Loading indicator flashes briefly when clicking
history versions because old session is destroyed before new one connects - Button flash during save (#4080): Action buttons briefly disable when
saving because HTTP response arrives before Y.Doc channel update - Stale run data: When switching runs quickly in the history panel, old
channel responses can arrive after new channel connects
Related Issues
- #4080 - Button flash during save (race between HTTP and channel updates)
- #4135 - Retry not possible from canvas under certain conditions
- #4140 - Race conditions in channel switching
- #4149 - Unable to go to latest (version switching broken)
Why This Matters
Several components use useRef to track previous state and detect transitions:
| File | Refs | What They Track |
|---|---|---|
WorkflowEditor.tsx |
7+ | Panel states, method changes, initialization |
useAISession.ts |
3 | Mode, job ID, subscription topic |
useProviderLifecycle.ts |
2 | Provider instance, initialization |
AIAssistantPanelWrapper.tsx |
4 | IDE state, URL sync, applied messages |
FullScreenIDE.tsx |
1 | Job ID for state reset |
The pattern looks like:
const prevFooRef = useRef(foo);
useEffect(() => {
if (prevFooRef.current !== foo) {
// transition happened, do something
}
prevFooRef.current = foo;
}, [foo]);
This works but:
- Each component re-implements transition detection
- Complex boolean logic for multi-state transitions
- Easy to get wrong, hard to debug
- Timing issues require
setTimeoutworkarounds - No central place to understand what transitions exist
Proposed Solution
Create a shared channel registry library. Components declare what they need, the
registry handles the messy lifecycle.
Architecture
┌─────────────────────────────────────────────────────────┐
│ Components │
│ (WorkflowEditor, RunViewer, AIAssistantPanel, etc.) │
└─────────────────────┬───────────────────────────────────┘
│ "I need workflow v5" / "I need run 123"
│ No connection logic - just declare intent
▼
┌─────────────────────────────────────────────────────────┐
│ Stores │
│ SessionStore, HistoryStore, AIAssistantStore │
│ - Use channel-registry helpers │
│ - Manage their own ChannelEntry instances │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Channel Registry Library │
│ - State machine: connecting→settling→active→draining │
│ - Multiple channels coexist (old drains while new │
│ connects) │
│ - Reference counting for shared channels │
│ - Pluggable "settling" logic per channel type │
└─────────────────────────────────────────────────────────┘
Stores That Need This
| Store | Channel Pattern | Current Problem |
|---|---|---|
| SessionStore | workflow:collaborate:* |
Destroys session before new one connects |
| HistoryStore | run:${runId} |
Manual 3-level guards for race conditions |
| AIAssistantStore | ai_assistant:* |
Connection logic in components |
State Machine
Each channel entry moves through these states:
connecting → settling → active → draining → destroyed
connecting: Channel created, join in progresssettling: Joined, waiting for initial sync (Y.Doc sync, first data push)active: Ready for usedraining: Superseded by new channel, kept alive during grace perioddestroyed: Resources released
Key Behaviors
-
Overlapping channels: Old channel stays alive (draining) while new one
connects. UI keeps showing old data until new channel is ready. No loading
flash. -
Grace period: Draining channels get ~2 seconds before cleanup. Handles
in-flight operations and quick back-and-forth switching. -
Pluggable settling: Different channels need different "ready" signals:
- Workflow: Y.Doc synced + first update received
- Run: Initial data push received
- AI: Just needs successful join
-
Reference counting: Multiple subscribers can share a channel. Cleanup
only when all leave.
What This Fixes
| Problem | How Registry Solves It |
|---|---|
| Loading flash on version switch | Old channel stays visible during transition |
| Button flash during save | isTransitioning flag suppresses mismatch |
| Stale data from old channels | Clear state machine, draining channels don't update store |
setTimeout workarounds |
No timing issues when channels coexist |
| Scattered transition logic | Centralized in registry, not in components |
Implementation Notes
Location: assets/js/collaborative-editor/lib/channel-registry/
Approach: Composable helper functions, not a class. Each store manages its
own channel entries using the shared helpers.
Files:
types.ts- ChannelEntry, ChannelState, ResourceManager interfaceshelpers.ts-createChannelEntry,transitionState,joinChannel,
startSettling,scheduleCleanup,destroyEntryresourceManagers.ts-noopResourceManager,createYjsResourceManagerindex.ts- Re-exports
API sketch:
// Store creates entry when switching channels
const entry = createChannelEntry(topic, channel, resourceManager);
// Join and wait for settling
await joinChannel(entry);
await startSettling(entry); // waits for resourceManager.isSettled()
// Mark old entry as draining, new as active
transitionState(oldEntry, 'draining');
transitionState(entry, 'active');
// Cleanup after grace period
scheduleCleanup(oldEntry, 2000);
What This Doesn't Solve
The channel registry handles channel lifecycle, but stores also need changes.
The gap: Even if the old channel stays alive during draining, stores currently
do destructive replacement of their state. When you switch from v4 to v5, the
store wipes v4 data immediately. The UI can't show "previous workflow" because
there's nowhere to read it from.
Needs separate design/issue (TBD):
- Store state partitioning (keyed by workflow ID + version)
- Hook changes to return stable data during transitions
- Action binding (which version do mutations target?)
- Observer management (per-entry Y.Doc observers)
This store work will likely come first, since it can use frozen snapshots during
transitions without requiring overlapping live channels. The channel registry
then builds on top for smoother transitions and other race condition fixes.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the proposed files under assets/js/collaborative-editor/lib/channel-registry/: types.ts, helpers.ts, resourceManagers.ts, and index.ts. Review SessionStore, HistoryStore, and AIAssistantStore to understand their current channel lifecycles and the separate state-partitioning gap. Done means a shared registry supports the documented channel states, settling, overlapping channels, cleanup, and reference counting without claiming to solve the store-state design.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100