mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
fix(remote): make the wake banner's poller page-wide and independent of tab switches
Reported as 'the tab shows no banner' while the host was verifiably unreachable: the banner only started polling from selectSession, which RETURNS EARLY for the tab you are already on (so a page loaded with the remote tab active never polled), and a long-lived tab keeps running the JS it loaded — the feature was invisible to anyone who did not switch tabs after the deploy. The poller is now page-wide: one interval (created on init and on the first session switch), re-targeted whenever the active session changes, plus a visibilitychange wake-up. It no longer depends on any single selection path running. Also adds test/sse-dispatch-table.test.ts: a static guard that every [SSE_EVENTS.X, '_onFoo'] entry names an event constants.js defines AND a handler some module defines. Both halves fail silently (a typo'd constant is an undefined table key; a renamed handler just never runs), which is exactly how a new banner can never appear with no error anywhere.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @fileoverview Static guard: every SSE dispatch entry must actually resolve.
|
||||
*
|
||||
* `app.js` dispatches server events through a table of `[SSE_EVENTS.X, '_onFoo']`
|
||||
* pairs. Both halves fail SILENTLY when they are wrong:
|
||||
*
|
||||
* - a handler name that exists in no module (renamed method, typo) → the event is
|
||||
* received and nothing happens, with no error anywhere;
|
||||
* - an `SSE_EVENTS.X` key that `constants.js` does not define → the table key is
|
||||
* `undefined`, so the entry can never match an incoming event.
|
||||
*
|
||||
* Both have happened in this codebase's feature areas (a new banner/toast that simply
|
||||
* never appears), and neither is visible to a test that only checks the modules compile.
|
||||
* Pure static analysis — no server, no browser.
|
||||
*/
|
||||
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const PUBLIC_DIR = fileURLToPath(new URL('../src/web/public', import.meta.url));
|
||||
|
||||
const appJs = readFileSync(join(PUBLIC_DIR, 'app.js'), 'utf-8');
|
||||
const constantsJs = readFileSync(join(PUBLIC_DIR, 'constants.js'), 'utf-8');
|
||||
const allModules = readdirSync(PUBLIC_DIR)
|
||||
.filter((name) => name.endsWith('.js'))
|
||||
.map((name) => readFileSync(join(PUBLIC_DIR, name), 'utf-8'))
|
||||
.join('\n');
|
||||
|
||||
/** `[SSE_EVENTS.FOO, '_onFoo'],` entries of the dispatch table. */
|
||||
function dispatchEntries(): { constant: string; handler: string }[] {
|
||||
const entries: { constant: string; handler: string }[] = [];
|
||||
const re = /\[SSE_EVENTS\.([A-Z0-9_]+),\s*'(_[A-Za-z0-9_]+)'\]/g;
|
||||
for (const match of appJs.matchAll(re)) {
|
||||
entries.push({ constant: match[1], handler: match[2] });
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
describe('SSE dispatch table', () => {
|
||||
it('has entries to check (the table is what this guard exists for)', () => {
|
||||
expect(dispatchEntries().length).toBeGreaterThan(20);
|
||||
});
|
||||
|
||||
it('names only events that constants.js defines', () => {
|
||||
const defined = new Set([...constantsJs.matchAll(/^\s{2}([A-Z0-9_]+):\s*'/gm)].map((m) => m[1]));
|
||||
const missing = dispatchEntries()
|
||||
.map((entry) => entry.constant)
|
||||
.filter((name) => !defined.has(name));
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it('names only handlers that some frontend module actually defines', () => {
|
||||
const missing = dispatchEntries()
|
||||
.map((entry) => entry.handler)
|
||||
.filter((handler) => !new RegExp(`(^|\\s)${handler}\\s*\\(`, 'm').test(allModules));
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user