react-native-mosquito-transport 0.0.18 → 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.
Files changed (30) hide show
  1. package/.jshintignore +4 -0
  2. package/.jshintrc +16 -0
  3. package/README.md +75 -1
  4. package/TODO +10 -1
  5. package/example/ios/MosquitodbExample.xcodeproj/project.pbxproj +6 -5
  6. package/example/ios/MosquitodbExample.xcworkspace/contents.xcworkspacedata +10 -0
  7. package/example/ios/MosquitodbExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  8. package/example/ios/MosquitodbExample.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  9. package/example/ios/MosquitodbExample.xcworkspace/xcuserdata/anthony.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  10. package/example/ios/MosquitodbExample.xcworkspace/xcuserdata/anthony.xcuserdatad/WorkspaceSettings.xcsettings +14 -0
  11. package/ios/Mosquitodb.swift +14 -1
  12. package/package.json +15 -14
  13. package/src/helpers/engine_api.js +39 -0
  14. package/src/helpers/peripherals.js +73 -127
  15. package/src/helpers/utils.js +48 -19
  16. package/src/helpers/values.js +8 -47
  17. package/src/helpers/variables.js +14 -6
  18. package/src/index.d.ts +103 -43
  19. package/src/index.js +198 -121
  20. package/src/products/auth/accessor.js +97 -36
  21. package/src/products/auth/index.js +151 -82
  22. package/src/products/database/accessor.js +720 -223
  23. package/src/products/database/bson.js +16 -0
  24. package/src/products/database/counter.js +16 -0
  25. package/src/products/database/index.js +303 -190
  26. package/src/products/database/types.js +1 -1
  27. package/src/products/database/validator.js +517 -254
  28. package/src/products/http_callable/index.js +111 -106
  29. package/src/products/storage/index.js +97 -88
  30. package/src/helpers/EngineApi.js +0 -33
@@ -1,21 +1,26 @@
1
1
  import { io } from "socket.io-client";
2
- import EngineApi from "../../helpers/EngineApi";
2
+ import EngineApi from "../../helpers/engine_api";
3
3
  import { DatabaseRecordsListener } from "../../helpers/listeners";
4
- import { IS_WHOLE_NUMBER, cloneInstance, deserializeE2E, listenReachableServer, niceTry, serializeE2E, simplifyCaughtError } from "../../helpers/peripherals";
5
- import { awaitStore, buildFetchInterface, getReachableServer } from "../../helpers/utils";
4
+ import { deserializeE2E, listenReachableServer, niceTry, serializeE2E } from "../../helpers/peripherals";
5
+ import { awaitStore, buildFetchInterface, buildFetchResult, getReachableServer } from "../../helpers/utils";
6
6
  import { CacheStore, Scoped } from "../../helpers/variables";
7
- import { addPendingWrites, generateRecordID, getRecord, insertRecord, listenQueryEntry, removePendingWrite } from "./accessor";
8
- import { validateCollectionPath, validateFilter, validateReadConfig, validateWriteValue } from "./validator";
7
+ import { addPendingWrites, generateRecordID, getRecord, insertRecord, listenQueryEntry, removePendingWrite, validateWriteValue } from "./accessor";
8
+ import { validateCollectionName, validateFilter, validateFindConfig, validateFindObject, validateListenFindConfig } from "./validator";
9
9
  import { awaitRefreshToken, listenToken } from "../auth/accessor";
10
- import { DEFAULT_DB_NAME, DEFAULT_DB_URL, DELIVERY, RETRIEVAL } from "../../helpers/values";
10
+ import { DELIVERY, RETRIEVAL } from "../../helpers/values";
11
11
  import setLodash from 'lodash.set';
12
+ import { ObjectId } from "bson";
13
+ import { guardObject, Validator } from "guard-object";
14
+ import { simplifyCaughtError } from "simplify-error";
15
+ import cloneDeep from "lodash.clonedeep";
16
+ import { deserializeBSON, serializeToBase64 } from "./bson";
12
17
 
