diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d9cc6e6eb96..50d553835979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog ### Unreleased +- Added polyfill for `requestIdleCallback` - Improved the way of inner iterators cleaning in iterator helpers - Slight performance improvement for engines with native `Array#fill` on `ArrayBuffer` constructor and `%TypedArray%#fill` - Compat data improvements: diff --git a/docs/web/docs/features/web-standards/idle-callback.md b/docs/web/docs/features/web-standards/idle-callback.md new file mode 100644 index 000000000000..08b8d9806f3b --- /dev/null +++ b/docs/web/docs/features/web-standards/idle-callback.md @@ -0,0 +1,42 @@ +# Idle Callback +[Specification](https://w3c.github.io/requestidlecallback/)\ +[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) + +Note that `requestIdleCallback` cannot really be polyfilled as we don't know when an idle period happens. However, the polyfill contained in core-js uses `requestAnimationFrame` to ensure that operations contained in `requestIdleCallback` will not significantly throttle the rendering of graphics. + +## Modules +[`web.request-idle-callback`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/web.request-idle-callback.js) + +## Built-ins signatures +```ts +function requestIdleCallback( + callback: (deadline: IdleDeadline) => void, + options?: { timeout: number } +): number; +function cancelIdleCallback(handle: number): void; +interface IdleDeadline { + timeRemaining(): number; + readonly didTimeout: boolean; +} +``` + +## [Entry points]({docs-version}/docs/usage#h-entry-points) +```plaintext +core-js(-pure)/stable|actual|full|web/request-idle-callback +core-js(-pure)/stable|actual|full|web/cancel-idle-callback +core-js(-pure)/stable|actual|full|web/idle-deadline +``` + +- Some browsers (like Safari's Technical Preview) has only some components `requestIdleCallback`, `cancelIdleCallback`, and `IdleDeadline` exposed. In this case, importing any of those functions will use polyfilled versions. +- Polyfilled versions are not compatible with browser native versions in terms of request / cancel. Therefore it is strongly recommended to import all of the above-mentioned entry points together. +- Multiple polyfilled instances also do not have handles compatible with each other. + +## Examples +```js +requestIdleCallback( + deadline => { + console.log('Did timeout:', deadline.didTimeout, '| Time remaining (ms):', deadline.timeRemaining()); + }, + { timeout: 2000 } // forces callback with timeout after 2 seconds max +); +``` diff --git a/docs/web/docs/menu.json b/docs/web/docs/menu.json index a1c7edb1455a..cae41f8a9cba 100644 --- a/docs/web/docs/menu.json +++ b/docs/web/docs/menu.json @@ -413,6 +413,10 @@ { "title": "Iterable DOM collections", "url": "{docs-version}/docs/features/web-standards/iterable-dom-collections" + }, + { + "title": "Idle Callback", + "url": "{docs-version}/docs/features/web-standards/idle-callback" } ] }, diff --git a/packages/core-js-compat/src/data.mjs b/packages/core-js-compat/src/data.mjs index f3a1bf490fb0..750538735f0d 100644 --- a/packages/core-js-compat/src/data.mjs +++ b/packages/core-js-compat/src/data.mjs @@ -3210,6 +3210,10 @@ export const data = { node: '19.8', safari: '17.0', }, + 'web.request-idle-callback': { + chrome: '47', + // firefox: '55', -- incorrect type for timeout + }, }; export const renamed = new Map([ diff --git a/packages/core-js-compat/src/modules-by-versions.mjs b/packages/core-js-compat/src/modules-by-versions.mjs index 9b626655e7f6..5772735b6794 100644 --- a/packages/core-js-compat/src/modules-by-versions.mjs +++ b/packages/core-js-compat/src/modules-by-versions.mjs @@ -323,4 +323,7 @@ export default { 'es.weak-map.get-or-insert', 'es.weak-map.get-or-insert-computed', ], + '3.50': [ + 'web.request-idle-callback', + ], }; diff --git a/packages/core-js/actual/cancel-idle-callback.js b/packages/core-js/actual/cancel-idle-callback.js new file mode 100644 index 000000000000..ac9517767919 --- /dev/null +++ b/packages/core-js/actual/cancel-idle-callback.js @@ -0,0 +1,4 @@ +'use strict'; +var parent = require('../stable/cancel-idle-callback'); + +module.exports = parent; diff --git a/packages/core-js/actual/idle-deadline.js b/packages/core-js/actual/idle-deadline.js new file mode 100644 index 000000000000..1b6ca2e639f6 --- /dev/null +++ b/packages/core-js/actual/idle-deadline.js @@ -0,0 +1,4 @@ +'use strict'; +var parent = require('../stable/idle-deadline'); + +module.exports = parent; diff --git a/packages/core-js/actual/request-idle-callback.js b/packages/core-js/actual/request-idle-callback.js new file mode 100644 index 000000000000..2e12903a555b --- /dev/null +++ b/packages/core-js/actual/request-idle-callback.js @@ -0,0 +1,4 @@ +'use strict'; +var parent = require('../stable/request-idle-callback'); + +module.exports = parent; diff --git a/packages/core-js/full/cancel-idle-callback.js b/packages/core-js/full/cancel-idle-callback.js new file mode 100644 index 000000000000..efcff1f6611d --- /dev/null +++ b/packages/core-js/full/cancel-idle-callback.js @@ -0,0 +1,4 @@ +'use strict'; +var parent = require('../actual/cancel-idle-callback'); + +module.exports = parent; diff --git a/packages/core-js/full/idle-deadline.js b/packages/core-js/full/idle-deadline.js new file mode 100644 index 000000000000..aab1f6770c0c --- /dev/null +++ b/packages/core-js/full/idle-deadline.js @@ -0,0 +1,4 @@ +'use strict'; +var parent = require('../actual/idle-deadline'); + +module.exports = parent; diff --git a/packages/core-js/full/request-idle-callback.js b/packages/core-js/full/request-idle-callback.js new file mode 100644 index 000000000000..28a4c7b09c99 --- /dev/null +++ b/packages/core-js/full/request-idle-callback.js @@ -0,0 +1,4 @@ +'use strict'; +var parent = require('../actual/request-idle-callback'); + +module.exports = parent; diff --git a/packages/core-js/internals/idle-callback-detection.js b/packages/core-js/internals/idle-callback-detection.js new file mode 100644 index 000000000000..92f4943824f2 --- /dev/null +++ b/packages/core-js/internals/idle-callback-detection.js @@ -0,0 +1,6 @@ +'use strict'; +var globalThis = require('../internals/global-this'); +var Firefox = require('../internals/environment-ff-version'); + +exports.BASIC = globalThis.requestIdleCallback && globalThis.cancelIdleCallback && globalThis.IdleDeadline; +exports.BOUNDS = !Firefox; diff --git a/packages/core-js/internals/queue.js b/packages/core-js/internals/queue.js index 0785558e32ed..a1f17ef33e5d 100644 --- a/packages/core-js/internals/queue.js +++ b/packages/core-js/internals/queue.js @@ -6,19 +6,57 @@ var Queue = function () { Queue.prototype = { add: function (item) { - var entry = { item: item, next: null }; + var entry = { item: item, next: null, prev: this.tail, queue: this }; var tail = this.tail; if (tail) tail.next = entry; else this.head = entry; this.tail = entry; + return entry; + }, + addEntry: function (entry) { + var queue = entry.queue; + if (queue) queue.erase(entry); + + entry.prev = this.tail; + entry.next = null; + entry.queue = this; + + var tail = this.tail; + if (tail) tail.next = entry; + else this.head = entry; + this.tail = entry; + return entry; }, get: function () { + var entry = this.getEntry(); + if (entry) return entry.item; + }, + getEntry: function () { var entry = this.head; if (entry) { var next = this.head = entry.next; if (next === null) this.tail = null; - return entry.item; + else next.prev = null; + entry.prev = null; + entry.next = null; + entry.queue = null; + return entry; } + }, + erase: function (entry) { + if (entry.queue !== this) return; + var prev = entry.prev; + var next = entry.next; + if (prev) prev.next = next; + else this.head = next; + if (next) next.prev = prev; + else this.tail = prev; + entry.prev = null; + entry.next = null; + entry.queue = null; + }, + empty: function () { + return !this.head; } }; diff --git a/packages/core-js/internals/to-number.js b/packages/core-js/internals/to-number.js new file mode 100644 index 000000000000..22a75dbf25a2 --- /dev/null +++ b/packages/core-js/internals/to-number.js @@ -0,0 +1,51 @@ +'use strict'; +var globalThis = require('./global-this'); +var uncurryThis = require('./function-uncurry-this'); +var isSymbol = require('./is-symbol'); +var toPrimitive = require('./to-primitive'); +var trim = require('./string-trim').trim; + +var TypeError = globalThis.TypeError; +var stringSlice = uncurryThis(''.slice); +var charCodeAt = uncurryThis(''.charCodeAt); + +// `ToNumber` abstract operation +// https://tc39.es/ecma262/#sec-tonumber +module.exports = function (argument) { + var it = toPrimitive(argument, 'number'); + var first, third, radix, maxCode, digits, length, index, code; + if (isSymbol(it)) throw new TypeError('Cannot convert a Symbol value to a number'); + if (typeof it == 'string' && it.length > 2) { + it = trim(it); + first = charCodeAt(it, 0); + if (first === 43 || first === 45) { + third = charCodeAt(it, 2); + if (third === 88 || third === 120) return NaN; // Number('+0x1') should be NaN, old V8 fix + } else if (first === 48) { + switch (charCodeAt(it, 1)) { + // fast equal of /^0b[01]+$/i + case 66: + case 98: + radix = 2; + maxCode = 49; + break; + // fast equal of /^0o[0-7]+$/i + case 79: + case 111: + radix = 8; + maxCode = 55; + break; + default: + return +it; + } + digits = stringSlice(it, 2); + length = digits.length; + for (index = 0; index < length; index++) { + code = charCodeAt(digits, index); + // parseInt parses a string to a first unavailable symbol + // but ToNumber should return NaN if a string contains unavailable symbols + if (code < 48 || code > maxCode) return NaN; + } return parseInt(digits, radix); + } + } return +it; +}; diff --git a/packages/core-js/internals/to-unsigned-long.js b/packages/core-js/internals/to-unsigned-long.js new file mode 100644 index 000000000000..280ac6d9c583 --- /dev/null +++ b/packages/core-js/internals/to-unsigned-long.js @@ -0,0 +1,20 @@ +'use strict'; +var toNumber = require('./to-number'); + +var $isNaN = isNaN; +var abs = Math.abs; +var floor = Math.floor; +var MOD = 0x100000000; + +// WebIDL Unsigned Long +module.exports = function toUnsignedLong(value) { + value = toNumber(value); + // Normalize -0 + if (value === 0) value = 0; + if ($isNaN(value) || value === Infinity || value === -Infinity) return 0; + var r = floor(abs(value)); + if (value < 0) r = -r; + value = r; + value = ((value % MOD) + MOD) % MOD; + return value; +}; diff --git a/packages/core-js/modules/es.number.constructor.js b/packages/core-js/modules/es.number.constructor.js index a7e856c69dac..0143b99f5c0f 100644 --- a/packages/core-js/modules/es.number.constructor.js +++ b/packages/core-js/modules/es.number.constructor.js @@ -4,27 +4,22 @@ var IS_PURE = require('../internals/is-pure'); var DESCRIPTORS = require('../internals/descriptors'); var globalThis = require('../internals/global-this'); var path = require('../internals/path'); -var uncurryThis = require('../internals/function-uncurry-this'); var isForced = require('../internals/is-forced'); var hasOwn = require('../internals/has-own-property'); var inheritIfRequired = require('../internals/inherit-if-required'); var isPrototypeOf = require('../internals/object-is-prototype-of'); -var isSymbol = require('../internals/is-symbol'); var toPrimitive = require('../internals/to-primitive'); var fails = require('../internals/fails'); var getOwnPropertyNames = require('../internals/object-get-own-property-names').f; var getOwnPropertyDescriptor = require('../internals/object-get-own-property-descriptor').f; var defineProperty = require('../internals/object-define-property').f; var thisNumberValue = require('../internals/this-number-value'); -var trim = require('../internals/string-trim').trim; +var toNumber = require('../internals/to-number'); var NUMBER = 'Number'; var NativeNumber = globalThis[NUMBER]; var PureNumberNamespace = path[NUMBER]; var NumberPrototype = NativeNumber.prototype; -var TypeError = globalThis.TypeError; -var stringSlice = uncurryThis(''.slice); -var charCodeAt = uncurryThis(''.charCodeAt); // `ToNumeric` abstract operation // https://tc39.es/ecma262/#sec-tonumeric @@ -33,47 +28,6 @@ var toNumeric = function (value) { return typeof primValue == 'bigint' ? primValue : toNumber(primValue); }; -// `ToNumber` abstract operation -// https://tc39.es/ecma262/#sec-tonumber -var toNumber = function (argument) { - var it = toPrimitive(argument, 'number'); - var first, third, radix, maxCode, digits, length, index, code; - if (isSymbol(it)) throw new TypeError('Cannot convert a Symbol value to a number'); - if (typeof it == 'string' && it.length > 2) { - it = trim(it); - first = charCodeAt(it, 0); - if (first === 43 || first === 45) { - third = charCodeAt(it, 2); - if (third === 88 || third === 120) return NaN; // Number('+0x1') should be NaN, old V8 fix - } else if (first === 48) { - switch (charCodeAt(it, 1)) { - // fast equal of /^0b[01]+$/i - case 66: - case 98: - radix = 2; - maxCode = 49; - break; - // fast equal of /^0o[0-7]+$/i - case 79: - case 111: - radix = 8; - maxCode = 55; - break; - default: - return +it; - } - digits = stringSlice(it, 2); - length = digits.length; - for (index = 0; index < length; index++) { - code = charCodeAt(digits, index); - // parseInt parses a string to a first unavailable symbol - // but ToNumber should return NaN if a string contains unavailable symbols - if (code < 48 || code > maxCode) return NaN; - } return parseInt(digits, radix); - } - } return +it; -}; - var FORCED = isForced(NUMBER, !NativeNumber(' 0o1') || !NativeNumber('0b1') || NativeNumber('+0x1')); var calledWithNew = function (dummy) { diff --git a/packages/core-js/modules/web.request-idle-callback.js b/packages/core-js/modules/web.request-idle-callback.js new file mode 100644 index 000000000000..555b6efc3848 --- /dev/null +++ b/packages/core-js/modules/web.request-idle-callback.js @@ -0,0 +1,203 @@ +/* eslint no-underscore-dangle: 0 -- internal vars use __ for private state */ +'use strict'; +var $ = require('../internals/export'); + +var anObjectOrUndefined = require('../internals/an-object-or-undefined'); +var aCallable = require('../internals/a-callable'); +var validateArgumentsLength = require('../internals/validate-arguments-length'); +var uncurryThis = require('../internals/function-uncurry-this'); +var globalThis = require('../internals/global-this'); +var defineBuiltIn = require('../internals/define-built-in'); +var Queue = require('../internals/queue'); +var DESCRIPTORS = require('../internals/descriptors'); +var defineProperty = require('../internals/object-define-property').f; +var toUnsignedLong = require('../internals/to-unsigned-long'); +var InternalStateModule = require('../internals/internal-state'); +var BASIC = require('../internals/idle-callback-detection').BASIC; +var BOUNDS = require('../internals/idle-callback-detection').BOUNDS; + +var setToStringTag = require('../internals/set-to-string-tag'); + +var $TypeError = TypeError; + +var $Date = globalThis.Date; +var $setTimeout = globalThis.setTimeout; +var $clearTimeout = globalThis.clearTimeout; +var getTime = uncurryThis($Date.prototype.getTime); +var $performance = globalThis.performance; +var setInternalState = InternalStateModule.set; +var getInternalIdleDeadlineState = InternalStateModule.getterFor('IdleDeadline'); +var $now; +if ($performance) { + $now = uncurryThis(globalThis.performance.now).bind(null, $performance); +} +var $max = Math.max; +var $min = Math.min; +var now = $now || function () { + return getTime(new $Date()); +}; +var $rAF = globalThis.requestAnimationFrame || globalThis.mozRequestAnimationFrame || globalThis.webkitRequestAnimationFrame; +var rAF = $rAF || function (callback) { + $setTimeout(function () { + callback(); + }, 16); +}; + +var __idleRequestCallbacks = new Queue(); +var __runnableIdleCallbacks = new Queue(); +var __idleCallbackId = 0; +var __idleCallbackMap = {}; +var __idleRafScheduled = false; +var __timeoutHandles = {}; +var __handleObjects = {}; + +var IdleDeadlineState = function IdleDeadlineState(deadlineTime, didTimeout) { + this.deadlineTime = deadlineTime; + this.didTimeout = didTimeout; +}; +IdleDeadlineState.prototype.type = 'IdleDeadline'; + +var IdleDeadline = function IdleDeadline() { + throw new $TypeError('Illegal Constructor'); +}; +setToStringTag(IdleDeadline, 'IdleDeadline'); +defineBuiltIn(IdleDeadline.prototype, 'timeRemaining', function timeRemaining() { + return $max(getInternalIdleDeadlineState(this).deadlineTime - now(), 0); +}, { writable: true, enumerable: true, configurable: true }); +if (DESCRIPTORS) { + defineProperty(IdleDeadline.prototype, 'didTimeout', { + get: function () { + return getInternalIdleDeadlineState(this).didTimeout; + }, + enumerable: true, + configurable: true + }); +} + +var IdleDeadlinePriv = function IdleDeadlinePriv(deadlineTime, didTimeout) { + setInternalState(this, new IdleDeadlineState(deadlineTime, didTimeout)); + if (!DESCRIPTORS) { + this.didTimeout = getInternalIdleDeadlineState(this).didTimeout; + } +}; +IdleDeadlinePriv.prototype = IdleDeadline.prototype; + +function scheduleNextIdle() { + if (__idleRafScheduled) return; + __idleRafScheduled = true; + + rAF(function () { + $setTimeout(startIdlePeriod, 0); + }); +} + +// Start an idle period +function startIdlePeriod() { + // Move pending to runnable + var entry; + while (entry = __idleRequestCallbacks.getEntry()) { + __runnableIdleCallbacks.addEntry(entry); + } + __idleRafScheduled = false; + + // 8 does not drop framerate in most places; there's no way + // to actually get how much time we have before the browser + // starts to paint the next frame + var deadlineTime = now() + 8; + while (!__runnableIdleCallbacks.empty()) { + var handle = __runnableIdleCallbacks.get(); + var cb = __idleCallbackMap[handle]; + if (!cb) continue; // cancelled or timed out + delete __idleCallbackMap[handle]; + // Cancel the timeout timer. + if (__timeoutHandles[handle] !== undefined) { + $clearTimeout(__timeoutHandles[handle]); + delete __timeoutHandles[handle]; + } + + var deadline = new IdleDeadlinePriv(deadlineTime, false); + try { + cb(deadline); + } catch (error) { + $setTimeout(function () { throw error; }, 0); + } + if (now() >= deadlineTime) break; // yield mid-frame if already past schedule + } + + // Reschedule if any callbacks remain + if (!__runnableIdleCallbacks.empty()) { + scheduleNextIdle(); + } +} + +var nativeRequestIdleCallback = globalThis.requestIdleCallback; + +$({ global: true, forced: !(BASIC && BOUNDS) }, { + requestIdleCallback: function requestIdleCallback(callback) { + var options = arguments[1]; + anObjectOrUndefined(options); + var timeout = 0; + if (options !== undefined) timeout = toUnsignedLong(options.timeout); + // Clamp timeout to maximum allowed by setTimeout, which is long, not unsigned long. + // Even if native function exists, Firefox still only allows signed long in timeouts. + timeout = $min(timeout, 0x7FFFFFFF); + if (BASIC && !BOUNDS) { + if (timeout <= 0) return nativeRequestIdleCallback(callback); + return nativeRequestIdleCallback(callback, { timeout: timeout }); + } + validateArgumentsLength(arguments.length, 1); + aCallable(callback); + var handle = ++__idleCallbackId; + __idleCallbackMap[handle] = callback; + __handleObjects[handle] = __idleRequestCallbacks.add(handle); + if (options && timeout > 0) { + // FIXME: Spec says that the timeout calling must sort by currentTime + + // timeout, however maintaining such a priority queue would be very tedious + __timeoutHandles[handle] = $setTimeout(function timeoutCallback() { + var cb = __idleCallbackMap[handle]; + if (!cb) return; + delete __idleCallbackMap[handle]; + delete __timeoutHandles[handle]; + if (__handleObjects[handle].queue === __idleRequestCallbacks) { + __idleRequestCallbacks.erase(__handleObjects[handle]); + } + if (__handleObjects[handle].queue === __runnableIdleCallbacks) { + __runnableIdleCallbacks.erase(__handleObjects[handle]); + } + delete __handleObjects[handle]; + var deadline = new IdleDeadlinePriv(now(), true); + try { + cb(deadline); + } catch (error) { + $setTimeout(function () { + throw error; + }, 0); + } + }, timeout); + } + // Start running things on the next frame if needed + scheduleNextIdle(); + return handle; + } +}); + +$({ global: true, forced: !BASIC }, { + cancelIdleCallback: function cancelIdleCallback(handle) { + validateArgumentsLength(arguments.length, 1); + handle = toUnsignedLong(handle); + if (__handleObjects[handle] === undefined) return; + delete __idleCallbackMap[handle]; + if (__timeoutHandles[handle] !== undefined) { + $clearTimeout(__timeoutHandles[handle]); + delete __timeoutHandles[handle]; + } + if (__handleObjects[handle].queue === __idleRequestCallbacks) { + __idleRequestCallbacks.erase(__handleObjects[handle]); + } + if (__handleObjects[handle].queue === __runnableIdleCallbacks) { + __runnableIdleCallbacks.erase(__handleObjects[handle]); + } + delete __handleObjects[handle]; + }, + IdleDeadline: IdleDeadline +}); diff --git a/packages/core-js/stable/cancel-idle-callback.js b/packages/core-js/stable/cancel-idle-callback.js new file mode 100644 index 000000000000..0f90503b3749 --- /dev/null +++ b/packages/core-js/stable/cancel-idle-callback.js @@ -0,0 +1,5 @@ +'use strict'; +require('../modules/web.request-idle-callback'); +var path = require('../internals/path'); + +module.exports = path.cancelIdleCallback; diff --git a/packages/core-js/stable/idle-deadline.js b/packages/core-js/stable/idle-deadline.js new file mode 100644 index 000000000000..8ddedfdf0254 --- /dev/null +++ b/packages/core-js/stable/idle-deadline.js @@ -0,0 +1,5 @@ +'use strict'; +require('../modules/web.request-idle-callback'); +var path = require('../internals/path'); + +module.exports = path.IdleDeadline; diff --git a/packages/core-js/stable/request-idle-callback.js b/packages/core-js/stable/request-idle-callback.js new file mode 100644 index 000000000000..f496966ecedd --- /dev/null +++ b/packages/core-js/stable/request-idle-callback.js @@ -0,0 +1,5 @@ +'use strict'; +require('../modules/web.request-idle-callback'); +var path = require('../internals/path'); + +module.exports = path.requestIdleCallback; diff --git a/packages/core-js/web/cancel-idle-callback.js b/packages/core-js/web/cancel-idle-callback.js new file mode 100644 index 000000000000..0f90503b3749 --- /dev/null +++ b/packages/core-js/web/cancel-idle-callback.js @@ -0,0 +1,5 @@ +'use strict'; +require('../modules/web.request-idle-callback'); +var path = require('../internals/path'); + +module.exports = path.cancelIdleCallback; diff --git a/packages/core-js/web/idle-deadline.js b/packages/core-js/web/idle-deadline.js new file mode 100644 index 000000000000..8ddedfdf0254 --- /dev/null +++ b/packages/core-js/web/idle-deadline.js @@ -0,0 +1,5 @@ +'use strict'; +require('../modules/web.request-idle-callback'); +var path = require('../internals/path'); + +module.exports = path.IdleDeadline; diff --git a/packages/core-js/web/index.js b/packages/core-js/web/index.js index d0a6f4e5f969..ebc068c3473e 100644 --- a/packages/core-js/web/index.js +++ b/packages/core-js/web/index.js @@ -19,6 +19,7 @@ require('../modules/web.url-search-params'); require('../modules/web.url-search-params.delete'); require('../modules/web.url-search-params.has'); require('../modules/web.url-search-params.size'); +require('../modules/web.request-idle-callback'); var path = require('../internals/path'); module.exports = path; diff --git a/packages/core-js/web/request-idle-callback.js b/packages/core-js/web/request-idle-callback.js new file mode 100644 index 000000000000..f496966ecedd --- /dev/null +++ b/packages/core-js/web/request-idle-callback.js @@ -0,0 +1,5 @@ +'use strict'; +require('../modules/web.request-idle-callback'); +var path = require('../internals/path'); + +module.exports = path.requestIdleCallback; diff --git a/tests/compat/tests.js b/tests/compat/tests.js index 6f9e41bc168a..a296469ae519 100644 --- a/tests/compat/tests.js +++ b/tests/compat/tests.js @@ -2275,5 +2275,13 @@ GLOBAL.tests = { }], 'web.url-search-params.size': [URL_AND_URL_SEARCH_PARAMS_SUPPORT, function () { return 'size' in URLSearchParams.prototype; - }] + }], + 'web.request-idle-callback': [ + function () { + // Firefox bug on negative / 0 timeout + // Proper test would be requestIdleCallback(a=>(console.log(a.didTimeout)), {timeout: -1}) + // -- if that gives false we're good + return requestIdleCallback && cancelIdleCallback && IdleDeadline && !/Firefox/.test(USERAGENT); + } + ] }; diff --git a/tests/entries/unit.mjs b/tests/entries/unit.mjs index 289d86e4077f..d4701863ea02 100644 --- a/tests/entries/unit.mjs +++ b/tests/entries/unit.mjs @@ -689,6 +689,9 @@ for (PATH of ['core-js-pure', 'core-js']) { ok(typeof load(NS, 'clear-immediate') == 'function'); ok(typeof load(NS, 'queue-microtask') == 'function'); ok(typeof load(NS, 'url') == 'function'); + ok(typeof load(NS, 'request-idle-callback') == 'function'); + ok(typeof load(NS, 'cancel-idle-callback') == 'function'); + ok(typeof load(NS, 'idle-deadline') == 'function'); ok(load(NS, 'url/can-parse')('a:b') === true); ok(load(NS, 'url/parse')('a:b').href === 'a:b'); load(NS, 'url/to-json'); @@ -1025,6 +1028,9 @@ for (PATH of ['core-js-pure', 'core-js']) { ok(load('web/timers')); ok(load('web/url')); ok(load('web/url-search-params')); + ok(load('web/request-idle-callback')); + ok(load('web/cancel-idle-callback')); + ok(load('web/idle-deadline')); ok(load('web')); for (const key in entries) { diff --git a/tests/unit-global/web.request-idle-callback.js b/tests/unit-global/web.request-idle-callback.js new file mode 100644 index 000000000000..c7d2b136e5c5 --- /dev/null +++ b/tests/unit-global/web.request-idle-callback.js @@ -0,0 +1,72 @@ +QUnit.test('idle callbacks', assert => { + // Avoid infinite waiting if a handle is not called. + assert.timeout(3000); + + assert.isFunction(requestIdleCallback); + assert.arity(requestIdleCallback, 1); + assert.name(requestIdleCallback, 'requestIdleCallback'); + + const done = assert.async(6); + + requestIdleCallback(deadline => { + assert.false(deadline.didTimeout, 'timed out without a timeout'); + assert.strictEqual(typeof deadline.timeRemaining(), 'number', 'not a number'); + done(); + }); + + // A string is coerced into NaN ==> no timeout. + requestIdleCallback(deadline => { + assert.false(deadline.didTimeout, 'timed out with NaN timeout'); + done(); + }, { timeout: 'Allison' }); + + requestIdleCallback(deadline => { + assert.false(deadline.didTimeout, 'timed out with negative timeout'); + done(); + }, { timeout: -10 }); + + requestIdleCallback(deadline => { + assert.false(deadline.didTimeout, 'timed out with truncated 0 timeout'); + done(); + }, { timeout: 0.00001 }); + + assert.isFunction(cancelIdleCallback); + assert.arity(cancelIdleCallback, 1); + assert.name(cancelIdleCallback, 'cancelIdleCallback'); + + const handle = requestIdleCallback(() => { + // Shouldn't ever be called. + assert.true(false, 'canceled callback called'); + }); + // test that floats are truncated + cancelIdleCallback(handle + 0.1); + + // Give the inner "shouldn't ever be called" + // assert a chance to run and fail. + setTimeout(() => { + done(); + }, 500); + + let ran = false; + requestIdleCallback(() => { + ran = true; + }); + requestIdleCallback(() => { + // Test that the operation is FIFO. + assert.true(ran, 'not FIFO'); + done(); + }); + + assert.throws(requestIdleCallback, TypeError); + assert.throws(cancelIdleCallback, TypeError); + assert.throws(() => { + requestIdleCallback('allison'); + }, TypeError); + // Shouldn't do anything, as allison is not a number. + cancelIdleCallback('allison'); + + assert.isFunction(IdleDeadline); + assert.arity(IdleDeadline, 0); + assert.name(IdleDeadline, 'IdleDeadline'); + assert.throws(IdleDeadline, TypeError); +}); diff --git a/tests/unit-pure/web.request-idle-callback.js b/tests/unit-pure/web.request-idle-callback.js new file mode 100644 index 000000000000..9e5d900c574c --- /dev/null +++ b/tests/unit-pure/web.request-idle-callback.js @@ -0,0 +1,76 @@ +import requestIdleCallback from 'core-js-pure/stable/request-idle-callback'; +import cancelIdleCallback from 'core-js-pure/stable/cancel-idle-callback'; +import IdleDeadline from 'core-js-pure/stable/idle-deadline'; + +QUnit.test('idle callbacks', assert => { + // Avoid infinite waiting if a handle is not called. + assert.timeout(3000); + + assert.isFunction(requestIdleCallback); + assert.arity(requestIdleCallback, 1); + assert.name(requestIdleCallback, 'requestIdleCallback'); + + const done = assert.async(6); + + requestIdleCallback(deadline => { + assert.false(deadline.didTimeout, 'timed out without a timeout'); + assert.strictEqual(typeof deadline.timeRemaining(), 'number', 'not a number'); + done(); + }); + + // A string is coerced into NaN ==> no timeout. + requestIdleCallback(deadline => { + assert.false(deadline.didTimeout, 'timed out with NaN timeout'); + done(); + }, { timeout: 'Allison' }); + + requestIdleCallback(deadline => { + assert.false(deadline.didTimeout, 'timed out with negative timeout'); + done(); + }, { timeout: -10 }); + + requestIdleCallback(deadline => { + assert.false(deadline.didTimeout, 'timed out with truncated 0 timeout'); + done(); + }, { timeout: 0.00001 }); + + assert.isFunction(cancelIdleCallback); + assert.arity(cancelIdleCallback, 1); + assert.name(cancelIdleCallback, 'cancelIdleCallback'); + + const handle = requestIdleCallback(() => { + // Shouldn't ever be called. + assert.true(false, 'canceled callback called'); + }); + // test that floats are truncated + cancelIdleCallback(handle + 0.1); + + // Give the inner "shouldn't ever be called" + // assert a chance to run and fail. + setTimeout(() => { + done(); + }, 500); + + let ran = false; + requestIdleCallback(() => { + ran = true; + }); + requestIdleCallback(() => { + // Test that the operation is FIFO. + assert.true(ran, 'not FIFO'); + done(); + }); + + assert.throws(requestIdleCallback, TypeError); + assert.throws(cancelIdleCallback, TypeError); + assert.throws(() => { + requestIdleCallback('allison'); + }, TypeError); + // Shouldn't do anything, as allison is not a number. + cancelIdleCallback('allison'); + + assert.isFunction(IdleDeadline); + assert.arity(IdleDeadline, 0); + assert.name(IdleDeadline, 'IdleDeadline'); + assert.throws(IdleDeadline, TypeError); +});