/* socket.js, a simple wrapper for a YWOT websocket connection */ const ws = require('ws') const EventEmitter = require('events'); let mod = (div, end) => ( ( (div % end) + end ) % end ) // Makes it such that (-2) % 3 = 1 instead of -2. class retryws extends EventEmitter{ // a wrapper on ws that retries on failure constructor(addr) { super(); this._sock; this._sockdown = true; this._addr = addr; this.sockinit(); } send(message){ if (this._sockdown){ throw "Socket is down!"; } this._sock.send(message); } sockinit(){ // addr is not expected to change, so not handed as input; similarly, sock is not passed in a chain because behavior is expected to be serial (either initializing or retrying) let self = this; this._sock = new ws(this._addr); this._sock.on('open', () => { self._sockdown = false; self.emit('open'); }); this._sock.on('close', (err) => { console.log('close', err); setTimeout(self.sockinit,1000); self._sockdown = true; self.emit('close'); }); this._sock.on('error', (err) => { console.log('error', err); self._sock.close(); }); this._sock.on('message', (message) => { self.emit('message',message);}); } } class Socket extends retryws { constructor(world=''){ // Takes the name of the world, which can be an empty string. let loc = (world == '') ? '' : `${world}/`; let addr = `wss://www.yourworldoftext.com/${loc}ws/`; super(addr); this._writect = 0; // A running counter to make the server write confirmations meaningful this.on('message', this._msgparse); } _msgparse(msg){ msg = JSON.parse(msg); switch (msg.kind){ case 'write': this.emit('write', msg.accepted, msg.rejected); break; // A confirmation message for writes (accepted/rejected updates) case 'cursor': this.emit('cursor', msg.positions.map(pos => [pos.tileY*8+pos.charY,pos.tileX*16+pos.charX]), msg.sender); break; // Must be used for self-identification case 'tileUpdate': this.emit('tileUpdate', msg.sender, msg.source, msg.tiles); break; // Any change by another user or possibly oneself case 'fetch': this.emit('fetch', msg.tiles); break; // The response to a fetch request } } fetch(coords){ //coords is a list of min/max pairs of y/x coordinate pairs which each describe at most 1000 tiles for (let i=0; i 1000) throw "Fetch has overlarge rectangular request"; coords[i] = {"minY":c[0][0], "minX":c[0][1], "maxY":c[1][0], "maxX":c[1][1]}; } this.send( JSON.stringify({ "fetchRectangles": coords, "kind": "fetch", "v": "3" })); } write(chars){ //chars is an list of pairs [ pixel coordinate (y/x < 16), char ] if (chars.length > 200) throw "Too many characters to write by server decree"; if (chars.length == 0) throw "Send at least a character."; let write = []; for (let cha of chars){ let coord = cha[0]; write.push([ Math.floor(coord[0]/8), Math.floor(coord[1]/16), mod(coord[0], 8), mod(coord[1], 16), 0, cha[1], this._writect ]); this._writect++; } this.send( JSON.stringify({ "edits": write, "kind": "write" })); return this._writect-1; // Last label within write for cross-referencing with confirmation } cursor(coords){ //coords is just one pair of char coords; I think the api could handle more, but it's unnecessary for now. this.send( JSON.stringify({ "kind": "cursor", "positions": [ { "tileY": Math.floor(coords[0]/8), "tileX": Math.floor(coords[1]/16), "charY": coords[0] % 8, "charX": coords[1] % 16 } ] })); } } module.exports = Socket;