-
Notifications
You must be signed in to change notification settings - Fork 136
Description
Hello everyone,
I was thrilled to see the RTC polyfill working (❤) —it meant we could finally have an always-on node. 🙏
But then reality hit: if it's a browser app, the whole architecture has to change. IndexedDB doesn’t work in Node, so I had to switch to something like Level. A few other things broke too, which meant a full refactor.
But then I had an idea: What if we could use our existing Trystero-based browser apps as-is for always-on nodes?
Two solutions came up:
-
Wrap it in Electron. (can run on a server, but mostly for running our app as desktop app/relay)
-
Run it via Puppeteer—a headless Chromium browser on a Node server.
Honestly, option two feels like the real winner.
Here’s a minimal setup to run your browser app as an always-on node using Puppeteer. Just follow these simple steps:
Take the contents of your build folder (the output of your bundler, e.g., Rollup, Vite, Svelte, etc.).
Add the puppeteer-runner.js script and package.json (example below) into that same folder. (where your index.html is, usually top folder)
Run the script with Node.
That’s it—your app will run in a headless Chromium browser, acting as a persistent node without changing any of your existing browser code.
PS: I let the server restart all 5min to ensure its available. Should it crash, stuck or whatever inside that 5min, it also restarts. You can adjust this to whatever your scenario needs. 🙏
puppeteer-runner.js
import puppeteer from 'puppeteer';
import { createServer } from 'http';
import { readFile } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const serveDist = async () => {
const server = createServer(async (req, res) => {
const filePath = req.url === '/' ? '/index.html' : req.url;
const mimeTypes = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.wasm': 'application/wasm',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml',
};
try {
const fullPath = path.join(__dirname, filePath);
const file = await readFile(fullPath);
const ext = path.extname(filePath);
const contentType = mimeTypes[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType });
res.end(file);
} catch {
res.writeHead(404);
res.end('Not found');
}
});
return new Promise((resolve) => {
server.listen(3000, () => {
console.log('🚀 Local server running at http://localhost:3000');
resolve(server);
});
});
};
const launchBrowser = async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
'--enable-webgl',
'--ignore-certificate-errors',
'--allow-insecure-localhost',
'--enable-features=WebRTC-HW-Decoding,WebRTC-HW-Encoding',
],
defaultViewport: null,
userDataDir: path.join(__dirname, 'puppeteer-profile'),
});
const page = await browser.newPage();
page.on('console', msg => console.log('[PAGE]', msg.text()));
page.on('error', err => console.error('[PAGE ERROR]', err));
page.on('pageerror', err => console.error('[PAGE EXCEPTION]', err));
await page.goto('http://localhost:3000', { waitUntil: 'networkidle0' });
console.log('✅ App loaded in headless browser');
return { browser };
};
const runLoop = async () => {
await serveDist();
while (true) {
let browser = null;
try {
console.log('🔁 Starting new Puppeteer session...');
const result = await launchBrowser();
browser = result.browser;
// Wait for 5 minutes or until crash
await new Promise((resolve, reject) => {
const timeout = setTimeout(resolve, 5 * 60 * 1000);
browser.on('disconnected', () => {
clearTimeout(timeout);
reject(new Error('Browser disconnected unexpectedly'));
});
});
console.log('🧹 5 minutes passed, closing browser...');
} catch (err) {
console.error('💥 Error during session:', err.message);
} finally {
if (browser && browser.isConnected()) {
try {
await browser.close();
} catch (err) {
console.warn('⚠️ Error closing browser:', err.message);
}
}
console.log('🔄 Restarting session...');
}
}
};
runLoop();
package.json
{
"name": "your-apps-name",
"version": "1.0.0",
"description": "Your Trystero Browser App on Node Server",
"type": "module",
"main": "puppeteer-runner.js",
"scripts": {
"start": "node puppeteer-runner.js"
},
"dependencies": {
"puppeteer": "^24.7.2"
}
}