diff --git a/src/client.ts b/src/client.ts index 2c4e80e..45ba11f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -353,6 +353,13 @@ export class ConfigClient { this.close(); } + /** + * Async dispose pattern support — use with `await using`. + */ + async [Symbol.asyncDispose](): Promise { + this.close(); + } + // --- Private helpers --- private async fetchServerInfo(): Promise { diff --git a/src/watcher.ts b/src/watcher.ts index fa76986..c0add05 100644 --- a/src/watcher.ts +++ b/src/watcher.ts @@ -357,12 +357,19 @@ export class ConfigWatcher { * Dispose pattern support (TypeScript 5.2+). * * Calls stop() synchronously (best-effort). For full cleanup, prefer - * calling `await watcher.stop()` explicitly. + * `await using` or calling `await watcher.stop()` explicitly. */ [Symbol.dispose](): void { void this.stop(); } + /** + * Async dispose pattern support — use with `await using`. + */ + async [Symbol.asyncDispose](): Promise { + await this.stop(); + } + private async loadSnapshot(): Promise { const resp = await this.callGetConfig({ tenantId: this.tenantId, diff --git a/test/client.test.ts b/test/client.test.ts index 00d1e8b..e267cd3 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -382,6 +382,23 @@ describe("ConfigClient", () => { }); }); + describe("Symbol.asyncDispose", () => { + it("closes both stubs and resolves", async () => { + await client[Symbol.asyncDispose](); + expect(configStub.close).toHaveBeenCalledTimes(1); + expect(serverStub.close).toHaveBeenCalledTimes(1); + }); + + it("works with await using", async () => { + await (async () => { + await using c = client; + void c; + })(); + expect(configStub.close).toHaveBeenCalledTimes(1); + expect(serverStub.close).toHaveBeenCalledTimes(1); + }); + }); + describe("per-call timeout", () => { it("get() uses per-call timeout over client default", async () => { let capturedDeadline: number | undefined; diff --git a/test/watcher.test.ts b/test/watcher.test.ts index 297e352..eb34dd6 100644 --- a/test/watcher.test.ts +++ b/test/watcher.test.ts @@ -484,6 +484,33 @@ describe("ConfigWatcher", () => { }); }); + describe("Symbol.asyncDispose", () => { + it("awaits stop() before resolving", async () => { + const watcher = createWatcher(); + mockGetConfigSuccess([]); + watcher.field("payments.fee", Number, { default: 0.01 }); + + await watcher.start(); + await watcher[Symbol.asyncDispose](); + + expect(mockStream.cancel).toHaveBeenCalledOnce(); + }); + + it("works with await using", async () => { + const watcher = createWatcher(); + mockGetConfigSuccess([]); + watcher.field("payments.fee", Number, { default: 0.01 }); + await watcher.start(); + + await (async () => { + await using w = watcher; + void w; + })(); + + expect(mockStream.cancel).toHaveBeenCalledOnce(); + }); + }); + describe("processing changes", () => { it("updates fields on data events", async () => { const watcher = createWatcher(); diff --git a/tsconfig.json b/tsconfig.json index 6c7764d..7260e50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", - "lib": ["ES2022"], + "lib": ["ES2022", "esnext.disposable"], "outDir": "dist", "rootDir": "src", "declaration": true,