├── .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 |
--------------------------------------------------------------------------------