···11+# v2-pubsub-mini
22+33+Minimal standalone Electron reproducer for the v2-tile pubsub routing
44+pattern Peek uses. Two `BrowserWindow`s share a sandboxed
55+`contextIsolation: true` preload that exposes `app.publish` /
66+`app.subscribe` over `tile:pubsub:*` IPC channels. Main process records
77+subs and broadcasts publishes to other tile windows.
88+99+## Run
1010+1111+```
1212+cd /tmp/v2-pubsub-mini
1313+/Users/dietrich/misc/mpeek/node_modules/.bin/electron .
1414+```
1515+1616+The app:
1717+1. Creates two BrowserWindows (subscriber + publisher), both with
1818+ `sandbox: true, contextIsolation: true`, sharing `preload.cjs`.
1919+2. After both load, waits 250ms, then asks the publisher window to
2020+ `window.app.publish('test-topic', {...})` via `executeJavaScript`.
2121+3. Polls the subscriber window's `window.__received` for up to 1s.
2222+4. Logs `VERDICT: STANDALONE_WORKS` or `STANDALONE_FAILS`.
2323+2424+Logs go to stdout AND to `/tmp/v2-pubsub-mini.log`.
2525+2626+## Files
2727+2828+- `main.js` — IPC handlers, window creation, auto-test runner
2929+- `preload.cjs` — `contextBridge.exposeInMainWorld('app', {publish, subscribe, rlog})`
3030+- `publisher.html` — exposes a click button + auto-test target
3131+- `subscriber.html` — auto-subscribes on load, stores received data on `window.__received`
+140
scratch/v2-pubsub-mini/main.js
···11+// Standalone Electron reproducer for v2 tile pubsub pattern.
22+// Two BrowserWindows, sandboxed + contextIsolated, share a CJS preload.
33+// Main process records subscriptions and broadcasts publishes to other windows.
44+55+const { app, BrowserWindow, ipcMain } = require('electron');
66+const path = require('path');
77+const fs = require('fs');
88+99+const LOG_PATH = '/tmp/v2-pubsub-mini.log';
1010+fs.writeFileSync(LOG_PATH, '');
1111+1212+function log(...args) {
1313+ const line = `[main ${new Date().toISOString()}] ${args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')}`;
1414+ console.error(line);
1515+ try { fs.appendFileSync(LOG_PATH, line + '\n'); } catch {}
1616+}
1717+1818+log('main starting');
1919+2020+// topic -> Set<webContentsId>
2121+const subs = new Map();
2222+// Track windows by id for broadcasting
2323+const tileWindows = new Set();
2424+2525+function recordSub(senderId, topic) {
2626+ if (!subs.has(topic)) subs.set(topic, new Set());
2727+ subs.get(topic).add(senderId);
2828+ log('recorded subscription', { senderId, topic, current: [...subs.get(topic)] });
2929+}
3030+3131+function broadcast(topic, data, exceptId) {
3232+ const recipients = subs.get(topic);
3333+ log('broadcast', { topic, data, exceptId, recipients: recipients ? [...recipients] : null });
3434+ if (!recipients) return 0;
3535+ let n = 0;
3636+ for (const win of tileWindows) {
3737+ if (win.isDestroyed()) continue;
3838+ const wcid = win.webContents.id;
3939+ if (wcid === exceptId) continue;
4040+ if (!recipients.has(wcid)) continue;
4141+ log(' -> sending pubsub:message to wcid', wcid);
4242+ win.webContents.send('pubsub:message', { topic, data });
4343+ n++;
4444+ }
4545+ return n;
4646+}
4747+4848+// IPC handlers (registered BEFORE creating windows)
4949+ipcMain.on('tile:pubsub:subscribe', (evt, { topic }) => {
5050+ log('IPC tile:pubsub:subscribe', { topic, sender: evt.sender.id });
5151+ recordSub(evt.sender.id, topic);
5252+});
5353+5454+ipcMain.on('tile:pubsub:unsubscribe', (evt, { topic }) => {
5555+ log('IPC tile:pubsub:unsubscribe', { topic, sender: evt.sender.id });
5656+ subs.get(topic)?.delete(evt.sender.id);
5757+});
5858+5959+ipcMain.on('tile:pubsub:publish', (evt, { topic, data }) => {
6060+ log('IPC tile:pubsub:publish', { topic, data, sender: evt.sender.id });
6161+ const n = broadcast(topic, data, evt.sender.id);
6262+ log('broadcast complete', { delivered: n });
6363+});
6464+6565+ipcMain.on('mini:log', (evt, msg) => {
6666+ log(`[renderer wcid=${evt.sender.id}] ${msg}`);
6767+});
6868+6969+function createWindow(htmlPath, title) {
7070+ const win = new BrowserWindow({
7171+ width: 600,
7272+ height: 400,
7373+ title,
7474+ show: false, // headless-ish; we just need them loaded
7575+ webPreferences: {
7676+ sandbox: true,
7777+ contextIsolation: true,
7878+ preload: path.join(__dirname, 'preload.cjs'),
7979+ },
8080+ });
8181+ tileWindows.add(win);
8282+ win.on('closed', () => tileWindows.delete(win));
8383+ win.webContents.on('console-message', (_e, _level, message, line, source) => {
8484+ log(`[wc ${win.webContents.id} console] ${message}`);
8585+ });
8686+ win.webContents.on('did-finish-load', () => {
8787+ log(`window ${title} (wcid=${win.webContents.id}) finished loading`);
8888+ });
8989+ win.loadFile(htmlPath);
9090+ return win;
9191+}
9292+9393+app.whenReady().then(async () => {
9494+ log('app ready');
9595+9696+ const sub = createWindow(path.join(__dirname, 'subscriber.html'), 'subscriber');
9797+ const pub = createWindow(path.join(__dirname, 'publisher.html'), 'publisher');
9898+9999+ // Wait for both to finish loading
100100+ await Promise.all([
101101+ new Promise(r => sub.webContents.once('did-finish-load', r)),
102102+ new Promise(r => pub.webContents.once('did-finish-load', r)),
103103+ ]);
104104+ log('both windows loaded; subscriber=' + sub.webContents.id + ' publisher=' + pub.webContents.id);
105105+106106+ // Give subscriber a beat to register its subscription
107107+ await new Promise(r => setTimeout(r, 250));
108108+109109+ log('=== auto-test: triggering publish from publisher window ===');
110110+ // Ask publisher to publish via executeJavaScript (no user click needed)
111111+ const result = await pub.webContents.executeJavaScript(
112112+ "window.app.publish('test-topic', {hello: 'world', ts: Date.now()})"
113113+ );
114114+ log('publish call result from publisher renderer:', result);
115115+116116+ // Wait up to 1s for subscriber to receive
117117+ const start = Date.now();
118118+ let received = null;
119119+ while (Date.now() - start < 1000) {
120120+ received = await sub.webContents.executeJavaScript('window.__received || null');
121121+ if (received) break;
122122+ await new Promise(r => setTimeout(r, 50));
123123+ }
124124+125125+ if (received) {
126126+ log('=== ASSERTION PASS: subscriber received ===', received);
127127+ log('VERDICT: STANDALONE_WORKS');
128128+ } else {
129129+ log('=== ASSERTION FAIL: subscriber did NOT receive within 1s ===');
130130+ log('VERDICT: STANDALONE_FAILS');
131131+ }
132132+133133+ // Quit shortly after so we get clean stdout
134134+ setTimeout(() => {
135135+ log('quitting');
136136+ app.quit();
137137+ }, 250);
138138+});
139139+140140+app.on('window-all-closed', () => app.quit());