Deferring a Notification in ServiceWorker when browser/pwa is Minimized - javascript

self.setTimeout reliably fires self.registration.showNotification after the expected delay, only under the condition the browser is NOT minimized / hidden.
It seems to work up to 20 seconds later. After that, it silently fails.
I have not yet determined if it is the self.setTimeout which fails to run the callback, or if it is the self.registration.showNotification fails to show the notification.
Code:
importScripts('./ngsw-worker.js');
let pendingNotifications = new Map();
function wait(ms, pendingNotification) {
return new Promise(resolve => {
pendingNotification.TimerId = setTimeout(resolve, ms);
});
}
async function processNotification(data, pendingNotification) {
let delay = await wait(data.Data.DeferredSeconds * 1000, pendingNotification);
//let notCancelled = pendingNotifications.has(data.Data.CancellationToken);
pendingNotifications.delete(data.Data.CancellationToken);
//if (notCancelled) {
self.registration.showNotification(data.Data.Title, { icon: data.Data.Icon, vibrate: data.Data.VibrationPattern, body: data.Data.Body });
//}
return null;
}
self.addEventListener('message', function (messageEvent) {
let data = messageEvent.data;
if (data == null) {
return;
}
if (data.Type == "Notification") {
let pendingNotification = {
TimerId: -1,
CancellationToken: data.Data.CancellationToken
};
pendingNotifications.set(data.Data.CancellationToken, pendingNotification);
messageEvent.waitUntil(processNotification(data, pendingNotification));
}
if (data.Type == "NotificationCancel") {
let pendingNotification = pendingNotifications.get(data.Data.CancellationToken);
if (pendingNotification == null) {
return;
}
self.clearTimeout(pendingNotification.TimerId);
pendingNotifications.delete(pendingNotification.CancellationToken);
}
})

A service worker is aggressively killed by the browser when it "appears" to be idle.
If you have asynchronous work that you want to perform while keeping the service worker alive, you need to perform that work inside of a Promise which you then pass to event.waitUntil(). This does not keep the service worker alive forever, but it does tell the browser to extend the service worker's lifetime until either the Promise resolves or a browser-specific time limit (usually a few minutes) has elapsed.
Any service worker event that inherits from ExtendableEvent, including MessageEvent, exposes a waitUntil() method.
You'll need to rewrite your code a bit to translate the setTimeout() calls into something that's Promise-friendly, though.

Related

How do I prevent a function from being called from within a specific promise?

