import { ChangeSet, Text } from '@codemirror/state'; import {EditorView} from "codemirror" import { Update, collab, getSyncedVersion, receiveUpdates, sendableUpdates } from '@codemirror/collab'; import { ViewPlugin, ViewUpdate } from '@codemirror/view'; export class Connection { private requestId = 0; private resolves: Record void> = {}; constructor(private client: WebSocket) { client.addEventListener('message', (event) => { const response = JSON.parse(event.data); if ('_request' in response) { const resolve = this.resolves[response._request]; if (resolve) { resolve(response.payload); } else { console.error('Received response for unknown or already used request', response._request); } } else { console.error('Received invalid response', response._request); } }) } request(body: Record): Promise { body['_request'] = this.requestId; this.client.send(JSON.stringify(body)); return new Promise((resolve) => this.resolves[this.requestId++] = resolve); } } function pushUpdates( connection: Connection, version: number, fullUpdates: readonly Update[] ): Promise { // Strip off transaction data let updates = fullUpdates.map(u => ({ clientID: u.clientID, changes: u.changes.toJSON() })); return connection.request({type: "pushUpdates", version, updates}); } function pullUpdates( connection: Connection, version: number ): Promise { return connection.request({type: "pullUpdates", version}) .then(updates => updates.map((u: any) => ({ changes: ChangeSet.fromJSON(u.changes), clientID: u.clientID }))); } export function getDocument( connection: Connection ): Promise<{version: number, doc: Text}> { return connection.request({ type: "getDocument" }).then(data => ({ version: data.version, doc: Text.of(data.doc.split("\n")) })); } export function peerExtension(startVersion: number, connection: Connection) { let plugin = ViewPlugin.fromClass(class { private pushing = false private done = false constructor(private view: EditorView) { this.pull() } update(update: ViewUpdate) { if (update.docChanged) this.push() } async push() { let updates = sendableUpdates(this.view.state) if (this.pushing || !updates.length) return this.pushing = true let version = getSyncedVersion(this.view.state) await pushUpdates(connection, version, updates) this.pushing = false // Regardless of whether the push failed or new updates came in // while it was running, try again if there's updates remaining if (sendableUpdates(this.view.state).length) setTimeout(() => this.push(), 100) } async pull() { while (!this.done) { let version = getSyncedVersion(this.view.state) let updates = await pullUpdates(connection, version) this.view.dispatch(receiveUpdates(this.view.state, updates)) } } destroy() { this.done = true } }) return [collab({startVersion}), plugin] }