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
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
function __accessProp(key) {
|
|
6
|
+
return this[key];
|
|
7
|
+
}
|
|
8
|
+
var __toCommonJS = (from) => {
|
|
9
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
10
|
+
if (entry)
|
|
11
|
+
return entry;
|
|
12
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (var key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(entry, key))
|
|
16
|
+
__defProp(entry, key, {
|
|
17
|
+
get: __accessProp.bind(from, key),
|
|
18
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
__moduleCache.set(from, entry);
|
|
22
|
+
return entry;
|
|
23
|
+
};
|
|
24
|
+
var __moduleCache;
|
|
25
|
+
var __returnValue = (v) => v;
|
|
26
|
+
function __exportSetter(name, newValue) {
|
|
27
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
28
|
+
}
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, {
|
|
32
|
+
get: all[name],
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
set: __exportSetter.bind(all, name)
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/index.ts
|
|
40
|
+
var exports_src = {};
|
|
41
|
+
__export(exports_src, {
|
|
42
|
+
LIGHTSTREAM_ERROR_CODES: () => LIGHTSTREAM_ERROR_CODES,
|
|
43
|
+
LightStreamClient: () => LightStreamClient,
|
|
44
|
+
LightStreamError: () => LightStreamError,
|
|
45
|
+
LightStreamRestClient: () => LightStreamRestClient,
|
|
46
|
+
LightStreamStreamClient: () => LightStreamStreamClient,
|
|
47
|
+
createLightStreamClient: () => createLightStreamClient
|
|
48
|
+
});
|
|
49
|
+
module.exports = __toCommonJS(exports_src);
|
|
50
|
+
|
|
51
|
+
// src/rest-client.ts
|
|
52
|
+
class LightStreamRestClient {
|
|
53
|
+
config;
|
|
54
|
+
constructor(config) {
|
|
55
|
+
this.config = config;
|
|
56
|
+
}
|
|
57
|
+
async fetchJson(path, options = {}) {
|
|
58
|
+
const url = `${this.config.baseUrl.replace(/\/+$/, "")}${path}`;
|
|
59
|
+
const headers = new Headers(options.headers || {});
|
|
60
|
+
if (!headers.has("Accept"))
|
|
61
|
+
headers.set("Accept", "application/json");
|
|
62
|
+
if (this.config.apiKey) {
|
|
63
|
+
headers.set("x-api-key", this.config.apiKey);
|
|
64
|
+
} else if (this.config.jwtKey) {
|
|
65
|
+
headers.set("Authorization", `Bearer ${this.config.jwtKey}`);
|
|
66
|
+
}
|
|
67
|
+
const res = await fetch(url, { ...options, headers });
|
|
68
|
+
const data = await res.json();
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new Error(data?.error || `HTTP ${res.status}`);
|
|
71
|
+
}
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
async issueToken(options) {
|
|
75
|
+
const res = await this.fetchJson("/api/v1/auth/tokens", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: JSON.stringify(options)
|
|
79
|
+
});
|
|
80
|
+
return res.data;
|
|
81
|
+
}
|
|
82
|
+
async bulkCheckStreams(requests) {
|
|
83
|
+
const res = await this.fetchJson("/api/v1/streams/bulk-check", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ requests })
|
|
87
|
+
});
|
|
88
|
+
return res.results;
|
|
89
|
+
}
|
|
90
|
+
async getRoomStreamInfo(platform, channel) {
|
|
91
|
+
const res = await this.fetchJson(`/api/v1/rooms/${encodeURIComponent(platform)}/${encodeURIComponent(channel)}/stream-info`);
|
|
92
|
+
return res.data;
|
|
93
|
+
}
|
|
94
|
+
async getUserEarnings(platform, channel) {
|
|
95
|
+
const res = await this.fetchJson(`/api/v1/users/${encodeURIComponent(platform)}/${encodeURIComponent(channel)}/earnings`);
|
|
96
|
+
return res.data;
|
|
97
|
+
}
|
|
98
|
+
async getGiftsCatalog(platform = "tiktok") {
|
|
99
|
+
const res = await this.fetchJson(`/api/v1/gifts/catalog?platform=${encodeURIComponent(platform)}`);
|
|
100
|
+
return res.data;
|
|
101
|
+
}
|
|
102
|
+
async getLeaderboard(platform, timeframe = "daily", limit = 50) {
|
|
103
|
+
const res = await this.fetchJson(`/api/v1/rankings/leaderboard?platform=${encodeURIComponent(platform)}&timeframe=${encodeURIComponent(timeframe)}&limit=${limit}`);
|
|
104
|
+
return res.entries;
|
|
105
|
+
}
|
|
106
|
+
async searchRankings(channel, platform = "tiktok", timeframe = "daily") {
|
|
107
|
+
const res = await this.fetchJson(`/api/v1/rankings/search?channel=${encodeURIComponent(channel)}&platform=${encodeURIComponent(platform)}&timeframe=${encodeURIComponent(timeframe)}`);
|
|
108
|
+
return res.data;
|
|
109
|
+
}
|
|
110
|
+
moderation = {
|
|
111
|
+
mute: (platform, channel, targetUser, duration) => this.fetchJson("/api/v1/moderation/mutes", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify({ platform, channel, targetUser, duration })
|
|
115
|
+
}),
|
|
116
|
+
unmute: (platform, channel, targetUser) => this.fetchJson("/api/v1/moderation/mutes", {
|
|
117
|
+
method: "DELETE",
|
|
118
|
+
headers: { "Content-Type": "application/json" },
|
|
119
|
+
body: JSON.stringify({ platform, channel, targetUser })
|
|
120
|
+
}),
|
|
121
|
+
ban: (platform, channel, targetUser, reason) => this.fetchJson("/api/v1/moderation/bans", {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify({ platform, channel, targetUser, reason })
|
|
125
|
+
}),
|
|
126
|
+
unban: (platform, channel, targetUser) => this.fetchJson("/api/v1/moderation/bans", {
|
|
127
|
+
method: "DELETE",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify({ platform, channel, targetUser })
|
|
130
|
+
}),
|
|
131
|
+
deleteMessage: (platform, channel, messageId) => this.fetchJson("/api/v1/moderation/messages", {
|
|
132
|
+
method: "DELETE",
|
|
133
|
+
headers: { "Content-Type": "application/json" },
|
|
134
|
+
body: JSON.stringify({ platform, channel, messageId })
|
|
135
|
+
}),
|
|
136
|
+
toggleComments: (platform, channel, enabled) => this.fetchJson("/api/v1/moderation/comments/toggle", {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "Content-Type": "application/json" },
|
|
139
|
+
body: JSON.stringify({ platform, channel, enabled })
|
|
140
|
+
})
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/errors.ts
|
|
145
|
+
var LIGHTSTREAM_ERROR_CODES = {
|
|
146
|
+
STREAMER_OFFLINE: "STREAMER_OFFLINE",
|
|
147
|
+
ROOM_NOT_FOUND: "ROOM_NOT_FOUND",
|
|
148
|
+
AUTHENTICATION_FAILED: "AUTHENTICATION_FAILED",
|
|
149
|
+
FORBIDDEN_CHANNEL: "FORBIDDEN_CHANNEL",
|
|
150
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
151
|
+
UPSTREAM_ERROR: "UPSTREAM_ERROR",
|
|
152
|
+
TIMEOUT: "TIMEOUT",
|
|
153
|
+
INTERNAL_ERROR: "INTERNAL_ERROR"
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
class LightStreamError extends Error {
|
|
157
|
+
code;
|
|
158
|
+
details;
|
|
159
|
+
retryable;
|
|
160
|
+
timestamp;
|
|
161
|
+
constructor(payload) {
|
|
162
|
+
super(payload.message);
|
|
163
|
+
this.name = "LightStreamError";
|
|
164
|
+
this.code = payload.code;
|
|
165
|
+
this.details = payload.details;
|
|
166
|
+
this.retryable = payload.retryable ?? false;
|
|
167
|
+
this.timestamp = payload.timestamp;
|
|
168
|
+
}
|
|
169
|
+
toJSON() {
|
|
170
|
+
return {
|
|
171
|
+
code: this.code,
|
|
172
|
+
message: this.message,
|
|
173
|
+
details: this.details,
|
|
174
|
+
retryable: this.retryable,
|
|
175
|
+
timestamp: this.timestamp
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/stream-client.ts
|
|
181
|
+
class LightStreamStreamClient {
|
|
182
|
+
options;
|
|
183
|
+
ws = null;
|
|
184
|
+
_status = "idle";
|
|
185
|
+
listeners = new Map;
|
|
186
|
+
pingTimer = null;
|
|
187
|
+
reconnectTimer = null;
|
|
188
|
+
reconnectAttempts = 0;
|
|
189
|
+
currentPlatform = null;
|
|
190
|
+
currentChannel = null;
|
|
191
|
+
constructor(options) {
|
|
192
|
+
this.options = options;
|
|
193
|
+
}
|
|
194
|
+
get status() {
|
|
195
|
+
return this._status;
|
|
196
|
+
}
|
|
197
|
+
setStatus(newStatus) {
|
|
198
|
+
if (this._status === newStatus)
|
|
199
|
+
return;
|
|
200
|
+
this._status = newStatus;
|
|
201
|
+
this.emit("status", newStatus);
|
|
202
|
+
}
|
|
203
|
+
connect(platform, channel) {
|
|
204
|
+
this.currentPlatform = platform;
|
|
205
|
+
this.currentChannel = channel.trim().toLowerCase();
|
|
206
|
+
return this.initSocket();
|
|
207
|
+
}
|
|
208
|
+
async initSocket() {
|
|
209
|
+
if (!this.currentPlatform || !this.currentChannel) {
|
|
210
|
+
throw new Error("Platform and channel must be specified to connect");
|
|
211
|
+
}
|
|
212
|
+
this.cleanupSocket();
|
|
213
|
+
this.setStatus("connecting");
|
|
214
|
+
const baseWs = (this.options.wsUrl || "wss://gateway.lightstream.lat/ws").replace(/\/+$/, "");
|
|
215
|
+
const url = new URL(baseWs);
|
|
216
|
+
url.searchParams.set("platform", this.currentPlatform);
|
|
217
|
+
url.searchParams.set("channel", this.currentChannel);
|
|
218
|
+
if (this.options.token) {
|
|
219
|
+
url.searchParams.set("token", this.options.token);
|
|
220
|
+
} else if (this.options.apiKey) {
|
|
221
|
+
url.searchParams.set("apiKey", this.options.apiKey);
|
|
222
|
+
}
|
|
223
|
+
const SocketCtor = typeof WebSocket !== "undefined" ? WebSocket : (await import("ws")).default;
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
try {
|
|
226
|
+
const socket = new SocketCtor(url.toString());
|
|
227
|
+
this.ws = socket;
|
|
228
|
+
socket.onopen = () => {
|
|
229
|
+
this.reconnectAttempts = 0;
|
|
230
|
+
this.startHeartbeat();
|
|
231
|
+
resolve();
|
|
232
|
+
};
|
|
233
|
+
socket.onmessage = (event) => {
|
|
234
|
+
this.handleIncoming(typeof event.data === "string" ? event.data : event.data?.toString());
|
|
235
|
+
};
|
|
236
|
+
socket.onerror = (err) => {
|
|
237
|
+
this.handleFailure(LIGHTSTREAM_ERROR_CODES.UPSTREAM_ERROR, err?.message || "WebSocket connection error");
|
|
238
|
+
};
|
|
239
|
+
socket.onclose = () => {
|
|
240
|
+
this.cleanupSocket();
|
|
241
|
+
if (this._status !== "disconnected" && this._status !== "offline") {
|
|
242
|
+
this.setStatus("disconnected");
|
|
243
|
+
this.emit("disconnected", "Socket connection closed");
|
|
244
|
+
this.scheduleReconnect();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
} catch (err) {
|
|
248
|
+
this.handleFailure(LIGHTSTREAM_ERROR_CODES.INTERNAL_ERROR, err.message);
|
|
249
|
+
resolve();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
handleIncoming(rawData) {
|
|
254
|
+
if (!rawData)
|
|
255
|
+
return;
|
|
256
|
+
let parsed;
|
|
257
|
+
try {
|
|
258
|
+
parsed = JSON.parse(rawData);
|
|
259
|
+
} catch {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (parsed.type === "pong")
|
|
263
|
+
return;
|
|
264
|
+
if (parsed.type === "connected") {
|
|
265
|
+
this.setStatus("connected");
|
|
266
|
+
const evt = {
|
|
267
|
+
channel: parsed.channel || this.currentChannel,
|
|
268
|
+
platform: parsed.platform || this.currentPlatform,
|
|
269
|
+
isLive: true,
|
|
270
|
+
title: parsed.title,
|
|
271
|
+
viewerCount: parsed.viewerCount,
|
|
272
|
+
startedAt: parsed.startedAt
|
|
273
|
+
};
|
|
274
|
+
this.emit("connected", evt);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (parsed.type === "offline") {
|
|
278
|
+
this.setStatus("offline");
|
|
279
|
+
const offlineEvt = {
|
|
280
|
+
channel: parsed.channel || this.currentChannel,
|
|
281
|
+
platform: parsed.platform || this.currentPlatform,
|
|
282
|
+
isLive: false,
|
|
283
|
+
code: parsed.code || LIGHTSTREAM_ERROR_CODES.STREAMER_OFFLINE,
|
|
284
|
+
message: parsed.message || "The streamer is currently offline"
|
|
285
|
+
};
|
|
286
|
+
this.emit("stream:offline", offlineEvt);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (parsed.type === "error") {
|
|
290
|
+
this.handleFailure(parsed.code || LIGHTSTREAM_ERROR_CODES.INTERNAL_ERROR, parsed.message);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (parsed.event === "chat" || parsed.type === "chat") {
|
|
294
|
+
this.emit("chat", parsed.data);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (parsed.event === "gift" || parsed.type === "gift") {
|
|
298
|
+
this.emit("gift", parsed.data);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.emit("raw", parsed);
|
|
302
|
+
}
|
|
303
|
+
handleFailure(code, message) {
|
|
304
|
+
this.setStatus("error");
|
|
305
|
+
const errPayload = {
|
|
306
|
+
channel: this.currentChannel || undefined,
|
|
307
|
+
platform: this.currentPlatform || undefined,
|
|
308
|
+
code,
|
|
309
|
+
message,
|
|
310
|
+
retryable: code !== LIGHTSTREAM_ERROR_CODES.FORBIDDEN_CHANNEL && code !== LIGHTSTREAM_ERROR_CODES.AUTHENTICATION_FAILED
|
|
311
|
+
};
|
|
312
|
+
this.emit("error", errPayload);
|
|
313
|
+
if (errPayload.retryable) {
|
|
314
|
+
this.scheduleReconnect();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
scheduleReconnect() {
|
|
318
|
+
if (this.options.autoReconnect === false)
|
|
319
|
+
return;
|
|
320
|
+
const maxAttempts = this.options.maxReconnectAttempts ?? 5;
|
|
321
|
+
if (this.reconnectAttempts >= maxAttempts)
|
|
322
|
+
return;
|
|
323
|
+
this.reconnectAttempts++;
|
|
324
|
+
this.setStatus("reconnecting");
|
|
325
|
+
const baseDelay = this.options.reconnectIntervalMs ?? 2000;
|
|
326
|
+
const delay = Math.min(baseDelay * Math.pow(1.5, this.reconnectAttempts - 1), 30000);
|
|
327
|
+
this.reconnectTimer = setTimeout(() => {
|
|
328
|
+
this.initSocket().catch(() => {});
|
|
329
|
+
}, delay);
|
|
330
|
+
}
|
|
331
|
+
startHeartbeat() {
|
|
332
|
+
const interval = this.options.pingIntervalMs ?? 30000;
|
|
333
|
+
this.pingTimer = setInterval(() => {
|
|
334
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
335
|
+
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
336
|
+
}
|
|
337
|
+
}, interval);
|
|
338
|
+
}
|
|
339
|
+
cleanupSocket() {
|
|
340
|
+
if (this.pingTimer) {
|
|
341
|
+
clearInterval(this.pingTimer);
|
|
342
|
+
this.pingTimer = null;
|
|
343
|
+
}
|
|
344
|
+
if (this.reconnectTimer) {
|
|
345
|
+
clearTimeout(this.reconnectTimer);
|
|
346
|
+
this.reconnectTimer = null;
|
|
347
|
+
}
|
|
348
|
+
if (this.ws) {
|
|
349
|
+
try {
|
|
350
|
+
this.ws.onclose = null;
|
|
351
|
+
this.ws.onerror = null;
|
|
352
|
+
this.ws.close();
|
|
353
|
+
} catch {}
|
|
354
|
+
this.ws = null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
disconnect() {
|
|
358
|
+
this.cleanupSocket();
|
|
359
|
+
this.setStatus("disconnected");
|
|
360
|
+
this.currentChannel = null;
|
|
361
|
+
this.currentPlatform = null;
|
|
362
|
+
}
|
|
363
|
+
on(event, handler) {
|
|
364
|
+
if (!this.listeners.has(event)) {
|
|
365
|
+
this.listeners.set(event, new Set);
|
|
366
|
+
}
|
|
367
|
+
this.listeners.get(event).add(handler);
|
|
368
|
+
}
|
|
369
|
+
off(event, handler) {
|
|
370
|
+
const handlers = this.listeners.get(event);
|
|
371
|
+
if (handlers) {
|
|
372
|
+
handlers.delete(handler);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
emit(event, ...args) {
|
|
376
|
+
const handlers = this.listeners.get(event);
|
|
377
|
+
if (handlers) {
|
|
378
|
+
for (const handler of handlers) {
|
|
379
|
+
try {
|
|
380
|
+
handler(...args);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
console.error(`Error in LightStream listener [${event}]:`, e);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/index.ts
|
|
390
|
+
class LightStreamClient {
|
|
391
|
+
rest;
|
|
392
|
+
stream;
|
|
393
|
+
constructor(options) {
|
|
394
|
+
this.rest = new LightStreamRestClient({
|
|
395
|
+
baseUrl: options.baseUrl || "https://gateway.lightstream.lat",
|
|
396
|
+
apiKey: options.apiKey,
|
|
397
|
+
jwtKey: options.jwtKey || options.token
|
|
398
|
+
});
|
|
399
|
+
this.stream = new LightStreamStreamClient(options);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function createLightStreamClient(options) {
|
|
403
|
+
return new LightStreamClient(options);
|
|
404
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LightStreamRestClient } from './rest-client.js';
|
|
2
|
+
import { LightStreamStreamClient } from './stream-client.js';
|
|
3
|
+
import type { ClientOptions } from './types.js';
|
|
4
|
+
export declare class LightStreamClient {
|
|
5
|
+
readonly rest: LightStreamRestClient;
|
|
6
|
+
readonly stream: LightStreamStreamClient;
|
|
7
|
+
constructor(options: ClientOptions);
|
|
8
|
+
}
|
|
9
|
+
export declare function createLightStreamClient(options: ClientOptions): LightStreamClient;
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
export * from './errors.js';
|
|
12
|
+
export * from './rest-client.js';
|
|
13
|
+
export * from './stream-client.js';
|