From 44c4de02beeb12944735c4b4b14e2cb95e0983b2 Mon Sep 17 00:00:00 2001 From: Praveen Kumar Date: Thu, 3 Sep 2020 21:24:54 +0530 Subject: [PATCH] Fix update plugin schema (#60) * IRAD-1040:Update effective schema in collab server * Fix: avoid error in update schema method in non collaborative mode. * Update Collab server with the effective schema. Co-authored-by: seybi.ea Co-authored-by: Ashfaq Shamsudeen --- flow-typed/flatted.js | 5 +++ licit/client/index.js | 2 +- licit/server/collab/instance.js | 34 +++++++++++--- licit/server/collab/server.js | 79 +++++++++++++++++++++++++++------ package.json | 1 + src/client/CollabConnector.js | 9 +++- src/client/EditorConnection.js | 55 ++++++++++++++--------- src/client/Licit.js | 5 +++ src/client/SimpleConnector.js | 6 +++ src/convertFromJSON.js | 8 ++-- 10 files changed, 161 insertions(+), 43 deletions(-) create mode 100644 flow-typed/flatted.js diff --git a/flow-typed/flatted.js b/flow-typed/flatted.js new file mode 100644 index 00000000..53090d48 --- /dev/null +++ b/flow-typed/flatted.js @@ -0,0 +1,5 @@ +// @flow + +declare module 'flatted' { + declare module.exports: any; +} diff --git a/licit/client/index.js b/licit/client/index.js index 49c5133d..59884a78 100644 --- a/licit/client/index.js +++ b/licit/client/index.js @@ -29,7 +29,7 @@ function main(): void { const plugins = null; ReactDOM.render(, el); + runtime={null} plugins={plugins} />, el); } function onChangeCB(data) { diff --git a/licit/server/collab/instance.js b/licit/server/collab/instance.js index e6263d31..d645f8e6 100644 --- a/licit/server/collab/instance.js +++ b/licit/server/collab/instance.js @@ -2,7 +2,10 @@ const { readFileSync, writeFile } = require("fs") -import EditorSchema from "../../../src/EditorSchema" +// [FS] IRAD-1040 2020-09-02 +import { Schema } from 'prosemirror-model'; + +let _editorSchema: Schema = null; const MAX_STEP_HISTORY = 10000 @@ -19,10 +22,11 @@ export class Instance { waiting = []; collecting: any; // end - constructor(id: any, doc: any) { + constructor(id: any, doc: any, effectiveSchema: Schema) { this.id = id - this.doc = doc || EditorSchema.node("doc", null, [EditorSchema.node("paragraph", null, [ - EditorSchema.text(" ") + // [FS] IRAD-1040 2020-09-02 + this.doc = doc || _editorSchema.node("doc", null, [_editorSchema.node("paragraph", null, [ + _editorSchema.text(" ") ])]) // The version number of the document instance. this.version = 0 @@ -135,7 +139,8 @@ if (process.argv.indexOf("--fresh") == -1) { if (json) { for (let prop in json) - newInstance(prop, EditorSchema.nodeFromJSON(json[prop].doc)) + // [FS] IRAD-1040 2020-09-02 + newInstance(prop, _editorSchema.nodeFromJSON(json[prop].doc)) } let saveTimeout = null, saveEvery = 1e4 @@ -143,6 +148,7 @@ function scheduleSave() { if (saveTimeout != null) return saveTimeout = setTimeout(doSave, saveEvery) } + function doSave() { saveTimeout = null let out = {} @@ -151,6 +157,24 @@ function doSave() { writeFile(saveFile, JSON.stringify(out), () => { null }) } +// [FS] IRAD-1040 2020-09-02 +function updateDocs() { + for (var prop in instances) { + instances[prop].doc = _editorSchema.nodeFromJSON(instances[prop].doc.toJSON()); + } +} + +export function setEditorSchema(effectiveSchema: Schema) { + _editorSchema = effectiveSchema; + updateDocs(); +} + +export function initEditorSchema(effectiveSchema: Schema) { + if(null == _editorSchema) { + _editorSchema = effectiveSchema; + } +} + export function getInstance(id: any, ip: any) { let inst = instances[id] || newInstance(id) if (ip) inst.registerUser(ip) diff --git a/licit/server/collab/server.js b/licit/server/collab/server.js index 5a59be7c..5ffe42ed 100644 --- a/licit/server/collab/server.js +++ b/licit/server/collab/server.js @@ -1,28 +1,38 @@ // @flow import { Step } from "prosemirror-transform" +import { Schema } from 'prosemirror-model' import Router from "./route" import EditorSchema from "../../../src/EditorSchema" -import { getInstance, instanceInfo } from "./instance" +import { getInstance, instanceInfo, setEditorSchema, initEditorSchema } from "./instance" // [FS] IRAD-899 2020-03-13 // This is for Capcomode document attribute. Shared Step, so that capcomode can be dealt collaboratively. import SetDocAttrStep from "../../../src/SetDocAttrStep"; +// [FS] IRAD-1040 2020-09-02 +import * as Flatted from 'flatted'; const router = new Router(); +// [FS] IRAD-1040 2020-09-02 +let effectiveSchema = EditorSchema; +let lastUpdatedSchema = null; + function handleCollabRequest(req: any, resp: any) { + // [FS] IRAD-1040 2020-09-02 + initEditorSchema(effectiveSchema); + const headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': ';POST, GET, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Credential': false, + 'Access-Control-Max-Age': 86400, // 24hrs + 'Access-Control-Allow-Headers': + 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept' + }; if (!router.resolve(req, resp)) { const method = req.method.toUpperCase(); if (method === 'OPTIONS') { - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': ';POST, GET, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Credential': false, - 'Access-Control-Max-Age': 86400, // 24hrs - 'Access-Control-Allow-Headers': - 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept' - }; + resp.writeHead(200, headers); resp.end(); } else { @@ -84,9 +94,23 @@ function readStreamAsJSON(stream, callback) { stream.on("error", e => callback(e)) } +// : (stream.Readable, Function) +// Invoke a callback with a stream's data. +function readStreamAsFlatted(stream, callback) { + let data = "" + stream.on("data", chunk => data += chunk) + stream.on("end", () => { + let result, error + try { result = Flatted.parse(data) } + catch (e) { error = e } + callback(error, result) + }) + stream.on("error", e => callback(e)) +} + // : (string, Array, Function) // Register a server route. -function handle(method, url, f) { +function handle(method, url, f, readFlatted = false) { router.add(method, url, (req, resp, ...args) => { function finish() { let output @@ -99,11 +123,13 @@ function handle(method, url, f) { if (output) output.resp(resp) } - if (method == "PUT" || method == "POST") - readStreamAsJSON(req, (err, val) => { + if (method == "PUT" || method == "POST") { + const readMethod = readFlatted ? readStreamAsFlatted : readStreamAsJSON; + readMethod(req, (err, val) => { if (err) new Output(500, err.toString()).resp(resp) else { args.unshift(val); finish() } }) + } else finish() }) @@ -209,10 +235,37 @@ function reqIP(request) { // The event submission endpoint, which a client sends an event to. handle("POST", ["docs", null, "events"], (data, id, req) => { let version = nonNegInteger(data.version) - let steps = data.steps.map(s => Step.fromJSON(EditorSchema, s)) + let steps = data.steps.map(s => Step.fromJSON(effectiveSchema, s)) let result = getInstance(id, reqIP(req)).addEvents(version, steps, data.clientID) if (!result) return new Output(409, "Version not current") else return Output.json(result) }) + +// [FS] IRAD-1040 2020-09-02 +// set the effective schema from client to work the plugins collaboratively +handle("POST", ["docs", null, "schema"], (data, id, req) => { + const updatedSchema = Flatted.stringify(data); + // Do a string comparison to see if they are same or not. + // if same, don't update + if(lastUpdatedSchema !== updatedSchema) { + lastUpdatedSchema = updatedSchema; + const spec = data['spec']; + updateSpec(spec, 'nodes'); + updateSpec(spec, 'marks'); + effectiveSchema = new Schema({ nodes: effectiveSchema.spec.nodes, marks: effectiveSchema.spec.marks }); + setEditorSchema(effectiveSchema); + } + return Output.json({ result: 'success' }); +}, true) + +function updateSpec(spec, attrName) { + // clear current array + effectiveSchema.spec[attrName].content.splice(0, effectiveSchema.spec[attrName].content.length); + const collection = spec[attrName]['content']; + // update current array with the latest info + for (var i = 0; i < collection.length; i += 2) { + effectiveSchema.spec[attrName] = effectiveSchema.spec[attrName].update(collection[i], collection[i+1]); + } +} \ No newline at end of file diff --git a/package.json b/package.json index 5d7bc07e..9462b19d 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "cors": "^2.8.5", "exports-loader": "1.1.0", "express": "^4.17.1", + "flatted": "^3.0.4", "flow-typed": "^3.2.0", "formidable": "^1.2.2", "invariant": "2.2.4", diff --git a/src/client/CollabConnector.js b/src/client/CollabConnector.js index aace277c..278eac63 100644 --- a/src/client/CollabConnector.js +++ b/src/client/CollabConnector.js @@ -2,6 +2,7 @@ import { Transform } from 'prosemirror-transform'; import { EditorState } from 'prosemirror-state'; +import { Schema } from 'prosemirror-model'; import SimpleConnector from './SimpleConnector'; import type { SetStateCall } from './SimpleConnector'; import EditorConnection from './EditorConnection'; @@ -29,7 +30,7 @@ class CollabConnector extends SimpleConnector { this._docID = docID; // [FS][11-MAR-2020] - // Modified the scripts to ensure not to always replace 3001 with 3002 to run both servers together, + // Modified the scripts to ensure not to always replace 3001 with 3002 to run both servers together, // instead used running hostname and configured port. const url = window.location.protocol + '\/\/' + window.location.hostname + ':3002/docs/' + @@ -57,6 +58,12 @@ class CollabConnector extends SimpleConnector { this._connection.dispatch({ type: 'transaction', transaction }); }); }; + + // FS IRAD-1040 2020-09-02 + // Send the modified schema to server + updateSchema = (schema: Schema) => { + this._connection.updateSchema(schema); + }; } export default CollabConnector; diff --git a/src/client/EditorConnection.js b/src/client/EditorConnection.js index 20de953c..e17ab230 100644 --- a/src/client/EditorConnection.js +++ b/src/client/EditorConnection.js @@ -1,21 +1,21 @@ // @flow -import nullthrows from 'nullthrows'; import { collab, getVersion, receiveTransaction, sendableSteps, } from 'prosemirror-collab'; -import {EditorState} from 'prosemirror-state'; -import {Step} from 'prosemirror-transform'; -import {EditorView} from 'prosemirror-view'; - +import { EditorState } from 'prosemirror-state'; +import { Step } from 'prosemirror-transform'; +import { EditorView } from 'prosemirror-view'; import EditorPlugins from '../EditorPlugins'; import EditorSchema from '../EditorSchema'; import uuid from '../uuid'; -import {GET, POST} from './http'; -import throttle from './throttle'; +import { GET, POST } from './http'; +// [FS] IRAD-1040 2020-09-02 +import { Schema } from 'prosemirror-model'; +import { stringify } from 'flatted'; function badVersion(err: Object) { return err.status == 400 && /invalid version/i.test(String(err)); @@ -168,25 +168,25 @@ class EditorConnection { if (err.status == 410 || badVersion(err)) { // Too far behind. Revert to server state this.report.failure(err); - this.dispatch({type: 'restart'}); + this.dispatch({ type: 'restart' }); } else if (err) { - this.dispatch({type: 'recover', error: err}); + this.dispatch({ type: 'recover', error: err }); } } ); } - sendable(editState: EditorState): ?{steps: Array} { + sendable(editState: EditorState): ?{ steps: Array } { const steps = sendableSteps(editState); if (steps) { - return {steps}; + return { steps }; } return null; } // Send the given steps to the server send(editState: EditorState, sendable: Object) { - const {steps} = sendable; + const { steps } = sendable; const json = JSON.stringify({ version: getVersion(editState), steps: steps ? steps.steps.map(s => s.toJSON()) : [], @@ -198,10 +198,10 @@ class EditorConnection { this.backOff = 0; const tr = steps ? receiveTransaction( - this.state.edit, - steps.steps, - repeat(steps.clientID, steps.steps.length) - ) + this.state.edit, + steps.steps, + repeat(steps.clientID, steps.steps.length) + ) : this.state.edit.tr; this.dispatch({ @@ -215,17 +215,32 @@ class EditorConnection { // The client's document conflicts with the server's version. // Poll for changes and then try again. this.backOff = 0; - this.dispatch({type: 'poll'}); + this.dispatch({ type: 'poll' }); } else if (badVersion(err)) { this.report.failure(err); - this.dispatch({type: 'restart'}); + this.dispatch({ type: 'restart' }); } else { - this.dispatch({type: 'recover', error: err}); + this.dispatch({ type: 'recover', error: err }); } } ); } + // [FS] IRAD-1040 2020-09-02 + // Send the modified schema to server + updateSchema(schema: Schema) { + // to avoid cyclic reference error, use flatted string. + const schemaFlatted = stringify(schema); + this.run(POST(this.url + '/schema/', schemaFlatted, 'text/plain')).then( + data => { + console.log('schema updated'); + }, + err => { + this.report.failure(err); + } + ); + } + // Try to recover from an error recover(err: Error): void { const newBackOff = this.backOff ? Math.min(this.backOff * 2, 6e4) : 200; @@ -235,7 +250,7 @@ class EditorConnection { this.backOff = newBackOff; setTimeout(() => { if (this.state.comm == 'recover') { - this.dispatch({type: 'poll'}); + this.dispatch({ type: 'poll' }); } }, this.backOff); } diff --git a/src/client/Licit.js b/src/client/Licit.js index 02d0eed4..300725cc 100644 --- a/src/client/Licit.js +++ b/src/client/Licit.js @@ -89,6 +89,11 @@ class Licit extends React.Component { embedded, runtime }; + // FS IRAD-1040 2020-26-08 + // Get the modified schema from editorstate and send it to collab server + if (this._connector.updateSchema) { + this._connector.updateSchema(this.state.editorState.schema); + } } setContent = (content: any = {}): void => { diff --git a/src/client/SimpleConnector.js b/src/client/SimpleConnector.js index a0784742..54c5f275 100644 --- a/src/client/SimpleConnector.js +++ b/src/client/SimpleConnector.js @@ -2,6 +2,7 @@ import {EditorState} from 'prosemirror-state'; import {Transform} from 'prosemirror-transform'; +import {Schema} from 'prosemirror-model'; import ReactDOM from 'react-dom'; export type SetStateCall = ( @@ -36,6 +37,11 @@ class SimpleConnector { getState = (): EditorState => { return this._editorState; }; + + // FS IRAD-1040 2020-09-02 + // Send the modified schema to server + updateSchema = (schema: Schema) => { + }; } export default SimpleConnector; \ No newline at end of file diff --git a/src/convertFromJSON.js b/src/convertFromJSON.js index e897dba5..c7f2b6e0 100644 --- a/src/convertFromJSON.js +++ b/src/convertFromJSON.js @@ -21,9 +21,11 @@ export default function convertFromJSON( let effectivePlugins = EditorPlugins; if (plugins) { for (let p of plugins) { - effectivePlugins.push(p); - if (p.getEffectiveSchema) { - editorSchema = p.getEffectiveSchema(editorSchema); + if (!effectivePlugins.includes(p)) { + effectivePlugins.push(p); + if (p.getEffectiveSchema) { + editorSchema = p.getEffectiveSchema(editorSchema); + } } } }