mosquito-transport 1.9.2 → 1.9.3

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.
@@ -1,6 +1,6 @@
1
1
  import { MongoClient } from "mongodb";
2
2
  import { serialize } from 'mongodb/lib/bson.js';
3
- import { BLOCKS_IDENTIFIERS, encryptData, isPath, isValidColName, isValidDbName, one_gb, resolvePath } from "./utils.js";
3
+ import { BLOCKS_IDENTIFIERS, encryptData, isPath, isValidColName, isValidDbName, one_gb, resolvePath, wait } from "./utils.js";
4
4
  import { readdir, stat } from "fs/promises";
5
5
  import { createReadStream } from "fs";
6
6
  import { Validator } from "guard-object";
@@ -8,7 +8,7 @@ import { WritableBit } from "@deflexable/bit-stream";
8
8
  import { join } from "path";
9
9
 
10
10
  const BIT_SIZE = one_gb * .2;
11
- const DOC_LIMITER = 500;
11
+ const DOC_LIMITER = 300;
12
12
 
13
13
  export const extractBackup = (config) => {
14
14
  let { database, storage, password, onMongodbOption } = { ...config };
@@ -90,6 +90,7 @@ export const extractBackup = (config) => {
90
90
  pushBuffer(Buffer.from(`${thisCol}`, 'utf8'));
91
91
 
92
92
  while (canLoadMore) {
93
+ await wait(7); // pause for garbage collection
93
94
  const data = await dbNameInstance.collection(thisCol).find({})
94
95
  .skip(offset).limit(DOC_LIMITER).toArray();
95
96
  offset += DOC_LIMITER;
@@ -149,6 +150,7 @@ export const extractBackup = (config) => {
149
150
  reject(err);
150
151
  });
151
152
  });
153
+ await wait(1); // pause for garbage collection
152
154
  } else {
153
155
  const files = await readdir(dir);
154
156
  if (files.length) {
@@ -1,4 +1,4 @@
1
- import { BLOCKS_IDENTIFIERS, decryptData, resolvePath } from "./utils.js";
1
+ import { BLOCKS_IDENTIFIERS, decryptData, resolvePath, wait } from "./utils.js";
2
2
  import { MongoClient } from "mongodb";
3
3
  import { deserialize } from 'mongodb/lib/bson.js';
4
4
  import { mkdir } from "fs/promises";
@@ -100,7 +100,9 @@ export const installBackup = (config) => new Promise((callResolve, callReject) =
100
100
  { ...docRest },
101
101
  { upsert: true }
102
102
  );
103
- ++installionStats.totalWrittenDocuments;
103
+ if (!(++installionStats.totalWrittenDocuments % 300)) {
104
+ await wait(7); // pause for garbage collection
105
+ }
104
106
  } else {
105
107
  lastBlocks.database = INIT_BLOCKS.database;
106
108
 
@@ -135,7 +137,9 @@ export const installBackup = (config) => new Promise((callResolve, callReject) =
135
137
  const writeStream = createWriteStream(lastBlocks.storage.path);
136
138
  writeStream.write(thisElem);
137
139
  lastBlocks.storage.file = writeStream;
138
- ++installionStats.totalWrittenFiles;
140
+ if (!(++installionStats.totalWrittenFiles % 50)) {
141
+ await wait(3); // pause for garbage collection
142
+ };
139
143
  };
140
144
  } else throw `unknown block identifier "${prevHeader}" at block_id ${BLOCK_ID}`;
141
145
  }
package/bin/utils.js CHANGED
@@ -4,6 +4,11 @@ import { createCipheriv, createDecipheriv, createHash } from 'node:crypto';
4
4
  export const one_mb = 1024 * 1024,
5
5
  one_gb = one_mb * 1024;
6
6
 
7
+ export const wait = (ms = 1000) =>
8
+ new Promise(resolve => {
9
+ setTimeout(resolve, ms);
10
+ });
11
+
7
12
  export const BLOCKS_IDENTIFIERS = {
8
13
  DB_URL: '--->[DB_URL]:',
9
14
  DB_NAME: '--->[DB_NAME]:',
@@ -22,5 +22,9 @@ export const Scoped = {
22
22
  * }}
