├── .gitignore ├── .npmignore ├── .npmrc ├── README.md ├── cjs ├── client.js ├── index.js ├── package.json └── server.js ├── esm ├── client.js ├── index.js └── server.js ├── package.json └── test ├── express.js ├── index.html ├── index.js ├── oled.sh ├── oled ├── README.md ├── index.html ├── index.js ├── namespace.js └── package.json ├── package.json ├── sqlite ├── README.md ├── index.html ├── index.js ├── namespace.js └── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | test/oled/node_modules/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | coverage/ 6 | node_modules/ 7 | rollup/ 8 | test/ 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proxied-node DEPRECATED - See [coincident](https://github.com/WebReflection/coincident#coincidentserver) 2 | 3 | This is exactly the same [proxied-worker](https://github.com/WebReflection/proxied-worker#readme) module, specific for a *NodeJS* proxied namespace. 4 | 5 | The only difference is that the client side is already the exported namespace, not a Worker to initialize, and transported data uses the [Structured Clone algorithm](https://github.com/ungap/structured-clone/#readme), enabling both recursive data, but also much more than what JSON allows. 6 | 7 | Check the [oled screen demo](./test/oled/) to try it out on a Raspberry Pi. 8 | 9 | 10 | ## API 11 | 12 | The default export is a common server handler factory function. 13 | 14 | It accepts few configurations options to enable a variety of use cases, even multiple proxied namespaces, whenever that's needed. 15 | 16 | ```js 17 | // same as: const proxiedNode = require('proxied-node'); 18 | import proxiedNode from 'proxied-node'; 19 | 20 | // handler(request, response, next = void 0) 21 | const handler = proxiedNode({ 22 | wss, // a WebSocketServer options or a WebSocketServer instance 23 | namespace, // the namespace to proxy to each client 24 | match, // an optional client side URL to match. By default is /js/proxied-node.js 25 | host, // an optional host name to use. it's IPv4 / localhost otherwise 26 | port, // an optional port to use when wss is an instance of WebSocketServer already 27 | }); 28 | 29 | // express 30 | app.use(handler); 31 | 32 | // or standard http 33 | createServer((req, res) => { 34 | if (handler(req, res)) 35 | return; 36 | // ... rest of the logic 37 | }); 38 | ``` 39 | 40 | 41 | ### Server Side Example 42 | ```js 43 | const express = require('express'); 44 | const proxiedNode = require('proxied-node'); 45 | 46 | const {PORT = 8080} = process.env; 47 | 48 | const app = express(); 49 | 50 | const handler = proxiedNode({ 51 | wss: {port: 5000}, 52 | namespace: { 53 | test: 'OK', 54 | exit() { 55 | console.log('bye bye'); 56 | process.exit(0); 57 | }, 58 | sum(a, b) { 59 | return a + b; 60 | }, 61 | on(type, callback) { 62 | setTimeout(() => { 63 | callback('Event', type); 64 | }); 65 | }, 66 | async delayed() { 67 | console.log('context', this.test); 68 | // postMessage({action: 'greetings'}); 69 | return await new Promise($ => setTimeout($, 500, Math.random())); 70 | }, 71 | Class: class { 72 | constructor(name) { 73 | this.name = name; 74 | } 75 | sum(a, b) { 76 | console.log(this.name, a, b); 77 | return a + b; 78 | } 79 | } 80 | } 81 | }); 82 | 83 | app.use(handler); 84 | app.use(express.static(__dirname)); 85 | app.listen(PORT); 86 | ``` 87 | 88 | 89 | ### Client Side Example 90 | ```html 91 | 92 | 93 | 94 | 95 | 96 | proxied-node 97 | 116 | 117 | 118 | 119 | ``` 120 | -------------------------------------------------------------------------------- /cjs/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! (c) Andrea Giammarchi */ 3 | 4 | const {readFileSync} = require('fs'); 5 | const {dirname, join} = require('path'); 6 | const umeta = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('umeta')); 7 | 8 | const {require: cjs} = umeta(({url: require('url').pathToFileURL(__filename).href})); 9 | 10 | const StructuredJSON = readFileSync( 11 | join( 12 | dirname(cjs.resolve('@ungap/structured-clone')), 13 | '..', 14 | 'structured-json.js' 15 | ) 16 | ).toString().replace(/^var\s*/, 'const '); 17 | 18 | const proxy = String(function ({parse, stringify}) { 19 | 20 | const worker = $ => $; 21 | 22 | const bus = new Promise(resolve => { 23 | const ws = new WebSocket('{{URL}}'); 24 | ws.addEventListener('open', () => resolve(new Port(ws)), {once: true}); 25 | ws.addEventListener('message', ({data}) => { 26 | if (data == 'ping') 27 | ws.send('ping'); 28 | }); 29 | ws.addEventListener('error', error => { 30 | console.error(error); 31 | location.reload(true); 32 | }); 33 | ws.addEventListener('close', () => { 34 | location.reload(true); 35 | }); 36 | }); 37 | 38 | class Port { 39 | constructor(_) { 40 | this._ = _; 41 | this.$ = new Map; 42 | } 43 | postMessage(data) { 44 | this._.send(stringify(data)); 45 | } 46 | addEventListener(type, callback) { 47 | const {_: ws, $: types} = this; 48 | if (!types.has(type)) 49 | types.set(type, new Map); 50 | const listeners = types.get(type); 51 | if (!listeners.has(callback)) { 52 | listeners.set(callback, ({data}) => { 53 | if (data != 'ping') 54 | callback.call(ws, {data: parse(data)}); 55 | }); 56 | ws.addEventListener(type, listeners.get(callback)); 57 | } 58 | } 59 | removeEventListener(type, callback) { 60 | const {_: ws, $: types} = this; 61 | if (!types.has(type)) 62 | return; 63 | 64 | const listeners = types.get(type); 65 | if (listeners.has(callback)) { 66 | ws.removeEventListener(type, listeners.get(callback)); 67 | listeners.delete(callback); 68 | } 69 | } 70 | } 71 | 72 | // the rest of this scope is proxied-worker client code 73 | 74 | const {isArray} = Array; 75 | const {random} = Math; 76 | 77 | const ids = []; 78 | const cbs = []; 79 | 80 | const callbacks = ({data: {id, args}}) => { 81 | if (isArray(args)) { 82 | const i = ids.indexOf(id); 83 | if (-1 < i) 84 | cbs[i](...args); 85 | } 86 | }; 87 | 88 | let uid = 0; 89 | const post = ( 90 | port, instance, list, 91 | args = null, 92 | $ = o => o 93 | ) => new Promise((ok, err) => { 94 | const id = `proxied-worker:${instance}:${uid++}`; 95 | const target = worker(port); 96 | target.addEventListener('message', function message({ 97 | data: {id: wid, result, error} 98 | }) { 99 | if (wid === id) { 100 | target.removeEventListener('message', message); 101 | if (error != null) 102 | err(new Error(error)); 103 | else 104 | ok($(result)); 105 | } 106 | }); 107 | if (isArray(args)) { 108 | list.push(args); 109 | for (let i = 0, {length} = args; i < length; i++) { 110 | switch (typeof args[i]) { 111 | case 'string': 112 | args[i] = '$' + args[i]; 113 | break; 114 | case 'function': 115 | target.addEventListener('message', callbacks); 116 | let index = cbs.indexOf(args[i]); 117 | if (index < 0) { 118 | index = cbs.push(args[i]) - 1; 119 | ids[index] = `proxied-worker:cb:${uid++ + random()}`; 120 | } 121 | args[i] = ids[index]; 122 | break; 123 | } 124 | } 125 | } 126 | port.postMessage({id, list}); 127 | }); 128 | 129 | const create = (id, list) => new Proxy(Proxied.bind({id, list}), handler); 130 | 131 | const registry = new FinalizationRegistry(instance => { 132 | bus.then(port => port.postMessage({ 133 | id: `proxied-worker:${instance}:-0`, 134 | list: [] 135 | })); 136 | }); 137 | 138 | const handler = { 139 | apply(target, _, args) { 140 | const {id, list} = target(); 141 | return bus.then(port => post( 142 | port, id, ['apply'].concat(list), args) 143 | ); 144 | }, 145 | construct(target, args) { 146 | const {id, list} = target(); 147 | return bus.then( 148 | port => post( 149 | port, 150 | id, 151 | ['new'].concat(list), 152 | args, 153 | result => { 154 | const proxy = create(result, []); 155 | registry.register(proxy, result); 156 | return proxy; 157 | } 158 | ) 159 | ); 160 | }, 161 | get(target, key) { 162 | const {id, list} = target(); 163 | const {length} = list; 164 | switch (key) { 165 | case 'then': 166 | return length ? 167 | (ok, err) => bus.then( 168 | port => post(port, id, ['get'].concat(list)).then(ok, err) 169 | ) : 170 | void 0; 171 | case 'addEventListener': 172 | case 'removeEventListener': 173 | if (!length && !id) 174 | return (...args) => bus.then(port => { 175 | worker(port)[key](...args); 176 | }); 177 | } 178 | return create(id, list.concat(key)); 179 | } 180 | }; 181 | 182 | return create('', []); 183 | 184 | function Proxied() { 185 | return this; 186 | } 187 | }); 188 | 189 | module.exports = (URL, keys) => `${StructuredJSON} 190 | const _ = (${proxy})(StructuredJSON); 191 | export default _; 192 | export const {${keys.join(', ')}} = _; 193 | `.replace('{{URL}}', URL); 194 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! (c) Andrea Giammarchi */ 3 | 4 | const IPv4 = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('any-ipv4')); 5 | const {WebSocketServer} = require('ws'); 6 | 7 | const clientExport = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./client.js')); 8 | const serverExport = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./server.js')); 9 | 10 | const {keys} = Object; 11 | 12 | /** 13 | * @callback RequestHandler an express or generic node http server handler 14 | * @param {object} request the request instance 15 | * @param {object} response the response instance 16 | * @param {function=} next the optional `next()` to call in express 17 | */ 18 | 19 | /** 20 | * @typedef {Object} ProxiedNodeConfig used to configure the proxied namespace 21 | * @property {WebSocketServer|object} wss a WebSocketServer options or a WebSocketServer instance 22 | * @property {object} namespace the namespace to proxy to each client 23 | * @property {RegExp=} match an optional client side URL to match. By default is /\/(?:m?js\/)?proxied-node\.m?js$/ 24 | * @property {string=} host an optional host name to use. it's IPv4 / localhost otherwise 25 | * @property {number=} port an optional port to use when wss is an instance of WebSocketServer already 26 | */ 27 | 28 | /** 29 | * Configure the proxied namespace handling. 30 | * @param {ProxiedNodeConfig} config 31 | * @returns {RequestHandler} 32 | */ 33 | module.exports = function ({wss: options, namespace, match, host, port}) { 34 | const address = host || (IPv4.length ? IPv4 : 'localhost'); 35 | const ws = `ws://${address}:${port || options.port}`; 36 | const re = match || /\/(?:m?js\/)?proxied-node\.m?js$/; 37 | const exported = keys(namespace).filter(key => /^[a-z$][a-z0-9$_]*$/i.test(key)); 38 | serverExport( 39 | options instanceof WebSocketServer ? 40 | options : new WebSocketServer(options), 41 | namespace 42 | ); 43 | return (request, response, next) => { 44 | const {method, url} = request; 45 | if (method === 'GET' && re.test(url)) { 46 | response.writeHead(200, { 47 | 'Cache-Control': 'no-store', 48 | 'Content-Type': 'application/javascript;charset=utf-8' 49 | }); 50 | response.end(clientExport(ws, exported)); 51 | return true; 52 | } 53 | try { return false; } 54 | finally { 55 | if (typeof next === 'function') 56 | next(); 57 | } 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /cjs/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! (c) Andrea Giammarchi */ 3 | 4 | const {stringify, parse} = require('@ungap/structured-clone/json'); 5 | 6 | const PING_INTERVAL = 30000; 7 | 8 | const APPLY = 'apply'; 9 | const GET = 'get'; 10 | const NEW = 'new'; 11 | 12 | module.exports = (wss, Namespace) => { 13 | const alive = new WeakMap; 14 | 15 | const timer = setInterval( 16 | () => { 17 | for (const ws of wss.clients) { 18 | if (!alive.has(ws)) 19 | return; 20 | 21 | if (!alive.get(ws)) { 22 | alive.delete(ws); 23 | return ws.terminate(); 24 | } 25 | 26 | alive.set(ws, false); 27 | ws.send('ping'); 28 | } 29 | }, 30 | PING_INTERVAL 31 | ); 32 | 33 | wss.on('connection', ws => { 34 | alive.set(ws, true); 35 | ws.on('message', onmessage); 36 | ws.on('close', onclose); 37 | }); 38 | 39 | wss.on('close', () => { 40 | clearInterval(timer); 41 | }); 42 | 43 | // the rest of this scope is proxied-worker server code 44 | 45 | const instances = new WeakMap; 46 | let uid = 0; 47 | 48 | async function loopThrough(_, $, list) { 49 | const action = list.shift(); 50 | let {length} = list; 51 | 52 | if (action !== GET) 53 | length--; 54 | if (action === APPLY) 55 | length--; 56 | 57 | for (let i = 0; i < length; i++) 58 | $ = await $[list[i]]; 59 | 60 | if (action === NEW) { 61 | const instance = new $(...list.pop().map(args, _)); 62 | instances.get(this).set($ = String(uid++), instance); 63 | } 64 | else if (action === APPLY) { 65 | $ = await $[list[length]](...list.pop().map(args, _)); 66 | } 67 | 68 | return $; 69 | } 70 | 71 | async function onmessage(data) { 72 | const message = String(data); 73 | if (message === 'ping') { 74 | alive.set(this, true); 75 | return; 76 | } 77 | try { 78 | const {id, list} = parse(message); 79 | if (!/^proxied-worker:([^:]*?):-?\d+$/.test(id)) 80 | return; 81 | 82 | const instance = RegExp.$1; 83 | const bus = this; 84 | 85 | if (!instances.has(this)) 86 | instances.set(this, new Map); 87 | 88 | let result, error; 89 | if (instance.length) { 90 | const ref = instances.get(this); 91 | if (list.length) { 92 | try { 93 | result = await loopThrough.call(this, bus, ref.get(instance), list); 94 | } 95 | catch ({message}) { 96 | error = message; 97 | } 98 | } 99 | else { 100 | ref.delete(instance); 101 | return; 102 | } 103 | } 104 | else { 105 | try { 106 | result = await loopThrough.call(this, bus, Namespace, list); 107 | } 108 | catch ({message}) { 109 | error = message; 110 | } 111 | } 112 | 113 | bus.send(stringify({id, result, error})); 114 | } 115 | catch (o_O) {} 116 | } 117 | 118 | const relatedCallbacks = new WeakMap; 119 | function args(id) { 120 | if (typeof id === 'string') { 121 | if (/^proxied-worker:cb:/.test(id)) { 122 | if (!relatedCallbacks.has(this)) 123 | relatedCallbacks.set(this, new Map); 124 | 125 | const cbs = relatedCallbacks.get(this); 126 | if (!cbs.has(id)) 127 | cbs.set(id, (...args) => { this.send(stringify({id, args})); }); 128 | return cbs.get(id); 129 | } 130 | return id.slice(1); 131 | } 132 | return id; 133 | } 134 | 135 | function onclose() { 136 | alive.delete(this); 137 | } 138 | }; 139 | -------------------------------------------------------------------------------- /esm/client.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi */ 2 | 3 | import {readFileSync} from 'fs'; 4 | import {dirname, join} from 'path'; 5 | import umeta from 'umeta'; 6 | 7 | const {require: cjs} = umeta(import.meta); 8 | 9 | const StructuredJSON = readFileSync( 10 | join( 11 | dirname(cjs.resolve('@ungap/structured-clone')), 12 | '..', 13 | 'structured-json.js' 14 | ) 15 | ).toString().replace(/^var\s*/, 'const '); 16 | 17 | const proxy = String(function ({parse, stringify}) { 18 | 19 | const worker = $ => $; 20 | 21 | const bus = new Promise(resolve => { 22 | const ws = new WebSocket('{{URL}}'); 23 | ws.addEventListener('open', () => resolve(new Port(ws)), {once: true}); 24 | ws.addEventListener('message', ({data}) => { 25 | if (data == 'ping') 26 | ws.send('ping'); 27 | }); 28 | ws.addEventListener('error', error => { 29 | console.error(error); 30 | location.reload(true); 31 | }); 32 | ws.addEventListener('close', () => { 33 | location.reload(true); 34 | }); 35 | }); 36 | 37 | class Port { 38 | constructor(_) { 39 | this._ = _; 40 | this.$ = new Map; 41 | } 42 | postMessage(data) { 43 | this._.send(stringify(data)); 44 | } 45 | addEventListener(type, callback) { 46 | const {_: ws, $: types} = this; 47 | if (!types.has(type)) 48 | types.set(type, new Map); 49 | const listeners = types.get(type); 50 | if (!listeners.has(callback)) { 51 | listeners.set(callback, ({data}) => { 52 | if (data != 'ping') 53 | callback.call(ws, {data: parse(data)}); 54 | }); 55 | ws.addEventListener(type, listeners.get(callback)); 56 | } 57 | } 58 | removeEventListener(type, callback) { 59 | const {_: ws, $: types} = this; 60 | if (!types.has(type)) 61 | return; 62 | 63 | const listeners = types.get(type); 64 | if (listeners.has(callback)) { 65 | ws.removeEventListener(type, listeners.get(callback)); 66 | listeners.delete(callback); 67 | } 68 | } 69 | } 70 | 71 | // the rest of this scope is proxied-worker client code 72 | 73 | const {isArray} = Array; 74 | const {random} = Math; 75 | 76 | const ids = []; 77 | const cbs = []; 78 | 79 | const callbacks = ({data: {id, args}}) => { 80 | if (isArray(args)) { 81 | const i = ids.indexOf(id); 82 | if (-1 < i) 83 | cbs[i](...args); 84 | } 85 | }; 86 | 87 | let uid = 0; 88 | const post = ( 89 | port, instance, list, 90 | args = null, 91 | $ = o => o 92 | ) => new Promise((ok, err) => { 93 | const id = `proxied-worker:${instance}:${uid++}`; 94 | const target = worker(port); 95 | target.addEventListener('message', function message({ 96 | data: {id: wid, result, error} 97 | }) { 98 | if (wid === id) { 99 | target.removeEventListener('message', message); 100 | if (error != null) 101 | err(new Error(error)); 102 | else 103 | ok($(result)); 104 | } 105 | }); 106 | if (isArray(args)) { 107 | list.push(args); 108 | for (let i = 0, {length} = args; i < length; i++) { 109 | switch (typeof args[i]) { 110 | case 'string': 111 | args[i] = '$' + args[i]; 112 | break; 113 | case 'function': 114 | target.addEventListener('message', callbacks); 115 | let index = cbs.indexOf(args[i]); 116 | if (index < 0) { 117 | index = cbs.push(args[i]) - 1; 118 | ids[index] = `proxied-worker:cb:${uid++ + random()}`; 119 | } 120 | args[i] = ids[index]; 121 | break; 122 | } 123 | } 124 | } 125 | port.postMessage({id, list}); 126 | }); 127 | 128 | const create = (id, list) => new Proxy(Proxied.bind({id, list}), handler); 129 | 130 | const registry = new FinalizationRegistry(instance => { 131 | bus.then(port => port.postMessage({ 132 | id: `proxied-worker:${instance}:-0`, 133 | list: [] 134 | })); 135 | }); 136 | 137 | const handler = { 138 | apply(target, _, args) { 139 | const {id, list} = target(); 140 | return bus.then(port => post( 141 | port, id, ['apply'].concat(list), args) 142 | ); 143 | }, 144 | construct(target, args) { 145 | const {id, list} = target(); 146 | return bus.then( 147 | port => post( 148 | port, 149 | id, 150 | ['new'].concat(list), 151 | args, 152 | result => { 153 | const proxy = create(result, []); 154 | registry.register(proxy, result); 155 | return proxy; 156 | } 157 | ) 158 | ); 159 | }, 160 | get(target, key) { 161 | const {id, list} = target(); 162 | const {length} = list; 163 | switch (key) { 164 | case 'then': 165 | return length ? 166 | (ok, err) => bus.then( 167 | port => post(port, id, ['get'].concat(list)).then(ok, err) 168 | ) : 169 | void 0; 170 | case 'addEventListener': 171 | case 'removeEventListener': 172 | if (!length && !id) 173 | return (...args) => bus.then(port => { 174 | worker(port)[key](...args); 175 | }); 176 | } 177 | return create(id, list.concat(key)); 178 | } 179 | }; 180 | 181 | return create('', []); 182 | 183 | function Proxied() { 184 | return this; 185 | } 186 | }); 187 | 188 | export default (URL, keys) => `${StructuredJSON} 189 | const _ = (${proxy})(StructuredJSON); 190 | export default _; 191 | export const {${keys.join(', ')}} = _; 192 | `.replace('{{URL}}', URL); 193 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi */ 2 | 3 | import IPv4 from 'any-ipv4'; 4 | import {WebSocketServer} from 'ws'; 5 | 6 | import clientExport from './client.js'; 7 | import serverExport from './server.js'; 8 | 9 | const {keys} = Object; 10 | 11 | /** 12 | * @callback RequestHandler an express or generic node http server handler 13 | * @param {object} request the request instance 14 | * @param {object} response the response instance 15 | * @param {function=} next the optional `next()` to call in express 16 | */ 17 | 18 | /** 19 | * @typedef {Object} ProxiedNodeConfig used to configure the proxied namespace 20 | * @property {WebSocketServer|object} wss a WebSocketServer options or a WebSocketServer instance 21 | * @property {object} namespace the namespace to proxy to each client 22 | * @property {RegExp=} match an optional client side URL to match. By default is /\/(?:m?js\/)?proxied-node\.m?js$/ 23 | * @property {string=} host an optional host name to use. it's IPv4 / localhost otherwise 24 | * @property {number=} port an optional port to use when wss is an instance of WebSocketServer already 25 | */ 26 | 27 | /** 28 | * Configure the proxied namespace handling. 29 | * @param {ProxiedNodeConfig} config 30 | * @returns {RequestHandler} 31 | */ 32 | export default function ({wss: options, namespace, match, host, port}) { 33 | const address = host || (IPv4.length ? IPv4 : 'localhost'); 34 | const ws = `ws://${address}:${port || options.port}`; 35 | const re = match || /\/(?:m?js\/)?proxied-node\.m?js$/; 36 | const exported = keys(namespace).filter(key => /^[a-z$][a-z0-9$_]*$/i.test(key)); 37 | serverExport( 38 | options instanceof WebSocketServer ? 39 | options : new WebSocketServer(options), 40 | namespace 41 | ); 42 | return (request, response, next) => { 43 | const {method, url} = request; 44 | if (method === 'GET' && re.test(url)) { 45 | response.writeHead(200, { 46 | 'Cache-Control': 'no-store', 47 | 'Content-Type': 'application/javascript;charset=utf-8' 48 | }); 49 | response.end(clientExport(ws, exported)); 50 | return true; 51 | } 52 | try { return false; } 53 | finally { 54 | if (typeof next === 'function') 55 | next(); 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /esm/server.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi */ 2 | 3 | import {stringify, parse} from '@ungap/structured-clone/json'; 4 | 5 | const PING_INTERVAL = 30000; 6 | 7 | const APPLY = 'apply'; 8 | const GET = 'get'; 9 | const NEW = 'new'; 10 | 11 | export default (wss, Namespace) => { 12 | const alive = new WeakMap; 13 | 14 | const timer = setInterval( 15 | () => { 16 | for (const ws of wss.clients) { 17 | if (!alive.has(ws)) 18 | return; 19 | 20 | if (!alive.get(ws)) { 21 | alive.delete(ws); 22 | return ws.terminate(); 23 | } 24 | 25 | alive.set(ws, false); 26 | ws.send('ping'); 27 | } 28 | }, 29 | PING_INTERVAL 30 | ); 31 | 32 | wss.on('connection', ws => { 33 | alive.set(ws, true); 34 | ws.on('message', onmessage); 35 | ws.on('close', onclose); 36 | }); 37 | 38 | wss.on('close', () => { 39 | clearInterval(timer); 40 | }); 41 | 42 | // the rest of this scope is proxied-worker server code 43 | 44 | const instances = new WeakMap; 45 | let uid = 0; 46 | 47 | async function loopThrough(_, $, list) { 48 | const action = list.shift(); 49 | let {length} = list; 50 | 51 | if (action !== GET) 52 | length--; 53 | if (action === APPLY) 54 | length--; 55 | 56 | for (let i = 0; i < length; i++) 57 | $ = await $[list[i]]; 58 | 59 | if (action === NEW) { 60 | const instance = new $(...list.pop().map(args, _)); 61 | instances.get(this).set($ = String(uid++), instance); 62 | } 63 | else if (action === APPLY) { 64 | $ = await $[list[length]](...list.pop().map(args, _)); 65 | } 66 | 67 | return $; 68 | } 69 | 70 | async function onmessage(data) { 71 | const message = String(data); 72 | if (message === 'ping') { 73 | alive.set(this, true); 74 | return; 75 | } 76 | try { 77 | const {id, list} = parse(message); 78 | if (!/^proxied-worker:([^:]*?):-?\d+$/.test(id)) 79 | return; 80 | 81 | const instance = RegExp.$1; 82 | const bus = this; 83 | 84 | if (!instances.has(this)) 85 | instances.set(this, new Map); 86 | 87 | let result, error; 88 | if (instance.length) { 89 | const ref = instances.get(this); 90 | if (list.length) { 91 | try { 92 | result = await loopThrough.call(this, bus, ref.get(instance), list); 93 | } 94 | catch ({message}) { 95 | error = message; 96 | } 97 | } 98 | else { 99 | ref.delete(instance); 100 | return; 101 | } 102 | } 103 | else { 104 | try { 105 | result = await loopThrough.call(this, bus, Namespace, list); 106 | } 107 | catch ({message}) { 108 | error = message; 109 | } 110 | } 111 | 112 | bus.send(stringify({id, result, error})); 113 | } 114 | catch (o_O) {} 115 | } 116 | 117 | const relatedCallbacks = new WeakMap; 118 | function args(id) { 119 | if (typeof id === 'string') { 120 | if (/^proxied-worker:cb:/.test(id)) { 121 | if (!relatedCallbacks.has(this)) 122 | relatedCallbacks.set(this, new Map); 123 | 124 | const cbs = relatedCallbacks.get(this); 125 | if (!cbs.has(id)) 126 | cbs.set(id, (...args) => { this.send(stringify({id, args})); }); 127 | return cbs.get(id); 128 | } 129 | return id.slice(1); 130 | } 131 | return id; 132 | } 133 | 134 | function onclose() { 135 | alive.delete(this); 136 | } 137 | }; 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxied-node", 3 | "version": "0.2.0", 4 | "description": "A proxied-worker for NodeJS", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run test", 8 | "cjs": "ascjs --no-default esm cjs", 9 | "test": "echo 'http://localhost:8080/'; node test/express.js" 10 | }, 11 | "keywords": [ 12 | "proxy", 13 | "nodejs", 14 | "worker", 15 | "electroff", 16 | "proxied-worker" 17 | ], 18 | "author": "Andrea Giammarchi", 19 | "license": "ISC", 20 | "devDependencies": { 21 | "ascjs": "^5.0.1", 22 | "express": "^4.17.1" 23 | }, 24 | "module": "./esm/index.js", 25 | "type": "module", 26 | "exports": { 27 | ".": { 28 | "import": "./esm/index.js", 29 | "default": "./cjs/index.js" 30 | }, 31 | "./package.json": "./package.json" 32 | }, 33 | "dependencies": { 34 | "@ungap/structured-clone": "^0.3.3", 35 | "any-ipv4": "^0.1.1", 36 | "umeta": "^0.2.4", 37 | "ws": "^8.2.3" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/WebReflection/proxied-node.git" 42 | }, 43 | "bugs": { 44 | "url": "https://github.com/WebReflection/proxied-node/issues" 45 | }, 46 | "homepage": "https://github.com/WebReflection/proxied-node#readme" 47 | } 48 | -------------------------------------------------------------------------------- /test/express.js: -------------------------------------------------------------------------------- 1 | const {PORT = 8080} = process.env; 2 | 3 | const express = require('express'); 4 | 5 | const handler = require('./test.js'); 6 | 7 | const app = express(); 8 | app.use(handler); 9 | app.use(express.static(__dirname)); 10 | app.listen(PORT); 11 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | proxied-node 8 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const {createServer} = require('http'); 2 | const {readFileSync} = require('fs'); 3 | const {join} = require('path'); 4 | 5 | const handler = require('./test.js'); 6 | 7 | const index = readFileSync(join(__dirname, 'index.html')); 8 | const headers = {'Content-Type': 'text/html;charset=utf-8'}; 9 | 10 | createServer((request, response) => { 11 | if (handler(request, response)) 12 | return; 13 | response.writeHead(200, headers); 14 | response.end(index); 15 | }).listen(8080); 16 | -------------------------------------------------------------------------------- /test/oled.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | curl -LO https://webreflection.github.io/proxied-node/test/oled/index.html 4 | curl -LO https://webreflection.github.io/proxied-node/test/oled/index.js 5 | curl -LO https://webreflection.github.io/proxied-node/test/oled/namespace.js 6 | curl -LO https://webreflection.github.io/proxied-node/test/oled/package.json 7 | curl -LO https://webreflection.github.io/proxied-node/test/oled/README.md 8 | -------------------------------------------------------------------------------- /test/oled/README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Oled Demo 2 | 3 | Install the _pigpio_ first, and **reboot** the board: 4 | 5 | * in ArchLinux via [pigpio](https://aur.archlinux.org/packages/pigpio/) - Please note this requires a dedicated raspberrypi linux kernel 6 | * in Debian or Raspbian via `sudo apt install pigpio` 7 | 8 | Create a folder to put files via `mkdir -p oled` and enter such folder via `cd oled`. 9 | 10 | Download all files of this folder, or use the downloader: 11 | 12 | ```sh 13 | bash <(curl -s https://webreflection.github.io/proxied-node/oled.sh)> 14 | ``` 15 | 16 | Install modules via `npm i`. 17 | 18 | Start the project via `sudo node index.js`, connect via any browser to `http://${IP_ADDRESS}:3000/` and write something on the screen 🥳 19 | -------------------------------------------------------------------------------- /test/oled/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oled Update 5 | 6 | 7 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/oled/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {PORT = 3000} = process.env; 4 | 5 | const IPv4 = require('any-ipv4'); 6 | const express = require('express'); 7 | const proxy = require('proxied-node'); 8 | 9 | const app = express(); 10 | app.use(proxy({ 11 | wss: {port: PORT + 1}, 12 | namespace: require('./namespace.js') 13 | })); 14 | app.use(express.static(__dirname)); 15 | app.listen(PORT, () => console.log(`http://${IPv4}:${PORT}`)); 16 | -------------------------------------------------------------------------------- /test/oled/namespace.js: -------------------------------------------------------------------------------- 1 | const five = require('johnny-five'); 2 | const {RaspiIO} = require('raspi-io'); 3 | 4 | const font = require('oled-font-5x7'); 5 | const Oled = require('oled-js'); 6 | 7 | const {ceil, pow} = Math; 8 | const options = { 9 | width: 128, 10 | height: 32, 11 | address: 0x3c 12 | }; 13 | 14 | const board = new five.Board({io: new RaspiIO}); 15 | const ready = new Promise($ => { 16 | board.on('ready', () => { 17 | $(new Oled(board, five, options)); 18 | }); 19 | }); 20 | 21 | module.exports = { 22 | show: async (text, scale = 2, h = 7) => { 23 | return ready.then(oled => { 24 | oled.clearDisplay(); 25 | oled.setCursor(1, ceil((options.height - h) / pow(2, scale))); 26 | oled.writeString(font, scale, text, 1, true, 2); 27 | oled.update(); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /test/oled/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxied-node-oled", 3 | "private": true, 4 | "scripts": { 5 | "start": "sudo node index.js" 6 | }, 7 | "dependencies": { 8 | "any-ipv4": "latest", 9 | "proxied-node": "latest", 10 | "express": "latest", 11 | "johnny-five": "latest", 12 | "oled-font-5x7": "latest", 13 | "oled-js": "latest", 14 | "raspi-io": "latest" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/sqlite/README.md: -------------------------------------------------------------------------------- 1 | # SQLite Demo 2 | 3 | A way to run a backend persistent sqlite istance shared across all connected people. 4 | 5 | ```sh 6 | npm i 7 | npm start 8 | ``` 9 | -------------------------------------------------------------------------------- /test/sqlite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oled Update 5 | 6 | 7 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/sqlite/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {PORT = 3000} = process.env; 4 | 5 | const IPv4 = require('any-ipv4'); 6 | const express = require('express'); 7 | const proxy = require('proxied-node'); 8 | 9 | const app = express(); 10 | app.use(proxy({ 11 | wss: {port: PORT + 1}, 12 | namespace: require('./namespace.js') 13 | })); 14 | app.use(express.static(__dirname)); 15 | app.listen(PORT, () => console.log(`http://${IPv4}:${PORT}`)); 16 | -------------------------------------------------------------------------------- /test/sqlite/namespace.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose(); 2 | const SQLiteTag = require('sqlite-tag'); 3 | 4 | const db = new sqlite3.Database(':memory:'); 5 | 6 | module.exports = { 7 | tags: SQLiteTag(db) 8 | }; 9 | -------------------------------------------------------------------------------- /test/sqlite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxied-node-sqlite", 3 | "private": true, 4 | "scripts": { 5 | "start": "node index.js" 6 | }, 7 | "dependencies": { 8 | "any-ipv4": "latest", 9 | "proxied-node": "latest", 10 | "sqlite3": "latest", 11 | "sqlite-tag": "latest", 12 | "express": "latest" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const proxiedNode = require('../cjs'); 2 | 3 | module.exports = proxiedNode({ 4 | wss: {port: 5000}, 5 | namespace: { 6 | test: 'OK', 7 | exit() { 8 | console.log('bye bye'); 9 | process.exit(0); 10 | }, 11 | sum(a, b) { 12 | return a + b; 13 | }, 14 | on(type, callback) { 15 | setTimeout(() => { 16 | callback('Event', type); 17 | }); 18 | }, 19 | async delayed() { 20 | console.log('context', this.test); 21 | // postMessage({action: 'greetings'}); 22 | return await new Promise($ => setTimeout($, 500, Math.random())); 23 | }, 24 | Class: class { 25 | constructor(name) { 26 | this.name = name; 27 | } 28 | sum(a, b) { 29 | console.log(this.name, a, b); 30 | return a + b; 31 | } 32 | } 33 | } 34 | }); 35 | --------------------------------------------------------------------------------