@amityco/ts-sdk-react-native 7.12.1-f6ba70a0.0 → 7.13.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.
Files changed (42) hide show
  1. package/dist/@types/core/events.d.ts +1 -1
  2. package/dist/@types/core/events.d.ts.map +1 -1
  3. package/dist/@types/domains/liveStreamPlayer.d.ts +6 -2
  4. package/dist/@types/domains/liveStreamPlayer.d.ts.map +1 -1
  5. package/dist/@types/domains/room.d.ts +29 -0
  6. package/dist/@types/domains/room.d.ts.map +1 -1
  7. package/dist/client/api/setSessionState.d.ts +1 -1
  8. package/dist/client/api/setSessionState.d.ts.map +1 -1
  9. package/dist/client/events/onSessionStateChange.d.ts +1 -1
  10. package/dist/client/events/onSessionStateChange.d.ts.map +1 -1
  11. package/dist/client/utils/SessionWatcher.d.ts +2 -2
  12. package/dist/client/utils/SessionWatcher.d.ts.map +1 -1
  13. package/dist/core/events.d.ts +1 -1
  14. package/dist/core/events.d.ts.map +1 -1
  15. package/dist/feedRepository/observers/utils.d.ts +1 -1
  16. package/dist/index.cjs.js +433 -90
  17. package/dist/index.esm.js +433 -90
  18. package/dist/index.umd.js +2 -2
  19. package/dist/liveStreamPlayer/utils/cryptoSignatureUtils.d.ts +2 -2
  20. package/dist/liveStreamPlayer/utils/cryptoSignatureUtils.d.ts.map +1 -1
  21. package/dist/postRepository/observers/utils.d.ts +2 -2
  22. package/dist/reactionRepository/events/onReactionAdded.d.ts +1 -1
  23. package/dist/reactionRepository/events/onReactionAdded.d.ts.map +1 -1
  24. package/dist/reactionRepository/events/onReactionRemoved.d.ts +1 -1
  25. package/dist/reactionRepository/events/onReactionRemoved.d.ts.map +1 -1
  26. package/dist/roomRepository/api/analytics/AmityRoomAnalytics.d.ts +30 -0
  27. package/dist/roomRepository/api/analytics/AmityRoomAnalytics.d.ts.map +1 -0
  28. package/dist/roomRepository/api/analytics/WatchSessionStorage.d.ts +38 -0
  29. package/dist/roomRepository/api/analytics/WatchSessionStorage.d.ts.map +1 -0
  30. package/dist/roomRepository/api/analytics/index.d.ts +4 -0
  31. package/dist/roomRepository/api/analytics/index.d.ts.map +1 -0
  32. package/dist/roomRepository/api/analytics/syncWatchSessions.d.ts +6 -0
  33. package/dist/roomRepository/api/analytics/syncWatchSessions.d.ts.map +1 -0
  34. package/dist/roomRepository/api/index.d.ts +1 -0
  35. package/dist/roomRepository/api/index.d.ts.map +1 -1
  36. package/dist/userRepository/relationship/block/api/blockUser.d.ts.map +1 -1
  37. package/dist/utils/event.d.ts +1 -1
  38. package/dist/utils/event.d.ts.map +1 -1
  39. package/dist/utils/linkedObject/roomLinkedObject.d.ts.map +1 -1
  40. package/dist/utils/liveObject.d.ts +1 -1
  41. package/dist/utils/liveObject.d.ts.map +1 -1
  42. package/package.json +2 -2
package/dist/index.cjs.js CHANGED
@@ -260,8 +260,8 @@ exports.AmityEventOrderOption = void 0;
260
260
 
