// This worker calls waitUntil() and respondWith() asynchronously and // reports back to the test whether they threw. // // These test cases are confusing. Bear in mind that the event is active // (calling waitUntil() is allowed) if: // * The pending promise count is not 0, or // * The event dispatch flag is set. // Controlled by 'init'/'done' messages. var resolveLockPromise; var port; self.addEventListener('message', function(event) { var waitPromise; var resolveTestPromise; switch (event.data.step) { case 'init': event.waitUntil(new Promise((res) => { resolveLockPromise = res; })); port = event.data.port; break; case 'done': resolveLockPromise(); break; // Throws because waitUntil() is called in a task after event dispatch // finishes. case 'no-current-extension-different-task': async_task_waituntil(event).then(reportResultExpecting('InvalidStateError')); break; // OK because waitUntil() is called in a microtask that runs after the // event handler runs, while the event dispatch flag is still set. case 'no-current-extension-different-microtask': async_microtask_waituntil(event).then(reportResultExpecting('OK')); break; // OK because the second waitUntil() is called while the first waitUntil() // promise is still pending. case 'current-extension-different-task': event.waitUntil(new Promise((res) => { resolveTestPromise = res; })); async_task_waituntil(event).then(reportResultExpecting('OK')).then(resolveTestPromise); break; // OK because all promises involved resolve "immediately", so the second // waitUntil() is called during the microtask checkpoint at the end of // event dispatching, when the event dispatch flag is still set. case 'during-event-dispatch-current-extension-expired-same-microtask-turn': waitPromise = Promise.resolve(); event.waitUntil(waitPromise); waitPromise.then(() => { return sync_waituntil(event); }) .then(reportResultExpecting('OK')) break; // OK for the same reason as above. case 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra': waitPromise = Promise.resolve(); event.waitUntil(waitPromise); waitPromise.then(() => { return async_microtask_waituntil(event); }) .then(reportResultExpecting('OK')) break; // OK because the pending promise count is decremented in a microtask // queued upon fulfillment of the first waitUntil() promise, so the second // waitUntil() is called while the pending promise count is still // positive. case 'after-event-dispatch-current-extension-expired-same-microtask-turn': waitPromise = makeNewTaskPromise(); event.waitUntil(waitPromise); waitPromise.then(() => { return sync_waituntil(event); }) .then(reportResultExpecting('OK')) break; // Throws because the second waitUntil() is called after the pending // promise count was decremented to 0. case 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra': waitPromise = makeNewTaskPromise(); event.waitUntil(waitPromise); waitPromise.then(() => { return async_microtask_waituntil(event); }) .then(reportResultExpecting('InvalidStateError')) break; // Throws because the second waitUntil() is called in a new task, after // first waitUntil() promise settled and the event dispatch flag is unset. case 'current-extension-expired-different-task': event.waitUntil(Promise.resolve()); async_task_waituntil(event).then(reportResultExpecting('InvalidStateError')); break; case 'script-extendable-event': self.dispatchEvent(new ExtendableEvent('nontrustedevent')); break; } event.source.postMessage('ACK'); }); self.addEventListener('fetch', function(event) { const path = new URL(event.request.url).pathname; const step = path.substring(path.lastIndexOf('/') + 1); let response; switch (step) { // OK because waitUntil() is called while the respondWith() promise is still // unsettled, so the pending promise count is positive. case 'pending-respondwith-async-waituntil': var resolveFetch; response = new Promise((res) => { resolveFetch = res; }); event.respondWith(response); async_task_waituntil(event) .then(reportResultExpecting('OK')) .then(() => { resolveFetch(new Response('OK')); }); break; // OK because all promises involved resolve "immediately", so waitUntil() is // called during the microtask checkpoint at the end of event dispatching, // when the event dispatch flag is still set. case 'during-event-dispatch-respondwith-microtask-sync-waituntil': response = Promise.resolve(new Response('RESP')); event.respondWith(response); response.then(() => { return sync_waituntil(event); }) .then(reportResultExpecting('OK')); break; // OK because all promises involved resolve "immediately", so waitUntil() is // called during the microtask checkpoint at the end of event dispatching, // when the event dispatch flag is still set. case 'during-event-dispatch-respondwith-microtask-async-waituntil': response = Promise.resolve(new Response('RESP')); event.respondWith(response); response.then(() => { return async_microtask_waituntil(event); }) .then(reportResultExpecting('OK')); break; // OK because the pending promise count is decremented in a microtask queued // upon fulfillment of the respondWith() promise, so waitUntil() is called // while the pending promise count is still positive. case 'after-event-dispatch-respondwith-microtask-sync-waituntil': response = makeNewTaskPromise().then(() => {return new Response('RESP');}); event.respondWith(response); response.then(() => { return sync_waituntil(event); }) .then(reportResultExpecting('OK')); break; // Throws because waitUntil() is called after the pending promise count was // decremented to 0. case 'after-event-dispatch-respondwith-microtask-async-waituntil': response = makeNewTaskPromise().then(() => {return new Response('RESP');}); event.respondWith(response); response.then(() => { return async_microtask_waituntil(event); }) .then(reportResultExpecting('InvalidStateError')) break; } }); self.addEventListener('nontrustedevent', function(event) { sync_waituntil(event).then(reportResultExpecting('InvalidStateError')); }); function reportResultExpecting(expectedResult) { return function (result) { port.postMessage({result : result, expected: expectedResult}); return result; }; } function sync_waituntil(event) { return new Promise((res, rej) => { try { event.waitUntil(Promise.resolve()); res('OK'); } catch (error) { res(error.name); } }); } function async_microtask_waituntil(event) { return new Promise((res, rej) => { Promise.resolve().then(() => { try { event.waitUntil(Promise.resolve()); res('OK'); } catch (error) { res(error.name); } }); }); } function async_task_waituntil(event) { return new Promise((res, rej) => { setTimeout(() => { try { event.waitUntil(Promise.resolve()); res('OK'); } catch (error) { res(error.name); } }, 0); }); } // Returns a promise that settles in a separate task. function makeNewTaskPromise() { return new Promise(resolve => { setTimeout(resolve, 0); }); }