Skip to content

Commit 8b9bc0e

Browse files
committed
feat: add tests
1 parent 60fb646 commit 8b9bc0e

13 files changed

Lines changed: 1041 additions & 21 deletions

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: oven-sh/setup-bun@v2
17+
with:
18+
bun-version: latest
19+
20+
- name: Install dependencies
21+
run: bun install
22+
23+
- name: Type check
24+
run: bun run typecheck
25+
26+
- name: Run tests
27+
run: bun run test:run
28+
29+
- name: Build
30+
run: bun run build

bun.lock

Lines changed: 187 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
"scripts": {
3333
"build": "tsdown",
3434
"dev": "tsdown --watch",
35-
"typecheck": "tsc --noEmit"
35+
"typecheck": "tsc --noEmit",
36+
"test": "vitest",
37+
"test:run": "vitest run"
3638
},
3739
"peerDependencies": {
3840
"@effect/rpc": ">=0.70.0",
@@ -42,9 +44,14 @@
4244
},
4345
"dependencies": {},
4446
"devDependencies": {
47+
"@effect/rpc": "^0.70.0",
48+
"@tanstack/devtools-event-client": "^0.3.0",
4549
"@types/node": "^24.10.1",
4650
"@types/react": "^19.2.7",
51+
"effect": "^3.16.0",
52+
"react": "^19.1.0",
4753
"tsdown": "^0.17.2",
48-
"typescript": "^5.9.3"
54+
"typescript": "^5.9.3",
55+
"vitest": "^3.2.4"
4956
}
5057
}

src/components/RpcDevtoolsPanel.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import { useEffect, useMemo, useState } from "react"
2+
import { setDebug } from "../event-client"
23
import { clearRequests, useRpcRequests, useRpcStats } from "../store"
34
import { RequestDetail } from "./RequestDetail"
45
import { RequestList } from "./RequestList"
56
import { injectKeyframes, styles } from "./styles"
67