261
261
  function getVersion() {
262
262
  try {
263
- // the string ''v7.12.0-cjs'' should be replaced by actual value by @rollup/plugin-replace
264
- return 'v7.12.0-cjs';
263
+ // the string ''v7.13.0-cjs'' should be replaced by actual value by @rollup/plugin-replace
264
+ return 'v7.13.0-cjs';
265
265
  }
266
266
  catch (error) {
267
267
  return '__dev__';
@@ -6904,11 +6904,11 @@ class SessionWatcher {
6904
6904
  this._listener.delete(callback);
6905
6905
  };
6906
6906
  }
6907
- setSessionState(state) {
6907
+ setSessionState(state, reason) {
6908
6908
  if (this._sessionState === state)
6909
6909
  return;
6910
6910
  this._sessionState = state;
6911
- this._listener.forEach(cb => cb(state));
6911
+ this._listener.forEach(cb => cb(state, reason));
6912
6912
  }
6913
6913
  destroy() {
6914
6914
  this._listener.clear();
@@ -7348,9 +7348,9 @@ const isValidStateChange = (prevState, nextState) => {
7348
7348
  *
7349
7349
  * @category private
7350
7350
  */
7351
- const setSessionState = (state) => {
7351
+ const setSessionState = (state, reason) => {
7352
7352
  const client = getActiveClient();
7353
- client.log('client/api/setSessionState', state);
7353
+ client.log('client/api/setSessionState', state, reason);
7354
7354
  const { sessionState: prevState } = client;
7355
7355
  if (prevState === state)
7356
7356
  return false;
@@ -7359,7 +7359,7 @@ const setSessionState = (state) => {
7359
7359
  throw new ASCError(`Session state cannot change from ${prevState} to ${state}`, 800000 /* Amity.ClientError.UNKNOWN_ERROR */, "error" /* Amity.ErrorLevel.ERROR */);
7360
7360
  }
7361
7361
  client.sessionState = state;
7362
- SessionWatcher$1.getInstance().setSessionState(state);
7362
+ SessionWatcher$1.getInstance().setSessionState(state, reason);
7363
7363
  return true;
7364
7364
  };
7365
7365
 
@@ -8842,7 +8842,7 @@ const logout = async () => {
8842
8842
  */
8843
8843
  const terminateClient = (terminationReason) => {
8844
8844
  const client = getActiveClient();
8845
- setSessionState("terminated" /* Amity.SessionStates.TERMINATED */);
8845
+ setSessionState("terminated" /* Amity.SessionStates.TERMINATED */, terminationReason);
8846
8846
  if (client.http.defaults.metadata) {
8847
8847
  if (terminationReason === "globalBan" /* Amity.TokenTerminationReason.GLOBAL_BAN */)
8848
8848
  client.http.defaults.metadata.isGlobalBanned = true;
@@ -10799,12 +10799,19 @@ const blockUser = async (userId) => {
10799
10799
  const { data } = await client.http.post(`/api/v4/me/user-blocks/${userId}`);
10800
10800
  const cachedAt = client.cache && Date.now();
10801
10801
  const { follows, followCounts } = data;
10802
- const followStatus = { follows };
10802
+ const blockedFollowStatus = { follows };
10803
10803
  if (client.cache) {
10804
- ingestInCache(followStatus, { cachedAt });
10804
+ ingestInCache(blockedFollowStatus, { cachedAt });
10805
10805
  upsertInCache(['followInfo', 'get', userId], followCounts[0], { cachedAt });
10806
10806
  }
10807
- const payload = prepareFollowStatusPayload(followStatus);
10807
+ // need to swap {from} and {to} to get the correct id for unfollow event to filter from follower live collection
10808
+ const blockedUnfollowStatus = {
10809
+ follows: follows.map(follow => {
10810
+ const unfollowStatus = Object.assign(Object.assign({}, follow), { to: follow.from, from: follow.to, status: 'none' });
10811
+ return follow.status === 'blocked' ? unfollowStatus : follow;
10812
+ }),
10813
+ };
10814
+ const payload = prepareFollowStatusPayload(blockedUnfollowStatus);
10808
10815
  fireEvent('local.follow.unfollowed', payload);
10809
10816
  return data;
10810
10817
  };
@@ -12709,6 +12716,414 @@ const getInvitation = async (params) => {
12709
12716
  };
12710
12717
  /* end_public_function */
12711
12718
 
12719
+ /**
12720
+ * WatchSessionStorage manages watch session data in memory
12721
+ * Similar to UsageCollector for stream watch minutes
12722
+ */
12723
+ class WatchSessionStorage {
12724
+ constructor() {
12725
+ this.sessions = new Map();
12726
+ }
12727
+ /**
12728
+ * Add a new watch session
12729
+ */
12730
+ addSession(session) {
12731
+ this.sessions.set(session.sessionId, session);
12732
+ }
12733
+ /**
12734
+ * Get a watch session by sessionId
12735
+ */
12736
+ getSession(sessionId) {
12737
+ return this.sessions.get(sessionId);
12738
+ }
12739
+ /**
12740
+ * Update a watch session
12741
+ */
12742
+ updateSession(sessionId, updates) {
12743
+ const session = this.sessions.get(sessionId);
12744
+ if (session) {
12745
+ this.sessions.set(sessionId, Object.assign(Object.assign({}, session), updates));
12746
+ }
12747
+ }
12748
+ /**
12749
+ * Get all sessions with a specific sync state
12750
+ */
12751
+ getSessionsByState(state) {
12752
+ return Array.from(this.sessions.values()).filter(session => session.syncState === state);
12753
+ }
12754
+ /**
12755
+ * Delete a session
12756
+ */
12757
+ deleteSession(sessionId) {
12758
+ this.sessions.delete(sessionId);
12759
+ }
12760
+ /**
12761
+ * Get all pending sessions
12762
+ */
12763
+ getPendingSessions() {
12764
+ return this.getSessionsByState('PENDING');
12765
+ }
12766
+ /**
12767
+ * Get all syncing sessions
12768
+ */
12769
+ getSyncingSessions() {
12770
+ return this.getSessionsByState('SYNCING');
12771
+ }
12772
+ }
12773
+ // Singleton instance
12774
+ let storageInstance = null;
12775
+ const getWatchSessionStorage = () => {
12776
+ if (!storageInstance) {
12777
+ storageInstance = new WatchSessionStorage();
12778
+ }
12779
+ return storageInstance;
12780
+ };
12781
+
12782
+ const privateKey = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHo80SecH7FuF2\nhFYnb+l26/VN8UMLXAQFLnxciNTEwkGVFMpdezlH8rU2HtUJL4RETogbAOLVY0XM\njs6sPn8G1nALmh9qeDpUtVqFOVtBHxEZ910TLOtQiunjqJKO5nWdqZ71EC3OFluR\niGQkO84BiIFbv37ub7xl3S8XarbtKoLcyVpkDHi+1wx1pgCAn6gtBUgckPL5NR8j\nLseabl3HAXQfhTCKo4tmOFM2Dxwl1IUMmIJrJg/aIU/U0tj/1Eoo7mG0JcNWX19l\nW3EecCbi0ncCJOrkUdwlBrcjaMayaX/ubEwyUeTGiLdyc4L3GRLHjyK8xgVNXRMH\nbZWJ2a5NAgMBAAECggEASxuE+35zTFO/XydKgmvIGcWL9FbgMlXb7Vcf0nBoG945\nbiz0NVc2paraIhJXc608xbYF3qLmtAE1MVBI0ORyRdBHNxY024l/6H6SH60Ed+uI\nM4ysp5ourY6Vj+DLwpdRiI9YDjqYAQDIUmhNxJP7XPhOMoZI6st+xZQBM34ic/bv\nAMSJm9OZphSp3+qXVkFZztr2mxD2EZSJJLYxi8BCdgM2qhazalbcJ6zDKHCZWVWm\n8RRxDGldyMb/237JxETzP40tAlzOZDmBAbUgEnurDJ93RVDIE3rbZUshwgeQd18a\nem096mWgvB1AIKYgsTAR3pw+V19YWAjq/glP6fz8wQKBgQD/oQq+ukKF0PRgBeM5\ngeTjSwsdGppQLmf5ndujvoiz/TpdjDEPu6R8kigQr1rG2t4K/yfdZoI8RdmJD1al\n3Q7N9hofooSy4rj6E3txzWZCHJjHad2cnCp/O26HiReGAl7wTcfTmNdiFHhZQzm5\nJBkvWAiwuvQMNfEbnXxw6/vIDwKBgQDH7fX8gsc77JLvAWgp1MaQN/sbqVb6JeT1\nFQfR8E/WFCSmzQBtNzd5KgYuCeelwr/8DyYytvN2BzCYZXp73gI1jF3YlW5jVn74\nOY6TwQ095digwo6Z0yuxopdIOApKgAkL9PRKgNrqAf3NAyMua6lOGifzjDojC3KU\nfylQmxMn4wKBgHp2B9O/H0dEBw5JQ8W0+JX6yWQz7mEjGiR2/1W+XXb8hQ1zr709\nw1r6Gb+EghRpnZ3fBpYGGbYOMFx8wKHM+N6qW3F0ReX8v2juFGE8aRSa5oYBrWzt\nU16Idjbv8hj84cZ1PJmdyvDtpYn9rpWHOZl4rxEbPvbqkIsOMyNVqdT5AoGAOSge\nmwIIU2le2FVeohbibXiToWTYKMuMmURZ5/r72AgKMmWJKbAPe+Q3wBG01/7FRBpQ\noU8Ma0HC8s6QJbliiEyIx9JwrJWd1vkdecBHONrtA4ibm/5zD2WcOllLF+FitLhi\n3qnX6+6F0IaFGFBPJrTzlv0P4dTz/OAdv52V7GECgYEA2TttOKBAqWllgOaZOkql\nLVMJVmgR7s6tLi1+cEP8ZcapV9aRbRzTAKXm4f8AEhtlG9F9kCOvHYCYGi6JaiWJ\nZkHjeex3T+eE6Di6y5Bm/Ift5jtVhJ4jCVwHOKTMej79NPUFTJfv8hCo29haBDv6\nRXFrv+T21KCcw8k3sJeJWWQ=\n-----END PRIVATE KEY-----";
12783
+ /*
12784
+ * The crypto algorithm used for importing key and signing string
12785
+ */
12786
+ const ALGORITHM = {
12787
+ name: 'RSASSA-PKCS1-v1_5',
12788
+ hash: { name: 'SHA-256' },
12789
+ };
12790
+ /*
12791
+ * IMPORTANT!
12792
+ * If you are recieving key from other platforms use an online tool to convert
12793
+ * the PKCS1 to PKCS8. For instance the key from Android SDK is of the format
12794
+ * PKCS1.
12795
+ *
12796
+ * If recieving from the platform, verify if it's already in the expected
12797
+ * format. Otherwise the crypto.subtle.importKey will throw a DOMException
12798
+ */
12799
+ const PRIVATE_KEY_SIGNATURE = 'pkcs8';
12800
+ /*
12801
+ * Ensure that the private key in the .env follows this format
12802
+ */
12803
+ const PEM_HEADER = '-----BEGIN PRIVATE KEY-----';
12804
+ const PEM_FOOTER = '-----END PRIVATE KEY-----';
12805
+ /*
12806
+ * The crypto.subtle.sign function returns an ArrayBuffer whereas the server
12807
+ * expects a base64 string. This util helps facilitate that process
12808
+ */
12809
+ function base64FromArrayBuffer(buffer) {
12810
+ const uint8Array = new Uint8Array(buffer);
12811
+ let binary = '';
12812
+ uint8Array.forEach(byte => {
12813
+ binary += String.fromCharCode(byte);
12814
+ });
12815
+ return jsBase64.btoa(binary);
12816
+ }
12817
+ /*
12818
+ * Encode ASN.1 length field
12819
+ */
12820
+ function encodeLength(length) {
12821
+ if (length < 128) {
12822
+ return new Uint8Array([length]);
12823
+ }
12824
+ if (length < 256) {
12825
+ return new Uint8Array([0x81, length]);
12826
+ }
12827
+ // eslint-disable-next-line no-bitwise
12828
+ return new Uint8Array([0x82, (length >> 8) & 0xff, length & 0xff]);
12829
+ }
12830
+ /*
12831
+ * Convert PKCS1 private key to PKCS8 format
12832
+ * PKCS1 is RSA-specific format, PKCS8 is generic format that crypto.subtle requires
12833
+ * Android uses PKCS8EncodedKeySpec which expects PKCS8 format
12834
+ */
12835
+ function pkcs1ToPkcs8(pkcs1) {
12836
+ // Algorithm identifier for RSA
12837
+ const algorithmIdentifier = new Uint8Array([
12838
+ 0x30,
12839
+ 0x0d,
12840
+ 0x06,
12841
+ 0x09,
12842
+ 0x2a,
12843
+ 0x86,
12844
+ 0x48,
12845
+ 0x86,
12846
+ 0xf7,
12847
+ 0x0d,
12848
+ 0x01,
12849
+ 0x01,
12850
+ 0x01,
12851
+ 0x05,
12852
+ 0x00, // NULL
12853
+ ]);
12854
+ // Version (INTEGER 0)
12855
+ const version = new Uint8Array([0x02, 0x01, 0x00]);
12856
+ // OCTET STRING tag + length + pkcs1 data
12857
+ const octetStringTag = 0x04;
12858
+ const octetStringLength = encodeLength(pkcs1.length);
12859
+ // Calculate total content length (version + algorithm + octet string)
12860
+ const contentLength = version.length + algorithmIdentifier.length + 1 + octetStringLength.length + pkcs1.length;
12861
+ // SEQUENCE tag + length
12862
+ const sequenceTag = 0x30;
12863
+ const sequenceLength = encodeLength(contentLength);
12864
+ // Build the PKCS8 structure
12865
+ const pkcs8 = new Uint8Array(1 + sequenceLength.length + contentLength);
12866
+ let offset = 0;
12867
+ pkcs8[offset] = sequenceTag;
12868
+ offset += 1;
12869
+ pkcs8.set(sequenceLength, offset);
12870
+ offset += sequenceLength.length;
12871
+ pkcs8.set(version, offset);
12872
+ offset += version.length;
12873
+ pkcs8.set(algorithmIdentifier, offset);
12874
+ offset += algorithmIdentifier.length;
12875
+ pkcs8[offset] = octetStringTag;
12876
+ offset += 1;
12877
+ pkcs8.set(octetStringLength, offset);
12878
+ offset += octetStringLength.length;
12879
+ pkcs8.set(pkcs1, offset);
12880
+ return pkcs8;
12881
+ }
12882
+ async function importPrivateKey(keyString) {
12883
+ // Remove PEM headers if present and any whitespace
12884
+ let base64Key = keyString;
12885
+ if (keyString.includes(PEM_HEADER)) {
12886
+ base64Key = keyString
12887
+ .substring(PEM_HEADER.length, keyString.length - PEM_FOOTER.length)
12888
+ .replace(/\s/g, '');
12889
+ }
12890
+ else {
12891
+ base64Key = keyString.replace(/\s/g, '');
12892
+ }
12893
+ // Base64 decode to get binary data
12894
+ const binaryDerString = jsBase64.atob(base64Key);
12895
+ // Convert to Uint8Array for manipulation
12896
+ const pkcs1 = new Uint8Array(binaryDerString.length);
12897
+ for (let i = 0; i < binaryDerString.length; i += 1) {
12898
+ pkcs1[i] = binaryDerString.charCodeAt(i);
12899
+ }
12900
+ // Convert PKCS1 to PKCS8 (crypto.subtle requires PKCS8)
12901
+ const pkcs8 = pkcs1ToPkcs8(pkcs1);
12902
+ // Import the key
12903
+ const key = await crypto.subtle.importKey(PRIVATE_KEY_SIGNATURE, pkcs8.buffer, ALGORITHM, false, ['sign']);
12904
+ return key;
12905
+ }
12906
+ async function createSignature({ timestamp, rooms, }) {
12907
+ const dataStr = rooms
12908
+ .map(item => Object.keys(item)
12909
+ .sort()
12910
+ .map(key => `${key}=${item[key]}`)
12911
+ .join('&'))
12912
+ .join(';');
12913
+ /*
12914
+ * nonceStr needs to be unique for each request
12915
+ */
12916
+ const nonceStr = uuid__default["default"].v4();
12917
+ const signStr = `nonceStr=${nonceStr}&timestamp=${timestamp}&data=${dataStr}==`;
12918
+ const encoder = new TextEncoder();
12919
+ const data = encoder.encode(signStr);
12920
+ const key = await importPrivateKey(privateKey);
12921
+ const sign = await crypto.subtle.sign(ALGORITHM, key, data);
12922
+ return { signature: base64FromArrayBuffer(sign), nonceStr };
12923
+ }
12924
+
12925
+ /**
12926
+ * Sync pending watch sessions to backend
12927
+ * This function implements the full sync flow with network resilience
12928
+ */
12929
+ async function syncWatchSessions() {
12930
+ const storage = getWatchSessionStorage();
12931
+ // Get all pending sessions
12932
+ const pendingSessions = storage.getPendingSessions();
12933
+ if (pendingSessions.length === 0) {
12934
+ return true;
12935
+ }
12936
+ try {
12937
+ const timestamp = new Date().toISOString();
12938
+ // Convert sessions to API format - always include all fields like syncUsage does
12939
+ const rooms = pendingSessions.map(session => ({
12940
+ sessionId: session.sessionId,
12941
+ roomId: session.roomId,
12942
+ watchSeconds: session.watchSeconds,
12943
+ startTime: session.startTime.toISOString(),
12944
+ endTime: session.endTime ? session.endTime.toISOString() : '',
12945
+ }));
12946
+ // Create signature (reuse from stream feature) - pass directly like syncUsage
12947
+ const signatureData = await createSignature({
12948
+ timestamp,
12949
+ rooms,
12950
+ });
12951
+ if (!signatureData || !signatureData.signature) {
12952
+ throw new Error('Signature is undefined');
12953
+ }
12954
+ // Update sync state to SYNCING
12955
+ pendingSessions.forEach(session => {
12956
+ storage.updateSession(session.sessionId, { syncState: 'SYNCING' });
12957
+ });
12958
+ const payload = {
12959
+ signature: signatureData.signature,
12960
+ nonceStr: signatureData.nonceStr,
12961
+ timestamp,
12962
+ rooms,
12963
+ };
12964
+ const client = getActiveClient();
12965
+ // Send to backend
12966
+ await client.http.post('/api/v3/user-event/room', payload);
12967
+ // Success - update to SYNCED
12968
+ pendingSessions.forEach(session => {
12969
+ storage.updateSession(session.sessionId, {
12970
+ syncState: 'SYNCED',
12971
+ syncedAt: new Date(),
12972
+ });
12973
+ });
12974
+ return true;
12975
+ }
12976
+ catch (err) {
12977
+ console.error('[SDK syncWatchSessions] ERROR caught:', (err === null || err === void 0 ? void 0 : err.message) || err);
12978
+ // Failure - update back to PENDING and increment retry count
12979
+ pendingSessions.forEach(session => {
12980
+ const currentSession = storage.getSession(session.sessionId);
12981
+ if (currentSession) {
12982
+ const newRetryCount = currentSession.retryCount + 1;
12983
+ if (newRetryCount >= 3) {
12984
+ // Delete session if retry count exceeds 3
12985
+ storage.deleteSession(session.sessionId);
12986
+ }
12987
+ else {
12988
+ // Update to PENDING with incremented retry count
12989
+ storage.updateSession(session.sessionId, {
12990
+ syncState: 'PENDING',
12991
+ retryCount: newRetryCount,
12992
+ });
12993
+ }
12994
+ }
12995
+ });
12996
+ return false;
12997
+ }
12998
+ }
12999
+
13000
+ /**
13001
+ * Room statuses that are considered watchable
13002
+ */
13003
+ const WATCHABLE_ROOM_STATUSES = ['live', 'recorded'];
13004
+ /**
13005
+ * Generate a random jitter delay between 5 and 30 seconds
13006
+ */
13007
+ const getJitterDelay = () => {
13008
+ const minDelay = 5000; // 5 seconds
13009
+ const maxDelay = 30000; // 30 seconds
13010
+ return Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay;
13011
+ };
13012
+ /**
13013
+ * Wait for network connection with timeout
13014
+ * @param maxWaitTime Maximum time to wait in milliseconds (default: 60000 = 60 seconds)
13015
+ * @returns Promise that resolves when network is available or timeout is reached
13016
+ */
13017
+ const waitForNetwork = (maxWaitTime = 60000) => {
13018
+ return new Promise(resolve => {
13019
+ // Simple check - if navigator.onLine is available, use it
13020
+ // Otherwise, assume network is available
13021
+ if (typeof navigator === 'undefined' || typeof navigator.onLine === 'undefined') {
13022
+ resolve();
13023
+ return;
13024
+ }
13025
+ const startTime = Date.now();
13026
+ const checkInterval = 1000; // 1 second
13027
+ const checkConnection = () => {
13028
+ if (navigator.onLine || Date.now() - startTime >= maxWaitTime) {
13029
+ resolve();
13030
+ }
13031
+ else {
13032
+ setTimeout(checkConnection, checkInterval);
13033
+ }
13034
+ };
13035
+ checkConnection();
13036
+ });
13037
+ };
13038
+ /**
13039
+ * AmityRoomAnalytics provides analytics capabilities for room watch sessions
13040
+ */
13041
+ class AmityRoomAnalytics {
13042
+ constructor(room) {
13043
+ this.storage = getWatchSessionStorage();
13044
+ this.client = getActiveClient();
13045
+ this.room = room;
13046
+ }
13047
+ /**
13048
+ * Create a new watch session for the current room
13049
+ * @param startedAt The timestamp when watching started
13050
+ * @returns Promise<string> sessionId unique identifier for this watch session
13051
+ * @throws ASCApiError if room is not in watchable state (not LIVE or RECORDED)
13052
+ */
13053
+ async createWatchSession(startedAt) {
13054
+ // Validate room status
13055
+ if (!WATCHABLE_ROOM_STATUSES.includes(this.room.status)) {
13056
+ throw new ASCApiError('room is not in watchable state', 500000 /* Amity.ServerError.BUSINESS_ERROR */, "error" /* Amity.ErrorLevel.ERROR */);
13057
+ }
13058
+ // Generate session ID with prefix
13059
+ const prefix = this.room.status === 'live' ? 'room_' : 'room_playback_';
13060
+ const sessionId = prefix + uuid__default["default"].v4();
13061
+ // Create watch session entity
13062
+ const session = {
13063
+ sessionId,
13064
+ roomId: this.room.roomId,
13065
+ watchSeconds: 0,
13066
+ startTime: startedAt,
13067
+ endTime: null,
13068
+ syncState: 'PENDING',
13069
+ syncedAt: null,
13070
+ retryCount: 0,
13071
+ };
13072
+ // Persist to storage
13073
+ this.storage.addSession(session);
13074
+ return sessionId;
13075
+ }
13076
+ /**
13077
+ * Update an existing watch session with duration
13078
+ * @param sessionId The unique identifier of the watch session
13079
+ * @param duration The total watch duration in seconds
13080
+ * @param endedAt The timestamp when this update occurred
13081
+ * @returns Promise<void> Completion status
13082
+ */
13083
+ async updateWatchSession(sessionId, duration, endedAt) {
13084
+ const session = this.storage.getSession(sessionId);
13085
+ if (!session) {
13086
+ throw new Error(`Watch session ${sessionId} not found`);
13087
+ }
13088
+ // Update session
13089
+ this.storage.updateSession(sessionId, {
13090
+ watchSeconds: duration,
13091
+ endTime: endedAt,
13092
+ });
13093
+ }
13094
+ /**
13095
+ * Sync all pending watch sessions to backend
13096
+ * This function uses jitter delay and handles network resilience
13097
+ */
13098
+ syncPendingWatchSessions() {
13099
+ // Execute with jitter delay (5-30 seconds)
13100
+ const jitterDelay = getJitterDelay();
13101
+ this.client.log('room/RoomAnalytics: syncPendingWatchSessions called, jitter delay:', `${jitterDelay}ms`);
13102
+ setTimeout(async () => {
13103
+ this.client.log('room/RoomAnalytics: Jitter delay completed, starting sync process');
13104
+ try {
13105
+ // Reset any SYNCING sessions back to PENDING
13106
+ const syncingSessions = this.storage.getSyncingSessions();
13107
+ this.client.log('room/RoomAnalytics: SYNCING sessions to reset:', syncingSessions.length);
13108
+ syncingSessions.forEach(session => {
13109
+ this.storage.updateSession(session.sessionId, { syncState: 'PENDING' });
13110
+ });
13111
+ // Wait for network connection (max 60 seconds)
13112
+ await waitForNetwork(60000);
13113
+ this.client.log('room/RoomAnalytics: Network available');
13114
+ // Sync pending sessions
13115
+ this.client.log('room/RoomAnalytics: Calling syncWatchSessions()');
13116
+ await syncWatchSessions();
13117
+ this.client.log('room/RoomAnalytics: syncWatchSessions completed');
13118
+ }
13119
+ catch (error) {
13120
+ // Error is already handled in syncWatchSessions
13121
+ console.error('Failed to sync watch sessions:', error);
13122
+ }
13123
+ }, jitterDelay);
13124
+ }
13125
+ }
13126
+
12712
13127
  const roomLinkedObject = (room) => {
12713
13128
  return Object.assign(Object.assign({}, room), { get post() {
12714
13129
  var _a;
@@ -12755,6 +13170,9 @@ const roomLinkedObject = (room) => {
12755
13170
  type: "livestreamCohostInvite" /* InvitationTypeEnum.LivestreamCohostInvite */,
12756
13171
  });
12757
13172
  return data;
13173
+ }, analytics() {
13174
+ // Use 'this' to avoid creating a new room object
13175
+ return new AmityRoomAnalytics(this);
12758
13176
  } });
12759
13177
  };
12760
13178
 
@@ -29133,6 +29551,10 @@ var index$b = /*#__PURE__*/Object.freeze({
29133
29551
  getRecordedUrl: getRecordedUrl,
29134
29552
  removeParticipant: removeParticipant,
29135
29553
  leaveRoom: leaveRoom,
29554
+ AmityRoomAnalytics: AmityRoomAnalytics,
29555
+ WatchSessionStorage: WatchSessionStorage,
29556
+ getWatchSessionStorage: getWatchSessionStorage,
29557
+ syncWatchSessions: syncWatchSessions,
29136
29558
  onRoomStartBroadcasting: onRoomStartBroadcasting,
29137
29559
  onRoomWaitingReconnect: onRoomWaitingReconnect,
29138
29560
  onRoomEndBroadcasting: onRoomEndBroadcasting,
@@ -30641,85 +31063,6 @@ var index$7 = /*#__PURE__*/Object.freeze({
30641
31063
  getPoll: getPoll
30642
31064
  });
30643
31065
 
30644
- const privateKey = "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAARz+hmBgi8pJ\nQb8LeY41gtHhk+ACMwRfhsn7GqpqRQNG2qU0755mzZuVDUqjQMGSo8THJB7O+OJs\nflbZRkFXlFoFOVNw1UpNOgwEQZ6wB9oRwzepTJAfF1sVhm/o/ixvXh1zDFNDy6yZ\npXyiiJHUVxqyjllZhxnwdvjoVtDs6hW6awG09bB9nh/TTejlUKXoAgzqVwu/1QMu\nUVViET495elEe19aUarEy+oL2iKeXCEvqda/pWNBdbieFyJvvZ08HN8dPuT88wq2\njZLEAth1vrwQ2IAa4ktaLcBQdLJgIkrbDvAiVZ8lQAjS/bq5vXQikTGvoPlC5bbn\nvuOM/3eLAgMBAAECggEAVZ+peHAghq2QVj71nX5lxsNCKaCyYwixSJBpfouTt7Rz\nE6PpzMOXFi1W1o+I22jDakuSM2SOQKqI/u0QefB0r0O/KVk5NrZHXk0mkrdYtxOp\nUgaGyf8UvmjB+8VqHrNKyZdk9qtmbnNj01kTTcAtmE4H39zPR7eR/8Rul94vaZbs\nwCnKJS3mLT3JxyGug6lxanveKkjG+CKC1nJQYWaxCJxaFSzbwXQPvDhB+TvrIbee\npd5v4EAyEJohpr+T9oDGGJkb/KARBZCtwLyB976PKJwwBA8MRVL1i5QwawuMiMq5\nUtnOnbGKtCeFzaLbNU0Qi8bqyims84EQxC6DOu1fkQKBgQDdvsoBsEhsOXV7hlIJ\naEd0eSJZVkdqimxH8uGoMM2FeNaOrcB6yBXqTSP0R3OIyf8eaY6yjRvP30ZNXcll\n/gD3O1Mu6YmWQdt1W2WA6pKOsUuPXasf0pdOF7IiFZKlSabz5YHXFqwVuqm8loaj\nsXel3YWqPVdHiankE7tz+3ssnQKBgQDdqi4TNdD1MdEpihx19jr0QjUiXW3939FK\nqp30HESPEGDGQzXdmJgif9HhZb+cJSuWaHEbjgBrYahvgCF+y6LbEpOD+D/dmT+s\nDEAQaR84sah6dokwPjV8fjBSrcVFjCS+doxv0d3p/9OUEeyUhFrY03nxtIEYkLIE\n/Zvn37b4RwKBgQCLENVFe9XfsaVhQ5r9dV2iyTlmh7qgMZG5CbTFs12hQGhm8McO\n+Z7s41YSJCFr/yq1WwP4LJDtrBw99vyQr1zRsG35tNLp3gGRNzGQSQyC2uQFVHw2\np+7mNewsfhUK/gbrXNsyFnDz6635rPlhfbII3sWuP2wWXFqkxE9CbMwR7QKBgQC6\nawDMzxmo2/iYArrkyevSuEuPVxvFwpF1RgAI6C0QVCnPE38dmdN4UB7mfHekje4W\nVEercMURidPp0cxZolCYBQtilUjAyL0vqC3In1/Ogjq6oy3FEMxSop1pKxMY5j+Q\nnoqFD+6deLUrddeNH7J3X4LSr4dSbX4JjG+tlgt+yQKBgQCuwTL4hA6KqeInQ0Ta\n9VQX5Qr8hFlqJz1gpymi/k63tW/Ob8yedbg3WWNWyShwRMFYyY9S81ITFWM95uL6\nvF3x9rmRjwElJw9PMwVu6dmf/CO0Z1wzXSp2VVD12gbrUD/0/d7MUoJ9LgC8X8f/\nn0txLHYGHbx+nf95+JUg6lV3hg==\n-----END PRIVATE KEY-----";
30645
- /*
30646
- * The crypto algorithm used for importing key and signing string
30647
- */
30648
- const ALGORITHM = {
30649
- name: 'RSASSA-PKCS1-v1_5',
30650
- hash: { name: 'SHA-256' },
30651
- };
30652
- /*
30653
- * IMPORTANT!
30654
- * If you are recieving key from other platforms use an online tool to convert
30655
- * the PKCS1 to PKCS8. For instance the key from Android SDK is of the format
30656
- * PKCS1.
30657
- *
30658
- * If recieving from the platform, verify if it's already in the expected
30659
- * format. Otherwise the crypto.subtle.importKey will throw a DOMException
30660
- */
30661
- const PRIVATE_KEY_SIGNATURE = 'pkcs8';
30662
- /*
30663
- * Ensure that the private key in the .env follows this format
30664
- */
30665
- const PEM_HEADER = '-----BEGIN PRIVATE KEY-----';
30666
- const PEM_FOOTER = '-----END PRIVATE KEY-----';
30667
- /*
30668
- * The crypto.subtle.sign function returns an ArrayBuffer whereas the server
30669
- * expects a base64 string. This util helps facilitate that process
30670
- */
30671
- function base64FromArrayBuffer(buffer) {
30672
- const uint8Array = new Uint8Array(buffer);
30673
- let binary = '';
30674
- uint8Array.forEach(byte => {
30675
- binary += String.fromCharCode(byte);
30676
- });
30677
- return jsBase64.btoa(binary);
30678
- }
30679
- /*
30680
- * Convert a string into an ArrayBuffer
30681
- * from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
30682
- *
30683
- * Solely used by the importPrivateKey method
30684
- */
30685
- function str2ab(str) {
30686
- const buf = new ArrayBuffer(str.length);
30687
- const bufView = new Uint8Array(buf);
30688
- for (let i = 0, strLen = str.length; i < strLen; i += 1) {
30689
- bufView[i] = str.charCodeAt(i);
30690
- }
30691
- return buf;
30692
- }
30693
- function importPrivateKey(pem) {
30694
- // fetch the part of the PEM string between header and footer
30695
- const pemContents = pem.substring(PEM_HEADER.length, pem.length - PEM_FOOTER.length);
30696
- /*
30697
- * base64 decode the string to get the binary data
30698
- */
30699
- const binaryDerString = jsBase64.atob(pemContents);
30700
- // convert from a binary string to an ArrayBuffer
30701
- const binaryDer = str2ab(binaryDerString);
30702
- return crypto.subtle.importKey(PRIVATE_KEY_SIGNATURE, binaryDer, ALGORITHM, false, ['sign']);
30703
- }
30704
- async function createSignature({ timestamp, streams, }) {
30705
- const dataStr = streams
30706
- .map(item => Object.keys(item)
30707
- .sort()
30708
- .map(key => `${key}=${item[key]}`)
30709
- .join('&'))
30710
- .join(';');
30711
- /*
30712
- * nonceStr needs to be unique for each request
30713
- */
30714
- const nonceStr = uuid__default["default"].v4();
30715
- const signStr = `nonceStr=${nonceStr}&timestamp=${timestamp}&data=${dataStr}==`;
30716
- const encoder = new TextEncoder();
30717
- const data = encoder.encode(signStr);
30718
- const key = await importPrivateKey(privateKey);
30719
- const sign = await crypto.subtle.sign(ALGORITHM, key, data);
30720
- return { signature: base64FromArrayBuffer(sign), nonceStr };
30721
- }
30722
-
30723
31066
  async function syncUsage({ bufferCurrentUsage, getActiveStreams, updateUsage, dispose, }) {
30724
31067
  const streams = bufferCurrentUsage();
30725
31068
  if (!streams.length)