From 0307abfb0deb2f6c4c4374018cec47dd8c932c74 Mon Sep 17 00:00:00 2001 From: chioarub Date: Tue, 2 Jun 2026 22:13:50 +0300 Subject: [PATCH 1/3] fix(typecheck): restore proactive module import surface --- src/proactive/index.ts | 70 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/proactive/index.ts diff --git a/src/proactive/index.ts b/src/proactive/index.ts new file mode 100644 index 0000000000..16ac8057e6 --- /dev/null +++ b/src/proactive/index.ts @@ -0,0 +1,70 @@ +type ProactiveListener = () => void + +const listeners = new Set() + +let proactiveActive = false +let proactivePaused = false +let contextBlocked = false +let nextTickAt: number | null = null + +function notifyProactiveListeners(): void { + for (const listener of listeners) { + listener() + } +} + +export function subscribeToProactiveChanges( + listener: ProactiveListener, +): () => void { + listeners.add(listener) + return () => { + listeners.delete(listener) + } +} + +export function isProactiveActive(): boolean { + return proactiveActive +} + +export function isProactivePaused(): boolean { + return proactivePaused || contextBlocked +} + +export function getNextTickAt(): number | null { + return isProactivePaused() ? null : nextTickAt +} + +export function activateProactive(_source?: string): void { + proactiveActive = true + proactivePaused = false + notifyProactiveListeners() +} + +export function deactivateProactive(): void { + proactiveActive = false + proactivePaused = false + contextBlocked = false + nextTickAt = null + notifyProactiveListeners() +} + +export function pauseProactive(): void { + proactivePaused = true + notifyProactiveListeners() +} + +export function resumeProactive(): void { + if (!proactiveActive) { + return + } + proactivePaused = false + notifyProactiveListeners() +} + +export function setContextBlocked(blocked: boolean): void { + contextBlocked = blocked + if (blocked) { + nextTickAt = null + } + notifyProactiveListeners() +} From f03c60e8c52565c9263494fa734162c18ea35a14 Mon Sep 17 00:00:00 2001 From: chioarub Date: Tue, 2 Jun 2026 22:40:47 +0300 Subject: [PATCH 2/3] test(proactive): harden state change notifications --- src/proactive/index.test.ts | 46 +++++++++++++++++++++++++++++++++++++ src/proactive/index.ts | 10 ++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/proactive/index.test.ts diff --git a/src/proactive/index.test.ts b/src/proactive/index.test.ts new file mode 100644 index 0000000000..f82b0d99f4 --- /dev/null +++ b/src/proactive/index.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, test } from 'bun:test' +import { + activateProactive, + deactivateProactive, + isProactivePaused, + setContextBlocked, + subscribeToProactiveChanges, +} from './index.js' + +describe('proactive state', () => { + const unsubscribers: Array<() => void> = [] + + afterEach(() => { + for (const unsubscribe of unsubscribers.splice(0)) { + unsubscribe() + } + deactivateProactive() + }) + + test('activation clears a stale context block', () => { + setContextBlocked(true) + + activateProactive() + + expect(isProactivePaused()).toBe(false) + }) + + test('one failing listener does not block later listeners or state changes', () => { + let notified = false + unsubscribers.push( + subscribeToProactiveChanges(() => { + throw new Error('listener failed') + }), + ) + unsubscribers.push( + subscribeToProactiveChanges(() => { + notified = true + }), + ) + + expect(() => activateProactive()).not.toThrow() + + expect(notified).toBe(true) + expect(isProactivePaused()).toBe(false) + }) +}) diff --git a/src/proactive/index.ts b/src/proactive/index.ts index 16ac8057e6..c1e52d21f9 100644 --- a/src/proactive/index.ts +++ b/src/proactive/index.ts @@ -8,8 +8,12 @@ let contextBlocked = false let nextTickAt: number | null = null function notifyProactiveListeners(): void { - for (const listener of listeners) { - listener() + for (const listener of [...listeners]) { + try { + listener() + } catch { + // Listener failures must not prevent state transitions or later listeners. + } } } @@ -37,6 +41,8 @@ export function getNextTickAt(): number | null { export function activateProactive(_source?: string): void { proactiveActive = true proactivePaused = false + contextBlocked = false + nextTickAt = null notifyProactiveListeners() } From d00be517bde174b8dcc75686ee04212fa8a36cc9 Mon Sep 17 00:00:00 2001 From: chioarub Date: Wed, 3 Jun 2026 18:33:19 +0300 Subject: [PATCH 3/3] fix(proactive): log listener notification errors --- src/proactive/index.test.ts | 44 +++++++++++++++++++++++-------------- src/proactive/index.ts | 3 ++- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/proactive/index.test.ts b/src/proactive/index.test.ts index f82b0d99f4..85f468b22f 100644 --- a/src/proactive/index.test.ts +++ b/src/proactive/index.test.ts @@ -26,21 +26,33 @@ describe('proactive state', () => { }) test('one failing listener does not block later listeners or state changes', () => { - let notified = false - unsubscribers.push( - subscribeToProactiveChanges(() => { - throw new Error('listener failed') - }), - ) - unsubscribers.push( - subscribeToProactiveChanges(() => { - notified = true - }), - ) - - expect(() => activateProactive()).not.toThrow() - - expect(notified).toBe(true) - expect(isProactivePaused()).toBe(false) + const consoleError = console.error + const consoleErrors: unknown[][] = [] + console.error = (...args: unknown[]) => { + consoleErrors.push(args) + } + try { + let notified = false + unsubscribers.push( + subscribeToProactiveChanges(() => { + throw new Error('listener failed') + }), + ) + unsubscribers.push( + subscribeToProactiveChanges(() => { + notified = true + }), + ) + + expect(() => activateProactive()).not.toThrow() + + expect(notified).toBe(true) + expect(isProactivePaused()).toBe(false) + expect(consoleErrors).toEqual([ + ['proactive listener error', expect.any(Error)], + ]) + } finally { + console.error = consoleError + } }) }) diff --git a/src/proactive/index.ts b/src/proactive/index.ts index c1e52d21f9..862584d70a 100644 --- a/src/proactive/index.ts +++ b/src/proactive/index.ts @@ -11,7 +11,8 @@ function notifyProactiveListeners(): void { for (const listener of [...listeners]) { try { listener() - } catch { + } catch (error) { + console.error('proactive listener error', error) // Listener failures must not prevent state transitions or later listeners. } }