7-
export function RpcDevtoolsPanel() {
8+
export interface RpcDevtoolsPanelOptions {
9+
/** Enable debug logging for the event client. Default: false */
10+
debug?: boolean
11+
}
12+
13+
interface RpcDevtoolsPanelProps {
14+
options?: RpcDevtoolsPanelOptions
15+
}
16+
17+
export function RpcDevtoolsPanel({ options }: RpcDevtoolsPanelProps) {
818
const requests = useRpcRequests()
919
const stats = useRpcStats()
1020
const [selectedId, setSelectedId] = useState<string | null>(null)
@@ -15,6 +25,11 @@ export function RpcDevtoolsPanel() {
1525
injectKeyframes()
1626
}, [])
1727

28+
// Sync debug option with event client
29+
useEffect(() => {
30+
setDebug(options?.debug ?? false)
31+
}, [options?.debug])
32+
1833
const filteredRequests = useMemo(() => {
1934
if (!filter) return requests
2035
const lowerFilter = filter.toLowerCase()

src/components/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export { RequestDetail } from "./RequestDetail"
22
export { RequestList } from "./RequestList"
3-
export { RpcDevtoolsPanel } from "./RpcDevtoolsPanel"
3+
export { RpcDevtoolsPanel, type RpcDevtoolsPanelOptions } from "./RpcDevtoolsPanel"
44
export { injectKeyframes, styles } from "./styles"

src/event-client.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { EventClient } from "@tanstack/devtools-event-client"
22
import type { RpcDevtoolsEventMap } from "./types"
33

4-
/**
5-
* Global key for the singleton event client
6-
* Using globalThis ensures the same instance is used across dynamic imports
7-
*/
8-
const GLOBAL_KEY = "__EFFECT_RPC_DEVTOOLS_CLIENT__" as const
4+
type RpcEventClient = EventClient<RpcDevtoolsEventMap, "effect-rpc">
5+
6+
const CLIENT_KEY = "__EFFECT_RPC_DEVTOOLS_CLIENT__" as const
7+
const DEBUG_KEY = "__EFFECT_RPC_DEVTOOLS_DEBUG__" as const
98

109
declare global {
11-
var __EFFECT_RPC_DEVTOOLS_CLIENT__: EventClient<RpcDevtoolsEventMap, "effect-rpc"> | undefined
10+
var __EFFECT_RPC_DEVTOOLS_CLIENT__: RpcEventClient | undefined
11+
var __EFFECT_RPC_DEVTOOLS_DEBUG__: boolean | undefined
1212
}
1313

1414
/**
@@ -34,16 +34,15 @@ const isDev = () => {
3434
/**
3535
* Get or create the singleton event client
3636
*/
37-
function getOrCreateClient(): EventClient<RpcDevtoolsEventMap, "effect-rpc"> {
38-
if (!globalThis[GLOBAL_KEY]) {
39-
const dev = isDev()
40-
globalThis[GLOBAL_KEY] = new EventClient<RpcDevtoolsEventMap, "effect-rpc">({
37+
function getClient(): RpcEventClient {
38+
if (!globalThis[CLIENT_KEY]) {
39+
globalThis[CLIENT_KEY] = new EventClient<RpcDevtoolsEventMap, "effect-rpc">({
4140
pluginId: "effect-rpc",
42-
debug: dev,
43-
enabled: dev,
41+
debug: globalThis[DEBUG_KEY] ?? false,
42+
enabled: isDev(),
4443
})
4544
}
46-
return globalThis[GLOBAL_KEY]
45+
return globalThis[CLIENT_KEY]
4746
}
4847

4948
/**
@@ -52,6 +51,27 @@ function getOrCreateClient(): EventClient<RpcDevtoolsEventMap, "effect-rpc"> {
5251
* This client emits events when RPC requests are made and responses are received.
5352
* The devtools panel subscribes to these events to display the RPC traffic.
5453
*
55-
* Uses globalThis to ensure singleton across dynamic imports.
54+
* Implemented as a Proxy to ensure setDebug() changes are reflected even after import.
55+
*/
56+
export const rpcEventClient: RpcEventClient = new Proxy({} as RpcEventClient, {
57+
get(_, prop) {
58+
const client = getClient()
59+
const value = client[prop as keyof RpcEventClient]
60+
return typeof value === "function" ? value.bind(client) : value
61+
},
62+
})
63+
64+
/**
65+
* Enable or disable debug logging for the RPC devtools event client.
66+
* Debug is disabled by default. This recreates the client with the new setting.
5667
*/
57-
export const rpcEventClient = getOrCreateClient()
68+
export function setDebug(debug: boolean): void {
69+
if ((globalThis[DEBUG_KEY] ?? false) !== debug) {
70+
globalThis[DEBUG_KEY] = debug
71+
globalThis[CLIENT_KEY] = new EventClient<RpcDevtoolsEventMap, "effect-rpc">({
72+
pluginId: "effect-rpc",
73+
debug,
74+
enabled: isDev(),
75+
})
76+
}
77+
}

test/event-client.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest"
2+
import { rpcEventClient, setDebug } from "../src/event-client"
3+
4+
describe("rpcEventClient", () => {
5+
beforeEach(() => {
6+
// Reset is handled by test/setup.ts
7+
})
8+
9+
describe("proxy behavior", () => {
10+
it("forwards method calls to the real client", () => {
11+
expect(typeof rpcEventClient.emit).toBe("function")
12+
expect(typeof rpcEventClient.on).toBe("function")
13+
expect(typeof rpcEventClient.getPluginId).toBe("function")
14+
})
15+
16+
it("returns correct pluginId", () => {
17+
expect(rpcEventClient.getPluginId()).toBe("effect-rpc")
18+
})
19+
20+
it("maintains reference after multiple accesses", () => {
21+
const emit1 = rpcEventClient.emit
22+
const emit2 = rpcEventClient.emit
23+
// Functions should be equivalent (bound to same client)
24+
expect(typeof emit1).toBe("function")
25+
expect(typeof emit2).toBe("function")
26+
})
27+
})
28+
29+
describe("singleton pattern", () => {
30+
it("caches client in globalThis", () => {
31+
// Access the client
32+
rpcEventClient.getPluginId()
33+
34+
// Check globalThis has the client
35+
expect(globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__).toBeDefined()
36+
})
37+
38+
it("reuses cached client", () => {
39+
const pluginId1 = rpcEventClient.getPluginId()
40+
const pluginId2 = rpcEventClient.getPluginId()
41+
42+
expect(pluginId1).toBe(pluginId2)
43+
expect(pluginId1).toBe("effect-rpc")
44+
})
45+
})
46+
})
47+
48+
describe("setDebug", () => {
49+
beforeEach(() => {
50+
// Reset is handled by test/setup.ts
51+
})
52+
53+
it("sets debug flag in globalThis", () => {
54+
expect(globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__).toBeUndefined()
55+
56+
setDebug(true)
57+
58+
expect(globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__).toBe(true)
59+
})
60+
61+
it("recreates client when debug changes", () => {
62+
// Access client to create it
63+
rpcEventClient.getPluginId()
64+
const firstClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__
65+
66+
// Change debug setting
67+
setDebug(true)
68+
const secondClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__
69+
70+
expect(secondClient).not.toBe(firstClient)
71+
})
72+
73+
it("does not recreate client when debug value is same", () => {
74+
setDebug(true)
75+
const firstClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__
76+
77+
setDebug(true)
78+
const secondClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__
79+
80+
expect(secondClient).toBe(firstClient)
81+
})
82+
83+
it("proxy reflects new client after setDebug", () => {
84+
// Create initial client
85+
rpcEventClient.getPluginId()
86+
const initialClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__
87+
88+
// Change debug
89+
setDebug(true)
90+
91+
// Proxy should now use new client
92+
rpcEventClient.getPluginId()
93+
const currentClient = globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__
94+
95+
expect(currentClient).not.toBe(initialClient)
96+
})
97+
98+
it("toggles debug off", () => {
99+
setDebug(true)
100+
expect(globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__).toBe(true)
101+
102+
setDebug(false)
103+
expect(globalThis.__EFFECT_RPC_DEVTOOLS_DEBUG__).toBe(false)
104+
})
105+
})
106+
107+
describe("isDev detection", () => {
108+
beforeEach(() => {
109+
vi.unstubAllEnvs()
110+
})
111+
112+
it("creates client with enabled based on environment", () => {
113+
// In test environment, NODE_ENV is typically 'test', not 'development'
114+
// So the client should be created but may not be enabled
115+
rpcEventClient.getPluginId()
116+
expect(globalThis.__EFFECT_RPC_DEVTOOLS_CLIENT__).toBeDefined()
117+
})
118+
})

0 commit comments

Comments
 (0)