13
18
  export class MTCollection {
14
19
  constructor(config) {
15
20
  this.builder = { ...config };
16
21
  }
17
22
 
18
- find = (find) => ({
23
+ find = (find = {}) => ({
19
24
  get: (config) => findObject({ ...this.builder, command: { find } }, config),
20
25
  listen: (callback, error, config) => listenDocument(callback, error, { ...this.builder, command: { find } }, config),
21
26
  count: (config) => countCollection({ ...this.builder, command: { find } }, config),
@@ -51,11 +56,11 @@ export class MTCollection {
51
56
 
52
57
  limit = (limit) => this.find().limit(limit);
53
58
 
54
- count = (config) => countCollection({ ...this.builder }, config);
59
+ count = (config) => this.find().count(config);
55
60
 
56
- get = (config) => findObject({ ...this.builder }, config);
61
+ get = (config) => this.find().get(config);
57
62
 
58
- listen = (callback, error, config) => listenDocument(callback, error, { ...this.builder }, config);
63
+ listen = (callback, error, config) => this.find().listen(callback, error, config);
59
64
 
60
65
  findOne = (findOne = {}) => ({
61
66
  listen: (callback, error, config) => listenDocument(callback, error, { ...this.builder, command: { findOne } }, config),
@@ -65,36 +70,50 @@ export class MTCollection {
65
70
  onDisconnect = () => ({
66
71
  setOne: (value) => initOnDisconnectionTask({ ...this.builder }, value, 'setOne'),
67
72
  setMany: (value) => initOnDisconnectionTask({ ...this.builder }, value, 'setMany'),
68
- updateOne: (find, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'updateOne'),
69
- updateMany: (find, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'updateMany'),
70
- mergeOne: (find, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'mergeOne'),
71
- mergeMany: (find, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'mergeMany'),
72
- deleteOne: (find) => initOnDisconnectionTask({ ...this.builder, command: { find } }, null, 'deleteOne'),
73
- deleteMany: (find) => initOnDisconnectionTask({ ...this.builder, command: { find } }, null, 'deleteMany'),
74
- replaceOne: (find, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'replaceOne'),
75
- putOne: (find, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'putOne')
76
- })
73
+ updateOne: (find = {}, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'updateOne'),
74
+ updateMany: (find = {}, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'updateMany'),
75
+ mergeOne: (find = {}, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'mergeOne'),
76
+ mergeMany: (find = {}, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'mergeMany'),
77
+ deleteOne: (find = {}) => initOnDisconnectionTask({ ...this.builder, command: { find } }, undefined, 'deleteOne'),
78
+ deleteMany: (find = {}) => initOnDisconnectionTask({ ...this.builder, command: { find } }, undefined, 'deleteMany'),
79
+ replaceOne: (find = {}, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'replaceOne'),
80
+ putOne: (find = {}, value) => initOnDisconnectionTask({ ...this.builder, command: { find } }, value, 'putOne')
81
+ });
77
82
 
78
83
  setOne = (value, config) => commitData(this.builder, value, 'setOne', config);
79
84
 
80
85
  setMany = (value, config) => commitData(this.builder, value, 'setMany', config);
81
86
 
82
- updateOne = (find, value, config) => commitData({ ...this.builder, find }, value, 'updateOne', config);
87
+ addOne = (value, config) => commitData(
88
+ this.builder,
89
+ Validator.OBJECT(value) ? { ...value, _id: new ObjectId() } : value,
90
+ 'setOne',
91
+ config
92
+ );
93
+
94
+ addMany = (value, config) => commitData(
95
+ this.builder,
96
+ value.map(v => Validator.OBJECT(v) ? ({ ...v, _id: new ObjectId() }) : v),
97
+ 'setMany',
98
+ config
99
+ );
83
100
 
84
- updateMany = (find, value, config) => commitData({ ...this.builder, find }, value, 'updateMany', config);
101
+ updateOne = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'updateOne', config);
85
102
 
86
- mergeOne = (find, value, config) => commitData({ ...this.builder, find }, value, 'mergeOne', config);
103
+ updateMany = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'updateMany', config);
87
104
 
88
- mergeMany = (find, value, config) => commitData({ ...this.builder, find }, value, 'mergeMany', config);
105
+ mergeOne = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'mergeOne', config);
89
106
 
90
- replaceOne = (find, value, config) => commitData({ ...this.builder, find }, value, 'replaceOne', config);
107
+ mergeMany = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'mergeMany', config);
91
108
 
92
- putOne = (find, value, config) => commitData({ ...this.builder, find }, value, 'putOne', config);
109
+ replaceOne = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'replaceOne', config);
93
110
 
94
- deleteOne = (find, config) => commitData({ ...this.builder, find }, null, 'deleteOne', config);
111
+ putOne = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'putOne', config);
95
112
 
96
- deleteMany = (find, config) => commitData({ ...this.builder, find }, null, 'deleteMany', config);
97
- }
113
+ deleteOne = (find = {}, config) => commitData({ ...this.builder, find }, undefined, 'deleteOne', config);
114
+
115
+ deleteMany = (find = {}, config) => commitData({ ...this.builder, find }, undefined, 'deleteMany', config);
116
+ };
98
117
 
99
118
  export const batchWrite = (builder, map, config) => commitData({ ...builder }, map, 'batchWrite', config);
100
119
 
@@ -111,16 +130,16 @@ const {
111
130
  } = EngineApi;
112
131
 
