iidrak-analytics-react 1.3.0 → 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-mmkv react-native-nitro-modules @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,13 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
  import { strf } from "./string.format.js";
4
- import { createMMKV } from "react-native-mmkv";
5
- const mmkvStorage = createMMKV();
6
- const AsyncStorage = {
7
- getItem: async (key) => mmkvStorage.getString(key) || null,
8
- setItem: async (key, value) => { mmkvStorage.set(key, value); return Promise.resolve(); },
9
- removeItem: async (key) => { mmkvStorage.delete(key); return Promise.resolve(); }
10
- };
11
4
  import NetInfo from "@react-native-community/netinfo";
12
5
 
13
6
  strf();
@@ -21,7 +14,7 @@ const EVENT_QUEUE_KEY = "metastream_offline_events";
21
14
  */
22
15
 
23
16
  class MetaStreamIONetwork {
24
- constructor({ constants, endpoints, logger, silentMode, headers } = {}) {
17
+ constructor({ constants, endpoints, logger, silentMode, headers, storageEngine } = {}) {
25
18
  this.silentMode = silentMode;
26
19
  this.constant = constants;
27
20
  this.endpoint = null;
@@ -29,13 +22,23 @@ class MetaStreamIONetwork {
29
22
  this.headers = headers;
30
23
  this.logger = logger;
31
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
+
32
33
  // Initialize offline handling
33
34
  this.initOfflineHandling();
34
35
  }
35
36
 
36
37
  initOfflineHandling() {
38
+ this.isOnline = true; // Default assume online, Listener updates it
37
39
  // Flush on startup if connected
38
40
  NetInfo.fetch().then(state => {
41
+ this.isOnline = state.isConnected;
39
42
  if (state.isConnected) {
40
43
  this.flushEvents();
41
44
  }
@@ -43,10 +46,16 @@ class MetaStreamIONetwork {
43
46
 
44
47
  // Flush when network comes back online
45
48
  NetInfo.addEventListener(state => {
49
+ this.isOnline = state.isConnected;
46
50
  if (state.isConnected) {
47
51
  this.flushEvents();
48
52
  }
49
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
50
59
  }
51
60
 
52
61
  reset() {
@@ -59,14 +68,15 @@ class MetaStreamIONetwork {
59
68
  this.endpoint = await this.getData({ endpoint: this.endpoints[_e] })
60
69
  .then((data) => {
61
70
  try {
62
- if (data.status === 200) {
71
+ if (data && data.status > 0) {
63
72
  return this.endpoints[_e];
64
73
  }
74
+ return false;
65
75
  } catch (err) {
66
76
  return false;
67
77
  }
68
78
  })
69
- .catch(() => { });
79
+ .catch(() => { return false; });
70
80
  if (this.endpoint) {
71
81
  this.logger.log(
72
82
  "network",
@@ -92,15 +102,12 @@ class MetaStreamIONetwork {
92
102
  }
93
103
 
94
104
  async getData({ endpoint = this.endpoint } = {}) {
95
- var rx;
96
-
97
105
  let get_config = {
98
106
  method: "GET",
99
- // mode: "no-cors", // required for local testing
100
107
  cache: "no-cache",
101
108
  credentials: "same-origin",
102
- redirect: "follow", // manual, *follow, error
103
- referrerPolicy: "no-referrer", // no-referrer, *client
109
+ redirect: "follow",
110
+ referrerPolicy: "no-referrer",
104
111
  headers: {},
105
112
  };
106
113
 
@@ -109,37 +116,14 @@ class MetaStreamIONetwork {
109
116
  }
110
117
 
111
118
  try {
112
- await fetch(endpoint, get_config)
113
- .then((response) => {
114
- const contentType = response.headers.get("content-type");
115
- if (!contentType || !contentType.includes("application/json")) {
116
- return null;
117
- } else {
118
- return response;
119
- }
120
- })
121
- .catch((err) => {
122
- return Promise.resolve(err);
123
- })
124
- .then(async (response) => {
125
- rx = {
126
- body: await response.json(),
127
- status: response.status,
128
- };
129
- })
130
- .catch((err) => {
131
- return {
132
- body: err,
133
- status: 400,
134
- };
135
- });
136
- 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" };
137
125
  } catch (err) {
138
- return Promise.reject(err);
139
- throw this.constant.MetaStreamIO_Network_GetDataIssue.format(
140
- err,
141
- endpoint
142
- );
126
+ return { status: 0, body: String(err) };
143
127
  }
144
128
  }
145
129
 
@@ -161,12 +145,25 @@ class MetaStreamIONetwork {
161
145
  };
162
146
  }
163
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
+
164
160
  if (!this.endpoint) {
165
- _endpoint = await this.testConnection()
166
- .then((rx_endpoint) => {
167
- return Promise.resolve(rx_endpoint + (endpoint ? endpoint : ""));
168
- })
169
- .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
+ }
170
167
  }
171
168
 
172
169
  if (_endpoint) {
@@ -204,6 +201,9 @@ class MetaStreamIONetwork {
204
201
  }
205
202
 
206
203
  async post({ data = {}, endpoint = this.endpoint } = {}) {
204
+ // Non-blocking trigger to flush stuck offline events (if any)
205
+ this.flushEvents();
206
+
207
207
  let rx = {};
208
208
 
209
209
  let post_config = {
@@ -223,95 +223,151 @@ class MetaStreamIONetwork {
223
223
  post_config.headers = Object.assign(post_config.headers, this.headers);
224
224
  }
225
225
 
226
- let isOffline = false;
227
- await NetInfo.fetch().then(state => {
228
- if (!state.isConnected) {
229
- isOffline = true;
230
- }
231
- });
232
-
233
- if (isOffline) {
226
+ if (!this.isOnline) {
234
227
  this.logger.log("network", "Offline detected. Storing event.");
235
228
  await this.storeOfflineEvent(endpoint, data);
236
229
  return { status: 200, body: "Stored offline" };
237
230
  }
238
231
 
239
- 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
+ ])
240
238
  .then((response) => {
239
+ rx.status = response.status;
241
240
  let _contentType = response.headers.get("content-type");
242
241
  if (!_contentType || !_contentType.includes("application/json")) {
243
242
  return null;
244
243
  } else {
245
- rx.status = response.status;
246
244
  return response;
247
245
  }
248
246
  })
249
247
  .then(async (response) => {
250
- rx.body = await response.json();
248
+ if (response) {
249
+ rx.body = await response.json();
250
+ } else {
251
+ rx.body = "OK";
252
+ }
251
253
  })
252
254
  .catch(async (err) => {
253
- this.logger.error("network", "fetch() response", err);
254
- // Store offline on network error
255
- await this.storeOfflineEvent(endpoint, data);
256
- rx.status = 200; // Treat as success to deque
257
- 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;
258
259
  });
259
260
 
260
261
  return rx;
261
262
  }
262
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
+
263
275
  async storeOfflineEvent(endpoint, payload) {
264
276
  try {
265
- const existing = JSON.parse(await AsyncStorage.getItem(EVENT_QUEUE_KEY)) || [];
277
+ const existing = await this._getQueue();
266
278
  const newEvent = {
267
279
  endpoint,
268
280
  payload,
269
281
  failedAt: new Date().toISOString(),
270
282
  };
271
283
  existing.push(newEvent);
272
- await AsyncStorage.setItem(EVENT_QUEUE_KEY, JSON.stringify(existing));
284
+ await this.storageEngine.setItem(EVENT_QUEUE_KEY, JSON.stringify(existing));
273
285
  this.logger.log("network", `Stored event locally. Queue size: ${existing.length}`);
274
286
  } catch (error) {
275
- this.logger.error("network", "Failed to store event:", error);
287
+ this.logger.error("network", "Failed to store event:", error.message || error);
276
288
  }
277
289
  }
278
290
 
279
291
  async flushEvents() {
292
+ if (this._isFlushing) return;
293
+ this._isFlushing = true;
294
+
280
295
  try {
281
- const stored = JSON.parse(await AsyncStorage.getItem(EVENT_QUEUE_KEY)) || [];
296
+ const stored = await this._getQueue();
282
297
  if (stored.length === 0) {
283
298
  return;
284
299
  }
285
300
 
286
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
+
287
318
  const remainingEvents = [];
288
319
 
289
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
+
290
327
  try {
291
- const response = await fetch(e.endpoint, {
292
- method: "POST",
293
- headers: { "Content-Type": "application/json" },
294
- body: JSON.stringify(e.payload),
295
- });
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
+ ]);
296
336
 
297
337
  if (response.ok) {
298
- this.logger.log("network", "Retried & sent event");
338
+ this.logger.log("network", "Retried & sent event successfully.");
299
339
  } else {
300
- // Drop event if server rejects it (4xx, 5xx), avoid infinite loop
301
- 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.`);
302
341
  }
303
342
  } catch (err) {
304
- // Keep if network error
343
+ this.logger.log("network", "Retry failed (still offline or bad host), keeping event.", err.message);
305
344
  remainingEvents.push(e);
306
345
  }
307
346
  }
308
347
 
309
- 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
+
310
364
  if (remainingEvents.length < stored.length) {
311
- this.logger.log("network", `Flush complete. Remaining: ${remainingEvents.length}`);
365
+ this.logger.log("network", `Flush complete. Remaining: ${updatedQueue.length}`);
312
366
  }
313
367
  } catch (error) {
314
- 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;
315
371
  }
316
372
  }
317
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,10 +1,11 @@
1
1
  {
2
2
  "name": "iidrak-analytics-react",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "react native client for metastreamio",
5
5
  "peerDependencies": {
6
6
  "react-native-mmkv": ">=4.0.0",
7
7
  "react-native-nitro-modules": ">=0.30.0",
8
+ "@react-native-async-storage/async-storage": ">=1.17.0",
8
9
  "@react-native-community/netinfo": ">=11.5.2",
9
10
  "react": ">=18.0.0",
10
11
  "react-native": ">=0.70.0",
@@ -12,6 +13,17 @@
12
13
  "react-native-view-shot": ">=3.0.0",
13
14
  "react-native-safe-area-context": ">=4.0.0"
14
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
+ },
15
27
  "dependencies": {
16
28
  "string-format": "^0.5.0"
17
29
  },