experiments in a post-browser web
10
fork

Configure Feed

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

feat(tile-compat,main): ensureTileIpcHandlers at top of initialize so handlers register before any BrowserWindow

+154
+8
backend/electron/main.ts
··· 17 17 import { initPage } from './page-glue.js'; 18 18 import { discoverExtensions, loadExtensionManifest, isBuiltinExtensionEnabled, getExternalExtensions, type ExtensionManifest, type ManifestCommand, type ManifestShortcut } from './extensions.js'; 19 19 import { initializeFeatures, type FeatureStartupResult } from './feature-startup.js'; 20 + import { ensureTileIpcHandlers } from './tile-compat.js'; 20 21 import { getLoadedTileIds, getTileManifest, getAllTileWindows, unloadAllTiles } from './tile-launcher.js'; 21 22 import { initTray } from './tray.js'; 22 23 import { registerLocalShortcut, unregisterLocalShortcut, handleLocalShortcut, registerGlobalShortcut, unregisterGlobalShortcut, unregisterShortcutsForAddress } from './shortcuts.js'; ··· 184 185 185 186 // Initialize protocol handler 186 187 initProtocol(config.rootDir); 188 + 189 + // Register tile:* IPC handlers BEFORE any BrowserWindow is created. 190 + // Without this, core renderers (cmd, hud, page) launching with 191 + // tile-preload would silently drop their `tile:validate-token` and 192 + // `tile:pubsub:*` IPC sends — handlers were historically registered 193 + // lazily from `loadV2Tile()` which runs after core glue. 194 + ensureTileIpcHandlers(); 187 195 188 196 // Initialize database 189 197 const dbPath = path.join(config.userDataPath, config.profile, 'datastore.sqlite');
+17
backend/electron/tile-compat.ts
··· 38 38 39 39 let ipcHandlersRegistered = false; 40 40 41 + /** 42 + * Ensure the `tile:*` ipcMain handlers are registered. Idempotent. 43 + * 44 + * Historically called lazily from `loadV2Tile()`, so when trustedBuiltin 45 + * core renderers (cmd, hud, page) launched before any v2 feature tile, 46 + * the handlers weren't yet registered and every 47 + * `ipcRenderer.send('tile:pubsub:publish', …)` silently dropped. Core 48 + * renderers and any other caller that needs tile IPC up front should 49 + * call this helper directly — typically from `main.ts::initialize()` 50 + * before any BrowserWindow is created. 51 + */ 52 + export function ensureTileIpcHandlers(): void { 53 + if (ipcHandlersRegistered) return; 54 + registerTileIpcHandlers(); 55 + ipcHandlersRegistered = true; 56 + } 57 + 41 58 // ─── Discovery ─────────────────────────────────────────────────────── 42 59 43 60 /**
+129
tests/desktop/v2-pubsub-reproducer.spec.ts
··· 1 + /** 2 + * Minimal reproducer for the v2-tile pubsub routing bug that blocked 3 + * v1-removal Phase 2. 4 + * 5 + * Approach: use windows that already exist post-Phase-1 as a pure 6 + * v2→v2 pubsub round-trip. Both cmd (via cmd-glue) and hud (via 7 + * hud-glue) are trustedBuiltin tile renderers loaded at startup 8 + * with `tile-preload.cjs`. cmd publishes, hud subscribes. If the 9 + * subscriber never fires, the v2→v2 pubsub path is broken — no test 10 + * fixture needed. 11 + * 12 + * Observed: under `yarn test:grep` (E2E test profile), hud window is 13 + * NOT present in `electronApp.windows()` (only cmd, app/background, 14 + * app/extension-host, app/settings). Falls back to looking for ANY 15 + * other v2 tile window (e.g. entities/background.html) as subscriber. 16 + * Test throws with the available URL list when no pairing is found — 17 + * serves as diagnostic output for later investigation. 18 + */ 19 + 20 + import { test, expect, DesktopApp, getSharedApp, closeSharedApp } from '../fixtures/desktop-app'; 21 + import { Page } from '@playwright/test'; 22 + import { waitForExtensionsReady } from '../helpers/window-utils'; 23 + 24 + let sharedApp: DesktopApp; 25 + let sharedBgWindow: Page; 26 + 27 + test.beforeAll(async () => { 28 + sharedApp = await getSharedApp(); 29 + sharedBgWindow = await sharedApp.getBackgroundWindow(); 30 + await waitForExtensionsReady(sharedBgWindow); 31 + }); 32 + 33 + test.afterAll(async () => { 34 + await closeSharedApp(); 35 + }); 36 + 37 + function findTileWindow(app: DesktopApp, urlMatch: string): Page | null { 38 + for (const w of app.windows()) { 39 + if (w.url().includes(urlMatch)) return w; 40 + } 41 + return null; 42 + } 43 + 44 + test('v2 trustedBuiltin tile → v2 trustedBuiltin tile pubsub round-trip', async () => { 45 + // Give hud/page glue time to launch their resident renderers. 46 + // initHud / initPage are awaited inside loadExtensions() but the 47 + // BrowserWindow's URL may not resolve to the final peek:// host until 48 + // the protocol handler fires. 2s is generous. 49 + await new Promise(r => setTimeout(r, 2000)); 50 + 51 + const urls = sharedApp.windows().map(w => w.url()); 52 + console.log('[repro] available window URLs:', JSON.stringify(urls)); 53 + 54 + // cmd resident (publisher) — trustedBuiltin tile from cmd-glue 55 + const cmdWin = findTileWindow(sharedApp, 'peek://cmd/index.html'); 56 + if (!cmdWin) { 57 + throw new Error(`cmd window not found. URLs: ${JSON.stringify(urls)}`); 58 + } 59 + 60 + // Pick any second v2 tile window (subscriber). Try hud, then page, 61 + // then any eager feature tile. 62 + const subWin = 63 + findTileWindow(sharedApp, 'peek://hud/index.html') || 64 + findTileWindow(sharedApp, 'peek://page/') || 65 + findTileWindow(sharedApp, 'peek://entities/') || 66 + findTileWindow(sharedApp, 'peek://me/') || 67 + findTileWindow(sharedApp, 'peek://atproto/'); 68 + 69 + if (!subWin) { 70 + throw new Error( 71 + `No second v2 tile window found for subscriber pairing. ` + 72 + `Available URLs: ${JSON.stringify(urls)}` 73 + ); 74 + } 75 + console.log('[repro] subscriber window:', subWin.url()); 76 + 77 + // 1. Subscribe on the second v2 tile. 78 + await subWin.evaluate(() => { 79 + const api = (window as any).app; 80 + (window as any).__reproReceived = null; 81 + (window as any).__reproDiag = { 82 + apiExists: !!api, 83 + hasSubscribe: !!(api && api.subscribe), 84 + hasScopes: !!(api && api.scopes), 85 + scopeGlobal: api && api.scopes && api.scopes.GLOBAL, 86 + }; 87 + if (api && api.subscribe && api.scopes) { 88 + api.subscribe('repro:v2-v2-hello', (msg: unknown) => { 89 + (window as any).__reproReceived = msg; 90 + }, api.scopes.GLOBAL); 91 + (window as any).__reproDiag.subscribed = true; 92 + } 93 + }); 94 + 95 + // Small buffer: IPC subscribe is async in main. 96 + await new Promise(r => setTimeout(r, 200)); 97 + 98 + // 2. Publish from cmd. 99 + const pubResult = await cmdWin.evaluate(() => { 100 + const api = (window as any).app; 101 + const diag: any = { 102 + apiExists: !!api, 103 + hasPublish: !!(api && api.publish), 104 + scopeGlobal: api && api.scopes && api.scopes.GLOBAL, 105 + }; 106 + if (api && api.publish && api.scopes) { 107 + api.publish('repro:v2-v2-hello', { from: 'cmd', ts: Date.now() }, api.scopes.GLOBAL); 108 + diag.published = true; 109 + } 110 + return diag; 111 + }); 112 + console.log('[repro] publish diag:', JSON.stringify(pubResult)); 113 + 114 + // 3. Wait for subscriber to receive. 5s is generous — v1 pubsub 115 + // round-trips finish in <50ms. 116 + const received = await subWin.waitForFunction( 117 + () => (window as any).__reproReceived !== null, 118 + undefined, 119 + { timeout: 5000 } 120 + ).then(h => h.jsonValue()).catch(async () => { 121 + const diag = await subWin.evaluate(() => (window as any).__reproDiag); 122 + console.log('[repro] sub diag at timeout:', JSON.stringify(diag)); 123 + return null; 124 + }); 125 + console.log('[repro] received:', JSON.stringify(received)); 126 + 127 + expect(pubResult.published, `cmd failed to publish: ${JSON.stringify(pubResult)}`).toBe(true); 128 + expect(received, 'subscriber never received cmd publish — v2 pubsub routing broken').not.toBeNull(); 129 + });