Skip to content

Consider using Y.Map instead of Y.Array for jobs/triggers/edges in Y.Doc #4253

@elias-ba

Description

@elias-ba

Context

When multiple users apply an AI-generated workflow simultaneously, the Y.Array CRDT can create duplicate entries. This happens because Y.Array's push() operation is always additive - concurrent pushes from different users all get merged.

Current data structure:

ydoc.getArray('jobs')     // Y.Array<Y.Map>
ydoc.getArray('triggers') // Y.Array<Y.Map>
ydoc.getArray('edges')    // Y.Array<Y.Map>

Current apply logic (in YAMLStateToYDoc.applyToYDoc):

const jobsArray = ydoc.getArray('jobs');
jobsArray.delete(0, jobsArray.length);  // Clear
jobsArray.push(transformedJobs);        // Add new

When two users click Apply concurrently:

  1. Both delete operations target the same items
  2. Both push operations add the same items
  3. CRDT merge includes BOTH pushes → duplicate entries

Current Solution (Implemented)

We implemented server-coordinated locking via Phoenix Channel:

  • When a user clicks Apply, backend broadcasts workflow_applying to all clients
  • All clients disable their Apply buttons
  • When apply completes, backend broadcasts workflow_applied
  • All clients re-enable Apply buttons

This is 100% effective because the server is the single source of truth.

Files changed:

  • lib/lightning_web/channels/workflow_channel.ex - Added start_applying_workflow and done_applying_workflow handlers
  • assets/js/collaborative-editor/stores/createWorkflowStore.ts - Channel listeners and coordination methods
  • assets/js/collaborative-editor/components/AIAssistantPanelWrapper.tsx - Calls coordination methods

Proposed Future Improvement

Change the Y.Doc structure to use Y.Map keyed by ID:

ydoc.getMap('jobs')       // Y.Map<id, Y.Map>
ydoc.getMap('triggers')   // Y.Map<id, Y.Map>
ydoc.getMap('edges')      // Y.Map<id, Y.Map>

Why this would be better:

  • map.set(id, data) is idempotent - two users setting the same key = same result, no duplicates
  • More semantically correct (entities are keyed by ID)
  • Simpler apply logic without needing coordination

What would need to change:

  1. Frontend:

    • YAMLStateToYDoc.applyToYDoc - Use map.set(id, data) instead of array.push()
    • createWorkflowStore.ts - Update observers to read from maps
    • Convert map values to arrays for Immer state (React expects arrays)
  2. Backend:

    • lib/lightning/collaboration/workflow_serializer.ex - Change Yex.Doc.get_array to Yex.Doc.get_map
    • Update initialization and extraction logic

Priority

Low - The server-coordinated solution works perfectly. This is an architectural improvement for cleaner CRDT semantics, not a bug fix.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Tech Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions