assignment-terrible-editor/app/index.js

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) {
}
})
);
}