From e5a80d4f06cd0d4bf64e6b94b50f7430829c336a Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Tue, 14 Jul 2026 20:49:49 +0000 Subject: [PATCH] fix(session): isolate availability observers --- src/application/BinarySession.ts | 15 +++++++-- src/application/BinarySessionPort.ts | 2 +- tests/binarySession.test.ts | 49 ++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/application/BinarySession.ts b/src/application/BinarySession.ts index 9844a9ef4..2f5ad536e 100644 --- a/src/application/BinarySession.ts +++ b/src/application/BinarySession.ts @@ -82,7 +82,7 @@ export class BinarySession implements BinarySessionPort { string, { readonly available: boolean; readonly reason: string | null } >(); - readonly #availabilityListeners = new Set<() => void>(); + readonly #availabilityListeners = new Set<() => void | Promise>(); readonly #snapshotsEnabled: boolean; #snapshotInvalidated = false; @@ -132,7 +132,7 @@ export class BinarySession implements BinarySessionPort { } /** Observe runtime provider-health changes that affect discovery metadata. */ - onAvailabilityChanged(listener: () => void): () => void { + onAvailabilityChanged(listener: () => void | Promise): () => void { this.#availabilityListeners.add(listener); return () => this.#availabilityListeners.delete(listener); } @@ -583,7 +583,16 @@ export class BinarySession implements BinarySessionPort { } #emitAvailabilityChanged(): void { - for (const listener of this.#availabilityListeners) listener(); + for (const listener of this.#availabilityListeners) { + try { + const notification = listener(); + if (notification !== undefined) + void notification.catch(() => undefined); + } catch { + // External observers are best-effort notifications. Contain only the + // callback failure so state transitions and other listeners continue. + } + } } #serialize(operation: () => Promise): Promise { diff --git a/src/application/BinarySessionPort.ts b/src/application/BinarySessionPort.ts index 1cd6f3111..60ad2ed73 100644 --- a/src/application/BinarySessionPort.ts +++ b/src/application/BinarySessionPort.ts @@ -81,5 +81,5 @@ export interface BinarySessionPort extends AnalysisOperationPort { UnknownRegistryError >; providerIdentity(operation?: AnalysisOperation): ProviderIdentity; - onAvailabilityChanged?(listener: () => void): () => void; + onAvailabilityChanged?(listener: () => void | Promise): () => void; } diff --git a/tests/binarySession.test.ts b/tests/binarySession.test.ts index ab90a8666..703825144 100644 --- a/tests/binarySession.test.ts +++ b/tests/binarySession.test.ts @@ -254,6 +254,55 @@ describe("binary session", () => { expect(changes).toBe(2); }); + it("isolates availability observers from execution results and session state", async () => { + const [first] = await targets(); + const provider = cacheProvider([]); + let providerCalls = 0; + provider.createClient = () => ({ + execute: (operation) => { + if (operation === "health") return Promise.resolve(ok(null)); + providerCalls += 1; + return Promise.resolve( + providerCalls === 1 + ? err(new ProviderAdapterError("fixture", operation)) + : ok(operation), + ); + }, + close: () => Promise.resolve(), + }); + const session = new BinarySession(provider); + expect((await session.open(first)).ok).toBe(true); + const input = { address: "0x1000", document: "first" }; + expect((await session.execute("address_name", input)).ok).toBe(false); + + session.onAvailabilityChanged(() => { + throw new Error("external observer failed"); + }); + session.onAvailabilityChanged(() => + Promise.reject(new Error("async external observer failed")), + ); + let delivered = 0; + session.onAvailabilityChanged(() => { + delivered += 1; + }); + + await expect(session.execute("address_name", input)).resolves.toMatchObject( + { ok: true }, + ); + expect(delivered).toBe(1); + expect(session.status()).toMatchObject({ + capabilities: expect.arrayContaining([ + expect.objectContaining({ + operation: "address_name", + available: true, + reason: null, + }), + ]), + }); + expect((await session.execute("address_name", input)).ok).toBe(true); + expect(providerCalls).toBe(2); + }); + it("does not replay operations with filesystem side effects", async () => { const [first] = await targets(); const calls: string[] = [];