I'm working on a library and I'd like to prevent users from calling a specific function in order to prevent infinite loops.
Usually I'd go about doing it like this:
let preventFooCalls = false;
function fireUserCallbacks() {
preventFooCalls = true;
// Fire callbacks of the user here...
preventFooCalls = false;
}
function foo() {
if (preventFooCalls) throw Error();
// Run the content of foo() ...
// It will probably call fireUserCallbacks() at some point
}
However, if fireUserCallbacks is async, this method is not possible. It might be called multiple times, and with async user callbacks, preventFooCalls is not guaranteed to have the correct value. For instance:
let preventFooCalls = false;
async function fireUserCallbacks() {
preventFooCalls = true;
// Fire callbacks of the user here one of which being:
await new Promise(r => setTimeout(r, 1000));
preventFooCalls = false;
}
// Then when doing:
fireUserCallbacks();
foo(); // This will throw even though it's being called from outside fireUserCallbacks()
How can I detect if code is running from within a specific promise?
The only thing I can think of is new Error().stack, but oof that sounds like a terrible way to do it.
Some context
The reason why I want this is because I'm working on a part of a library that takes care of loading assets. Some of these assets might contain other assets with the possibility of infinite recursion. In order to handle recursion I have another function that I want users to call instead. Therefore I want to warn users when they call foo() from within one of the fireUserCallbacks() callbacks. While this will only be an issue when assets actually contain infinite loops, I'd rather block the usage of foo() completely to prevent unexpected hangs due to infinite loops.
edit: Here's a somewhat more sophisticated example of my actual code. I would share my actual code but that is really way too long for this format, tbh this example is already getting a bit too complex.
class AssetManager {
constructor() {
this.registeredAssetTypes = new Map();
this.availableAssets = new Map();
}
registerAssetType(typeId, assetTypeConstructor) {
this.registeredAssetTypes.set(typeId, assetTypeConstructor);
}
fillAvailableAssets(assetDatas) {
for (const assetData of assetDatas) {
const constructor = this.registeredAssetTypes.get(assetData.type);
const asset = new constructor(assetData.id, assetData.data);
this.availableAssets.set(assetData.id, asset);
}
}
async loadAsset(assetId, recursionTracker = null) {
// I have some extra code here that makes sure this function will only have
// a single running instance, but this example is getting way too long already
const asset = this.availableAssets.get(assetId);
let isRootRecursionTracker = false;
if (!recursionTracker) {
isRootRecursionTracker = true;
recursionTracker = new RecursionTracker(assetId);
}
const assetData = await asset.generateAsset(recursionTracker);
if (isRootRecursionTracker) {
// If this call was made from outside any `generateAsset` implementation,
// we will wait for all assets to be loaded and put on the right place.
await recursionTracker.waitForAll();
// Finally we will give the recursionTracker the created root asset,
// in case any of the sub assets reference the root asset.
// Note that circular references in any of the sub assets (i.e. not
// containing the root asset anywhere in the chain) are already taken care of.
if (recursionTracker.rootLoadingAsset) {
recursionTracker.rootLoadingAsset.setLoadedAssetData(assetData);
}
}
return assetData;
}
}
const assetManager = new AssetManager();
class RecursionTracker {
constructor(rootAssetId) {
this.rootAssetId = rootAssetId;
this.rootLoadingAsset = null;
this.loadingAssets = new Map();
}
loadAsset(assetId, cb){
let loadingAsset = this.loadingAssets.get(assetId);
if (!loadingAsset) {
loadingAsset = new LoadingAsset(assetId);
this.loadingAssets.set(assetId, loadingAsset);
if (assetId != this.rootAssetId) {
loadingAsset.startLoading(this);
} else {
this.rootLoadingAsset = loadingAsset;
}
}
loadingAsset.onLoad(cb);
}
async waitForAll() {
const promises = [];
for (const loadingAsset of this.loadingAssets.values()) {
promises.push(loadingAsset.waitForLoad());
}
await Promise.all(promises);
}
}
class LoadingAsset {
constructor(assetId) {
this.assetId = assetId;
this.onLoadCbs = new Set();
this.loadedAssetData = null;
}
async startLoading(recursionTracker) {
const loadedAssetData = await assetManager.loadAsset(this.assetId, recursionTracker);
this.setLoadedAssetData(loadedAssetData);
}
onLoad(cb) {
if (this.loadedAssetData) {
cb(this.loadedAssetData)
} else {
this.onLoadCbs.add(cb);
}
}
setLoadedAssetData(assetData) {
this.loadedAssetData = assetData;
this.onLoadCbs.forEach(cb => cb(assetData));
}
async waitForLoad() {
await new Promise(r => this.onLoad(r));
}
}
class AssetTypeInterface {
constructor(id, rawAssetData) {
this.id = id;
this.rawAssetData = rawAssetData;
}
async generateAsset(recursionTracker) {}
}
class AssetTypeFoo extends AssetTypeInterface {
async generateAsset(recursionTracker) {
// This is here just to simulate network traffic, an indexeddb lookup, or any other async operation:
await new Promise(r => setTimeout(r, 200));
const subAssets = [];
for (const subAssetId of this.rawAssetData.subAssets) {
// This won't work, as it will start waiting for itself to finish:
// const subAsset = await assetManager.loadAsset(subAssetId);
// subAssets.push(subAsset);
// So instead we will create a dummy asset:
const dummyAsset = {}
const insertionIndex = subAssets.length;
subAssets[insertionIndex] = dummyAsset;
// and load the asset with a callback rather than waiting for a promise
recursionTracker.loadAsset(subAssetId, (loadedAsset) => {
// since this will be called outside the `generateAsset` function, this won't hang
subAssets[insertionIndex] = loadedAsset;
});
}
return {
foo: this.id,
subAssets,
}
}
}
assetManager.registerAssetType("foo", AssetTypeFoo);
class AssetTypeBar extends AssetTypeInterface {
async generateAsset(recursionTracker) {
// This is here just to simulate network traffic, an indexeddb lookup, or any other async operation:
await new Promise(r => setTimeout(r, 200));
// We'll just return a simple object for this one.
// No recursion here...
return {
bar: this.id,
};
}
}
assetManager.registerAssetType("bar", AssetTypeBar);
// This is all the raw asset data as stored on the users disk.
// These are not instances of the assets yet, so no circular references yet.
// The assets only reference other assets by their "id"
assetManager.fillAvailableAssets([
{
id: "mainAsset",
type: "foo",
data: {
subAssets: ["subAsset1", "subAsset2"]
}
},
{
id: "subAsset1",
type: "bar",
data: {},
},
{
id: "subAsset2",
type: "foo",
data: {
subAssets: ["mainAsset"]
}
}
]);
// This sets the loading of the "mainAsset" in motion. It recursively loads
// all referenced assets and finally puts the loaded assets in the right place,
// completing the circle.
(async () => {
const asset = await assetManager.loadAsset("mainAsset");
console.log(asset);
})();
Maintain a queue and a set. The queue contains pending requests. The set contains pending requests, requests in progress, and successfully completed requests. (Each item would include the request itself; the request's status: pending, processing, complete; and possibly a retry counter.)
When request is made, check if it is in the set. If it is in the set, it was already requested and will be processed, is being processed, or was processed successfully and is already available. If not in the set, add it to both the set and the queue, then trigger queue processing. If queue processing is already running, the trigger is ignored. If not, queue processing starts.
Queue processing pulls requests off the queue, one by one, and processes them. If a request fails, it can either be put back onto the queue for repeat attempts (a counter can be included in the item to limit retries) or it can be removed from the set so it can be requested again later. Queue processing ends when the queue is empty.
This avoids recursion and unnecessary repeat requests.

Chrome crash after 1 hour of running due to out of memory. Javascript memory management issue probably

I'm doing a polling method to my API every 5 seconds, to get real-time data. The code below works but after 1 hour of running, the page crash(Aww Snap, with the dinosaur image and error: out of memory). The data I'm collecting is quite large, and I'm expecting that javascript will offload the memory(garbage collection) every time the function is being called again. I can see in the Chrome Task Manager, the memory footprint is growing over time. Is there a way to clear the memory or offload the memory from growing over time?
data(){
return{
newdata:[],
};
},
methods: {
loadData:async function () {
try {
let response = await axios.get('/monitoring_data');
if (response.status != 200) {
await new Promise(resolve => setTimeout(resolve, 1000));
await this.loadData();
}else {
// Get the data
this.newdata= response.data.Ppahvc;
// Call loadData() again to get the next data
await new Promise(resolve => setTimeout(resolve, 5000));
await this.loadData();
}
} catch (e) {
await this.loadData();
}
},
},
mounted:function(){
this.loadData();
},
You can use setinterval instead of calling the loadData function recursivley
You are calling the loadData function recursivley (function calls itself) which is likely the cause of your memory issue because the browser has to remember these nested calls to eventually return again.
Try using an iterative approach instead:
setInterval(function {
let response = await axios.get('/monitoring_data');
this.newdata = response.data.Ppahvc;
}, 5000);

Force stop function execution [duplicate]

How to implement a timeout in Javascript, not the window.timeout but something like session timeout or socket timeout - basically - a "function timeout"
A specified period of time that will be allowed to elapse in a system
before a specified event is to take place, unless another specified
event occurs first; in either case, the period is terminated when
either event takes place.
Specifically, I want a javascript observing timer that will observe the execution time of a function and if reached or going more than a specified time then the observing timer will stop/notify the executing function.
Any help is greatly appreciated! Thanks a lot.
I'm not entirely clear what you're asking, but I think that Javascript does not work the way you want so it cannot be done. For example, it cannot be done that a regular function call lasts either until the operation completes or a certain amount of time whichever comes first. That can be implemented outside of javascript and exposed through javascript (as is done with synchronous ajax calls), but can't be done in pure javascript with regular functions.
Unlike other languages, Javascript is single threaded so that while a function is executing a timer will never execute (except for web workers, but they are very, very limited in what they can do). The timer can only execute when the function finishes executing. Thus, you can't even share a progress variable between a synchronous function and a timer so there's no way for a timer to "check on" the progress of a function.
If your code was completely stand-alone (didn't access any of your global variables, didn't call your other functions and didn't access the DOM in anyway), then you could run it in a web-worker (available in newer browsers only) and use a timer in the main thread. When the web-worker code completes, it sends a message to the main thread with it's results. When the main thread receives that message, it stops the timer. If the timer fires before receiving the results, it can kill the web-worker. But, your code would have to live with the restrictions of web-workers.
Soemthing can also be done with asynchronous operations (because they work better with Javascript's single-threaded-ness) like this:
Start an asynchronous operation like an ajax call or the loading of an image.
Start a timer using setTimeout() for your timeout time.
If the timer fires before your asynchronous operation completes, then stop the asynchronous operation (using the APIs to cancel it).
If the asynchronous operation completes before the timer fires, then cancel the timer with clearTimeout() and proceed.
For example, here's how to put a timeout on the loading of an image:
function loadImage(url, maxTime, data, fnSuccess, fnFail) {
var img = new Image();
var timer = setTimeout(function() {
timer = null;
fnFail(data, url);
}, maxTime);
img.onLoad = function() {
if (timer) {
clearTimeout(timer);
fnSuccess(data, img);
}
}
img.onAbort = img.onError = function() {
clearTimeout(timer);
fnFail(data, url);
}
img.src = url;
}
My question has been marked as a duplicate of this one so I thought I'd answer it even though the original post is already nine years old.
It took me a while to wrap my head around what it means for Javascript to be single-threaded (and I'm still not sure I understood things 100%) but here's how I solved a similar use-case using Promises and a callback. It's mostly based on this tutorial.
First, we define a timeout function to wrap around Promises:
const timeout = (prom, time, exception) => {
let timer;
return Promise.race([
prom,
new Promise((_r, rej) => timer = setTimeout(rej, time, exception))
]).finally(() => clearTimeout(timer));
}
This is the promise I want to timeout:
const someLongRunningFunction = async () => {
...
return ...;
}
Finally, I use it like this.
const TIMEOUT = 2000;
const timeoutError = Symbol();
var value = "some default value";
try {
value = await timeout(someLongRunningFunction(), TIMEOUT, timeoutError);
}
catch(e) {
if (e === timeoutError) {
console.log("Timeout");
}
else {
console.log("Error: " + e);
}
}
finally {
return callback(value);
}
This will call the callback function with the return value of someLongRunningFunction or a default value in case of a timeout. You can modify it to handle timeouts differently (e.g. throw an error).
You could execute the code in a web worker. Then you are still able to handle timeout events while the code is running. As soon as the web worker finishes its job you can cancel the timeout. And as soon as the timeout happens you can terminate the web worker.
execWithTimeout(function() {
if (Math.random() < 0.5) {
for(;;) {}
} else {
return 12;
}
}, 3000, function(err, result) {
if (err) {
console.log('Error: ' + err.message);
} else {
console.log('Result: ' + result);
}
});
function execWithTimeout(code, timeout, callback) {
var worker = new Worker('data:text/javascript;base64,' + btoa('self.postMessage((' + String(code) + '\n)());'));
var id = setTimeout(function() {
worker.terminate();
callback(new Error('Timeout'));
}, timeout);
worker.addEventListener('error', function(e) {
clearTimeout(id);
callback(e);
});
worker.addEventListener('message', function(e) {
clearTimeout(id);
callback(null, e.data);
});
}
I realize this is an old question/thread but perhaps this will be helpful to others.
Here's a generic callWithTimeout that you can await:
export function callWithTimeout(func, timeout) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("timeout")), timeout)
func().then(
response => resolve(response),
err => reject(new Error(err))
).finally(() => clearTimeout(timer))
})
}
Tests/examples:
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
const func1 = async () => {
// test: func completes in time
await sleep(100)
}
const func2 = async () => {
// test: func does not complete in time
await sleep(300)
}
const func3 = async () => {
// test: func throws exception before timeout
await sleep(100)
throw new Error("exception in func")
}
const func4 = async () => {
// test: func would have thrown exception but timeout occurred first
await sleep(300)
throw new Error("exception in func")
}
Call with:
try {
await callWithTimeout(func, 200)
console.log("finished in time")
}
catch (err) {
console.log(err.message) // can be "timeout" or exception thrown by `func`
}
You can achieve this only using some hardcore tricks. Like for example if you know what kind of variable your function returns (note that EVERY js function returns something, default is undefined) you can try something like this: define variable
var x = null;
and run test in seperate "thread":
function test(){
if (x || x == undefined)
console.log("Cool, my function finished the job!");
else
console.log("Ehh, still far from finishing!");
}
setTimeout(test, 10000);
and finally run function:
x = myFunction(myArguments);
This only works if you know that your function either does not return any value (i.e. the returned value is undefined) or the value it returns is always "not false", i.e. is not converted to false statement (like 0, null, etc).
Here is my answer which essentially simplifies Martin's answer and is based upon the same tutorial.
Timeout wrapper for a promise:
const timeout = (prom, time) => {
const timeoutError = new Error(`execution time has exceeded the allowed time frame of ${time} ms`);
let timer; // will receive the setTimeout defined from time
timeoutError.name = "TimeoutErr";
return Promise.race([
prom,
new Promise((_r, rej) => timer = setTimeout(rej, time, timeoutError)) // returns the defined timeoutError in case of rejection
]).catch(err => { // handle errors that may occur during the promise race
throw(err);
}) .finally(() => clearTimeout(timer)); // clears timer
}
A promise for testing purposes:
const fn = async (a) => { // resolves in 500 ms or throw an error if a == true
if (a == true) throw new Error('test error');
await new Promise((res) => setTimeout(res, 500));
return "p2";
}
Now here is a test function:
async function test() {
let result;
try { // finishes before the timeout
result = await timeout(fn(), 1000); // timeouts in 1000 ms
console.log('• Returned Value :', result, '\n'); // result = p2
} catch(err) {
console.log('• Captured exception 0 : \n ', err, '\n');
}
try { // don't finish before the timeout
result = await timeout(fn(), 100); // timeouts in 100 ms
console.log(result); // not executed as the timeout error was triggered
} catch (err) {
console.log('• Captured exception 1 : \n ', err, '\n');
}
try { // an error occured during fn execution time
result = await timeout(fn(true), 100); // fn will throw an error
console.log(result); // not executed as an error occured
} catch (err) {
console.log('• Captured exception 2 : \n ', err, '\n');
}
}
that will produce this output:
• Returned Value : p2
• Captured exception 1 :
TimeoutErr: execution time has exceeded the allowed time frame of 100 ms
at C:\...\test-promise-race\test.js:33:34
at async test (C:\...\test-promise-race\test.js:63:18)
• Captured exception 2 :
Error: test error
at fn (C:\...\test-promise-race\test.js:45:26)
at test (C:\...\test-promise-race\test.js:72:32)
If you don't want to use try ... catch instructions in the test function you can alternatively replace the throw instructions in the catch part of the timeout promise wrapper by return.
By doing so the result variable will receive the error that is throwed otherwise. You can then use this to detect if the result variable actually contains an error.
if (result instanceof Error) {
// there was an error during execution
}
else {
// result contains the value returned by fn
}
If you want to check if the error is relative to the defined timeout you will have to check the error.name value for "TimeoutErr".
Share a variable between the observing timer and the executing function.
Implement the observing timer with window.setTimeout or window.setInterval. When the observing timer executes, it sets an exit value to the shared variable.
The executing function constantly checks for the variable value.. and returns if the exit value is specified.