113
132
  const listenDocument = (callback, onError, builder, config) => {
114
- const { projectUrl, wsPrefix, serverE2E_PublicKey, baseUrl, dbUrl, dbName, accessKey, path, disableCache, command, uglify } = builder,
115
- { find, findOne, sort, direction, limit } = command,
116
- { disableAuth } = config || {},
117
- accessId = generateRecordID(builder, config),
118
- shouldCache = !disableCache,
119
- processId = `${++Scoped.AnyProcessIte}`;
120
-
121
- validateReadConfig(config, ['retrieval', 'disableAuth']);
133
+ const { projectUrl, wsPrefix, serverE2E_PublicKey, baseUrl, dbUrl, dbName, accessKey, path, disableCache, command, uglify, extraHeaders, castBSON } = builder;
134
+ const { find, findOne, sort, direction, limit } = command;
135
+ const { disableAuth } = config || {};
136
+ const shouldCache = !disableCache;
137
+ const processId = `${++Scoped.AnyProcessIte}`;
138
+ let accessId;
139
+
140
+ validateListenFindConfig(config);
122
141
  validateFilter(findOne || find);
123
- validateCollectionPath(path);
142
+ validateCollectionName(path);
124
143
 
125
144
  let hasCancelled,
126
145
  hasRespond,
@@ -129,17 +148,33 @@ const listenDocument = (callback, onError, builder, config) => {
129
148
  wasDisconnected,
130
149
  lastToken = Scoped.AuthJWTToken[projectUrl] || null,
131
150
  lastInitRef = 0,
132
- connectedListener;
151
+ connectedListener,
152
+ lastSnapshot;
153
+
154
+ const dispatchSnapshot = s => {
155
+ const thisSnapshotId = serializeToBase64({ _: s });
156
+ if (thisSnapshotId === lastSnapshot) return;
157
+ lastSnapshot = thisSnapshotId;
158
+ callback?.(cloneDeep(transformBSON(s, castBSON)));
159
+ };
133
160
 
134
161
  if (shouldCache) {
135
- cacheListener = listenQueryEntry(callback, { accessId, builder, config, processId });
136
-
137
- connectedListener = listenReachableServer(async connected => {
138
- connectedListener();
139
- await awaitStore();
140
- if (!connected && !hasRespond && !hasCancelled && shouldCache)
141
- DatabaseRecordsListener.dispatch(accessId, processId);
142
- }, projectUrl);
162
+ accessId = generateRecordID(builder, config).then(hash => {
163
+ if (hasCancelled) return hash;
164
+ cacheListener = listenQueryEntry(snapshot => {
165
+ if (!Scoped.IS_CONNECTED[projectUrl]) dispatchSnapshot(snapshot);
166
+ }, { accessId: hash, builder, config, processId });
167
+ return hash;
168
+ });
169
+
170
+ awaitStore().then(() => {
171
+ if (hasCancelled) return;
172
+ connectedListener = listenReachableServer(async connected => {
173
+ connectedListener();
174
+ if (!connected && !hasRespond && !hasCancelled && shouldCache)
175
+ DatabaseRecordsListener.dispatch('d', processId);
176
+ }, projectUrl);
177
+ });
143
178
  }
144
179
 
145
180
  const init = async () => {
@@ -147,24 +182,27 @@ const listenDocument = (callback, onError, builder, config) => {
147
182
  if (!disableAuth) await awaitRefreshToken(projectUrl);
148
183
  if (hasCancelled || processID !== lastInitRef) return;
149
184
 
150
- const mtoken = disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl],
151
- authObj = {
152
- commands: {
153
- config: stripRequestConfig(config),
154
- path,
155
- find: findOne || find,
156
- sort,
157
- direction,
158
- limit
159
- },
160
- dbName,
161
- dbUrl
162
- };
185
+ const mtoken = disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl];
186
+ const pureConfig = stripRequestConfig(config);
187
+ const authObj = {
188
+ commands: stripUndefined({
189
+ config: pureConfig && serializeToBase64(pureConfig),
190
+ path,
191
+ find: serializeToBase64(findOne || find),
192
+ sort,
193
+ direction,
194
+ limit
195
+ }),
196
+ dbName,
197
+ dbUrl
198
+ };
163
199
 
164
- const [encPlate, [privateKey]] = uglify ? serializeE2E({ accessKey, _body: authObj }, mtoken, serverE2E_PublicKey) : ['', []];
200
+ const [encPlate, [privateKey]] = uglify ? await serializeE2E({ accessKey, _body: authObj }, mtoken, serverE2E_PublicKey) : ['', []];
165
201
 
166
202
  socket = io(`${wsPrefix}://${baseUrl}`, {
167
- auth: uglify ? { e2e: encPlate, _m_internal: true } : {
203
+ transports: ['websocket', 'polling', 'flashsocket'],
204
+ extraHeaders,
205
+ auth: uglify ? { e2e: encPlate.toString('base64'), _m_internal: true } : {
168
206
  accessKey,
169
207
  _body: authObj,
170
208
  ...mtoken ? { mtoken } : {},
@@ -176,13 +214,15 @@ const listenDocument = (callback, onError, builder, config) => {
176
214
  socket.on('mSnapshot', async ([err, snapshot]) => {
177
215
  hasRespond = true;
178
216
  if (err) {
179
- onError?.(simplifyCaughtError(err).simpleError);
217
+ if (typeof onError === 'function') {
218
+ onError(simplifyCaughtError(err).simpleError);
219
+ } else console.error('unhandled listen for:', { path, find }, ' error:', err);
180
220
  } else {
181
- if (uglify) snapshot = deserializeE2E(snapshot, serverE2E_PublicKey, privateKey);
182
- callback?.(snapshot);
221
+ if (uglify) snapshot = await deserializeE2E(snapshot, serverE2E_PublicKey, privateKey);
222
+ snapshot = deserializeBSON(snapshot)._;
223
+ dispatchSnapshot(snapshot);
183
224
 
184
- if (shouldCache)
185
- insertRecord(builder, accessId, { sort, direction, limit, find, findOne, config }, snapshot);
225
+ if (shouldCache) insertRecord(builder, config, await accessId, snapshot);
186
226
  }
187
227
  });
188
228
 
@@ -214,14 +254,16 @@ const listenDocument = (callback, onError, builder, config) => {
214
254
  tokenListener?.();
215
255
  if (socket) socket.close();
216
256
  }
217
- }
257
+ };
218
258
 
219
259
  const initOnDisconnectionTask = (builder, value, type) => {
220
- const { projectUrl, wsPrefix, baseUrl, serverE2E_PublicKey, dbUrl, dbName, accessKey, path, command, uglify } = builder,
221
- { find } = command || {},
222
- disableAuth = false;
260
+ const { projectUrl, wsPrefix, baseUrl, serverE2E_PublicKey, dbUrl, dbName, accessKey, path, extraHeaders, command, uglify } = builder;
261
+ const { find } = command || {};
262
+ const disableAuth = false;
263
+
264
+ validateCollectionName(path);
265
+ validateWriteValue({ type, find, value });
223
266
 
224
- validateCollectionPath(path);
225
267
  let hasCancelled,
226
268
  socket,
227
269
  wasDisconnected,
@@ -233,16 +275,23 @@ const initOnDisconnectionTask = (builder, value, type) => {
233
275
  if (!disableAuth) await awaitRefreshToken(projectUrl);
234
276
  if (hasCancelled || processID !== lastInitRef) return;
235
277
 
236
- const mtoken = disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl],
237
- authObj = {
238
- commands: { path, find, value, scope: type },
239
- dbName,
240
- dbUrl
241
- };
278
+ const mtoken = disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl];
279
+ const authObj = {
280
+ commands: stripUndefined({
281
+ path,
282
+ find: find && serializeToBase64(find),
283
+ value: value && serializeToBase64({ _: value }),
284
+ scope: type
285
+ }),
286
+ dbName,
287
+ dbUrl
288
+ };
242
289
 
243
290
  socket = io(`${wsPrefix}://${baseUrl}`, {
291
+ transports: ['websocket', 'polling', 'flashsocket'],
292
+ extraHeaders,
244
293
  auth: uglify ? {
245
- e2e: serializeE2E(authObj, mtoken, serverE2E_PublicKey)[0],
294
+ e2e: (await serializeE2E({ accessKey, _body: authObj }, mtoken, serverE2E_PublicKey))[0].toString('base64'),
246
295
  _m_internal: true
247
296
  } : {
248
297
  ...mtoken ? { mtoken } : {},
@@ -267,7 +316,7 @@ const initOnDisconnectionTask = (builder, value, type) => {
267
316
  const tokenListener = listenToken(async t => {
268
317
  if ((t || null) !== lastToken) {
269
318
  if (socket) {
270
- await niceTry(() => socket.timeout(10000).emitWithAck(_cancelDisconnectWriteTask(uglify)));
319
+ await niceTry(() => socket.timeout(7000).emitWithAck(_cancelDisconnectWriteTask(uglify)));
271
320
  socket.close();
272
321
  }
273
322
  wasDisconnected = undefined;
@@ -280,31 +329,26 @@ const initOnDisconnectionTask = (builder, value, type) => {
280
329
  if (hasCancelled) return;
281
330
  tokenListener();
282
331
  if (socket)
283
- (async () => {
284
- await niceTry(() => socket.timeout(10000).emitWithAck(_cancelDisconnectWriteTask(uglify)));
332
+ niceTry(() => socket.timeout(7000).emitWithAck(_cancelDisconnectWriteTask(uglify))).then(() => {
285
333
  socket.close();
286
- })();
334
+ });
287
335
  hasCancelled = true;
288
- }
289
- }
336
+ };
337
+ };
290
338
 
291
339
  const countCollection = async (builder, config) => {
292
- const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, accessKey, maxRetries = 7, uglify, path, disableCache, command = {} } = builder,
293
- { find } = command,
294
- { disableAuth } = config || {},
295
- accessId = generateRecordID({ ...builder, countDoc: true }, config);
340
+ const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, accessKey, maxRetries = 7, uglify, extraHeaders, path, disableCache, command = {} } = builder;
341
+ const { find } = command;
342
+ const { disableAuth } = config || {};
343
+ const accessId = await generateRecordID({ ...builder, countDoc: true }, config);
296
344
 
297
345
  await awaitStore();
298
- validateReadConfig(config, [
299
- 'excludeFields',
300
- 'returnOnly',
301
- 'extraction',
302
- 'episode',
303
- 'retrieval',
304
- 'disableMinimizer'
305
- ]);
306
- validateFilter(find || {});
307
- validateCollectionPath(path);
346
+ if (config !== undefined)
347
+ guardObject({
348
+ disableAuth: t => t === undefined || Validator.BOOLEAN(t)
349
+ }).validate(config);
350
+ validateFilter(find);
351
+ validateCollectionName(path);
308
352
 
309
353
  let retries = 0;
310
354
 
@@ -312,41 +356,42 @@ const countCollection = async (builder, config) => {
312
356
  ++retries;
313
357
 
314
358
  const finalize = (a, b) => {
315
- if (isNaN(a)) {
316
- reject(b);
317
- } else resolve(a);
318
- }
359
+ if (Validator.NUMBER(a)) {
360
+ resolve(a);
361
+ } else reject(b);
362
+ };
319
363
 
320
364
  try {
321
- if (!disableAuth && await getReachableServer(projectUrl)) await awaitRefreshToken(projectUrl);
365
+ if (!disableAuth && await getReachableServer(projectUrl))
366
+ await awaitRefreshToken(projectUrl);
322
367
 
323
- const [reqBuilder, [privateKey]] = buildFetchInterface({
368
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
324
369
  body: {
325
- commands: { path, find },
370
+ commands: { path, find: serializeToBase64(find) },
326
371
  dbName,
327
372
  dbUrl
328
373
  },
329
374
  accessKey,
330
375
  ...disableAuth ? {} : { authToken: Scoped.AuthJWTToken[projectUrl] },
331
376
  serverE2E_PublicKey,
332
- uglify
377
+ uglify,
378
+ extraHeaders
333
379
  });
334
380
 
335
- const r = await (await fetch(_documentCount(projectUrl, uglify), reqBuilder)).json();
336
- if (r.simpleError) throw r;
381
+ const data = await buildFetchResult(await fetch(_documentCount(projectUrl, uglify), reqBuilder), uglify);
337
382
 
338
- const f = uglify ? deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
383
+ const f = uglify ? await deserializeE2E(data, serverE2E_PublicKey, privateKey) : data;
339
384
 
340
385
  if (!disableCache)
341
- setLodash(CacheStore.DatabaseCountResult, [projectUrl, dbUrl || DEFAULT_DB_URL, dbName || DEFAULT_DB_NAME, accessId], f.result);
386
+ setLodash(CacheStore.DatabaseCountResult, [projectUrl, dbUrl, dbName, accessId], f.result);
342
387
 
343
388
  finalize(f.result);
344
389
  } catch (e) {
345
- const b4Data = setLodash(CacheStore.DatabaseCountResult, [projectUrl, dbUrl || DEFAULT_DB_URL, dbName || DEFAULT_DB_NAME, accessId]);
390
+ const b4Data = setLodash(CacheStore.DatabaseCountResult, [projectUrl, dbUrl, dbName, accessId]);
346
391
 
347
392
  if (e?.simpleError) {
348
393
  finalize(undefined, e.simpleError);
349
- } else if (!disableCache && !isNaN(b4Data)) {
394
+ } else if (!disableCache && !Validator.NUMBER(b4Data)) {
350
395
  finalize(b4Data);
351
396
  } else if (retries > maxRetries) {
352
397
  finalize(undefined, { error: 'retry_limit_exceeded', message: `retry exceed limit(${maxRetries})` });
@@ -364,9 +409,8 @@ const countCollection = async (builder, config) => {
364
409
  }
365
410
  });
366
411
 
367
- const g = await readValue();
368
- return g;
369
- }
412
+ return await readValue();
413
+ };
370
414
 
371
415
  const stripRequestConfig = (config) => {
372
416
  const known_fields = ['extraction', 'returnOnly', 'excludeFields'];
@@ -374,28 +418,33 @@ const stripRequestConfig = (config) => {
374
418
  known_fields.includes(k) ? [k, v] : null
375
419
  ).filter(v => v);
376
420
  return requestConfig.length ? Object.fromEntries(requestConfig) : undefined;
377
- }
421
+ };
378
422
 
379
- const findObject = async (builder, config) => {
380
- const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, accessKey, maxRetries = 7, path, disableCache, uglify, command } = builder,
381
- { find, findOne, sort, direction, limit, random } = command,
382
- { retrieval = RETRIEVAL.DEFAULT, episode = 0, disableAuth, disableMinimizer } = config || {},
383
- enableMinimizer = !disableMinimizer,
384
- accessId = generateRecordID(builder, config),
385
- processAccessId = `${accessId}${projectUrl}${dbUrl}${dbName}${retrieval}`,
386
- getRecordData = () => getRecord(builder, accessId),
387
- shouldCache = (retrieval === RETRIEVAL.DEFAULT ? !disableCache : true) &&
388
- retrieval !== RETRIEVAL.NO_CACHE_NO_AWAIT;
423
+ const stripUndefined = o => Object.fromEntries(
424
+ Object.entries(o).filter(v => v[1] !== undefined)
425
+ );
389
426
 
390
- await awaitStore();
391
- if (shouldCache) {
392
- validateReadConfig(config);
393
- validateCollectionPath(path);
394
- validateFilter(findOne || find);
427
+ const transformBSON = (d, castBSON) => {
428
+ if (castBSON) return d && deserializeBSON(serializeToBase64({ _: d }), true)._;
429
+ return cloneDeep(d);
430
+ };
395
431
 
396
- if (typeof limit === 'number' && (!IS_WHOLE_NUMBER(limit) || limit <= 0))
397
- throw `limit() has an invalid argument for "${path}", expected a positive whole number but got ${limit}`;
398
- }
432
+ const findObject = async (builder, config) => {
433
+ const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, accessKey, maxRetries = 7, path, disableCache, uglify, extraHeaders, command, castBSON } = builder;
434
+ const { find, findOne, sort, direction, limit, random } = command;
435
+ const { retrieval = RETRIEVAL.DEFAULT, episode = 0, disableAuth, disableMinimizer } = config || {};
436
+ const enableMinimizer = !disableMinimizer;
437
+ const accessId = await generateRecordID(builder, config);
438
+ const processAccessId = `${accessId}${projectUrl}${dbUrl}${dbName}${retrieval}`;
439
+ const getRecordData = () => getRecord(builder, config, accessId);
440
+ const shouldCache = (retrieval !== RETRIEVAL.DEFAULT || !disableCache) &&
441
+ ![RETRIEVAL.NO_CACHE_NO_AWAIT, RETRIEVAL.NO_CACHE_AWAIT].includes(retrieval);
442
+
443
+ const pureConfig = stripRequestConfig(config);
444
+ validateFindObject(command);
445
+ validateFindConfig(config);
446
+ validateCollectionName(path);
447
+ await awaitStore();
399
448
 
400
449
  let retries = 0, hasFinalize;
401
450
 
@@ -406,18 +455,18 @@ const findObject = async (builder, config) => {
406
455
  const finalize = (a, b) => {
407
456
  const res = (instantProcess && a) ?
408
457
  (a.liveResult || a.liveResult === null) ?
409
- (a.liveResult || undefined) :
410
- a.episode[episode] : a;
458
+ transformBSON(a.liveResult || undefined, castBSON) :
459
+ transformBSON(a.episode[episode], castBSON) : a;
411
460
 
412
461
  if (a) {
413
- resolve(instantProcess ? res : a);
414
- } else reject(b);
462
+ resolve(instantProcess ? cloneDeep(res) : a);
463
+ } else reject(instantProcess ? cloneDeep(b) : b);
415
464
  if (hasFinalize || !instantProcess) return;
416
465
  hasFinalize = true;
417
466
 
418
467
  if (enableMinimizer) {
419
468
  (Scoped.PendingDbReadCollective.pendingResolution[processAccessId] || []).forEach(e => {
420
- e(a ? { result: cloneInstance(res) } : undefined, b);
469
+ e(a ? { result: res } : undefined, b);
421
470
  });
422
471
  if (Scoped.PendingDbReadCollective.pendingResolution[processAccessId])
423
472
  delete Scoped.PendingDbReadCollective.pendingResolution[processAccessId];
@@ -435,8 +484,8 @@ const findObject = async (builder, config) => {
435
484
  Scoped.PendingDbReadCollective.pendingResolution[processAccessId] = [];
436
485
 
437
486
  Scoped.PendingDbReadCollective.pendingResolution[processAccessId].push((a, b) => {
438
- if (a) resolve(a.result);
439
- else reject(b);
487
+ if (a) resolve(cloneDeep(a.result));
488
+ else reject(cloneDeep(b));
440
489
  });
441
490
  return;
442
491
  }
@@ -448,51 +497,56 @@ const findObject = async (builder, config) => {
448
497
  if (retrieval !== RETRIEVAL.STICKY_RELOAD) return;
449
498
  }
450
499
  }
500
+
451
501
  if (!disableAuth && await getReachableServer(projectUrl))
452
502
  await awaitRefreshToken(projectUrl);
453
503
 
454
- const [reqBuilder, [privateKey]] = buildFetchInterface({
504
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
455
505
  body: {
456
- commands: {
457
- config: stripRequestConfig(config),
506
+ commands: stripUndefined({
507
+ config: pureConfig && serializeToBase64(pureConfig),
458
508
  path,
459
- find: findOne || find,
509
+ find: serializeToBase64(findOne || find),
460
510
  sort,
461
511
  direction,
462
512
  limit,
463
513
  random
464
- },
514
+ }),
465
515
  dbName,
466
516
  dbUrl
467
517
  },
468
518
  accessKey,
469
519
  authToken: disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl],
470
520
  serverE2E_PublicKey,
471
- uglify
521
+ uglify,
522
+ extraHeaders
472
523
  });
473
524
 
474
- const r = await (await fetch((findOne ? _readDocument : _queryCollection)(projectUrl, uglify), reqBuilder)).json();
475
- if (r.simpleError) throw r;
525
+ const data = await buildFetchResult(await fetch((findOne ? _readDocument : _queryCollection)(projectUrl, uglify), reqBuilder), uglify);
476
526
 
477
- const f = uglify ? deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
527
+ const result = deserializeBSON((uglify ? await deserializeE2E(data, serverE2E_PublicKey, privateKey) : data).result)._;
478
528
 
479
- if (shouldCache) insertRecord(builder, accessId, { ...command, config }, f.result);
480
- finalize({ liveResult: f.result || null });
529
+ if (shouldCache) insertRecord(builder, config, accessId, result);
530
+ finalize({ liveResult: result || null });
481
531
  } catch (e) {
532
+ let thisRecord;
533
+ const getThisRecord = async () => thisRecord ? thisRecord.value :
534
+ (thisRecord = { value: await getRecordData() }).value;
535
+
482
536
  if (e?.simpleError) {
483
537
  finalize(undefined, e?.simpleError);
484
538
  } else if (
485
- (retrieval === RETRIEVAL.CACHE_NO_AWAIT && !(await getRecordData())) ||
539
+ (retrieval === RETRIEVAL.CACHE_NO_AWAIT && !(await getThisRecord())) ||
486
540
  retrieval === RETRIEVAL.STICKY_NO_AWAIT ||
487
541
  retrieval === RETRIEVAL.NO_CACHE_NO_AWAIT
488
542
  ) {
489
543
  finalize(undefined, simplifyCaughtError(e).simpleError);
490
544
  } else if (
491
545
  shouldCache &&
492
- (retrieval === RETRIEVAL.DEFAULT || retrieval === RETRIEVAL.CACHE_NO_AWAIT) &&
493
- await getRecordData()
546
+ [RETRIEVAL.DEFAULT, RETRIEVAL.CACHE_NO_AWAIT].includes(retrieval) &&
547
+ await getThisRecord()
494
548
  ) {
495
- finalize({ episode: await getRecordData() });
549
+ finalize({ episode: await getThisRecord() });
496
550
  } else if (retries > maxRetries) {
497
551
  finalize(undefined, { error: 'retry_limit_exceeded', message: `retry exceed limit(${maxRetries})` });
498
552
  } else {
@@ -509,26 +563,50 @@ const findObject = async (builder, config) => {
509
563
  }
510
564
  });
511
565
 
512
- const g = await readValue();
513
- return g;
566
+ return await readValue();
514
567
  };
515
568
 
569
+ const transformNullRecursively = obj => Object.fromEntries(
570
+ Object.entries(obj).map(([k, v]) =>
571
+ [k, [undefined, Infinity, NaN].includes(v) ? null : Validator.OBJECT(v) ? transformNullRecursively(v) : v]
572
+ )
573
+ );
574
+
516
575
  const commitData = async (builder, value, type, config) => {
517
- const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, accessKey, maxRetries = 7, path, find, disableCache, uglify } = builder,
518
- { disableAuth, delivery = DELIVERY.DEFAULT, stepping } = config || {},
519
- writeId = `${Date.now() + ++Scoped.PendingIte}`,
520
- isBatchWrite = type === 'batchWrite',
521
- shouldCache = (delivery === DELIVERY.DEFAULT ? !disableCache : true) &&
522
- delivery !== DELIVERY.NO_CACHE &&
523
- delivery !== DELIVERY.NO_AWAIT_NO_CACHE &&
524
- delivery !== DELIVERY.AWAIT_NO_CACHE;
576
+ // transform undefined
577
+ if (Validator.OBJECT(value)) {
578
+ value = value && deserializeBSON(serializeToBase64({ _: transformNullRecursively(value) }))._;
579
+ } else if (type === 'batchWrite' && Array.isArray(value)) {
580
+ value = deserializeBSON(
581
+ serializeToBase64({
582
+ _: value.map(v => {
583
+ if (Validator.OBJECT(v?.value)) {
584
+ v.value = transformNullRecursively(v.value);
585
+ } else if (Array.isArray(v?.value)) {
586
+ v.value = v.value.map(e =>
587
+ Validator.OBJECT(e) ? transformNullRecursively(e) : e
588
+ );
589
+ }
590
+ return v;
591
+ })
592
+ })
593
+ )._;
594
+ }
595
+
596
+ const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, accessKey, maxRetries = 7, path, find, disableCache, uglify, extraHeaders } = builder;
597
+ const { disableAuth, delivery = DELIVERY.DEFAULT, stepping } = config || {};
598
+ const writeId = `${Date.now() + ++Scoped.PendingIte}`;
599
+ const isBatchWrite = type === 'batchWrite';
600
+ const shouldCache = (delivery !== DELIVERY.DEFAULT || !disableCache) &&
601
+ delivery !== DELIVERY.NO_CACHE &&
602
+ delivery !== DELIVERY.NO_AWAIT_NO_CACHE &&
603
+ delivery !== DELIVERY.AWAIT_NO_CACHE;
525
604
 
526
605
  await awaitStore();
527
606
  if (shouldCache) {
528
- validateCollectionPath(path);
529
- // TODO: batchWrite
530
- validateWriteValue(value, builder.find, type);
531
- await addPendingWrites(builder, writeId, { value, type, find });
607
+ await addPendingWrites(builder, writeId, { value, type, find, config });
608
+ Scoped.OutgoingWrites[writeId] = true;
609
+ await Scoped.dispatchingWritesPromise;
532
610
  }
533
611
 
534
612
  let retries = 0, hasFinalize;
@@ -549,55 +627,65 @@ const commitData = async (builder, value, type, config) => {
549
627
  } else reject(b);
550
628
  if (hasFinalize || !instantProcess) return;
551
629
  hasFinalize = true;
552
- if (removeCache && shouldCache) removePendingWrite(builder, writeId, revertCache);
630
+ if (shouldCache) {
631
+ if (removeCache) removePendingWrite(builder, writeId, revertCache);
632
+ if (Scoped.OutgoingWrites[writeId])
633
+ delete Scoped.OutgoingWrites[writeId];
634
+ }
553
635
  };
554
636
 
555
637
  try {
556
638
  if (!disableAuth && await getReachableServer(projectUrl))
557
639
  await awaitRefreshToken(projectUrl);
558
640
 
559
- const [reqBuilder, [privateKey]] = buildFetchInterface({
641
+ const [reqBuilder, [privateKey]] = await buildFetchInterface({
560
642
  body: {
561
- commands: {
562
- value,
643
+ commands: stripUndefined({
644
+ value: value && serializeToBase64({ _: value }),
563
645
  ...isBatchWrite ? { stepping } : {
564
646
  path,
565
647
  scope: type,
566
- find
648
+ find: find && serializeToBase64(find)
567
649
  }
568
- },
650
+ }),
569
651
  dbName,
570
652
  dbUrl
571
653
  },
572
654
  accessKey,
573
655
  serverE2E_PublicKey,
574
656
  authToken: disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl],
575
- uglify
657
+ uglify,
658
+ extraHeaders
576
659
  });
577
660
 
578
- const r = await (await fetch((isBatchWrite ? _writeMapDocument : _writeDocument)(projectUrl, uglify), reqBuilder)).json();
579
- if (r.simpleError) throw r;
661
+ const data = await buildFetchResult(await fetch((isBatchWrite ? _writeMapDocument : _writeDocument)(projectUrl, uglify), reqBuilder), uglify);
580
662
 
581
- const f = uglify ? deserializeE2E(r.e2e, serverE2E_PublicKey, privateKey) : r;
663
+ const f = uglify ? await deserializeE2E(data, serverE2E_PublicKey, privateKey) : data;
582
664
 
583
- finalize({ status: 'sent', committed: f.committed }, undefined, { removeCache: true });
665
+ finalize({ ...f.statusData }, undefined, { removeCache: true });
584
666
  } catch (e) {
585
667
  if (e?.simpleError) {
586
668
  console.error(`${type} error (${path}), ${e.simpleError?.message}`);
587
669
  finalize(undefined, e?.simpleError, { removeCache: true, revertCache: true });
588
670
  } else if (
589
- delivery === DELIVERY.NO_AWAIT ||
590
- delivery === DELIVERY.CACHE_NO_AWAIT ||
591
- delivery === DELIVERY.NO_AWAIT_NO_CACHE ||
592
- delivery === DELIVERY.NO_CACHE
671
+ [
672
+ DELIVERY.NO_AWAIT,
673
+ DELIVERY.CACHE_NO_AWAIT,
674
+ DELIVERY.NO_AWAIT_NO_CACHE,
675
+ DELIVERY.NO_CACHE
676
+ ].includes(delivery)
593
677
  ) {
594
678
  finalize(
595
679
  undefined,
596
680
  simplifyCaughtError(e).simpleError,
597
- await getReachableServer(projectUrl) ? { removeCache: true } : null
681
+ await getReachableServer(projectUrl) ? { removeCache: true } : undefined
682
+ );
683
+ } else if (retries >= maxRetries) {
684
+ finalize(
685
+ undefined,
686
+ { error: 'retry_limit_exceeded', message: `retry exceed limit(${maxRetries})` },
687
+ { removeCache: true, revertCache: true }
598
688
  );
599
- } else if (retries > maxRetries) {
600
- finalize(undefined, { error: 'retry_limit_exceeded', message: `retry exceed limit(${maxRetries})` });
601
689
  } else {
602
690
  if (delivery === DELIVERY.AWAIT_NO_CACHE) {
603
691
  const onlineListener = listenReachableServer(connected => {
@@ -609,11 +697,36 @@ const commitData = async (builder, value, type, config) => {
609
697
  );
610
698
  }
611
699
  }, projectUrl);
612
- } else if (shouldCache) finalize({ status: 'pending' });
700
+ } else if (shouldCache) finalize({ status: 'queued' });
613
701
  else finalize(undefined, simplifyCaughtError(e).simpleError);
614
702
  }
615
703
  }
