@thestatic-tv/dcl-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/dist/index.js ADDED
@@ -0,0 +1,516 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ GuideModule: () => GuideModule,
24
+ HeartbeatModule: () => HeartbeatModule,
25
+ InteractionsModule: () => InteractionsModule,
26
+ KEY_TYPE_CHANNEL: () => KEY_TYPE_CHANNEL,
27
+ KEY_TYPE_SCENE: () => KEY_TYPE_SCENE,
28
+ SessionModule: () => SessionModule,
29
+ StaticTVClient: () => StaticTVClient
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/utils/http.ts
34
+ async function request(url, options = {}) {
35
+ const { timeout = 1e4, ...fetchOptions } = options;
36
+ const controller = new AbortController();
37
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
38
+ try {
39
+ const response = await fetch(url, {
40
+ ...fetchOptions,
41
+ signal: controller.signal
42
+ });
43
+ if (!response.ok) {
44
+ const errorData = await response.json().catch(() => ({}));
45
+ throw new Error(errorData.error || `HTTP ${response.status}`);
46
+ }
47
+ return response.json();
48
+ } finally {
49
+ clearTimeout(timeoutId);
50
+ }
51
+ }
52
+
53
+ // src/modules/guide.ts
54
+ var GuideModule = class {
55
+ // 30 seconds
56
+ constructor(client) {
57
+ this.cachedChannels = null;
58
+ this.cacheTimestamp = 0;
59
+ this.cacheDuration = 3e4;
60
+ this.client = client;
61
+ }
62
+ /**
63
+ * Get all channels from thestatic.tv
64
+ * Results are cached for 30 seconds
65
+ */
66
+ async getChannels(forceRefresh = false) {
67
+ const now = Date.now();
68
+ if (!forceRefresh && this.cachedChannels && now - this.cacheTimestamp < this.cacheDuration) {
69
+ this.client.log("Returning cached channels");
70
+ return this.cachedChannels;
71
+ }
72
+ const response = await this.client.request("/guide");
73
+ this.cachedChannels = response.channels;
74
+ this.cacheTimestamp = now;
75
+ this.client.log(`Fetched ${response.channels.length} channels`);
76
+ return response.channels;
77
+ }
78
+ /**
79
+ * Get only live channels
80
+ */
81
+ async getLiveChannels(forceRefresh = false) {
82
+ const channels = await this.getChannels(forceRefresh);
83
+ return channels.filter((c) => c.isLive);
84
+ }
85
+ /**
86
+ * Get a specific channel by slug
87
+ */
88
+ async getChannel(slug) {
89
+ const channels = await this.getChannels();
90
+ return channels.find((c) => c.slug === slug);
91
+ }
92
+ /**
93
+ * Get VODs (videos on demand)
94
+ */
95
+ async getVods() {
96
+ const response = await this.client.request("/guide");
97
+ return response.vods;
98
+ }
99
+ /**
100
+ * Clear the channel cache
101
+ */
102
+ clearCache() {
103
+ this.cachedChannels = null;
104
+ this.cacheTimestamp = 0;
105
+ }
106
+ };
107
+
108
+ // src/utils/identity.ts
109
+ function getPlayerWallet() {
110
+ try {
111
+ const { getPlayer } = require("@dcl/sdk/src/players");
112
+ const player = getPlayer();
113
+ return player?.userId ?? null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+ function getPlayerDisplayName() {
119
+ try {
120
+ const { getPlayer } = require("@dcl/sdk/src/players");
121
+ const player = getPlayer();
122
+ return player?.name ?? null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ // src/modules/session.ts
129
+ var SessionModule = class {
130
+ constructor(client) {
131
+ this.sessionId = null;
132
+ this.heartbeatInterval = null;
133
+ this.isActive = false;
134
+ this.client = client;
135
+ }
136
+ /**
137
+ * Get the appropriate session endpoint based on key type
138
+ */
139
+ getEndpoint() {
140
+ return this.client.isLite ? "/scene-session" : "/session";
141
+ }
142
+ /**
143
+ * Start a new session
144
+ * Called automatically if autoStartSession is true
145
+ */
146
+ async startSession(metadata) {
147
+ if (this.isActive) {
148
+ this.client.log("Session already active");
149
+ return this.sessionId;
150
+ }
151
+ try {
152
+ const response = await this.client.request(this.getEndpoint(), {
153
+ method: "POST",
154
+ body: JSON.stringify({
155
+ action: "enter",
156
+ walletAddress: getPlayerWallet(),
157
+ dclDisplayName: getPlayerDisplayName(),
158
+ metadata
159
+ })
160
+ });
161
+ if (response.success && response.sessionId) {
162
+ this.sessionId = response.sessionId;
163
+ this.isActive = true;
164
+ this.startHeartbeat();
165
+ this.client.log(`Session started: ${this.sessionId}`);
166
+ return this.sessionId;
167
+ }
168
+ return null;
169
+ } catch (error) {
170
+ this.client.log(`Failed to start session: ${error}`);
171
+ return null;
172
+ }
173
+ }
174
+ /**
175
+ * Start the heartbeat interval
176
+ */
177
+ startHeartbeat() {
178
+ if (this.heartbeatInterval) return;
179
+ const interval = this.client.getConfig().sessionHeartbeatInterval || 3e4;
180
+ this.heartbeatInterval = setInterval(() => {
181
+ this.sendHeartbeat();
182
+ }, interval);
183
+ }
184
+ /**
185
+ * Send a session heartbeat
186
+ */
187
+ async sendHeartbeat() {
188
+ if (!this.sessionId || !this.isActive) return;
189
+ try {
190
+ await this.client.request(this.getEndpoint(), {
191
+ method: "POST",
192
+ body: JSON.stringify({
193
+ action: "heartbeat",
194
+ sessionId: this.sessionId
195
+ })
196
+ });
197
+ this.client.log("Session heartbeat sent");
198
+ } catch (error) {
199
+ this.client.log(`Session heartbeat failed: ${error}`);
200
+ }
201
+ }
202
+ /**
203
+ * End the current session
204
+ */
205
+ async endSession() {
206
+ if (!this.isActive) return;
207
+ if (this.heartbeatInterval) {
208
+ clearInterval(this.heartbeatInterval);
209
+ this.heartbeatInterval = null;
210
+ }
211
+ if (this.sessionId) {
212
+ try {
213
+ await this.client.request(this.getEndpoint(), {
214
+ method: "POST",
215
+ body: JSON.stringify({
216
+ action: "leave",
217
+ sessionId: this.sessionId
218
+ })
219
+ });
220
+ this.client.log("Session ended");
221
+ } catch (error) {
222
+ this.client.log(`Failed to end session: ${error}`);
223
+ }
224
+ }
225
+ this.sessionId = null;
226
+ this.isActive = false;
227
+ }
228
+ /**
229
+ * Get the current session ID
230
+ */
231
+ getSessionId() {
232
+ return this.sessionId;
233
+ }
234
+ /**
235
+ * Check if a session is currently active
236
+ */
237
+ isSessionActive() {
238
+ return this.isActive;
239
+ }
240
+ };
241
+
242
+ // src/modules/heartbeat.ts
243
+ var HeartbeatModule = class {
244
+ constructor(client) {
245
+ this.watchInterval = null;
246
+ this.currentChannel = null;
247
+ this.isWatching = false;
248
+ this.client = client;
249
+ }
250
+ /**
251
+ * Start tracking video watching for a channel
252
+ * Sends heartbeats every minute while watching
253
+ *
254
+ * @param channelSlug The slug of the channel being watched
255
+ */
256
+ startWatching(channelSlug) {
257
+ if (this.isWatching && this.currentChannel === channelSlug) {
258
+ this.client.log(`Already watching ${channelSlug}`);
259
+ return;
260
+ }
261
+ if (this.isWatching && this.currentChannel !== channelSlug) {
262
+ this.stopWatching();
263
+ }
264
+ this.currentChannel = channelSlug;
265
+ this.isWatching = true;
266
+ this.sendHeartbeat();
267
+ const interval = this.client.getConfig().watchHeartbeatInterval || 6e4;
268
+ this.watchInterval = setInterval(() => {
269
+ this.sendHeartbeat();
270
+ }, interval);
271
+ this.client.log(`Started watching ${channelSlug}`);
272
+ }
273
+ /**
274
+ * Stop tracking video watching
275
+ */
276
+ stopWatching() {
277
+ if (!this.isWatching) return;
278
+ if (this.watchInterval) {
279
+ clearInterval(this.watchInterval);
280
+ this.watchInterval = null;
281
+ }
282
+ this.client.log(`Stopped watching ${this.currentChannel}`);
283
+ this.currentChannel = null;
284
+ this.isWatching = false;
285
+ }
286
+ /**
287
+ * Send a watching heartbeat (1 heartbeat = 1 minute watched)
288
+ */
289
+ async sendHeartbeat() {
290
+ if (!this.currentChannel || !this.isWatching) return;
291
+ try {
292
+ const sessionId = this.client.session.getSessionId();
293
+ await this.client.request("/heartbeat", {
294
+ method: "POST",
295
+ body: JSON.stringify({
296
+ channelSlug: this.currentChannel,
297
+ walletAddress: getPlayerWallet(),
298
+ sessionId
299
+ })
300
+ });
301
+ this.client.log(`Heartbeat sent for ${this.currentChannel}`);
302
+ } catch (error) {
303
+ this.client.log(`Heartbeat failed: ${error}`);
304
+ }
305
+ }
306
+ /**
307
+ * Get the currently watched channel
308
+ */
309
+ getCurrentChannel() {
310
+ return this.currentChannel;
311
+ }
312
+ /**
313
+ * Check if currently watching
314
+ */
315
+ isCurrentlyWatching() {
316
+ return this.isWatching;
317
+ }
318
+ };
319
+
320
+ // src/modules/interactions.ts
321
+ var InteractionsModule = class {
322
+ constructor(client) {
323
+ this.client = client;
324
+ }
325
+ /**
326
+ * Like a channel
327
+ * Requires the user to be connected with a wallet
328
+ *
329
+ * @param channelSlug The slug of the channel to like
330
+ * @returns The interaction response or null if failed
331
+ */
332
+ async like(channelSlug) {
333
+ const walletAddress = getPlayerWallet();
334
+ if (!walletAddress) {
335
+ this.client.log("Cannot like: wallet not connected");
336
+ return null;
337
+ }
338
+ try {
339
+ const response = await this.client.request("/interact", {
340
+ method: "POST",
341
+ body: JSON.stringify({
342
+ action: "like",
343
+ channelSlug,
344
+ walletAddress
345
+ })
346
+ });
347
+ if (response.alreadyExists) {
348
+ this.client.log(`Already liked ${channelSlug}`);
349
+ } else {
350
+ this.client.log(`Liked ${channelSlug}`);
351
+ }
352
+ return response;
353
+ } catch (error) {
354
+ this.client.log(`Failed to like ${channelSlug}: ${error}`);
355
+ return null;
356
+ }
357
+ }
358
+ /**
359
+ * Follow a channel
360
+ * Requires the user to be connected with a wallet
361
+ *
362
+ * @param channelSlug The slug of the channel to follow
363
+ * @returns The interaction response or null if failed
364
+ */
365
+ async follow(channelSlug) {
366
+ const walletAddress = getPlayerWallet();
367
+ if (!walletAddress) {
368
+ this.client.log("Cannot follow: wallet not connected");
369
+ return null;
370
+ }
371
+ try {
372
+ const response = await this.client.request("/interact", {
373
+ method: "POST",
374
+ body: JSON.stringify({
375
+ action: "follow",
376
+ channelSlug,
377
+ walletAddress
378
+ })
379
+ });
380
+ if (response.alreadyExists) {
381
+ this.client.log(`Already following ${channelSlug}`);
382
+ } else {
383
+ this.client.log(`Followed ${channelSlug}`);
384
+ }
385
+ return response;
386
+ } catch (error) {
387
+ this.client.log(`Failed to follow ${channelSlug}: ${error}`);
388
+ return null;
389
+ }
390
+ }
391
+ };
392
+
393
+ // src/StaticTVClient.ts
394
+ var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
395
+ var KEY_TYPE_CHANNEL = "channel";
396
+ var KEY_TYPE_SCENE = "scene";
397
+ var StaticTVClient = class {
398
+ /**
399
+ * Create a new StaticTVClient
400
+ *
401
+ * @param config Configuration options
402
+ *
403
+ * @example
404
+ * ```typescript
405
+ * // Full access with channel key
406
+ * const staticTV = new StaticTVClient({
407
+ * apiKey: 'dclk_your_channel_key_here',
408
+ * debug: true
409
+ * });
410
+ *
411
+ * // Lite mode with scene key (visitors only)
412
+ * const staticTV = new StaticTVClient({
413
+ * apiKey: 'dcls_your_scene_key_here'
414
+ * });
415
+ * ```
416
+ */
417
+ constructor(config) {
418
+ if (!config.apiKey) {
419
+ throw new Error("StaticTVClient: apiKey is required");
420
+ }
421
+ if (config.apiKey.startsWith("dclk_")) {
422
+ this._keyType = KEY_TYPE_CHANNEL;
423
+ } else if (config.apiKey.startsWith("dcls_")) {
424
+ this._keyType = KEY_TYPE_SCENE;
425
+ } else {
426
+ throw new Error("StaticTVClient: invalid apiKey format. Must start with dclk_ or dcls_");
427
+ }
428
+ this.config = {
429
+ autoStartSession: true,
430
+ sessionHeartbeatInterval: 3e4,
431
+ watchHeartbeatInterval: 6e4,
432
+ debug: false,
433
+ ...config
434
+ };
435
+ this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
436
+ this.session = new SessionModule(this);
437
+ if (this._keyType === KEY_TYPE_CHANNEL) {
438
+ this.guide = new GuideModule(this);
439
+ this.heartbeat = new HeartbeatModule(this);
440
+ this.interactions = new InteractionsModule(this);
441
+ } else {
442
+ this.guide = null;
443
+ this.heartbeat = null;
444
+ this.interactions = null;
445
+ }
446
+ if (this.config.autoStartSession) {
447
+ this.session.startSession().catch((err) => {
448
+ this.log(`Auto-start session failed: ${err}`);
449
+ });
450
+ }
451
+ this.log(`StaticTVClient initialized (${this._keyType} mode)`);
452
+ }
453
+ /**
454
+ * Get the key type (channel or scene)
455
+ */
456
+ get keyType() {
457
+ return this._keyType;
458
+ }
459
+ /**
460
+ * Check if this is a lite (scene-only) client
461
+ */
462
+ get isLite() {
463
+ return this._keyType === KEY_TYPE_SCENE;
464
+ }
465
+ /**
466
+ * Make an authenticated API request
467
+ * @internal
468
+ */
469
+ async request(endpoint, options = {}) {
470
+ const url = `${this.baseUrl}${endpoint}`;
471
+ return request(url, {
472
+ ...options,
473
+ headers: {
474
+ "Content-Type": "application/json",
475
+ "x-dcl-api-key": this.config.apiKey,
476
+ ...options.headers
477
+ }
478
+ });
479
+ }
480
+ /**
481
+ * Log a message if debug is enabled
482
+ * @internal
483
+ */
484
+ log(message, ...args) {
485
+ if (this.config.debug) {
486
+ console.log(`[StaticTV] ${message}`, ...args);
487
+ }
488
+ }
489
+ /**
490
+ * Get the current configuration
491
+ * @internal
492
+ */
493
+ getConfig() {
494
+ return this.config;
495
+ }
496
+ /**
497
+ * Cleanup when done (call before scene unload)
498
+ */
499
+ async destroy() {
500
+ if (this.heartbeat) {
501
+ this.heartbeat.stopWatching();
502
+ }
503
+ await this.session.endSession();
504
+ this.log("StaticTVClient destroyed");
505
+ }
506
+ };
507
+ // Annotate the CommonJS export names for ESM import in node:
508
+ 0 && (module.exports = {
509
+ GuideModule,
510
+ HeartbeatModule,
511
+ InteractionsModule,
512
+ KEY_TYPE_CHANNEL,
513
+ KEY_TYPE_SCENE,
514
+ SessionModule,
515
+ StaticTVClient
516
+ });