diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 28a4cdd8b..b507d1988 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "11c3ebd9479ef9f7eca0", + "artifactVersion": "382e8ff3544e0a3e29a8", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 4471c5305..3468361c2 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -1067,9 +1067,9 @@ }, "dynamic": { "supported": 0, - "partial": 10, + "partial": 22, "refused": 0, - "not-implemented": 15, + "not-implemented": 3, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -44567,7 +44567,7 @@ }, "dynamic": { "status": "partial", - "detail": "Provided when the embedded npm module bootstrap runs; absent from standalone dynamic islands.", + "detail": "Provided by the embedded engine's web/global compatibility layer.", "verification": "test-backed" } }, @@ -44707,7 +44707,7 @@ }, "dynamic": { "status": "partial", - "detail": "Provided when the embedded npm module bootstrap runs; absent from standalone dynamic islands.", + "detail": "Provided by the embedded engine's web/global compatibility layer.", "verification": "test-backed" } }, @@ -78240,7 +78240,7 @@ }, "dynamic": { "status": "partial", - "detail": "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.", + "detail": "Provides cancellable promise timers, buffered interval iteration, scheduler waits and yields, and ref options over the native event loop; delay warnings remain outside this subset.", "verification": "test-backed" } }, @@ -78268,7 +78268,7 @@ }, "dynamic": { "status": "partial", - "detail": "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.", + "detail": "Provides cancellable promise timers, buffered interval iteration, scheduler waits and yields, and ref options over the native event loop; delay warnings remain outside this subset.", "verification": "test-backed" } }, @@ -78296,7 +78296,7 @@ }, "dynamic": { "status": "partial", - "detail": "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.", + "detail": "Provides cancellable promise timers, buffered interval iteration, scheduler waits and yields, and ref options over the native event loop; delay warnings remain outside this subset.", "verification": "test-backed" } }, @@ -78323,9 +78323,9 @@ "verification": "registry-gap" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides cancellable promise timers, buffered interval iteration, scheduler waits and yields, and ref options over the native event loop; delay warnings remain outside this subset.", + "verification": "test-backed" } }, { @@ -78351,9 +78351,9 @@ "verification": "registry-gap" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides cancellable promise timers, buffered interval iteration, scheduler waits and yields, and ref options over the native event loop; delay warnings remain outside this subset.", + "verification": "test-backed" } }, { @@ -78407,9 +78407,9 @@ "verification": "test-backed" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { @@ -78435,9 +78435,9 @@ "verification": "test-backed" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { @@ -78463,9 +78463,9 @@ "verification": "test-backed" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { @@ -78491,9 +78491,9 @@ "verification": "test-backed" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { @@ -78547,9 +78547,9 @@ "verification": "registry-gap" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { @@ -78575,9 +78575,9 @@ "verification": "test-backed" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { @@ -78603,9 +78603,9 @@ "verification": "test-backed" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { @@ -78631,9 +78631,9 @@ "verification": "test-backed" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { @@ -78659,9 +78659,9 @@ "verification": "test-backed" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { @@ -78715,9 +78715,9 @@ "verification": "test-backed" }, "dynamic": { - "status": "not-implemented", - "detail": "This API is not implemented by the dynamic-island shim.", - "verification": "registry-gap" + "status": "partial", + "detail": "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/dynamic-support.json b/internal/compatibility/dynamic-support.json index 7248b78dc..a6d83d7ff 100644 --- a/internal/compatibility/dynamic-support.json +++ b/internal/compatibility/dynamic-support.json @@ -19,6 +19,8 @@ "wasi": "by-design" }, "features": [ + { "chapter": "timers", "symbols": ["timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-promise-timers/main.ts", "tests/fixtures/npm/cases/node24-timers-unref/main.ts"] }, + { "chapter": "timers", "symbols": ["immediate.ref", "immediate.unref", "immediate.hasRef", "timeout.ref", "timeout.unref", "timeout.hasRef", "timeout.refresh", "timeout.close"], "signatures": ["`immediate[Symbol.dispose]()`", "`timeout[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/island-timers/main.ts", "tests/fixtures/npm/cases/node24-promise-timers/main.ts", "tests/fixtures/npm/cases/node24-timers-unref/main.ts"] }, { "chapter": "events", "symbols": ["events.once", "events.on", "events.addAbortListener"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-async-events/main.ts"] }, { "chapter": "events", "symbols": ["emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.removeListener", "emitter.off", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.eventNames", "events.errorMonitor"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/stream-shims/main.ts", "tests/fixtures/npm/cases/node24-event-emitter/main.ts"] }, { "chapter": "util", "symbols": ["util.promisify", "util.callbackify"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts", "tests/fixtures/npm/cases/node24-util-adapters/main.ts"] }, @@ -91,8 +93,8 @@ "util/types": { "exports": ["isAnyArrayBuffer", "isArrayBufferView", "isArgumentsObject", "isArrayBuffer", "isAsyncFunction", "isBigInt64Array", "isBigUint64Array", "isBooleanObject", "isBoxedPrimitive", "isBigIntObject", "isCryptoKey", "isDataView", "isDate", "isExternal", "isFloat16Array", "isFloat32Array", "isFloat64Array", "isGeneratorFunction", "isGeneratorObject", "isInt8Array", "isInt16Array", "isInt32Array", "isKeyObject", "isMap", "isMapIterator", "isModuleNamespaceObject", "isNativeError", "isNumberObject", "isPromise", "isProxy", "isRegExp", "isSet", "isSetIterator", "isSharedArrayBuffer", "isStringObject", "isSymbolObject", "isTypedArray", "isUint8Array", "isUint8ClampedArray", "isUint16Array", "isUint32Array", "isWeakMap", "isWeakSet"], "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts"] } }, "globals": { - "supported": ["atob", "btoa", "clearInterval", "clearTimeout", "queueMicrotask", "setInterval", "setTimeout", "structuredClone", "AbortController", "AbortSignal", "Blob", "CustomEvent", "DOMException", "Event", "EventTarget", "File", "Headers", "MessageChannel", "MessageEvent", "MessagePort", "Request", "Response", "TextDecoder", "TextDecoderStream", "TextEncoder", "TransformStream", "URLSearchParams", "ReadableStream"], - "npmSupported": ["Buffer", "URL", "clearImmediate", "setImmediate"], + "supported": ["atob", "btoa", "clearImmediate", "clearInterval", "clearTimeout", "queueMicrotask", "setImmediate", "setInterval", "setTimeout", "structuredClone", "AbortController", "AbortSignal", "Blob", "CustomEvent", "DOMException", "Event", "EventTarget", "File", "Headers", "MessageChannel", "MessageEvent", "MessagePort", "Request", "Response", "TextDecoder", "TextDecoderStream", "TextEncoder", "TransformStream", "URLSearchParams", "ReadableStream"], + "npmSupported": ["Buffer", "URL"], "absent": ["BroadcastChannel", "ByteLengthQueuingStrategy", "CloseEvent", "CompressionStream", "CountQueuingStrategy", "CryptoKey", "DecompressionStream", "EventSource", "FormData", "Navigator", "ReadableByteStreamController", "ReadableStreamBYOBReader", "ReadableStreamBYOBRequest", "Storage", "SubtleCrypto", "TextEncoderStream", "TransformStreamDefaultController", "URLPattern", "WebAssembly", "WebSocket", "WritableStream", "WritableStreamDefaultController", "WritableStreamDefaultWriter"], "evidence": ["tests/harness/web-globals.test.ts"] } diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index d661b6da9..747c2b9c5 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -14,8 +14,8 @@ "implement": 81 }, "dynamic": { - "audit-partial": 816, - "verify-gap": 2222, + "audit-partial": 828, + "verify-gap": 2210, "replace-refusal": 112, "implement": 218 } @@ -45956,7 +45956,7 @@ "action": "audit-partial", "verification": "test-backed", "confidence": "high", - "source": "island-npm-global:clearImmediate", + "source": "island-global:clearImmediate", "tests": [ "tests/harness/web-globals.test.ts" ] @@ -46144,7 +46144,7 @@ "action": "audit-partial", "verification": "test-backed", "confidence": "high", - "source": "island-npm-global:setImmediate", + "source": "island-global:setImmediate", "tests": [ "tests/harness/web-globals.test.ts" ] @@ -86829,9 +86829,10 @@ "action": "audit-partial", "verification": "test-backed", "confidence": "high", - "source": "island-export:node:timers/promises.setTimeout", + "source": "island-feature:timers.timersPromises.setTimeout", "tests": [ - "tests/fixtures/npm/cases/misc-shims/main.ts" + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" ] } } @@ -86869,9 +86870,10 @@ "action": "audit-partial", "verification": "test-backed", "confidence": "high", - "source": "island-export:node:timers/promises.setImmediate", + "source": "island-feature:timers.timersPromises.setImmediate", "tests": [ - "tests/fixtures/npm/cases/misc-shims/main.ts" + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" ] } } @@ -86909,9 +86911,10 @@ "action": "audit-partial", "verification": "test-backed", "confidence": "high", - "source": "island-export:node:timers/promises.setInterval", + "source": "island-feature:timers.timersPromises.setInterval", "tests": [ - "tests/fixtures/npm/cases/misc-shims/main.ts" + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" ] } } @@ -86940,12 +86943,15 @@ "tests": [] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.timersPromises.scheduler.wait", + "tests": [ + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -86973,12 +86979,15 @@ "tests": [] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.timersPromises.scheduler.yield", + "tests": [ + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87044,12 +87053,16 @@ ] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.immediate.hasRef", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87082,12 +87095,16 @@ ] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.immediate.ref", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87120,12 +87137,16 @@ ] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.immediate.unref", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87155,12 +87176,16 @@ ] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.immediate", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87221,12 +87246,16 @@ "tests": [] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.timeout.close", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87259,12 +87288,16 @@ ] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.timeout.hasRef", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87297,12 +87330,16 @@ ] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.timeout.ref", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87335,12 +87372,16 @@ ] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.timeout.refresh", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87373,12 +87414,16 @@ ] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.timeout.unref", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, @@ -87445,12 +87490,16 @@ ] }, "dynamic": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "island-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "island-feature:timers.timeout", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 737bbef7c..3191315df 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1188,9 +1188,9 @@ }, "dynamic": { "supported": 0, - "partial": 10, + "partial": 22, "refused": 0, - "not-implemented": 15, + "not-implemented": 3, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -61878,7 +61878,7 @@ }, "dynamic": { "status": "partial", - "evidence": "island-npm-global:clearImmediate", + "evidence": "island-global:clearImmediate", "tests": [ "tests/harness/web-globals.test.ts" ] @@ -62043,7 +62043,7 @@ }, "dynamic": { "status": "partial", - "evidence": "island-npm-global:setImmediate", + "evidence": "island-global:setImmediate", "tests": [ "tests/harness/web-globals.test.ts" ] @@ -104044,7 +104044,10 @@ "status": "partial", "evidence": "derived:descendants", "tests": [ - "tests/fixtures/npm/cases/misc-shims/main.ts" + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/misc-shims/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" ] }, "anchorSource": "chapter" @@ -104375,9 +104378,10 @@ }, "dynamic": { "status": "partial", - "evidence": "island-export:node:timers/promises.setTimeout", + "evidence": "island-feature:timers.timersPromises.setTimeout", "tests": [ - "tests/fixtures/npm/cases/misc-shims/main.ts" + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" ] }, "anchorSource": "exact" @@ -104411,9 +104415,10 @@ }, "dynamic": { "status": "partial", - "evidence": "island-export:node:timers/promises.setImmediate", + "evidence": "island-feature:timers.timersPromises.setImmediate", "tests": [ - "tests/fixtures/npm/cases/misc-shims/main.ts" + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" ] }, "anchorSource": "exact" @@ -104447,9 +104452,10 @@ }, "dynamic": { "status": "partial", - "evidence": "island-export:node:timers/promises.setInterval", + "evidence": "island-feature:timers.timersPromises.setInterval", "tests": [ - "tests/fixtures/npm/cases/misc-shims/main.ts" + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" ] }, "anchorSource": "exact" @@ -104476,8 +104482,12 @@ "evidence": "compiler-unmatched:timers" }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.timersPromises.scheduler.wait", + "tests": [ + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104503,8 +104513,12 @@ "evidence": "compiler-unmatched:timers" }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.timersPromises.scheduler.yield", + "tests": [ + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104563,8 +104577,13 @@ ] }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.immediate.hasRef", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104596,8 +104615,13 @@ ] }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.immediate.ref", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104629,8 +104653,13 @@ ] }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.immediate.unref", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104659,8 +104688,13 @@ ] }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.immediate", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104713,8 +104747,13 @@ "evidence": "compiler-unmatched:timers" }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.timeout.close", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104746,8 +104785,13 @@ ] }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.timeout.hasRef", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104779,8 +104823,13 @@ ] }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.timeout.ref", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104812,8 +104861,13 @@ ] }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.timeout.refresh", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104845,8 +104899,13 @@ ] }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.timeout.unref", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, @@ -104907,8 +104966,13 @@ ] }, "dynamic": { - "status": "not-implemented", - "evidence": "island-unmatched:timers" + "status": "partial", + "evidence": "island-feature:timers.timeout", + "tests": [ + "tests/fixtures/npm/cases/island-timers/main.ts", + "tests/fixtures/npm/cases/node24-promise-timers/main.ts", + "tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 3da1af5e8..7eb75c51a 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -678,6 +678,8 @@ function publicDetail(tier) { return "No dynamic-island implementation exists for this Node API family yet."; } if (source.startsWith("island-feature:")) { + if (source.startsWith("island-feature:timers.timersPromises.")) return "Provides cancellable promise timers, buffered interval iteration, scheduler waits and yields, and ref options over the native event loop; delay warnings remain outside this subset."; + if (source.startsWith("island-feature:timers.")) return "Provides timer and immediate handles with reference control, refresh, cancellation, and disposal over the native event loop."; if (source === "island-feature:events.events.once") return "Waits for EventEmitter or EventTarget events with AbortSignal cancellation, Node abort errors, and listener cleanup."; if (source === "island-feature:events.events.on") return "Iterates EventEmitter or EventTarget events with buffered delivery, close events, cancellation, watermark backpressure, and listener cleanup."; if (source === "island-feature:events.events.addAbortListener") return "Registers disposable abort listeners that run even when ordinary propagation is stopped; already-aborted signals schedule the callback in a microtask."; diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 6cb4f3d91..456cdfffb 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -13426,6 +13426,18 @@ "/tests/fixtures/npm/cases/node24-async-events/main.ts" ], "diags": [] + }, + "/tests/fixtures/npm/cases/node24-promise-timers/main.ts": { + "order": [ + "/tests/fixtures/npm/cases/node24-promise-timers/main.ts" + ], + "diags": [] + }, + "/tests/fixtures/npm/cases/node24-timers-unref/main.ts": { + "order": [ + "/tests/fixtures/npm/cases/node24-timers-unref/main.ts" + ], + "diags": [] } } } diff --git a/packages/runtime/src/scr_island.c b/packages/runtime/src/scr_island.c index e6622a99b..27f818f9f 100644 --- a/packages/runtime/src/scr_island.c +++ b/packages/runtime/src/scr_island.c @@ -9459,9 +9459,8 @@ static const char isl_modules_bootstrap[] = " c.default = c;\n" " return c;\n" " });\n" - /* node:timers (+ timers/promises) over the island's timer - * bridge; setImmediate rides a zero-delay timer (Node's check - * phase does not exist here — documented divergence). */ + /* node:timers and timers/promises share the native timer heap and + * immediate queue through the web prelude's handle objects. */ " builtins.timers = memo(() => {\n" " const t = {\n" " setTimeout: globalThis.setTimeout,\n" @@ -9475,30 +9474,94 @@ static const char isl_modules_bootstrap[] = " return t;\n" " });\n" " builtins['timers/promises'] = memo(() => {\n" - " const delay = (ms, value, options) => new Promise((resolve, reject) => {\n" - " const t = globalThis.setTimeout(() => resolve(value), ms);\n" - " if (options !== undefined && options !== null && options.signal !== undefined && typeof options.signal.addEventListener === 'function') {\n" - " options.signal.addEventListener('abort', () => {\n" - " globalThis.clearTimeout(t);\n" - " const e = new Error('The operation was aborted');\n" - " e.name = 'AbortError';\n" - " e.code = 'ABORT_ERR';\n" - " reject(e);\n" - " });\n" + " const resistant = Symbol.for('scriptc.events.resistStopPropagation');\n" + " const checkDelay = (delay) => {\n" + " if (delay !== undefined && typeof delay !== 'number') envArgumentError('delay', 'of type number', delay);\n" + " };\n" + " const checkOptions = (options) => {\n" + " if (options === null || typeof options !== 'object' || Array.isArray(options)) envArgumentError('options', 'of type object', options);\n" + " if (options.signal !== undefined) {\n" + " const signal = options.signal;\n" + " if (signal === null || typeof signal !== 'object' || !('aborted' in signal)) envArgumentError('options.signal', 'an instance of AbortSignal', signal);\n" " }\n" - " });\n" + " if (options.ref !== undefined) {\n" + " const ref = options.ref;\n" + " if (typeof ref !== 'boolean') envArgumentError('options.ref', 'of type boolean', ref);\n" + " }\n" + " const { signal, ref = true } = options;\n" + " return { signal, ref };\n" + " };\n" + " const abortError = (signal) => {\n" + " const error = new Error('The operation was aborted', { cause: signal.reason });\n" + " error.name = 'AbortError'; error.code = 'ABORT_ERR';\n" + " return error;\n" + " };\n" + " const wait = (immediate, delay, value, options) => {\n" + " let signal, ref;\n" + " try {\n" + " if (!immediate) checkDelay(delay);\n" + " ({ signal, ref } = checkOptions(options));\n" + " } catch (error) { return Promise.reject(error); }\n" + " if (signal && signal.aborted) return Promise.reject(abortError(signal));\n" + " let timer, onCancel;\n" + " const promise = new Promise((resolve, reject) => {\n" + " timer = immediate ? globalThis.setImmediate(resolve, value) : globalThis.setTimeout(resolve, delay, value);\n" + " if (!ref) timer.unref();\n" + " if (signal) {\n" + " onCancel = () => {\n" + " if (!timer._destroyed) {\n" + " if (immediate) globalThis.clearImmediate(timer); else globalThis.clearTimeout(timer);\n" + " reject(abortError(signal));\n" + " }\n" + " };\n" + " signal.addEventListener('abort', onCancel, { __proto__: null, [resistant]: true });\n" + " }\n" + " });\n" + " return onCancel ? promise.finally(() => signal.removeEventListener('abort', onCancel)) : promise;\n" + " };\n" + " function setTimeout(delay, value, options = {}) { return wait(false, delay, value, options); }\n" + " function setImmediate(value, options = {}) { return wait(true, undefined, value, options); }\n" + " async function* setInterval(delay, value, options = {}) {\n" + " checkDelay(delay);\n" + " const { signal, ref } = checkOptions(options);\n" + " if (signal && signal.aborted) throw abortError(signal);\n" + " let timer, onCancel;\n" + " try {\n" + " let notYielded = 0, callback;\n" + " timer = globalThis.setInterval(() => {\n" + " notYielded++;\n" + " if (callback) { callback(); callback = undefined; }\n" + " }, delay);\n" + " if (!ref) timer.unref();\n" + " if (signal) {\n" + " onCancel = () => {\n" + " globalThis.clearInterval(timer);\n" + " if (callback) { callback(Promise.reject(abortError(signal))); callback = undefined; }\n" + " };\n" + " signal.addEventListener('abort', onCancel, { __proto__: null, once: true, [resistant]: true });\n" + " }\n" + " while (!(signal && signal.aborted)) {\n" + " if (notYielded === 0) await new Promise(resolve => callback = resolve);\n" + " for (; notYielded > 0; notYielded--) yield value;\n" + " }\n" + " throw abortError(signal);\n" + " } finally {\n" + " globalThis.clearInterval(timer);\n" + " if (signal) signal.removeEventListener('abort', onCancel);\n" + " }\n" + " }\n" + " const schedulerBrand = Symbol('kScheduler');\n" + " const checkScheduler = (receiver) => {\n" + " if (!receiver[schedulerBrand]) { const error = new TypeError('Value of \"this\" must be of type Scheduler'); error.code = 'ERR_INVALID_THIS'; throw error; }\n" + " };\n" + " class Scheduler {\n" + " constructor() { const error = new TypeError('Illegal constructor'); error.code = 'ERR_ILLEGAL_CONSTRUCTOR'; throw error; }\n" + " yield() { checkScheduler(this); return setImmediate(); }\n" + " wait(delay, options) { checkScheduler(this); return setTimeout(delay, undefined, options); }\n" + " }\n" + " const scheduler = Object.create(Scheduler.prototype); scheduler[schedulerBrand] = true;\n" " const tp = {\n" - " setTimeout: delay,\n" - " setImmediate: (value) => delay(0, value),\n" - " setInterval: (ms, value, options) => ({\n" - " async *[Symbol.asyncIterator]() {\n" - " for (;;) {\n" - " await delay(ms, undefined, options);\n" - " yield value;\n" - " }\n" - " },\n" - " }),\n" - " scheduler: { wait: (ms) => delay(ms), yield: () => delay(0) },\n" + " setTimeout, setImmediate, setInterval, scheduler,\n" " };\n" " tp.default = tp;\n" " return tp;\n" @@ -10050,15 +10113,9 @@ static const char isl_modules_bootstrap[] = /* Node's Buffer global (and `global` itself) — embedded CJS * reaches both without requiring anything. */ " globalThis.Buffer = builtins.buffer().Buffer;\n" - /* setImmediate/clearImmediate globals (zero-delay timers — no - * check phase in the island; documented divergence), and the - * global console upgraded to Node's util.format semantics for + /* The global console upgraded to Node's util.format semantics for * npm builds (the web prelude's String() console stays for * non-npm islands). */ - " if (globalThis.setImmediate === undefined) {\n" - " globalThis.setImmediate = (fn, ...args) => globalThis.setTimeout(fn, 0, ...args);\n" - " globalThis.clearImmediate = (t) => globalThis.clearTimeout(t);\n" - " }\n" " {\n" " const fmt = (...a) => builtins.util().formatWithOptions({}, ...a);\n" " const to = (fd, prefix) => (...a) => { host.write(fd, (prefix || '') + fmt(...a) + '\\n'); };\n" diff --git a/packages/runtime/src/scr_web.c b/packages/runtime/src/scr_web.c index 9751f1b66..73f619511 100644 --- a/packages/runtime/src/scr_web.c +++ b/packages/runtime/src/scr_web.c @@ -62,43 +62,77 @@ static const char web_prelude[] = " * (host.setTimer/host.clearTimer) — REF'd like Node's (an armed\n" " * timer keeps the process alive), FIFO-ordered against static\n" " * timers on one heap, Node's <1ms clamp. Returns a Timeout-shaped\n" - " * object (ref/unref/refresh/close, numeric via toPrimitive) that\n" - " * clearTimeout/clearInterval accept alongside plain ids; unref is\n" - " * accepted but not honored (the entry stays ref'd — a documented\n" - " * approximation). */\n" + " * object with real ref/unref/refresh/close operations. Immediates\n" + " * use the native check queue, independently of timer ids. */\n" + " const timers = new Map();\n" " class Timeout {\n" - " constructor(fn, delay, repeat) {\n" + " constructor(fn, delay, args, repeat) {\n" " this._fn = fn;\n" " this._delay = delay;\n" + " this._args = args;\n" " this._repeat = repeat;\n" - " this._id = host.setTimer(fn, delay, repeat);\n" + " this._reffed = true;\n" + " this._closed = false;\n" + " this._callback = () => {\n" + " if (!this._repeat) { this._destroyed = true; timers.delete(this._id); }\n" + " this._fn.apply(this, this._args);\n" + " };\n" + " this._arm();\n" " }\n" - " ref() { return this; }\n" - " unref() { return this; }\n" - " hasRef() { return true; }\n" + " _arm() {\n" + " this._destroyed = false;\n" + " this._id = host.setTimer(this._callback, this._delay, this._repeat, false);\n" + " timers.set(this._id, this);\n" + " if (!this._reffed) host.refTimer(this._id, false, false);\n" + " }\n" + " ref() { this._reffed = true; host.refTimer(this._id, true, false); return this; }\n" + " unref() { this._reffed = false; host.refTimer(this._id, false, false); return this; }\n" + " hasRef() { return this._reffed; }\n" " refresh() {\n" - " host.clearTimer(this._id);\n" - " this._id = host.setTimer(this._fn, this._delay, this._repeat);\n" + " if (this._closed) return this;\n" + " if (this._destroyed) this._arm();\n" + " else host.refreshTimer(this._id);\n" " return this;\n" " }\n" - " close() { host.clearTimer(this._id); return this; }\n" + " close() { this._closed = true; this._destroyed = true; timers.delete(this._id); host.clearTimer(this._id, false); return this; }\n" + " [Symbol.dispose]() { this.close(); }\n" " [Symbol.toPrimitive]() { return this._id; }\n" " }\n" " const mkTimer = (fn, ms, args, repeat) => {\n" " if (typeof fn !== 'function') {\n" " throw new TypeError('The \"callback\" argument must be of type function. Received type ' + typeof fn);\n" " }\n" - " const cb = args.length === 0 ? fn : () => fn(...args);\n" - " return new Timeout(cb, Number(ms), repeat);\n" + " return new Timeout(fn, Number(ms), args, repeat);\n" " };\n" " g.setTimeout = (fn, ms, ...args) => mkTimer(fn, ms, args, false);\n" " g.setInterval = (fn, ms, ...args) => mkTimer(fn, ms, args, true);\n" " g.clearTimeout = (t) => {\n" " if (t === undefined || t === null) return;\n" " const id = typeof t === 'number' ? t : Number(t);\n" - " if (Number.isFinite(id)) host.clearTimer(id);\n" + " if (Number.isFinite(id)) {\n" + " const timer = timers.get(id);\n" + " if (timer) timer.close(); else host.clearTimer(id, false);\n" + " }\n" " };\n" " g.clearInterval = g.clearTimeout;\n" + " class Immediate {\n" + " constructor(fn, args) {\n" + " this._reffed = true; this._destroyed = false;\n" + " this._id = host.setTimer(() => {\n" + " this._destroyed = true; this._reffed = false; fn.apply(this, args);\n" + " }, 0, false, true);\n" + " }\n" + " ref() { if (!this._destroyed) { this._reffed = true; host.refTimer(this._id, true, true); } return this; }\n" + " unref() { if (!this._destroyed) { this._reffed = false; host.refTimer(this._id, false, true); } return this; }\n" + " hasRef() { return this._reffed; }\n" + " _close() { this._destroyed = true; this._reffed = false; host.clearTimer(this._id, true); }\n" + " [Symbol.dispose]() { this._close(); }\n" + " }\n" + " g.setImmediate = (fn, ...args) => {\n" + " if (typeof fn !== 'function') throw new TypeError('The \"callback\" argument must be of type function. Received type ' + typeof fn);\n" + " return new Immediate(fn, args);\n" + " };\n" + " g.clearImmediate = (immediate) => { if (immediate instanceof Immediate) immediate._close(); };\n" "\n" " class ReadableStream {\n" " constructor(source, _strategy) {\n" @@ -1673,7 +1707,6 @@ static void web_timer_fire_cb(ScrClosure *env) { static JSValue web_host_set_timer(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; - (void)argc; double ms = 0; if (JS_ToFloat64(ctx, &ms, argv[1])) return JS_EXCEPTION; bool repeat = JS_ToBool(ctx, argv[2]) > 0; @@ -1689,17 +1722,45 @@ static JSValue web_host_set_timer(JSContext *ctx, JSValueConst this_val, scr_box_set_ref(box, h); /* the box owns the +1 */ ScrClosure *cb = scr_closure_new((void *)web_timer_fire_cb, 1); cb->caps[0] = box; - double id = repeat ? scr_set_interval(cb, ms) : scr_set_timeout_handle(cb, ms); + bool immediate = argc > 3 && JS_ToBool(ctx, argv[3]) > 0; + double id = immediate ? scr_set_immediate(cb) + : repeat ? scr_set_interval(cb, ms) : scr_set_timeout_handle(cb, ms); return JS_NewFloat64(ctx, id); } static JSValue web_host_clear_timer(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; + double id = 0; + if (JS_ToFloat64(ctx, &id, argv[0])) return JS_EXCEPTION; + if (argc > 1 && JS_ToBool(ctx, argv[1]) > 0) scr_clear_immediate(id); + else scr_clear_interval(id); + return JS_UNDEFINED; +} + +static JSValue web_host_ref_timer(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + (void)this_val; (void)argc; double id = 0; if (JS_ToFloat64(ctx, &id, argv[0])) return JS_EXCEPTION; - scr_clear_interval(id); + bool reffed = JS_ToBool(ctx, argv[1]) > 0; + bool immediate = JS_ToBool(ctx, argv[2]) > 0; + if (immediate) { + if (reffed) scr_immediate_ref(id); else scr_immediate_unref(id); + } else { + if (reffed) scr_timer_ref(id); else scr_timer_unref(id); + } + return JS_UNDEFINED; +} + +static JSValue web_host_refresh_timer(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + (void)this_val; + (void)argc; + double id = 0; + if (JS_ToFloat64(ctx, &id, argv[0])) return JS_EXCEPTION; + scr_timer_refresh(id); return JS_UNDEFINED; } @@ -1877,8 +1938,10 @@ void scr_island_web_boot(void *jsctx) { JS_SetPropertyStr(ctx, host, "write", JS_NewCFunction(ctx, web_host_write, "write", 2)); JS_SetPropertyStr(ctx, host, "tzname", JS_NewCFunction(ctx, web_host_tzname, "tzname", 1)); JS_SetPropertyStr(ctx, host, "timer", JS_NewCFunction(ctx, web_host_timer, "timer", 2)); - JS_SetPropertyStr(ctx, host, "setTimer", JS_NewCFunction(ctx, web_host_set_timer, "setTimer", 3)); - JS_SetPropertyStr(ctx, host, "clearTimer", JS_NewCFunction(ctx, web_host_clear_timer, "clearTimer", 1)); + JS_SetPropertyStr(ctx, host, "setTimer", JS_NewCFunction(ctx, web_host_set_timer, "setTimer", 4)); + JS_SetPropertyStr(ctx, host, "clearTimer", JS_NewCFunction(ctx, web_host_clear_timer, "clearTimer", 2)); + JS_SetPropertyStr(ctx, host, "refTimer", JS_NewCFunction(ctx, web_host_ref_timer, "refTimer", 3)); + JS_SetPropertyStr(ctx, host, "refreshTimer", JS_NewCFunction(ctx, web_host_refresh_timer, "refreshTimer", 1)); JSValue r = JS_Call(ctx, fn, JS_UNDEFINED, 1, (JSValueConst *)&host); JS_FreeValue(ctx, host); JS_FreeValue(ctx, fn); diff --git a/tests/fixtures/npm/cases/node24-promise-timers/main.ts b/tests/fixtures/npm/cases/node24-promise-timers/main.ts new file mode 100644 index 000000000..1fae75104 --- /dev/null +++ b/tests/fixtures/npm/cases/node24-promise-timers/main.ts @@ -0,0 +1,4 @@ +// @dynamic +// @stderr +import { report } from "node24-promise-timers-fixture"; +await report(); diff --git a/tests/fixtures/npm/cases/node24-promise-timers/package.json b/tests/fixtures/npm/cases/node24-promise-timers/package.json new file mode 100644 index 000000000..089153bcb --- /dev/null +++ b/tests/fixtures/npm/cases/node24-promise-timers/package.json @@ -0,0 +1 @@ +{"type":"module"} diff --git a/tests/fixtures/npm/cases/node24-timers-unref/main.ts b/tests/fixtures/npm/cases/node24-timers-unref/main.ts new file mode 100644 index 000000000..23159b40c --- /dev/null +++ b/tests/fixtures/npm/cases/node24-timers-unref/main.ts @@ -0,0 +1,4 @@ +// @dynamic +// @stderr +import { unref } from "node24-promise-timers-fixture"; +unref(); diff --git a/tests/fixtures/npm/cases/node24-timers-unref/package.json b/tests/fixtures/npm/cases/node24-timers-unref/package.json new file mode 100644 index 000000000..089153bcb --- /dev/null +++ b/tests/fixtures/npm/cases/node24-timers-unref/package.json @@ -0,0 +1 @@ +{"type":"module"} diff --git a/tests/fixtures/npm/node_modules/node24-promise-timers-fixture/index.d.ts b/tests/fixtures/npm/node_modules/node24-promise-timers-fixture/index.d.ts new file mode 100644 index 000000000..de5fbac50 --- /dev/null +++ b/tests/fixtures/npm/node_modules/node24-promise-timers-fixture/index.d.ts @@ -0,0 +1,2 @@ +export function report(): Promise; +export function unref(): void; diff --git a/tests/fixtures/npm/node_modules/node24-promise-timers-fixture/index.js b/tests/fixtures/npm/node_modules/node24-promise-timers-fixture/index.js new file mode 100644 index 000000000..647bb68b6 --- /dev/null +++ b/tests/fixtures/npm/node_modules/node24-promise-timers-fixture/index.js @@ -0,0 +1,165 @@ +import { setTimeout as delay, setImmediate as immediate, setInterval as interval, scheduler } from 'node:timers/promises'; +import timers from 'node:timers'; + +export async function report() { + const line = (name, value) => console.log(name, JSON.stringify(value)); + const shape = e => [e.name, e.code, e.message]; + const reject = async (name, fn) => { try { await fn(); line(name, 'no error'); } catch (e) { line(name, shape(e)); } }; + for (const value of [null, false, '5', {}, [], 1n, Symbol('delay')]) { + await reject('timeout delay', () => delay(value)); + const iterator = interval(value); + line('interval lazy validation', typeof iterator.next); + await reject('interval delay', () => iterator.next()); + await reject('scheduler delay', () => scheduler.wait(value)); + } + for (const value of [null, false, 3, 'bad', [], () => {}]) { + await reject('timeout options', () => delay(1, undefined, value)); + await reject('immediate options', () => immediate(undefined, value)); + await reject('interval options', () => interval(1, undefined, value).next()); + } + for (const key of ['signal', 'ref']) { + for (const value of [null, 0, 'bad', {}]) { + const options = { [key]: value }; + await reject('timeout ' + key, () => delay(1, undefined, options)); + await reject('immediate ' + key, () => immediate(undefined, options)); + await reject('interval ' + key, () => interval(1, undefined, options).next()); + } + } + { + let threw = false, promise; + try { promise = delay('bad'); } catch { threw = true; } + line('invalid returns rejection', [threw, promise instanceof Promise]); + await promise.catch(() => {}); + line('default values', [await delay(), await immediate(), await scheduler.wait(0), await scheduler.yield()]); + const value = { answer: 42 }; + line('timeout value identity', await delay(1.5, value) === value); + line('immediate value identity', await immediate(value) === value); + } + for (const kind of ['timeout', 'immediate', 'scheduler']) { + for (const preaborted of [false, true]) { + const ac = new AbortController(), cause = { why: 'cancelled' }; + ac.signal.addEventListener('abort', e => e.stopImmediatePropagation()); + if (preaborted) ac.abort(cause); + const promise = kind === 'timeout' ? delay(60000, 'unused', { signal: ac.signal }) + : kind === 'immediate' ? immediate('unused', { signal: ac.signal }) + : scheduler.wait(60000, { signal: ac.signal }); + if (!preaborted) ac.abort(cause); + try { await promise; } catch (e) { line('cancelled ' + kind, [preaborted, ...shape(e), e.cause === cause]); } + } + } + { + const ac = new AbortController(); ac.abort(); + try { await delay(60000, undefined, { signal: ac.signal }); } + catch (e) { line('default abort cause', [shape(e), e.cause === ac.signal.reason, e.cause.name]); } + } + { + const ac = new AbortController(); let removed = 0; + const original = ac.signal.removeEventListener.bind(ac.signal); + ac.signal.removeEventListener = (...args) => { removed++; return original(...args); }; + await delay(1, undefined, { signal: ac.signal }); + await immediate(undefined, { signal: ac.signal }); + await scheduler.wait(1, { signal: ac.signal }); + const iterator = interval(1, undefined, { signal: ac.signal }); await iterator.next(); await iterator.return(); + line('completed signal cleanup', removed); ac.abort('after completion'); + } + { + const ac = new AbortController(); let removed = 0; + const original = ac.signal.removeEventListener.bind(ac.signal); + ac.signal.removeEventListener = (...args) => { removed++; return original(...args); }; + const promise = delay(60000, undefined, { signal: ac.signal }); ac.abort('early'); + await promise.catch(() => {}); line('cancelled signal cleanup', removed); + } + { + const value = { tick: 'value' }, iterator = interval(1, value); + line('interval self', iterator[Symbol.asyncIterator]() === iterator); + line('interval identity', (await iterator.next()).value === value); + line('interval return', await iterator.return('finished')); + line('interval done', await iterator.next()); + let ticks = 0; for await (const value of interval(1, 'tick')) { if (value === 'tick') ticks++; if (ticks === 3) break; } + line('interval loop break', ticks); + } + for (const preaborted of [false, true]) { + const ac = new AbortController(), cause = { why: 'interval cancellation' }; + ac.signal.addEventListener('abort', e => e.stopImmediatePropagation()); + if (preaborted) ac.abort(cause); + const iterator = interval(60000, 'unused', { signal: ac.signal }); + const a = iterator.next().catch(e => [shape(e), e.cause === cause]), b = iterator.next(); + if (!preaborted) ac.abort(cause); + line('interval cancelled', [preaborted, await a, await b, await iterator.next()]); + } + { + const ac = new AbortController(), iterator = interval(5, 'buffered', { signal: ac.signal }); + await iterator.next(); await delay(100); ac.abort('buffered abort'); + line('interval drains buffered tick', await iterator.next()); await iterator.return(); + } + { + const iterator = interval(1, 'value'), failure = new Error('thrown'); + await iterator.next(); try { await iterator.throw(failure); } catch (e) { line('interval throw identity', e === failure); } + line('interval throw done', await iterator.next()); + const lazy = interval('invalid delay'); line('return before start', await lazy.return('skipped')); + } + { + const keep = setTimeout(() => {}, 60000); + line('unref timeout with live loop', await delay(1, 'value', { ref: false })); + line('unref immediate with live loop', await immediate('value', { ref: false })); + const iterator = interval(1, 'value', { ref: false }); line('unref interval with live loop', await iterator.next()); await iterator.return(); + await scheduler.wait(1, { ref: false }); line('unref scheduler with live loop', true); clearTimeout(keep); + } + { + const log = []; + await new Promise(resolve => setImmediate(() => { + setImmediate(() => log.push('first')); + queueMicrotask(() => log.push('microtask')); + setImmediate(() => { log.push('second'); setImmediate(() => { log.push('nested'); resolve(); }); }); + })); + line('immediate queue', log); + line('global module identity', [timers.setImmediate === setImmediate, timers.clearImmediate === clearImmediate]); + } + { + const timer = setTimeout(() => {}, 60000), id = +timer; + line('timeout ref control', [timer.hasRef(), timer.unref() === timer, timer.hasRef(), timer.ref() === timer, timer.hasRef()]); + timer.unref(); timer.refresh(); line('refresh preserves ref and identity', [timer.hasRef(), +timer === id]); + clearTimeout(String(id)); timer.refresh(); line('cleared timeout stays cleared', timer.hasRef()); + const disposable = setTimeout(() => line('disposed timer ran', true), 60000); + line('timeout disposal', disposable[Symbol.dispose]() === undefined); + const stopped = setInterval(() => line('disposed interval ran', true), 60000); stopped[Symbol.dispose](); + const fired = setTimeout(function(a, b) { line('timeout receiver', [this === fired, a, b]); }, 1, 'a', 'b'); + await delay(10); line('fired timeout ref', fired.hasRef()); + const refreshed = new Promise(resolve => { + let calls = 0; + const handle = setTimeout(() => { if (++calls === 1) handle.refresh(); else { line('refresh after firing', calls); resolve(); } }, 1); + }); await refreshed; + } + { + const handle = setImmediate(() => line('cleared immediate ran', true)); + line('immediate ref control', [handle.hasRef(), handle.unref() === handle, handle.hasRef(), handle.ref() === handle, handle.hasRef()]); + clearImmediate(handle); line('cleared immediate ref', handle.hasRef()); + line('immediate disposal', setImmediate(() => line('disposed immediate ran', true))[Symbol.dispose]() === undefined); + await new Promise(resolve => { + const fired = setImmediate(function(a, b) { line('immediate receiver', [this === fired, a, b, fired.hasRef()]); resolve(); }, 'a', 'b'); + }); + } + { + line('scheduler prototype', Object.getOwnPropertyNames(Object.getPrototypeOf(scheduler))); + await reject('scheduler constructor', () => new scheduler.constructor()); + await reject('scheduler wait receiver', () => scheduler.wait.call({}, 1)); + await reject('scheduler yield receiver', () => scheduler.yield.call({})); + const ac = new AbortController(); let signals = 0, refs = 0; + const options = { get signal() { signals++; return ac.signal; }, get ref() { refs++; return true; } }; + await delay(1, undefined, options); await immediate(undefined, options); + const iterator = interval(1, undefined, options); await iterator.next(); await iterator.return(); + line('option getters', [signals, refs]); + } +} + +export function unref() { + const unexpected = () => console.log('unreferenced timer fired'); + setTimeout(unexpected, 60000).unref(); + setInterval(unexpected, 60000).unref(); + setImmediate(unexpected).unref(); + delay(60000, undefined, { ref: false }).then(unexpected); + immediate(undefined, { ref: false }).then(unexpected); + interval(60000, undefined, { ref: false }).next().then(unexpected); + scheduler.wait(60000, { ref: false }).then(unexpected); + console.log('scheduled unreferenced timers'); +} diff --git a/tests/fixtures/npm/node_modules/node24-promise-timers-fixture/package.json b/tests/fixtures/npm/node_modules/node24-promise-timers-fixture/package.json new file mode 100644 index 000000000..f5d3b201b --- /dev/null +++ b/tests/fixtures/npm/node_modules/node24-promise-timers-fixture/package.json @@ -0,0 +1 @@ +{"name":"node24-promise-timers-fixture","version":"1.0.0","type":"module","main":"index.js","types":"index.d.ts"} diff --git a/tests/harness/web-globals.test.ts b/tests/harness/web-globals.test.ts index 47d1fc3f6..425e19a76 100644 --- a/tests/harness/web-globals.test.ts +++ b/tests/harness/web-globals.test.ts @@ -119,12 +119,12 @@ describe(`island web globals (scriptc-only${sanitize ? ", sanitized" : ""})`, () // Event and buffer.Blob at LOAD); structuredClone joined with the // globals lane (the HTML StructuredSerialize subset, cycles // included); WritableStream/FormData/WebSocket stay fenced by - // absence. URL, Buffer, and the immediate-timer globals are installed - // only when the embedded npm module bootstrap runs. + // absence. URL and Buffer are installed only when the embedded npm + // module bootstrap runs; immediate timers use the native check queue. expect(out).toBe( "function function function function function function function function function " + "object object function function function function function function " + - "undefined function undefined undefined undefined undefined undefined undefined", + "undefined function undefined undefined undefined undefined function function", ); });