@vobs/http 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/stream.ts ADDED
@@ -0,0 +1,128 @@
1
+ export interface SSEOptions {
2
+ readonly withCredentials?: boolean
3
+ readonly eventSource?: SSEConstructor
4
+ readonly onOpen?: (event: Event) => void
5
+ readonly onMessage?: (event: MessageEvent) => void
6
+ readonly onError?: (event: Event) => void
7
+ }
8
+
9
+ export interface SSEClient {
10
+ readonly source: EventSource
11
+ close(): void
12
+ }
13
+
14
+ export type SSEConstructor = new (url: string, options?: EventSourceInit) => EventSource
15
+
16
+ export function createSSE(url: string, options: SSEOptions = {}): SSEClient {
17
+ const EventSourceImpl = options.eventSource ?? globalThis.EventSource
18
+ if (!EventSourceImpl) throw new Error('HTTP: 当前环境没有可用的 EventSource')
19
+ const source = new EventSourceImpl(url, { withCredentials: options.withCredentials ?? false })
20
+ if (options.onOpen) source.addEventListener('open', options.onOpen)
21
+ if (options.onMessage) source.addEventListener('message', options.onMessage)
22
+ if (options.onError) source.addEventListener('error', options.onError)
23
+ return {
24
+ source,
25
+ close: () => source.close()
26
+ }
27
+ }
28
+
29
+ export interface WebSocketOptions {
30
+ readonly protocols?: string | readonly string[]
31
+ readonly webSocket?: WebSocketConstructor
32
+ readonly autoReconnect?: boolean
33
+ readonly reconnectDelay?: number
34
+ }
35
+
36
+ export interface WebSocketClient {
37
+ readonly socket: WebSocket | null
38
+ readonly state: WebSocketState
39
+ connect(): void
40
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void
41
+ close(code?: number, reason?: string): void
42
+ on<K extends WebSocketEventName>(event: K, listener: WebSocketEventListener<K>): () => void
43
+ }
44
+
45
+ export type WebSocketState = 'idle' | 'connecting' | 'open' | 'closed'
46
+ export type WebSocketEventName = 'open' | 'message' | 'error' | 'close'
47
+ export type WebSocketEventListener<K extends WebSocketEventName> =
48
+ K extends 'message' ? (event: MessageEvent) => void
49
+ : K extends 'close' ? (event: CloseEvent) => void
50
+ : (event: Event) => void
51
+ export type WebSocketConstructor = new (
52
+ url: string,
53
+ protocols?: string | string[]
54
+ ) => WebSocket
55
+
56
+ export function createWebSocket(url: string, options: WebSocketOptions = {}): WebSocketClient {
57
+ const WebSocketImpl = options.webSocket ?? globalThis.WebSocket
58
+ if (!WebSocketImpl) throw new Error('HTTP: 当前环境没有可用的 WebSocket')
59
+ const reconnectDelay = options.reconnectDelay ?? 1_000
60
+ if (!Number.isFinite(reconnectDelay) || reconnectDelay < 0) {
61
+ throw new Error('HTTP: reconnectDelay 必须是大于等于 0 的有限数字')
62
+ }
63
+
64
+ const listeners = new Map<WebSocketEventName, Set<(event: Event) => void>>()
65
+ let socket: WebSocket | null = null
66
+ let state: WebSocketState = 'idle'
67
+ let manuallyClosed = false
68
+ let reconnectTimer: ReturnType<typeof setTimeout> | undefined
69
+
70
+ const client: WebSocketClient = {
71
+ get socket(): WebSocket | null { return socket },
72
+ get state(): WebSocketState { return state },
73
+ connect,
74
+ send(data) {
75
+ if (!socket || state !== 'open') throw new Error('HTTP: WebSocket 尚未连接')
76
+ socket.send(data)
77
+ },
78
+ close(code, reason) {
79
+ manuallyClosed = true
80
+ if (reconnectTimer !== undefined) clearTimeout(reconnectTimer)
81
+ socket?.close(code, reason)
82
+ if (!socket) state = 'closed'
83
+ },
84
+ on(event, listener) {
85
+ let eventListeners = listeners.get(event)
86
+ if (!eventListeners) {
87
+ eventListeners = new Set()
88
+ listeners.set(event, eventListeners)
89
+ }
90
+ eventListeners.add(listener as (event: Event) => void)
91
+ return () => eventListeners?.delete(listener as (event: Event) => void)
92
+ }
93
+ }
94
+
95
+ connect()
96
+ return client
97
+
98
+ function connect(): void {
99
+ if (state === 'connecting' || state === 'open') return
100
+ manuallyClosed = false
101
+ state = 'connecting'
102
+ const protocols = options.protocols === undefined
103
+ ? undefined
104
+ : typeof options.protocols === 'string' ? options.protocols : [...options.protocols]
105
+ socket = new WebSocketImpl(url, protocols)
106
+ socket.addEventListener('open', event => {
107
+ state = 'open'
108
+ notify('open', event)
109
+ })
110
+ socket.addEventListener('message', event => notify('message', event))
111
+ socket.addEventListener('error', event => notify('error', event))
112
+ socket.addEventListener('close', event => {
113
+ state = 'closed'
114
+ socket = null
115
+ notify('close', event)
116
+ if (options.autoReconnect && !manuallyClosed) {
117
+ reconnectTimer = setTimeout(() => {
118
+ reconnectTimer = undefined
119
+ connect()
120
+ }, reconnectDelay)
121
+ }
122
+ })
123
+ }
124
+
125
+ function notify(event: WebSocketEventName, value: Event): void {
126
+ for (const listener of [...(listeners.get(event) ?? [])]) listener(value)
127
+ }
128
+ }