diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91e428e..79dde6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,9 @@ name: "Test" on: pull_request: types: [opened, synchronize, reopened] + push: + branches: + - "main" # Only allow one workflow run at a time per PR concurrency: @@ -26,4 +29,4 @@ jobs: - name: "Test the package" run: npm cit --quiet - timeout-minutes: 5 \ No newline at end of file + timeout-minutes: 5 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index db595ca..58bccf2 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -8,7 +8,8 @@ on: types: ["created"] jobs: - build: + test: + name: "Test" runs-on: "ubuntu-latest" steps: - uses: actions/checkout@v4 @@ -19,7 +20,8 @@ jobs: timeout-minutes: 5 publish-npm: - needs: build + name: "Publish to npm" + needs: ["test"] permissions: contents: read id-token: write @@ -30,7 +32,6 @@ jobs: with: node-version: 22 registry-url: https://registry.npmjs.org/ - - run: npm ci - run: npm publish --provenance env: NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}} diff --git a/.npmignore b/.npmignore index 805c139..ed7205a 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,4 @@ +.github + .prettierrc.json -.github \ No newline at end of file +.prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +README.md diff --git a/README.md b/README.md index 5c768bd..e450951 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,39 @@ # post-task -A pre-configured progressively-enhancement utility function based on the Scheduler API. +A pre-configured progressively-enhancement utility function based on the +[Scheduler API](https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API). -If the scheduler API is available, use it. -If not, but `requestIdleCallback` is, then use that. Otherwise set a timeout. +If the Scheduler API is available, use it. Otherwise set a timeout as a +fallback. -The Scheduler API relies on browser heuristics, while the fallbacks wait and -call back (in the case of `requestIdleCallback`, as soon as possible in idle time -and definitely at the timeout, for `setTimeout` only at the timeout). - -If not in a browser environment, call back immediately. +The Scheduler API relies on browser heuristics, while the fallback waits and +calls back only after a certain time has passed. The tasks all return a `Promise` since the `scheduler.postTask` returns -one, but without any return value. +one. + +The interface re-exposes the values accepted for the +[`scheduler.postTask` API](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask) +and forwards them through when that API is available. + +The fallbacks are configured as following: + +| Priority | Timeout delay (ms) | +| ----------------- | ------------------ | +| `"user-blocking"` | 0 | +| `"user-visible"` | 0 | +| `"background"` | 150 | -The API is preconfigured with defaults to schedule tasks, based on the -[`scheduler.postTask` API](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask). +There is one exception: if a priority of `"user-blocking"` is passed, and the +Scheduler API is not available, the fallback will be +[`queueMicrotask`](https://developer.mozilla.org/en-US/docs/Web/API/Window/queueMicrotask) +if that function +[is available, which it usually will be, including in Node.js](https://developer.mozilla.org/en-US/docs/Web/API/Window/queueMicrotask#browser_compatibility). -This function is useful for breaking up chunks of work and freeing the main -thread, particularly important when focusing on the +This function is useful for breaking up chunks of work and allowing the event +loop to cycle, which is particularly important when focusing on the [Interaction to Next Paint](https://web.dev/articles/inp) -web vital. +web vital and of course the smooth interaction which it tries to measure. ## Use @@ -29,7 +42,7 @@ import postTask from "post-task"; // ... postTask(() => { - trackEvent("something-happened"); + trackEvent("something-happened"); }, "background"); ``` @@ -37,4 +50,3 @@ postTask(() => { This package is equally available as ESM and CJS and has a single, default export. -The code is identical between the formats except on the exporting itself. \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index f200e05..9cdf589 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,8 +3,17 @@ declare module "post-task" { /** - * Queues an arbitrary task to be executed in the browser, with the given priority. - * Allows breaking up the work of potentially long-running tasks to avoid blocking the main thread. + * Queues an arbitrary task to be scheduled for execution with the given + * priority. + * + * Allows the discrete and prioritised queuing of tasks which if run serially + * would block the main thread, but which do not have to be run immediately. + * + * @param task The callback to be executed. + * @param priority The priority of the task, following the + * Scheduler API. + * @returns A promise that resolves when the task is executed, + * in case it needs to be tracked. */ export default function postTask( task: () => void, diff --git a/index.mjs b/index.mjs index ff51c8e..6e50174 100644 --- a/index.mjs +++ b/index.mjs @@ -4,74 +4,76 @@ /** * The priority of the task: these are the priorities of the Scheduler API. * @typedef {('background' | 'user-visible' | 'user-blocking')} SchedulerPriority - */ - -/** @typedef {Record} PriorityConfigurationFallback */ - -/** - * The timeouts used for requestIdleCallback, which define the maximum time the task can be delayed. - * The task will be executed as soon as possible, in idle time, but guaranteed within the timeout. - * @type {PriorityConfigurationFallback} - */ -const priorityIdleTimeouts = Object.create(null, { - background: { value: 1000, enumerable: true }, - "user-visible": { value: 100, enumerable: true }, - "user-blocking": { value: 50, enumerable: true }, -}); - -/** - * The timeouts used for setTimeout, which define the delay before the task is executed. - * @type {PriorityConfigurationFallback} + * + * The timeouts used for `setTimeout`, which define the minimum delay in + * milliseconds before the callback will be executed. + * @type {Record} */ const priorityCallbackDelays = Object.create(null, { - background: { value: 150, enumerable: true }, - "user-visible": { value: 0, enumerable: true }, - "user-blocking": { value: 0, enumerable: true }, + /** + * A 150ms duration defines a "long task" for the Web Vitals. + * Waiting at least that long will give a better chance for long queues of + * work to be broken up. + * + * The task will then be scheduled as part of the next event loop, but + * ideally without directly adding to congestion if the CPU is busy. + */ + background: { value: 150 }, + /** + * User-visible tasks are scheduled immediately, and although this is the + * same timeout as for `"user-blocking"`, the fallback for that case will + * almost always schedule a microtask with the `queueMicrotask` function; + * while the 0ms timeout here will schedule a **macro**task which will run + * with a lower priority in the event loop. + */ + "user-visible": { value: 0 }, + /** + * User-blocking callbacks are scheduled immediately, mirroring the + * user-visible case as a fallback for the unlikely case that + * `queueMicrotask` is not available. + */ + "user-blocking": { value: 0 }, }); -/** @typedef {() => void} Task */ - /** - * Queues an arbitrary task to be executed in the browser, with the given priority. - * Allows breaking up the work of potentially long-running tasks to avoid blocking the main thread. - * @param {Task} task The callback to be executed. - * @param {SchedulerPriority} priority The priority of the task, following the Scheduler API. - * @returns {Promise} A promise that resolves when the task is executed, in case it needs to be tracked. + * Queues an arbitrary task to be scheduled for execution with the given + * priority. + * + * Allows the discrete and prioritised queuing of tasks which if run serially + * would block the main thread, but which do not have to be run immediately. + * + * @param {() => void} task The callback to be executed. + * @param {SchedulerPriority} priority The priority of the task, following the + * Scheduler API. + * @returns {Promise} A promise that resolves when the task is executed, + * in case it needs to be tracked. */ const postTask = (task, priority) => { - if (typeof window !== "undefined") { - // Prefer to use the Scheduler API, if available. - if ("scheduler" in window) { - return scheduler.postTask(task, { - priority, - }); - } - // Otherwise, if possible, queue the tracking in browser idle time. - else if ("requestIdleCallback" in window) { - return new Promise((resolve) => { - requestIdleCallback( - () => { - task(); - resolve(); - }, - { timeout: priorityIdleTimeouts[priority] }, - ); - }); - } - // Otherwise set a timeout with the appropriate delay - else { - return new Promise((resolve) => { - setTimeout(() => { - task(); - resolve(); - }, priorityCallbackDelays[priority]); + // Prefer to use the Scheduler API, if available. + if ("scheduler" in globalThis) { + return globalThis.scheduler.postTask(task, { + priority, + }); + } + // Otherwise, if available and for user-blocking tasks, + // use the native `queueMicrotask`. + else if (priority === "user-blocking" && "queueMicrotask" in globalThis) { + return new Promise((resolve) => { + globalThis.queueMicrotask(() => { + task(); + resolve(); }); - } - } else { - // On Node.js, just run the task immediately. - // This should be an edge case, but we will not suppress tasks. - task(); - return Promise.resolve(); + }); + } + // Otherwise, and always for the lower priorities on Node.js where the + // Scheduler API is not available, set a timeout with the appropriate delay. + else { + return new Promise((resolve) => { + globalThis.setTimeout(() => { + task(); + resolve(); + }, priorityCallbackDelays[priority]); + }); } }; diff --git a/package-lock.json b/package-lock.json index 267d903..b5cadb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "post-task", - "version": "1.1.5", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "post-task", - "version": "1.1.5", + "version": "1.2.0", "license": "MIT", "devDependencies": { "prettier": "3.4.2" diff --git a/package.json b/package.json index 9892f68..a315477 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "post-task", - "version": "1.1.5", + "version": "1.2.0", "description": "A polyfill for the Scheduler API with a pre-configured progressively-enhanced function helps to split long-running tasks into chunks.", "type": "module", "exports": { @@ -9,7 +9,7 @@ "require": "./index.cjs" }, "scripts": { - "test": "echo 'no tests yet'", + "test": "node --check index.mjs", "prepublishOnly": "sed 's/export default/module.exports =/g' ./index.mjs > index.cjs" }, "repository": { @@ -22,7 +22,10 @@ "INP", "yield" ], - "author": "Daniel Arthur Gallagher ", + "author": { + "name": "Daniel Arthur Gallagher", + "email": "daniel.gallagher@adevinta.com" + }, "license": "MIT", "bugs": { "url": "https://github.com/adevinta/post-task/issues"