Skip to content

Commit 2458759

Browse files
committed
feat(iii-browser): expose addConnectionStateListener on ISdk
1 parent eb85dc5 commit 2458759

4 files changed

Lines changed: 118 additions & 0 deletions

File tree

sdk/packages/node/iii-browser/src/iii.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class Sdk implements ISdk {
7979
private reconnectionConfig: IIIReconnectionConfig
8080
private reconnectAttempt = 0
8181
private connectionState: IIIConnectionState = 'disconnected'
82+
private connectionListeners = new Set<(state: IIIConnectionState) => void>()
8283
private isShuttingDown = false
8384

8485
constructor(
@@ -435,11 +436,35 @@ class Sdk implements ISdk {
435436
this.setConnectionState('disconnected')
436437
}
437438

439+
/**
440+
* Subscribe to connection-state transitions. The handler is fired immediately
441+
* with the current state, then on every transition. Multiple listeners are
442+
* supported. Returns an unsubscribe function.
443+
*/
444+
addConnectionStateListener = (handler: (state: IIIConnectionState) => void): (() => void) => {
445+
this.connectionListeners.add(handler)
446+
try {
447+
handler(this.connectionState)
448+
} catch (e) {
449+
console.error('[iii] connection listener threw on initial fire', e)
450+
}
451+
return () => {
452+
this.connectionListeners.delete(handler)
453+
}
454+
}
455+
438456
// private methods
439457

440458
private setConnectionState(state: IIIConnectionState): void {
441459
if (this.connectionState !== state) {
442460
this.connectionState = state
461+
for (const handler of this.connectionListeners) {
462+
try {
463+
handler(state)
464+
} catch (e) {
465+
console.error('[iii] connection listener threw', e)
466+
}
467+
}
443468
}
444469
}
445470

sdk/packages/node/iii-browser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { ChannelReader, ChannelWriter } from './channels'
22

33
export { EngineFunctions, EngineTriggers } from './iii-constants'
4+
export type { IIIConnectionState } from './iii-constants'
45

56
export { type InitOptions, registerWorker, TriggerAction } from './iii'
67

sdk/packages/node/iii-browser/src/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ChannelReader, ChannelWriter } from './channels'
2+
import type { IIIConnectionState } from './iii-constants'
23
import type {
34
RegisterFunctionMessage,
45
RegisterServiceMessage,
@@ -242,6 +243,23 @@ export interface ISdk {
242243
* ```
243244
*/
244245
shutdown(): Promise<void>
246+
247+
/**
248+
* Subscribe to connection-state transitions. The handler is fired immediately
249+
* with the current state, then on every transition. Multiple listeners are
250+
* supported. Returns an unsubscribe function.
251+
*
252+
* @example
253+
* ```typescript
254+
* const unsub = iii.addConnectionStateListener((state) => {
255+
* console.log('connection state:', state)
256+
* })
257+
*
258+
* // Later, stop receiving updates
259+
* unsub()
260+
* ```
261+
*/
262+
addConnectionStateListener(handler: (state: IIIConnectionState) => void): () => void
245263
}
246264

247265
/**
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2+
import { registerWorker } from '../src/iii'
3+
import type { IIIConnectionState } from '../src/iii-constants'
4+
import type { ISdk } from '../src/types'
5+
import { MockEngine } from './mock-websocket'
6+
7+
describe('addConnectionStateListener', () => {
8+
let engine: MockEngine
9+
let sdk: ISdk
10+
11+
beforeEach(() => {
12+
engine = new MockEngine()
13+
engine.install()
14+
sdk = registerWorker('ws://test:49135')
15+
})
16+
17+
afterEach(async () => {
18+
await sdk.shutdown()
19+
engine.uninstall()
20+
})
21+
22+
it('returns an unsubscribe function', () => {
23+
const unsub = sdk.addConnectionStateListener(() => {})
24+
expect(typeof unsub).toBe('function')
25+
})
26+
27+
it('fires immediately with the current state on subscribe', () => {
28+
const states: IIIConnectionState[] = []
29+
sdk.addConnectionStateListener((s) => states.push(s))
30+
expect(states.length).toBeGreaterThanOrEqual(1)
31+
expect(['connecting', 'connected', 'disconnected', 'reconnecting', 'failed']).toContain(states[0])
32+
})
33+
34+
it('multiple listeners receive same events', async () => {
35+
const a: IIIConnectionState[] = []
36+
const b: IIIConnectionState[] = []
37+
sdk.addConnectionStateListener((s) => a.push(s))
38+
sdk.addConnectionStateListener((s) => b.push(s))
39+
40+
// Trigger a transition by waiting for the open event the engine schedules.
41+
await engine.waitForOpen()
42+
43+
expect(a.length).toBe(b.length)
44+
expect(a[0]).toBe(b[0])
45+
if (a.length > 1) {
46+
expect(a).toEqual(b)
47+
}
48+
})
49+
50+
it('unsubscribe stops further calls', async () => {
51+
const calls: IIIConnectionState[] = []
52+
const unsub = sdk.addConnectionStateListener((s) => calls.push(s))
53+
54+
await engine.waitForOpen()
55+
const beforeUnsub = calls.length
56+
57+
unsub()
58+
59+
// Trigger another transition (close should drive a state change).
60+
engine.socket.simulateClose()
61+
62+
expect(calls.length).toBe(beforeUnsub)
63+
})
64+
65+
it('emits transitions through connecting -> connected', async () => {
66+
const states: IIIConnectionState[] = []
67+
sdk.addConnectionStateListener((s) => states.push(s))
68+
69+
await engine.waitForOpen()
70+
71+
// Should have observed at minimum the initial state and a transition to connected.
72+
expect(states).toContain('connected')
73+
})
74+
})

0 commit comments

Comments
 (0)