Stop running task after request timeout in Express.js

Let's assume that we have below code, which has timeout set for 5 seconds.
router.get('/timeout', async (req, res, next) => {
req.setTimeout(5000, () => {
res.status(503)
res.send()
})
while (true) {
console.log("I'm alive")
}
res.status(200)
res.send({msg: 'success'})
})
I know that the last two lines will never be reached, but that's not a point. The problem which I want to solve is that the while loop is still working despite response was sent.
Is there some way to kill such still working tasks?
There are two types of long running tasks and cancelling is different for both:
1) Asynchronous tasks:
They may take a while, however they are not using the JavaScript engine, instead the engine is in idle to wait for some external data (database / files / timers whatever). In some cases (timers for example) you can easily discard that external action, also you can trigger it as an event as the engine is not blocked and can handle the cancellation. If the async action cannot be cancelled directly (database read for example) you can wait until it is done and cancel it then:
class Cancelable {
constructor() {
this.cancelled = false;
this.handlers = [];
}
onCancel(handler) { this.handlers.push(handler); }
cancel() {
this.cancelled = true;
this.handlers.forEach(handler => handler());
}
}
// inside of the request handler:
const canceller = new Cancelable;
req.setTimeout(5000, () => {
res.status(503);
res.send();
canceller.cancel(); // propagate cancellation
});
// Some long running, async cancellable task
const timer = setTimeout(function() {
res.send("done");
}, 10000 * Math.random())
// on cancellation just remove the timer
canceller.onCancel(() => clearTimeout(timer));
unCancellableAction(function callback() {
if(canceller.canceled) return; // exit early if it was cancelled
res.send("done");
});
2) Synchronous tasks:
You cannot cancel synchronous tasks directly as the engine is busy doing the task, and can't handle the cancellation. To make them cancellable you have to use polling, the task has to pause its job, check wether it should cancel, and then either continue or abort. In JS that can be done with generator functions (as they can yield their execution):
function runMax(time, action) {
const gen = action(), start = Date.now();
let done, value;
do {
({ done, value } = gen.next());
} while(!done && Date.now() < start + time)
return value;
}
// inside the request handler:
runMax(5000, function* () {
while(true) {
// ... some jobs
// yield at a safe position to allow abortion:
yield;
}
});
I think you need to add inside the while loop the new if statement to break the loop.
eg.:
while (!req.timeout) {
if (!req.timeout) {
break;
}
}

