Skip to content

Commit 8b60c84

Browse files
committed
fix(client): make DevframeRpcClient.close() optional
Copilot's review on this PR flagged both close() additions (DevframeRpcClient and DevframeRpcClientMode) as a breaking change: adding a required property to an exported, externally-implementable interface breaks any existing consumer that hand-types a mock/adapter against it without close() — as this repo's own rpc-auth-gate.test.ts mock did until now. Made close?: () => void on both interfaces instead. Every factory this repo owns (createWsRpcClientMode, createStaticRpcClientMode) still provides it unconditionally, so nothing here loses close(); only the type requirement is relaxed. getDevframeRpcClient's close now calls mode.close?.() to match. Updated the three call sites that invoked close() directly against the now-optional type (rpc.test.ts, rpc-ws-status.test.ts) to close?.(), and removed the auth-gate mock's close() entirely rather than keep it, since a mode without one is now exactly the scenario this is meant to keep working — a new test asserts rpc.close() against it doesn't throw. Verified with a full `pnpm run build` + `pnpm exec vitest run --project tests -u`: only the client tsnapi snapshot changed (close: () => void -> close?: () => void), nothing else. Full suite: 101 files, 1088 tests (was 1087), all green. `pnpm run typecheck` and `pnpm run lint` clean across all 22 packages. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 8300293 commit 8b60c84

5 files changed

Lines changed: 27 additions & 9 deletions

File tree

packages/devframe/src/client/rpc-auth-gate.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ vi.mock('./rpc-ws', () => ({
3737
call: fakeMode.call as DevframeRpcClientMode['call'],
3838
callOptional: fakeMode.callOptional as DevframeRpcClientMode['callOptional'],
3939
callEvent: fakeMode.callEvent as DevframeRpcClientMode['callEvent'],
40-
close: () => {},
40+
// No `close` here on purpose — `close` is optional precisely so a mode written before it
41+
// existed (this one) still satisfies the interface.
4142
})),
4243
}))
4344

@@ -138,4 +139,17 @@ describe('getDevframeRpcClient — auth bootstrap gates outbound calls', () => {
138139
// Sent straight through — no more waiting once bootstrap is over.
139140
expect(fakeMode.call).toHaveBeenCalledTimes(1)
140141
})
142+
143+
it('close() is a no-op, not a throw, against a mode that predates it', async () => {
144+
const { getDevframeRpcClient } = await import('./rpc')
145+
const rpc = await getDevframeRpcClient({
146+
connectionMeta,
147+
otpParam: false,
148+
simpleAuth: false,
149+
})
150+
151+
// The mocked mode above has no `close` at all — exactly the pre-existing-mode case
152+
// `close?:` exists to keep working.
153+
expect(() => rpc.close?.()).not.toThrow()
154+
})
141155
})

packages/devframe/src/client/rpc-ws-status.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ describe('ws client connection status', () => {
153153
const { mode, ws } = setup()
154154
const closeSpy = vi.spyOn(ws, 'close')
155155

156-
mode.close()
156+
mode.close?.()
157157

158158
expect(closeSpy).toHaveBeenCalledTimes(1)
159159
})

packages/devframe/src/client/rpc.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ describe('getDevframeRpcClient — connection meta base', () => {
172172
const ws = FakeWebSocket.instances.at(-1)!
173173
const closeSpy = vi.spyOn(ws, 'close')
174174

175-
rpc.close()
175+
rpc.close?.()
176176

177177
expect(closeSpy).toHaveBeenCalledTimes(1)
178178
})
@@ -190,7 +190,7 @@ describe('getDevframeRpcClient — connection meta base', () => {
190190

191191
const rpc = await getDevframeRpcClient({ baseURL: '/__foo/', otpParam: false })
192192

193-
expect(() => rpc.close()).not.toThrow()
193+
expect(() => rpc.close?.()).not.toThrow()
194194
// Static backends never open a socket in the first place.
195195
expect(FakeWebSocket.instances).toHaveLength(0)
196196
})

packages/devframe/src/client/rpc.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,12 @@ export interface DevframeRpcClient {
203203
*
204204
* There is no corresponding "reconnect" — a closed client is done. Discard it and call
205205
* {@link getDevframeRpcClient} again to reconnect.
206+
*
207+
* Optional so a `DevframeRpcClientMode` implemented before this method existed — a custom
208+
* transport, a hand-typed mock — still satisfies the interface; an absent `close` is treated
209+
* as nothing to close.
206210
*/
207-
close: () => void
211+
close?: () => void
208212
}
209213

210214
export interface DevframeRpcClientMode {
@@ -223,7 +227,7 @@ export interface DevframeRpcClientMode {
223227
callEvent: DevframeRpcClient['callEvent']
224228
callOptional: DevframeRpcClient['callOptional']
225229
/** See {@link DevframeRpcClient.close}. */
226-
close: () => void
230+
close?: () => void
227231
}
228232

229233
export async function getDevframeRpcClient(
@@ -387,7 +391,7 @@ export async function getDevframeRpcClient(
387391
streaming: undefined!,
388392
cacheManager,
389393
scope: undefined!,
390-
close: () => mode.close(),
394+
close: () => mode.close?.(),
391395
}
392396

393397
rpc.sharedState = createRpcSharedStateClientHost(rpc)

tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export interface DevframeRpcClient {
2929
<NS extends string>(_: NS): DevframeScopedClientContext<NS, SettingsForNamespace<NS>>;
3030
(_?: null | ''): DevframeRpcClient;
3131
};
32-
close: () => void;
32+
close?: () => void;
3333
}
3434
export interface DevframeRpcClientMode {
3535
readonly isTrusted: boolean;
@@ -42,7 +42,7 @@ export interface DevframeRpcClientMode {
4242
call: DevframeRpcClient['call'];
4343
callEvent: DevframeRpcClient['callEvent'];
4444
callOptional: DevframeRpcClient['callOptional'];
45-
close: () => void;
45+
close?: () => void;
4646
}
4747
export interface DevframeRpcClientOptions extends SetupDevframeConnectionOptions {
4848
authToken?: string;

0 commit comments

Comments
 (0)