From 8f58378836af00a76440ad572d1bd44ceb507cb5 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 10 Nov 2025 10:12:18 -0300 Subject: [PATCH 1/5] FEATURE: `Meteor.deferrable` While working on updating apps to use the new modern bundle, we came across some places that used this exciting pattern to get some performance gains on dev mode: ```js if (Meteor.isDevelopment) { Meteor.defer(loadSomethingRemote); } else { await loadSomethingRemote(); } ``` Sometimes we do not need this service while in dev and this would only make our startup slower, _now_ with this function we can have this nice api to wrap it: ```js await Meteor.deferrable(connectToExternalDB, { on: ["development"], }); ``` --- packages/meteor/timers.js | 62 ++++++++++++++++-- packages/meteor/timers_tests.js | 62 +++++++++++++++--- v3-docs/docs/api/meteor.md | 111 ++++++++++++++++++++++++++------ 3 files changed, 199 insertions(+), 36 deletions(-) diff --git a/packages/meteor/timers.js b/packages/meteor/timers.js index 9b0596bfa1..4575a58dcc 100644 --- a/packages/meteor/timers.js +++ b/packages/meteor/timers.js @@ -1,13 +1,13 @@ function withoutInvocation(f) { if (Package.ddp) { - var DDP = Package.ddp.DDP; - var CurrentInvocation = + const DDP = Package.ddp.DDP; + const CurrentInvocation = DDP._CurrentMethodInvocation || // For backwards compatibility, as explained in this issue: // https://github.com/meteor/meteor/issues/8947 DDP._CurrentInvocation; - var invocation = CurrentInvocation.get(); + const invocation = CurrentInvocation.get(); if (invocation && invocation.isSimulation) { throw new Error("Can't set timers inside simulations"); } @@ -15,9 +15,8 @@ function withoutInvocation(f) { return function () { CurrentInvocation.withValue(null, f); }; - } else { - return f; } + return f; } function bindAndCatch(context, f) { @@ -56,7 +55,7 @@ Meteor.setInterval = function (f, duration) { * @locus Anywhere * @param {Object} id The handle returned by `Meteor.setInterval` */ -Meteor.clearInterval = function(x) { +Meteor.clearInterval = function (x) { return clearInterval(x); }; @@ -66,7 +65,7 @@ Meteor.clearInterval = function(x) { * @locus Anywhere * @param {Object} id The handle returned by `Meteor.setTimeout` */ -Meteor.clearTimeout = function(x) { +Meteor.clearTimeout = function (x) { return clearTimeout(x); }; @@ -84,3 +83,52 @@ Meteor.clearTimeout = function(x) { Meteor.defer = function (f) { Meteor._setImmediate(bindAndCatch("defer callback", f)); }; + +/** + * @memberOf Meteor + * @summary Defer execution of a function to run asynchronously in the background based on environment (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)). + * @locus Anywhere + * @param {Function} func The function to run + * @param {Object} options The options object + * @param {Array} options.on Condition to determine whether to defer the function, you can pass an array of environments ['development', 'production', 'test'] + */ +Meteor.deferrable = function (f, { on }) { + // throw if on is not an array + if (!Array.isArray(on)) { + throw new Error("options.on must be an array"); + } + + const env = Meteor.isDevelopment + ? "development" + : Meteor.isProduction + ? "production" + : "test"; + + if (on.includes(env)) { + return Meteor.defer(f); + } + + return f(); +}; + +/** + * @memberOf Meteor + * @summary Defer execution of a function to run asynchronously in the background in development (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)). + * @locus Anywhere + * @param {Function} func The function to run + * @param {Object} options The options object + */ +Meteor.deferDev = function (f) { + return Meteor.deferrable(f, { on: ["development", "test"] }); +}; + +/** + * @memberOf Meteor + * @summary Defer execution of a function to run asynchronously in the background in production (similar to Meteor.isProduction ? Meteor.defer(fn) : Meteor.startup(fn)). + * @locus Anywhere + * @param {Function} func The function to run + * @param {Object} options The options object + */ +Meteor.deferProd = function (f) { + return Meteor.deferrable(f, { on: ["production"] }); +}; diff --git a/packages/meteor/timers_tests.js b/packages/meteor/timers_tests.js index 246f7e7b39..6406b90b26 100644 --- a/packages/meteor/timers_tests.js +++ b/packages/meteor/timers_tests.js @@ -1,21 +1,63 @@ -Tinytest.addAsync('timers - defer', function (test, onComplete) { - var x = 'a'; +Tinytest.addAsync("timers - defer", function (test, onComplete) { + let x = "a"; Meteor.defer(function () { - test.equal(x, 'b'); + test.equal(x, "b"); onComplete(); }); - x = 'b'; + x = "b"; }); -Tinytest.addAsync('timers - nested defer', function (test, onComplete) { - var x = 'a'; +Tinytest.addAsync("timers - nested defer", function (test, onComplete) { + let x = "a"; Meteor.defer(function () { - test.equal(x, 'b'); + test.equal(x, "b"); Meteor.defer(function () { - test.equal(x, 'c'); + test.equal(x, "c"); onComplete(); }); - x = 'c'; + x = "c"; }); - x = 'b'; + x = "b"; }); + +Tinytest.addAsync("timers - deferrable", function (test, onComplete) { + let x = "a"; + Meteor.deferrable( + function () { + test.equal(x, "b"); + onComplete(); + }, + { on: ["development", "production", "test"] } + ); + x = "b"; +}); + +Tinytest.addAsync( + "timers - deferrable not in current env", + function (test, onComplete) { + let x = "a"; + Meteor.deferrable( + function () { + x = "b"; + }, + { on: [] } + ); + test.equal(x, "b"); + } +); + +Tinytest.addAsync( + "timers - defer works with async functions", + async function (test, onComplete) { + let x = "a"; + Meteor.deferrable( + async function () { + await new Promise((resolve) => setTimeout(resolve, 10)); + test.equal(x, "b"); + onComplete(); + }, + { on: ["development", "production", "test"] } + ); + await Meteor.deferrable(async () => (x = "b"), { on: [] }); + } +); diff --git a/v3-docs/docs/api/meteor.md b/v3-docs/docs/api/meteor.md index 5f3899014a..d37d518c29 100644 --- a/v3-docs/docs/api/meteor.md +++ b/v3-docs/docs/api/meteor.md @@ -54,6 +54,83 @@ Meteor.startup(() => { + + +This helper function allows you to defer the execution of a function based on the environment. + +::: code-group + +```js [with-deferrable.js] +import { Meteor } from "meteor/meteor"; + +Meteor.startup(async () => { + await Meteor.deferrable(connectToExternalDB, { + on: ["development"], + }); +}); +``` + +```js [without-deferrable.js] +import { Meteor } from "meteor/meteor"; + +Meteor.startup(async () => { + if (Meteor.isDevelopment) { + Meteor.defer(connectToExternalDB); + } else { + await connectToExternalDB(); + } +}); +``` + +::: + + +This helper function allows you to defer the execution of a function only in development environments. + +::: code-group + +```js [with-deferrable.js] +import { Meteor } from "meteor/meteor"; +Meteor.startup(async () => { + await Meteor.deferDev(connectToExternalDB); +}); +``` + +```js [without-deferrable.js] +import { Meteor } from "meteor/meteor"; +Meteor.startup(async () => { + if (Meteor.isTest || Meteor.isDevelopment) { + Meteor.defer(connectToExternalDB); + } else { + await connectToExternalDB(); + } +}); +``` + + + +This helper function allows you to defer the execution of a function only in production environments. +::: code-group + +```js [with-deferrable.js] +import { Meteor } from "meteor/meteor"; +Meteor.startup(async () => { + await Meteor.deferProd(loadDevTools); +}); +``` + +```js [without-deferrable.js] +import { Meteor } from "meteor/meteor"; + +Meteor.startup(async () => { + if (Meteor.isProduction) { + Meteor.defer(loadDevTools); + } else { + await loadDevTools(); + } +}); +``` + @@ -398,7 +475,6 @@ even if the method's writes are not available yet, you can specify an Use `Meteor.call` only to call methods that do not have a stub, or have a sync stub. If you want to call methods with an async stub, `Meteor.callAsync` can be used with any method. ::: - `Meteor.callAsync` is just like `Meteor.call`, except that it'll return a promise that you need to solve to get the server result. Along with the promise returned by `callAsync`, you can also handle `stubPromise` and `serverPromise` for managing client-side simulation and server response. @@ -409,64 +485,63 @@ The following sections guide you in understanding these promises and how to mana ```javascript try { - await Meteor.callAsync('greetUser', 'John'); - // ๐ŸŸข Server ended with success -} catch(e) { - console.error("Error:", error.reason); // ๐Ÿ”ด Server ended with error + await Meteor.callAsync("greetUser", "John"); + // ๐ŸŸข Server ended with success +} catch (e) { + console.error("Error:", error.reason); // ๐Ÿ”ด Server ended with error } -Greetings.findOne({ name: 'John' }); // ๐Ÿ—‘๏ธ Data is NOT available +Greetings.findOne({ name: "John" }); // ๐Ÿ—‘๏ธ Data is NOT available ``` #### stubPromise ```javascript -await Meteor.callAsync('greetUser', 'John').stubPromise; +await Meteor.callAsync("greetUser", "John").stubPromise; // ๐Ÿ”ต Client simulation -Greetings.findOne({ name: 'John' }); // ๐Ÿงพ Data is available (Optimistic-UI) +Greetings.findOne({ name: "John" }); // ๐Ÿงพ Data is available (Optimistic-UI) ``` #### stubPromise and serverPromise ```javascript -const { stubPromise, serverPromise } = Meteor.callAsync('greetUser', 'John'); +const { stubPromise, serverPromise } = Meteor.callAsync("greetUser", "John"); await stubPromise; // ๐Ÿ”ต Client simulation -Greetings.findOne({ name: 'John' }); // ๐Ÿงพ Data is available (Optimistic-UI) +Greetings.findOne({ name: "John" }); // ๐Ÿงพ Data is available (Optimistic-UI) try { await serverPromise; // ๐ŸŸข Server ended with success -} catch(e) { +} catch (e) { console.error("Error:", error.reason); // ๐Ÿ”ด Server ended with error } -Greetings.findOne({ name: 'John' }); // ๐Ÿ—‘๏ธ Data is NOT available +Greetings.findOne({ name: "John" }); // ๐Ÿ—‘๏ธ Data is NOT available ``` #### Meteor 2.x contrast For those familiar with legacy Meteor 2.x, the handling of client simulation and server response was managed using fibers, as explained in the following section. This comparison illustrates how async inclusion with standard promises has transformed the way Meteor operates in modern versions. -``` javascript -Meteor.call('greetUser', 'John', function(error, result) { +```javascript +Meteor.call("greetUser", "John", function (error, result) { if (error) { console.error("Error:", error.reason); // ๐Ÿ”ด Server ended with error } else { console.log("Result:", result); // ๐ŸŸข Server ended with success } - Greetings.findOne({ name: 'John' }); // ๐Ÿ—‘๏ธ Data is NOT available + Greetings.findOne({ name: "John" }); // ๐Ÿ—‘๏ธ Data is NOT available }); // ๐Ÿ”ต Client simulation -Greetings.findOne({ name: 'John' }); // ๐Ÿงพ Data is available (Optimistic-UI) +Greetings.findOne({ name: "John" }); // ๐Ÿงพ Data is available (Optimistic-UI) ``` - `Meteor.apply` is just like `Meteor.call`, except that the method arguments are @@ -504,8 +579,6 @@ different collections. We hope to lift this restriction in a future release. - - ```js import { Meteor } from "meteor/meteor"; import { check } from "meteor/check"; From 618744af7b544e71bd6e163478f277120a80754f Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 10 Nov 2025 10:16:30 -0300 Subject: [PATCH 2/5] DEV: rollback to legacy sintax --- packages/meteor/timers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/meteor/timers.js b/packages/meteor/timers.js index 4575a58dcc..6070e91bc7 100644 --- a/packages/meteor/timers.js +++ b/packages/meteor/timers.js @@ -1,13 +1,13 @@ function withoutInvocation(f) { if (Package.ddp) { - const DDP = Package.ddp.DDP; - const CurrentInvocation = + var DDP = Package.ddp.DDP; + var CurrentInvocation = DDP._CurrentMethodInvocation || // For backwards compatibility, as explained in this issue: // https://github.com/meteor/meteor/issues/8947 DDP._CurrentInvocation; - const invocation = CurrentInvocation.get(); + var invocation = CurrentInvocation.get(); if (invocation && invocation.isSimulation) { throw new Error("Can't set timers inside simulations"); } @@ -98,7 +98,7 @@ Meteor.deferrable = function (f, { on }) { throw new Error("options.on must be an array"); } - const env = Meteor.isDevelopment + var env = Meteor.isDevelopment ? "development" : Meteor.isProduction ? "production" From 2c74b2f2d0d657fa915afc27ef25d871b395b578 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 10 Nov 2025 10:25:44 -0300 Subject: [PATCH 3/5] DOCS: add detail on why this function is nice to have --- v3-docs/docs/api/meteor.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/v3-docs/docs/api/meteor.md b/v3-docs/docs/api/meteor.md index d37d518c29..5291376348 100644 --- a/v3-docs/docs/api/meteor.md +++ b/v3-docs/docs/api/meteor.md @@ -84,6 +84,9 @@ Meteor.startup(async () => { ::: +Using this pattern can get some performance gains on the defined environments as sometimes we do not need to wait for this function, +this can increase the speed of startup. + This helper function allows you to defer the execution of a function only in development environments. From 4d4f6661af708cd64eeb06927b7bfaf0e894ce1a Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 10 Nov 2025 10:26:59 -0300 Subject: [PATCH 4/5] DEV: add local variable `on` --- packages/meteor/timers.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/meteor/timers.js b/packages/meteor/timers.js index 6070e91bc7..a6c5d25c0d 100644 --- a/packages/meteor/timers.js +++ b/packages/meteor/timers.js @@ -92,7 +92,9 @@ Meteor.defer = function (f) { * @param {Object} options The options object * @param {Array} options.on Condition to determine whether to defer the function, you can pass an array of environments ['development', 'production', 'test'] */ -Meteor.deferrable = function (f, { on }) { +Meteor.deferrable = function (f, options) { + var on = (options && options.on) || []; + // throw if on is not an array if (!Array.isArray(on)) { throw new Error("options.on must be an array"); From 64cc7937f97d7cc489a27b91e2ff2e36065f07ed Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Tue, 11 Nov 2025 10:58:18 -0300 Subject: [PATCH 5/5] DEV: make tests good --- packages/meteor/timers_tests.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/meteor/timers_tests.js b/packages/meteor/timers_tests.js index 6406b90b26..1db203dd00 100644 --- a/packages/meteor/timers_tests.js +++ b/packages/meteor/timers_tests.js @@ -43,21 +43,35 @@ Tinytest.addAsync( { on: [] } ); test.equal(x, "b"); + onComplete(); } ); Tinytest.addAsync( - "timers - defer works with async functions", - async function (test, onComplete) { - let x = "a"; + "timers - deferrable works with async functions", + function (test, onComplete) { + let x = Meteor.deferrable( + function () { + return "start value"; + }, + { on: [] } + ); + test.equal(x, "start value"); + Meteor.deferrable( - async function () { - await new Promise((resolve) => setTimeout(resolve, 10)); - test.equal(x, "b"); + function () { + test.equal(x, "value"); onComplete(); }, { on: ["development", "production", "test"] } ); - await Meteor.deferrable(async () => (x = "b"), { on: [] }); + + Meteor.deferrable( + async function () { + return "value"; + }, + { on: [] } + ).then((value) => (x = value)); + } );