Comparing version 0.1.0-alpha.11 to 1.0.0-alpha.0
@@ -5,7 +5,15 @@ # Changelog | ||
To be released in 0.1.0 | ||
To be released in 1.0.0 | ||
- Metacom protocol implementation for client and server | ||
- Support ws, wss, http and https transports | ||
- Automatic reconnect on network errors or disconnect | ||
- Server-side introspection and Client-side scaffolding | ||
- Domain errors passing to browser client code | ||
- Support ping interval (default 60s) and call timeout (default 7s) | ||
- Reconnect active connections on browser `onlene` event | ||
## [0.0.0][] | ||
Module stub v0.0.0 and all before 0.1.0 are experiments with syntactic and | ||
Module stub v0.0.0 and all before 1.0.0 are experiments with syntactic and | ||
binary structures and multiple different ideas originated from JSTP and old | ||
@@ -12,0 +20,0 @@ protocols like USP and CLEAR. |
@@ -0,1 +1,15 @@ | ||
import { EventEmitter } from './events.js'; | ||
const CALL_TIMEOUT = 7 * 1000; | ||
const PING_INTERVAL = 60 * 1000; | ||
const RECONNECT_TIMEOUT = 2 * 1000; | ||
const connections = new Set(); | ||
window.addEventListener('online', () => { | ||
for (const connection of connections) { | ||
if (!connection.connected) connection.open(); | ||
} | ||
}); | ||
class MetacomError extends Error { | ||
@@ -26,20 +40,32 @@ constructor({ message, code }) { | ||
export class Metacom { | ||
constructor(url) { | ||
export class Metacom extends EventEmitter { | ||
constructor(url, options = {}) { | ||
super(); | ||
this.url = url; | ||
this.socket = new WebSocket(url); | ||
this.socket = null; | ||
this.api = {}; | ||
this.callId = 0; | ||
this.calls = new Map(); | ||
this.socket.addEventListener('message', ({ data }) => { | ||
this.message(data); | ||
}); | ||
this.active = false; | ||
this.connected = false; | ||
this.lastActivity = new Date().getTime(); | ||
this.callTimeout = options.callTimeout || CALL_TIMEOUT; | ||
this.pingInterval = options.pingInterval || PING_INTERVAL; | ||
this.reconnectTimeout = options.reconnectTimeout || RECONNECT_TIMEOUT; | ||
this.open(); | ||
} | ||
static create(url, options) { | ||
const { transport } = Metacom; | ||
const Transport = url.startsWith('ws') ? transport.ws : transport.http; | ||
return new Transport(url, options); | ||
} | ||
message(data) { | ||
if (data === '{}') return; | ||
this.lastActivity = new Date().getTime(); | ||
let packet; | ||
try { | ||
packet = JSON.parse(data); | ||
} catch (err) { | ||
console.error(err); | ||
} catch { | ||
return; | ||
@@ -70,11 +96,4 @@ } | ||
ready() { | ||
return new Promise(resolve => { | ||
if (this.socket.readyState === WebSocket.OPEN) resolve(); | ||
else this.socket.addEventListener('open', resolve); | ||
}); | ||
} | ||
async load(...interfaces) { | ||
const introspect = this.httpCall('system')('introspect'); | ||
const introspect = this.scaffold('system')('introspect'); | ||
const introspection = await introspect(interfaces); | ||
@@ -86,3 +105,3 @@ const available = Object.keys(introspection); | ||
const iface = introspection[interfaceName]; | ||
const request = this.socketCall(interfaceName); | ||
const request = this.scaffold(interfaceName); | ||
const methodNames = Object.keys(iface); | ||
@@ -96,29 +115,3 @@ for (const methodName of methodNames) { | ||
httpCall(iname, ver) { | ||
return methodName => (args = {}) => { | ||
const callId = ++this.callId; | ||
const interfaceName = ver ? `${iname}.${ver}` : iname; | ||
const target = interfaceName + '/' + methodName; | ||
const packet = { call: callId, [target]: args }; | ||
const dest = new URL(this.url); | ||
const protocol = dest.protocol === 'ws:' ? 'http' : 'https'; | ||
const url = `${protocol}://${dest.host}/api`; | ||
return fetch(url, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: JSON.stringify(packet), | ||
}).then(res => { | ||
const { status } = res; | ||
if (status === 200) { | ||
return res.json().then(packet => { | ||
if (packet.error) throw new MetacomError(packet.error); | ||
return packet.result; | ||
}); | ||
} | ||
throw new Error(`Status Code: ${status}`); | ||
}); | ||
}; | ||
} | ||
socketCall(iname, ver) { | ||
scaffold(iname, ver) { | ||
return methodName => async (args = {}) => { | ||
@@ -128,7 +121,13 @@ const callId = ++this.callId; | ||
const target = interfaceName + '/' + methodName; | ||
await this.ready(); | ||
if (!this.connected) await this.open(); | ||
return new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
if (this.calls.has(callId)) { | ||
this.calls.delete(callId); | ||
reject(new Error('Request timeout')); | ||
} | ||
}, this.callTimeout); | ||
this.calls.set(callId, [resolve, reject]); | ||
const packet = { call: callId, [target]: args }; | ||
this.socket.send(JSON.stringify(packet)); | ||
this.send(JSON.stringify(packet)); | ||
}); | ||
@@ -138,1 +137,89 @@ }; | ||
} | ||
class WebsocketTransport extends Metacom { | ||
async open() { | ||
if (this.connected) return; | ||
const socket = new WebSocket(this.url); | ||
this.active = true; | ||
this.socket = socket; | ||
connections.add(this); | ||
socket.addEventListener('message', ({ data }) => { | ||
this.message(data); | ||
}); | ||
socket.addEventListener('close', () => { | ||
this.connected = false; | ||
setTimeout(() => { | ||
if (this.active) this.open(); | ||
}, this.reconnectTimeout); | ||
}); | ||
socket.addEventListener('error', () => { | ||
socket.close(); | ||
}); | ||
setInterval(() => { | ||
if (this.active) { | ||
const interval = new Date().getTime() - this.lastActivity; | ||
if (interval > this.pingInterval) this.send('{}'); | ||
} | ||
}, this.pingInterval); | ||
return new Promise(resolve => { | ||
socket.addEventListener('open', () => { | ||
this.connected = true; | ||
resolve(); | ||
}); | ||
}); | ||
} | ||
close() { | ||
this.active = false; | ||
connections.delete(this); | ||
if (!this.socket) return; | ||
this.socket.close(); | ||
this.socket = null; | ||
} | ||
send(data) { | ||
if (!this.connected) return; | ||
this.lastActivity = new Date().getTime(); | ||
this.socket.send(data); | ||
} | ||
} | ||
class HttpTransport extends Metacom { | ||
async open() { | ||
this.active = true; | ||
this.connected = true; | ||
} | ||
close() { | ||
this.active = false; | ||
this.connected = false; | ||
} | ||
send(data) { | ||
this.lastActivity = new Date().getTime(); | ||
fetch(this.url, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: data, | ||
}).then(res => { | ||
const { status } = res; | ||
if (status === 200) { | ||
return res.text().then(packet => { | ||
if (packet.error) throw new MetacomError(packet.error); | ||
this.message(packet); | ||
}); | ||
} | ||
throw new Error(`Status Code: ${status}`); | ||
}); | ||
} | ||
} | ||
Metacom.transport = { | ||
ws: WebsocketTransport, | ||
http: HttpTransport, | ||
}; |
@@ -110,2 +110,6 @@ 'use strict'; | ||
message(data) { | ||
if (data === '{}') { | ||
this.connection.send('{}'); | ||
return; | ||
} | ||
let packet; | ||
@@ -112,0 +116,0 @@ try { |
{ | ||
"name": "metacom", | ||
"version": "0.1.0-alpha.11", | ||
"version": "1.0.0-alpha.0", | ||
"author": "Timur Shemsedinov <timur.shemsedinov@gmail.com>", | ||
@@ -52,3 +52,3 @@ "description": "Communication protocol for Metarhia stack with rpc, events, binary streams, memory and db access", | ||
"eslint-plugin-import": "^2.22.1", | ||
"eslint-plugin-prettier": "^3.2.0", | ||
"eslint-plugin-prettier": "^3.3.0", | ||
"metatests": "^0.7.2", | ||
@@ -55,0 +55,0 @@ "prettier": "^2.2.1" |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
24877
12
675