Newby having problems with yjs/yrb/codemirror

Hello!

I’m working with yjs on my college project, intending to use it on a collaborative editor for coding for schools, but I’m having some trouble making it work with a backend. As a background, I am mainly a backend Rails developer, though this is my first time using action cable for anything, for this project I have to use Angular (not my choice) so I’m trying to build a proof of concept before actually working on the real deal.

My problem is that even though I get no error messages, the editors do work collaboratively between tabs of the same browser, and I do see messages being sent and received between backend and frontend, I do not see the actual document from the backend in the frontend.

I’ll post the code below, but here’s a link to the repo: https://github.com/DJA89/crdt_yjs_matefun/tree/add_collaborative_eidition_libraries_for_help

// crdt_yjs_matefun/frontend/src/app/code-editor/code-editor.component.ts
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 }
  }
}
# backend/app/channels/application_cable/document_channel.rb
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

Could anyone guide me to make this work, or hint at what could the problem be?

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 ^^

1 Like