Skip to content

Commit 480a0f5

Browse files
zeevdrclaude
andauthored
feat(dispose): add Symbol.asyncDispose to ConfigClient and ConfigWatcher (#82)
Symbol.dispose fires stop() as fire-and-forget, so callers using `await using` would not wait for cleanup to complete. Adding Symbol.asyncDispose lets the runtime await stop() properly. Also adds esnext.disposable to tsconfig lib so the symbol type is recognized by the compiler without a target bump. Closes #51 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8fb4ce4 commit 480a0f5

5 files changed

Lines changed: 60 additions & 2 deletions

File tree

src/client.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,13 @@ export class ConfigClient {
353353
this.close();
354354
}
355355

356+
/**
357+
* Async dispose pattern support — use with `await using`.
358+
*/
359+
async [Symbol.asyncDispose](): Promise<void> {
360+
this.close();
361+
}
362+
356363
// --- Private helpers ---
357364

358365
private async fetchServerInfo(): Promise<ServerInfo> {

src/watcher.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,12 +357,19 @@ export class ConfigWatcher {
357357
* Dispose pattern support (TypeScript 5.2+).
358358
*
359359
* Calls stop() synchronously (best-effort). For full cleanup, prefer
360-
* calling `await watcher.stop()` explicitly.
360+
* `await using` or calling `await watcher.stop()` explicitly.
361361
*/
362362
[Symbol.dispose](): void {
363363
void this.stop();
364364
}
365365

366+
/**
367+
* Async dispose pattern support — use with `await using`.
368+
*/
369+
async [Symbol.asyncDispose](): Promise<void> {
370+
await this.stop();
371+
}
372+
366373
private async loadSnapshot(): Promise<void> {
367374
const resp = await this.callGetConfig({
368375
tenantId: this.tenantId,

test/client.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,23 @@ describe("ConfigClient", () => {
382382
});
383383
});
384384

385+
describe("Symbol.asyncDispose", () => {
386+
it("closes both stubs and resolves", async () => {
387+
await client[Symbol.asyncDispose]();
388+
expect(configStub.close).toHaveBeenCalledTimes(1);
389+
expect(serverStub.close).toHaveBeenCalledTimes(1);
390+
});
391+
392+
it("works with await using", async () => {
393+
await (async () => {
394+
await using c = client;
395+
void c;
396+
})();
397+
expect(configStub.close).toHaveBeenCalledTimes(1);
398+
expect(serverStub.close).toHaveBeenCalledTimes(1);
399+
});
400+
});
401+
385402
describe("per-call timeout", () => {
386403
it("get() uses per-call timeout over client default", async () => {
387404
let capturedDeadline: number | undefined;

test/watcher.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,33 @@ describe("ConfigWatcher", () => {
484484
});
485485
});
486486

487+
describe("Symbol.asyncDispose", () => {
488+
it("awaits stop() before resolving", async () => {
489+
const watcher = createWatcher();
490+
mockGetConfigSuccess([]);
491+
watcher.field("payments.fee", Number, { default: 0.01 });
492+
493+
await watcher.start();
494+
await watcher[Symbol.asyncDispose]();
495+
496+
expect(mockStream.cancel).toHaveBeenCalledOnce();
497+
});
498+
499+
it("works with await using", async () => {
500+
const watcher = createWatcher();
501+
mockGetConfigSuccess([]);
502+
watcher.field("payments.fee", Number, { default: 0.01 });
503+
await watcher.start();
504+
505+
await (async () => {
506+
await using w = watcher;
507+
void w;
508+
})();
509+
510+
expect(mockStream.cancel).toHaveBeenCalledOnce();
511+
});
512+
});
513+
487514
describe("processing changes", () => {
488515
it("updates fields on data events", async () => {
489516
const watcher = createWatcher();

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"target": "ES2022",
44
"module": "Node16",
55
"moduleResolution": "Node16",
6-
"lib": ["ES2022"],
6+
"lib": ["ES2022", "esnext.disposable"],
77
"outDir": "dist",
88
"rootDir": "src",
99
"declaration": true,

0 commit comments

Comments
 (0)