@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
package/src/eedl/eedl.ts CHANGED
@@ -2,18 +2,15 @@ import { Utils } from '../common/utils';
2
2
  import type { EventProcessorFn, MapLike, TypedAny } from '../common/app-types';
3
3
  import { Logger } from '../common/logger';
4
4
  import AsyncStorage from '@react-native-async-storage/async-storage';
5
- import { customAlphabet } from 'nanoid/non-secure';
5
+ import { ulid } from 'ulid';
6
6
 
7
7
  const eventsToPersist: Record<string, string> = {
8
8
  user_info: '_mpPendingUserInfo',
9
9
  mp_purchase: '_mpPendingPurchase',
10
10
  };
11
11
 
12
- // UUID v4 generator for React Native
13
- const getUUIDV4 = customAlphabet(
14
- '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
15
- 36
16
- );
12
+ // Maximum number of events to keep in memory for state tracking and queues
13
+ const MAX_TRACKED_EVENTS = 500;
17
14
 
18
15
  export class MpDataLayerHelper {
19
16
  isReady = false;
@@ -24,8 +21,14 @@ export class MpDataLayerHelper {
24
21
  dlInitEvent: string;
25
22
  receivedInitialEvent = false;
26
23
  eventQueue: Array<any> = [];
24
+ private isProcessing = false;
27
25
  private isEntryPoint = 0;
28
26
 
27
+ // Event deduplication
28
+ private eventDedupWindowMs = 5000; // Default 5 seconds
29
+ private eventDeduplicationCache: Map<string, number> = new Map();
30
+ private purchaseTransactionCache: Map<string, number> = new Map();
31
+
29
32
  constructor(
30
33
  private readonly dlEventName: string,
31
34
  private readonly dlInitMode: string,
@@ -72,8 +75,135 @@ export class MpDataLayerHelper {
72
75
  }
73
76
  }
74
77
 
78
+ /**
79
+ * Set the event deduplication window
80
+ * @param windowMs Window in milliseconds (0 to disable)
81
+ */
82
+ setDeduplicationWindow(windowMs: number): void {
83
+ this.eventDedupWindowMs = windowMs;
84
+ Logger.logDbg(`Event deduplication window set to: ${windowMs}ms`);
85
+ }
86
+
87
+ /**
88
+ * Generate a unique fingerprint for an event
89
+ * @param eventName Event name
90
+ * @param payload Event payload
91
+ * @returns Fingerprint string
92
+ */
93
+ private getEventFingerprint(eventName: string, payload: MapLike): string {
94
+ try {
95
+ // Use JSON.stringify for deterministic payload representation
96
+ const payloadStr = JSON.stringify(payload);
97
+ return `${eventName}::${payloadStr}`;
98
+ } catch (err) {
99
+ Logger.logError('Error generating event fingerprint', err);
100
+ return `${eventName}::${Date.now()}`; // Fallback to never match
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Extract transaction_id from purchase event payload
106
+ * @param payload Event payload
107
+ * @returns transaction_id or null
108
+ */
109
+ private extractTransactionId(payload: MapLike): string | null {
110
+ // Check common field names for transaction_id
111
+ return (
112
+ payload.transaction_id ||
113
+ payload.transactionId ||
114
+ payload.orderId ||
115
+ payload.order_id ||
116
+ null
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Check if event is a duplicate within the deduplication window
122
+ * @param eventName Event name
123
+ * @param payload Event payload
124
+ * @returns true if duplicate, false otherwise
125
+ */
126
+ private isDuplicateEvent(eventName: string, payload: MapLike): boolean {
127
+ // If deduplication is disabled (window = 0), never treat as duplicate
128
+ if (this.eventDedupWindowMs === 0) {
129
+ return false;
130
+ }
131
+
132
+ const now = Date.now();
133
+
134
+ // Special handling for purchase events - deduplicate by transaction_id
135
+ if (eventName === 'purchase' || eventName === 'mp_purchase') {
136
+ const transactionId = this.extractTransactionId(payload);
137
+
138
+ if (transactionId) {
139
+ const lastSeenTs = this.purchaseTransactionCache.get(transactionId);
140
+
141
+ if (lastSeenTs && now - lastSeenTs < this.eventDedupWindowMs) {
142
+ Logger.logDbg(
143
+ `Duplicate purchase event detected (transaction_id: ${transactionId}) within ${this.eventDedupWindowMs}ms window, skipping`
144
+ );
145
+ return true; // Duplicate
146
+ }
147
+
148
+ // Record this transaction
149
+ this.purchaseTransactionCache.set(transactionId, now);
150
+ } else {
151
+ Logger.logDbg(
152
+ 'Purchase event without transaction_id, cannot deduplicate by transaction'
153
+ );
154
+ }
155
+ }
156
+
157
+ // General event deduplication by full fingerprint
158
+ const fingerprint = this.getEventFingerprint(eventName, payload);
159
+ const lastSeenTs = this.eventDeduplicationCache.get(fingerprint);
160
+
161
+ if (lastSeenTs && now - lastSeenTs < this.eventDedupWindowMs) {
162
+ Logger.logDbg(
163
+ `Duplicate event detected (${eventName}) within ${this.eventDedupWindowMs}ms window, skipping`
164
+ );
165
+ return true; // Duplicate
166
+ }
167
+
168
+ // Record this event
169
+ this.eventDeduplicationCache.set(fingerprint, now);
170
+
171
+ // Cleanup old entries periodically
172
+ this.cleanupDeduplicationCache(now);
173
+
174
+ return false;
175
+ }
176
+
177
+ /**
178
+ * Remove expired entries from deduplication caches
179
+ * @param now Current timestamp
180
+ */
181
+ private cleanupDeduplicationCache(now: number): void {
182
+ // Cleanup general event cache
183
+ for (const [
184
+ fingerprint,
185
+ timestamp,
186
+ ] of this.eventDeduplicationCache.entries()) {
187
+ if (now - timestamp > this.eventDedupWindowMs) {
188
+ this.eventDeduplicationCache.delete(fingerprint);
189
+ }
190
+ }
191
+
192
+ // Cleanup purchase transaction cache
193
+ for (const [
194
+ transactionId,
195
+ timestamp,
196
+ ] of this.purchaseTransactionCache.entries()) {
197
+ if (now - timestamp > this.eventDedupWindowMs) {
198
+ this.purchaseTransactionCache.delete(transactionId);
199
+ }
200
+ }
201
+ }
202
+
75
203
  pushEvent(eventName: string, payload: MapLike): void {
76
204
  Logger.logDbg('EV Push Event:: ', JSON.stringify(payload));
205
+
206
+ // Special events bypass queue and process immediately (no deduplication)
77
207
  if (
78
208
  eventName === 'set' ||
79
209
  eventName === 'persist' ||
@@ -84,22 +214,33 @@ export class MpDataLayerHelper {
84
214
  this.processQItems(eventName, payload).catch((err) =>
85
215
  Logger.logError('Error processing event', err)
86
216
  );
87
- } else {
88
- if (!this.receivedInitialEvent) {
89
- // set only if this is not true already
90
- this.receivedInitialEvent = eventName === this.dlInitEvent;
91
- this.eventQueue.push([eventName, payload]);
92
- if (this.isReady && this.receivedInitialEvent) {
93
- this.ready();
94
- }
95
- } else if (!this.isReady) {
96
- this.eventQueue.push([eventName, payload]);
97
- } else {
98
- // fire and forget for async operations
99
- this.processQItems(eventName, payload).catch((err) =>
100
- Logger.logError('Error processing event', err)
101
- );
102
- }
217
+ return;
218
+ }
219
+
220
+ // Check for duplicate events BEFORE queuing
221
+ if (this.isDuplicateEvent(eventName, payload)) {
222
+ // Duplicate detected, skip this event
223
+ return;
224
+ }
225
+
226
+ // Track if we received the initial event
227
+ if (!this.receivedInitialEvent) {
228
+ this.receivedInitialEvent = eventName === this.dlInitEvent;
229
+ }
230
+
231
+ // Always queue events for sequential processing (with eviction of oldest if at capacity)
232
+ if (this.eventQueue.length >= MAX_TRACKED_EVENTS) {
233
+ const evictedEvent = this.eventQueue.shift();
234
+ Logger.logDbg(
235
+ `Event queue at capacity (${MAX_TRACKED_EVENTS}), evicting oldest event:`,
236
+ evictedEvent?.[0]
237
+ );
238
+ }
239
+ this.eventQueue.push([eventName, payload]);
240
+
241
+ // Trigger processing if ready and initial event received
242
+ if (this.isReady && this.receivedInitialEvent) {
243
+ this.processNext();
103
244
  }
104
245
  }
105
246
 
@@ -292,8 +433,8 @@ export class MpDataLayerHelper {
292
433
  if (eventName === 'reset' || eventName === this.dlInitEvent) {
293
434
  this.reset();
294
435
 
295
- // add an init event based uuid to the core data model
296
- model['page_load_uid'] = getUUIDV4();
436
+ // add an init event based ulid to the core data model
437
+ model['page_load_uid'] = ulid();
297
438
  model['is_entry_point'] = this.getIsEntryPointValue();
298
439
  if (model['is_entry_point'] === 1) {
299
440
  // Note: In React Native, we don't have document.referrer, so we'll use a placeholder
@@ -341,7 +482,10 @@ export class MpDataLayerHelper {
341
482
  })
342
483
  );
343
484
 
344
- // add to state tracker
485
+ // add to state tracker (with eviction of oldest events if at capacity)
486
+ if (this.stateTracker.length >= MAX_TRACKED_EVENTS) {
487
+ this.stateTracker.shift(); // Remove oldest event
488
+ }
345
489
  this.stateTracker.push(eventPayload);
346
490
 
347
491
  // trigger an event that can be listened to by other listeners
@@ -402,16 +546,32 @@ export class MpDataLayerHelper {
402
546
  return true;
403
547
  }
404
548
 
549
+ /**
550
+ * Full shutdown - clears all state and allows reinitialization
551
+ */
552
+ shutdown(): void {
553
+ this.isReady = false;
554
+ this._masterDataLayer = {};
555
+ this._persistedVars = {};
556
+ this.eventProcessors = {};
557
+ this.stateTracker = [];
558
+ this.receivedInitialEvent = false;
559
+ this.eventQueue = [];
560
+ this.isProcessing = false;
561
+ this.isEntryPoint = 0;
562
+ this.eventDeduplicationCache.clear();
563
+ this.purchaseTransactionCache.clear();
564
+ Logger.logDbg('EEDL shutdown complete');
565
+ }
566
+
405
567
  ready(): void {
406
568
  this.isReady = true;
407
569
  if (this.receivedInitialEvent) {
408
570
  Logger.logDbg('Initial event received: ', this.dlInitEvent);
409
- // drain queue only if the initial configured event has been received,
410
- // otherwise mark isReady but dont drain the queue. queue will be drained first when the init
411
- // event is received
412
- this.drainQueue().catch((err) =>
413
- Logger.logError('Error draining queue', err)
414
- );
571
+ // Start processing queue if we have events and initial event received
572
+ if (this.eventQueue.length > 0) {
573
+ this.processNext();
574
+ }
415
575
  } else {
416
576
  Logger.logDbg(
417
577
  `Initial event (${this.dlInitEvent}) NOT received. Events will be queued`
@@ -419,27 +579,41 @@ export class MpDataLayerHelper {
419
579
  }
420
580
  }
421
581
 
422
- async drainQueue(): Promise<void> {
423
- Logger.logDbg('drainQueue...');
424
- // create a copy of the array
425
- const _temp = [...this.eventQueue];
426
- // clean the array
427
- this.eventQueue.splice(0, this.eventQueue.length);
428
- // find the event with dlInit event and execute it first, rest continues
429
- const initialEventObjectIndex = _temp.findIndex(
430
- (e) => [e][0]?.[0] === this.dlInitEvent
431
- );
432
- if (initialEventObjectIndex > -1) {
433
- // splice and execute it
434
- await this.processQItems(
435
- _temp[initialEventObjectIndex][0],
436
- _temp[initialEventObjectIndex][1]
437
- );
438
- _temp.splice(initialEventObjectIndex, 1);
582
+ /**
583
+ * Process next event in queue with atomic check-and-set
584
+ * Ensures only one event processes at a time, eliminating race conditions
585
+ */
586
+ processNext(): void {
587
+ // Atomic check-and-set: if already processing or queue empty, return
588
+ if (this.isProcessing || this.eventQueue.length === 0) {
589
+ return;
439
590
  }
440
- // continue with other items
441
- for (const item of _temp) {
442
- await this.processQItems(item[0], item[1]);
591
+
592
+ this.isProcessing = true;
593
+
594
+ // Dequeue from front for FIFO ordering
595
+ const item = this.eventQueue.shift();
596
+
597
+ if (item) {
598
+ const [eventName, payload] = item;
599
+
600
+ // Process the event
601
+ this.processQItems(eventName, payload)
602
+ .catch((err) => {
603
+ Logger.logError('Error processing event in queue', err);
604
+ })
605
+ .finally(() => {
606
+ // Reset processing flag
607
+ this.isProcessing = false;
608
+
609
+ // Process next item if queue not empty
610
+ // Use setImmediate for non-blocking event loop
611
+ if (this.eventQueue.length > 0) {
612
+ setImmediate(() => this.processNext());
613
+ }
614
+ });
615
+ } else {
616
+ this.isProcessing = false;
443
617
  }
444
618
  }
445
619