lightstream-sdk 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/LICENSE +21 -0
- package/QUICKSTART.md +150 -0
- package/README.md +178 -0
- package/dist/errors.d.ts +26 -0
- package/dist/index.cjs +404 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +365 -0
- package/dist/rest-client.d.ts +45 -0
- package/dist/src/errors.d.ts +26 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/rest-client.d.ts +45 -0
- package/dist/src/stream-client.d.ts +26 -0
- package/dist/src/types.d.ts +86 -0
- package/dist/stream-client.d.ts +26 -0
- package/dist/types.d.ts +86 -0
- package/examples/basic-connection.ts +15 -0
- package/examples/bot-moderation.ts +27 -0
- package/examples/earnings-tracker.ts +19 -0
- package/examples/react-use-stream.tsx +48 -0
- package/package.json +53 -0
- package/src/errors.ts +49 -0
- package/src/index.ts +29 -0
- package/src/rest-client.ts +143 -0
- package/src/stream-client.ts +250 -0
- package/src/types.ts +102 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// src/stream-client.ts
|
|
2
|
+
// Real-time Raw WebSocket client for LightStream Gateway.
|
|
3
|
+
|
|
4
|
+
import { LIGHTSTREAM_ERROR_CODES, type LightStreamErrorCode } from './errors.js'
|
|
5
|
+
import type {
|
|
6
|
+
ClientOptions,
|
|
7
|
+
ConnectionStatus,
|
|
8
|
+
LightStreamEventMap,
|
|
9
|
+
Platform,
|
|
10
|
+
StreamConnectedEvent,
|
|
11
|
+
StreamOfflineEvent,
|
|
12
|
+
ConnectionErrorEvent,
|
|
13
|
+
ChatMessageEvent,
|
|
14
|
+
GiftEvent,
|
|
15
|
+
} from './types.js'
|
|
16
|
+
|
|
17
|
+
export class LightStreamStreamClient {
|
|
18
|
+
private ws: any = null
|
|
19
|
+
private _status: ConnectionStatus = 'idle'
|
|
20
|
+
private listeners: Map<string, Set<Function>> = new Map()
|
|
21
|
+
private pingTimer: any = null
|
|
22
|
+
private reconnectTimer: any = null
|
|
23
|
+
private reconnectAttempts = 0
|
|
24
|
+
private currentPlatform: Platform | null = null
|
|
25
|
+
private currentChannel: string | null = null
|
|
26
|
+
|
|
27
|
+
constructor(private options: ClientOptions) {}
|
|
28
|
+
|
|
29
|
+
get status(): ConnectionStatus {
|
|
30
|
+
return this._status
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private setStatus(newStatus: ConnectionStatus) {
|
|
34
|
+
if (this._status === newStatus) return
|
|
35
|
+
this._status = newStatus
|
|
36
|
+
this.emit('status', newStatus)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
connect(platform: Platform, channel: string): Promise<void> {
|
|
40
|
+
this.currentPlatform = platform
|
|
41
|
+
this.currentChannel = channel.trim().toLowerCase()
|
|
42
|
+
return this.initSocket()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async initSocket(): Promise<void> {
|
|
46
|
+
if (!this.currentPlatform || !this.currentChannel) {
|
|
47
|
+
throw new Error('Platform and channel must be specified to connect')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.cleanupSocket()
|
|
51
|
+
this.setStatus('connecting')
|
|
52
|
+
|
|
53
|
+
const baseWs = (this.options.wsUrl || 'wss://gateway.lightstream.lat/ws').replace(/\/+$/, '')
|
|
54
|
+
const url = new URL(baseWs)
|
|
55
|
+
url.searchParams.set('platform', this.currentPlatform)
|
|
56
|
+
url.searchParams.set('channel', this.currentChannel)
|
|
57
|
+
|
|
58
|
+
if (this.options.token) {
|
|
59
|
+
url.searchParams.set('token', this.options.token)
|
|
60
|
+
} else if (this.options.apiKey) {
|
|
61
|
+
url.searchParams.set('apiKey', this.options.apiKey)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SocketCtor = typeof WebSocket !== 'undefined'
|
|
65
|
+
? WebSocket
|
|
66
|
+
: ((await import('ws')).default as any)
|
|
67
|
+
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
try {
|
|
70
|
+
const socket = new SocketCtor(url.toString())
|
|
71
|
+
this.ws = socket
|
|
72
|
+
|
|
73
|
+
socket.onopen = () => {
|
|
74
|
+
this.reconnectAttempts = 0
|
|
75
|
+
this.startHeartbeat()
|
|
76
|
+
resolve()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
socket.onmessage = (event: any) => {
|
|
80
|
+
this.handleIncoming(typeof event.data === 'string' ? event.data : event.data?.toString())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
socket.onerror = (err: any) => {
|
|
84
|
+
this.handleFailure(LIGHTSTREAM_ERROR_CODES.UPSTREAM_ERROR, err?.message || 'WebSocket connection error')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
socket.onclose = () => {
|
|
88
|
+
this.cleanupSocket()
|
|
89
|
+
if (this._status !== 'disconnected' && this._status !== 'offline') {
|
|
90
|
+
this.setStatus('disconnected')
|
|
91
|
+
this.emit('disconnected', 'Socket connection closed')
|
|
92
|
+
this.scheduleReconnect()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
this.handleFailure(LIGHTSTREAM_ERROR_CODES.INTERNAL_ERROR, err.message)
|
|
97
|
+
resolve()
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private handleIncoming(rawData?: string) {
|
|
103
|
+
if (!rawData) return
|
|
104
|
+
let parsed: any
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(rawData)
|
|
107
|
+
} catch {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (parsed.type === 'pong') return
|
|
112
|
+
|
|
113
|
+
if (parsed.type === 'connected') {
|
|
114
|
+
this.setStatus('connected')
|
|
115
|
+
const evt: StreamConnectedEvent = {
|
|
116
|
+
channel: parsed.channel || this.currentChannel!,
|
|
117
|
+
platform: parsed.platform || this.currentPlatform!,
|
|
118
|
+
isLive: true,
|
|
119
|
+
title: parsed.title,
|
|
120
|
+
viewerCount: parsed.viewerCount,
|
|
121
|
+
startedAt: parsed.startedAt,
|
|
122
|
+
}
|
|
123
|
+
this.emit('connected', evt)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (parsed.type === 'offline') {
|
|
128
|
+
this.setStatus('offline')
|
|
129
|
+
const offlineEvt: StreamOfflineEvent = {
|
|
130
|
+
channel: parsed.channel || this.currentChannel!,
|
|
131
|
+
platform: parsed.platform || this.currentPlatform!,
|
|
132
|
+
isLive: false,
|
|
133
|
+
code: (parsed.code as LightStreamErrorCode) || LIGHTSTREAM_ERROR_CODES.STREAMER_OFFLINE,
|
|
134
|
+
message: parsed.message || 'The streamer is currently offline',
|
|
135
|
+
}
|
|
136
|
+
this.emit('stream:offline', offlineEvt)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (parsed.type === 'error') {
|
|
141
|
+
this.handleFailure(parsed.code || LIGHTSTREAM_ERROR_CODES.INTERNAL_ERROR, parsed.message)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (parsed.event === 'chat' || parsed.type === 'chat') {
|
|
146
|
+
this.emit('chat', parsed.data as ChatMessageEvent)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (parsed.event === 'gift' || parsed.type === 'gift') {
|
|
151
|
+
this.emit('gift', parsed.data as GiftEvent)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.emit('raw', parsed)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private handleFailure(code: LightStreamErrorCode, message: string) {
|
|
159
|
+
this.setStatus('error')
|
|
160
|
+
const errPayload: ConnectionErrorEvent = {
|
|
161
|
+
channel: this.currentChannel || undefined,
|
|
162
|
+
platform: this.currentPlatform || undefined,
|
|
163
|
+
code,
|
|
164
|
+
message,
|
|
165
|
+
retryable: code !== LIGHTSTREAM_ERROR_CODES.FORBIDDEN_CHANNEL && code !== LIGHTSTREAM_ERROR_CODES.AUTHENTICATION_FAILED,
|
|
166
|
+
}
|
|
167
|
+
this.emit('error', errPayload)
|
|
168
|
+
if (errPayload.retryable) {
|
|
169
|
+
this.scheduleReconnect()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private scheduleReconnect() {
|
|
174
|
+
if (this.options.autoReconnect === false) return
|
|
175
|
+
const maxAttempts = this.options.maxReconnectAttempts ?? 5
|
|
176
|
+
if (this.reconnectAttempts >= maxAttempts) return
|
|
177
|
+
|
|
178
|
+
this.reconnectAttempts++
|
|
179
|
+
this.setStatus('reconnecting')
|
|
180
|
+
|
|
181
|
+
const baseDelay = this.options.reconnectIntervalMs ?? 2000
|
|
182
|
+
const delay = Math.min(baseDelay * Math.pow(1.5, this.reconnectAttempts - 1), 30000)
|
|
183
|
+
|
|
184
|
+
this.reconnectTimer = setTimeout(() => {
|
|
185
|
+
this.initSocket().catch(() => {})
|
|
186
|
+
}, delay)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private startHeartbeat() {
|
|
190
|
+
const interval = this.options.pingIntervalMs ?? 30000
|
|
191
|
+
this.pingTimer = setInterval(() => {
|
|
192
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
193
|
+
this.ws.send(JSON.stringify({ type: 'ping' }))
|
|
194
|
+
}
|
|
195
|
+
}, interval)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private cleanupSocket() {
|
|
199
|
+
if (this.pingTimer) {
|
|
200
|
+
clearInterval(this.pingTimer)
|
|
201
|
+
this.pingTimer = null
|
|
202
|
+
}
|
|
203
|
+
if (this.reconnectTimer) {
|
|
204
|
+
clearTimeout(this.reconnectTimer)
|
|
205
|
+
this.reconnectTimer = null
|
|
206
|
+
}
|
|
207
|
+
if (this.ws) {
|
|
208
|
+
try {
|
|
209
|
+
this.ws.onclose = null
|
|
210
|
+
this.ws.onerror = null
|
|
211
|
+
this.ws.close()
|
|
212
|
+
} catch {}
|
|
213
|
+
this.ws = null
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
disconnect(): void {
|
|
218
|
+
this.cleanupSocket()
|
|
219
|
+
this.setStatus('disconnected')
|
|
220
|
+
this.currentChannel = null
|
|
221
|
+
this.currentPlatform = null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
on<K extends keyof LightStreamEventMap>(event: K, handler: LightStreamEventMap[K]): void {
|
|
225
|
+
if (!this.listeners.has(event)) {
|
|
226
|
+
this.listeners.set(event, new Set())
|
|
227
|
+
}
|
|
228
|
+
this.listeners.get(event)!.add(handler)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
off<K extends keyof LightStreamEventMap>(event: K, handler: LightStreamEventMap[K]): void {
|
|
232
|
+
const handlers = this.listeners.get(event)
|
|
233
|
+
if (handlers) {
|
|
234
|
+
handlers.delete(handler)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private emit(event: string, ...args: any[]): void {
|
|
239
|
+
const handlers = this.listeners.get(event)
|
|
240
|
+
if (handlers) {
|
|
241
|
+
for (const handler of handlers) {
|
|
242
|
+
try {
|
|
243
|
+
handler(...args)
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.error(`Error in LightStream listener [${event}]:`, e)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
// Domain Types and Contracts for LightStream SDK.
|
|
3
|
+
|
|
4
|
+
import type { LightStreamErrorCode } from './errors.js'
|
|
5
|
+
|
|
6
|
+
export type Platform = 'tiktok' | 'kick' | 'twitch'
|
|
7
|
+
export type Timeframe = 'hourly' | 'daily' | 'weekly' | 'all'
|
|
8
|
+
|
|
9
|
+
export type ConnectionStatus =
|
|
10
|
+
| 'idle'
|
|
11
|
+
| 'connecting'
|
|
12
|
+
| 'connected'
|
|
13
|
+
| 'offline'
|
|
14
|
+
| 'reconnecting'
|
|
15
|
+
| 'disconnected'
|
|
16
|
+
| 'error'
|
|
17
|
+
|
|
18
|
+
export interface ClientOptions {
|
|
19
|
+
baseUrl?: string
|
|
20
|
+
wsUrl?: string
|
|
21
|
+
apiKey?: string
|
|
22
|
+
token?: string
|
|
23
|
+
jwtKey?: string
|
|
24
|
+
autoReconnect?: boolean
|
|
25
|
+
maxReconnectAttempts?: number
|
|
26
|
+
reconnectIntervalMs?: number
|
|
27
|
+
pingIntervalMs?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StreamConnectedEvent {
|
|
31
|
+
channel: string
|
|
32
|
+
platform: Platform
|
|
33
|
+
isLive: true
|
|
34
|
+
title?: string
|
|
35
|
+
viewerCount?: number
|
|
36
|
+
startedAt?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StreamOfflineEvent {
|
|
40
|
+
channel: string
|
|
41
|
+
platform: Platform
|
|
42
|
+
isLive: false
|
|
43
|
+
code: LightStreamErrorCode
|
|
44
|
+
message: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ConnectionErrorEvent {
|
|
48
|
+
channel?: string
|
|
49
|
+
platform?: Platform
|
|
50
|
+
code: LightStreamErrorCode
|
|
51
|
+
message: string
|
|
52
|
+
details?: unknown
|
|
53
|
+
retryable: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ChatMessageEvent {
|
|
57
|
+
id: string
|
|
58
|
+
platform: Platform
|
|
59
|
+
channel: string
|
|
60
|
+
user: {
|
|
61
|
+
id: string
|
|
62
|
+
username: string
|
|
63
|
+
displayName: string
|
|
64
|
+
avatar?: string
|
|
65
|
+
badges?: string[]
|
|
66
|
+
}
|
|
67
|
+
content: string
|
|
68
|
+
timestamp: number
|
|
69
|
+
emotes?: Array<{ name: string; url: string }>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface GiftEvent {
|
|
73
|
+
id: string
|
|
74
|
+
platform: Platform
|
|
75
|
+
channel: string
|
|
76
|
+
user: {
|
|
77
|
+
id: string
|
|
78
|
+
username: string
|
|
79
|
+
displayName: string
|
|
80
|
+
avatar?: string
|
|
81
|
+
}
|
|
82
|
+
gift: {
|
|
83
|
+
id: string
|
|
84
|
+
name: string
|
|
85
|
+
value: number
|
|
86
|
+
repeatCount: number
|
|
87
|
+
repeatEnd: boolean
|
|
88
|
+
iconUrl?: string
|
|
89
|
+
}
|
|
90
|
+
timestamp: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface LightStreamEventMap {
|
|
94
|
+
status: (status: ConnectionStatus) => void
|
|
95
|
+
connected: (event: StreamConnectedEvent) => void
|
|
96
|
+
'stream:offline': (event: StreamOfflineEvent) => void
|
|
97
|
+
error: (event: ConnectionErrorEvent) => void
|
|
98
|
+
disconnected: (reason: string) => void
|
|
99
|
+
chat: (event: ChatMessageEvent) => void
|
|
100
|
+
gift: (event: GiftEvent) => void
|
|
101
|
+
raw: (data: unknown) => void
|
|
102
|
+
}
|