iidrak-analytics-react 1.2.9 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,8 +18,13 @@ This package relies on several peer dependencies for device info, storage, and s
18
18
  ```bash
19
19
  npm install iidrak-analytics-react
20
20
 
21
- # Install required peer dependencies
22
- npm install @react-native-async-storage/async-storage @react-native-community/netinfo react-native-device-info react-native-view-shot react-native-safe-area-context
21
+ # Install required peer dependencies. For storage, choose ONE: MMKV or AsyncStorage
22
+ npm install react-native-nitro-modules react-native-mmkv
23
+ # OR
24
+ npm install @react-native-async-storage/async-storage
25
+
26
+ # Install core peer dependencies
27
+ npm install @react-native-community/netinfo react-native-device-info react-native-view-shot react-native-safe-area-context
23
28
  ```
24
29
 
25
30
  **iOS Users**: after installing, remember to run:
@@ -44,6 +49,13 @@ The configuration object requires strict structure for `app`, `config`, and `use
44
49
  ```javascript
45
50
  import { MetaStreamIO, MetaStreamProvider } from 'iidrak-analytics-react';
46
51
 
52
+ // Recommended: Import your preferred storage engine
53
+ import { createMMKV } from 'react-native-mmkv';
54
+ const mmkvStorage = createMMKV();
55
+
56
+ // Alternatively, import AsyncStorage
57
+ // import AsyncStorage from '@react-native-async-storage/async-storage';
58
+
47
59
  const iidrakConfig = {
48
60
  // Application Details
49
61
  app: {
@@ -70,6 +82,17 @@ const iidrakConfig = {
70
82
  sessionLength: 1800, // Session timeout in seconds (e.g. 30 mins)
71
83
  quality: 0.1, // Recording quality (0.1 - 1.0)
72
84
  fps: 2, // Frames Per Second for recording
85
+
86
+ // Provide the storage engine to the SDK.
87
+ // Why wrap MMKV? The SDK strictly expects an asynchronous `getItem/setItem` Interface.
88
+ // MMKV v4 is completely synchronous C++. This wrapper bridges MMKV into a standard Promise architecture.
89
+ storageEngine: {
90
+ getItem: async (key) => mmkvStorage.getString(key) || null,
91
+ setItem: async (key, value) => { mmkvStorage.set(key, value); return Promise.resolve(); },
92
+ removeItem: async (key) => { mmkvStorage.remove(key); return Promise.resolve(); }
93
+ },
94
+ // For AsyncStorage, no wrapper is needed because it natively uses Promises:
95
+ // storageEngine: AsyncStorage,
73
96
  },
74
97
 
75
98
  // Initial User Info
@@ -56,82 +56,87 @@ class MetaStreamIOEnvironment {
56
56
  }
57
57
 
58
58
  async logAppInfo() {
59
- let app_id = null;
60
- let first_installation_time = null;
61
- let installation_id = null;
62
- let install_referrer = null;
63
- let last_update_time = null;
64
- let version = null;
65
-
66
- app_id = DeviceInfo.getBundleId() ? DeviceInfo.getBundleId() : null;
67
-
68
- first_installation_time = await DeviceInfo.getFirstInstallTime()
69
- .then((result) => {
70
- if (this.date.validateDatetime(result)) {
71
- return result;
72
- } else {
59
+ if (this._appInfoCached) return this._appInfoCached;
60
+ if (this._appInfoPromise) return this._appInfoPromise;
61
+
62
+ this._appInfoPromise = (async () => {
63
+ let app_id = null;
64
+ let first_installation_time = null;
65
+ let installation_id = null;
66
+ let install_referrer = null;
67
+ let last_update_time = null;
68
+ let version = null;
69
+
70
+ app_id = DeviceInfo.getBundleId() ? DeviceInfo.getBundleId() : null;
71
+
72
+ first_installation_time = await DeviceInfo.getFirstInstallTime()
73
+ .then((result) => {
74
+ if (this.date.validateDatetime(result)) {
75
+ return result;
76
+ } else {
77
+ return null;
78
+ }
79
+ })
80
+ .catch((err) => {
81
+ this.logger.warn(
82
+ this.constant.MetaStreamIO_Logger_Category_Environment,
83
+ err
84
+ );
73
85
  return null;
74
- }
75
- })
76
- .catch((err) => {
77
- this.logger.warn(
78
- this.constant.MetaStreamIO_Logger_Category_Environment,
79
- err
80
- );
81
- return null;
82
- });
86
+ });
83
87
 
84
- installation_id = "deprecated"; // Constants.installationId;
85
-
86
- install_referrer = await DeviceInfo.getInstallReferrer()
87
- .then((result) => {
88
- return result;
89
- })
90
- .catch((err) => {
91
- this.logger.warn(
92
- this.constant.MetaStreamIO_Logger_Category_Environment,
93
- err
94
- );
95
- return null;
96
- });
88
+ installation_id = "deprecated"; // Constants.installationId;
97
89
 
98
- last_update_time = await DeviceInfo.getLastUpdateTime()
99
- .then((result) => {
100
- if (this.date.validateDatetime(result)) {
90
+ install_referrer = await DeviceInfo.getInstallReferrer()
91
+ .then((result) => {
101
92
  return result;
102
- } else {
93
+ })
94
+ .catch((err) => {
95
+ this.logger.warn(
96
+ this.constant.MetaStreamIO_Logger_Category_Environment,
97
+ err
98
+ );
103
99
  return null;
104
- }
105
- })
106
- .catch((err) => {
107
- this.logger.warn(
108
- this.constant.MetaStreamIO_Logger_Category_Environment,
109
- err
110
- );
111
- return null;
112
- });
113
-
114
- version = DeviceInfo.getVersion() ? DeviceInfo.getVersion() : null;
115
-
116
- this.app_info = new AppInfoModel({
117
- first_installation_time: first_installation_time,
118
- id: app_id,
119
- installation_id: installation_id,
120
- install_referrer: install_referrer,
121
- last_update_time: last_update_time,
122
- version: version,
123
- });
100
+ });
101
+
102
+ last_update_time = await DeviceInfo.getLastUpdateTime()
103
+ .then((result) => {
104
+ if (this.date.validateDatetime(result)) {
105
+ return result;
106
+ } else {
107
+ return null;
108
+ }
109
+ })
110
+ .catch((err) => {
111
+ this.logger.warn(
112
+ this.constant.MetaStreamIO_Logger_Category_Environment,
113
+ err
114
+ );
115
+ return null;
116
+ });
124
117
 
125
- this.logger.log(
126
- this.constant.MetaStreamIO_Logger_Category_Environment,
127
- this.constant.MetaStreamIO_Environment_AppInfo.format(
128
- JSON.stringify(this.app_info)
129
- )
130
- );
118
+ version = DeviceInfo.getVersion() ? DeviceInfo.getVersion() : null;
131
119
 
132
- return this.app_info.json();
120
+ this.app_info = new AppInfoModel({
121
+ first_installation_time: first_installation_time,
122
+ id: app_id,
123
+ installation_id: installation_id,
124
+ install_referrer: install_referrer,
125
+ last_update_time: last_update_time,
126
+ version: version,
127
+ });
133
128
 
134
- // return Promise.resolve(this.app_info.json());
129
+ this.logger.log(
130
+ this.constant.MetaStreamIO_Logger_Category_Environment,
131
+ this.constant.MetaStreamIO_Environment_AppInfo.format(
132
+ JSON.stringify(this.app_info)
133
+ )
134
+ );
135
+
136
+ this._appInfoCached = this.app_info.json();
137
+ return this._appInfoCached;
138
+ })();
139
+ return this._appInfoPromise;
135
140
  }
136
141
 
137
142
  async getAppPerformance() {
@@ -185,146 +190,153 @@ class MetaStreamIOEnvironment {
185
190
  }
186
191
 
187
192
  async logDevice() {
188
- let brand,
189
- device_type,
190
- model,
191
- operating_system,
192
- operating_system_version,
193
- pin_or_fingerprint_set,
194
- screen_width,
195
- screen_height,
196
- storage_total,
197
- storage_free,
198
- supported_architectures,
199
- timezone_offset_sec,
200
- total_memory,
201
- unique_device_id,
202
- used_memory;
203
-
204
- brand = await DeviceInfo.getBrand();
205
-
206
- device_type = await DeviceInfo.getDeviceType();
207
-
208
- model = DeviceInfo.getModel();
209
-
210
- operating_system = DeviceInfo.getSystemName();
211
-
212
- operating_system_version = DeviceInfo.getSystemVersion();
213
-
214
- try {
215
- screen_height = Math.round(Dimensions.get("window").height);
216
- } catch {
217
- screen_height = null;
218
- }
219
-
220
- try {
221
- screen_width = Math.round(Dimensions.get("window").width);
222
- } catch {
223
- screen_width = null;
224
- }
225
-
226
- storage_free = await DeviceInfo.getFreeDiskStorage()
227
- .then((result) => {
228
- return result;
229
- })
230
- .catch((err) => {
231
- this.logger.warn(
232
- this.constant.MetaStreamIO_Logger_Category_Environment,
233
- err
234
- );
235
- });
236
-
237
- storage_total = await DeviceInfo.getTotalDiskCapacity()
238
- .then((result) => {
239
- return result;
240
- })
241
- .catch((err) => {
242
- this.logger.warn(
243
- this.constant.MetaStreamIO_Logger_Category_Environment,
244
- err
245
- );
246
- });
247
-
248
- supported_architectures = await DeviceInfo.supportedAbis()
249
- .then((result) => {
250
- return result.join(",");
251
- })
252
- .catch((err) => {
253
- this.logger.warn(
254
- this.constant.MetaStreamIO_Logger_Category_Environment,
255
- err
256
- );
257
- });
258
-
259
- total_memory = await DeviceInfo.getTotalMemory()
260
- .then((result) => {
261
- return result;
262
- })
263
- .catch((err) => {
264
- this.logger.warn(
265
- this.constant.MetaStreamIO_Logger_Category_Environment,
266
- err
267
- );
268
- });
269
-
270
- unique_device_id = DeviceInfo.getUniqueId()
271
- ? DeviceInfo.getUniqueId()
272
- : null;
273
-
274
- used_memory = await DeviceInfo.getUsedMemory()
275
- .then((result) => {
276
- return result;
277
- })
278
- .catch((err) => {
279
- this.logger.warn(
280
- this.constant.MetaStreamIO_Logger_Category_Environment,
281
- err
282
- );
283
- });
284
-
285
- try {
286
- timezone_offset_sec = this.date.getDeviceTimezoneOffset();
287
- } catch {
288
- timezone_offset_sec = null;
289
- }
290
-
291
- pin_or_fingerprint_set = await DeviceInfo.isPinOrFingerprintSet()
292
- .then((result) => {
293
- return result;
294
- })
295
- .catch((err) => {
296
- this.logger.warn(
297
- this.constant.MetaStreamIO_Logger_Category_Environment,
298
- err
299
- );
193
+ if (this._deviceCached) return this._deviceCached;
194
+ if (this._devicePromise) return this._devicePromise;
195
+
196
+ this._devicePromise = (async () => {
197
+ let brand,
198
+ device_type,
199
+ model,
200
+ operating_system,
201
+ operating_system_version,
202
+ pin_or_fingerprint_set,
203
+ screen_width,
204
+ screen_height,
205
+ storage_total,
206
+ storage_free,
207
+ supported_architectures,
208
+ timezone_offset_sec,
209
+ total_memory,
210
+ unique_device_id,
211
+ used_memory;
212
+
213
+ brand = await DeviceInfo.getBrand();
214
+
215
+ device_type = await DeviceInfo.getDeviceType();
216
+
217
+ model = DeviceInfo.getModel();
218
+
219
+ operating_system = DeviceInfo.getSystemName();
220
+
221
+ operating_system_version = DeviceInfo.getSystemVersion();
222
+
223
+ try {
224
+ screen_height = Math.round(Dimensions.get("window").height);
225
+ } catch {
226
+ screen_height = null;
227
+ }
228
+
229
+ try {
230
+ screen_width = Math.round(Dimensions.get("window").width);
231
+ } catch {
232
+ screen_width = null;
233
+ }
234
+
235
+ storage_free = await DeviceInfo.getFreeDiskStorage()
236
+ .then((result) => {
237
+ return result;
238
+ })
239
+ .catch((err) => {
240
+ this.logger.warn(
241
+ this.constant.MetaStreamIO_Logger_Category_Environment,
242
+ err
243
+ );
244
+ });
245
+
246
+ storage_total = await DeviceInfo.getTotalDiskCapacity()
247
+ .then((result) => {
248
+ return result;
249
+ })
250
+ .catch((err) => {
251
+ this.logger.warn(
252
+ this.constant.MetaStreamIO_Logger_Category_Environment,
253
+ err
254
+ );
255
+ });
256
+
257
+ supported_architectures = await DeviceInfo.supportedAbis()
258
+ .then((result) => {
259
+ return result.join(",");
260
+ })
261
+ .catch((err) => {
262
+ this.logger.warn(
263
+ this.constant.MetaStreamIO_Logger_Category_Environment,
264
+ err
265
+ );
266
+ });
267
+
268
+ total_memory = await DeviceInfo.getTotalMemory()
269
+ .then((result) => {
270
+ return result;
271
+ })
272
+ .catch((err) => {
273
+ this.logger.warn(
274
+ this.constant.MetaStreamIO_Logger_Category_Environment,
275
+ err
276
+ );
277
+ });
278
+
279
+ unique_device_id = DeviceInfo.getUniqueId()
280
+ ? DeviceInfo.getUniqueId()
281
+ : null;
282
+
283
+ used_memory = await DeviceInfo.getUsedMemory()
284
+ .then((result) => {
285
+ return result;
286
+ })
287
+ .catch((err) => {
288
+ this.logger.warn(
289
+ this.constant.MetaStreamIO_Logger_Category_Environment,
290
+ err
291
+ );
292
+ });
293
+
294
+ try {
295
+ timezone_offset_sec = this.date.getDeviceTimezoneOffset();
296
+ } catch {
297
+ timezone_offset_sec = null;
298
+ }
299
+
300
+ pin_or_fingerprint_set = await DeviceInfo.isPinOrFingerprintSet()
301
+ .then((result) => {
302
+ return result;
303
+ })
304
+ .catch((err) => {
305
+ this.logger.warn(
306
+ this.constant.MetaStreamIO_Logger_Category_Environment,
307
+ err
308
+ );
309
+ });
310
+
311
+ this.device = new DeviceModel({
312
+ brand: brand,
313
+ device_type: device_type,
314
+ model: model,
315
+ operating_system: operating_system,
316
+ operating_system_version: operating_system_version,
317
+ pin_or_fingerprint_set: pin_or_fingerprint_set,
318
+ screen_height: screen_height,
319
+ screen_width: screen_width,
320
+ storage_free: storage_free,
321
+ storage_total: storage_total,
322
+ supported_architectures: supported_architectures,
323
+ timezone_offset_sec: timezone_offset_sec,
324
+ total_memory: total_memory,
325
+ unique_device_id: unique_device_id,
326
+ used_memory: used_memory,
300
327
  });
301
328
 
302
- this.device = new DeviceModel({
303
- brand: brand,
304
- device_type: device_type,
305
- model: model,
306
- operating_system: operating_system,
307
- operating_system_version: operating_system_version,
308
- pin_or_fingerprint_set: pin_or_fingerprint_set,
309
- screen_height: screen_height,
310
- screen_width: screen_width,
311
- storage_free: storage_free,
312
- storage_total: storage_total,
313
- supported_architectures: supported_architectures,
314
- timezone_offset_sec: timezone_offset_sec,
315
- total_memory: total_memory,
316
- unique_device_id: unique_device_id,
317
- used_memory: used_memory,
318
- });
319
-
320
- this.logger.log(
321
- this.constant.MetaStreamIO_Logger_Category_Environment,
322
- this.constant.MetaStreamIO_Environment_Device.format(
323
- JSON.stringify(this.device)
324
- )
325
- );
326
-
327
- return Promise.resolve(this.device.json());
329
+ this.logger.log(
330
+ this.constant.MetaStreamIO_Logger_Category_Environment,
331
+ this.constant.MetaStreamIO_Environment_Device.format(
332
+ JSON.stringify(this.device)
333
+ )
334
+ );
335
+
336
+ this._deviceCached = this.device.json();
337
+ return this._deviceCached;
338
+ })();
339
+ return this._devicePromise;
328
340
  }
329
341
 
330
342
  async getNetworkInfo() {
@@ -338,68 +350,66 @@ class MetaStreamIOEnvironment {
338
350
  }
339
351
 
340
352
  async logNetworkInfo() {
341
- let carrier,
342
- carrier_iso_country_code,
343
- cellular_generation,
344
- mobile_country_code,
345
- mobile_network_code,
346
- network_state_type,
347
- net_info;
348
-
349
- carrier = await DeviceInfo.getCarrier()
350
- .then((result) => {
351
- return result;
352
- })
353
- .catch((err) => {
354
- this.logger.warn(
355
- this.constant.MetaStreamIO_Logger_Category_Environment,
356
- err
357
- );
358
- });
359
-
360
- net_info = await NetInfo.fetch()
361
- .then((state) => {
362
- return {
353
+ if (this._networkCached && (Date.now() - this._networkCachedTime < 5000)) {
354
+ return this._networkCached;
355
+ }
356
+ if (this._networkPromise) return this._networkPromise;
357
+
358
+ this._networkPromise = (async () => {
359
+ let carrier,
360
+ carrier_iso_country_code,
361
+ cellular_generation,
362
+ mobile_country_code,
363
+ mobile_network_code,
364
+ network_state_type,
365
+ net_info;
366
+
367
+ carrier = await Promise.race([
368
+ DeviceInfo.getCarrier().then(r => r).catch(() => null),
369
+ new Promise(res => setTimeout(() => res(null), 500))
370
+ ]);
371
+
372
+ net_info = await Promise.race([
373
+ NetInfo.fetch().then((state) => ({
363
374
  connection_type: state.type,
364
375
  connection_active: state.isConnected,
365
- cellular_generation:
366
- state.type === "cellular" ? state.details.cellularGeneration : null,
367
- };
368
- })
369
- .catch((err) => {
370
- this.logger.warn(
371
- this.constant.MetaStreamIO_Logger_Category_Environment,
372
- err
373
- );
374
- });
376
+ cellular_generation: state.type === "cellular" ? state.details.cellularGeneration : null,
377
+ })).catch(() => ({ connection_type: "unknown", connection_active: true, cellular_generation: null })),
378
+ new Promise(res => setTimeout(() => res({ connection_type: "unknown", connection_active: true, cellular_generation: null }), 500))
379
+ ]);
375
380
 
376
- carrier_iso_country_code = null;
381
+ carrier_iso_country_code = null;
377
382
 
378
- cellular_generation = net_info.cellular_generation;
383
+ cellular_generation = net_info.cellular_generation;
379
384
 
380
- mobile_country_code = null;
385
+ mobile_country_code = null;
381
386
 
382
- mobile_network_code = null;
387
+ mobile_network_code = null;
383
388
 
384
- network_state_type = net_info.connection_type;
389
+ network_state_type = net_info.connection_type;
385
390
 
386
- this.logger.log(
387
- this.constant.MetaStreamIO_Logger_Category_Environment,
388
- this.constant.MetaStreamIO_Environment_Network.format(
389
- JSON.stringify(this.network_info)
390
- )
391
- );
391
+ this.logger.log(
392
+ this.constant.MetaStreamIO_Logger_Category_Environment,
393
+ this.constant.MetaStreamIO_Environment_Network.format(
394
+ JSON.stringify(this.network_info)
395
+ )
396
+ );
392
397
 
393
- this.network_info = new NetworkInfoModel({
394
- carrier: carrier,
395
- carrier_iso_country_code: carrier_iso_country_code,
396
- cellular_generation: cellular_generation,
397
- mobile_country_code: mobile_country_code,
398
- mobile_network_code: mobile_network_code,
399
- network_state_type: network_state_type,
400
- });
398
+ this.network_info = new NetworkInfoModel({
399
+ carrier: carrier,
400
+ carrier_iso_country_code: carrier_iso_country_code,
401
+ cellular_generation: cellular_generation,
402
+ mobile_country_code: mobile_country_code,
403
+ mobile_network_code: mobile_network_code,
404
+ network_state_type: network_state_type,
405
+ });
401
406
 
402
- return Promise.resolve(this.network_info.json());
407
+ this._networkCached = this.network_info.json();
408
+ this._networkCachedTime = Date.now();
409
+ this._networkPromise = null;
410
+ return this._networkCached;
411
+ })();
412
+ return this._networkPromise;
403
413
  }
404
414
  }
405
415
 
@@ -82,6 +82,7 @@ class MetaStreamIO {
82
82
  silentMode: this.config.silentMode,
83
83
  endpoints: this.appConfig.endpoints,
84
84
  headers: this.appConfig.headers,
85
+ storageEngine: this.config.storageEngine,
85
86
  });
86
87
  this.account = new MetaStreamIOAccountData({
87
88
  constants: this.constant,
@@ -469,7 +470,7 @@ class MetaStreamIO {
469
470
  );
470
471
  return Promise.resolve(event_data);
471
472
  } else {
472
- this.logger.error(
473
+ this.logger.warn(
473
474
  "event",
474
475
  this.constant.MetaStreamIO_Log_EventTrackFailed.format(_data)
475
476
  );
@@ -480,13 +481,14 @@ class MetaStreamIO {
480
481
  return Promise.reject(_data);
481
482
  }
482
483
  })
483
- .catch((err) => {
484
- this.logger.error("event", "<post_event> could not post: " + err);
485
- return Promise.reject(err);
484
+ .catch(async (err) => {
485
+ this.logger.warn("event", "<post_event> could not post: " + (err.message || JSON.stringify(err)));
486
+ await this.network.storeOfflineEvent(this.network.endpoint, event_data);
487
+ return Promise.resolve(event_data); // Resolve to deque, since it's now safely offline
486
488
  });
487
489
  return status;
488
490
  } else {
489
- this.logger.error("event", "<post_event> event_data is undefined");
491
+ this.logger.warn("event", "<post_event> event_data is undefined");
490
492
  }
491
493
  }
492
494
 
@@ -515,7 +517,7 @@ class MetaStreamIO {
515
517
  return Promise.resolve();
516
518
  })
517
519
  .catch((err) => {
518
- this.logger.error(
520
+ this.logger.warn(
519
521
  "event",
520
522
  "<runQueue> this.post_event failed: " + err
521
523
  );
@@ -526,7 +528,7 @@ class MetaStreamIO {
526
528
  try {
527
529
  this.queue.enqueue(event);
528
530
  } catch (err) {
529
- this.logger.error(
531
+ this.logger.warn(
530
532
  "event",
531
533
  "<runQueue> could not enqueue event: " + err
532
534
  );
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
  import { strf } from "./string.format.js";
4
- import AsyncStorage from "@react-native-async-storage/async-storage";
5
4
  import NetInfo from "@react-native-community/netinfo";
6
5
 
7
6
  strf();
@@ -15,7 +14,7 @@ const EVENT_QUEUE_KEY = "metastream_offline_events";
15
14
  */
16
15
 
17
16
  class MetaStreamIONetwork {
18
- constructor({ constants, endpoints, logger, silentMode, headers } = {}) {
17
+ constructor({ constants, endpoints, logger, silentMode, headers, storageEngine } = {}) {
19
18
  this.silentMode = silentMode;
20
19
  this.constant = constants;
21
20
  this.endpoint = null;
@@ -23,13 +22,23 @@ class MetaStreamIONetwork {
23
22
  this.headers = headers;
24
23
  this.logger = logger;
25
24
 
25
+ // Use provided storageEngine from config or fallback to a transient in-memory store
26
+ this.storageEngine = storageEngine || {
27
+ _memory: {},
28
+ getItem: async (key) => this.storageEngine._memory[key] || null,
29
+ setItem: async (key, value) => { this.storageEngine._memory[key] = value; return Promise.resolve(); },
30
+ removeItem: async (key) => { delete this.storageEngine._memory[key]; return Promise.resolve(); }
31
+ };
32
+
26
33
  // Initialize offline handling
27
34
  this.initOfflineHandling();
28
35
  }
29
36
 
30
37
  initOfflineHandling() {
38
+ this.isOnline = true; // Default assume online, Listener updates it
31
39
  // Flush on startup if connected
32
40
  NetInfo.fetch().then(state => {
41
+ this.isOnline = state.isConnected;
33
42
  if (state.isConnected) {
34
43
  this.flushEvents();
35
44
  }
@@ -37,10 +46,16 @@ class MetaStreamIONetwork {
37
46
 
38
47
  // Flush when network comes back online
39
48
  NetInfo.addEventListener(state => {
49
+ this.isOnline = state.isConnected;
40
50
  if (state.isConnected) {
41
51
  this.flushEvents();
42
52
  }
43
53
  });
54
+
55
+ // Background heartbeat to auto-flush stuck events gracefully
56
+ this._flushInterval = setInterval(() => {
57
+ this.flushEvents();
58
+ }, 3000); // Poll every 3 seconds for perfect sync
44
59
  }
45
60
 
46
61
  reset() {
@@ -53,14 +68,15 @@ class MetaStreamIONetwork {
53
68
  this.endpoint = await this.getData({ endpoint: this.endpoints[_e] })
54
69
  .then((data) => {
55
70
  try {
56
- if (data.status === 200) {
71
+ if (data && data.status > 0) {
57
72
  return this.endpoints[_e];
58
73
  }
74
+ return false;
59
75
  } catch (err) {
60
76
  return false;
61
77
  }
62
78
  })
63
- .catch(() => { });
79
+ .catch(() => { return false; });
64
80
  if (this.endpoint) {
65
81
  this.logger.log(
66
82
  "network",
@@ -86,15 +102,12 @@ class MetaStreamIONetwork {
86
102
  }
87
103
 
88
104
  async getData({ endpoint = this.endpoint } = {}) {
89
- var rx;
90
-
91
105
  let get_config = {
92
106
  method: "GET",
93
- // mode: "no-cors", // required for local testing
94
107
  cache: "no-cache",
95
108
  credentials: "same-origin",
96
- redirect: "follow", // manual, *follow, error
97
- referrerPolicy: "no-referrer", // no-referrer, *client
109
+ redirect: "follow",
110
+ referrerPolicy: "no-referrer",
98
111
  headers: {},
99
112
  };
100
113
 
@@ -103,37 +116,14 @@ class MetaStreamIONetwork {
103
116
  }
104
117
 
105
118
  try {
106
- await fetch(endpoint, get_config)
107
- .then((response) => {
108
- const contentType = response.headers.get("content-type");
109
- if (!contentType || !contentType.includes("application/json")) {
110
- return null;
111
- } else {
112
- return response;
113
- }
114
- })
115
- .catch((err) => {
116
- return Promise.resolve(err);
117
- })
118
- .then(async (response) => {
119
- rx = {
120
- body: await response.json(),
121
- status: response.status,
122
- };
123
- })
124
- .catch((err) => {
125
- return {
126
- body: err,
127
- status: 400,
128
- };
129
- });
130
- return rx;
119
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 3000));
120
+ const response = await Promise.race([
121
+ fetch(endpoint, get_config),
122
+ timeoutPromise
123
+ ]);
124
+ return { status: response.status, body: "OK" };
131
125
  } catch (err) {
132
- return Promise.reject(err);
133
- throw this.constant.MetaStreamIO_Network_GetDataIssue.format(
134
- err,
135
- endpoint
136
- );
126
+ return { status: 0, body: String(err) };
137
127
  }
138
128
  }
139
129
 
@@ -155,12 +145,25 @@ class MetaStreamIONetwork {
155
145
  };
156
146
  }
157
147
 
148
+ if (!this.isOnline) {
149
+ this.logger.log("network", "Offline detected in postData. Storing event.");
150
+ const fallbackEndpoint = this.endpoints.length > 0 ? this.endpoints[0] : null;
151
+ if (fallbackEndpoint) {
152
+ await this.storeOfflineEvent(fallbackEndpoint + (endpoint ? endpoint : ""), data);
153
+ return {
154
+ body: "Stored offline",
155
+ status: 200
156
+ };
157
+ }
158
+ }
159
+
158
160
  if (!this.endpoint) {
159
- _endpoint = await this.testConnection()
160
- .then((rx_endpoint) => {
161
- return Promise.resolve(rx_endpoint + (endpoint ? endpoint : ""));
162
- })
163
- .catch((err) => err);
161
+ const rx_endpoint = await this.testConnection().catch(() => null);
162
+ if (rx_endpoint) {
163
+ _endpoint = rx_endpoint + (endpoint ? endpoint : "");
164
+ } else {
165
+ _endpoint = null;
166
+ }
164
167
  }
165
168
 
166
169
  if (_endpoint) {
@@ -198,6 +201,9 @@ class MetaStreamIONetwork {
198
201
  }
199
202
 
200
203
  async post({ data = {}, endpoint = this.endpoint } = {}) {
204
+ // Non-blocking trigger to flush stuck offline events (if any)
205
+ this.flushEvents();
206
+
201
207
  let rx = {};
202
208
 
203
209
  let post_config = {
@@ -217,95 +223,151 @@ class MetaStreamIONetwork {
217
223
  post_config.headers = Object.assign(post_config.headers, this.headers);
218
224
  }
219
225
 
220
- let isOffline = false;
221
- await NetInfo.fetch().then(state => {
222
- if (!state.isConnected) {
223
- isOffline = true;
224
- }
225
- });
226
-
227
- if (isOffline) {
226
+ if (!this.isOnline) {
228
227
  this.logger.log("network", "Offline detected. Storing event.");
229
228
  await this.storeOfflineEvent(endpoint, data);
230
229
  return { status: 200, body: "Stored offline" };
231
230
  }
232
231
 
233
- await fetch(endpoint, post_config)
232
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 3000));
233
+
234
+ await Promise.race([
235
+ fetch(endpoint, post_config),
236
+ timeoutPromise
237
+ ])
234
238
  .then((response) => {
239
+ rx.status = response.status;
235
240
  let _contentType = response.headers.get("content-type");
236
241
  if (!_contentType || !_contentType.includes("application/json")) {
237
242
  return null;
238
243
  } else {
239
- rx.status = response.status;
240
244
  return response;
241
245
  }
242
246
  })
243
247
  .then(async (response) => {
244
- rx.body = await response.json();
248
+ if (response) {
249
+ rx.body = await response.json();
250
+ } else {
251
+ rx.body = "OK";
252
+ }
245
253
  })
246
254
  .catch(async (err) => {
247
- this.logger.error("network", "fetch() response", err);
248
- // Store offline on network error
249
- await this.storeOfflineEvent(endpoint, data);
250
- rx.status = 200; // Treat as success to deque
251
- rx.body = "Stored offline on error";
255
+ this.logger.warn("network", "fetch() error in post()", err.message || err);
256
+ rx.status = 500;
257
+ rx.body = "fetch error";
258
+ throw err;
252
259
  });
253
260
 
254
261
  return rx;
255
262
  }
256
263
 
264
+ async _getQueue() {
265
+ try {
266
+ const raw = await this.storageEngine.getItem(EVENT_QUEUE_KEY);
267
+ if (!raw) return [];
268
+ const parsed = JSON.parse(raw);
269
+ return Array.isArray(parsed) ? parsed : [];
270
+ } catch {
271
+ return [];
272
+ }
273
+ }
274
+
257
275
  async storeOfflineEvent(endpoint, payload) {
258
276
  try {
259
- const existing = JSON.parse(await AsyncStorage.getItem(EVENT_QUEUE_KEY)) || [];
277
+ const existing = await this._getQueue();
260
278
  const newEvent = {
261
279
  endpoint,
262
280
  payload,
263
281
  failedAt: new Date().toISOString(),
264
282
  };
265
283
  existing.push(newEvent);
266
- await AsyncStorage.setItem(EVENT_QUEUE_KEY, JSON.stringify(existing));
284
+ await this.storageEngine.setItem(EVENT_QUEUE_KEY, JSON.stringify(existing));
267
285
  this.logger.log("network", `Stored event locally. Queue size: ${existing.length}`);
268
286
  } catch (error) {
269
- this.logger.error("network", "Failed to store event:", error);
287
+ this.logger.error("network", "Failed to store event:", error.message || error);
270
288
  }
271
289
  }
272
290
 
273
291
  async flushEvents() {
292
+ if (this._isFlushing) return;
293
+ this._isFlushing = true;
294
+
274
295
  try {
275
- const stored = JSON.parse(await AsyncStorage.getItem(EVENT_QUEUE_KEY)) || [];
296
+ const stored = await this._getQueue();
276
297
  if (stored.length === 0) {
277
298
  return;
278
299
  }
279
300
 
280
301
  this.logger.log("network", `Flushing ${stored.length} stored event(s)...`);
302
+
303
+ // Pre-flight check: Do not spam the Native Bridge with 20 offline events if the server route is down
304
+ const pingEndpoint = this.endpoint || (this.endpoints.length > 0 ? this.endpoints[0] : null);
305
+ if (pingEndpoint) {
306
+ const pingTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 1500));
307
+ const routeValid = await Promise.race([
308
+ fetch(pingEndpoint, { method: "OPTIONS" }),
309
+ pingTimeout
310
+ ]).then(() => true).catch(() => false);
311
+
312
+ if (!routeValid) {
313
+ this.logger.log("network", "flushEvents aborted: Node route not fully established natively yet.");
314
+ return;
315
+ }
316
+ }
317
+
281
318
  const remainingEvents = [];
282
319
 
283
320
  for (const e of stored) {
321
+ let reqHeaders = { "Content-Type": "application/json" };
322
+ if (this.headers) {
323
+ reqHeaders = Object.assign(reqHeaders, this.headers);
324
+ }
325
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 3000));
326
+
284
327
  try {
285
- const response = await fetch(e.endpoint, {
286
- method: "POST",
287
- headers: { "Content-Type": "application/json" },
288
- body: JSON.stringify(e.payload),
289
- });
328
+ const response = await Promise.race([
329
+ fetch(e.endpoint, {
330
+ method: "POST",
331
+ headers: reqHeaders,
332
+ body: JSON.stringify(e.payload),
333
+ }),
334
+ timeoutPromise
335
+ ]);
290
336
 
291
337
  if (response.ok) {
292
- this.logger.log("network", "Retried & sent event");
338
+ this.logger.log("network", "Retried & sent event successfully.");
293
339
  } else {
294
- // Drop event if server rejects it (4xx, 5xx), avoid infinite loop
295
- this.logger.warn("network", "Retry failed with non-OK status, dropping event");
340
+ this.logger.warn("network", `Retry got non-OK status (${response.status}), dropping event.`);
296
341
  }
297
342
  } catch (err) {
298
- // Keep if network error
343
+ this.logger.log("network", "Retry failed (still offline or bad host), keeping event.", err.message);
299
344
  remainingEvents.push(e);
300
345
  }
301
346
  }
302
347
 
303
- await AsyncStorage.setItem(EVENT_QUEUE_KEY, JSON.stringify(remainingEvents));
348
+ const currentQueue = await this._getQueue();
349
+ // Only keep events that were just inserted (after we started flushing)
350
+ // We assume new events were appended to the end of the array.
351
+ let newAdditions = [];
352
+ if (currentQueue.length > stored.length) {
353
+ newAdditions = currentQueue.slice(stored.length);
354
+ }
355
+
356
+ const updatedQueue = [...remainingEvents, ...newAdditions];
357
+
358
+ if (updatedQueue.length > 0) {
359
+ await this.storageEngine.setItem(EVENT_QUEUE_KEY, JSON.stringify(updatedQueue));
360
+ } else {
361
+ await this.storageEngine.removeItem(EVENT_QUEUE_KEY);
362
+ }
363
+
304
364
  if (remainingEvents.length < stored.length) {
305
- this.logger.log("network", `Flush complete. Remaining: ${remainingEvents.length}`);
365
+ this.logger.log("network", `Flush complete. Remaining: ${updatedQueue.length}`);
306
366
  }
307
367
  } catch (error) {
308
- this.logger.error("network", "Failed to flush events:", error);
368
+ this.logger.warn("network", "Failed to flush events:", error.message || error);
369
+ } finally {
370
+ this._isFlushing = false;
309
371
  }
310
372
  }
311
373
 
@@ -12,6 +12,7 @@ export class ConfigConstructorModel {
12
12
  quality = null,
13
13
  fps = null,
14
14
  recordingEndpoint = null,
15
+ storageEngine = null,
15
16
  } = {}) {
16
17
  this.logging = logging;
17
18
  this.loggingLevel = loggingLevel;
@@ -26,6 +27,7 @@ export class ConfigConstructorModel {
26
27
  this.quality = quality;
27
28
  this.fps = fps;
28
29
  this.recordingEndpoint = recordingEndpoint;
30
+ this.storageEngine = storageEngine;
29
31
  }
30
32
  json() {
31
33
  return {
@@ -41,6 +43,7 @@ export class ConfigConstructorModel {
41
43
  quality: this.quality,
42
44
  fps: this.fps,
43
45
  recordingEndpoint: this.recordingEndpoint,
46
+ storageEngine: this.storageEngine,
44
47
  };
45
48
  }
46
49
  }
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "iidrak-analytics-react",
3
- "version": "1.2.9",
3
+ "version": "1.5.0",
4
4
  "description": "react native client for metastreamio",
5
5
  "peerDependencies": {
6
- "@react-native-async-storage/async-storage": ">=2.2.0",
6
+ "react-native-mmkv": ">=4.0.0",
7
+ "react-native-nitro-modules": ">=0.30.0",
8
+ "@react-native-async-storage/async-storage": ">=1.17.0",
7
9
  "@react-native-community/netinfo": ">=11.5.2",
8
10
  "react": ">=18.0.0",
9
11
  "react-native": ">=0.70.0",
@@ -11,6 +13,17 @@
11
13
  "react-native-view-shot": ">=3.0.0",
12
14
  "react-native-safe-area-context": ">=4.0.0"
13
15
  },
16
+ "peerDependenciesMeta": {
17
+ "react-native-mmkv": {
18
+ "optional": true
19
+ },
20
+ "react-native-nitro-modules": {
21
+ "optional": true
22
+ },
23
+ "@react-native-async-storage/async-storage": {
24
+ "optional": true
25
+ }
26
+ },
14
27
  "dependencies": {
15
28
  "string-format": "^0.5.0"
16
29
  },