@harmoni-org/sdk 0.0.1

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,2054 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var axios = require('axios');
6
+ var socket_ioClient = require('socket.io-client');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var axios__default = /*#__PURE__*/_interopDefault(axios);
11
+
12
+ var __defProp = Object.defineProperty;
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+
18
+ // src/core/errors/ApiError.ts
19
+ var ApiError = class _ApiError extends Error {
20
+ constructor(message, status, code, details) {
21
+ super(message);
22
+ this.isApiError = true;
23
+ this.name = "ApiError";
24
+ this.status = status;
25
+ this.code = code;
26
+ this.details = details;
27
+ if (Error.captureStackTrace) {
28
+ Error.captureStackTrace(this, _ApiError);
29
+ }
30
+ }
31
+ toJSON() {
32
+ return {
33
+ message: this.message,
34
+ code: this.code,
35
+ status: this.status,
36
+ details: this.details
37
+ };
38
+ }
39
+ static isApiError(error) {
40
+ return error instanceof _ApiError || error?.isApiError === true;
41
+ }
42
+ };
43
+ var NetworkError = class extends ApiError {
44
+ constructor(message = "Network error occurred") {
45
+ super(message, 0, "NETWORK_ERROR");
46
+ this.name = "NetworkError";
47
+ }
48
+ };
49
+ var TimeoutError = class extends ApiError {
50
+ constructor(message = "Request timeout") {
51
+ super(message, 408, "TIMEOUT_ERROR");
52
+ this.name = "TimeoutError";
53
+ }
54
+ };
55
+ var UnauthorizedError = class extends ApiError {
56
+ constructor(message = "Unauthorized") {
57
+ super(message, 401, "UNAUTHORIZED");
58
+ this.name = "UnauthorizedError";
59
+ }
60
+ };
61
+ var ForbiddenError = class extends ApiError {
62
+ constructor(message = "Forbidden") {
63
+ super(message, 403, "FORBIDDEN");
64
+ this.name = "ForbiddenError";
65
+ }
66
+ };
67
+ var NotFoundError = class extends ApiError {
68
+ constructor(message = "Resource not found") {
69
+ super(message, 404, "NOT_FOUND");
70
+ this.name = "NotFoundError";
71
+ }
72
+ };
73
+ var ValidationError = class extends ApiError {
74
+ constructor(message = "Validation failed", details) {
75
+ super(message, 422, "VALIDATION_ERROR", details);
76
+ this.name = "ValidationError";
77
+ }
78
+ };
79
+
80
+ // src/core/http/HttpClient.ts
81
+ var HttpClient = class {
82
+ constructor(config) {
83
+ this.config = config;
84
+ this.authToken = null;
85
+ this.retryCount = /* @__PURE__ */ new Map();
86
+ this.client = axios__default.default.create({
87
+ baseURL: config.baseURL,
88
+ timeout: config.timeout || 3e4,
89
+ headers: {
90
+ "Content-Type": "application/json",
91
+ ...config.headers
92
+ },
93
+ withCredentials: config.withCredentials || false
94
+ });
95
+ this.setupInterceptors();
96
+ }
97
+ setupInterceptors() {
98
+ this.client.interceptors.request.use(
99
+ (config) => {
100
+ const requestConfig = config;
101
+ if (this.authToken && !requestConfig.skipAuth) {
102
+ config.headers = config.headers || {};
103
+ config.headers.Authorization = `Bearer ${this.authToken}`;
104
+ }
105
+ return config;
106
+ },
107
+ (error) => {
108
+ return Promise.reject(this.handleError(error));
109
+ }
110
+ );
111
+ this.client.interceptors.response.use(
112
+ (response) => response,
113
+ async (error) => {
114
+ const originalRequest = error.config;
115
+ if (error.response?.status === 401 && !originalRequest._retry && this.refreshTokenFn) {
116
+ originalRequest._retry = true;
117
+ try {
118
+ const newToken = await this.refreshTokenFn();
119
+ this.setAuthToken(newToken);
120
+ if (!originalRequest.headers) {
121
+ originalRequest.headers = {};
122
+ }
123
+ originalRequest.headers.Authorization = `Bearer ${newToken}`;
124
+ return this.client.request(originalRequest);
125
+ } catch (refreshError) {
126
+ this.authToken = null;
127
+ return Promise.reject(this.handleError(refreshError));
128
+ }
129
+ }
130
+ if (this.shouldRetry(error, originalRequest)) {
131
+ return this.retryRequest(originalRequest);
132
+ }
133
+ return Promise.reject(this.handleError(error));
134
+ }
135
+ );
136
+ }
137
+ shouldRetry(error, config) {
138
+ if (!config || config.skipRetry) return false;
139
+ const retryConfig = this.config.retryConfig;
140
+ if (!retryConfig || !retryConfig.maxRetries) return false;
141
+ const method = config.method?.toUpperCase();
142
+ const safeToRetry = method === "GET" || method === "HEAD" || method === "OPTIONS";
143
+ if (!safeToRetry && !config._retry) {
144
+ return false;
145
+ }
146
+ const requestId = this.getRequestId(config);
147
+ const currentRetries = this.retryCount.get(requestId) || 0;
148
+ if (currentRetries >= retryConfig.maxRetries) {
149
+ this.retryCount.delete(requestId);
150
+ return false;
151
+ }
152
+ const retryableStatuses = retryConfig.retryableStatuses || [408, 429, 500, 502, 503, 504];
153
+ return error.response ? retryableStatuses.includes(error.response.status) : true;
154
+ }
155
+ async retryRequest(config) {
156
+ const requestId = this.getRequestId(config);
157
+ const currentRetries = this.retryCount.get(requestId) || 0;
158
+ this.retryCount.set(requestId, currentRetries + 1);
159
+ const delay = this.config.retryConfig?.retryDelay || 1e3;
160
+ const backoffDelay = delay * Math.pow(2, currentRetries);
161
+ await this.sleep(backoffDelay);
162
+ config._retryCount = currentRetries + 1;
163
+ return this.client.request(config);
164
+ }
165
+ getRequestId(config) {
166
+ return `${config.method}-${config.url}`;
167
+ }
168
+ sleep(ms) {
169
+ return new Promise((resolve) => setTimeout(resolve, ms));
170
+ }
171
+ handleError(error) {
172
+ if (ApiError.isApiError(error)) {
173
+ return error;
174
+ }
175
+ if (axios__default.default.isAxiosError(error)) {
176
+ const axiosError = error;
177
+ if (!axiosError.response) {
178
+ if (axiosError.code === "ECONNABORTED") {
179
+ return new TimeoutError();
180
+ }
181
+ return new NetworkError(axiosError.message);
182
+ }
183
+ const status = axiosError.response.status;
184
+ const data = axiosError.response.data;
185
+ const message = data?.message || axiosError.message;
186
+ switch (status) {
187
+ case 401:
188
+ return new UnauthorizedError(message);
189
+ case 403:
190
+ return new ForbiddenError(message);
191
+ case 404:
192
+ return new NotFoundError(message);
193
+ case 422:
194
+ return new ValidationError(message, data);
195
+ default:
196
+ return new ApiError(message, status, data?.code, data);
197
+ }
198
+ }
199
+ return new ApiError(error instanceof Error ? error.message : "An unknown error occurred", 500);
200
+ }
201
+ // Public methods
202
+ setAuthToken(token) {
203
+ this.authToken = token;
204
+ }
205
+ getAuthToken() {
206
+ return this.authToken;
207
+ }
208
+ setRefreshTokenFunction(fn) {
209
+ this.refreshTokenFn = fn;
210
+ }
211
+ addRequestInterceptor(interceptor) {
212
+ return this.client.interceptors.request.use(
213
+ interceptor.onFulfilled,
214
+ interceptor.onRejected
215
+ );
216
+ }
217
+ addResponseInterceptor(interceptor) {
218
+ return this.client.interceptors.response.use(interceptor.onFulfilled, interceptor.onRejected);
219
+ }
220
+ removeRequestInterceptor(id) {
221
+ this.client.interceptors.request.eject(id);
222
+ }
223
+ removeResponseInterceptor(id) {
224
+ this.client.interceptors.response.eject(id);
225
+ }
226
+ // HTTP methods
227
+ async get(url, config) {
228
+ const response = await this.client.get(url, config);
229
+ return response.data;
230
+ }
231
+ async post(url, data, config) {
232
+ const requestConfig = { ...config };
233
+ if (data instanceof FormData) {
234
+ requestConfig.headers = {
235
+ ...requestConfig.headers,
236
+ "Content-Type": "multipart/form-data"
237
+ };
238
+ }
239
+ const response = await this.client.post(url, data, requestConfig);
240
+ return response.data;
241
+ }
242
+ async put(url, data, config) {
243
+ const requestConfig = { ...config };
244
+ if (data instanceof FormData) {
245
+ requestConfig.headers = {
246
+ ...requestConfig.headers,
247
+ "Content-Type": "multipart/form-data"
248
+ };
249
+ }
250
+ const response = await this.client.put(url, data, requestConfig);
251
+ return response.data;
252
+ }
253
+ async patch(url, data, config) {
254
+ const requestConfig = { ...config };
255
+ if (data instanceof FormData) {
256
+ requestConfig.headers = {
257
+ ...requestConfig.headers,
258
+ "Content-Type": "multipart/form-data"
259
+ };
260
+ }
261
+ const response = await this.client.patch(url, data, requestConfig);
262
+ return response.data;
263
+ }
264
+ async delete(url, config) {
265
+ const response = await this.client.delete(url, config);
266
+ return response.data;
267
+ }
268
+ };
269
+
270
+ // src/modules/auth/AuthModule.ts
271
+ var AuthModule = class {
272
+ constructor(http) {
273
+ this.http = http;
274
+ }
275
+ /**
276
+ * Authenticates the user by calling the login endpoint.
277
+ * @param credentials - The login request payload containing emailOrUsername and password.
278
+ * @returns The logged-in user information including the access token.
279
+ */
280
+ async login(credentials) {
281
+ const response = await this.http.post("/auth/login", credentials, {
282
+ skipAuth: true
283
+ });
284
+ const { user, accessToken, refreshToken } = response;
285
+ this.http.setAuthToken(accessToken);
286
+ const normalizedUser = {
287
+ ...user,
288
+ id: user.userId || user.id,
289
+ token: accessToken,
290
+ refreshToken
291
+ };
292
+ return normalizedUser;
293
+ }
294
+ /**
295
+ * Registers a new user by calling the registration endpoint.
296
+ * @param registrationData - The registration request payload containing username, password, and optionally email.
297
+ * @returns The newly created user information including the access token.
298
+ */
299
+ async register(data) {
300
+ const response = await this.http.post("/auth/register", data, {
301
+ skipAuth: true
302
+ });
303
+ const { user, accessToken, refreshToken } = response;
304
+ this.http.setAuthToken(accessToken);
305
+ const normalizedUser = {
306
+ ...user,
307
+ id: user.userId || user.id,
308
+ token: accessToken,
309
+ refreshToken
310
+ };
311
+ return normalizedUser;
312
+ }
313
+ /**
314
+ * Verifies if the provided JWT token is valid.
315
+ * @returns The user information if the token is valid.
316
+ */
317
+ async verifyToken() {
318
+ const response = await this.http.get("/auth/verify-token");
319
+ if (response.user && response.user.userId) {
320
+ response.user.id = response.user.userId;
321
+ }
322
+ return response;
323
+ }
324
+ /**
325
+ * Refreshes the access token using the stored refresh token.
326
+ * @returns The newly issued access token.
327
+ */
328
+ async refreshAccessToken() {
329
+ const response = await this.http.get("/auth/refresh-token");
330
+ const { accessToken } = response;
331
+ this.http.setAuthToken(accessToken);
332
+ return accessToken;
333
+ }
334
+ /**
335
+ * Checks if the provided email is unique (not already registered).
336
+ * @param email - The email address to check.
337
+ * @returns True if the email is unique, otherwise false.
338
+ */
339
+ async isEmailUnique(email) {
340
+ const response = await this.http.get(`/auth/check-email/${email}`, {
341
+ skipAuth: true
342
+ });
343
+ return response.isUnique;
344
+ }
345
+ /**
346
+ * Checks if the provided username is unique (not already taken).
347
+ * @param username - The username to check.
348
+ * @returns True if the username is unique, otherwise false.
349
+ */
350
+ async isUsernameUnique(username) {
351
+ const response = await this.http.get(
352
+ `/auth/check-username/${username}`,
353
+ { skipAuth: true }
354
+ );
355
+ return response.isUnique;
356
+ }
357
+ /**
358
+ * Logout the current user
359
+ */
360
+ async logout() {
361
+ this.http.setAuthToken(null);
362
+ }
363
+ /**
364
+ * Request password reset
365
+ */
366
+ async requestPasswordReset(email) {
367
+ await this.http.post("/auth/password/reset-request", { email }, { skipAuth: true });
368
+ }
369
+ /**
370
+ * Reset password with token
371
+ */
372
+ async resetPassword(token, newPassword) {
373
+ await this.http.post("/auth/password/reset", { token, newPassword }, { skipAuth: true });
374
+ }
375
+ /**
376
+ * Change password for authenticated user
377
+ */
378
+ async changePassword(currentPassword, newPassword) {
379
+ await this.http.post("/auth/password/change", {
380
+ currentPassword,
381
+ newPassword
382
+ });
383
+ }
384
+ /**
385
+ * Verify email with token
386
+ */
387
+ async verifyEmail(token) {
388
+ await this.http.post("/auth/email/verify", { token }, { skipAuth: true });
389
+ }
390
+ /**
391
+ * Resend verification email
392
+ */
393
+ async resendVerificationEmail(email) {
394
+ await this.http.post("/auth/email/resend", { email }, { skipAuth: true });
395
+ }
396
+ };
397
+
398
+ // src/modules/user/UserModule.ts
399
+ var UserModule = class {
400
+ constructor(http) {
401
+ this.http = http;
402
+ }
403
+ /**
404
+ * Get user by ID
405
+ */
406
+ async getById(userId) {
407
+ return this.http.get(`/users/${userId}`);
408
+ }
409
+ /**
410
+ * Get current user profile
411
+ */
412
+ async getCurrentUser() {
413
+ return this.http.get("/users/me");
414
+ }
415
+ /**
416
+ * Update current user profile
417
+ */
418
+ async updateProfile(data) {
419
+ return this.http.patch("/users/me", data);
420
+ }
421
+ /**
422
+ * Delete current user account
423
+ */
424
+ async deleteAccount() {
425
+ await this.http.delete("/users/me");
426
+ }
427
+ /**
428
+ * Get list of users with pagination and filters
429
+ */
430
+ async list(params) {
431
+ return this.http.get("/users", { params });
432
+ }
433
+ /**
434
+ * Search users by query
435
+ */
436
+ async search(query, params) {
437
+ return this.http.get("/users/search", {
438
+ params: { query, ...params }
439
+ });
440
+ }
441
+ /**
442
+ * Upload user avatar
443
+ */
444
+ async uploadAvatar(file, fileName) {
445
+ const formData = new FormData();
446
+ formData.append("avatar", file, fileName);
447
+ return this.http.post("/users/me/avatar", formData);
448
+ }
449
+ /**
450
+ * Delete user avatar
451
+ */
452
+ async deleteAvatar() {
453
+ await this.http.delete("/users/me/avatar");
454
+ }
455
+ };
456
+
457
+ // src/modules/watchTogether/WatchTogetherError.ts
458
+ var WatchTogetherError = class _WatchTogetherError extends Error {
459
+ constructor(message, code, details) {
460
+ super(message);
461
+ this.code = code;
462
+ this.details = details;
463
+ this.isWatchTogetherError = true;
464
+ this.name = "WatchTogetherError";
465
+ if (Error.captureStackTrace) {
466
+ Error.captureStackTrace(this, _WatchTogetherError);
467
+ }
468
+ }
469
+ toJSON() {
470
+ return {
471
+ name: this.name,
472
+ message: this.message,
473
+ code: this.code,
474
+ details: this.details
475
+ };
476
+ }
477
+ static isWatchTogetherError(error) {
478
+ return error instanceof _WatchTogetherError || error?.isWatchTogetherError === true;
479
+ }
480
+ };
481
+
482
+ // src/types/actions.ts
483
+ var USER_ACTIONS = {
484
+ // Core playback actions (synced with Watch Together)
485
+ PLAY: "PLAY",
486
+ PAUSE: "PAUSE",
487
+ SEEK: "SEEK",
488
+ // File/media actions
489
+ FILE_UPDATE: "FILE_UPDATE",
490
+ // Advanced actions (may not be synced)
491
+ STOP: "STOP",
492
+ VOLUME_UPDATE: "VOLUME_UPDATE",
493
+ ENTER_FULLSCREEN: "ENTER_FULLSCREEN",
494
+ EXIT_FULLSCREEN: "EXIT_FULLSCREEN",
495
+ SPEED_UPDATE: "SPEED_UPDATE",
496
+ AUDIO_DELAY_UPDATE: "AUDIO_DELAY_UPDATE",
497
+ SUBTITLE_DELAY_UPDATE: "SUBTITLE_DELAY_UPDATE",
498
+ ASPECT_RATIO_UPDATE: "ASPECT_RATIO_UPDATE",
499
+ TOGGLE_REPEAT: "TOGGLE_REPEAT",
500
+ TOGGLE_LOOP: "TOGGLE_LOOP",
501
+ VIDEO_EFFECT_UPDATE: "VIDEO_EFFECT_UPDATE",
502
+ AUDIO_FILTER_UPDATE: "AUDIO_FILTER_UPDATE",
503
+ TOGGLE_RANDOM: "TOGGLE_RANDOM",
504
+ API_VERSION_UPDATE: "API_VERSION_UPDATE",
505
+ PLAYLIST_UPDATE: "PLAYLIST_UPDATE",
506
+ TITLE_OR_CHAPTER_UPDATE: "TITLE_OR_CHAPTER_UPDATE",
507
+ NO_CHANGE: "NO_CHANGE"
508
+ };
509
+
510
+ // src/types/watchTogether.ts
511
+ var SyncActions = {
512
+ PLAY: "play",
513
+ PAUSE: "pause",
514
+ SEEK: "seek"
515
+ };
516
+ var WatchTogetherErrorCodes = {
517
+ ROOM_NOT_FOUND: "ROOM_NOT_FOUND",
518
+ UNAUTHORIZED: "UNAUTHORIZED",
519
+ ROOM_FULL: "ROOM_FULL",
520
+ INVALID_PASSWORD: "INVALID_PASSWORD",
521
+ NOT_IN_ROOM: "NOT_IN_ROOM",
522
+ PERMISSION_DENIED: "PERMISSION_DENIED",
523
+ CONNECTION_ERROR: "CONNECTION_ERROR",
524
+ ALREADY_IN_ROOM: "ALREADY_IN_ROOM"
525
+ };
526
+ var WatchTogetherConstants = {
527
+ NAMESPACE: "/watch-together",
528
+ SYNC_CHECK_INTERVAL_MS: 3e4,
529
+ // 30 seconds
530
+ SYNC_TOLERANCE_SECONDS: 1,
531
+ // Time difference tolerance
532
+ MAX_RECONNECT_ATTEMPTS: 5,
533
+ RECONNECT_DELAY_MS: 2e3
534
+ };
535
+ var WatchTogetherIDs = {
536
+ JOIN_ROOM_INPUT: "join-room-code",
537
+ JOIN_AND_INVITE_SECTION: "join-invite-section",
538
+ JOIN_BUTTON: "join-room-button",
539
+ START_BUTTON: "watchtogether-start-button",
540
+ LEAVE_BUTTON: "watchtogether-leave-button",
541
+ JOINED_ROOM_SECTION: "joined-room-section",
542
+ ACTION_SECTION: "watchtogether-action-section",
543
+ CREATE_ROOM_BUTTON: "watchtogether-create-room-button"
544
+ };
545
+ var SECTION_NAMES = {
546
+ JOIN_AND_INVITE: "JOIN_AND_INVITE",
547
+ JOINED_ROOM: "JOINED_ROOM"
548
+ };
549
+ var WatchTogetherMessages = {
550
+ START_SUCCESS: "WatchTogether session started successfully!",
551
+ JOIN_SUCCESS: "Joined the WatchTogether session!",
552
+ LEAVE_SUCCESS: "You have left the WatchTogether session.",
553
+ INVALID_ARGS: "Missing required arguments. Please check the usage.",
554
+ CONNECTION_SUCCESS: "Connected to WatchTogether service.",
555
+ DISCONNECTED: "Disconnected from WatchTogether service."
556
+ };
557
+
558
+ // src/modules/watchTogether/WatchTogetherModule.ts
559
+ var WatchTogetherModule = class {
560
+ // Flag for persistent connection
561
+ constructor(http, config) {
562
+ this.http = http;
563
+ this.config = config;
564
+ this.socket = null;
565
+ this.currentRoom = null;
566
+ this.syncInterval = null;
567
+ this.eventListeners = /* @__PURE__ */ new Map();
568
+ this.connectionStatus = {
569
+ connected: false,
570
+ connecting: false,
571
+ authenticated: false,
572
+ inRoom: false,
573
+ roomId: null,
574
+ error: null
575
+ };
576
+ this.shouldStayConnected = false;
577
+ if (config.autoConnect || config.persistentConnection) {
578
+ this.initializePersistentConnection();
579
+ }
580
+ }
581
+ /**
582
+ * Initialize persistent connection (always on)
583
+ */
584
+ initializePersistentConnection() {
585
+ this.shouldStayConnected = true;
586
+ this.connect().catch(() => {
587
+ setTimeout(() => {
588
+ if (this.shouldStayConnected) {
589
+ this.initializePersistentConnection();
590
+ }
591
+ }, 2e3);
592
+ });
593
+ }
594
+ /**
595
+ * Connect to the Watch Together service
596
+ */
597
+ async connect() {
598
+ if (this.socket?.connected) {
599
+ this.updateConnectionStatus({ connected: true, connecting: false });
600
+ return;
601
+ }
602
+ const isPersistent = this.config.persistentConnection || this.shouldStayConnected;
603
+ const token = this.http.getAuthToken();
604
+ if (!token && !isPersistent) {
605
+ throw new WatchTogetherError("Authentication required. Please login first.", "UNAUTHORIZED");
606
+ }
607
+ this.updateConnectionStatus({ connecting: true, error: null });
608
+ const namespace = this.config.namespace || WatchTogetherConstants.NAMESPACE;
609
+ const url = `${this.config.baseURL}${namespace}`;
610
+ const socketOptions = {
611
+ query: { connectionId: "watchTogether" },
612
+ forceNew: !this.socket,
613
+ // Reuse socket if exists
614
+ reconnection: true,
615
+ reconnectionAttempts: Infinity,
616
+ // Infinite reconnection attempts
617
+ reconnectionDelay: 1e3,
618
+ reconnectionDelayMax: 5e3,
619
+ timeout: 1e4
620
+ };
621
+ if (token) {
622
+ socketOptions.extraHeaders = {
623
+ Authorization: `Bearer ${token}`
624
+ };
625
+ this.updateConnectionStatus({ authenticated: true });
626
+ }
627
+ this.socket = socket_ioClient.io(url, socketOptions);
628
+ this.setupSocketListeners();
629
+ return new Promise((resolve, reject) => {
630
+ const timeout = setTimeout(() => {
631
+ if (!isPersistent) {
632
+ reject(new WatchTogetherError("Connection timeout", "CONNECTION_ERROR"));
633
+ } else {
634
+ resolve();
635
+ }
636
+ }, 1e4);
637
+ this.socket.once("connect", () => {
638
+ clearTimeout(timeout);
639
+ this.updateConnectionStatus({
640
+ connected: true,
641
+ connecting: false,
642
+ error: null
643
+ });
644
+ this.emit("connect", void 0);
645
+ resolve();
646
+ });
647
+ this.socket.once("connect_error", (error) => {
648
+ clearTimeout(timeout);
649
+ this.updateConnectionStatus({
650
+ connected: false,
651
+ connecting: false,
652
+ error: error.message
653
+ });
654
+ if (!isPersistent) {
655
+ reject(new WatchTogetherError(error.message || "Connection failed", "CONNECTION_ERROR"));
656
+ } else {
657
+ resolve();
658
+ }
659
+ });
660
+ });
661
+ }
662
+ /**
663
+ * Disconnect from the Watch Together service
664
+ */
665
+ disconnect() {
666
+ this.shouldStayConnected = false;
667
+ this.stopSyncCheck();
668
+ if (this.socket) {
669
+ this.socket.removeAllListeners();
670
+ this.socket.disconnect();
671
+ this.socket = null;
672
+ }
673
+ this.currentRoom = null;
674
+ this.updateConnectionStatus({
675
+ connected: false,
676
+ connecting: false,
677
+ authenticated: false,
678
+ inRoom: false,
679
+ roomId: null
680
+ });
681
+ this.emit("disconnect", "manual");
682
+ }
683
+ /**
684
+ * Enable persistent connection mode (always on)
685
+ */
686
+ enablePersistentConnection() {
687
+ if (!this.shouldStayConnected) {
688
+ this.shouldStayConnected = true;
689
+ this.initializePersistentConnection();
690
+ }
691
+ }
692
+ /**
693
+ * Disable persistent connection mode
694
+ */
695
+ disablePersistentConnection() {
696
+ this.shouldStayConnected = false;
697
+ }
698
+ /**
699
+ * Create a new room
700
+ */
701
+ async createRoom(options) {
702
+ this.ensureConnected();
703
+ return new Promise((resolve, reject) => {
704
+ const timeout = setTimeout(() => {
705
+ reject(new WatchTogetherError("Create room timeout", "CONNECTION_ERROR"));
706
+ }, 5e3);
707
+ this.socket.once("roomState", (room) => {
708
+ clearTimeout(timeout);
709
+ this.currentRoom = room;
710
+ this.startSyncCheck();
711
+ resolve(room);
712
+ });
713
+ this.socket.once("error", (error) => {
714
+ clearTimeout(timeout);
715
+ reject(new WatchTogetherError(error.message, error.code));
716
+ });
717
+ this.socket.emit("createRoom", options || {});
718
+ });
719
+ }
720
+ /**
721
+ * Join an existing room or create a new one
722
+ */
723
+ async joinRoom(options) {
724
+ this.ensureConnected();
725
+ return new Promise((resolve, reject) => {
726
+ const timeout = setTimeout(() => {
727
+ reject(new WatchTogetherError("Join room timeout", "CONNECTION_ERROR"));
728
+ }, 5e3);
729
+ this.socket.once("roomState", (room) => {
730
+ clearTimeout(timeout);
731
+ this.currentRoom = room;
732
+ this.updateConnectionStatus({
733
+ inRoom: true,
734
+ roomId: room.roomId
735
+ });
736
+ this.startSyncCheck();
737
+ resolve(room);
738
+ });
739
+ this.socket.once("error", (error) => {
740
+ clearTimeout(timeout);
741
+ reject(new WatchTogetherError(error.message, error.code));
742
+ });
743
+ this.socket.emit("joinRoom", options);
744
+ });
745
+ }
746
+ /**
747
+ * Leave the current room
748
+ */
749
+ leaveRoom() {
750
+ if (!this.currentRoom) {
751
+ return;
752
+ }
753
+ this.ensureConnected();
754
+ this.stopSyncCheck();
755
+ this.socket.emit("leaveRoom", { roomId: this.currentRoom.roomId });
756
+ this.currentRoom = null;
757
+ this.updateConnectionStatus({
758
+ inRoom: false,
759
+ roomId: null
760
+ });
761
+ }
762
+ /**
763
+ * Play the video (synced to all users)
764
+ */
765
+ play() {
766
+ this.ensureInRoom();
767
+ this.updateSyncState("play");
768
+ }
769
+ /**
770
+ * Pause the video (synced to all users)
771
+ */
772
+ pause() {
773
+ this.ensureInRoom();
774
+ this.updateSyncState("pause");
775
+ }
776
+ /**
777
+ * Seek to a specific time (synced to all users)
778
+ */
779
+ seek(timeInSeconds) {
780
+ this.ensureInRoom();
781
+ this.updateSyncState("seek", timeInSeconds);
782
+ }
783
+ /**
784
+ * Request sync state from server
785
+ */
786
+ requestSync() {
787
+ this.ensureInRoom();
788
+ this.socket.emit("checkSync", { roomId: this.currentRoom.roomId });
789
+ }
790
+ /**
791
+ * Update current file information
792
+ */
793
+ updateFileInfo(fileInfo) {
794
+ this.ensureInRoom();
795
+ this.socket.emit("updateFileInfo", {
796
+ roomId: this.currentRoom.roomId,
797
+ fileInfo
798
+ });
799
+ }
800
+ /**
801
+ * Send a chat message
802
+ */
803
+ async sendMessage(text) {
804
+ this.ensureInRoom();
805
+ const tempMessageId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
806
+ return new Promise((resolve, reject) => {
807
+ const timeout = setTimeout(() => {
808
+ reject(new WatchTogetherError("Send message timeout", "CONNECTION_ERROR"));
809
+ }, 5e3);
810
+ this.socket.once("chat:ack", () => {
811
+ clearTimeout(timeout);
812
+ resolve();
813
+ });
814
+ this.socket.once("chat:error", (error) => {
815
+ clearTimeout(timeout);
816
+ reject(new WatchTogetherError(error.message, "CHAT_ERROR"));
817
+ });
818
+ this.socket.emit("chat:send", {
819
+ roomId: this.currentRoom.roomId,
820
+ text,
821
+ tempMessageId
822
+ });
823
+ });
824
+ }
825
+ /**
826
+ * Get chat history for current room
827
+ */
828
+ getChatHistory() {
829
+ return this.currentRoom?.chat || [];
830
+ }
831
+ /**
832
+ * Get current room state
833
+ */
834
+ getCurrentRoom() {
835
+ return this.currentRoom;
836
+ }
837
+ /**
838
+ * Check if connected to the service
839
+ */
840
+ isConnected() {
841
+ return this.socket?.connected || false;
842
+ }
843
+ /**
844
+ * Check if currently in a room
845
+ */
846
+ isInRoom() {
847
+ return this.currentRoom !== null;
848
+ }
849
+ /**
850
+ * Get current connection status
851
+ */
852
+ getConnectionStatus() {
853
+ return { ...this.connectionStatus };
854
+ }
855
+ /**
856
+ * Check if user is online (connected to socket)
857
+ */
858
+ isOnline() {
859
+ return this.connectionStatus.connected;
860
+ }
861
+ // ============================================
862
+ // EVENT LISTENERS
863
+ // ============================================
864
+ /**
865
+ * Listen for room state events
866
+ */
867
+ onRoomState(callback) {
868
+ return this.addEventListener("roomState", callback);
869
+ }
870
+ /**
871
+ * Listen for room update events
872
+ */
873
+ onRoomUpdate(callback) {
874
+ return this.addEventListener("roomUpdates", callback);
875
+ }
876
+ /**
877
+ * Listen for sync state events
878
+ */
879
+ onSyncState(callback) {
880
+ return this.addEventListener("syncState", callback);
881
+ }
882
+ /**
883
+ * Listen for chat messages
884
+ */
885
+ onChatMessage(callback) {
886
+ return this.addEventListener("chat:receive", callback);
887
+ }
888
+ /**
889
+ * Listen for system messages
890
+ */
891
+ onSystemMessage(callback) {
892
+ return this.addEventListener("newMessage", callback);
893
+ }
894
+ /**
895
+ * Listen for chat history
896
+ */
897
+ onChatHistory(callback) {
898
+ return this.addEventListener("chatHistory", callback);
899
+ }
900
+ /**
901
+ * Listen for errors
902
+ */
903
+ onError(callback) {
904
+ return this.addEventListener("error", callback);
905
+ }
906
+ /**
907
+ * Listen for connection events
908
+ */
909
+ onConnect(callback) {
910
+ return this.addEventListener("connect", callback);
911
+ }
912
+ /**
913
+ * Listen for disconnection events
914
+ */
915
+ onDisconnect(callback) {
916
+ return this.addEventListener("disconnect", callback);
917
+ }
918
+ /**
919
+ * Listen for connection status changes
920
+ */
921
+ onConnectionStatusChange(callback) {
922
+ return this.addEventListener("connectionStatusChange", callback);
923
+ }
924
+ /**
925
+ * Listen for online/offline status
926
+ */
927
+ onOnlineStatusChange(callback) {
928
+ return this.addEventListener("onlineStatusChange", callback);
929
+ }
930
+ // ============================================
931
+ // PRIVATE METHODS
932
+ // ============================================
933
+ setupSocketListeners() {
934
+ if (!this.socket) return;
935
+ this.socket.on("roomState", (room) => {
936
+ this.currentRoom = room;
937
+ this.updateConnectionStatus({
938
+ inRoom: true,
939
+ roomId: room.roomId
940
+ });
941
+ this.emit("roomState", room);
942
+ });
943
+ this.socket.on("roomUpdates", (update) => {
944
+ if (this.currentRoom && update.roomUpdates) {
945
+ this.currentRoom = {
946
+ ...this.currentRoom,
947
+ ...update.roomUpdates
948
+ };
949
+ }
950
+ this.emit("roomUpdates", update);
951
+ });
952
+ this.socket.on("syncState", (syncState) => {
953
+ if (this.currentRoom) {
954
+ this.currentRoom.syncState = syncState;
955
+ }
956
+ this.emit("syncState", syncState);
957
+ });
958
+ this.socket.on("chat:receive", (data) => {
959
+ if (this.currentRoom) {
960
+ this.currentRoom.chat.push(data.message);
961
+ }
962
+ this.emit("chat:receive", data.message);
963
+ });
964
+ this.socket.on("newMessage", (data) => {
965
+ const message = { ...data.message, systemMessage: true };
966
+ if (this.currentRoom) {
967
+ this.currentRoom.chat.push(message);
968
+ }
969
+ this.emit("newMessage", message);
970
+ });
971
+ this.socket.on("chatHistory", (data) => {
972
+ if (this.currentRoom) {
973
+ this.currentRoom.chat = data.messages;
974
+ }
975
+ this.emit("chatHistory", data.messages);
976
+ });
977
+ this.socket.on("chat:ack", (data) => {
978
+ this.emit("chat:ack", data.message);
979
+ });
980
+ this.socket.on("chat:error", (data) => {
981
+ this.emit("chat:error", data);
982
+ });
983
+ this.socket.on("error", (data) => {
984
+ this.updateConnectionStatus({ error: data.message });
985
+ this.emit("error", data);
986
+ });
987
+ this.socket.on("connect", () => {
988
+ this.updateConnectionStatus({
989
+ connected: true,
990
+ connecting: false,
991
+ error: null
992
+ });
993
+ });
994
+ this.socket.on("disconnect", (reason) => {
995
+ this.stopSyncCheck();
996
+ this.updateConnectionStatus({
997
+ connected: false,
998
+ error: `Disconnected: ${reason}`
999
+ });
1000
+ this.emit("disconnect", reason);
1001
+ if (this.shouldStayConnected) {
1002
+ setTimeout(() => {
1003
+ if (this.shouldStayConnected && !this.socket?.connected) {
1004
+ this.connect().catch(() => {
1005
+ });
1006
+ }
1007
+ }, 2e3);
1008
+ }
1009
+ });
1010
+ this.socket.on("connect_error", async (error) => {
1011
+ this.updateConnectionStatus({
1012
+ connected: false,
1013
+ connecting: false,
1014
+ error: error.message
1015
+ });
1016
+ if (error.message.includes("Unauthorized") || error.message.includes("401")) {
1017
+ try {
1018
+ await this.refreshTokenAndReconnect();
1019
+ } catch {
1020
+ this.emit("error", {
1021
+ code: "UNAUTHORIZED",
1022
+ message: "Session expired. Please login again."
1023
+ });
1024
+ }
1025
+ }
1026
+ if (this.shouldStayConnected) {
1027
+ setTimeout(() => {
1028
+ if (this.shouldStayConnected && !this.socket?.connected) {
1029
+ this.connect().catch(() => {
1030
+ });
1031
+ }
1032
+ }, 3e3);
1033
+ }
1034
+ });
1035
+ this.socket.on("reconnect_attempt", (attempt) => {
1036
+ this.updateConnectionStatus({
1037
+ connecting: true,
1038
+ error: `Reconnecting (attempt ${attempt})...`
1039
+ });
1040
+ });
1041
+ this.socket.on("reconnect", (_attempt) => {
1042
+ this.updateConnectionStatus({
1043
+ connected: true,
1044
+ connecting: false,
1045
+ error: null
1046
+ });
1047
+ this.emit("connect", void 0);
1048
+ });
1049
+ this.socket.on("reconnect_failed", () => {
1050
+ this.updateConnectionStatus({
1051
+ connected: false,
1052
+ connecting: false,
1053
+ error: "Reconnection failed"
1054
+ });
1055
+ if (this.shouldStayConnected) {
1056
+ setTimeout(() => {
1057
+ if (this.shouldStayConnected) {
1058
+ this.connect().catch(() => {
1059
+ });
1060
+ }
1061
+ }, 5e3);
1062
+ }
1063
+ });
1064
+ }
1065
+ async refreshTokenAndReconnect() {
1066
+ if (!this.shouldStayConnected) {
1067
+ this.disconnect();
1068
+ throw new WatchTogetherError("Session expired. Please login again.", "UNAUTHORIZED");
1069
+ }
1070
+ this.updateConnectionStatus({ authenticated: false });
1071
+ }
1072
+ /**
1073
+ * Update authentication token after refresh
1074
+ */
1075
+ updateAuthToken(token) {
1076
+ if (this.socket && this.socket.io.opts.extraHeaders) {
1077
+ this.socket.io.opts.extraHeaders.Authorization = `Bearer ${token}`;
1078
+ this.updateConnectionStatus({ authenticated: true, error: null });
1079
+ if (!this.socket.connected) {
1080
+ this.socket.connect();
1081
+ }
1082
+ }
1083
+ }
1084
+ /**
1085
+ * Update connection status and emit events
1086
+ */
1087
+ updateConnectionStatus(updates) {
1088
+ const previousOnlineStatus = this.connectionStatus.connected;
1089
+ this.connectionStatus = {
1090
+ ...this.connectionStatus,
1091
+ ...updates
1092
+ };
1093
+ this.emit("connectionStatusChange", this.connectionStatus);
1094
+ if (previousOnlineStatus !== this.connectionStatus.connected) {
1095
+ this.emit("onlineStatusChange", this.connectionStatus.connected);
1096
+ }
1097
+ }
1098
+ updateSyncState(action, value) {
1099
+ this.ensureInRoom();
1100
+ const payload = {
1101
+ roomId: this.currentRoom.roomId,
1102
+ action
1103
+ };
1104
+ if (action === "seek" && value !== void 0) {
1105
+ payload.value = value;
1106
+ }
1107
+ this.socket.emit("updateSyncState", payload);
1108
+ }
1109
+ startSyncCheck() {
1110
+ this.stopSyncCheck();
1111
+ this.syncInterval = setInterval(() => {
1112
+ if (this.currentRoom && this.socket?.connected) {
1113
+ this.socket.emit("checkSync", { roomId: this.currentRoom.roomId });
1114
+ }
1115
+ }, WatchTogetherConstants.SYNC_CHECK_INTERVAL_MS);
1116
+ }
1117
+ stopSyncCheck() {
1118
+ if (this.syncInterval) {
1119
+ clearInterval(this.syncInterval);
1120
+ this.syncInterval = null;
1121
+ }
1122
+ }
1123
+ ensureConnected() {
1124
+ if (!this.socket?.connected) {
1125
+ throw new WatchTogetherError(
1126
+ "Not connected. Please call connect() first.",
1127
+ "CONNECTION_ERROR"
1128
+ );
1129
+ }
1130
+ }
1131
+ ensureInRoom() {
1132
+ this.ensureConnected();
1133
+ if (!this.currentRoom) {
1134
+ throw new WatchTogetherError(
1135
+ "Not in a room. Please join or create a room first.",
1136
+ "NOT_IN_ROOM"
1137
+ );
1138
+ }
1139
+ }
1140
+ addEventListener(event, callback) {
1141
+ if (!this.eventListeners.has(event)) {
1142
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
1143
+ }
1144
+ this.eventListeners.get(event).add(callback);
1145
+ return () => {
1146
+ const listeners = this.eventListeners.get(event);
1147
+ if (listeners) {
1148
+ listeners.delete(callback);
1149
+ }
1150
+ };
1151
+ }
1152
+ emit(event, data) {
1153
+ const listeners = this.eventListeners.get(event);
1154
+ if (listeners) {
1155
+ listeners.forEach((callback) => {
1156
+ try {
1157
+ callback(data);
1158
+ } catch (error) {
1159
+ console.error(`Error in event listener for ${event}:`, error);
1160
+ }
1161
+ });
1162
+ }
1163
+ }
1164
+ };
1165
+
1166
+ // src/sdk/HarmoniSDK.ts
1167
+ var HarmoniSDK = class {
1168
+ constructor(config) {
1169
+ this.httpClient = new HttpClient(config);
1170
+ if (config.autoRefreshToken) {
1171
+ this.setupAutoRefresh();
1172
+ }
1173
+ this.auth = new AuthModule(this.httpClient);
1174
+ this.user = new UserModule(this.httpClient);
1175
+ this.watchTogether = new WatchTogetherModule(this.httpClient, {
1176
+ baseURL: config.baseURL,
1177
+ namespace: config.watchTogetherNamespace,
1178
+ persistentConnection: config.persistentConnection
1179
+ });
1180
+ }
1181
+ /**
1182
+ * Setup automatic token refresh
1183
+ */
1184
+ setupAutoRefresh() {
1185
+ this.httpClient.setRefreshTokenFunction(async () => {
1186
+ const accessToken = await this.auth.refreshAccessToken();
1187
+ this.storeAccessToken(accessToken);
1188
+ return accessToken;
1189
+ });
1190
+ }
1191
+ /**
1192
+ * Set authentication token
1193
+ */
1194
+ setAuthToken(token) {
1195
+ this.httpClient.setAuthToken(token);
1196
+ }
1197
+ /**
1198
+ * Get current authentication token
1199
+ */
1200
+ getAuthToken() {
1201
+ return this.httpClient.getAuthToken();
1202
+ }
1203
+ /**
1204
+ * Store access token (override this method or use storage)
1205
+ */
1206
+ storeAccessToken(accessToken) {
1207
+ if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
1208
+ const storage = globalThis.localStorage;
1209
+ storage?.setItem("harmoni_access_token", accessToken);
1210
+ }
1211
+ }
1212
+ /**
1213
+ * Get the underlying HTTP client (for advanced usage)
1214
+ */
1215
+ getHttpClient() {
1216
+ return this.httpClient;
1217
+ }
1218
+ };
1219
+
1220
+ // src/types/player.ts
1221
+ var PlayerConstants = {
1222
+ DEFAULT_POLLING_INTERVAL_MS: 2e3,
1223
+ SEEK_THRESHOLD_SECONDS: 0.5,
1224
+ COMMAND_STACK_TIMEOUT_MS: 3e3,
1225
+ MIN_INTERVAL_MS: 100
1226
+ };
1227
+
1228
+ // src/utils/string.ts
1229
+ var string_exports = {};
1230
+ __export(string_exports, {
1231
+ camelToSnake: () => camelToSnake,
1232
+ capitalize: () => capitalize,
1233
+ isValidEmail: () => isValidEmail,
1234
+ randomString: () => randomString,
1235
+ slugify: () => slugify,
1236
+ snakeToCamel: () => snakeToCamel,
1237
+ titleCase: () => titleCase,
1238
+ truncate: () => truncate
1239
+ });
1240
+ function capitalize(str) {
1241
+ if (!str) return "";
1242
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
1243
+ }
1244
+ function titleCase(str) {
1245
+ if (!str) return "";
1246
+ return str.toLowerCase().split(" ").map((word) => capitalize(word)).join(" ");
1247
+ }
1248
+ function camelToSnake(str) {
1249
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
1250
+ }
1251
+ function snakeToCamel(str) {
1252
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1253
+ }
1254
+ function truncate(str, length, suffix = "...") {
1255
+ if (!str || str.length <= length) return str;
1256
+ return str.slice(0, length - suffix.length) + suffix;
1257
+ }
1258
+ function isValidEmail(email) {
1259
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1260
+ return emailRegex.test(email);
1261
+ }
1262
+ function randomString(length) {
1263
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1264
+ let result = "";
1265
+ for (let i = 0; i < length; i++) {
1266
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
1267
+ }
1268
+ return result;
1269
+ }
1270
+ function slugify(str) {
1271
+ return str.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1272
+ }
1273
+
1274
+ // src/utils/date.ts
1275
+ var date_exports = {};
1276
+ __export(date_exports, {
1277
+ addDays: () => addDays,
1278
+ addHours: () => addHours,
1279
+ formatDate: () => formatDate,
1280
+ isFuture: () => isFuture,
1281
+ isPast: () => isPast,
1282
+ isToday: () => isToday,
1283
+ isValidDate: () => isValidDate,
1284
+ timeAgo: () => timeAgo,
1285
+ toISOString: () => toISOString
1286
+ });
1287
+ function toISOString(date) {
1288
+ return new Date(date).toISOString();
1289
+ }
1290
+ function isValidDate(date) {
1291
+ return date instanceof Date && !isNaN(date.getTime());
1292
+ }
1293
+ function timeAgo(date) {
1294
+ const now = /* @__PURE__ */ new Date();
1295
+ const past = new Date(date);
1296
+ const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1e3);
1297
+ if (diffInSeconds < 60) return "just now";
1298
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
1299
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
1300
+ if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`;
1301
+ if (diffInSeconds < 2592e3) return `${Math.floor(diffInSeconds / 604800)} weeks ago`;
1302
+ if (diffInSeconds < 31536e3) return `${Math.floor(diffInSeconds / 2592e3)} months ago`;
1303
+ return `${Math.floor(diffInSeconds / 31536e3)} years ago`;
1304
+ }
1305
+ function addDays(date, days) {
1306
+ const result = new Date(date);
1307
+ result.setDate(result.getDate() + days);
1308
+ return result;
1309
+ }
1310
+ function addHours(date, hours) {
1311
+ const result = new Date(date);
1312
+ result.setHours(result.getHours() + hours);
1313
+ return result;
1314
+ }
1315
+ function formatDate(date, format = "YYYY-MM-DD") {
1316
+ const d = new Date(date);
1317
+ const year = d.getFullYear();
1318
+ const month = String(d.getMonth() + 1).padStart(2, "0");
1319
+ const day = String(d.getDate()).padStart(2, "0");
1320
+ const hours = String(d.getHours()).padStart(2, "0");
1321
+ const minutes = String(d.getMinutes()).padStart(2, "0");
1322
+ const seconds = String(d.getSeconds()).padStart(2, "0");
1323
+ return format.replace("YYYY", String(year)).replace("MM", month).replace("DD", day).replace("HH", hours).replace("mm", minutes).replace("ss", seconds);
1324
+ }
1325
+ function isToday(date) {
1326
+ const today = /* @__PURE__ */ new Date();
1327
+ const checkDate = new Date(date);
1328
+ return checkDate.getDate() === today.getDate() && checkDate.getMonth() === today.getMonth() && checkDate.getFullYear() === today.getFullYear();
1329
+ }
1330
+ function isPast(date) {
1331
+ return new Date(date).getTime() < Date.now();
1332
+ }
1333
+ function isFuture(date) {
1334
+ return new Date(date).getTime() > Date.now();
1335
+ }
1336
+
1337
+ // src/utils/object.ts
1338
+ var object_exports = {};
1339
+ __export(object_exports, {
1340
+ deepClone: () => deepClone,
1341
+ deepMerge: () => deepMerge,
1342
+ getNestedValue: () => getNestedValue,
1343
+ isEmpty: () => isEmpty,
1344
+ isObject: () => isObject,
1345
+ omit: () => omit,
1346
+ pick: () => pick,
1347
+ setNestedValue: () => setNestedValue
1348
+ });
1349
+ function deepClone(obj) {
1350
+ if (obj === null || typeof obj !== "object") return obj;
1351
+ if (obj instanceof Date) return new Date(obj.getTime());
1352
+ if (obj instanceof Array) return obj.map((item) => deepClone(item));
1353
+ if (obj instanceof Object) {
1354
+ const clonedObj = {};
1355
+ for (const key in obj) {
1356
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
1357
+ clonedObj[key] = deepClone(obj[key]);
1358
+ }
1359
+ }
1360
+ return clonedObj;
1361
+ }
1362
+ return obj;
1363
+ }
1364
+ function deepMerge(target, source) {
1365
+ const output = { ...target };
1366
+ if (isObject(target) && isObject(source)) {
1367
+ Object.keys(source).forEach((key) => {
1368
+ const sourceValue = source[key];
1369
+ const targetValue = target[key];
1370
+ if (isObject(sourceValue) && isObject(targetValue)) {
1371
+ output[key] = deepMerge(
1372
+ targetValue,
1373
+ sourceValue
1374
+ );
1375
+ } else {
1376
+ output[key] = sourceValue;
1377
+ }
1378
+ });
1379
+ }
1380
+ return output;
1381
+ }
1382
+ function isObject(value) {
1383
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1384
+ }
1385
+ function pick(obj, keys) {
1386
+ const result = {};
1387
+ keys.forEach((key) => {
1388
+ if (key in obj) {
1389
+ result[key] = obj[key];
1390
+ }
1391
+ });
1392
+ return result;
1393
+ }
1394
+ function omit(obj, keys) {
1395
+ const result = { ...obj };
1396
+ keys.forEach((key) => {
1397
+ delete result[key];
1398
+ });
1399
+ return result;
1400
+ }
1401
+ function isEmpty(obj) {
1402
+ if (obj === null || obj === void 0) return true;
1403
+ if (typeof obj === "string" || Array.isArray(obj)) return obj.length === 0;
1404
+ if (typeof obj === "object") return Object.keys(obj).length === 0;
1405
+ return false;
1406
+ }
1407
+ function getNestedValue(obj, path) {
1408
+ const keys = path.split(".");
1409
+ let result = obj;
1410
+ for (const key of keys) {
1411
+ if (result && typeof result === "object" && key in result) {
1412
+ result = result[key];
1413
+ } else {
1414
+ return void 0;
1415
+ }
1416
+ }
1417
+ return result;
1418
+ }
1419
+ function setNestedValue(obj, path, value) {
1420
+ const keys = path.split(".");
1421
+ const lastKey = keys.pop();
1422
+ if (!lastKey) return;
1423
+ let current = obj;
1424
+ for (const key of keys) {
1425
+ if (!(key in current) || typeof current[key] !== "object") {
1426
+ current[key] = {};
1427
+ }
1428
+ current = current[key];
1429
+ }
1430
+ current[lastKey] = value;
1431
+ }
1432
+
1433
+ // src/utils/validation.ts
1434
+ var validation_exports = {};
1435
+ __export(validation_exports, {
1436
+ maxLength: () => maxLength,
1437
+ minLength: () => minLength,
1438
+ required: () => required,
1439
+ validateEmail: () => validateEmail,
1440
+ validatePassword: () => validatePassword,
1441
+ validatePhone: () => validatePhone,
1442
+ validateUrl: () => validateUrl
1443
+ });
1444
+ function validateEmail(email) {
1445
+ if (!email) {
1446
+ return { valid: false, message: "Email is required" };
1447
+ }
1448
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1449
+ if (!emailRegex.test(email)) {
1450
+ return { valid: false, message: "Invalid email format" };
1451
+ }
1452
+ return { valid: true };
1453
+ }
1454
+ function validatePassword(password, options = {}) {
1455
+ const {
1456
+ minLength: minLength2 = 8,
1457
+ requireUppercase = true,
1458
+ requireLowercase = true,
1459
+ requireNumbers = true,
1460
+ requireSpecialChars = true
1461
+ } = options;
1462
+ if (!password) {
1463
+ return { valid: false, message: "Password is required" };
1464
+ }
1465
+ if (password.length < minLength2) {
1466
+ return { valid: false, message: `Password must be at least ${minLength2} characters` };
1467
+ }
1468
+ if (requireUppercase && !/[A-Z]/.test(password)) {
1469
+ return { valid: false, message: "Password must contain at least one uppercase letter" };
1470
+ }
1471
+ if (requireLowercase && !/[a-z]/.test(password)) {
1472
+ return { valid: false, message: "Password must contain at least one lowercase letter" };
1473
+ }
1474
+ if (requireNumbers && !/\d/.test(password)) {
1475
+ return { valid: false, message: "Password must contain at least one number" };
1476
+ }
1477
+ if (requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
1478
+ return { valid: false, message: "Password must contain at least one special character" };
1479
+ }
1480
+ return { valid: true };
1481
+ }
1482
+ function validateUrl(url) {
1483
+ if (!url) {
1484
+ return { valid: false, message: "URL is required" };
1485
+ }
1486
+ try {
1487
+ new URL(url);
1488
+ return { valid: true };
1489
+ } catch {
1490
+ return { valid: false, message: "Invalid URL format" };
1491
+ }
1492
+ }
1493
+ function validatePhone(phone) {
1494
+ if (!phone) {
1495
+ return { valid: false, message: "Phone number is required" };
1496
+ }
1497
+ const phoneRegex = /^[+]?[(]?[0-9]{1,4}[)]?[-\s.]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{1,9}$/;
1498
+ if (!phoneRegex.test(phone)) {
1499
+ return { valid: false, message: "Invalid phone number format" };
1500
+ }
1501
+ return { valid: true };
1502
+ }
1503
+ function required(value, fieldName = "Field") {
1504
+ if (value === null || value === void 0 || value === "") {
1505
+ return { valid: false, message: `${fieldName} is required` };
1506
+ }
1507
+ return { valid: true };
1508
+ }
1509
+ function minLength(value, min, fieldName = "Field") {
1510
+ if (value.length < min) {
1511
+ return { valid: false, message: `${fieldName} must be at least ${min} characters` };
1512
+ }
1513
+ return { valid: true };
1514
+ }
1515
+ function maxLength(value, max, fieldName = "Field") {
1516
+ if (value.length > max) {
1517
+ return { valid: false, message: `${fieldName} must be at most ${max} characters` };
1518
+ }
1519
+ return { valid: true };
1520
+ }
1521
+
1522
+ // src/utils/storage.ts
1523
+ var isBrowser = typeof globalThis !== "undefined" && "localStorage" in globalThis;
1524
+ var Storage = class {
1525
+ constructor(type = "local") {
1526
+ this.memoryStorage = /* @__PURE__ */ new Map();
1527
+ if (isBrowser) {
1528
+ const global = globalThis;
1529
+ this.storage = type === "local" ? global.localStorage ?? null : global.sessionStorage ?? null;
1530
+ } else {
1531
+ this.storage = null;
1532
+ }
1533
+ }
1534
+ /**
1535
+ * Set item in storage
1536
+ */
1537
+ set(key, value) {
1538
+ try {
1539
+ const serialized = JSON.stringify(value);
1540
+ if (this.storage) {
1541
+ this.storage.setItem(key, serialized);
1542
+ } else {
1543
+ this.memoryStorage.set(key, serialized);
1544
+ }
1545
+ } catch (error) {
1546
+ if (process.env.NODE_ENV !== "production") {
1547
+ console.error(`Error saving to storage:`, error);
1548
+ }
1549
+ }
1550
+ }
1551
+ /**
1552
+ * Get item from storage
1553
+ */
1554
+ get(key) {
1555
+ try {
1556
+ let item;
1557
+ if (this.storage) {
1558
+ item = this.storage.getItem(key);
1559
+ } else {
1560
+ item = this.memoryStorage.get(key);
1561
+ }
1562
+ if (!item) return null;
1563
+ return JSON.parse(item);
1564
+ } catch (error) {
1565
+ if (process.env.NODE_ENV !== "production") {
1566
+ console.error(`Error reading from storage:`, error);
1567
+ }
1568
+ return null;
1569
+ }
1570
+ }
1571
+ /**
1572
+ * Remove item from storage
1573
+ */
1574
+ remove(key) {
1575
+ try {
1576
+ if (this.storage) {
1577
+ this.storage.removeItem(key);
1578
+ } else {
1579
+ this.memoryStorage.delete(key);
1580
+ }
1581
+ } catch (error) {
1582
+ if (process.env.NODE_ENV !== "production") {
1583
+ console.error(`Error removing from storage:`, error);
1584
+ }
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Clear all items from storage
1589
+ */
1590
+ clear() {
1591
+ try {
1592
+ if (this.storage) {
1593
+ this.storage.clear();
1594
+ } else {
1595
+ this.memoryStorage.clear();
1596
+ }
1597
+ } catch (error) {
1598
+ if (process.env.NODE_ENV !== "production") {
1599
+ console.error(`Error clearing storage:`, error);
1600
+ }
1601
+ }
1602
+ }
1603
+ /**
1604
+ * Check if key exists in storage
1605
+ */
1606
+ has(key) {
1607
+ if (this.storage) {
1608
+ return this.storage.getItem(key) !== null;
1609
+ } else {
1610
+ return this.memoryStorage.has(key);
1611
+ }
1612
+ }
1613
+ /**
1614
+ * Get all keys from storage
1615
+ */
1616
+ keys() {
1617
+ if (this.storage) {
1618
+ return Object.keys(this.storage);
1619
+ } else {
1620
+ return Array.from(this.memoryStorage.keys());
1621
+ }
1622
+ }
1623
+ /**
1624
+ * Check if storage is available (browser environment)
1625
+ */
1626
+ isAvailable() {
1627
+ return this.storage !== null;
1628
+ }
1629
+ };
1630
+ var localStorage = new Storage("local");
1631
+ var sessionStorage = new Storage("session");
1632
+
1633
+ // src/player/stateDetection.ts
1634
+ function identifyUserActions(oldState, newState, intervalMs) {
1635
+ const actions = [];
1636
+ if (!oldState || !newState) return [];
1637
+ const normalizedInterval = Math.max(intervalMs, PlayerConstants.MIN_INTERVAL_MS);
1638
+ if (oldState.isPlaying !== newState.isPlaying) {
1639
+ actions.push({
1640
+ event: newState.isPlaying ? USER_ACTIONS.PLAY : USER_ACTIONS.PAUSE,
1641
+ value: {
1642
+ isPlaying: newState.isPlaying,
1643
+ currentTime: newState.currentTime
1644
+ }
1645
+ });
1646
+ }
1647
+ const timeDifference = Math.abs(oldState.currentTime - newState.currentTime);
1648
+ const expectedTimeAdvance = normalizedInterval / 1e3 * (newState.isPlaying ? 1 : 0);
1649
+ const seekThreshold = Math.max(PlayerConstants.SEEK_THRESHOLD_SECONDS, expectedTimeAdvance * 1.5);
1650
+ if (timeDifference > expectedTimeAdvance + seekThreshold) {
1651
+ actions.push({
1652
+ event: USER_ACTIONS.SEEK,
1653
+ value: { currentTime: newState.currentTime }
1654
+ });
1655
+ }
1656
+ if (oldState.filename !== newState.filename) {
1657
+ actions.push({
1658
+ event: USER_ACTIONS.FILE_UPDATE,
1659
+ value: {
1660
+ filename: newState.filename === "no-input" ? null : newState.filename
1661
+ }
1662
+ });
1663
+ }
1664
+ if (oldState.loop !== newState.loop) {
1665
+ actions.push({
1666
+ event: USER_ACTIONS.TOGGLE_LOOP,
1667
+ value: { loop: newState.loop }
1668
+ });
1669
+ }
1670
+ if (oldState.repeat !== newState.repeat) {
1671
+ actions.push({
1672
+ event: USER_ACTIONS.TOGGLE_REPEAT,
1673
+ value: { repeat: newState.repeat }
1674
+ });
1675
+ }
1676
+ if (oldState.title !== newState.title || oldState.chapter !== newState.chapter) {
1677
+ actions.push({
1678
+ event: USER_ACTIONS.TITLE_OR_CHAPTER_UPDATE,
1679
+ value: { title: newState.title, chapter: newState.chapter }
1680
+ });
1681
+ }
1682
+ return actions;
1683
+ }
1684
+ function normalizePlayerState(state) {
1685
+ return {
1686
+ isPlaying: state.isPlaying ?? false,
1687
+ currentTime: Number.isFinite(state.currentTime) ? state.currentTime : 0,
1688
+ loop: state.loop ?? false,
1689
+ repeat: state.repeat ?? false,
1690
+ title: state.title ?? "",
1691
+ chapter: state.chapter ?? "",
1692
+ filename: state.filename ?? "",
1693
+ duration: Number.isFinite(state.duration) ? state.duration : 0,
1694
+ filepath: state.filepath ?? ""
1695
+ };
1696
+ }
1697
+
1698
+ // src/player/PlayerStatusMonitor.ts
1699
+ var PlayerStatusMonitor = class {
1700
+ constructor(player, onStateChange, onStop, pollingIntervalMs = PlayerConstants.DEFAULT_POLLING_INTERVAL_MS) {
1701
+ this.player = player;
1702
+ this.onStateChange = onStateChange;
1703
+ this.onStop = onStop;
1704
+ this.pollingIntervalMs = pollingIntervalMs;
1705
+ this.lastKnownState = null;
1706
+ this.lastCheckTime = 0;
1707
+ this.pollingTimer = null;
1708
+ this.isRunning = false;
1709
+ }
1710
+ /**
1711
+ * Starts monitoring the player status.
1712
+ */
1713
+ start() {
1714
+ if (this.isRunning) {
1715
+ return;
1716
+ }
1717
+ this.isRunning = true;
1718
+ this.lastCheckTime = Date.now();
1719
+ this.checkStatus();
1720
+ }
1721
+ /**
1722
+ * Stops monitoring the player status.
1723
+ */
1724
+ stop() {
1725
+ this.isRunning = false;
1726
+ if (this.pollingTimer) {
1727
+ clearTimeout(this.pollingTimer);
1728
+ this.pollingTimer = null;
1729
+ }
1730
+ }
1731
+ /**
1732
+ * Check if monitor is currently running.
1733
+ */
1734
+ getIsRunning() {
1735
+ return this.isRunning;
1736
+ }
1737
+ /**
1738
+ * Get the last known player state.
1739
+ */
1740
+ getLastKnownState() {
1741
+ return this.lastKnownState;
1742
+ }
1743
+ /**
1744
+ * Manually update the last known state (used when app initiates actions).
1745
+ */
1746
+ updateLastKnownState(state) {
1747
+ this.lastKnownState = state;
1748
+ }
1749
+ async checkStatus() {
1750
+ if (!this.isRunning) {
1751
+ return;
1752
+ }
1753
+ try {
1754
+ const isRunning = await this.player.checkPlayerRunning();
1755
+ if (!isRunning) {
1756
+ this.isRunning = false;
1757
+ this.onStop();
1758
+ return;
1759
+ }
1760
+ const currentTime = Date.now();
1761
+ const timeSinceLastCheck = this.lastCheckTime > 0 ? currentTime - this.lastCheckTime : this.pollingIntervalMs;
1762
+ this.lastCheckTime = currentTime;
1763
+ const currentState = await this.player.getStatus();
1764
+ if (this.lastKnownState) {
1765
+ const detectedActions = identifyUserActions(
1766
+ this.lastKnownState,
1767
+ currentState,
1768
+ timeSinceLastCheck
1769
+ );
1770
+ if (detectedActions.length > 0) {
1771
+ const source = this.determineActionSource(detectedActions);
1772
+ this.onStateChange(detectedActions, source);
1773
+ }
1774
+ }
1775
+ this.lastKnownState = currentState;
1776
+ if (this.isRunning) {
1777
+ this.pollingTimer = setTimeout(() => this.checkStatus(), this.pollingIntervalMs);
1778
+ }
1779
+ } catch (error) {
1780
+ console.error("Error fetching player status:", error);
1781
+ this.isRunning = false;
1782
+ this.onStop();
1783
+ }
1784
+ }
1785
+ /**
1786
+ * Determines if the action came from app sync or user interaction.
1787
+ * Should be overridden to integrate with command tracking.
1788
+ */
1789
+ determineActionSource(_actions) {
1790
+ return "user";
1791
+ }
1792
+ };
1793
+
1794
+ // src/player/HTML5VideoController.ts
1795
+ var HTML5VideoController = class {
1796
+ constructor(videoElementId) {
1797
+ this.videoElementId = videoElementId;
1798
+ this.video = null;
1799
+ this.statusMonitor = null;
1800
+ }
1801
+ async start(onReady) {
1802
+ if (typeof document === "undefined") {
1803
+ throw new Error("HTML5VideoController requires a browser environment");
1804
+ }
1805
+ this.video = document.getElementById(this.videoElementId);
1806
+ if (!this.video) {
1807
+ throw new Error(`Video element with id "${this.videoElementId}" not found`);
1808
+ }
1809
+ if (this.video.readyState >= 2) {
1810
+ onReady?.();
1811
+ } else {
1812
+ this.video.addEventListener("canplay", () => onReady?.(), { once: true });
1813
+ }
1814
+ }
1815
+ async play() {
1816
+ if (!this.video) {
1817
+ throw new Error("Player not initialized");
1818
+ }
1819
+ await this.video.play();
1820
+ }
1821
+ async pause() {
1822
+ if (!this.video) {
1823
+ throw new Error("Player not initialized");
1824
+ }
1825
+ this.video.pause();
1826
+ }
1827
+ async stop() {
1828
+ if (this.video) {
1829
+ this.video.pause();
1830
+ this.video.currentTime = 0;
1831
+ }
1832
+ }
1833
+ async seek(timeInSeconds) {
1834
+ if (!this.video) {
1835
+ throw new Error("Player not initialized");
1836
+ }
1837
+ this.video.currentTime = timeInSeconds;
1838
+ }
1839
+ async loadMedia(source) {
1840
+ if (!this.video) {
1841
+ throw new Error("Player not initialized");
1842
+ }
1843
+ this.video.src = source;
1844
+ this.video.load();
1845
+ }
1846
+ async getStatus() {
1847
+ if (!this.video) {
1848
+ throw new Error("Player not initialized");
1849
+ }
1850
+ return {
1851
+ isPlaying: !this.video.paused && !this.video.ended,
1852
+ currentTime: this.video.currentTime,
1853
+ loop: this.video.loop,
1854
+ repeat: false,
1855
+ title: "",
1856
+ chapter: "",
1857
+ filename: this.video.src.split("/").pop() || "",
1858
+ duration: Number.isFinite(this.video.duration) ? this.video.duration : 0,
1859
+ filepath: this.video.src
1860
+ };
1861
+ }
1862
+ async checkPlayerRunning() {
1863
+ return this.video !== null && this.video.readyState >= 2;
1864
+ }
1865
+ async quit() {
1866
+ if (this.video) {
1867
+ this.video.pause();
1868
+ this.video.src = "";
1869
+ }
1870
+ this.statusMonitor?.stop();
1871
+ }
1872
+ observeStatusChange(onChange, onStop, intervalMs = 500) {
1873
+ this.statusMonitor?.stop();
1874
+ this.statusMonitor = new PlayerStatusMonitor(this, onChange, onStop, intervalMs);
1875
+ this.statusMonitor.start();
1876
+ }
1877
+ async displayMessage(message, durationSeconds = 5) {
1878
+ console.log(`[Player OSD] ${message} (${durationSeconds}s)`);
1879
+ }
1880
+ /**
1881
+ * Get the underlying HTMLVideoElement (for advanced usage).
1882
+ */
1883
+ getVideoElement() {
1884
+ return this.video;
1885
+ }
1886
+ };
1887
+
1888
+ // src/player/SyncedPlayer.ts
1889
+ var SyncedPlayer = class {
1890
+ constructor(player, watchTogether, options) {
1891
+ this.player = player;
1892
+ this.watchTogether = watchTogether;
1893
+ this.options = options;
1894
+ this.commandStack = [];
1895
+ this.currentUserId = null;
1896
+ this.unsubscribers = [];
1897
+ this.currentUserId = options.getCurrentUserId();
1898
+ this.setupListeners();
1899
+ }
1900
+ /**
1901
+ * Setup listeners for sync and player changes.
1902
+ */
1903
+ setupListeners() {
1904
+ const unsubRoomUpdate = this.watchTogether.onRoomUpdate((update) => {
1905
+ this.handleRemoteSyncUpdate(update);
1906
+ });
1907
+ this.unsubscribers.push(unsubRoomUpdate);
1908
+ this.player.observeStatusChange(
1909
+ (actions, _source) => this.handleLocalPlayerChange(actions),
1910
+ () => {
1911
+ this.options.onPlayerStopped?.();
1912
+ }
1913
+ );
1914
+ }
1915
+ /**
1916
+ * Handle sync updates from other users.
1917
+ */
1918
+ async handleRemoteSyncUpdate(update) {
1919
+ const { action, userId } = update.metadata;
1920
+ if (userId === this.currentUserId) {
1921
+ return;
1922
+ }
1923
+ this.commandStack.push({ action, timestamp: Date.now() });
1924
+ try {
1925
+ switch (action) {
1926
+ case "play":
1927
+ await this.player.play();
1928
+ break;
1929
+ case "pause":
1930
+ await this.player.pause();
1931
+ break;
1932
+ case "seek": {
1933
+ const time = update.roomUpdates.syncState?.time;
1934
+ if (time !== void 0) {
1935
+ await this.player.seek(time);
1936
+ }
1937
+ break;
1938
+ }
1939
+ }
1940
+ } catch (error) {
1941
+ console.error("Error applying remote sync update:", error);
1942
+ }
1943
+ }
1944
+ /**
1945
+ * Handle local player changes and broadcast to room.
1946
+ */
1947
+ handleLocalPlayerChange(actions) {
1948
+ for (const action of actions) {
1949
+ const isFromSync = this.popMatchingCommand(action.event);
1950
+ if (isFromSync) {
1951
+ continue;
1952
+ }
1953
+ try {
1954
+ switch (action.event) {
1955
+ case USER_ACTIONS.PLAY:
1956
+ this.watchTogether.play();
1957
+ break;
1958
+ case USER_ACTIONS.PAUSE:
1959
+ this.watchTogether.pause();
1960
+ break;
1961
+ case USER_ACTIONS.SEEK: {
1962
+ const time = action.value?.currentTime;
1963
+ if (time !== void 0) {
1964
+ this.watchTogether.seek(time);
1965
+ }
1966
+ break;
1967
+ }
1968
+ case USER_ACTIONS.FILE_UPDATE: {
1969
+ const filename = action.value?.filename;
1970
+ if (filename) {
1971
+ }
1972
+ break;
1973
+ }
1974
+ }
1975
+ } catch (error) {
1976
+ console.error("Error broadcasting player action:", error);
1977
+ }
1978
+ }
1979
+ }
1980
+ /**
1981
+ * Check if a matching command exists in the stack (from sync).
1982
+ * Returns true if found (and removes it from stack).
1983
+ */
1984
+ popMatchingCommand(event) {
1985
+ const now = Date.now();
1986
+ this.commandStack = this.commandStack.filter(
1987
+ (cmd) => now - cmd.timestamp < PlayerConstants.COMMAND_STACK_TIMEOUT_MS
1988
+ );
1989
+ const index = this.commandStack.findIndex(
1990
+ (cmd) => cmd.action === this.eventToAction(event) && now - cmd.timestamp < PlayerConstants.COMMAND_STACK_TIMEOUT_MS
1991
+ );
1992
+ if (index !== -1) {
1993
+ this.commandStack.splice(index, 1);
1994
+ return true;
1995
+ }
1996
+ return false;
1997
+ }
1998
+ /**
1999
+ * Map user action event to sync action name.
2000
+ */
2001
+ eventToAction(event) {
2002
+ const mapping = {
2003
+ [USER_ACTIONS.PLAY]: "play",
2004
+ [USER_ACTIONS.PAUSE]: "pause",
2005
+ [USER_ACTIONS.SEEK]: "seek"
2006
+ };
2007
+ return mapping[event] || event.toLowerCase();
2008
+ }
2009
+ /**
2010
+ * Cleanup listeners.
2011
+ */
2012
+ destroy() {
2013
+ this.unsubscribers.forEach((unsub) => unsub());
2014
+ this.unsubscribers = [];
2015
+ this.commandStack = [];
2016
+ }
2017
+ };
2018
+
2019
+ exports.ApiError = ApiError;
2020
+ exports.AuthModule = AuthModule;
2021
+ exports.ForbiddenError = ForbiddenError;
2022
+ exports.HTML5VideoController = HTML5VideoController;
2023
+ exports.HarmoniSDK = HarmoniSDK;
2024
+ exports.HttpClient = HttpClient;
2025
+ exports.NetworkError = NetworkError;
2026
+ exports.NotFoundError = NotFoundError;
2027
+ exports.PlayerConstants = PlayerConstants;
2028
+ exports.PlayerStatusMonitor = PlayerStatusMonitor;
2029
+ exports.SECTION_NAMES = SECTION_NAMES;
2030
+ exports.Storage = Storage;
2031
+ exports.SyncActions = SyncActions;
2032
+ exports.SyncedPlayer = SyncedPlayer;
2033
+ exports.TimeoutError = TimeoutError;
2034
+ exports.USER_ACTIONS = USER_ACTIONS;
2035
+ exports.UnauthorizedError = UnauthorizedError;
2036
+ exports.UserModule = UserModule;
2037
+ exports.ValidationError = ValidationError;
2038
+ exports.WatchTogetherConstants = WatchTogetherConstants;
2039
+ exports.WatchTogetherError = WatchTogetherError;
2040
+ exports.WatchTogetherErrorCodes = WatchTogetherErrorCodes;
2041
+ exports.WatchTogetherIDs = WatchTogetherIDs;
2042
+ exports.WatchTogetherMessages = WatchTogetherMessages;
2043
+ exports.WatchTogetherModule = WatchTogetherModule;
2044
+ exports.dateUtils = date_exports;
2045
+ exports.default = HarmoniSDK;
2046
+ exports.identifyUserActions = identifyUserActions;
2047
+ exports.localStorage = localStorage;
2048
+ exports.normalizePlayerState = normalizePlayerState;
2049
+ exports.objectUtils = object_exports;
2050
+ exports.sessionStorage = sessionStorage;
2051
+ exports.stringUtils = string_exports;
2052
+ exports.validationUtils = validation_exports;
2053
+ //# sourceMappingURL=index.js.map
2054
+ //# sourceMappingURL=index.js.map