@magicpixel/rn-mp-client-sdk 1.13.0 → 1.13.20

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 (104) hide show
  1. package/README.md +163 -14
  2. package/lib/commonjs/common/app-types.js.map +1 -1
  3. package/lib/commonjs/common/constants.js +11 -2
  4. package/lib/commonjs/common/constants.js.map +1 -1
  5. package/lib/commonjs/common/data-store.js +13 -30
  6. package/lib/commonjs/common/data-store.js.map +1 -1
  7. package/lib/commonjs/common/deeplink-helper.js +174 -0
  8. package/lib/commonjs/common/deeplink-helper.js.map +1 -0
  9. package/lib/commonjs/common/device-info-helper.js +168 -0
  10. package/lib/commonjs/common/device-info-helper.js.map +1 -0
  11. package/lib/commonjs/common/event-bus.js +39 -0
  12. package/lib/commonjs/common/event-bus.js.map +1 -1
  13. package/lib/commonjs/common/network-service.js +119 -15
  14. package/lib/commonjs/common/network-service.js.map +1 -1
  15. package/lib/commonjs/common/reporter.js +28 -10
  16. package/lib/commonjs/common/reporter.js.map +1 -1
  17. package/lib/commonjs/common/storage-helper.js +227 -0
  18. package/lib/commonjs/common/storage-helper.js.map +1 -0
  19. package/lib/commonjs/common/utils.js +20 -2
  20. package/lib/commonjs/common/utils.js.map +1 -1
  21. package/lib/commonjs/eedl/eedl.js +198 -44
  22. package/lib/commonjs/eedl/eedl.js.map +1 -1
  23. package/lib/commonjs/index.js +290 -48
  24. package/lib/commonjs/index.js.map +1 -1
  25. package/lib/commonjs/models/mp-client-sdk.js +17 -10
  26. package/lib/commonjs/models/mp-client-sdk.js.map +1 -1
  27. package/lib/commonjs/processors/data-element.processor.js +51 -7
  28. package/lib/commonjs/processors/data-element.processor.js.map +1 -1
  29. package/lib/commonjs/processors/visit-id.processor.js +78 -15
  30. package/lib/commonjs/processors/visit-id.processor.js.map +1 -1
  31. package/lib/module/common/app-types.js.map +1 -1
  32. package/lib/module/common/constants.js +11 -2
  33. package/lib/module/common/constants.js.map +1 -1
  34. package/lib/module/common/data-store.js +13 -30
  35. package/lib/module/common/data-store.js.map +1 -1
  36. package/lib/module/common/deeplink-helper.js +168 -0
  37. package/lib/module/common/deeplink-helper.js.map +1 -0
  38. package/lib/module/common/device-info-helper.js +161 -0
  39. package/lib/module/common/device-info-helper.js.map +1 -0
  40. package/lib/module/common/event-bus.js +39 -0
  41. package/lib/module/common/event-bus.js.map +1 -1
  42. package/lib/module/common/network-service.js +119 -15
  43. package/lib/module/common/network-service.js.map +1 -1
  44. package/lib/module/common/reporter.js +29 -10
  45. package/lib/module/common/reporter.js.map +1 -1
  46. package/lib/module/common/storage-helper.js +221 -0
  47. package/lib/module/common/storage-helper.js.map +1 -0
  48. package/lib/module/common/utils.js +20 -2
  49. package/lib/module/common/utils.js.map +1 -1
  50. package/lib/module/eedl/eedl.js +198 -44
  51. package/lib/module/eedl/eedl.js.map +1 -1
  52. package/lib/module/index.js +279 -47
  53. package/lib/module/index.js.map +1 -1
  54. package/lib/module/models/mp-client-sdk.js +16 -9
  55. package/lib/module/models/mp-client-sdk.js.map +1 -1
  56. package/lib/module/processors/data-element.processor.js +51 -7
  57. package/lib/module/processors/data-element.processor.js.map +1 -1
  58. package/lib/module/processors/visit-id.processor.js +78 -15
  59. package/lib/module/processors/visit-id.processor.js.map +1 -1
  60. package/lib/typescript/{common → src/common}/app-types.d.ts +29 -9
  61. package/lib/typescript/{common → src/common}/constants.d.ts +0 -1
  62. package/lib/typescript/{common → src/common}/data-store.d.ts +3 -8
  63. package/lib/typescript/src/common/deeplink-helper.d.ts +60 -0
  64. package/lib/typescript/src/common/device-info-helper.d.ts +54 -0
  65. package/lib/typescript/src/common/event-bus.d.ts +21 -0
  66. package/lib/typescript/src/common/network-service.d.ts +32 -0
  67. package/lib/typescript/src/common/storage-helper.d.ts +47 -0
  68. package/lib/typescript/{common → src/common}/utils.d.ts +7 -0
  69. package/lib/typescript/{eedl → src/eedl}/eedl.d.ts +43 -1
  70. package/lib/typescript/{index.d.ts → src/index.d.ts} +39 -5
  71. package/lib/typescript/{models → src/models}/mp-client-sdk.d.ts +7 -0
  72. package/lib/typescript/src/processors/visit-id.processor.d.ts +23 -0
  73. package/package.json +26 -37
  74. package/src/common/app-types.ts +32 -10
  75. package/src/common/constants.ts +0 -6
  76. package/src/common/data-store.ts +8 -30
  77. package/src/common/deeplink-helper.ts +181 -0
  78. package/src/common/device-info-helper.ts +190 -0
  79. package/src/common/event-bus.ts +39 -0
  80. package/src/common/network-service.ts +154 -21
  81. package/src/common/reporter.ts +31 -10
  82. package/src/common/storage-helper.ts +266 -0
  83. package/src/common/utils.ts +20 -2
  84. package/src/eedl/eedl.ts +225 -51
  85. package/src/index.tsx +332 -67
  86. package/src/models/mp-client-sdk.ts +8 -0
  87. package/src/processors/data-element.processor.ts +85 -7
  88. package/src/processors/visit-id.processor.ts +92 -22
  89. package/lib/commonjs/processors/trans-function.processor.js +0 -73
  90. package/lib/commonjs/processors/trans-function.processor.js.map +0 -1
  91. package/lib/module/processors/trans-function.processor.js +0 -66
  92. package/lib/module/processors/trans-function.processor.js.map +0 -1
  93. package/lib/typescript/common/event-bus.d.ts +0 -6
  94. package/lib/typescript/common/network-service.d.ts +0 -8
  95. package/lib/typescript/processors/trans-function.processor.d.ts +0 -12
  96. package/lib/typescript/processors/visit-id.processor.d.ts +0 -9
  97. package/src/processors/trans-function.processor.ts +0 -85
  98. /package/lib/typescript/{common → src/common}/logger.d.ts +0 -0
  99. /package/lib/typescript/{common → src/common}/reporter.d.ts +0 -0
  100. /package/lib/typescript/{models → src/models}/geo-api-response.d.ts +0 -0
  101. /package/lib/typescript/{processors → src/processors}/data-element.processor.d.ts +0 -0
  102. /package/lib/typescript/{processors → src/processors}/geo-location.processor.d.ts +0 -0
  103. /package/lib/typescript/{processors → src/processors}/qc.processor.d.ts +0 -0
  104. /package/lib/typescript/{processors → src/processors}/tag.processor.d.ts +0 -0
