Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/main/services/app-state.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class AppStateService {

this.broadcastTimer = setTimeout(() => {
this.broadcastTimer = null;
this.flushRenderer();
this.sendToRenderer();
}, BROADCAST_COALESCE_MS);
}

Expand All @@ -143,7 +143,17 @@ export class AppStateService {

clearTimeout(this.broadcastTimer);
this.broadcastTimer = null;
this.sendToRenderer();
}

/**
* The actual send. Separate from flushRenderer because the two callers disagree about the
* pending check: flushRenderer needs it, the timer callback must not have it. Folding the
* send into flushRenderer meant the timer cleared its own handle and then called a method
* guarded on that handle, so the coalesced path - the only one production uses - silently
* sent nothing at all.
*/
private sendToRenderer(): void {
try {
const win = getWindowReference();
if (win && !win.isDestroyed()) {
Expand Down
13 changes: 13 additions & 0 deletions test/app-state.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
* streamed token and every ASR partial, and each one clones a transcript array that grows for
* the whole interview. So these checks flush explicitly rather than counting one send per
* mutation. The two invariants above are unaffected and are what this file pins.
*
* Flushing explicitly is also the one path production never takes, so the coalesced path gets
* its own check below. Every check here used to flush synchronously while the timer was still
* pending - the single arrangement in which the send worked - and that blind spot let a release
* ship in which the renderer received no state update at all, ever.
*/
import { createChecker, loadMain } from './helpers.mjs';

Expand Down Expand Up @@ -65,6 +70,14 @@ export async function run() {
check('a real change still broadcasts', sent.length === baseline + 1);
check('the change is applied', appStateService.getState().isBackendLive === false);

// Nothing in src/ ever calls flushRenderer - the app relies entirely on the timer firing on
// its own. So let it, with no flush at all, and check the send actually lands.
const beforeCoalesced = sent.length;
appStateService.updateState({ isBackendLive: true });
await new Promise((resolve) => setTimeout(resolve, 200));
check('a coalesced broadcast reaches the renderer', sent.length === beforeCoalesced + 1);
check('the coalesced broadcast carries the change', sent.at(-1).payload.isBackendLive === true);

appStateService.updateState({ interviewConfig: { fullName: 'Jane', profileData: ' ', context: '' } });
check(
'whitespace-only profile is not reported as set',
Expand Down