Idempotently setting the initial document content (with webrtc

First, yes looks very impressive, thank you!

I’m looking to create a Monaco instance:

  1. That has some textual value that’s known aheads of time
  2. Then connected via the webrtc provider to check to see if others are editing the same document
  3. And if so, update the local instance

Working from the basic Monaco examples provided, I have the following, which erases whatever content Monaco is created with, but works well for the subsequent edits:

console.log("Editor contents: ", editorHandle.getValue()); // Logs the editor content as expected

var ydocument = new YjsLib.Doc();
var yprovider = new YWebrtc.WebrtcProvider(chain.name, ydocument, {});
var text = ydocument.getText("monaco");

var awareness = yprovider.awareness;
var editors = new Set([editorHandle]);
yprovider.connect();
Yjs.Monaco.createBinding(text, model, editors, awareness);

Based on other forum posts, I can add this to the code:

let text = ydocument->Yjs.Document.getText()
text->Yjs.Document.Text.insert(0, content)

And that indeed fills in the doc when it’s empty. Unfortunately, every client that subsequently connects will also insert the text.

I know this may be a more intricate problem than I’m giving it credit for, but what’s the recommended way to simply say, “here’s the current state of the document, start editing from here, unless it already exists on one of the peers?”

Hey @sgrove, I gave a solution to this problem here: Initial offline value of a shared document

I recommend to store the Yjs document instead of a pure text representation. If you store the Yjs document, you can guarantee that all clients always sync and you avoid many complex cases that you would need to fix in your protocol.

In short: If you receive the text content from some server that updates the text representation, you should rather store the Yjs document on the server (maybe alongside the text representation).

I don’t believe the linked post addresses my issue (though very happy for this to be PEBKAC!).

Here’s my code that I put together based on the post:

import * as Yjs from "yjs";
import * as YWebrtc from "y-webrtc";
import * as Belt_HashMapString from "bs-platform/lib/es6/belt_HashMapString.mjs";

var globalState = Belt_HashMapString.make(10);

function idempotentCreate(name, initialOptimisticContext) {
 // Only create the room once globally, to prevent the "room already exists" error
  var room = Belt_HashMapString.get(globalState, name);
  if (room !== undefined) {
    return room;
  }

  // Room doesn't exist locally, so we should create a doc and connect

  // First, create a template with our initial value, and encode it
  var templateDocument = new Yjs.Doc();
  var templateText = templateDocument.getText("monaco");
  templateText.insert(0, initialOptimisticContext);
  var encodedUpdate = Yjs.encodeStateAsUpdate(templateDocument);

  // Now create the actual document that we will eventually bind to monaco
  var ydocument = new Yjs.Doc();

  // Apply the update before connecting to the provider
  Yjs.applyUpdate(ydocument, encodedUpdate);

  // Connect to the webrtc provider
  var yprovider = new YWebrtc.WebrtcProvider(name, ydocument, {
        maxConns: 20
      });

  // Store our document and provider for later reuse
  var sharedRoom = {
    document: ydocument,
    provider: yprovider
  };

  // And put it in our global store
  Belt_HashMapString.set(globalState, name, sharedRoom);

  // Returning it for immediate use
  return sharedRoom;
}

The issue is, this works exactly the same as if I just ran text.insert(0, content) on every client - every client that connects will append what they think the initial state of the document is!

Instead, I just want:

  1. After you’ve connected to the room, check to see if there’s a current value
  2. If not, then run text.insert(0, content)

If there were one or two hooks in the lifecycle of the provider or the document, I might be able to pull this off a bit more easily.

Also, I’m largely hoping to do the majority of this client-side, without involving the server. We run an OCaml-native GraphQL server, and it’s nice to be able to (for the time being) keep the collaborative stuff purely client-side via webrtc.

What I was saying in the other thread was that you need to calculate this once:

var templateDocument = new Yjs.Doc();
var templateText = templateDocument.getText("monaco");
templateText.insert(0, initialOptimisticContext);
var encodedUpdate = toBase64(Yjs.encodeStateAsUpdate(templateDocument))
console.log(encodedUpdate) // It logs something like: "f43a.."

Then, you include the encoded document in the document.

import * as Yjs from "yjs";
import * as YWebrtc from "y-webrtc";
import * as Belt_HashMapString from "bs-platform/lib/es6/belt_HashMapString.mjs";

var globalState = Belt_HashMapString.make(10);

function idempotentCreate(name, initialOptimisticContext) {
 // Only create the room once globally, to prevent the "room already exists" error
  var room = Belt_HashMapString.get(globalState, name);
  if (room !== undefined) {
    return room;
  }

  // Room doesn't exist locally, so we should create a doc and connect

  var encodedUpdate = toBuffer("f43a..") // the string you computed above

  // Now create the actual document that we will eventually bind to monaco
  var ydocument = new Yjs.Doc();

Please also have a look at the whole discussion at Initial offline value of a shared document

You can keep the collaboration stuff over webrtc. But you should send & retrieve the Yjs from GraphQL. Not the text document. You will always duplicate content if you don’t start with the same history.