experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

scratch: bare-Electron v2 pubsub repro — STANDALONE_WORKS, bug is Peek-specific

+283
+31
scratch/v2-pubsub-mini/README.md
··· 1 + # v2-pubsub-mini 2 + 3 + Minimal standalone Electron reproducer for the v2-tile pubsub routing 4 + pattern Peek uses. Two `BrowserWindow`s share a sandboxed 5 + `contextIsolation: true` preload that exposes `app.publish` / 6 + `app.subscribe` over `tile:pubsub:*` IPC channels. Main process records 7 + subs and broadcasts publishes to other tile windows. 8 + 9 + ## Run 10 + 11 + ``` 12 + cd /tmp/v2-pubsub-mini 13 + /Users/dietrich/misc/mpeek/node_modules/.bin/electron . 14 + ``` 15 + 16 + The app: 17 + 1. Creates two BrowserWindows (subscriber + publisher), both with 18 + `sandbox: true, contextIsolation: true`, sharing `preload.cjs`. 19 + 2. After both load, waits 250ms, then asks the publisher window to 20 + `window.app.publish('test-topic', {...})` via `executeJavaScript`. 21 + 3. Polls the subscriber window's `window.__received` for up to 1s. 22 + 4. Logs `VERDICT: STANDALONE_WORKS` or `STANDALONE_FAILS`. 23 + 24 + Logs go to stdout AND to `/tmp/v2-pubsub-mini.log`. 25 + 26 + ## Files 27 + 28 + - `main.js` — IPC handlers, window creation, auto-test runner 29 + - `preload.cjs` — `contextBridge.exposeInMainWorld('app', {publish, subscribe, rlog})` 30 + - `publisher.html` — exposes a click button + auto-test target 31 + - `subscriber.html` — auto-subscribes on load, stores received data on `window.__received`
+140
scratch/v2-pubsub-mini/main.js
··· 1 + // Standalone Electron reproducer for v2 tile pubsub pattern. 2 + // Two BrowserWindows, sandboxed + contextIsolated, share a CJS preload. 3 + // Main process records subscriptions and broadcasts publishes to other windows. 4 + 5 + const { app, BrowserWindow, ipcMain } = require('electron'); 6 + const path = require('path'); 7 + const fs = require('fs'); 8 + 9 + const LOG_PATH = '/tmp/v2-pubsub-mini.log'; 10 + fs.writeFileSync(LOG_PATH, ''); 11 + 12 + function log(...args) { 13 + const line = `[main ${new Date().toISOString()}] ${args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')}`; 14 + console.error(line); 15 + try { fs.appendFileSync(LOG_PATH, line + '\n'); } catch {} 16 + } 17 + 18 + log('main starting'); 19 + 20 + // topic -> Set<webContentsId> 21 + const subs = new Map(); 22 + // Track windows by id for broadcasting 23 + const tileWindows = new Set(); 24 + 25 + function recordSub(senderId, topic) { 26 + if (!subs.has(topic)) subs.set(topic, new Set()); 27 + subs.get(topic).add(senderId); 28 + log('recorded subscription', { senderId, topic, current: [...subs.get(topic)] }); 29 + } 30 + 31 + function broadcast(topic, data, exceptId) { 32 + const recipients = subs.get(topic); 33 + log('broadcast', { topic, data, exceptId, recipients: recipients ? [...recipients] : null }); 34 + if (!recipients) return 0; 35 + let n = 0; 36 + for (const win of tileWindows) { 37 + if (win.isDestroyed()) continue; 38 + const wcid = win.webContents.id; 39 + if (wcid === exceptId) continue; 40 + if (!recipients.has(wcid)) continue; 41 + log(' -> sending pubsub:message to wcid', wcid); 42 + win.webContents.send('pubsub:message', { topic, data }); 43 + n++; 44 + } 45 + return n; 46 + } 47 + 48 + // IPC handlers (registered BEFORE creating windows) 49 + ipcMain.on('tile:pubsub:subscribe', (evt, { topic }) => { 50 + log('IPC tile:pubsub:subscribe', { topic, sender: evt.sender.id }); 51 + recordSub(evt.sender.id, topic); 52 + }); 53 + 54 + ipcMain.on('tile:pubsub:unsubscribe', (evt, { topic }) => { 55 + log('IPC tile:pubsub:unsubscribe', { topic, sender: evt.sender.id }); 56 + subs.get(topic)?.delete(evt.sender.id); 57 + }); 58 + 59 + ipcMain.on('tile:pubsub:publish', (evt, { topic, data }) => { 60 + log('IPC tile:pubsub:publish', { topic, data, sender: evt.sender.id }); 61 + const n = broadcast(topic, data, evt.sender.id); 62 + log('broadcast complete', { delivered: n }); 63 + }); 64 + 65 + ipcMain.on('mini:log', (evt, msg) => { 66 + log(`[renderer wcid=${evt.sender.id}] ${msg}`); 67 + }); 68 + 69 + function createWindow(htmlPath, title) { 70 + const win = new BrowserWindow({ 71 + width: 600, 72 + height: 400, 73 + title, 74 + show: false, // headless-ish; we just need them loaded 75 + webPreferences: { 76 + sandbox: true, 77 + contextIsolation: true, 78 + preload: path.join(__dirname, 'preload.cjs'), 79 + }, 80 + }); 81 + tileWindows.add(win); 82 + win.on('closed', () => tileWindows.delete(win)); 83 + win.webContents.on('console-message', (_e, _level, message, line, source) => { 84 + log(`[wc ${win.webContents.id} console] ${message}`); 85 + }); 86 + win.webContents.on('did-finish-load', () => { 87 + log(`window ${title} (wcid=${win.webContents.id}) finished loading`); 88 + }); 89 + win.loadFile(htmlPath); 90 + return win; 91 + } 92 + 93 + app.whenReady().then(async () => { 94 + log('app ready'); 95 + 96 + const sub = createWindow(path.join(__dirname, 'subscriber.html'), 'subscriber'); 97 + const pub = createWindow(path.join(__dirname, 'publisher.html'), 'publisher'); 98 + 99 + // Wait for both to finish loading 100 + await Promise.all([ 101 + new Promise(r => sub.webContents.once('did-finish-load', r)), 102 + new Promise(r => pub.webContents.once('did-finish-load', r)), 103 + ]); 104 + log('both windows loaded; subscriber=' + sub.webContents.id + ' publisher=' + pub.webContents.id); 105 + 106 + // Give subscriber a beat to register its subscription 107 + await new Promise(r => setTimeout(r, 250)); 108 + 109 + log('=== auto-test: triggering publish from publisher window ==='); 110 + // Ask publisher to publish via executeJavaScript (no user click needed) 111 + const result = await pub.webContents.executeJavaScript( 112 + "window.app.publish('test-topic', {hello: 'world', ts: Date.now()})" 113 + ); 114 + log('publish call result from publisher renderer:', result); 115 + 116 + // Wait up to 1s for subscriber to receive 117 + const start = Date.now(); 118 + let received = null; 119 + while (Date.now() - start < 1000) { 120 + received = await sub.webContents.executeJavaScript('window.__received || null'); 121 + if (received) break; 122 + await new Promise(r => setTimeout(r, 50)); 123 + } 124 + 125 + if (received) { 126 + log('=== ASSERTION PASS: subscriber received ===', received); 127 + log('VERDICT: STANDALONE_WORKS'); 128 + } else { 129 + log('=== ASSERTION FAIL: subscriber did NOT receive within 1s ==='); 130 + log('VERDICT: STANDALONE_FAILS'); 131 + } 132 + 133 + // Quit shortly after so we get clean stdout 134 + setTimeout(() => { 135 + log('quitting'); 136 + app.quit(); 137 + }, 250); 138 + }); 139 + 140 + app.on('window-all-closed', () => app.quit());
+9
scratch/v2-pubsub-mini/package.json
··· 1 + { 2 + "name": "v2-pubsub-mini", 3 + "version": "1.0.0", 4 + "main": "main.js", 5 + "private": true, 6 + "scripts": { 7 + "start": "electron ." 8 + } 9 + }
+55
scratch/v2-pubsub-mini/preload.cjs
··· 1 + // V2 tile-style preload: contextBridge + ipcRenderer.send for pubsub 2 + // Mirrors Peek's tile-preload.cts pattern. 3 + const { contextBridge, ipcRenderer } = require('electron'); 4 + 5 + const subscribers = new Map(); // topic -> Set<callback> 6 + const log = (...args) => { 7 + const msg = `[preload pid=${process.pid}] ${args.join(' ')}`; 8 + console.error(msg); 9 + }; 10 + 11 + log('preload loaded'); 12 + 13 + // Forward main-process broadcasts to in-renderer subscribers. 14 + // We listen on a single fan-in channel: 'pubsub:message' carrying {topic, data}. 15 + ipcRenderer.on('pubsub:message', (_evt, payload) => { 16 + log('renderer got pubsub:message', JSON.stringify(payload)); 17 + const { topic, data } = payload || {}; 18 + const cbs = subscribers.get(topic); 19 + if (!cbs) return; 20 + for (const cb of cbs) { 21 + try { cb(data); } catch (e) { log('subscriber callback error', e.message); } 22 + } 23 + }); 24 + 25 + contextBridge.exposeInMainWorld('app', { 26 + pid: process.pid, 27 + publish(topic, data) { 28 + log('publish() called', topic); 29 + try { 30 + ipcRenderer.send('tile:pubsub:publish', { topic, data }); 31 + return { published: true }; 32 + } catch (e) { 33 + log('publish() threw', e.message); 34 + return { published: false, error: e.message }; 35 + } 36 + }, 37 + subscribe(topic, callback) { 38 + log('subscribe() called', topic); 39 + if (!subscribers.has(topic)) subscribers.set(topic, new Set()); 40 + subscribers.get(topic).add(callback); 41 + try { 42 + ipcRenderer.send('tile:pubsub:subscribe', { topic }); 43 + } catch (e) { 44 + log('subscribe() send threw', e.message); 45 + } 46 + return () => { 47 + subscribers.get(topic)?.delete(callback); 48 + ipcRenderer.send('tile:pubsub:unsubscribe', { topic }); 49 + }; 50 + }, 51 + // Renderer-side log function so HTML pages can write to main stdout 52 + rlog(msg) { 53 + ipcRenderer.send('mini:log', msg); 54 + }, 55 + });
+23
scratch/v2-pubsub-mini/publisher.html
··· 1 + <!doctype html> 2 + <html> 3 + <head><meta charset="utf-8"><title>publisher</title></head> 4 + <body> 5 + <h1>publisher</h1> 6 + <button id="b">publish</button> 7 + <div id="log"></div> 8 + <script> 9 + const logEl = document.getElementById('log'); 10 + function ui(msg) { 11 + const d = document.createElement('div'); 12 + d.textContent = msg; 13 + logEl.appendChild(d); 14 + } 15 + ui('publisher ready, app=' + (typeof window.app)); 16 + window.app.rlog('publisher HTML script running, app=' + (typeof window.app)); 17 + document.getElementById('b').addEventListener('click', () => { 18 + const r = window.app.publish('test-topic', {hello: 'from-button', ts: Date.now()}); 19 + ui('publish result: ' + JSON.stringify(r)); 20 + }); 21 + </script> 22 + </body> 23 + </html>
+25
scratch/v2-pubsub-mini/subscriber.html
··· 1 + <!doctype html> 2 + <html> 3 + <head><meta charset="utf-8"><title>subscriber</title></head> 4 + <body> 5 + <h1>subscriber</h1> 6 + <div id="log"></div> 7 + <script> 8 + window.__received = null; 9 + const logEl = document.getElementById('log'); 10 + function ui(msg) { 11 + const d = document.createElement('div'); 12 + d.textContent = msg; 13 + logEl.appendChild(d); 14 + } 15 + ui('subscriber ready, app=' + (typeof window.app)); 16 + window.app.rlog('subscriber HTML script running, app=' + (typeof window.app)); 17 + window.app.subscribe('test-topic', (data) => { 18 + window.__received = data; 19 + ui('RECEIVED: ' + JSON.stringify(data)); 20 + window.app.rlog('subscriber callback fired: ' + JSON.stringify(data)); 21 + }); 22 + window.app.rlog('subscriber subscribe() returned'); 23 + </script> 24 + </body> 25 + </html>