616
704
  });
617
705
 
618
706
  return await sendValue();
619
- }
707
+ };
708
+
709
+ export const trySendPendingWrite = (projectUrl) => {
710
+ if (Scoped.dispatchingWritesPromise) return;
711
+
712
+ Scoped.dispatchingWritesPromise = new Promise(async resolve => {
713
+ const sortedWrite = Object.entries(CacheStore.PendingWrites[projectUrl] || {})
714
+ .filter(([k]) => !Scoped.OutgoingWrites[k])
715
+ .sort((a, b) => a[1].addedOn - b[1].addedOn);
716
+
717
+ for (const [writeId, { snapshot, builder, attempts = 1 }] of sortedWrite) {
718
+ try {
719
+ await commitData(builder, snapshot.value, snapshot.type, { ...snapshot.config, delivery: DELIVERY.NO_AWAIT_NO_CACHE });
720
+ delete CacheStore.PendingWrites[projectUrl][writeId];
721
+ } catch (_) {
722
+ const { maxRetries } = builder;
723
+ if (!maxRetries || attempts >= maxRetries) {
724
+ delete CacheStore.PendingWrites[projectUrl][writeId];
725
+ }
726
+ }
727
+ }
728
+ resolve();
729
+ Scoped.dispatchingWritesPromise = undefined;
730
+ if (sortedWrite.length && await getReachableServer(projectUrl)) trySendPendingWrite(projectUrl);
731
+ });
732
+ };