How to properly close or destroy y-codemirror.next / switch providers

Hi. I’m looking to integrate y.js with Obsidian.

Basically I’m trying to implement something like this

  • wait for Obsidian to switch file
  • disconnect previous yjs connections from any editors (not 100% sure what I gotta do here)
  • connect yjs to the codemirror editor
  • set some text that should be inserted automatically

The logic works on first connect. When switching the file, any write operation (ytext.insert…) does nothing and produces no error. I’m guessing because I’m still writing to the wrong doc? Did I not close something I should have closed? Afaik, obsidian doesn’t load a new codemirror instance but just switches its contents.

Specifically I’m wondering what the recommended way of switching yjs-providers is, atm I’m only calling provider.disconnect() because that’s something I found in the javadocs. Not sure if that’s correct though.


This is my draft which does not work:

		const switchCollab = async () => {
			if (!(this.app.workspace.activeLeaf?.view instanceof MarkdownView))
				return console.error('cant switch collab in non markdown view');
			const editor = (this.app.workspace.activeLeaf.view as MarkdownView).editor;

			currentlyActive?.yjsProvider?.disconnect();
			currentlyActive = this.getActiveFileVersions();

			if (!currentlyActive.localFile) return new Notice('there is no file here');
			if (!currentlyActive.remoteFile) return console.log('no remote file');
			if (!currentlyActive.yjsProvider) throw new Error('cant happen');

			editor.setValue('');

			currentlyActive.yjsProvider.connect();
			const ytext = currentlyActive.yjsProvider.doc.getText('codemirror' + currentlyActive.localFile.path);
			const undoManager = new UndoManager(ytext);

			setNewExtension(yCollab(ytext, currentlyActive.yjsProvider.awareness, { undoManager }));

			// just hope all clients sync faster than this. Yes it sucks. Proper solution is seeding the history via the central server
			await new Promise<void>(resolve => setTimeout(() => resolve(), 200));

			// if nothing is in the editor, check what the server has got. This is a bad way of doing it, server should be full peer and/or provide the history
			if (!ytext.length) editor.setValue(currentlyActive.remoteFile.data);
		};

		switchCollab();
		this.registerEvent(this.app.workspace.on('active-leaf-change', async leaf => switchCollab()));

Hi @yorrd,

It is fairly easy to switch providers. Simply destroy the previous one and then connect the new one (or in a different order). E.g.

provider.destroy()
provider = new YWebsocketProvider(ydoc, 'room', ..)

However, switching a provider doesn’t eliminate old content from the Yjs document. I get the impression that you want to switch providers whenever you switch to a different document.

I suggest that you create a new Yjs document when you switch to a different document. The new document will be connected to a different provider. You can destroy the old document (including the editor binding and the old provider) simply by calling ydoc.destroy().

I also recommend to use fixed root-type namings instead of dynamic ones.

ydoc.get('codemirror') // good
ydoc.get('codemirror' + somedynamicpath) bad

There is a lot of information on yjs.dev that will be helpful to you in your task. I recommend checking out the “getting started” section (all 4 sections).

@dmonad really appreciate the help. I took the time to hopefully understand everything a little better. My main issue was around integration with obsidian because obsidian would not properly replace the codemirror extension the way I was handling it.

Also, I switched to having one doc with several texts, like in the last getting started demo. Now one last problem remains which revolves around cleanup as well, though not providers but observing.

I have noticed a comment: // @todo, which is written in the destroy lifecycle hook of y-sync.js in y-codemirror.next. This indeed seems to be problematic because I’ll get “phantom updates” even though the respective plugin instance should have been garbage collected.

I have cloned y-codemirror.next and have tried to fix the issue. While not fixing the leak, when I just save a destroyed field on the instance and set it to true to, I can then skip incoming updates for destroyed plugin instances. Obviously that’s a bad solution. I’ve already tried calling this.conf.ytext.unobserve(functionReference), which errors with [yjs] Tried to remove event handler that doesn't exist.. What might I be missing? I’m pretty certain that event handler should still exist and it also seems pretty straight forward to me that it should be cleaned up in the destroy hook, no?

Glad to provide current state of the code if helpful, just don’t want to make a mess here.

You are right, the destroy method is currently not implemented. this.type = this.conf.ytext; this.type.observe(this.observer) should be in the constructor and this.type.unobserve(this.observer) should be in the destroy method. Probably, the facet was replaced with a new type and therefore the old type is no longer accessible. If this still doesn’t work, maybe you can send the exact code you wrote?

I’d be happy about a PR. Also, feel free to open a ticket on y-websocket.next and I will eventually work on it.

awesome. Saving the text instance solved that problem and it’s obvious as well because naturally the instance can be gone at that point. Appreciate the pointer.

Before I make a pull request: I’ve started modifying the repo so that the ytext can be passed in a variable. Reason being that Obsidian doesn’t give me a way to replace the extension in time before the other file has been opened and throws events. Therefore the API surface changes to

		const extensionArray: Extension[] = yCollab(
			() => {
			    ... find and return current ytext
			},
			provider.awareness,
			{},
		);
		this.registerEditorExtension(extensionArray);

I don’t think that’s a particularly pretty way of doing it but because of the lack of hooks in Obsidian, I’m forced to have the codemirror extension worry about loading the correct y-text.

If I’m going to make a PR, might as well think about this first because that’s a change on my fork as well.

Lastly, how are you formatting your code at the moment? My prettier seems to be set up differently and removes some whitespace every now and then. Don’t want to push formatting updates in the PR…

I’m using standard: npm run lint or just standard. standard --fix autofixes issues.

I just implemented the destroy method. The awareness plugin also didn’t have a destroy method yet.

@dmonad much appreciated. Working on being able to update the ytext instance without recreating the codemirror extension - which apparently is necessary to make this work on Obsidian. I’ll send you a PR when I’m somewhat done and you just let me know if that’s something you’re willing to take into the repo :slight_smile:

1 Like