facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
17.5k stars 1.45k forks source link

Bug: syncYjsChangesToLexical unable to find active editor in headless mode #3361

Closed hiadamk closed 1 year ago

hiadamk commented 1 year ago

I am currently working on converting the representation of lexical in yjs to html. I received a very helpful gist when I initially enquired about this in discord. Whilst working on implementing this, I have come across an issue using the syncYjsChangesToLexical method. The stack trace of the error is as follows:

 Error: Unable to find an active editor. This method can only be used synchronously during the callback of editor.update().
[0]     at getActiveEditor (C:<project 
-path>\node_modules\lexical\Lexical.dev.js:5541:13)
[0]     at $setNodeKey (C:<project 
-path>\node_modules\lexical\Lexical.dev.js:550:18)
[0]     at new LexicalNode (C:<project 
-path>\node_modules\lexical\Lexical.dev.js:6334:5)
[0]     at new ElementNode (C:<project 
-path>\node_modules\lexical\Lexical.dev.js:7077:5)
[0]     at new LinkNode (C:<project 
-path>\node_modules\@lexical\link\LexicalLink.dev.js:33:5)
[0]     at createLexicalNodeFromCollabNode (C:<project 
-path>\node_modules\@lexical\yjs\LexicalYjs.dev.js:336:23)
[0]     at CollabElementNode.syncChildrenFromYjs (C:<project 
-path>\node_modules\@lexical\yjs\LexicalYjs.dev.js:847:34)
[0]     at createLexicalNodeFromCollabNode (C:<project 
-path>\node_modules\@lexical\yjs\LexicalYjs.dev.js:345:16)
[0]     at CollabElementNode.syncChildrenFromYjs (C:\<project 
-path>\node_modules\@lexical\yjs\LexicalYjs.dev.js:847:34)
[0]     at syncEvent (C:<project 
-path>\node_modules\@lexical\yjs\LexicalYjs.dev.js:1523:18)

This errror does not occur when the editor contains simple text. Once I start using more sophisticated nodes such as links and tables, the error is thrown.

Lexical version: 0.6.0

Steps To Reproduce

  1. Create a nodejs project
  2. implement a headless editor using jsdom
  3. Try to generate html from yjs doc

An example of code to reproduce this is shown below.

Link to code example:

A gist containing a modified version of the gist provided previously can be found here.

The current behavior

As mentionned above, when generating html which only contains text, this works as expected. When generating html which contains nodes such as tables and links, an error with the stack trace above is shown.

The expected behavior

I would expect that the behaviour is the same for both text nodes and other nodes.

fantactuka commented 1 year ago

Could you post specific code snippet you're using?

trueadm commented 1 year ago

Is this line needed?

https://gist.github.com/adamkona/40c8075bc12432a91158e0de47a283b6#file-lexical-ts-L111

I wonder if this is a bug with nested updates if it is, because the editor should be active in an update. It's only not active in a read. I believe we may have already fixed this bug if this is the case.

hiadamk commented 1 year ago

Is this line needed?

https://gist.github.com/adamkona/40c8075bc12432a91158e0de47a283b6#file-lexical-ts-L111

I wonder if this is a bug with nested updates if it is, because the editor should be active in an update. It's only not active in a read. I believe we may have already fixed this bug if this is the case.

I've just updated the gist and have tested the code without the update which you saw and I'm still observing the same issue

hiadamk commented 1 year ago

Could you post specific code snippet you're using?

The function in the gist is the code which is being called with the parameters it requests. Could you clarify the snippet of code you'd like to see?

trueadm commented 1 year ago

In the call-stack above, does it go any further than syncEvent? That function should be called from syncYjsChangesToLexical which contains an editor update. Have you any way to debug the callstack upon the error being thrown using Chrome dev tools? I think it would useful to examine the frame where Lexical created the update to see what path that comes from.

hiadamk commented 1 year ago

In the call-stack above, does it go any further than syncEvent? That function should be called from syncYjsChangesToLexical which contains an editor update. Have you any way to debug the callstack upon the error being thrown using Chrome dev tools? I think it would useful to examine the frame where Lexical created the update to see what path that comes from.

The only other line that comes before syncEvent in the stack trace is:

[0]     at syncEvent (C:\Projects\notes\local\collabo\node_modules\@lexical\yjs\LexicalYjs.dev.js:1523:18)
[0]     at C:<project-dir>\node_modules\@lexical\yjs\LexicalYjs.dev.js:1556:7
trueadm commented 1 year ago

That's strange. The root of the callstack should be coming from your code here:

binding.root.getSharedType().observeDeep((events: any, transaction: any) => {
  if (transaction?.origin !== binding) {
    syncYjsChangesToLexical(binding, provider, events);
  }
});

What version of Lexical are you using?

hiadamk commented 1 year ago

Using lexical version: 0.6.0

trueadm commented 1 year ago

Please can you try using the latest 0.6.3.

hiadamk commented 1 year ago

Please can you try using the latest 0.6.3.

Upgraded to 0.6.3 and still experiencing the same crash

ebads67 commented 1 year ago

I found the root cause of this issue. I try to explain it as clear as possible: In the gist code headlessConvertYDocStateToLexicalJSON takes an array of LexicalNodes. The LexicalNode constructor calls $setNodeKey() in LexicalUtils.ts, which again calls getActiveEditor() in LexicalUpdates.ts, which returns a global variable, activeEditor defined in that file. In summary, LexicalNode accesses a global variable in its constructor. The reported issue can be reproduced if LexicalNodes use a different LexicalUtils.ts file than the one editor.update() uses in the gist. In @adamkona's case, we are developing a library (npm package) to convert YDoc states to HTML, to be used in a backend node.js server. We were referencing the conversion library code from the node.js server locally (using npm link), in a separate repo, in our dev environment. That is why we were using two separate Lexical libraries.

I hope the explanation was clear.

trueadm commented 1 year ago

You really can’t use two separate lexicals. You must keep it to one as those variables may seem “global” but they’re actually lexicallly bound to the lexical source (one of the reasons it’s called Lexical). Can you avoid duplicating lexical in your env?

ebads67 commented 1 year ago

Yes we can, that is not an issue. But I'm wondering if the fact that we can't have multiple lexicals makes us some issues in a node.js app where we potentially need to call headlessConvertYDocStateToLexicalJSON for different ydocs in parallel. Is it going to be thread safe given that the activeEditor is a "global" variable in the lexical library?

trueadm commented 1 year ago

It’s still single threaded so yes it should be fine.

ebads67 commented 1 year ago

Thank you. We can close this issue now @adamkona.