@@ -4,7 +4,6 @@ import type {
4
4
  MapLike,
5
5
  MpDeviceInfo,
6
6
  QcInfoItem,
7
- RunQueueModel,
8
7
  SdkInitOptions,
9
8
  UrlInfo,
10
9
  VisitInfo,
@@ -21,8 +20,7 @@ import type {
21
20
  } from '../models/mp-client-sdk';
22
21
  import { Platform } from 'react-native';
23
22
  import { Utils } from './utils';
24
-
25
- const CORE_VERSION = '0.8.6';
23
+ import { version as SDK_VERSION } from '../../package.json';
26
24
 
27
25
  export class DataStore {
28
26
  private static isDataStoreInitialized = false;
@@ -34,14 +32,12 @@ export class DataStore {
34
32
  private static clientSdk: MpClientSdk;
35
33
  private static urls: UrlInfo;
36
34
  private static debugId: string;
37
- private static visitorInfo: VisitorInfo;
35
+ private static visitorInfo: VisitorInfo | null;
38
36
  private static visitInfo: VisitInfo;
39
37
  private static pageLang = 'en';
40
38
  private static mpEnvShort: 'stg' | 'prd';
41
39
  private static devicePlatform = 'unknown';
42
40
  private static deviceType = 'mobile';
43
- private static runQueue: RunQueueModel[] = [];
44
- private static isProcessing = false;
45
41
  private static sdkInitOptions: SdkInitOptions;
46
42
  private static deviceInfo: MpDeviceInfo;
47
43
  private static geoInfo: GeoResponse;
@@ -117,16 +113,6 @@ export class DataStore {
117
113
  return true;
118
114
  }
119
115
 
120
- static enQueueTMFire(
121
- sseOnly: boolean,
122
- name: string,
123
- dcrName: string,
124
- eventId: string,
125
- dcrPayload?: Record<string, any>
126
- ): void {
127
- this.runQueue.push({ sseOnly, name, dcrName, eventId, dcrPayload });
128
- }
129
-
130
116
  static isDataStoreReady(): boolean {
131
117
  return this.isDataStoreInitialized;
132
118
  }
@@ -163,18 +149,6 @@ export class DataStore {
163
149
  this.sdkInitOptions = options;
164
150
  }
165
151
 
166
- static deQueueTMFire(): RunQueueModel {
167
- return this.runQueue.length > 0 ? this.runQueue.pop() : null;
168
- }
169
-
170
- static isTagManagerProcessing(): boolean {
171
- return this.isProcessing;
172
- }
173
-
174
- static setTagManagerProcessing(status: boolean): void {
175
- this.isProcessing = status;
176
- }
177
-
178
152
  static getSdkDataElements(): MapLike<ClientSdkDeItem> {
179
153
  return this.clientSdk?.d || {};
180
154
  }
@@ -217,10 +191,14 @@ export class DataStore {
217
191
  : undefined;
218
192
  }
219
193
 
220
- static setVisitorInfo(visitorInfo: VisitorInfo): void {
194
+ static setVisitorInfo(visitorInfo: VisitorInfo | null): void {
221
195
  this.visitorInfo = visitorInfo;
222
196
  }
223
197
 
198
+ static getVisitorInfo(): VisitorInfo | null {
199
+ return this.visitorInfo;
200
+ }
201
+
224
202
  static getDebugId(): string {
225
203
  return this.debugId;
226
204
  }
@@ -380,7 +358,7 @@ export class DataStore {
380
358
  }
381
359
 
382
360
  static getCoreVersion(): string {
383
- return CORE_VERSION;
361
+ return SDK_VERSION;
384
362
  }
385
363
 
386
364
  static getMpEnv(): 'stg' | 'prd' {
@@ -0,0 +1,181 @@
1
+ import { Linking } from 'react-native';
2
+ import { Logger } from './logger';
3
+
4
+ export enum DeepLinkType {
5
+ CUSTOM_SCHEME = 'custom_scheme', // myapp://
6
+ UNIVERSAL_LINK = 'universal_link', // https:// (iOS Universal Links)
7
+ APP_LINK = 'app_link', // https:// (Android App Links)
8
+ HTTP_LINK = 'http_link', // http://
9
+ }
10
+
11
+ /**
12
+ * Helper class to automatically detect and handle deeplinks
13
+ *
14
+ * Supports multiple link types:
15
+ * 1. Custom URL Schemes: myapp://product/123
16
+ * 2. Universal Links (iOS): https://my-site.com/product/123
17
+ * 3. App Links (Android): https://my-site.com/product/123
18
+ * 4. HTTP Links: http://my-site.com/product/123
19
+ *
20
+ * Common use cases:
21
+ * - Ad clicks (Facebook, Google, TikTok ads with http/https URLs)
22
+ * - Push notification deep links
23
+ * - Email marketing links
24
+ * - SMS campaign links
25
+ * - QR codes
26
+ */
27
+ export class DeepLinkHelper {
28
+ private static listener: any = null;
29
+ private static onDeepLinkCallback:
30
+ | ((url: string, linkType: DeepLinkType) => void)
31
+ | null = null;
32
+
33
+ /**
34
+ * Determine the type of deeplink
35
+ * @param url The URL to analyze
36
+ * @returns The type of deeplink
37
+ */
38
+ static getLinkType(url: string): DeepLinkType {
39
+ try {
40
+ if (url.startsWith('https://')) {
41
+ // Universal Links (iOS) and App Links (Android) both use HTTPS
42
+ return DeepLinkType.UNIVERSAL_LINK;
43
+ } else if (url.startsWith('http://')) {
44
+ return DeepLinkType.HTTP_LINK;
45
+ } else {
46
+ // Custom URL scheme (e.g., myapp://)
47
+ return DeepLinkType.CUSTOM_SCHEME;
48
+ }
49
+ } catch (error) {
50
+ Logger.logError('Error determining link type:', error);
51
+ return DeepLinkType.CUSTOM_SCHEME;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Initialize deeplink detection
57
+ * - Checks if app was opened with initial URL (custom scheme, universal link, or HTTP link)
58
+ * - Sets up listener for deeplinks while app is running
59
+ * @param callback Optional callback to be notified when deeplink is detected
60
+ * @returns The initial URL if app was opened with deeplink, null otherwise
61
+ */
62
+ static async initialize(
63
+ callback?: (url: string, linkType: DeepLinkType) => void
64
+ ): Promise<string | null> {
65
+ try {
66
+ // Store callback if provided
67
+ if (callback) {
68
+ this.onDeepLinkCallback = callback;
69
+ }
70
+
71
+ // Check if app was opened with a deeplink
72
+ // This works for:
73
+ // - Custom schemes: myapp://product/123
74
+ // - Universal Links (iOS): https://my-site.com/product/123
75
+ // - App Links (Android): https://my-site.com/product/123
76
+ // - HTTP links: http://my-site.com/product/123
77
+ const initialUrl = await Linking.getInitialURL();
78
+
79
+ if (initialUrl) {
80
+ const linkType = this.getLinkType(initialUrl);
81
+ Logger.logDbg(`App opened with initial ${linkType}:`, initialUrl);
82
+ this.handleDeepLink(initialUrl);
83
+ }
84
+
85
+ // ALWAYS set up listener for deeplinks while app is running
86
+ // This ensures we can detect deeplinks even if app was opened with initial deeplink
87
+ this.setupListener();
88
+
89
+ return initialUrl;
90
+ } catch (error) {
91
+ Logger.logError('Error initializing deeplink detection:', error);
92
+ return null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Set up listener for deeplinks received while app is running
98
+ * Works for all link types: custom schemes, universal links, app links, HTTP links
99
+ */
100
+ private static setupListener(): void {
101
+ // Remove existing listener if any
102
+ if (this.listener) {
103
+ this.listener.remove();
104
+ }
105
+
106
+ // Listen for deeplinks while app is running (e.g., user clicks link while app is open)
107
+ this.listener = Linking.addEventListener('url', (event) => {
108
+ const linkType = this.getLinkType(event.url);
109
+ Logger.logDbg(`Deeplink received (${linkType}):`, event.url);
110
+ this.handleDeepLink(event.url);
111
+ });
112
+
113
+ Logger.logDbg(
114
+ 'Deeplink listener set up (supports custom schemes, universal/app links, HTTP links)'
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Handle deeplink URL - notify callback if registered
120
+ * @param url The deeplink URL
121
+ */
122
+ private static handleDeepLink(url: string): void {
123
+ if (this.onDeepLinkCallback) {
124
+ const linkType = this.getLinkType(url);
125
+ this.onDeepLinkCallback(url, linkType);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Remove deeplink listener and clear callback
131
+ */
132
+ static cleanup(): void {
133
+ if (this.listener) {
134
+ this.listener.remove();
135
+ this.listener = null;
136
+ }
137
+ this.onDeepLinkCallback = null;
138
+ Logger.logDbg('Deeplink listener and callback cleared');
139
+ }
140
+
141
+ /**
142
+ * Parse UTM parameters and other campaign data from URL
143
+ * @param url The deeplink URL
144
+ * @returns Object containing campaign parameters
145
+ */
146
+ static parseCampaignParams(url: string): Record<string, string> {
147
+ try {
148
+ const urlObj = new URL(url);
149
+ const params: Record<string, string> = {};
150
+
151
+ // Common campaign parameters
152
+ const campaignParams = [
153
+ 'utm_source',
154
+ 'utm_medium',
155
+ 'utm_campaign',
156
+ 'utm_term',
157
+ 'utm_content',
158
+ 'utm_id',
159
+ 'gclid', // Google Click ID
160
+ 'fbclid', // Facebook Click ID
161
+ 'ttclid', // TikTok Click ID
162
+ 'campaign_id',
163
+ 'ad_id',
164
+ 'adset_id',
165
+ 'creative_id',
166
+ ];
167
+
168
+ campaignParams.forEach((param) => {
169
+ const value = urlObj.searchParams.get(param);
170
+ if (value) {
171
+ params[param] = value;
172
+ }
173
+ });
174
+
175
+ return params;
176
+ } catch (error) {
177
+ Logger.logError('Error parsing campaign params from URL:', error);
178
+ return {};
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,190 @@
1
+ import { Platform } from 'react-native';
2
+ import { Logger } from './logger';
3
+
4
+ interface DeviceAppInfo {
5
+ app_version: string;
6
+ build_number: string;
7
+ package_name: string;
8
+ device_model_name: string;
9
+ device_manufacturer: string;
10
+ device_id: string;
11
+ os_name: string;
12
+ os_version: string;
13
+ is_tablet: boolean;
14
+ is_emulator: boolean;
15
+ }
16
+
17
+ /**
18
+ * Helper class to automatically detect device and app information
19
+ * Supports both Expo and bare React Native:
20
+ * 1. Tries Expo modules first (works in Expo Go)
21
+ * 2. Falls back to react-native-device-info (for bare RN)
22
+ * 3. Returns safe defaults if neither is available
23
+ */
24
+ export class DeviceInfoHelper {
25
+ private static expoApplication: any = null;
26
+ private static expoDevice: any = null;
27
+ private static expoConstants: any = null;
28
+ private static deviceInfo: any = null;
29
+ private static initialized = false;
30
+
31
+ /**
32
+ * Try to load Expo modules and react-native-device-info
33
+ * This uses dynamic imports to avoid errors when modules aren't available
34
+ */
35
+ private static async initialize(): Promise<void> {
36
+ if (this.initialized) return;
37
+
38
+ // Try to load Expo modules first (works in Expo Go)
39
+ try {
40
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
41
+ this.expoApplication = require('expo-application');
42
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
43
+ this.expoDevice = require('expo-device');
44
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
45
+ this.expoConstants = require('expo-constants').default;
46
+ Logger.logDbg('Using Expo modules for device info');
47
+ } catch (error) {
48
+ Logger.logDbg(
49
+ 'Expo modules not available, will try react-native-device-info'
50
+ );
51
+ }
52
+
53
+ // Fall back to react-native-device-info (for bare RN)
54
+ if (!this.expoApplication) {
55
+ try {
56
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
57
+ this.deviceInfo = require('react-native-device-info').default;
58
+ Logger.logDbg('Using react-native-device-info for device info');
59
+ } catch (error) {
60
+ Logger.logError(
61
+ 'Neither Expo modules nor react-native-device-info available:',
62
+ error
63
+ );
64
+ }
65
+ }
66
+
67
+ this.initialized = true;
68
+ }
69
+
70
+ /**
71
+ * Get comprehensive app and device information
72
+ * All methods are async-safe and handle errors gracefully
73
+ */
74
+ static async getAppInfo(): Promise<DeviceAppInfo> {
75
+ await this.initialize();
76
+
77
+ try {
78
+ // Try Expo modules first
79
+ if (this.expoApplication && this.expoDevice) {
80
+ return await this.getAppInfoFromExpo();
81
+ }
82
+
83
+ // Fall back to react-native-device-info
84
+ if (this.deviceInfo) {
85
+ return await this.getAppInfoFromDeviceInfo();
86
+ }
87
+
88
+ // Return safe fallback values
89
+ return this.getSafeDefaults();
90
+ } catch (error) {
91
+ Logger.logError('Error getting device info:', error);
92
+ return this.getSafeDefaults();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get device info using Expo modules (works in Expo Go)
98
+ */
99
+ private static async getAppInfoFromExpo(): Promise<DeviceAppInfo> {
100
+ const version = this.expoApplication.nativeApplicationVersion || 'unknown';
101
+ const buildNumber = this.expoApplication.nativeBuildVersion || 'unknown';
102
+ const packageName = this.expoApplication.applicationId || 'unknown';
103
+
104
+ return {
105
+ // App information
106
+ app_version: version,
107
+ build_number: buildNumber,
108
+ package_name: packageName,
109
+
110
+ // Device information
111
+ device_model_name: this.expoDevice.modelName || 'unknown',
112
+ device_manufacturer: this.expoDevice.manufacturer || 'unknown',
113
+ device_id: this.expoConstants.sessionId || 'unknown',
114
+
115
+ // OS information
116
+ os_name: Platform.OS,
117
+ os_version:
118
+ this.expoDevice.osVersion || Platform.Version?.toString() || 'unknown',
119
+
120
+ // Additional useful info
121
+ // DeviceType enum: UNKNOWN=0, PHONE=1, TABLET=2, DESKTOP=3, TV=4
122
+ is_tablet: this.expoDevice.deviceType === 2, // DeviceType.TABLET = 2
123
+ is_emulator: !this.expoDevice.isDevice,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Get device info using react-native-device-info (for bare RN)
129
+ */
130
+ private static async getAppInfoFromDeviceInfo(): Promise<DeviceAppInfo> {
131
+ const [version, buildNumber, uniqueId, bundleId] = await Promise.all([
132
+ this.deviceInfo.getVersion(),
133
+ this.deviceInfo.getBuildNumber(),
134
+ this.deviceInfo.getUniqueId(),
135
+ this.deviceInfo.getBundleId(),
136
+ ]);
137
+
138
+ return {
139
+ // App information
140
+ app_version: version,
141
+ build_number: buildNumber,
142
+ package_name: bundleId,
143
+
144
+ // Device information
145
+ device_model_name: this.deviceInfo.getModel(),
146
+ device_manufacturer: this.deviceInfo.getManufacturerSync(),
147
+ device_id: uniqueId,
148
+
149
+ // OS information
150
+ os_name: Platform.OS,
151
+ os_version: this.deviceInfo.getSystemVersion(),
152
+
153
+ // Additional useful info
154
+ is_tablet: this.deviceInfo.isTablet(),
155
+ is_emulator: await this.deviceInfo.isEmulator(),
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Safe default values when no device info library is available
161
+ */
162
+ private static getSafeDefaults(): DeviceAppInfo {
163
+ return {
164
+ app_version: 'unknown',
165
+ build_number: 'unknown',
166
+ package_name: 'unknown',
167
+ device_model_name: 'unknown',
168
+ device_manufacturer: 'unknown',
169
+ device_id: 'unknown',
170
+ os_name: Platform.OS,
171
+ os_version: Platform.Version?.toString() || 'unknown',
172
+ is_tablet: false,
173
+ is_emulator: false,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Get app version in format: "1.0.0 (123)"
179
+ * Combines version and build number for better tracking
180
+ */
181
+ static async getFormattedAppVersion(): Promise<string> {
182
+ try {
183
+ const info = await this.getAppInfo();
184
+ return `${info.app_version} (${info.build_number})`;
185
+ } catch (error) {
186
+ Logger.logError('Error getting formatted app version:', error);
187
+ return 'unknown';
188
+ }
189
+ }
190
+ }
@@ -16,6 +16,45 @@ export class EventBus {
16
16
  }
17
17
  }
18
18
 
19
+ /**
20
+ * Remove a specific callback for an event
21
+ * @param eventName Event name to remove callback from
22
+ * @param callback The callback function to remove
23
+ */
24
+ static off(eventName: string, callback: EventBusCallback): void {
25
+ if (this.eventRegister.has(eventName)) {
26
+ const callbacks = this.eventRegister.get(eventName);
27
+ const index = callbacks.indexOf(callback);
28
+ if (index > -1) {
29
+ callbacks.splice(index, 1);
30
+ Logger.logDbg(`Removed listener for event: ${eventName}`);
31
+ }
32
+ // Clean up empty arrays
33
+ if (callbacks.length === 0) {
34
+ this.eventRegister.delete(eventName);
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Remove all callbacks for a specific event
41
+ * @param eventName Event name to clear all listeners for
42
+ */
43
+ static clearEvent(eventName: string): void {
44
+ if (this.eventRegister.has(eventName)) {
45
+ this.eventRegister.delete(eventName);
46
+ Logger.logDbg(`Cleared all listeners for event: ${eventName}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Remove all event listeners (used during SDK shutdown)
52
+ */
53
+ static clearAll(): void {
54
+ this.eventRegister.clear();
55
+ Logger.logDbg('Cleared all event listeners');
56
+ }
57
+
19
58
  static triggerEvent(eventName: string, payload: any): void {
20
59
  if (this.eventRegister.has(eventName)) {
21
60
  for (const cb of this.eventRegister.get(eventName)) {
@@ -1,40 +1,173 @@
1
1
  import { Logger } from './logger';
2
2
  import type { MapLike, SdkInitOptions, VisitorInfo } from './app-types';
3
3
  import { DataStore } from './data-store';
4
- import { Reporter } from './reporter';
5
4
  import type { MpClientSdk } from '../models/mp-client-sdk';
6
5
 
6
+ const DEFAULT_RETRY_COUNT = 3;
7
+ const DEFAULT_RETRY_DELAY_MS = 1000;
8
+ const DEFAULT_TIMEOUT_MS = 10000;
9
+
7
10
  export class NetworkService {
8
- public static async fetchIdlInfo(url: string): Promise<VisitorInfo> {
9
- const response = await NetworkService.sendNetworkRequest('get', url);
10
- if (response) {
11
- return response.json();
12
- } else {
11
+ /**
12
+ * Sleep for a specified duration
13
+ */
14
+ private static sleep(ms: number): Promise<void> {
15
+ return new Promise((resolve) => setTimeout(resolve, ms));
16
+ }
17
+
18
+ /**
19
+ * Fetch with timeout wrapper
20
+ */
21
+ private static async fetchWithTimeout(
22
+ url: string,
23
+ options: RequestInit,
24
+ timeoutMs: number = DEFAULT_TIMEOUT_MS
25
+ ): Promise<Response> {
26
+ const controller = new AbortController();
27
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
28
+
29
+ try {
30
+ const response = await fetch(url, {
31
+ ...options,
32
+ signal: controller.signal,
33
+ });
34
+ return response;
35
+ } finally {
36
+ clearTimeout(timeoutId);
37
+ }
38
+ }
39
+ /**
40
+ * Fetch visitor/identity info from server
41
+ * Returns null if fetch fails (caller should handle retry logic)
42
+ */
43
+ public static async fetchIdlInfo(url: string): Promise<VisitorInfo | null> {
44
+ try {
45
+ const response = await this.fetchWithTimeout(
46
+ url,
47
+ {
48
+ method: 'GET',
49
+ headers: {
50
+ 'Content-Type': 'text/plain',
51
+ 'cache-control': 'no-store',
52
+ ...DataStore.getCommonCookies(),
53
+ },
54
+ },
55
+ DEFAULT_TIMEOUT_MS
56
+ );
57
+
58
+ if (response && response.ok) {
59
+ return response.json();
60
+ } else {
61
+ Logger.logError(
62
+ `Failed to fetch IDL info: ${response?.status} ${response?.statusText}`
63
+ );
64
+ return null;
65
+ }
66
+ } catch (err) {
67
+ Logger.logError('Error fetching IDL info:', err);
13
68
  return null;
14
69
  }
15
70
  }
16
71
 
72
+ /**
73
+ * Fetch visitor/identity info with retry logic
74
+ * @param url The IDL URL
75
+ * @param retries Number of retries (default: 3)
76
+ * @returns VisitorInfo or null if all retries fail
77
+ */
78
+ public static async fetchIdlInfoWithRetry(
79
+ url: string,
80
+ retries: number = DEFAULT_RETRY_COUNT
81
+ ): Promise<VisitorInfo | null> {
82
+ let lastError: Error | null = null;
83
+
84
+ for (let attempt = 1; attempt <= retries; attempt++) {
85
+ try {
86
+ Logger.logDbg(`Fetching IDL info (attempt ${attempt}/${retries})`);
87
+ const result = await this.fetchIdlInfo(url);
88
+
89
+ if (result) {
90
+ Logger.logDbg('IDL info fetched successfully');
91
+ return result;
92
+ }
93
+ } catch (err) {
94
+ lastError = err instanceof Error ? err : new Error(String(err));
95
+ Logger.logError(`IDL fetch attempt ${attempt} failed:`, err);
96
+ }
97
+
98
+ // Wait before retry (exponential backoff)
99
+ if (attempt < retries) {
100
+ const delay = DEFAULT_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
101
+ Logger.logDbg(`Retrying IDL fetch in ${delay}ms...`);
102
+ await this.sleep(delay);
103
+ }
104
+ }
105
+
106
+ Logger.logError(`All ${retries} IDL fetch attempts failed`, lastError);
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Refresh client SDK JSON configuration with retry logic
112
+ * @param sdkInitOptions SDK initialization options
113
+ * @returns true if successful, throws Error if all retries fail
114
+ */
17
115
  public static async refreshClientSdkJson(
18
116
  sdkInitOptions: SdkInitOptions
19
117
  ): Promise<void> {
20
- try {
21
- const response = await this.sendNetworkRequest(
22
- 'get',
23
- `${sdkInitOptions.baseUrl}/${sdkInitOptions.projectId}${
24
- sdkInitOptions.env === 'staging' ? '-staging' : ''
25
- }.json`
26
- );
118
+ const url = `${sdkInitOptions.baseUrl}/${sdkInitOptions.projectId}${
119
+ sdkInitOptions.env === 'staging' ? '-staging' : ''
120
+ }.json`;
121
+
122
+ let lastError: Error | null = null;
27
123
 
28
- if (response) {
29
- const sdkJson: MpClientSdk = await response.json();
30
- Logger.logDbg('Client Sdk:: ', sdkJson);
31
- // sdkJson.s.c_url = 'http://localhost:3003';
32
- await DataStore.init(sdkJson);
124
+ for (let attempt = 1; attempt <= DEFAULT_RETRY_COUNT; attempt++) {
125
+ try {
126
+ Logger.logDbg(
127
+ `Fetching SDK config (attempt ${attempt}/${DEFAULT_RETRY_COUNT}): ${url}`
128
+ );
129
+
130
+ const response = await this.fetchWithTimeout(
131
+ url,
132
+ {
133
+ method: 'GET',
134
+ headers: {
135
+ 'Content-Type': 'text/plain',
136
+ 'cache-control': 'no-store',
137
+ },
138
+ },
139
+ DEFAULT_TIMEOUT_MS
140
+ );
141
+
142
+ if (response && response.ok) {
143
+ const sdkJson: MpClientSdk = await response.json();
144
+ Logger.logDbg('Client SDK config fetched successfully');
145
+ await DataStore.init(sdkJson);
146
+ return; // Success - exit the retry loop
147
+ } else {
148
+ throw new Error(
149
+ `HTTP ${response?.status}: ${
150
+ response?.statusText || 'Unknown error'
151
+ }`
152
+ );
153
+ }
154
+ } catch (err) {
155
+ lastError = err instanceof Error ? err : new Error(String(err));
156
+ Logger.logError(`SDK config fetch attempt ${attempt} failed:`, err);
157
+
158
+ // Wait before retry (exponential backoff)
159
+ if (attempt < DEFAULT_RETRY_COUNT) {
160
+ const delay = DEFAULT_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
161
+ Logger.logDbg(`Retrying SDK config fetch in ${delay}ms...`);
162
+ await this.sleep(delay);
163
+ }
33
164
  }
34
- } catch (err) {
35
- Reporter.reportError('refreshClientSdk', err);
36
- // throw new Error('Unable to fetch MagicPixel SDK at this time.');
37
165
  }
166
+
167
+ // All retries failed - throw error so caller can handle it
168
+ throw new Error(
169
+ `Failed to fetch SDK configuration after ${DEFAULT_RETRY_COUNT} attempts: ${lastError?.message}`
170
+ );
38
171
  }
39
172
 
40
173
  public static sendPostRequest(url: string, body: MapLike): void {