diff --git a/domain/resmon.js b/domain/resmon.js index 51f1a11..f1ea0c1 100644 --- a/domain/resmon.js +++ b/domain/resmon.js @@ -9,14 +9,13 @@ start() { const { interval } = application.resmon.config; - setInterval(() => { + setTimeout(() => { const stats = application.resmon.getStatistics(); - const { heapTotal, heapUsed, external, contexts, detached } = stats; + const { heapTotal, heapUsed, external } = stats; const total = application.utils.bytesToSize(heapTotal); const used = application.utils.bytesToSize(heapUsed); const ext = application.utils.bytesToSize(external); console.log(`Heap: ${used} of ${total}, ext: ${ext}`); - console.log(`Contexts: ${contexts}, detached: ${detached}`); }, interval); } }); diff --git a/lib/client.js b/lib/client.js index 058dac4..55eefa7 100644 --- a/lib/client.js +++ b/lib/client.js @@ -50,13 +50,15 @@ class Client { res.end(); } - error(status, err) { + error(status, err, callId = err) { const { req: { url }, res, connection } = this; const reason = http.STATUS_CODES[status]; + if (typeof err === 'number') err = undefined; const error = err ? err.stack : reason; const msg = status === 403 ? err.message : `${url} - ${error} - ${status}`; application.logger.error(msg); - const result = JSON.stringify({ result: 'error', reason }); + const packet = { callback: callId, error: { message: reason } }; + const result = JSON.stringify(packet); if (connection) { connection.send(result); return; @@ -66,24 +68,32 @@ class Client { res.end(result); } - async rpc(method, args) { + message(data) { + const packet = JSON.parse(data); + const [callType, methodName] = Object.keys(packet); + const callId = packet[callType]; + const args = packet[methodName]; + this.rpc(callId, methodName, args); + } + + async rpc(callId, method, args) { const { res, connection } = this; const { semaphore } = application.server; try { await semaphore.enter(); } catch { - this.error(504); + this.error(504, callId); return; } try { const session = await application.auth.restore(this); const proc = application.runMethod(method, session); if (!proc) { - this.error(404); + this.error(404, callId); return; } if (!session && proc.access !== 'public') { - this.error(403, new Error(`Forbidden: /api/${method}`)); + this.error(403, new Error(`Forbidden: /api/${method}`), callId); return; } const result = await proc.method(args); @@ -91,11 +101,12 @@ class Client { const session = application.auth.start(this, result.userId); result.token = session.token; } - const data = JSON.stringify(result); + const packet = { callback: callId, result }; + const data = JSON.stringify(packet); if (connection) connection.send(data); else res.end(data); } catch (err) { - this.error(500, err); + this.error(500, err, callId); } finally { semaphore.leave(); } diff --git a/lib/server.js b/lib/server.js index 48fb33a..41be437 100644 --- a/lib/server.js +++ b/lib/server.js @@ -9,7 +9,6 @@ const Client = require('./client.js'); const SHUTDOWN_TIMEOUT = 5000; const LONG_RESPONSE = 30000; -const METHOD_OFFSET = '/api/'.length; const clients = new Map(); @@ -51,15 +50,13 @@ const listener = (req, res) => { }); application.logger.log(`${method}\t${url}`); - if (url.startsWith('/api/')) { + if (url === '/api') { if (method !== 'POST') { client.error(403, new Error(`Forbidden: ${url}`)); return; } - receiveBody(req).then(body => { - const method = url.substring(METHOD_OFFSET); - const args = JSON.parse(body); - client.rpc(method, args); + receiveBody(req).then(data => { + client.message(data); }); } else { if (url === '/' && !req.connection.encrypted) { @@ -84,9 +81,8 @@ class Server { this.ws = new WebSocket.Server({ server: this.instance }); this.ws.on('connection', (connection, req) => { const client = new Client(req, null, connection); - connection.on('message', message => { - const { method, args } = JSON.parse(message); - client.rpc(method, args); + connection.on('message', data => { + client.message(data); }); }); this.instance.listen(port, host); diff --git a/static/.eslintrc.json b/static/.eslintrc.json new file mode 100644 index 0000000..bd14a19 --- /dev/null +++ b/static/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "parserOptions": { + "sourceType": "module" + } +} diff --git a/static/console.js b/static/console.js index f561a11..11cc39a 100644 --- a/static/console.js +++ b/static/console.js @@ -1,39 +1,8 @@ -'use strict'; +import { Metacom } from './metacom.js'; -// API Builder - -const socket = new WebSocket('wss://' + location.host); - -const buildAPI = (methods, socket = null) => { - const api = {}; - for (const method of methods) { - api[method] = (args = {}) => new Promise((resolve, reject) => { - if (socket) { - socket.send(JSON.stringify({ method, args })); - socket.onmessage = event => { - const obj = JSON.parse(event.data); - if (obj.result !== 'error') resolve(obj); - else reject(new Error(`Status Code: ${obj.reason}`)); - }; - } else { - fetch(`/api/${method}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(args), - }).then(res => { - const { status } = res; - if (status === 200) resolve(res.json()); - else reject(new Error(`Status Code: ${status}`)); - }); - } - }); - } - return api; -}; - -let api = buildAPI(['status', 'signIn', 'introspection'], socket); - -// Console Emulation +const metacom = new Metacom(location.host); +const { api } = metacom; +window.api = api; const ALPHA_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz'; @@ -276,12 +245,12 @@ function commandLoop() { const signIn = async () => { try { + await metacom.load('status', 'signIn', 'introspection'); await api.status(); } catch (err) { await api.signIn({ login: 'marcus', password: 'marcus' }); } - const methods = await api.introspection(); - api = buildAPI(methods, socket); + await metacom.load('example'); }; window.addEventListener('load', () => { diff --git a/static/index.html b/static/index.html index 6be5c1b..595ef62 100644 --- a/static/index.html +++ b/static/index.html @@ -5,7 +5,7 @@ - + diff --git a/static/metacom.js b/static/metacom.js new file mode 100644 index 0000000..afc5807 --- /dev/null +++ b/static/metacom.js @@ -0,0 +1,59 @@ +export class Metacom { + constructor(host) { + this.socket = new WebSocket('wss://' + host); + this.api = {}; + this.callId = 0; + this.calls = new Map(); + this.socket.onmessage = ({ data }) => { + try { + const packet = JSON.parse(data); + const { callback, event } = packet; + const callId = callback || event; + const [resolve, reject] = this.calls.get(callId); + if (packet.error) { + const { code, message } = packet.error; + const error = new Error(message); + error.code = code; + reject(error); + return; + } + resolve(packet.result); + } catch (err) { + console.error(err); + } + }; + } + + async load(...methods) { + for (const methodName of methods) { + this.api[methodName] = this.socketCall(methodName); + } + } + + httpCall(methodName) { + return (args = {}) => { + const callId = ++this.callId; + const packet = { call: callId, [methodName]: args }; + return fetch('/api', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(packet), + }).then(res => { + const { status } = res; + if (status === 200) return res.json().then(({ result }) => result); + throw new Error(`Status Code: ${status}`); + }); + }; + } + + socketCall(methodName) { + return (args = {}) => { + const callId = ++this.callId; + return new Promise((resolve, reject) => { + this.calls.set(callId, [resolve, reject]); + const packet = { call: callId, [methodName]: args }; + this.socket.send(JSON.stringify(packet)); + }); + }; + } +} diff --git a/test/system.js b/test/system.js index 9955cc4..fffec46 100644 --- a/test/system.js +++ b/test/system.js @@ -11,6 +11,8 @@ const PORT = 8000; const START_TIMEOUT = 1000; const TEST_TIMEOUT = 3000; +let callId = 0; + console.log('System test started'); setTimeout(async () => { worker.postMessage({ name: 'stop' }); @@ -24,8 +26,9 @@ const tasks = [ { get: '/', status: 302 }, { get: '/console.js' }, { - post: '/api/signIn', - data: { login: 'marcus', password: 'marcus' } + post: '/api', + method: 'signIn', + args: { login: 'marcus', password: 'marcus' } } ]; @@ -42,8 +45,9 @@ const getRequest = task => { request.method = 'POST'; request.path = task.post; } - if (task.data) { - task.data = JSON.stringify(task.data); + if (task.args) { + const packet = { call: ++callId, [task.method]: task.args }; + task.data = JSON.stringify(packet); request.headers = { 'Content-Type': 'application/json', 'Content-Length': task.data.length