react-native-mosquito-transport 0.0.19 → 0.0.21

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/src/index.js CHANGED
@@ -1,20 +1,20 @@
1
1
  import 'react-native-get-random-values';
2
- import { IS_WHOLE_NUMBER, deserializeE2E, listenReachableServer, serializeE2E } from "./helpers/peripherals";
3
- import { releaseCacheStore } from "./helpers/utils";
4
- import { Scoped } from "./helpers/variables";
5
- import { MTAuth } from "./products/auth";
6
- import { MTCollection, batchWrite } from "./products/database";
2
+ import { deserializeE2E, listenReachableServer, serializeE2E } from "./helpers/peripherals";
3
+ import { awaitStore, releaseCacheStore } from "./helpers/utils";
4
+ import { CacheStore, Scoped } from "./helpers/variables";
5
+ import { MTCollection, batchWrite, trySendPendingWrite } from "./products/database";
7
6
  import { MTStorage } from "./products/storage";
8
7
  import { ServerReachableListener, TokenRefreshListener } from "./helpers/listeners";
9
8
  import { initTokenRefresher, listenToken, listenTokenReady, triggerAuthToken } from "./products/auth/accessor";
10
9
  import { TIMESTAMP, DOCUMENT_EXTRACTION, FIND_GEO_JSON, GEO_JSON } from "./products/database/types";
11
10
  import { mfetch } from "./products/http_callable";
12
11
  import { io } from "socket.io-client";
13
- import { validateCollectionPath } from "./products/database/validator";
14
- import { AUTH_PROVIDER_ID, CACHE_PROTOCOL, Regexs } from "./helpers/values";
15
- import { trySendPendingWrite } from "./products/database/accessor";
16
- import EngineApi from './helpers/EngineApi';
17
- import { parse, stringify } from 'json-buffer';
12
+ import { AUTH_PROVIDER_ID, CACHE_PROTOCOL } from "./helpers/values";
13
+ import EngineApi from './helpers/engine_api';
14
+ import { Validator } from 'guard-object';
15
+ import cloneDeep from 'lodash.clonedeep';
16
+ import { Buffer } from 'buffer';
17
+ import MTAuth, { purgePendingToken } from './products/auth';
18
18
 