23
23
  */
24
24
  InstancesData: {},
25
- BlacklistedTokens: {}
25
+ BlacklistedTokens: {},
26
+ /**
27
+ * @type {{ [key: string]: { callers: Map; destroy: () => void; }}}
28
+ */
29
+ AccumulatedDatabaseEmittions: {}
26
30
  };
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Db, Document, MongoClient, MongoClientOptions, SortDirection, UpdateDescription } from "mongodb";
1
+ import { ChangeStreamOptions, Db, Document, MongoClient, MongoClientOptions, SortDirection, UpdateDescription } from "mongodb";
2
2
  import express from "express";
3
3
  import { CorsOptions } from "cors";
4
4
  import { Sort } from "mongodb";
@@ -882,22 +882,24 @@ interface MosquitoHttpOptions {
882
882
  allowDisabledAuth?: boolean;
883
883
  }
884
884
 
885
- interface DatabaseListenerOption {
886
- includeBeforeData?: boolean;
887
- includeAfterData?: boolean;
885
+ interface DatabaseListenerOption extends ChangeStreamOptions {
886
+ /**
887
+ * An array of {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents.
888
+ */
888
889
  pipeline?: { pipeline?: Document[] }
889
890
  }
890
891
 
891
892
  interface DatabaseListenerCallbackData {
892
893
  insertion?: { _id: string };
894
+ update?: UpdateDescription;
895
+ replacement?: { _id: string };
893
896
  deletion?: string;
894
- update?: UpdateDescription,
895
- before?: Document,
896
- after?: Document,
897
- timestamp: number,
898
- auth?: AuthData | undefined,
899
- operation: 'insert' | 'delete' | 'update';
897
+ before?: Document;
898
+ after?: Document;
899
+ timestamp: number;
900
+ operation: 'insert' | 'update' | 'replace' | 'delete';
900
901
  documentKey: string;
902
+ extras: any
901
903
  }
902
904
 
903
905
  interface StorageSnapshot {
package/lib/index.js CHANGED
@@ -799,13 +799,14 @@ export default class MosquitoTransportServer {
799
799
  if (normalizeRoute(route) === normalizeRoute(e))
800
800
  throw `"${e}" is a reserved route used internally`;
801
801
  });
802
+ const { logger } = this.config;
803
+ const hasLogger = logger.includes('all') || logger.includes('external-requests'),
804
+ hasErrorLogger = logger.includes('all') || logger.includes('error');
805
+
802
806
  Scoped.expressInstances[this.port].use(
803
807
  express.Router({ caseSensitive: true }).all(`/${normalizeRoute(route)}`, async (req, res) => {
804
808
  const { mtoken, uglified } = req.headers;
805
- const { logger } = this.config;
806
- const hasLogger = logger.includes('all') || logger.includes('external-requests'),
807
- hasErrorLogger = logger.includes('all') || logger.includes('error'),
808
- now = hasLogger && Date.now();
809
+ const now = hasLogger && Date.now();
809
810
 
810
811
  if (hasLogger) console.log(`started route: /${req.url}`);
811
812
  res.set(NO_CACHE_HEADER);
@@ -917,13 +918,13 @@ export default class MosquitoTransportServer {
917
918
 
918
919
  listenDatabase = (path, callback, options) => {
919
920
  if (typeof path !== 'string') throw `listenDatabase first argument must be a string but got ${path}`;
920
- const { dbName, dbUrl } = options || {},
921
- { logger } = this.config;
921
+ const { dbName, dbUrl } = options || {};
922
+ const { logger } = this.config;
923
+ const hasLogger = logger.includes('all') || logger.includes('database-snapshot'),
924
+ hasErrorLogger = logger.includes('all') || logger.includes('error');
922
925
 
923
926
  return emitDatabase(path, async function () {
924
- const hasLogger = logger.includes('all') || logger.includes('database-snapshot'),
925
- hasErrorLogger = logger.includes('all') || logger.includes('error'),
926
- now = hasLogger && Date.now();
927
+ const now = hasLogger && Date.now();
927
928
  if (hasLogger) console.log(`db-snapshot ${path}: `, arguments[0]);
928
929
  try {
929
930
  await callback?.(...arguments);
@@ -1006,11 +1007,11 @@ export default class MosquitoTransportServer {
1006
1007
 
1007
1008
  listenStorage = (callback) => {
1008
1009
  const { logger } = this.config;
1010
+ const hasLogger = logger.includes('all') || logger.includes('storage'),
1011
+ hasErrorLogger = logger.includes('all') || logger.includes('error');
1009
1012
 
1010
1013
  return StorageListener.listenTo(this.projectName, async ({ dest, ...rest }) => {
1011
- const hasLogger = logger.includes('all') || logger.includes('storage'),
1012
- hasErrorLogger = logger.includes('all') || logger.includes('error'),
1013
- now = hasLogger && Date.now();
1014
+ const now = hasLogger && Date.now();
1014
1015
 
1015
1016
  if (hasLogger) console.log(`started listenStorage ${dest}:`);
1016
1017
  try {
@@ -1022,18 +1023,20 @@ export default class MosquitoTransportServer {
1022
1023
  });
1023
1024
  };
1024
1025
 
1025
- listenNewUser = (callback) => emitDatabase(EnginePath.userAcct, s => {
1026
- if (s.insertion) {
1027
- const j = { ...s.insertion };
1028
- j.uid = j._id;
1029
- if (j._id) delete j._id;
1030
- callback?.(j);
1031
- }
1032
- }, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
1026
+ listenNewUser = (callback) =>
1027
+ emitDatabase(EnginePath.userAcct, s => {
1028
+ if (s.insertion) {
1029
+ const j = { ...s.insertion };
1030
+ j.uid = j._id;
1031
+ if (j._id) delete j._id;
1032
+ callback?.(j);
1033
+ }
1034
+ }, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
1033
1035
 
1034
- listenDeletedUser = (callback) => emitDatabase(EnginePath.userAcct, s => {
1035
- if (s.deletion) callback?.(s.deletion);
1036
- }, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
1036
+ listenDeletedUser = (callback) =>
1037
+ emitDatabase(EnginePath.userAcct, s => {
1038
+ if (s.deletion) callback?.(s.deletion);
1039
+ }, this.projectName, ADMIN_DB_NAME, ADMIN_DB_URL);
1037
1040
 
1038
1041
  updateUserProfile = async (uid, profile) => {
1039
1042
  if (!Validator.OBJECT(profile)) throw 'updateUserProfile() second argument must be an object';
@@ -6,19 +6,22 @@ import { Scoped } from "../../helpers/variables.js";
6
6
  */
7
7
  export const getDB = (projectName, name, url = DEFAULT_DB) => {
8
8
  if (!projectName) throw 'expected projectName in getDb()';
9
- const { defaultName: dbName, instance } = getDbInstance(projectName, url) || {};
10
-
11
- if (name === ADMIN_DB_NAME) name = dbName;
12
- if (!instance) throw `no MongoClient was found for database with dbRef "${url}"`;
13
- // if (!name && !dbName) throw `no dbName found for database with dbRef "${dbUrl}"`;
14
-
15
- return instance.db(name || dbName);
9
+ const { dbName, instance } = getDbNaming(projectName, name, url) || {};
10
+ return instance.instance.db(dbName);
16
11
  };
17
12
 
18
- export const getDbInstance = (projectName, dbUrl = DEFAULT_DB) => {
13
+ export const getDbNaming = (projectName, name, dbRef = DEFAULT_DB) => {
19
14
  if (!projectName) throw 'expected projectName in getDb()';
20
- if (dbUrl === 'admin' || dbUrl === 'default') throw `reserved keyword dbRef: "${dbUrl}"`;
15
+ if (dbRef === 'admin' || dbRef === 'default') throw `reserved keyword dbRef: "${dbRef}"`;
16
+
17
+ dbRef = dbRef === ADMIN_DB_URL ? 'admin' : dbRef === DEFAULT_DB ? 'default' : dbRef;
18
+ const instance = Scoped.InstancesData[projectName].mongoInstances[dbRef];
19
+
20
+ if (!instance) throw `no MongoClient was found for database with dbRef "${dbRef}"`;
21
21
 
22
- dbUrl = dbUrl === ADMIN_DB_URL ? 'admin' : dbUrl === DEFAULT_DB ? 'default' : dbUrl;
23
- return Scoped.InstancesData[projectName].mongoInstances[dbUrl];
22
+ return {
23
+ dbRef,
24
+ instance,
25
+ dbName: name === ADMIN_DB_NAME ? instance.defaultName : (name || instance.defaultName)
26
+ };
24
27
  }
@@ -1,6 +1,6 @@
1
1
  import express from "express";
2
2
  import { deserializeE2E, encodeBinary, niceTry, serializeE2E } from "../../helpers/utils.js";
3
- import { getDB, getDbInstance } from "./base.js";
3
+ import { getDB, getDbNaming } from "./base.js";
4
4
  import { validateJWT } from "../auth/tokenizer.js";
5
5
  import { Scoped } from "../../helpers/variables.js";
6
6
  import { EngineRoutes, ERRORS, NO_CACHE_HEADER } from "../../helpers/values.js";
@@ -79,12 +79,11 @@ const deserializeWriteValue = (value) => {
79
79
  } else return value;
80
80
  };
81
81
 
82
- const cleanseFind = (path, find, projectName, dbName, dbUrl) => {
83
- const { defaultName, instance } = getDbInstance(projectName, dbUrl);
84
- dbName = dbName || defaultName;
82
+ const cleanseFind = (path, find, projectName, name, dbUrl) => {
83
+ const { instance, dbName } = getDbNaming(projectName, name, dbUrl);
85
84
 
86
- if (instance.__intercepted) {
87
- const d = instance.interceptMap?.map?.[dbName]?.[path];
85
+ if (instance.instance.__intercepted) {
86
+ const d = instance.instance.interceptMap?.map?.[dbName]?.[path];
88
87
  if (d?.fulltext) return find;
89
88
  }
90
89
  return cleanseFindCore(find);
@@ -289,33 +288,95 @@ const extractDocField = async (d, commands, projectName, dbName, dbUrl, doc_hold
289
288
  };
290
289
 
291
290
  export const emitDatabase = (path, callback, projectName, dbName, dbUrl, options) => {
292
- const { includeBeforeData, includeAfterData, pipeline } = options || {};
291
+ const naming = getDbNaming(projectName, dbName, dbUrl);
293
292
 
294
- const col = getDB(projectName, dbName, dbUrl).collection(path),
295
- stream = col.watch(pipeline, {
296
- fullDocument: includeAfterData ? 'whenAvailable' : undefined,
297
- fullDocumentBeforeChange: includeBeforeData ? 'whenAvailable' : undefined
298
- });
293
+ const nodeId = `${path}:${projectName}:${naming.dbName}:${naming.dbRef}:${options && serializeToBase64(options)}`;
294
+ let instance = Scoped.AccumulatedDatabaseEmittions[nodeId];
295
+
296
+ if (!instance) {
297
+ const callers = new Map();
298
+ const destroy = internalEmitDatabase(path, (...args) => {
299
+ callers.forEach(value => {
300
+ value(...args);
301
+ });
302
+ }, projectName, dbName, dbUrl, options);
303
+
304
+ Scoped.AccumulatedDatabaseEmittions[nodeId] = (instance = { callers, destroy });
305
+ }
306
+
307
+ const ref = {};
308
+ instance.callers.set(ref, callback);
309
+
310
+ return () => {
311
+ if (!instance.callers.has(ref)) return;
312
+ instance.callers.delete(ref);
313
+ if (!instance.callers.size) {
314
+ instance.destroy();
315
+ delete Scoped.AccumulatedDatabaseEmittions[nodeId];
316
+ }
317
+ }
318
+ }
319
+
320
+ const requiredEvent = ['insert', 'update', 'replace', 'delete'];
321
+
322
+ const internalEmitDatabase = (path, callback, projectName, dbName, dbUrl, options, resumeAfter) => {
323
+ const { pipeline, ...restOptions } = options || {};
324
+
325
+ const col = getDB(projectName, dbName, dbUrl).collection(path);
326
+ const stream = col.watch(pipeline || [
327
+ {
328
+ $match: {
329
+ operationType: { $in: requiredEvent }
330
+ }
331
+ }
332
+ ], {
333
+ ...restOptions,
334
+ resumeAfter
335
+ });
299
336
 
300
337
  stream.on('change', l => {
301
- const { operationType: ops, fullDocument, fullDocumentBeforeChange, documentKey, updateDescription, clusterTime } = l;
338
+ const { operationType: ops, fullDocument, fullDocumentBeforeChange, documentKey, updateDescription, clusterTime, ...rest } = l;
339
+
340
+ if (!requiredEvent.includes(ops)) return;
302
341
 
303
- if (ops !== 'insert' && ops !== 'delete' && ops !== 'update') return;
304
342
  callback?.({
305
343
  documentKey: documentKey._id,
306
344
  insertion: ops === 'insert' ? fullDocument : undefined,
307
- deletion: ops === 'delete' ? documentKey._id : undefined,
308
345
  update: ops === 'update' ? { ...updateDescription } : undefined,
309
- before: includeBeforeData ? fullDocumentBeforeChange : undefined,
310
- after: includeAfterData ? fullDocument : undefined,
311
- timestamp: clusterTime?.toNumber?.(),
312
- auth: undefined,
313
- operation: ops
346
+ replacement: ops === 'replace' ? fullDocument : undefined,
347
+ deletion: ops === 'delete' ? documentKey._id : undefined,
348
+ before: fullDocumentBeforeChange,
349
+ after: fullDocument,
350
+ timestamp: clusterTime,
351
+ operation: ops,
352
+ extras: rest
314
353
  });
315
354
  });
316
355
 
356
+ stream.on('resumeTokenChanged', (token) => {
357
+ resumeAfter = token;
358
+ });
359
+
360
+ let closure = null;
361
+
362
+ stream.on('error', async (error) => {
363
+ await stream.close();
364
+ setTimeout(() => {
365
+ if (closure === null)
366
+ closure = internalEmitDatabase(path, callback, projectName, dbName, dbUrl, options, resumeAfter);
367
+ }, 7000);
368
+ process.emit('uncaughtException', `emitDatabase error: ${error}`);
369
+ });
370
+
317
371
  return () => {
318
- stream.close();
372
+ const thisClosure = closure;
373
+ closure = undefined;
374
+
375
+ if (thisClosure === null) {
376
+ stream.close();
377
+ } else if (typeof thisClosure === 'function') {
378
+ thisClosure();
379
+ }
319
380
  }
320
381
  };
321
382
 
@@ -658,7 +719,7 @@ export const databaseLiveRoutesHandler = ({
658
719
  } catch (e) {
659
720
  socket.emit('mSnapshot', [simplifyCaughtError(e), undefined]);
660
721
  }
661
- }, projectName, dbName, dbUrl, { pipeline: { ...commands.find } });
722
+ }, projectName, dbName, dbUrl);
662
723
  } catch (e) {
663
724
  if (hasErrorLoger) console.error(`errRoute /${route} err:`, e);
664
725
  socket.emit('mSnapshot', [simplifyCaughtError(e), undefined]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mosquito-transport",
3
- "version": "1.9.2",
3
+ "version": "1.9.3",
4
4
  "description": "Quickly spawn server infrastructure along robust authentication, database, storage, and cross-platform compatibility",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",