Search code examples
ruby-on-railsangularcodemirrorcollaborative-editing

Yjs and codemirror with action cable, no errors but it doesn't do anything


I am trying to use yrb-actioncable on Rails with yjs on Angular to have a collaborative text editor, I've never used ActionCable, Angular, Y.js, or CodeMirror before so I faced a lot of errors trying to make it work, I finally thought I had it when I stopped seeing any errors, but actually it still doesn't work.

I can see the editor on the screen and I can use collaborative editing between tabs of the same browser, I can see the requests to and from actioncable, but nothing else.

This is the whole repository, it's kind of a mess but I don't really know Angular and it's just a proof of concept to try and make things work. The DocumentChannel is the ActionCable part, the code-editor is the Angular component part. I'll paste them below too removing the commented and unused parts

require 'y-rb'

module ApplicationCable
  class DocumentChannel < ApplicationCable::Channel
    include Y::Actioncable::Sync

    def initialize(connection, identifier, params = nil)
      super
      load { |id| load_doc 1 }
    end

    def subscribed
      sync_from("document-1")
    end

    def receive(data)
      sync_to("document-1", data)
    end

    def unsubscribed
    end

    private

    def load_doc(id)
      doc_content = Document.first.content
      ydoc = Y::Doc.new
      ytext = ydoc.get_text('mine')
      ytext << doc_content
      data = []
      data = ydoc.diff unless doc_content.nil?
      data
    end
  end
end
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'
import { OnInit } from '@angular/core';
import * as Y from "yjs";
import { WebsocketProvider } from "@y-rb/actioncable";
import CodeMirror from "codemirror";
import { CodeMirrorBinding } from 'y-codemirror'
import ActionCable from 'actioncable'

@Component({
  selector: 'code-editor',
  templateUrl: './code-editor.component.html',
  styleUrls: ['./code-editor.component.scss']
})
export class CodeEditorComponent implements OnInit {
 

  constructor(private http: HttpClient) {
  }

  ngOnInit() {
    const accessToken = localStorage.getItem('accessToken')
    const uid = localStorage.getItem('uid')
    const client = localStorage.getItem('client')

    const yDocument = new Y.Doc();
    const consumer = ActionCable.createConsumer(`ws://localhost:3000/cable?uid=${uid}&access-token=${accessToken}&client=${client}`);
    const provider = new WebsocketProvider(
      yDocument,
      consumer,
      "ApplicationCable::DocumentChannel",
      {}
    );
    const yText = yDocument.getText('codemirror')

    const yUndoManager = new Y.UndoManager(yText)

    const editorContainer = document.createElement('div')
    editorContainer.setAttribute('id', 'editor')
    document.body.insertBefore(editorContainer, null)

    const editor = CodeMirror(editorContainer, {
      mode: 'javascript',
      lineNumbers: true,
    })

    const binding = new CodeMirrorBinding(yText, editor, provider.awareness, { yUndoManager })

    // @ts-ignore
    //window.example = { provider, ydoc, yText, binding, Y }
  }
}

I can't seem to find what's wrong with it as I don't get any actual error, nor have I been able to get any more help from online guides nor the official docs that I've tried following. Could anyone guide me on this?


Solution

  • So, I managed to make it work, my frontend code didn't change much, but my backend code did, you can see the end result in this link, but I'll paste the channel here so you can see the differences.

        # This method initializes the channel when someone tries to subscribe to it
        # and there's no one here yet
        # I imagine we should check here wether the document belongs to the user or not
        def initialize(connection, identifier, params = nil)
          super
          # This is a method from Y::Actioncable
          # it should return the full_diff of this channel's document
          load { load_doc 1 }
        end
    
        # This method is called when someone subscribes to this channel
        # If #initialize is called, this method will be called after
        # that one
        def subscribed
          # This method is from Y::Actioncable
          # It's just to facilitate set up, you should pass the actual
          # document you want to sync (here we are always syncing the first document).
          # The block is to save the document after syncing
          sync_for(Document.first) do |id, update|
            # Save it on the database
            save_doc(id, update)
          end
        end
    
        # Each time a change happens on the web this method will be
        # called with the new data to update our document
        def receive(data)
          # With this we spread the new data to all subscribers
          # so everyone is up to date with the document
          sync_to(Document.first, data)
          # Update the doc on the database, it would be good to not do
          # this every time this method is called, or maybe delay it
          # so it doesn't block every request
          save_doc(id, update)
        end
    
        # This method is called when a subscriber stops using the document
        def unsubscribed
        end
    
        private
    
        def load_doc(id)
          # For this POC we are using only the one document
          # otherwise we should look for the document with the given id
          doc_content = JSON.parse(Document.first.binary_doc)
    
          # Return the document binary
          doc_content
        end
    
        def save_doc(id, update)
          # Since we are only using the one document on this POC
          # We update that document only, otherwise we should look
          # for the correct document to update
          Document.first.update(binary_doc: update)
        end
    

    Now I'm glad to say it works just fine ^^