|
1 | 1 | import type { DevframeDefinition, DevframeNodeContext, DevframeRpcClientFunctions, DevframeRpcServerFunctions } from 'devframe/types' |
| 2 | +import type { DevframeDockPanelStateEvent } from '../../types/docks' |
2 | 3 | import { mkdtempSync, writeFileSync } from 'node:fs' |
3 | 4 | import { createServer } from 'node:http' |
4 | 5 | import { tmpdir } from 'node:os' |
5 | 6 | import { join } from 'node:path' |
6 | 7 | import { createRpcClient } from 'devframe/rpc/client' |
7 | 8 | import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' |
8 | 9 | import { getPort } from 'get-port-please' |
9 | | -import { describe, expect, it } from 'vitest' |
| 10 | +import { describe, expect, it, vi } from 'vitest' |
| 11 | +import { HUB_EVENTS } from '../../events' |
10 | 12 | import { DEVFRAMES_HUB_BASE, initHub } from '../initiate' |
11 | 13 |
|
12 | 14 | function makeDist(html: string): string { |
@@ -37,10 +39,12 @@ function makeFrame(id: string, distDir?: string): DevframeDefinition { |
37 | 39 | } |
38 | 40 |
|
39 | 41 | function connectWsClient(url: string) { |
40 | | - return createRpcClient<DevframeRpcServerFunctions, DevframeRpcClientFunctions>( |
| 42 | + const channel = createWsRpcChannel({ url }) |
| 43 | + const client = createRpcClient<DevframeRpcServerFunctions, DevframeRpcClientFunctions>( |
41 | 44 | {} as DevframeRpcClientFunctions, |
42 | | - { channel: createWsRpcChannel({ url }) }, |
| 45 | + { channel }, |
43 | 46 | ) |
| 47 | + return Object.assign(client, { close: channel.close }) |
44 | 48 | } |
45 | 49 |
|
46 | 50 | describe('initHub', () => { |
@@ -134,6 +138,63 @@ describe('initHub', () => { |
134 | 138 | } |
135 | 139 | }) |
136 | 140 |
|
| 141 | + it('tracks panel state by RPC connection and emits disconnect separately from close', async () => { |
| 142 | + expect.assertions(9) |
| 143 | + |
| 144 | + const host = '127.0.0.1' |
| 145 | + const port = await getPort({ port: 18215, host }) |
| 146 | + const hub = initHub({ |
| 147 | + base: DEVFRAMES_HUB_BASE, |
| 148 | + auth: false, |
| 149 | + host, |
| 150 | + ws: { port }, |
| 151 | + devframes: [makeFrame('alpha')], |
| 152 | + }) |
| 153 | + const clients: ReturnType<typeof connectWsClient>[] = [] |
| 154 | + |
| 155 | + try { |
| 156 | + await hub.ready |
| 157 | + const context = await hub.context |
| 158 | + const lifecycleEvents: DevframeDockPanelStateEvent[] = [] |
| 159 | + context.docks.events.on(HUB_EVENTS.bus.docksPanelState, event => lifecycleEvents.push(event)) |
| 160 | + |
| 161 | + const firstClient = connectWsClient(`ws://${host}:${port}/__ws`) |
| 162 | + const secondClient = connectWsClient(`ws://${host}:${port}/__ws`) |
| 163 | + clients.push(firstClient, secondClient) |
| 164 | + |
| 165 | + await firstClient.$call(HUB_EVENTS.rpc.docksPanelState, true) |
| 166 | + await firstClient.$call(HUB_EVENTS.rpc.docksPanelState, true) |
| 167 | + await firstClient.$call(HUB_EVENTS.rpc.docksPanelState, false) |
| 168 | + await secondClient.$call(HUB_EVENTS.rpc.docksPanelState, false) |
| 169 | + |
| 170 | + expect(lifecycleEvents).toHaveLength(3) |
| 171 | + expect(lifecycleEvents[0]).toMatchObject({ type: 'connected', open: true }) |
| 172 | + expect(typeof lifecycleEvents[0]!.sessionId).toBe('number') |
| 173 | + expect(lifecycleEvents[1]).toEqual({ type: 'changed', sessionId: lifecycleEvents[0]!.sessionId, open: false }) |
| 174 | + expect(lifecycleEvents[2]).toMatchObject({ type: 'connected', open: false }) |
| 175 | + expect(lifecycleEvents[2]!.sessionId).not.toBe(lifecycleEvents[0]!.sessionId) |
| 176 | + |
| 177 | + firstClient.close() |
| 178 | + await vi.waitFor(() => { |
| 179 | + if (lifecycleEvents.length !== 4) |
| 180 | + throw new Error('waiting for the first client to disconnect') |
| 181 | + }) |
| 182 | + expect(lifecycleEvents[3]).toEqual({ type: 'disconnected', sessionId: lifecycleEvents[0]!.sessionId }) |
| 183 | + |
| 184 | + const reconnectedClient = connectWsClient(`ws://${host}:${port}/__ws`) |
| 185 | + clients.push(reconnectedClient) |
| 186 | + await reconnectedClient.$call(HUB_EVENTS.rpc.docksPanelState, true) |
| 187 | + |
| 188 | + expect(lifecycleEvents[4]).toMatchObject({ type: 'connected', open: true }) |
| 189 | + expect([lifecycleEvents[0]!.sessionId, lifecycleEvents[2]!.sessionId]).not.toContain(lifecycleEvents[4]!.sessionId) |
| 190 | + } |
| 191 | + finally { |
| 192 | + for (const client of clients) |
| 193 | + client.close() |
| 194 | + await hub.close() |
| 195 | + } |
| 196 | + }) |
| 197 | + |
137 | 198 | it('ui slot: viewer owns the root, embedded.js serves the entry, discovery still wins', async () => { |
138 | 199 | const viewerDist = makeDist('<!doctype html><title>hub viewer</title>') |
139 | 200 | const embeddedDir = mkdtempSync(join(tmpdir(), 'hub-embedded-')) |
|
0 commit comments