327 lines
6.8 KiB
JavaScript
327 lines
6.8 KiB
JavaScript
const { readFile, writeFile } = require('fs');
|
|
const { connect, createServer } = require('net');
|
|
const EventEmitter = require('events');
|
|
const { promisify } = require('util');
|
|
const { getCurrentWindow, dialog } = require('electron').remote;
|
|
const DMP = require('diff-match-patch');
|
|
|
|
const { defaultValue, stepOne, stepServerTwo, stepServerThree, stepClientTwo, stepClientThree } = require('./strings.js');
|
|
|
|
// Setup code mirror
|
|
|
|
CodeMirror.commands.save = save;
|
|
CodeMirror.commands.open = open;
|
|
|
|
const m = CodeMirror(document.body, {
|
|
theme: 'tomorrow-night-eighties',
|
|
keyMap: 'sublime',
|
|
autofocus: true,
|
|
lineNumbers: true,
|
|
tabSize: 2,
|
|
indentUnit: 2,
|
|
indentWithTabs: true,
|
|
extraKeys: {
|
|
'Cmd-O': 'open',
|
|
'Ctrl-O': 'open',
|
|
'Cmd-S': 'save',
|
|
'Ctrl-S': 'save',
|
|
},
|
|
value: defaultValue + stepOne,
|
|
});
|
|
|
|
m.on('change', change);
|
|
|
|
// Simple APIs
|
|
|
|
function setTitle(t) {
|
|
console.log(t);
|
|
document.querySelector('.title').innerText = t;
|
|
}
|
|
|
|
let filename = '';
|
|
|
|
async function loadFile(f) {
|
|
const contents = await promisify(readFile)(f, { encoding: 'utf8' });
|
|
filename = f;
|
|
m.setValue(contents);
|
|
}
|
|
|
|
async function save(e) {
|
|
if (filename.length > 0) {
|
|
setTitle('saving');
|
|
await promisify(writeFile)(filename, m.getValue(), { encoding: 'utf8' });
|
|
setTitle('saved');
|
|
setTimeout(() => setTitle(filename), 1000);
|
|
}
|
|
};
|
|
|
|
async function open(e) {
|
|
if (mode === 'client' && !connected) {
|
|
return await attemptConnect();
|
|
}
|
|
if (mode === 'server' && !listening) {
|
|
await attemptListen();
|
|
}
|
|
setTitle('opening');
|
|
dialog.showOpenDialog(getCurrentWindow(), {
|
|
properties: ['openFile'],
|
|
}, async (f) => {
|
|
await loadFile(f[0]);
|
|
setTitle(f[0]);
|
|
});
|
|
};
|
|
|
|
// editor changes
|
|
|
|
let oldValue = '';
|
|
|
|
async function change(e) {
|
|
if (!connected && !listening) {
|
|
await validate();
|
|
} else {
|
|
const newValue = m.getValue();
|
|
// send new for now
|
|
const delta = dmp.patch_make(oldValue, newValue);
|
|
if (oldValue === newValue) {
|
|
return;
|
|
}
|
|
oldValue = newValue;
|
|
await sendDelta(delta);
|
|
}
|
|
};
|
|
|
|
const dmp = new DMP();
|
|
|
|
async function getFirstDelta() {
|
|
return dmp.patch_make('', m.getValue());
|
|
}
|
|
|
|
async function applyDelta(delta) {
|
|
const cursor = m.getCursor();
|
|
oldValue = dmp.patch_apply(delta, m.getValue())[0]
|
|
m.setValue(oldValue);
|
|
m.setCursor(cursor);
|
|
}
|
|
|
|
// config parser
|
|
|
|
let mode = '';
|
|
let addr = '';
|
|
|
|
async function validate() {
|
|
const value = m.getValue();
|
|
const matches = value.match(/\d\..*:.*$/gm);
|
|
const values = matches.map(m => m.split(/: /).pop().trim() || '');
|
|
|
|
// Parse for inputs
|
|
|
|
// mode
|
|
if (values[0] !== mode) {
|
|
if (values[0] === 'client' || values[0] === 'server') {
|
|
mode = values[0];
|
|
const cursor = m.getCursor();
|
|
m.setValue(
|
|
defaultValue
|
|
+ stepOne
|
|
+ mode
|
|
+ (mode === 'client' ? stepClientTwo : stepServerTwo)
|
|
);
|
|
m.setCursor(cursor);
|
|
}
|
|
}
|
|
|
|
// addr
|
|
if (values[1] && values[1] !== addr && mode !== '') {
|
|
if (validAddr(values[1])) {
|
|
addr = values[1];
|
|
const cursor = m.getCursor();
|
|
m.setValue(
|
|
defaultValue
|
|
+ stepOne
|
|
+ mode
|
|
+ (mode === 'client' ? stepClientTwo : stepServerTwo)
|
|
+ addr
|
|
+ (mode === 'client' ? stepClientThree : stepServerThree)
|
|
);
|
|
m.setCursor(cursor);
|
|
}
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
async function validAddr(addr) {
|
|
if (typeof addr !== 'string') {
|
|
return false;
|
|
}
|
|
const parts = addr.split(/:/);
|
|
if (parts.length != 2) {
|
|
return false;
|
|
}
|
|
const port = parseInt(parts[1]);
|
|
return 1 <= port && port <= 65535;
|
|
}
|
|
|
|
// Socket APIs
|
|
|
|
class Parser extends EventEmitter {
|
|
|
|
constructor(readable) {
|
|
super();
|
|
|
|
this.state = 'header';
|
|
this.header = {};
|
|
this.buf = Buffer.from([]);
|
|
|
|
readable.on('data', (chunk) => {
|
|
this.process(chunk);
|
|
});
|
|
}
|
|
|
|
process(chunk) {
|
|
this.buf = Buffer.concat([this.buf, chunk]);
|
|
|
|
if (this.state === 'header') {
|
|
const until = chunk.indexOf('\n\n');
|
|
if (until > -1) {
|
|
console.log('header');
|
|
this.header = this.parseHeader(this.buf.slice(0, until));
|
|
this.buf = this.buf.slice(until + 2);
|
|
this.state = 'body';
|
|
}
|
|
}
|
|
if (this.state === 'body') {
|
|
const contentLength = parseInt(this.header['Content-Length']);
|
|
if (this.buf.length >= contentLength) {
|
|
console.log('body', contentLength, this.buf);
|
|
this.readContent(this.buf.slice(0, contentLength));
|
|
this.buf = this.buf.slice(contentLength);
|
|
this.state = 'header';
|
|
}
|
|
}
|
|
}
|
|
|
|
parseHeader(buf) {
|
|
return buf
|
|
.toString()
|
|
.split(/\n/)
|
|
.map(l => l.split(':'))
|
|
.map(l => [l[0], l.slice(1).join(':')])
|
|
.filter(l => l[0].length > 0)
|
|
.reduce((a, l) => ({
|
|
...a,
|
|
[l[0]]: l[1],
|
|
}), {});
|
|
}
|
|
|
|
readContent(buf) {
|
|
this.emit('message', {
|
|
header: this.header,
|
|
body: JSON.parse(buf.toString()),
|
|
});
|
|
this.emit('delta', JSON.parse(buf.toString()));
|
|
}
|
|
|
|
}
|
|
|
|
function serialize(delta) {
|
|
const json = JSON.stringify(delta);
|
|
return 'Content-Length: ' + Buffer.from(json).length + '\n\n' + json;
|
|
}
|
|
|
|
// client
|
|
|
|
let connected = false;
|
|
let connection = null;
|
|
async function attemptConnect() {
|
|
setTitle('connecting');
|
|
const parts = addr.split(/:/);
|
|
|
|
// connect performs a connection to a host + port
|
|
connection = connect(parseInt(parts[1]), parts[0]);
|
|
|
|
connection.on('connect', () => {
|
|
connected = true;
|
|
setTitle('connected');
|
|
m.setValue('');
|
|
});
|
|
connection.on('error', () => {
|
|
setTitle('connection error!');
|
|
});
|
|
|
|
// set up a parser to read incoming deltas
|
|
const parser = new Parser(connection);
|
|
parser.on('delta', m => {
|
|
// apply delta to editor
|
|
applyDelta(m);
|
|
});
|
|
}
|
|
|
|
async function sendDelta(delta) {
|
|
if (mode === 'server') {
|
|
// send directly to server broadcast thing
|
|
return await broadcastDelta(delta);
|
|
}
|
|
// send delta to server
|
|
connection.write(serialize(delta));
|
|
}
|
|
|
|
// server
|
|
|
|
let listening = false;
|
|
let server = null;
|
|
let connections = [];
|
|
function attemptListen() {
|
|
return new Promise((resolve, reject) => {
|
|
setTitle('setting up server');
|
|
|
|
// createServer() opens a TCP port to accept incoming connections
|
|
server = createServer();
|
|
connections = [];
|
|
|
|
server.on('connection', async (c) => {
|
|
setTitle('new client connected');
|
|
setTimeout(() => setTitle(filename), 1000);
|
|
|
|
// maintain a list of clients to do broadcasting later
|
|
connections.push(c);
|
|
// send the first update
|
|
c.write(serialize(await getFirstDelta()));
|
|
|
|
// setup incoming parser for deltas
|
|
const parser = new Parser(c);
|
|
parser.on('delta', async m => {
|
|
// apply delta to editor
|
|
applyDelta(m);
|
|
broadcastDelta(m, c);
|
|
});
|
|
|
|
});
|
|
server.on('error', (e) => {
|
|
setTitle('can\'t start server, ' + e.code);
|
|
reject();
|
|
});
|
|
//server.on('listening', () => {
|
|
listening = true;
|
|
setTitle('server ready');
|
|
// resolve();
|
|
//});
|
|
server.listen(parseInt(addr));
|
|
resolve();
|
|
});
|
|
}
|
|
|
|
async function broadcastDelta(delta, omit) {
|
|
await Promise.all(
|
|
// for every connection that != omit, write data to the socket
|
|
connections
|
|
.filter(c => c !== omit)
|
|
.map(async c => {
|
|
try {
|
|
c.write(serialize(delta));
|
|
} catch (e) {
|
|
}
|
|
})
|
|
);
|
|
}
|