OpenFn / OpenFn/lightning

Use the new channel registry for runs

Open
#4,139 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Collab Editor
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 setTimeout workarounds
  • 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 progress
  • settling: Joined, waiting for initial sync (Y.Doc sync, first data push)
  • active: Ready for use
  • draining: Superseded by new channel, kept alive during grace period
  • destroyed: Resources released
Key Behaviors
  1. Overlapping channels: Old channel stays alive (draining) while new one
    connects. UI keeps showing old data until new channel is ready. No loading
    flash.

  2. Grace period: Draining channels get ~2 seconds before cleanup. Handles
    in-flight operations and quick back-and-forth switching.

  3. 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
  4. 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 interfaces
  • helpers.ts - createChannelEntry, transitionState, joinChannel,
    startSettling, scheduleCleanup, destroyEntry
  • resourceManagers.ts - noopResourceManager, createYjsResourceManager
  • index.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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.