Merge pull request #14006 from meteor/feature/meteor.deferrable

FEATURE: `Meteor.deferrable`
This commit is contained in:
Nacho Codoñer
2025-11-11 17:23:42 +01:00
committed by GitHub
3 changed files with 215 additions and 33 deletions

View File

@@ -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,54 @@ 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<String>} options.on Condition to determine whether to defer the function, you can pass an array of environments ['development', 'production', 'test']
*/
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");
}
var 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"] });
};

View File

@@ -1,21 +1,77 @@
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");
onComplete();
}
);
Tinytest.addAsync(
"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(
function () {
test.equal(x, "value");
onComplete();
},
{ on: ["development", "production", "test"] }
);
Meteor.deferrable(
async function () {
return "value";
},
{ on: [] }
).then((value) => (x = value));
}
);

View File

@@ -54,6 +54,86 @@ Meteor.startup(() => {
<ApiBox name="Meteor.promisify" />
<ApiBox name="Meteor.defer" />
<ApiBox name="Meteor.deferrable" hasCustomExample />
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();
}
});
```
:::
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.
<ApiBox name="Meteor.deferDev" hasCustomExample />
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();
}
});
```
<ApiBox name="Meteor.deferProd" hasCustomExample />
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();
}
});
```
<ApiBox name="Meteor.absoluteUrl" />
<ApiBox name="Meteor.settings" />
<ApiBox name="Meteor.release" />
@@ -398,7 +478,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.
:::
<ApiBox name="Meteor.callAsync" />
`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 +488,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)
```
<ApiBox name="Meteor.apply" />
`Meteor.apply` is just like `Meteor.call`, except that the method arguments are
@@ -504,8 +582,6 @@ different collections. We hope to lift this restriction in a future release.
</ApiBox>
```js
import { Meteor } from "meteor/meteor";
import { check } from "meteor/check";