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
7 changes: 7 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,13 @@ export class ConfigClient {
this.close();
}

/**
* Async dispose pattern support — use with `await using`.
*/
async [Symbol.asyncDispose](): Promise<void> {
this.close();
}

// --- Private helpers ---

private async fetchServerInfo(): Promise<ServerInfo> {
Expand Down
9 changes: 8 additions & 1 deletion src/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
await this.stop();
}

private async loadSnapshot(): Promise<void> {
const resp = await this.callGetConfig({
tenantId: this.tenantId,
Expand Down
17 changes: 17 additions & 0 deletions test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
27 changes: 27 additions & 0 deletions test/watcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"lib": ["ES2022", "esnext.disposable"],
"outDir": "dist",
"rootDir": "src",
"declaration": true,
Expand Down