{"id":20513965,"url":"https://github.com/webreflection/electroff","last_synced_at":"2025-04-14T00:04:10.381Z","repository":{"id":57221496,"uuid":"278042258","full_name":"WebReflection/electroff","owner":"WebReflection","description":"A cross browser, electron-less helper, for IoT projects and standalone applications.","archived":false,"fork":false,"pushed_at":"2023-07-11T13:58:23.000Z","size":346,"stargazers_count":69,"open_issues_count":0,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-12-31T15:16:10.006Z","etag":null,"topics":["electron","iot","nodejs"],"latest_commit_sha":null,"homepage":"https://medium.com/@WebReflection/electroff-an-electron-less-alternative-10920c6003b8","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/WebReflection.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2020-07-08T09:07:14.000Z","updated_at":"2024-06-12T21:34:38.000Z","dependencies_parsed_at":"2024-03-03T00:32:07.326Z","dependency_job_id":"e2f3eb48-b457-41d3-bae3-89cc93217c26","html_url":"https://github.com/WebReflection/electroff","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Felectroff","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Felectroff/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Felectroff/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Felectroff/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/WebReflection","download_url":"https://codeload.github.com/WebReflection/electroff/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":233824434,"owners_count":18736002,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["electron","iot","nodejs"],"created_at":"2024-11-15T21:14:06.250Z","updated_at":"2025-01-13T23:56:17.617Z","avatar_url":"https://github.com/WebReflection.png","language":"JavaScript","readme":"# electroff\n\n![Raspberry Pi Oled](./electroff-head.jpg)\n\nA cross browser, electron-less helper, for **IoT** projects and **standalone** applications.\n\nWith this module, you can run arbitrary _Node.js_ code from the client, from any browser, and *without* needing [electron](https://www.electronjs.org/).\n\n\n## 📣 Announcement\n\nelectroff can be fully replaced by [coincident/server](https://github.com/WebReflection/coincident-oled#readme)!\n\n- - -\n\nLooking for a lighter, faster, much safer, yet slightly more limited alternative? Try **[proxied-node](https://github.com/WebReflection/proxied-node#readme)** out instead.\n\n\n### Community\n\nPlease ask questions in the [dedicated forum](https://webreflection.boards.net/) to help the community around this project grow ♥\n\n---\n\n## Getting Started\n\nConsidering the following `app.js` file content:\n\n```js\nconst {PORT = 3000} = process.env;\n\nconst express = require('express');\nconst electroff = require('electroff');\n\nconst app = express();\napp.use(electroff);\napp.use(express.static(`${__dirname}/public`));\napp.listen(PORT, () =\u003e console.log(`http://localhost:${PORT}`));\n```\n\nThe `public/index.html` folder can contain something like this:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"\u003e\n  \u003ctitle\u003eElectroff Basic Test\u003c/title\u003e\n  \u003cscript type=\"module\"\u003e\n  // use \u003cscript src=\"electroff\"\u003e instead for a global utility\n  import CommonJS from '/electroff?module';\n\n  // pass an asynchronous callback to electroff\n  // it will be invoked instantly with few helpers\n  CommonJS(async ({require}) =\u003e {\n    const {arch, cpus, freemem} = require('os');\n    document.body.innerHTML = `\n      \u003ch1\u003e${document.title}\u003c/h1\u003e\n      \u003ch2\u003e CPUs: ${await cpus().length} \u003c/h2\u003e\n      \u003ch2\u003e Architecture: ${await arch()} \u003c/h2\u003e\n      \u003ch2\u003e Free memory: ${await freemem()} \u003c/h2\u003e\n    `;\n  });\n  \u003c/script\u003e\n\u003c/head\u003e\n\u003c/html\u003e\n```\n\n\n### Helpers passed as callback object / API\n\n  * `require`, usable to require any module or file, same as you would do in _CommonJS_\n  * `global`, usable to share a mirrored state across multiple `electroff(...)` calls \u003csup\u003e\u003csub\u003e(but not shared across multiple clients)\u003c/sub\u003e\u003c/sup\u003e\n  * `remove`, usable to remove instances when these are not available anymore \u003csup\u003e\u003csub\u003e(a _WeakRef_ implementation is coming soon)\u003c/sub\u003e\u003c/sup\u003e\n  * `__dirname`, which points at the _Node.js_ path that is currently running the module\n  * `until`, usable to `await` emitters events on the client-side \u003csup\u003e\u003csub\u003e(read more in F.A.Q.)\u003c/sub\u003e\u003c/sup\u003e\n\n\n\n## F.A.Q.\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eHow does it work?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nThe _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.\n\nSuch *sandbox* is in charge of executing code from the client, but only when the client *await* some value.\n\n```js\nconst {debug} = require('process').features;\nconsole.log('debug is', await debug);\n\nconst {join} = require('path');\nconst {readFile} = require('fs').promises;\nconst content = await readFile(join(__dirname, 'public', 'index.html'));\nconsole.log(content);\n```\n\n**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.\n\nIn few words, the following code:\n```js\nawait require('fs').promises.readFile('file.txt');\n```\n\nwould evaluated, within the _vm_ sandbox, the following code:\n```js\nawait require(\"fs\").promises.readFile.apply(\n  require(\"fs\").promises,\n  [\"test.txt\"]\n)\n```\n\nAll operations are inevitably repeated because every single `.property` access, `.method(...)` invoke, or even `new module.Thing(...)`, is a branch of the code a part.\n\n### The foreign vs local scope\n\nIt 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.\n```js\nelectroff(async ({require}) =\u003e {\n  // local scope code\n  const num = Math.random();\n\n  // foreign code (needs to be awaited)\n  const {EventEmitter} = require('events');\n  const ee = await new EventEmitter;\n  await ee.on('stuff', async function (value) {\n    // nothing in this scope can reach\n    // `num`, as example, is not accessible\n    // and neither is `ee` ... but `this` works fine\n    console.log(this);\n    // this log will be on the Node.js site, it won't log\n    // anything on the browser\n    console.log('stuff', value);\n  });\n\n  // DOM listeners should be async if these need to signal\n  // or interact with the foreign code because ...\n  someButtom.addEventListener('click', async () =\u003e {\n    // ... foreign code always need to be awaited!\n    await ee.emit('stuff', 123);\n  });\n});\n```\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eIs it safe?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nTheoretically, 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](https://nodejs.org/api/vm.html) and always the [same context](https://nodejs.org/api/vm.html#vm_script_runincontext_contextifiedobject_options) 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.\n\nIf the `ELECTROFF_ONCE=1` environment variable is present, *electroff* will increase security in the following way:\n\n  * a client can use *electroff* only via `import electroff from '/electroff?module'`, and any attempt to retrieve the electroff script in a different way will fail\n  * previous point ensures that the module can be executed *only once*, so there's one single room/window in the page to define its behavior, anot nothing else can interfeer with the server side *vm*\n  * using *CSP* would also work so that only known code on the page can safely run, and there's no `eval` nor `Function` call in here, so that nothing else can be injected\n\nRegardless 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.\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eAre Node.js instances possible?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nYes, but there are at least two things to keep in mind:\n\n  * any _Node.js_ instance *should* be _awaited_ on creation, i.e.: `const instance = await new require('events').EventEmitter;`, unless we're waiting for a specific listener, in which case it's better to await `until(thing).is('ready')` (see next F.A.Q.)\n  * there is currently no way to automatically free the _vm_ from previously created instances, if not by explicitly using `remove(instance)`\n\nLast 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.\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eHow to react to/until Node.js events?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nThe `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.\n\nFollowing an example of how this could work in practice.\n\n```js\nCommonJS(async ({require, until}) =\u003e {\n  const five = require('johnny-five');\n\n  // no need to await here, or ready could\n  // be fired before the next request is performed\n  const board = new five.Board();\n\n  // simply await everything at once in here\n  await until(board).is('ready');\n\n  // now all board dependent instances can be awaited\n  const led = await new five.Led(13);\n  // so that it's possible to await each method/invoke/property\n  await led.blink(500);\n\n  document.body.textContent = `it's blinking!`;\n});\n```\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eAny best practice?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nAt this early stage, I can recommend only few best-practices I've noticed while playing around with this module:\n\n  * don't _overdo_ server side instances/references, try to reach *only* the utilities you need the most, instead of creating everything on the _vm_ side\n  * when a server side reference *method* is invoked, you *must await* it, i.e. `await emitter.setMaxListeners(20)`. This grants next time you `await emitter.getMaxListeners()` you'll receive the answer you expect\n  * template literals are passed as plain arrays. If your library optimizes on template literals uniqueness, it will always re-parse/re-do any dance, because the array on the server side will be always a different one. Create a file that queries the DB, and simply `require(\"./db-helper\")` instead of writing all SQL queries on the client side, and use _Node.js_ regular helpers/files whenever it works\n  * try to keep `global` references to a minimum amount, as the back and forward dance is quite expensive, and most of the time you won't need it\n  * if any needed instance has an emit once ready, `const instance = new Thing; await until(instance).is('ready')` instead of `const instance = await new Thing; await instance.once('ready', doThing)`, so you ensure your instance is ready within the client side scope, instead of needing a foreign callback that cannot reach such scope\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eWhat about performance?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nThe _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.\n\nThis won't exactly be high performance but, for what I could try, performance is *good enough*, for most _IoT_ or standalone application.\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eWhat kind of data can be exchanged?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nAny *JSON* serializable data, with the nice touch that [flatted](https://github.com/WebReflection/flatted#readme) gives to responses objects, where even circular references can be returned to the client.\n\n**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.\n\n**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.\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eHow is this different from electron?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\n_electron_ is an awesome project, and I love it with all my heart ♥\n\nHowever, it has its own caveats:\n\n  * _electron_ itself is a huge dependency, and there are multiple versions, where different apps might use/need different versions, so its size is exponential, and it doesn't play too well with the fast pace _Node.js_ and its modules ecosystem get updated\n  * _electron_ uses modules that are not the same one used in _Node.js_. If we update a module in the system, _electron_ might still use its own version of such module\n  * _electron_ doesn't work cross browser, because it brings its own browser itself. This is both great, for application reliability across platforms, and bad, for platforms where there is already a better browser, and all it's missing is the ability to seamlessly interact with the system version of _Node.js_. As example, the best browser for _IoT_ devices is [WPE WebKit](https://wpewebkit.org/), and not _Chrome/ium_, because _WPE WebKit_ offers Hardware Acceleration, with a minimal footprint, and great performance for embedded solutions\n  * _electron_ cannot serve multiple clients, as each client would need an instance of the same _electron_ app. This module provides the ability, for any reasonably modern browser, to perform _Node.js_ operations through the Web, meaning that you don't need anyone to install _electron_, as everything is already working/available through this module to the masses\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eIs this ready for production?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nThis module is currently in its early development stage, and there are at least two main concerns regarding it:\n\n  * the `remove(...)` utility requires user-land care, 'cause if it's not performed, the _vm_ behind the scene could retain in RAM references \"_forever_\", or at least up to the time the associated _UID_ to each client gets purged (once every 5 minutes)\n  * the purge mechanism is based on requests: no requests whatsoever in 5 minutes, nothing gets purged\n\nThis 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.\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eWhich browser is compatible?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nAll evergreen browsers should work just fine, but these are the requirements for this module to work on the client:\n\n  * `async/await` [native capability](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function#Browser_compatibility) \n  * `fetch` [native API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility)\n  * `navigator.sendBeacon` [native method](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#Browser_compatibility)\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eHow to debug?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nIf 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.\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\n## Examples\n\n  * [Raspberri Pi + oled](./examples/oled/README.md), write on the RPi oled screen from any browser\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebreflection%2Felectroff","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwebreflection%2Felectroff","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebreflection%2Felectroff/lists"}