Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object extensible error #480

Open
jamfromouterspace opened this issue Jun 11, 2021 · 6 comments
Open

Object extensible error #480

jamfromouterspace opened this issue Jun 11, 2021 · 6 comments

Comments

@jamfromouterspace
Copy link

I honestly have no clue why this is happening. Here is how my stack works:

  1. The server creates a new doc, which is an empty list.
doc.create([], err => { if (err) console.log(err) })
  1. The client, after doing something, pushes to this list using a json0 op
doc.submitOp({ p: [0], li: "new-item" })
  1. The client gets an error
    TypeError: Cannot add property 0, object is not extensible at Array.splice in json0
  2. However, if I try again right away, it works for all subsequent ops.
  3. But other clients that are subscribed get the same error when the op gets broadcasted, so there seems to be something about the doc itself that is causing this.

This doesnt seem to happen when I create a doc on the client side and apply the same op to it. Is there anything special I need to do when creating the doc on the server side?

I have no clue where even to begin with this. Where in the ShareDB stack is Object.preventExtensions() being called? Does this have something to do with the websocket configuration?

@alecgibson
Copy link
Collaborator

Have you got a minimally working example?

@gsavvidis96
Copy link

Hello there, anything new on this? It actually happens in all 3 list operations: li, ld, lm. It fails the first time and after that works. If there is not anything new and the issue is still pending I can create a repo with a small example. Cheers!

@alecgibson
Copy link
Collaborator

@gsavvidis96 repro case would definitely be good, please

@gsavvidis96
Copy link

It seems I found the issue after a long day so I leave here what I found out in case it is of help.

I am using react with zustand for state management. After subscribing to the document I was saving the document.data in the zustand store. By doing that I was getting

object is not extensible

So instead, I saved the document.data using the lodash cloneDeep and the issue was resolved.

Here is a simple component of my use-case.

import { useState } from "react";
import ReconnectingWebSocket from "reconnecting-websocket";
import { Connection } from "sharedb/lib/client";
import { useMount } from "react-use";
import useTestStore from "./store";
import { cloneDeep } from "lodash";

const App = () => {
  const [document, setDocument] = useState(null);
  const { setDocumentData } = useTestStore();

  useMount(() => {
    const socket = new ReconnectingWebSocket(process.env.REACT_APP_SOCKER_URL);
    const connection = new Connection(socket);

    const document = connection.get("test-collection", "1234");

    setDocument(document);

    document.subscribe((error) => {
      if (error) return console.log(error);

      if (!document.type) {
        document.create(
          {
            testList: [],
          },
          (error) => {
            if (error) {
              console.error(error);
            }
          }
        );
      }

      console.log(document.data);

      setDocumentData(document.data); // <===== THIS CAUSES THE ERROR
      // setDocumentData(cloneDeep(document.data)); // <===== THIS PREVENTS THE ERROR
    });

    document.on("op", (op) => {
      console.log(op);
      console.log(document);
    });
  });

  const onInsertOp = () => {
    console.log("onInsertOp");

    document.submitOp({
      p: ["blocks", 0],
      li: {
        testProp: "this is a test property",
      },
    });
  };

  return (
    <div>
      <h1>ShareDB Test</h1>

      <button onClick={onInsertOp}>insert item to a list</button>
    </div>
  );
};

export default App;

@alecgibson
Copy link
Collaborator

In general, ShareDB's doc.data object should be considered immutable (unfortunately we don't currently have any safeguards around this), and it should only be manipulated through doc.submitOp().

The gotcha here is that libraries — especially data stores (Redux, Vuex, etc.) — can attempt to mutate doc.data, often to add their own metadata for reactivity, etc. This can lead to these errors.

When using ShareDB in conjunction with a frontend data store, I would recommend storing a deep clone of doc.data (just as you've suggested).

In addition, if you want/need reactivity, you'll also need to write adapters to apply your ops to your data store representation (so that you can incrementally change the object, rather than clobber reactivity by overwriting the whole thing).

For example, here's a function we're using to update a Vuex object according to an op:

export default function applyOp(json0: any, op: Op): void {
  op = clone(op);

  const path = op.p as string[];
  const key = path.pop();
  const parent = dig(json0, path);
  const parentKey = path.pop();
  const grandparent = dig(json0, path);

  if (parent === undefined) return;

  if ('oi' in op) {
    return Vue.set(parent, key, op.oi);
  }

  if ('li' in op) {
    // Don't need to use Vue.set, because it will already be watching
    // the array: https://vuejs.org/v2/guide/list.html#Mutation-Methods
    (parent as any[]).splice(parseInt(key), 0, op.li);
    return;
  }

  if ('od' in op) {
    return Vue.delete(parent, key);
  }

  if ('ld' in op) {
    (parent as any[]).splice(parseInt(key), 1);
    return;
  }

  if ('lm' in op) {
    (parent as any[]).splice(parseInt(op.lm), 0, (parent as any[]).splice(parseInt(key), 1)[0]);
    return;
  }

  if ('si' in op) {
    const updatedString = parent.substring(0, key) + op.si + parent.substring(key);
    return Vue.set(grandparent, parentKey, updatedString);
  }

  if ('sd' in op) {
    const updatedString = parent.substring(0, key) + parent.substring(key + op.sd.length);
    return Vue.set(grandparent, parentKey, updatedString);
  }

  throw new Error('Unrecognised op type');
}

@curran
Copy link
Contributor

curran commented Apr 27, 2023

FWIW json0.type.apply mutates the data, whereas json1.type.apply does not. Related to ottypes/json0#26

Here's a solution that makes json0.type.apply work without mutating the data, using Immer:

import ShareDB, { Connection } from 'sharedb/lib/client';
import { type as json0 } from 'ot-json0';
import produce from 'immer';

const originalApply = json0.apply;
json0.apply = (snapshot, op) =>
  produce(snapshot, (draftSnapshot) => {
    originalApply(draftSnapshot, op);
  });

This is used in the VizHub codebase as it makes it a lot easier to work with the state as it changes (e.g. triggering useEffect at the right times).

Not sure if this is related to the issue at hand, but might be!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants