Home

Awesome

electroff

Raspberry Pi Oled

A cross browser, electron-less helper, for IoT projects and standalone applications.

With this module, you can run arbitrary Node.js code from the client, from any browser, and without needing electron.

📣 Announcement

electroff can be fully replaced by coincident/server!


Looking for a lighter, faster, much safer, yet slightly more limited alternative? Try proxied-node out instead.

Community

Please ask questions in the dedicated forum to help the community around this project grow ♥


Getting Started

Considering the following app.js file content:

const {PORT = 3000} = process.env;

const express = require('express');
const electroff = require('electroff');

const app = express();
app.use(electroff);
app.use(express.static(`${__dirname}/public`));
app.listen(PORT, () => console.log(`http://localhost:${PORT}`));

The public/index.html folder can contain something like this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>Electroff Basic Test</title>
  <script type="module">
  // use <script src="electroff"> instead for a global utility
  import CommonJS from '/electroff?module';

  // pass an asynchronous callback to electroff
  // it will be invoked instantly with few helpers
  CommonJS(async ({require}) => {
    const {arch, cpus, freemem} = require('os');
    document.body.innerHTML = `
      <h1>${document.title}</h1>
      <h2> CPUs: ${await cpus().length} </h2>
      <h2> Architecture: ${await arch()} </h2>
      <h2> Free memory: ${await freemem()} </h2>
    `;
  });
  </script>
</head>
</html>

Helpers passed as callback object / API

F.A.Q.

<details> <summary><strong>How does it work?</strong></summary> <div>

The JS on the page is exactly like any regular JS, but anything referencing Node.js environment, through any require(...), is executed on a shared sandbox in Node.js, where each user gets its own global namespace a part.

Such sandbox is in charge of executing code from the client, but only when the client await some value.

const {debug} = require('process').features;
console.log('debug is', await debug);

const {join} = require('path');
const {readFile} = require('fs').promises;
const content = await readFile(join(__dirname, 'public', 'index.html'));
console.log(content);

In depth: every time we await something in JS, an implicit lookup for the .then(...) method is performed, and that's when electroff can perform a fancy client/server asynchronous interaction, through all the paths reached through the various references, which are nothing more than Proxies with special abilities.

In few words, the following code:

await require('fs').promises.readFile('file.txt');

would evaluated, within the vm sandbox, the following code:

await require("fs").promises.readFile.apply(
  require("fs").promises,
  ["test.txt"]
)

All operations are inevitably repeated because every single .property access, .method(...) invoke, or even new module.Thing(...), is a branch of the code a part.

The foreign vs local scope

It is important to keep in mind that there is a huge difference between foreign code, and scoped code, where foreign code cannot reach scoped code, and vive-versa.

electroff(async ({require}) => {
  // local scope code
  const num = Math.random();

  // foreign code (needs to be awaited)
  const {EventEmitter} = require('events');
  const ee = await new EventEmitter;
  await ee.on('stuff', async function (value) {
    // nothing in this scope can reach
    // `num`, as example, is not accessible
    // and neither is `ee` ... but `this` works fine
    console.log(this);
    // this log will be on the Node.js site, it won't log
    // anything on the browser
    console.log('stuff', value);
  });

  // DOM listeners should be async if these need to signal
  // or interact with the foreign code because ...
  someButtom.addEventListener('click', async () => {
    // ... foreign code always need to be awaited!
    await ee.emit('stuff', 123);
  });
});
</div> </details> <details> <summary><strong>Is it safe?</strong></summary> <div>

Theoretically, this is either "as safe as", or "as unsafe as", electron can be, but technically, the whole idea behind is based on client side code evaluation through a shared vm and always the same context per each client, although ensuring a "share nothing" global object per each context, so that multiple clients, with multiple instances/invokes, won't interfere with each other, given the same script on the page.

If the ELECTROFF_ONCE=1 environment variable is present, electroff will increase security in the following way:

Regardless of the ELECTROFF_ONCE=1 security guard though, please bear in mind that even if the whole communication channel is somehow based on very hard to guess unique random IDs per client, this project/module is not suitable for websites, but it can be used in any IoT related project, kiosk, or standalone applications, where we are sure there is no malicious code running arbitrary JS on our machines, which is not always the case for online Web pages.

</div> </details> <details> <summary><strong>Are Node.js instances possible?</strong></summary> <div>

Yes, but there are at least two things to keep in mind:

Last point means the vm memory related to any client would be freed only once the client refreshes the page, or closes the tab, but there's the possibility that the client crashes or has no network all of a sudden, and in such case the vm will trash any reference automatically, in about 5 minutes or more.

</div> </details> <details> <summary><strong>How to react to/until Node.js events?</strong></summary> <div>

The until utility keeps the POST request hanging until the observed event is triggered once. It pollutes the emitter, if not polluted already, with an is(eventName) that returns a promise resolved once the event name happens.

Following an example of how this could work in practice.

CommonJS(async ({require, until}) => {
  const five = require('johnny-five');

  // no need to await here, or ready could
  // be fired before the next request is performed
  const board = new five.Board();

  // simply await everything at once in here
  await until(board).is('ready');

  // now all board dependent instances can be awaited
  const led = await new five.Led(13);
  // so that it's possible to await each method/invoke/property
  await led.blink(500);

  document.body.textContent = `it's blinking!`;
});
</div> </details> <details> <summary><strong>Any best practice?</strong></summary> <div>

At this early stage, I can recommend only few best-practices I've noticed while playing around with this module:

</div> </details> <details> <summary><strong>What about performance?</strong></summary> <div>

The JS that runs on the browsers is as fast as it can get, but every Node.js handled setter, getter, or method invoke, will pass through a POST request, with some vm evaluation, recursive-capable serving and parsing, and eventually a result on the client.

This won't exactly be high performance but, for what I could try, performance is good enough, for most IoT or standalone application.

</div> </details> <details> <summary><strong>What kind of data can be exchanged?</strong></summary> <div>

Any JSON serializable data, with the nice touch that flatted gives to responses objects, where even circular references can be returned to the client.

However, you cannot send circular references to the server, but you can send callbacks that will be passed along as string to evaluate, meaning any surrounding closure variable won't be accessible once on the server so ... be careful when passing callbacks around.

On Node.js side though, be sure you use promisify or any already promisified version of its API, as utilities with callbacks can't be awaited, hence will likely throw errors, unless these are needed to operate exclusively on the Node.js side.

</div> </details> <details> <summary><strong>How is this different from electron?</strong></summary> <div>

electron is an awesome project, and I love it with all my heart ♥

However, it has its own caveats:

</div> </details> <details> <summary><strong>Is this ready for production?</strong></summary> <div>

This module is currently in its early development stage, and there are at least two main concerns regarding it:

This means we can use this project in IoT or standalone projects, as long as its constrains are clear, and user being redirected to a fake 404 page that requires them to reload is acceptable.

</div> </details> <details> <summary><strong>Which browser is compatible?</strong></summary> <div>

All evergreen browsers should work just fine, but these are the requirements for this module to work on the client:

</div> </details> <details> <summary><strong>How to debug?</strong></summary> <div>

If there is a DEBUG=1 or a DEBUG=true environment variable, a lot of helpful details are logged in the terminal, either via console.log, or via console.error, when something has been caught as an error.

</div> </details>

Examples