forked from ProseMirror/website
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
There's a bug where polling is dropped and never resumed
- Loading branch information
0 parents
commit 65bae40
Showing
9 changed files
with
376 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/node_modules | ||
/public/demo.js | ||
.tern-port |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"name": "prosemirror-demo", | ||
"version": "0.0.1", | ||
"description": "Demo of ProseMirror's collaborative editing", | ||
"maintainers": [ | ||
{ | ||
"name": "Marijn Haverbeke", | ||
"email": "[email protected]", | ||
"web": "http://marijnhaverbeke.nl" | ||
} | ||
], | ||
"dependencies": { | ||
"ecstatic": "^0.8.0", | ||
"prosemirror": "^0.0.1" | ||
}, | ||
"devDependencies": { | ||
"babel": "^5.4.7", | ||
"babelify": "^6.0.2", | ||
"cssify": "^0.7.0", | ||
"watchify": "^3.2.0" | ||
}, | ||
"scripts": { | ||
"start": "node src/server/start.js", | ||
"demo": "watchify -d --outfile public/demo.js -t babelify src/client/client.js" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<!doctype html> | ||
|
||
<meta charset="utf-8"> | ||
<title>ProseMirror demo</title> | ||
|
||
<body> | ||
|
||
<h1>ProseMirror collaborative editing demo</h1> | ||
|
||
<script src="demo.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import {Node} from "prosemirror/dist/model" | ||
import {ProseMirror} from "prosemirror/dist/edit" | ||
import "prosemirror/dist/collab" | ||
|
||
import {GET, POST} from "./http" | ||
|
||
function report(err) { | ||
console.log("ERROR:", err.toString()) // FIXME | ||
} | ||
|
||
class Channel { | ||
constructor(url, version) { | ||
this.url = url | ||
this.version = version | ||
this.listener = null | ||
this.sending = this.polling = false | ||
} | ||
|
||
listen(f) { | ||
this.listener = f | ||
this.poll() | ||
} | ||
|
||
send(version, steps, callback) { | ||
this.sending = true | ||
if (this.polling) this.polling.abort() | ||
console.log("sending", steps.length, this.version) | ||
|
||
POST(this.url + "/steps", | ||
JSON.stringify({version: version, steps: steps}), | ||
"application/json", | ||
err => { | ||
if (err && err.status != 406) { | ||
report(err) | ||
// FIXME retry a few times then callback(false)? this leaves | ||
// client never sending anything again | ||
// Move scheduling responsibility out of the collab module entirely? | ||
} else { | ||
let ok = !err | ||
console.log("sent", steps.length, ok) | ||
callback(ok) | ||
if (ok) this.version += steps.length | ||
this.sending = false | ||
this.poll() | ||
} | ||
}) | ||
} | ||
|
||
poll() { | ||
if (this.polling) return | ||
console.log("start polling @", this.version) | ||
this.polling = GET(this.url + "/steps/" + this.version, (err, steps) => { | ||
this.polling = false | ||
console.log("received response", steps.length, this.version, this.sending) | ||
if (this.sending) return | ||
|
||
if (err) { | ||
report(err) // FIXME swallow a few errors before giving up | ||
setTimeout(() => this.poll(), 500) | ||
} else { | ||
steps = JSON.parse(steps) | ||
if (steps.length) { | ||
this.version += steps.length | ||
this.listener(steps) | ||
} | ||
this.poll() | ||
} | ||
}) | ||
} | ||
|
||
static start(url, callback) { | ||
GET(url, (err, data) => { | ||
data = JSON.parse(data) | ||
callback(err, !err && {channel: new Channel(url, data.version), | ||
doc: Node.fromJSON(data.doc)}) | ||
}) | ||
} | ||
} | ||
|
||
Channel.start("/doc/test", (err, data) => { | ||
if (err) return report(err) | ||
window.pm = new ProseMirror({ | ||
place: document.body, | ||
doc: data.doc, | ||
collab: {channel: data.channel, | ||
version: data.channel.version} | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
export function req(conf, callback) { | ||
let req = new XMLHttpRequest() | ||
req.open(conf.method, conf.url, true) | ||
req.addEventListener("load", () => { | ||
if (req.status < 400) { | ||
callback(null, req.responseText) | ||
} else { | ||
let err = new Error("Request failed: " + req.statusText + (req.responseText ? "\n\n" + req.responseText : "")) | ||
err.status = req.status | ||
callback(err) | ||
} | ||
}) | ||
req.addEventListener("error", function() { | ||
callback(null, new Error("Network error")) | ||
}) | ||
if (conf.headers) for (let header in conf.headers) req.setRequestHeader(header, conf.headers[header]) | ||
req.send(conf.body || null) | ||
return req | ||
} | ||
|
||
export function GET(url, callback) { | ||
return req({url: url, method: "GET"}, callback) | ||
} | ||
|
||
export function POST(url, body, type, callback) { | ||
return req({url: url, method: "POST", body: body, headers: {"Content-Type": type}}, callback) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import {Node} from "prosemirror/dist/model" | ||
import {applyStep} from "prosemirror/dist/transform" | ||
|
||
class Instance { | ||
constructor(id) { | ||
this.id = id | ||
this.doc = new Node("doc", null, [new Node("paragraph")]) | ||
this.version = 0 | ||
this.steps = [] | ||
this.lastActive = Date.now() | ||
this.waiting = [] | ||
} | ||
} | ||
|
||
const instances = Object.create(null) | ||
let instanceCount = 0 | ||
let maxCount = 500 | ||
|
||
export function getInstance(id) { | ||
return instances[id] || newInstance(id) | ||
} | ||
|
||
function newInstance(id) { | ||
if (++instanceCount > maxCount) { | ||
let oldest = null | ||
for (let id in instances) { | ||
let inst = instances[id] | ||
if (!oldest || inst.lastActive < oldest.lastActive) oldest = inst | ||
} | ||
delete instances[oldest.id] | ||
} | ||
return instances[id] = new Instance(id) | ||
} | ||
|
||
export function addSteps(id, version, steps) { | ||
let inst = getInstance(id) | ||
if (version < 0 || version > inst.version) throw new Error("Bogus version " + version) | ||
if (inst.version != version) return false | ||
let doc = inst.doc | ||
for (let i = 0; i < steps.length; i++) | ||
doc = applyStep(doc, steps[i]).doc | ||
inst.doc = doc | ||
inst.version += steps.length | ||
inst.steps = inst.steps.concat(steps) | ||
inst.lastActive = Date.now() | ||
while (inst.waiting.length) inst.waiting.pop()() | ||
return true | ||
} | ||
|
||
export function getSteps(id, version) { | ||
let inst = getInstance(id) | ||
if (version < 0 || version > inst.version) throw new Error("Bogus version " + version) | ||
let startIndex = inst.steps.length - (inst.version - version) | ||
if (startIndex < 0) return false | ||
inst.lastActive = Date.now() | ||
return inst.steps.slice(startIndex) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import {parse} from "url" | ||
|
||
export class Router { | ||
constructor() { this.routes = [] } | ||
|
||
add(method, url, handler) { | ||
this.routes.push({method, url, handler}) | ||
} | ||
|
||
match(pattern, path) { | ||
if (typeof pattern == "string") { | ||
if (pattern == path) return [] | ||
} else if (pattern instanceof RegExp) { | ||
let match = pattern.exec(path) | ||
return match && match.slice(1) | ||
} else { | ||
let parts = path.slice(1).split("/") | ||
if (parts.length != pattern.length) return null | ||
let result = [] | ||
for (let i = 0; i < parts.length; i++) { | ||
let pat = pattern[i] | ||
if (pat) { | ||
if (pat != parts[i]) return null | ||
} else { | ||
result.push(parts[i]) | ||
} | ||
} | ||
return result | ||
} | ||
} | ||
|
||
resolve(request, response) { | ||
let path = parse(request.url).pathname | ||
|
||
return this.routes.some(route => { | ||
let match = route.method == request.method && this.match(route.url, path) | ||
if (!match) return false | ||
|
||
let urlParts = match.map(decodeURIComponent) | ||
route.handler(request, response, ...urlParts) | ||
return true | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import {createServer} from "http" | ||
import Promise from "promise" | ||
import {Router} from "./route" | ||
import ecstatic from "ecstatic" | ||
|
||
import {Step} from "prosemirror/dist/transform" | ||
|
||
import {getInstance, getSteps, addSteps} from "./instance" | ||
|
||
const port = 8000 | ||
|
||
const router = new Router | ||
const fileServer = ecstatic({root: __dirname + "/../../public"}) | ||
|
||
const server = createServer((req, resp) => { | ||
router.resolve(req, resp) || fileServer(req, resp) | ||
}) | ||
|
||
server.listen(port) | ||
|
||
class Output { | ||
constructor(code, body, type) { | ||
this.code = code | ||
this.body = body | ||
this.type = type || "text/plain" | ||
} | ||
|
||
static json(data) { | ||
return new Output(200, JSON.stringify(data), "application/json") | ||
} | ||
|
||
resp(resp) { | ||
resp.writeHead(this.code, {"Content-Type": this.type}) | ||
resp.end(this.body) | ||
} | ||
} | ||
|
||
function readStreamAsJSON(stream, callback) { | ||
var data = ""; | ||
stream.on("data", function(chunk) { | ||
data += chunk; | ||
}); | ||
stream.on("end", function() { | ||
var result, error; | ||
try { result = JSON.parse(data); } | ||
catch (e) { error = e; } | ||
callback(error, result); | ||
}); | ||
stream.on("error", function(error) { | ||
callback(error); | ||
}); | ||
} | ||
|
||
function handle(method, url, f) { | ||
router.add(method, url, (req, resp, ...args) => { | ||
function finish() { | ||
let output | ||
try { | ||
output = f(...args, req, resp) | ||
} catch (err) { | ||
output = new Output(500, err.toString()) | ||
} | ||
if (output) output.resp(resp) | ||
} | ||
|
||
if (method == "PUT" || method == "POST") | ||
readStreamAsJSON(req, (err, val) => { | ||
if (err) new Output(500, err.toString()).resp(resp) | ||
else { args.unshift(val); finish() } | ||
}) | ||
else | ||
finish() | ||
}) | ||
} | ||
|
||
handle("GET", ["doc", null], id => { | ||
let inst = getInstance(id) | ||
return Output.json({doc: inst.doc.toJSON(), version: inst.version}) | ||
}) | ||
|
||
function nonNegInteger(str) { | ||
let num = Number(str) | ||
if (!isNaN(num) && Math.floor(num) == num && num >= 0) return num | ||
throw new Error("Not a non-negative integer: " + str) | ||
} | ||
|
||
class Waiting { | ||
constructor(resp) { | ||
this.resp = resp | ||
this.done = false | ||
setTimeout(() => this.send([]), 1000 * 60 * 5) | ||
} | ||
|
||
send(steps) { | ||
if (this.done) return | ||
Output.json(steps.map(s => s.toJSON())).resp(this.resp) | ||
this.done = true | ||
} | ||
} | ||
|
||
handle("GET", ["doc", null, "steps", null], (id, version, _, resp) => { | ||
version = nonNegInteger(version) | ||
let steps = getSteps(id, version) | ||
if (steps === false) | ||
return new Output(410, "steps no longer available") | ||
if (steps.length) | ||
return Output.json(steps.map(s => s.toJSON())) | ||
let wait = new Waiting(resp) | ||
getInstance(id).waiting.push(() => wait.send(getSteps(id, version))) | ||
}) | ||
|
||
handle("POST", ["doc", null, "steps"], (data, id) => { | ||
let version = nonNegInteger(data.version) | ||
let steps = data.steps.map(s => Step.fromJSON(s)) | ||
if (addSteps(id, data.version, steps)) | ||
return new Output(204) | ||
else | ||
return new Output(406, "Version not current") | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
require("babel/register") | ||
require("./server") |