From 1ce67f6e7b5abad80f93326dcba08205860b66eb Mon Sep 17 00:00:00 2001 From: matheusccastro Date: Tue, 11 Apr 2023 18:31:18 -0300 Subject: [PATCH 1/4] Create `Meteor.wrapFn` to wrap hooks and functions that can/should accept a promise and apply that to accounts, startup and cursor actions code. --- packages/accounts-base/accounts_server.js | 4 ++-- packages/callback-hook/hook.js | 9 +++++++++ packages/meteor/dynamics_nodejs.js | 2 +- packages/meteor/helpers.js | 19 +++++++++++++++++++ packages/meteor/startup_server.js | 1 + packages/mongo/mongo_driver.js | 6 ++++-- 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index fa46fbc03c..39eac8779f 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -190,7 +190,7 @@ export class AccountsServer extends AccountsCommon { throw new Error("Can only call onCreateUser once"); } - this._onCreateUserHook = func; + this._onCreateUserHook = Meteor.wrapFn(func); } /** @@ -564,7 +564,7 @@ export class AccountsServer extends AccountsCommon { this._loginHandlers.push({ name: name, - handler: handler + handler: Meteor.wrapFn(handler) }); }; diff --git a/packages/callback-hook/hook.js b/packages/callback-hook/hook.js index 58d33cc17e..e5eb9961df 100644 --- a/packages/callback-hook/hook.js +++ b/packages/callback-hook/hook.js @@ -49,6 +49,11 @@ export class Hook { this.bindEnvironment = false; } + this.wrapAsync = true; + if (options.wrapAsync === false) { + this.wrapAsync = false; + } + if (options.exceptionHandler) { this.exceptionHandler = options.exceptionHandler; } else if (options.debugPrintExceptions) { @@ -73,6 +78,10 @@ export class Hook { callback = dontBindEnvironment(callback, exceptionHandler); } + if (this.wrapAsync) { + callback = Meteor.wrapFn(callback); + } + const id = this.nextCallbackId++; this.callbacks[id] = callback; diff --git a/packages/meteor/dynamics_nodejs.js b/packages/meteor/dynamics_nodejs.js index 3a5ccd6750..6ff4d51e6e 100644 --- a/packages/meteor/dynamics_nodejs.js +++ b/packages/meteor/dynamics_nodejs.js @@ -75,7 +75,7 @@ EVp.withValue = function (value, func) { var saved = currentValues[this.slot]; try { currentValues[this.slot] = value; - return func(); + return Meteor.wrapFn(func)(); } finally { currentValues[this.slot] = saved; } diff --git a/packages/meteor/helpers.js b/packages/meteor/helpers.js index 242921945c..1ddc13f6bc 100644 --- a/packages/meteor/helpers.js +++ b/packages/meteor/helpers.js @@ -156,6 +156,25 @@ Meteor.wrapAsync = function (fn, context) { }; }; +Meteor.wrapFn = (fn) => { + if (!fn || typeof fn !== 'function') { + throw new Meteor.Error("Expected to receive function to wrap"); + }; + + if (Meteor.isClient) { + return fn; + } + + return function(...args) { + const ret = fn.apply(this, args); + if (ret && typeof ret.then === 'function') { + return Promise.await(ret); + } + + return ret; + } +}; + // Sets child's prototype to a new object whose prototype is parent's // prototype. Used as: // Meteor._inherits(ClassB, ClassA). diff --git a/packages/meteor/startup_server.js b/packages/meteor/startup_server.js index 4fde153632..e80e41add7 100644 --- a/packages/meteor/startup_server.js +++ b/packages/meteor/startup_server.js @@ -1,4 +1,5 @@ Meteor.startup = function startup(callback) { + callback = Meteor.wrapFn(callback); if (process.env.METEOR_PROFILE) { // Create a temporary error to capture the current stack trace. var error = new Error("Meteor.startup"); diff --git a/packages/mongo/mongo_driver.js b/packages/mongo/mongo_driver.js index 71635d29f3..5ae6d2db28 100644 --- a/packages/mongo/mongo_driver.js +++ b/packages/mongo/mongo_driver.js @@ -1209,6 +1209,7 @@ _.extend(SynchronousCursor.prototype, { forEach: function (callback, thisArg) { var self = this; + const wrappedFn = Meteor.wrapFn(callback); // Get back to the beginning. self._rewind(); @@ -1220,16 +1221,17 @@ _.extend(SynchronousCursor.prototype, { while (true) { var doc = self._nextObject(); if (!doc) return; - callback.call(thisArg, doc, index++, self._selfForIteration); + wrappedFn.call(thisArg, doc, index++, self._selfForIteration); } }, // XXX Allow overlapping callback executions if callback yields. map: function (callback, thisArg) { var self = this; + const wrappedFn = Meteor.wrapFn(callback); var res = []; self.forEach(function (doc, index) { - res.push(callback.call(thisArg, doc, index, self._selfForIteration)); + res.push(wrappedFn.call(thisArg, doc, index, self._selfForIteration)); }); return res; }, From 343fa7f55a7fcac2f564c57294ec54e54523de24 Mon Sep 17 00:00:00 2001 From: matheusccastro Date: Tue, 11 Apr 2023 18:33:46 -0300 Subject: [PATCH 2/4] Add Assets async API and create findOneAsync on Mongo Connection API too --- packages/mongo/mongo_driver.js | 34 +++++++++++++------- packages/mongo/remote_collection_driver.js | 15 +++++++++ tools/isobuild/bundler.js | 37 +++++++++++++++++----- tools/static-assets/server/boot.js | 31 ++++++++++++++---- 4 files changed, 90 insertions(+), 27 deletions(-) diff --git a/packages/mongo/mongo_driver.js b/packages/mongo/mongo_driver.js index 5ae6d2db28..365aa57fb1 100644 --- a/packages/mongo/mongo_driver.js +++ b/packages/mongo/mongo_driver.js @@ -834,19 +834,35 @@ MongoConnection.prototype.find = function (collectionName, selector, options) { self, new CursorDescription(collectionName, selector, options)); }; -MongoConnection.prototype.findOne = function (collection_name, selector, - options) { +MongoConnection.prototype.findOneAsync = async function (collection_name, selector, + options) { var self = this; if (arguments.length === 1) selector = {}; + options = options || {}; + options.limit = 1; + return (await self.find(collection_name, selector, options).fetchAsync())[0]; +}; + +MongoConnection.prototype.findOne = function (collection_name, selector, + options) { + var self = this; // [FIBERS] // TODO: Remove this when 3.0 is released. warnUsingOldApi("findOne"); - options = options || {}; - options.limit = 1; - return self.find(collection_name, selector, options).fetch()[0]; + return Future.fromPromise(self.findOneAsync(collection_name, selector, options)).wait(); +}; + +MongoConnection.prototype.createIndexAsync = function (collectionName, index, + options) { + var self = this; + + // We expect this function to be called at startup, not from within a method, + // so we don't interact with the write fence. + var collection = self.rawCollection(collectionName); + return collection.createIndex(index, options); }; // We'll actually design an index API later. For now, we just pass through to @@ -854,17 +870,11 @@ MongoConnection.prototype.findOne = function (collection_name, selector, MongoConnection.prototype.createIndex = function (collectionName, index, options) { var self = this; - // [FIBERS] // TODO: Remove this when 3.0 is released. warnUsingOldApi("createIndex"); - // We expect this function to be called at startup, not from within a method, - // so we don't interact with the write fence. - var collection = self.rawCollection(collectionName); - var future = new Future; - var indexName = collection.createIndex(index, options, future.resolver()); - future.wait(); + return Future.fromPromise(self.createIndexAsync(collectionName, index, options)); }; MongoConnection.prototype.countDocuments = function (collectionName, ...args) { diff --git a/packages/mongo/remote_collection_driver.js b/packages/mongo/remote_collection_driver.js index 035af45157..b618b8141b 100644 --- a/packages/mongo/remote_collection_driver.js +++ b/packages/mongo/remote_collection_driver.js @@ -1,3 +1,8 @@ +import { + ASYNC_COLLECTION_METHODS, + getAsyncMethodName +} from "meteor/minimongo/constants"; + MongoInternals.RemoteCollectionDriver = function ( mongo_url, options) { var self = this; @@ -28,6 +33,16 @@ Object.assign(MongoInternals.RemoteCollectionDriver.prototype, { REMOTE_COLLECTION_METHODS.forEach( function (m) { ret[m] = _.bind(self.mongo[m], self.mongo, name); + + if (!ASYNC_COLLECTION_METHODS.includes(m)) return; + const asyncMethodName = getAsyncMethodName(m); + ret[asyncMethodName] = function (...args) { + try { + return Promise.resolve(ret[m](...args)); + } catch (error) { + return Promise.reject(error); + } + } }); return ret; } diff --git a/tools/isobuild/bundler.js b/tools/isobuild/bundler.js index 19da6c4827..6fbec630b3 100644 --- a/tools/isobuild/bundler.js +++ b/tools/isobuild/bundler.js @@ -2040,11 +2040,6 @@ class JsImage { assetPath = files.convertToStandardPath(assetPath); var promise; if (! callback) { - if (! Fiber.current) { - throw new Error("The synchronous Assets API can " + - "only be called from within a Fiber."); - } - promise = new Promise(function (resolve, reject) { callback = function (err, res) { err ? reject(err) : resolve(res); @@ -2069,7 +2064,7 @@ class JsImage { } if (promise) { - return promise.await(); + return promise; } }; @@ -2226,7 +2221,20 @@ class JsImage { * @param {Function} [asyncCallback] Optional callback, which is called asynchronously with the error or result after the function is complete. If not provided, the function runs synchronously. */ getText: function (assetPath, callback) { - return getAsset(item.assets, assetPath, "utf8", callback); + const result = getAsset(item.assets, assetPath, "utf8", callback); + + if (!callback) { + if (!Fiber.current) { + throw new Error("The synchronous Assets API can " + + "only be called from within a Fiber."); + } + + return Promise.await(result); + } + }, + + getTextAsync: function (assetPath) { + return getAsset(item.assets, assetPath, "utf8"); }, /** @@ -2237,7 +2245,20 @@ class JsImage { * @param {Function} [asyncCallback] Optional callback, which is called asynchronously with the error or result after the function is complete. If not provided, the function runs synchronously. */ getBinary: function (assetPath, callback) { - return getAsset(item.assets, assetPath, undefined, callback); + const result = getAsset(item.assets, assetPath, undefined, callback); + + if (!callback) { + if (!Fiber.current) { + throw new Error("The synchronous Assets API can " + + "only be called from within a Fiber."); + } + + return Promise.await(result); + } + }, + + getBinaryAsync: function (assetPath) { + return getAsset(item.assets, assetPath, undefined); } } }, bindings || {}); diff --git a/tools/static-assets/server/boot.js b/tools/static-assets/server/boot.js index 2707518670..c37cdee6b6 100644 --- a/tools/static-assets/server/boot.js +++ b/tools/static-assets/server/boot.js @@ -315,10 +315,14 @@ var loadServerBundles = Profile("Load server bundles", function () { }; var getAsset = function (assetPath, encoding, callback) { - var fut; + var promiseResolver, promise; if (! callback) { - fut = new Future(); - callback = fut.resolver(); + promise = new Promise((resolve, reject) => { + promiseResolver = function (error, result) { + error ? reject(error) : resolve(result); + } + }); + callback = promiseResolver; } // This assumes that we've already loaded the meteor package, so meteor // itself can't call Assets.get*. (We could change this function so that @@ -347,16 +351,29 @@ var loadServerBundles = Profile("Load server bundles", function () { var filePath = path.join(serverDir, fileInfo.assets[assetPath]); fs.readFile(files.convertToOSPath(filePath), encoding, _callback); } - if (fut) - return fut.wait(); + + if (promise) + return promise; }; var Assets = { getText: function (assetPath, callback) { - return getAsset(assetPath, "utf8", callback); + const result = getAsset(assetPath, "utf8", callback); + if (!callback) { + return Future.fromPromise(result).wait(); + } + }, + getTextAsync: function (assetPath) { + return getAsset(assetPath, "utf8"); }, getBinary: function (assetPath, callback) { - return getAsset(assetPath, undefined, callback); + const result = getAsset(assetPath, undefined, callback); + if (!callback) { + return Future.fromPromise(result).wait(); + } + }, + getBinaryAsync: function (assetPath) { + return getAsset(assetPath, undefined); }, /** * @summary Get the absolute path to the static server asset. Note that assets are read-only. From 458b1d3a946c8e96104c1a8c17c90b26d2a0fee7 Mon Sep 17 00:00:00 2001 From: matheusccastro Date: Tue, 11 Apr 2023 18:34:22 -0300 Subject: [PATCH 3/4] Add callAsync code dependencies when the client is a node instance. --- packages/meteor/dynamics_nodejs.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/meteor/dynamics_nodejs.js b/packages/meteor/dynamics_nodejs.js index 6ff4d51e6e..3b7750b094 100644 --- a/packages/meteor/dynamics_nodejs.js +++ b/packages/meteor/dynamics_nodejs.js @@ -3,6 +3,7 @@ var Fiber = Npm.require('fibers'); var nextSlot = 0; +var callAsyncMethodRunning = false; Meteor._nodeCodeMustBeInFiber = function () { if (!Fiber.current) { @@ -106,6 +107,15 @@ EVp._setNewContextAndGetCurrent = function (value) { return saved; }; +EVp._isCallAsyncMethodRunning = function () { + return callAsyncMethodRunning; +}; + +EVp._setCallAsyncMethodRunning = function (value) { + callAsyncMethodRunning = value; +}; + + // Meteor application code is always supposed to be run inside a // fiber. bindEnvironment ensures that the function it wraps is run from // inside a fiber and ensures it sees the values of Meteor environment From 67afd31e377c0947c4cd7985f724b4215d7be5c4 Mon Sep 17 00:00:00 2001 From: matheusccastro Date: Wed, 19 Apr 2023 20:51:54 -0300 Subject: [PATCH 4/4] Use es5 compatible syntax --- packages/meteor/helpers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/meteor/helpers.js b/packages/meteor/helpers.js index 31f1ff7f2d..851f1967f2 100644 --- a/packages/meteor/helpers.js +++ b/packages/meteor/helpers.js @@ -163,17 +163,17 @@ Meteor.wrapAsync = function (fn, context) { }; }; -Meteor.wrapFn = (fn) => { +Meteor.wrapFn = function (fn) { if (!fn || typeof fn !== 'function') { throw new Meteor.Error("Expected to receive function to wrap"); - }; + } if (Meteor.isClient) { return fn; } - return function(...args) { - const ret = fn.apply(this, args); + return function() { + var ret = fn.apply(this, arguments); if (ret && typeof ret.then === 'function') { return Promise.await(ret); }