How to implement a "function timeout" in Javascript - not just the 'setTimeout'

How to implement a timeout in Javascript, not the window.timeout but something like session timeout or socket timeout - basically - a "function timeout"
A specified period of time that will be allowed to elapse in a system
before a specified event is to take place, unless another specified
event occurs first; in either case, the period is terminated when
either event takes place.
Specifically, I want a javascript observing timer that will observe the execution time of a function and if reached or going more than a specified time then the observing timer will stop/notify the executing function.
Any help is greatly appreciated! Thanks a lot.
I'm not entirely clear what you're asking, but I think that Javascript does not work the way you want so it cannot be done. For example, it cannot be done that a regular function call lasts either until the operation completes or a certain amount of time whichever comes first. That can be implemented outside of javascript and exposed through javascript (as is done with synchronous ajax calls), but can't be done in pure javascript with regular functions.
Unlike other languages, Javascript is single threaded so that while a function is executing a timer will never execute (except for web workers, but they are very, very limited in what they can do). The timer can only execute when the function finishes executing. Thus, you can't even share a progress variable between a synchronous function and a timer so there's no way for a timer to "check on" the progress of a function.
If your code was completely stand-alone (didn't access any of your global variables, didn't call your other functions and didn't access the DOM in anyway), then you could run it in a web-worker (available in newer browsers only) and use a timer in the main thread. When the web-worker code completes, it sends a message to the main thread with it's results. When the main thread receives that message, it stops the timer. If the timer fires before receiving the results, it can kill the web-worker. But, your code would have to live with the restrictions of web-workers.
Soemthing can also be done with asynchronous operations (because they work better with Javascript's single-threaded-ness) like this:
Start an asynchronous operation like an ajax call or the loading of an image.
Start a timer using setTimeout() for your timeout time.
If the timer fires before your asynchronous operation completes, then stop the asynchronous operation (using the APIs to cancel it).
If the asynchronous operation completes before the timer fires, then cancel the timer with clearTimeout() and proceed.
For example, here's how to put a timeout on the loading of an image:
function loadImage(url, maxTime, data, fnSuccess, fnFail) {
var img = new Image();
var timer = setTimeout(function() {
timer = null;
fnFail(data, url);
}, maxTime);
img.onLoad = function() {
if (timer) {
clearTimeout(timer);
fnSuccess(data, img);
}
}
img.onAbort = img.onError = function() {
clearTimeout(timer);
fnFail(data, url);
}
img.src = url;
}
My question has been marked as a duplicate of this one so I thought I'd answer it even though the original post is already nine years old.
It took me a while to wrap my head around what it means for Javascript to be single-threaded (and I'm still not sure I understood things 100%) but here's how I solved a similar use-case using Promises and a callback. It's mostly based on this tutorial.
First, we define a timeout function to wrap around Promises:
const timeout = (prom, time, exception) => {
let timer;
return Promise.race([
prom,
new Promise((_r, rej) => timer = setTimeout(rej, time, exception))
]).finally(() => clearTimeout(timer));
}
This is the promise I want to timeout:
const someLongRunningFunction = async () => {
...
return ...;
}
Finally, I use it like this.
const TIMEOUT = 2000;
const timeoutError = Symbol();
var value = "some default value";
try {
value = await timeout(someLongRunningFunction(), TIMEOUT, timeoutError);
}
catch(e) {
if (e === timeoutError) {
console.log("Timeout");
}
else {
console.log("Error: " + e);
}
}
finally {
return callback(value);
}
This will call the callback function with the return value of someLongRunningFunction or a default value in case of a timeout. You can modify it to handle timeouts differently (e.g. throw an error).
You could execute the code in a web worker. Then you are still able to handle timeout events while the code is running. As soon as the web worker finishes its job you can cancel the timeout. And as soon as the timeout happens you can terminate the web worker.
execWithTimeout(function() {
if (Math.random() < 0.5) {
for(;;) {}
} else {
return 12;
}
}, 3000, function(err, result) {
if (err) {
console.log('Error: ' + err.message);
} else {
console.log('Result: ' + result);
}
});
function execWithTimeout(code, timeout, callback) {
var worker = new Worker('data:text/javascript;base64,' + btoa('self.postMessage((' + String(code) + '\n)());'));
var id = setTimeout(function() {
worker.terminate();
callback(new Error('Timeout'));
}, timeout);
worker.addEventListener('error', function(e) {
clearTimeout(id);
callback(e);
});
worker.addEventListener('message', function(e) {
clearTimeout(id);
callback(null, e.data);
});
}
I realize this is an old question/thread but perhaps this will be helpful to others.
Here's a generic callWithTimeout that you can await:
export function callWithTimeout(func, timeout) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("timeout")), timeout)
func().then(
response => resolve(response),
err => reject(new Error(err))
).finally(() => clearTimeout(timer))
})
}
Tests/examples:
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
const func1 = async () => {
// test: func completes in time
await sleep(100)
}
const func2 = async () => {
// test: func does not complete in time
await sleep(300)
}
const func3 = async () => {
// test: func throws exception before timeout
await sleep(100)
throw new Error("exception in func")
}
const func4 = async () => {
// test: func would have thrown exception but timeout occurred first
await sleep(300)
throw new Error("exception in func")
}
Call with:
try {
await callWithTimeout(func, 200)
console.log("finished in time")
}
catch (err) {
console.log(err.message) // can be "timeout" or exception thrown by `func`
}
You can achieve this only using some hardcore tricks. Like for example if you know what kind of variable your function returns (note that EVERY js function returns something, default is undefined) you can try something like this: define variable
var x = null;
and run test in seperate "thread":
function test(){
if (x || x == undefined)
console.log("Cool, my function finished the job!");
else
console.log("Ehh, still far from finishing!");
}
setTimeout(test, 10000);
and finally run function:
x = myFunction(myArguments);
This only works if you know that your function either does not return any value (i.e. the returned value is undefined) or the value it returns is always "not false", i.e. is not converted to false statement (like 0, null, etc).
Here is my answer which essentially simplifies Martin's answer and is based upon the same tutorial.
Timeout wrapper for a promise:
const timeout = (prom, time) => {
const timeoutError = new Error(`execution time has exceeded the allowed time frame of ${time} ms`);
let timer; // will receive the setTimeout defined from time
timeoutError.name = "TimeoutErr";
return Promise.race([
prom,
new Promise((_r, rej) => timer = setTimeout(rej, time, timeoutError)) // returns the defined timeoutError in case of rejection
]).catch(err => { // handle errors that may occur during the promise race
throw(err);
}) .finally(() => clearTimeout(timer)); // clears timer
}
A promise for testing purposes:
const fn = async (a) => { // resolves in 500 ms or throw an error if a == true
if (a == true) throw new Error('test error');
await new Promise((res) => setTimeout(res, 500));
return "p2";
}
Now here is a test function:
async function test() {
let result;
try { // finishes before the timeout
result = await timeout(fn(), 1000); // timeouts in 1000 ms
console.log('• Returned Value :', result, '\n'); // result = p2
} catch(err) {
console.log('• Captured exception 0 : \n ', err, '\n');
}
try { // don't finish before the timeout
result = await timeout(fn(), 100); // timeouts in 100 ms
console.log(result); // not executed as the timeout error was triggered
} catch (err) {
console.log('• Captured exception 1 : \n ', err, '\n');
}
try { // an error occured during fn execution time
result = await timeout(fn(true), 100); // fn will throw an error
console.log(result); // not executed as an error occured
} catch (err) {
console.log('• Captured exception 2 : \n ', err, '\n');
}
}
that will produce this output:
• Returned Value : p2
• Captured exception 1 :
TimeoutErr: execution time has exceeded the allowed time frame of 100 ms
at C:\...\test-promise-race\test.js:33:34
at async test (C:\...\test-promise-race\test.js:63:18)
• Captured exception 2 :
Error: test error
at fn (C:\...\test-promise-race\test.js:45:26)
at test (C:\...\test-promise-race\test.js:72:32)
If you don't want to use try ... catch instructions in the test function you can alternatively replace the throw instructions in the catch part of the timeout promise wrapper by return.
By doing so the result variable will receive the error that is throwed otherwise. You can then use this to detect if the result variable actually contains an error.
if (result instanceof Error) {
// there was an error during execution
}
else {
// result contains the value returned by fn
}
If you want to check if the error is relative to the defined timeout you will have to check the error.name value for "TimeoutErr".
Share a variable between the observing timer and the executing function.
Implement the observing timer with window.setTimeout or window.setInterval. When the observing timer executes, it sets an exit value to the shared variable.
The executing function constantly checks for the variable value.. and returns if the exit value is specified.

Categories

Resources