19
19
  const {
20
20
  _listenCollection,
@@ -24,33 +24,48 @@ const {
24
24
  _listenUserVerification
25
25
  } = EngineApi;
26
26
 
27
+ // https://socket.io/docs/v3/emit-cheatsheet/#reserved-events
28
+ const reservedEventName = [
29
+ 'connect',
30
+ 'connect_error',
31
+ 'disconnect',
32
+ 'disconnecting',
33
+ 'newListener',
34
+ 'removeListener'
35
+ ];
36
+
27
37
  class RNMT {
28
38
  constructor(config) {
29
39
  validateMTConfig(config, this);
30
40
  this.config = {
31
41
  ...config,
32
- uglify: config.enableE2E_Encryption,
33
- apiUrl: config.projectUrl,
34
- projectUrl: config.projectUrl.split('/').slice(0, -1).join('/')
42
+ serverE2E_PublicKey: config.serverE2E_PublicKey && new Uint8Array(Buffer.from(config.serverE2E_PublicKey, 'base64')),
43
+ castBSON: config.castBSON === undefined || config.castBSON,
44
+ maxRetries: config.maxRetries || 3,
45
+ uglify: config.enableE2E_Encryption
35
46
  };
36
- const { projectUrl } = this.config;
47
+ const { projectUrl, extraHeaders } = this.config;
37
48
 
38
49
  this.config.secureUrl = projectUrl.startsWith('https');
39
50
  this.config.baseUrl = projectUrl.split('://')[1];
40
51
  this.config.wsPrefix = this.config.secureUrl ? 'wss' : 'ws';
41
52
 
42
53
  if (!Scoped.ReleaseCacheData)
43
- throw `releaseCache must be called before creating any ${this.constructor.name} instance`;
54
+ throw `initializeCache must be called before creating any ${this.constructor.name} instance`;
44
55
 
45
56
  if (!Scoped.InitializedProject[projectUrl]) {
46
- Scoped.InitializedProject[projectUrl] = true;
57
+ Scoped.InitializedProject[projectUrl] = cloneDeep(this.config);
47
58
  Scoped.LastTokenRefreshRef[projectUrl] = 0;
48
59
  triggerAuthToken(projectUrl);
49
60
  initTokenRefresher({ ...this.config }, true);
50
61
 
51
- let isConnected, lastSentToken, queuedToken;
62
+ let isConnected,
63
+ lastSentToken,
64
+ queuedToken;
52
65
 
53
- const socket = io(`${this.config.wsPrefix}://${projectUrl.split('://')[1]}`, {
66
+ const socket = io(`${this.config.wsPrefix}://${this.config.baseUrl}`, {
67
+ transports: ['websocket', 'polling', 'flashsocket'],
68
+ extraHeaders,
54
69
  auth: {
55
70
  _m_internal: true,
56
71
  _from_base: true
@@ -63,12 +78,17 @@ class RNMT {
63
78
 
64
79
  socket.on('connect', () => {
65
80
  isConnected = true;
81
+ Scoped.IS_CONNECTED[projectUrl] = true;
66
82
  if (queuedToken) updateMountedToken(queuedToken.token);
67
83
  ServerReachableListener.dispatch(projectUrl, true);
84
+ awaitStore().then(() => {
85
+ trySendPendingWrite(projectUrl);
86
+ });
68
87
  });
69
88
 
70
89
  socket.on('disconnect', () => {
71
90
  isConnected = false;
91
+ Scoped.IS_CONNECTED[projectUrl] = false;
72
92
  ServerReachableListener.dispatch(projectUrl, false);
73
93
  });
74
94
 
@@ -78,7 +98,7 @@ class RNMT {
78
98
  lastSentToken = token;
79
99
  }
80
100
  queuedToken = undefined;
81
- }
101
+ };
82
102
 
83
103
  listenToken(token => {
84
104
  if (isConnected) {
@@ -86,36 +106,35 @@ class RNMT {
86
106
  } else queuedToken = { token };
87
107
  }, projectUrl);
88
108
 
89
- listenReachableServer(c => {
90
- Scoped.IS_CONNECTED[projectUrl] = c;
91
- if (c) trySendPendingWrite();
92
- }, projectUrl);
93
-
94
109
  TokenRefreshListener.listenTo(projectUrl, v => {
95
110
  Scoped.IS_TOKEN_READY[projectUrl] = v;
96
111
  });
97
112
  }
98
113
  }
99
114
 
100
- static releaseCache(prop) {
115
+ static initializeCache(prop) {
101
116
  if (Scoped.ReleaseCacheData) throw `calling ${this.name}() multiple times is prohibited`;
102
117
  validateReleaseCacheProp({ ...prop });
103
118
  Scoped.ReleaseCacheData = { ...prop };
104
119
  releaseCacheStore({ ...prop });
120
+ // purge residue tokens
121
+ awaitStore().then(() => {
122
+ Object.keys(CacheStore.PendingAuthPurge).forEach(k => {
123
+ purgePendingToken(k);
124
+ });
125
+ });
105
126
  }
106
127
 
107
128
  getDatabase = (dbName, dbUrl) => ({
108
129
  collection: (path) => new MTCollection({
109
130
  ...this.config,
110
131
  path,
111
- ...(dbName ? { dbName } : {}),
112
- ...(dbUrl ? { dbUrl } : {})
132
+ ...dbName ? { dbName } : {},
133
+ ...dbUrl ? { dbUrl } : {}
113
134
  })
114
135
  });
115
- collection = (path) => {
116
- validateCollectionPath(path);
117
- return new MTCollection({ ...this.config, path });
118
- }
136
+
137
+ collection = (path) => new MTCollection({ ...this.config, path });
119
138
  batchWrite = (map, configx) => batchWrite({ ...this.config }, map, configx);
120
139
  auth = () => new MTAuth({ ...this.config });
121
140
  storage = () => new MTStorage({ ...this.config });
@@ -123,8 +142,8 @@ class RNMT {
123
142
  listenReachableServer = (callback) => listenReachableServer(callback, this.config.projectUrl);
124
143
 
125
144
  getSocket = (configOpts) => {
126
- const { disableAuth, authHandshake } = configOpts || {},
127
- { projectUrl, uglify, accessKey, serverE2E_PublicKey, wsPrefix } = this.config;
145
+ const { disableAuth, authHandshake } = configOpts || {};
146
+ const { projectUrl, uglify, accessKey, serverE2E_PublicKey, wsPrefix, extraHeaders } = this.config;
128
147
 
129
148
  const restrictedRoute = [
130
149
  _listenCollection,
@@ -132,12 +151,14 @@ class RNMT {
132
151
  _startDisconnectWriteTask,
133
152
  _cancelDisconnectWriteTask,
134
153
  _listenUserVerification
135
- ];
154
+ ].map(v => [v(), v(true)]).flat();
136
155
 
137
- let socketReadyCallback,
138
- makeSocketCallback = () => new Promise(resolve => {
156
+ const makeSocketCallback = () =>
157
+ new Promise(resolve => {
139
158
  socketReadyCallback = resolve;
140
- }),
159
+ });
160
+
161
+ let socketReadyCallback,
141
162
  socketReadyPromise = makeSocketCallback(),
142
163
  socketListenerList = [],
143
164
  socketListenerIte = 0;
@@ -147,25 +168,31 @@ class RNMT {
147
168
  tokenListener,
148
169
  clientPrivateKey;
149
170
 
150
- const listenerCallback = (callback) => function () {
151
- const [args, ...restArgs] = [...arguments];
171
+ const listenerCallback = (route, callback) => async function () {
172
+ if (reservedEventName.includes(route)) {
173
+ callback?.(...[...arguments]);
174
+ return;
175
+ }
176
+
177
+ const [[args, not_encrypted], emitable] = [...arguments];
152
178
  let res;
153
179
 
154
180
  if (uglify) {
155
- res = parse(deserializeE2E(args, serverE2E_PublicKey, clientPrivateKey));
181
+ res = await deserializeE2E(args, serverE2E_PublicKey, clientPrivateKey);
156
182
  } else res = args;
183
+ const sortedArgs = discloseSocketArguments([res, not_encrypted]);
157
184
 
158
- callback?.(...res || [], ...typeof restArgs[0] === 'function' ? [function () {
159
- const args = [...arguments];
185
+ callback?.(...sortedArgs, ...typeof emitable === 'function' ? [async function () {
186
+ const [args, not_encrypted] = encloseSocketArguments([...arguments]);
160
187
  let res;
161
188
 
162
189
  if (uglify) {
163
- res = serializeE2E(stringify(args), undefined, serverE2E_PublicKey)[0];
190
+ res = (await serializeE2E(args, undefined, serverE2E_PublicKey))[0];
164
191
  } else res = args;
165
192
 
166
- restArgs[0](res);
193
+ emitable([res, not_encrypted]);
167
194
  }] : []);
168
- }
195
+ };
169
196
 
170
197
  const emit = ({ timeout, promise, emittion: emittionx }) => new Promise(async (resolve, reject) => {
171
198
  const [route, ...emittion] = emittionx;
@@ -178,41 +205,43 @@ class RNMT {
178
205
 
179
206
  let hasResolved, stime = Date.now();
180
207
 
181
- const timer = isNaN(timeout) ? undefined : setTimeout(() => {
208
+ const timer = timeout ? setTimeout(() => {
182
209
  hasResolved = true;
183
210
  reject(new Error('emittion timeout'));
184
- }, timeout);
211
+ }, timeout) : undefined;
185
212
 
186
213
  await socketReadyPromise;
187
214
  if (hasResolved) return;
188
215
  clearTimeout(timer);
189
216
 
190
217
  try {
191
- const h = isNaN(timeout) ? socket : socket.timeout(timeout - (Date.now() - stime));
218
+ const thisSocket = timeout ? socket.timeout(Math.max(timeout - (Date.now() - stime), 0)) : socket;
192
219
 
193
- const lastEmit = emittion.slice(-1)[0],
194
- mit = typeof lastEmit === 'function' ? emittion.slice(0, -1) : emittion;
220
+ const lastEmit = emittion.slice(-1)[0];
221
+ const hasEmitable = typeof lastEmit === 'function';
222
+ const [mit, not_encrypted] = encloseSocketArguments(hasEmitable ? emittion.slice(0, -1) : emittion);
195
223
 
196
- const [reqBuilder, [privateKey]] = uglify ? serializeE2E(stringify(mit), undefined, serverE2E_PublicKey) : [undefined, []];
224
+ const [reqBuilder, [privateKey]] = uglify ? await serializeE2E(mit, undefined, serverE2E_PublicKey) : [undefined, []];
197
225
 
198
- if (typeof lastEmit === 'function' && promise)
199
- throw 'emitWithAck cannot have function in it parameter';
226
+ if (hasEmitable && promise)
227
+ throw 'emitWithAck cannot have function in it argument';
200
228
 
201
- const p = await h[promise ? 'emitWithAck' : 'emit'](route,
202
- ...uglify ? [reqBuilder] : [mit],
203
- ...typeof lastEmit === 'function' ? [function () {
204
- const args = [...arguments][0];
229
+ const result = await thisSocket[promise ? 'emitWithAck' : 'emit'](route,
230
+ [uglify ? reqBuilder : mit, not_encrypted],
231
+ ...hasEmitable ? [async function () {
232
+ const [[args, not_encrypted]] = [...arguments];
205
233
  let res;
206
234
 
207
235
  if (uglify) {
208
- res = parse(deserializeE2E(args, serverE2E_PublicKey, privateKey));
236
+ res = await deserializeE2E(args, serverE2E_PublicKey, privateKey);
209
237
  } else res = args;
210
238
 
211
- lastEmit(...res || []);
239
+ lastEmit(...discloseSocketArguments([res, not_encrypted]));
212
240
  }] : []
213
241
  );
214
-
215
- resolve((promise && p) ? uglify ? parse(deserializeE2E(p, serverE2E_PublicKey, privateKey))[0] : p[0] : undefined);
242
+ if (promise && result) {
243
+ resolve(discloseSocketArguments([uglify ? await deserializeE2E(result[0], serverE2E_PublicKey, privateKey) : result[0], result[1]])[0]);
244
+ } else resolve();
216
245
  } catch (e) {
217
246
  reject(e);
218
247
  }
@@ -221,12 +250,14 @@ class RNMT {
221
250
  const init = async () => {
222
251
  if (hasCancelled) return;
223
252
  const mtoken = disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl];
224
- const [reqBuilder, [privateKey]] = uglify ? serializeE2E({ accessKey, a_extras: authHandshake }, mtoken, serverE2E_PublicKey) : [null, []];
253
+ const [reqBuilder, [privateKey]] = uglify ? await serializeE2E({ accessKey, a_extras: authHandshake }, mtoken, serverE2E_PublicKey) : [null, []];
225
254
 
226
255
  socket = io(`${wsPrefix}://${projectUrl.split('://')[1]}`, {
256
+ transports: ['websocket', 'polling', 'flashsocket'],
257
+ extraHeaders,
227
258
  auth: uglify ? {
228
259
  ugly: true,
229
- e2e: reqBuilder
260
+ e2e: reqBuilder.toString('base64')
230
261
  } : {
231
262
  ...mtoken ? { mtoken } : {},
232
263
  a_extras: authHandshake,
@@ -261,15 +292,20 @@ class RNMT {
261
292
  }
262
293
 
263
294
  return {
264
- timeout: (timeout) => ({
265
- emitWithAck: function () {
266
- return emit({
267
- timeout,
268
- promise: true,
269
- emittion: [...arguments]
270
- });
271
- }
272
- }),
295
+ timeout: (timeout) => {
296
+ if (timeout !== undefined && !Validator.POSITIVE_INTEGER(timeout))
297
+ throw `expected a positive integer for timeout but got ${timeout}`;
298
+
299
+ return {
300
+ emitWithAck: function () {
301
+ return emit({
302
+ timeout,
303
+ promise: true,
304
+ emittion: [...arguments]
305
+ });
306
+ }
307
+ };
308
+ },
273
309
  emit: function () { emit({ emittion: [...arguments] }) },
274
310
  emitWithAck: function () {
275
311
  return emit({
@@ -281,7 +317,7 @@ class RNMT {
281
317
  if (restrictedRoute.includes(route))
282
318
  throw `${route} is a restricted socket path, avoid using any of ${restrictedRoute}`;
283
319
  const ref = ++socketListenerIte,
284
- listener = listenerCallback(callback);
320
+ listener = listenerCallback(route, callback);
285
321
 
286
322
  socketListenerList.push([ref, 'on', route, listener]);
287
323
  if (socket) socket.on(route, listener);
@@ -295,7 +331,7 @@ class RNMT {
295
331
  if (restrictedRoute.includes(route))
296
332
  throw `${route} is a restricted socket path, avoid using any of ${restrictedRoute}`;
297
333
  const ref = ++socketListenerIte,
298
- listener = listenerCallback(callback);
334
+ listener = listenerCallback(route, callback);
299
335
 
300
336
  socketListenerList.push([ref, 'once', route, listener]);
301
337
  if (socket) socket.once(route, listener);
@@ -313,10 +349,31 @@ class RNMT {
313
349
  }
314
350
  }
315
351
  }
352
+ };
316
353
 
317
- wipeDatabaseCache = () => {
318
-
354
+ class DoNotEncrypt {
355
+ constructor(value) {
356
+ this.value = value;
319
357
  }
358
+ };
359
+
360
+ const encloseSocketArguments = (args) => {
361
+ const [encrypted, unencrypted] = [{}, {}];
362
+
363
+ args.forEach((v, i) => {
364
+ if (v instanceof DoNotEncrypt) {
365
+ unencrypted[i] = v.value;
366
+ } else encrypted[i] = v;
367
+ });
368
+ return [encrypted, unencrypted];
369
+ }
370
+
371
+ const discloseSocketArguments = (args = []) => {
372
+ return args.map((obj, i) => Object.entries(obj).map(v => i ? [v[0], new DoNotEncrypt(v[1])] : v)).flat()
373
+ .sort((a, b) => (a[0] * 1) - (b[0] * 1)).map((v, i) => {
374
+ if (v[0] * 1 !== i) throw 'corrupted socket arguments';
375
+ return v[1];
376
+ });
320
377
  }
321
378
 
322
379
  const validateReleaseCacheProp = (prop) => {
@@ -335,6 +392,11 @@ const validateReleaseCacheProp = (prop) => {
335
392
  throw `Invalid value supplied to "io.${k}", expected a function but got "${v}"`;
336
393
  } else throw `Unexpected property named "io.${k}"`;
337
394
  });
395
+ } else if (k === 'promoteCache') {
396
+ if (typeof v !== 'boolean') throw 'promoteCache should be a boolean';
397
+ } else if (k === 'heapMemory') {
398
+ if (typeof v !== 'number' || v <= 0)
399
+ throw `Invalid value supplied to heapMemory, value must be an integer greater than zero`;
338
400
  } else throw `Unexpected property named ${k}`;
339
401
  });
340
402
 
@@ -344,19 +406,15 @@ const validateReleaseCacheProp = (prop) => {
344
406
  const validator = {
345
407
  dbName: (v) => {
346
408
  if (typeof v !== 'string' || !v.trim())
347
- throw `Invalid value supplied to dbName, value must be string and greater than one`;
409
+ throw `Invalid value supplied to dbName, value must be a non-empty string`;
348
410
  },
349
411
  dbUrl: (v) => {
350
412
  if (typeof v !== 'string' || !v.trim())
351
- throw `Invalid value supplied to dbUrl, value must be string and greater than one`;
352
- },
353
- heapMemory: (v) => {
354
- if (typeof v !== 'number' || v <= 0)
355
- throw `Invalid value supplied to heapMemory, value must be number and greater than zero`;
413
+ throw `Invalid value supplied to dbUrl, value must be a non-empty string`;
356
414
  },
357
415
  projectUrl: (v) => {
358
- if (typeof v !== 'string' || !Regexs.LINK().test(v.trim()))
359
- throw `Invalid value supplied to projectUrl, value must be a string and greater than one`;
416
+ if (typeof v !== 'string' || (!Validator.HTTPS(v) && !Validator.HTTP(v)))
417
+ throw `Expected "projectUrl" to be valid https or http link but got "${v}"`;
360
418
  },
361
419
  disableCache: (v) => {
362
420
  if (typeof v !== 'boolean')
@@ -364,40 +422,57 @@ const validator = {
364
422
  },
365
423
  accessKey: (v) => {
366
424
  if (typeof v !== 'string' || !v.trim())
367
- throw `Invalid value supplied to accessKey, value must be a string and greater than one`;
425
+ throw `Invalid value supplied to accessKey, value must be a non-empty string`;
368
426
  },
369
427
  maxRetries: (v) => {
370
- if (typeof v !== 'number' || v <= 0 || !IS_WHOLE_NUMBER(v))
371
- throw `Invalid value supplied to maxRetries, value must be whole number and greater than zero`;
428
+ if (v <= 0 || !Validator.POSITIVE_INTEGER(v))
429
+ throw `Invalid value supplied to maxRetries, value must be positive integer greater than zero`;
372
430
  },
373
431
  enableE2E_Encryption: (v) => {
374
432
  if (typeof v !== 'boolean')
375
433
  throw `Invalid value supplied to enableE2E_Encryption, value must be a boolean`;
376
434
  },
435
+ castBSON: v => {
436
+ if (typeof v !== 'boolean')
437
+ throw `Invalid value supplied to castBSON, value must be a boolean`;
438
+ },
439
+ borrowToken: v => {
440
+ if (typeof v !== 'string' || (!Validator.HTTPS(v) && !Validator.HTTP(v)))
441
+ throw `Expected "borrowToken" to be valid https or http link but got "${v}"`;
442
+ },
377
443
  serverE2E_PublicKey: (v) => {
378
444
  if (typeof v !== 'string' || !v.trim())
379
- throw `Invalid value supplied to serverETE_PublicKey, value must be string and greater than one`;
445
+ throw `Invalid value supplied to serverETE_PublicKey, value must be a non-empty string`;
446
+ },
447
+ extraHeaders: v => {
448
+ if (!Validator.OBJECT(v)) throw '"extraHeaders" must be an object';
449
+ const reservedHeaders = ['mtoken', 'mosquito-token', 'init-content-type', 'content-type', 'authorization', 'uglified'];
450
+
451
+ Object.entries(v).forEach(([k, v]) => {
452
+ if (typeof v !== 'string') throw `expected a string at extraHeaders.${k} but got "${v}"`;
453
+ if (reservedHeaders.includes(v.toLowerCase()))
454
+ throw `extraHeaders must not include any reserved props which are: ${reservedHeaders}`;
455
+ });
380
456
  }
381
457
  };
382
458
 
383
459
  const validateMTConfig = (config, that) => {
384
- if (typeof config !== 'object') throw `${that.constructor.name} config is not an object`;
385
- const h = Object.keys(config);
386
-
387
- for (let i = 0; i < h.length; i++) {
388
- const k = h[i];
460
+ if (!Validator.OBJECT(config))
461
+ throw `${that.constructor.name} config is not an object`;
389
462
 
463
+ for (const [k, v] of Object.entries(config)) {
390
464
  if (!validator[k]) throw `Unexpected property named ${k}`;
391
- validator[k](config[k]);
465
+ validator[k](v);
392
466
  }
393
467
 
394
468
  if (config.enableE2E_Encryption && !config.serverE2E_PublicKey)
395
469
  throw '"serverE2E_PublicKey" is missing, enabling end-to-end encryption requires a public encryption key from the server';
396
- if (!config['projectUrl']) throw `projectUrl is a required property in ${that.constructor.name}() constructor`;
397
- if (!config['accessKey']) throw `accessKey is a required property in ${that.constructor.name}() constructor`;
470
+ if (!config.projectUrl) throw `projectUrl is a required property in ${that.constructor.name}() constructor`;
471
+ if (!config.accessKey) throw `accessKey is a required property in ${that.constructor.name}() constructor`;
398
472
  }
399
473
 
400
474
  export {
475
+ DoNotEncrypt,
401
476
  TIMESTAMP,
402
477
  DOCUMENT_EXTRACTION,
403
478
  FIND_GEO_JSON,