diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 4276802cd..924769a51 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": "d94b8161bbee649b9890", + "artifactVersion": "ad22ef7f9c0a3e2d2f5a", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 7f62476e4..4ddb1bb58 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -298,10 +298,10 @@ "entries": 78, "apiEntries": 78, "static": { - "supported": 0, - "partial": 22, + "supported": 18, + "partial": 34, "refused": 0, - "not-implemented": 56, + "not-implemented": 26, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -26433,9 +26433,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26456,9 +26456,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26502,9 +26502,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26525,9 +26525,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26548,9 +26548,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26571,9 +26571,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26594,9 +26594,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26617,9 +26617,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26640,9 +26640,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26663,9 +26663,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26686,9 +26686,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26709,9 +26709,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26732,9 +26732,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26755,9 +26755,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26778,9 +26778,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26801,9 +26801,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26824,9 +26824,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26847,9 +26847,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26870,9 +26870,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26893,9 +26893,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26916,9 +26916,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26939,9 +26939,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26962,9 +26962,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -26985,9 +26985,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -27008,9 +27008,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -27031,9 +27031,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -27054,9 +27054,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -27077,9 +27077,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -28066,9 +28066,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -28112,9 +28112,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 55c256e1f..195978dad 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -3,21 +3,21 @@ "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", "summary": { - "taskCount": 3288, - "tierItemCount": 3288, + "taskCount": 3270, + "tierItemCount": 3270, "tiers": { "static": { "replace-refusal": 40, - "verify-gap": 2384, - "audit-partial": 782, + "verify-gap": 2354, + "audit-partial": 794, "classify": 2, "implement": 80 } }, "priorities": { - "high": 2239, + "high": 2225, "normal": 673, - "low": 376 + "low": 372 } }, "tasks": [ @@ -22999,12 +22999,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.EventTarget", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] } } }, @@ -23024,12 +23030,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.EventTarget", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] } } }, @@ -23074,12 +23086,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.EventTarget", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] } } }, @@ -23099,462 +23117,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:e5c069095e5c", - "chapter": "events", - "label": "event.composedPath()", - "signature": "`event.composedPath()`", - "apiSymbol": "event.composedPath", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#eventcomposedpath", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:85436cb5ea2b", - "chapter": "events", - "label": "event.initEvent(type[, bubbles[, cancelable]])", - "signature": "`event.initEvent(type[, bubbles[, cancelable]])`", - "apiSymbol": "event.initEvent", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#eventiniteventtype-bubbles-cancelable", - "nodeStability": { - "index": "3", - "level": 3, - "text": "Legacy: The WHATWG spec considers it deprecated and users shouldn't use it at all.", - "inherited": false - }, - "priority": "low", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:5848ab30b2e5", - "chapter": "events", - "label": "event.preventDefault()", - "signature": "`event.preventDefault()`", - "apiSymbol": "event.preventDefault", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#eventpreventdefault", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:c16743d778c4", - "chapter": "events", - "label": "event.stopImmediatePropagation()", - "signature": "`event.stopImmediatePropagation()`", - "apiSymbol": "event.stopImmediatePropagation", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#eventstopimmediatepropagation", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:9bab844aa1c2", - "chapter": "events", - "label": "event.stopPropagation()", - "signature": "`event.stopPropagation()`", - "apiSymbol": "event.stopPropagation", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#eventstoppropagation", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:abe0ea02a52c", - "chapter": "events", - "label": "bubbles", - "signature": "`bubbles` Type: {boolean} Always returns `false`.", - "apiSymbol": "bubbles", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:44e2dc361428", - "chapter": "events", - "label": "cancelBubble", - "signature": "`cancelBubble` Type: {boolean}", - "apiSymbol": "cancelBubble", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "3", - "level": 3, - "text": "Legacy: Use [`event.stopPropagation()`][] instead.", - "inherited": false - }, - "priority": "low", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:51ee094d86d3", - "chapter": "events", - "label": "cancelable", - "signature": "`cancelable` Type: {boolean} True if the event was created with the `cancelable` option.", - "apiSymbol": "cancelable", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:369be877d737", - "chapter": "events", - "label": "composed", - "signature": "`composed` Type: {boolean} Always returns `false`.", - "apiSymbol": "composed", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:4e0c88e53f49", - "chapter": "events", - "label": "currentTarget", - "signature": "`currentTarget` Type: {EventTarget} The `EventTarget` dispatching the event.", - "apiSymbol": "currentTarget", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:1e8421d3cc04", - "chapter": "events", - "label": "defaultPrevented", - "signature": "`defaultPrevented` Type: {boolean}", - "apiSymbol": "defaultPrevented", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:602700d21dcf", - "chapter": "events", - "label": "eventPhase", - "signature": "`eventPhase` Type: {number} Returns `0` while an event is not being dispatched, `2` while it is being dispatched.", - "apiSymbol": "eventPhase", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:4e83ca3c5e4c", - "chapter": "events", - "label": "isTrusted", - "signature": "`isTrusted` Type: {boolean}", - "apiSymbol": "isTrusted", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:100e8d286de2", - "chapter": "events", - "label": "returnValue", - "signature": "`returnValue` Type: {boolean} True if the event has not been canceled.", - "apiSymbol": "returnValue", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "3", - "level": 3, - "text": "Legacy: Use [`event.defaultPrevented`][] instead.", - "inherited": false - }, - "priority": "low", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:3d3da3858847", - "chapter": "events", - "label": "srcElement", - "signature": "`srcElement` Type: {EventTarget} The `EventTarget` dispatching the event.", - "apiSymbol": "srcElement", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "3", - "level": 3, - "text": "Legacy: Use [`event.target`][] instead.", - "inherited": false - }, - "priority": "low", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:fdfee26f8c22", - "chapter": "events", - "label": "target", - "signature": "`target` Type: {EventTarget} The `EventTarget` dispatching the event.", - "apiSymbol": "target", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:051e03ef86de", - "chapter": "events", - "label": "timeStamp", - "signature": "`timeStamp` Type: {number}", - "apiSymbol": "timeStamp", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] - } - } - }, - { - "id": "events:88b512802b39", - "chapter": "events", - "label": "type", - "signature": "`type` Type: {string}", - "apiSymbol": "type", - "docsUrl": "https://nodejs.org/docs/v24.15.0/api/events.html#class-event", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.Event", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts" + ] } } }, @@ -23574,12 +23145,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.EventTarget", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] } } }, @@ -23599,12 +23176,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.eventTarget.addEventListener", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] } } }, @@ -23624,12 +23207,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.eventTarget.dispatchEvent", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] } } }, @@ -23649,12 +23238,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.eventTarget.removeEventListener", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] } } }, @@ -23674,12 +23269,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.CustomEvent", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts" + ] } } }, @@ -23699,12 +23297,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.detail", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts" + ] } } }, @@ -24837,12 +24438,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.events.addAbortListener", + "tests": [ + "tests/corpus/5133-abort-signal-event-target.ts" + ] } } }, @@ -24887,12 +24490,14 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:events", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:events.events.errorMonitor", + "tests": [ + "tests/corpus/5134-events-error-monitor.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 600d7a1db..0f55c0681 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -310,10 +310,10 @@ "entries": 84, "apiEntries": 78, "static": { - "supported": 0, - "partial": 22, + "supported": 18, + "partial": 34, "refused": 0, - "not-implemented": 56, + "not-implemented": 26, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -40005,7 +40005,13 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/1677-emitter-listeners.ts", - "tests/corpus/2620-ee-override-once-order.ts" + "tests/corpus/2620-ee-override-once-order.ts", + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5134-events-error-monitor.ts", + "tests/corpus/5135-event-target-warnings.js" ] }, "anchorSource": "chapter" @@ -40143,8 +40149,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.EventTarget", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] }, "anchorSource": "exact" }, @@ -40166,8 +40179,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.EventTarget", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] }, "anchorSource": "exact" }, @@ -40235,8 +40255,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.EventTarget", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] }, "anchorSource": "exact" }, @@ -40258,8 +40285,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.Event", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts" + ] }, "anchorSource": "exact" }, @@ -40281,8 +40312,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.event.composedPath", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "exact" }, @@ -40304,8 +40340,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.event.initEvent", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "exact" }, @@ -40327,8 +40368,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.event.preventDefault", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "exact" }, @@ -40350,8 +40396,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.event.stopImmediatePropagation", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "exact" }, @@ -40373,8 +40424,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.event.stopPropagation", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "exact" }, @@ -40396,8 +40452,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.bubbles", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40419,8 +40480,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.cancelBubble", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40442,8 +40508,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.cancelable", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40465,8 +40536,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.composed", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40488,8 +40564,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.currentTarget", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40511,8 +40592,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.defaultPrevented", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40534,8 +40620,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.eventPhase", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40557,8 +40648,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.isTrusted", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40580,8 +40676,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.returnValue", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40603,8 +40704,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.srcElement", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40626,8 +40732,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.target", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40649,8 +40760,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.timeStamp", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40672,8 +40788,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "supported", + "evidence": "compiler-feature:events.type", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "ancestor" }, @@ -40695,8 +40816,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.EventTarget", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] }, "anchorSource": "exact" }, @@ -40718,8 +40846,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.eventTarget.addEventListener", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] }, "anchorSource": "exact" }, @@ -40741,8 +40876,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.eventTarget.dispatchEvent", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] }, "anchorSource": "exact" }, @@ -40764,8 +40906,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.eventTarget.removeEventListener", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5131-event-target-listener-errors.js", + "tests/corpus/5132-event-target-typed.ts", + "tests/corpus/5133-abort-signal-event-target.ts", + "tests/corpus/5135-event-target-warnings.js" + ] }, "anchorSource": "exact" }, @@ -40787,8 +40936,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.CustomEvent", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts" + ] }, "anchorSource": "exact" }, @@ -40810,8 +40963,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.detail", + "tests": [ + "tests/corpus/5130-event-target-dispatch.js", + "tests/corpus/5132-event-target-typed.ts" + ] }, "anchorSource": "ancestor" }, @@ -41906,8 +42063,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.events.addAbortListener", + "tests": [ + "tests/corpus/5133-abort-signal-event-target.ts" + ] }, "anchorSource": "exact" }, @@ -41952,8 +42112,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:events" + "status": "partial", + "evidence": "compiler-feature:events.events.errorMonitor", + "tests": [ + "tests/corpus/5134-events-error-monitor.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 8a1969d6f..f0bc015fd 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -138,6 +138,12 @@ }, "features": [ { "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] }, + {"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js"]}, + {"chapter": "events", "symbols": ["Event"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts"]}, + {"chapter": "events", "symbols": ["event.composedPath", "event.preventDefault", "event.stopImmediatePropagation", "event.stopPropagation", "event.initEvent", "bubbles", "cancelable", "cancelBubble", "composed", "currentTarget", "defaultPrevented", "eventPhase", "isTrusted", "returnValue", "srcElement", "target", "timeStamp", "type"], "status": "supported", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts"]}, + {"chapter": "events", "symbols": ["CustomEvent", "detail"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts"]}, + {"chapter": "events", "symbols": ["events.errorMonitor"], "status": "partial", "evidence": ["tests/corpus/5134-events-error-monitor.ts"]}, + {"chapter": "events", "symbols": ["events.addAbortListener"], "status": "partial", "evidence": ["tests/corpus/5133-abort-signal-event-target.ts"]}, {"chapter": "worker_threads", "symbols": ["Worker"], "status": "partial", "evidence": ["tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", "tests/corpus/native-worker-roots/main.ts"]}, {"chapter": "worker_threads", "symbols": ["MessagePort", "parentPort", "worker_threads.workerData", "isMainThread", "threadId"], "status": "partial", "evidence": ["tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-references.ts"]}, {"chapter": "worker_threads", "symbols": ["worker.postMessage", "port.postMessage"], "status": "partial", "evidence": ["tests/corpus/native-worker-message-graphs.ts", "tests/corpus/native-worker-shared-views.ts", "tests/corpus/native-worker-clone-errors.ts", "tests/corpus/native-worker-delayed-messages.ts"]}, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index d533b2305..888139e0c 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -4469,16 +4469,20 @@ declare module "events" { class EventEmitter { constructor(); static defaultMaxListeners: number; - on(eventName: string, listener: (...args: any[]) => void): this; - addListener(eventName: string, listener: (...args: any[]) => void): this; - once(eventName: string, listener: (...args: any[]) => void): this; - prependListener(eventName: string, listener: (...args: any[]) => void): this; - prependOnceListener(eventName: string, listener: (...args: any[]) => void): this; - off(eventName: string, listener: (...args: any[]) => void): this; - removeListener(eventName: string, listener: (...args: any[]) => void): this; - removeAllListeners(eventName?: string): this; - emit(eventName: string, ...args: any[]): boolean; - listenerCount(eventName: string, listener?: (...args: any[]) => void): number; + /* Listeners registered under this symbol observe 'error' emits first + * (an emit with no 'error' listener still throws). */ + static readonly errorMonitor: unique symbol; + static addAbortListener(signal: AbortSignal, listener: (event: Event) => void): Disposable; + on(eventName: string | symbol, listener: (...args: any[]) => void): this; + addListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + once(eventName: string | symbol, listener: (...args: any[]) => void): this; + prependListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + prependOnceListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + off(eventName: string | symbol, listener: (...args: any[]) => void): this; + removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + removeAllListeners(eventName?: string | symbol): this; + emit(eventName: string | symbol, ...args: any[]): boolean; + listenerCount(eventName: string | symbol, listener?: (...args: any[]) => void): number; /* Declared as zero-parameter closures so the RESULT is statically * consumable (.length, indexing, identity): the lowering only admits * these calls for events whose argument tuple is empty — a listener diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index ba8a0a6d9..96cafbe18 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -13,6 +13,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { textDecoderLegacy: features.legacyTextDecoder, fileHandle: features.fileHandle, fetch: features.fetch, + ...(features.eventTarget ? { eventTarget: true } : {}), zlib: features.zlib, assert: features.assert, inspect: features.inspect, diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index ca5652cd2..586185dc7 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -397,6 +397,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "buffer": case "bytes": case "tlsca": + case "event": + case "events": return host.emitGenericLibCall(e); case "test": return emitTestLibCall(host, e); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 0eae483eb..bfe528a63 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -665,6 +665,11 @@ export const LIB_FN_SYMS: Record = { "fetch.responseNew": "scr_fetch_response_new", "fetch.input": "scr_fetch_input", "abort.controllerNew": "scr_fetch_abort_controller_new", + "event.targetNew": "scr_event_target_new", + "event.new": "scr_event_new", + "event.customNew": "scr_custom_event_new", + "event.is": "scr_event_is", + "events.addAbortListener": "scr_events_add_abort_listener", "abort.timeout": "scr_fetch_abort_timeout", "abort.now": "scr_fetch_abort_now", "abort.any": "scr_fetch_abort_any", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 8ad524693..adc6d2818 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -21,6 +21,7 @@ export interface NativeLinkFeatures { textDecoderLegacy: boolean; fileHandle: boolean; fetch: boolean; + eventTarget?: boolean; zlib: boolean; assert: boolean; inspect: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 82a4558b0..577fb30f6 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -107,6 +107,11 @@ export interface CcOptions { * reactions, AsyncLocalStorage, the unhandledRejection/warning * process events. */ dynAsync?: boolean; + /** The program constructs EventTarget/Event/CustomEvent values or + * calls events.addAbortListener (the event.* libCalls), or uses the + * native fetch AbortSignal built on the same engine: compiles + * scr_event_target.c. */ + eventTarget?: boolean; /** The program uses the process-events surface (signal/exit listeners, * stdin events, for-await over stdin — moduleUsesProcessEvents on the * IR): compiles scr_events.c into the binary. Event-free binaries keep diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 69bea51bd..7515f4246 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -486,6 +486,7 @@ export async function compileCInternal( : []), ...(opts.dynInvoke || opts.workers || nativeFetch ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []), ...(opts.dc ? [rt(join(rtDir, "scr_dc.c"))] : []), + ...(opts.eventTarget || nativeFetch ? [rt(join(rtDir, "scr_event_target.c"))] : []), ...(opts.dynAsync || opts.dynInvoke || opts.workers || opts.dc || opts.fileHandle || nativeFetch ? [rt(join(rtDir, "scr_async_dyn.c"))] : []), diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index eeb2f84ca..68465a792 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -80,6 +80,7 @@ export interface RuntimePackManifest { export interface RuntimeFeatureSet extends NativeLinkFeatures { workers: boolean; + eventTarget: boolean; nativeFetch: boolean; netEffective: boolean; httpEffective: boolean; @@ -264,6 +265,7 @@ export function effectiveRuntimeFeatures( return { ...features, workers: features.workers === true, + eventTarget: features.eventTarget === true, nativeFetch, netEffective, httpEffective, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 9620463ba..4cffb09bf 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -54,6 +54,7 @@ export interface EarlyExecutableNativeFeatures { textDecoderLegacy: boolean; fileHandle: boolean; fetch: boolean; + eventTarget?: boolean; zlib: boolean; assert: boolean; inspect: boolean; diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index 0b7848588..d9aa998e1 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -1,11 +1,50 @@ import * as ts from "../ts7/adapter.js"; -import type { IrExpr, SrcLoc } from "../../ir/ir.js"; +import { dynUndefinedExpr } from "../../ir/build.js"; +import { DYN, type IrExpr, type SrcLoc } from "../../ir/ir.js"; import type { Lowerer } from "./lowerer.js"; import { lowerTimersMemberCall } from "./lower-timers.js"; import { lowerStreamModuleCall } from "./lower-stream.js"; import { lowerNodeModuleCall } from "./builtins/module-resolution.js"; import { builtinModuleFnOf, builtinFenceHintOf } from "./surfaces.js"; +/** `events.addAbortListener(signal, listener)`: the runtime registers the + * once/resist-stop-propagation abort listener (or queues the call for an + * aborted signal) and answers the disposer; the Disposable is a fresh + * object carrying it under Symbol.dispose. */ +export function lowerAddAbortListener( + lowerer: Lowerer, + expr: ts.CallExpression, + loc: SrcLoc, +): IrExpr { + const args = expr.arguments; + if (args.length > 2 || args.some(ts.isSpreadElement)) { + lowerer.noLowering(`addAbortListener with this argument list`, expr); + } + const values = [0, 1].map((i) => + args[i] ? lowerer.lowerExprExpecting(args[i]!, DYN) : dynUndefinedExpr(loc), + ); + const key: IrExpr = { + kind: "dynFrom", + value: { + kind: "libCall", + fn: "sym.wellKnown", + args: [{ kind: "strLit", value: "dispose", type: { kind: "string" }, loc }], + type: { kind: "symbol" }, + loc, + }, + type: DYN, + loc, + }; + const dispose: IrExpr = { + kind: "libCall", + fn: "events.addAbortListener", + args: values, + type: DYN, + loc, + }; + return { kind: "dynObjLit", fields: [{ key, value: dispose }], type: DYN, loc }; +} + /** Named imports, namespace members and callable aliases share validation * and dispatch. Resolve diagnostic provenance only if no handler accepts. */ export function lowerBuiltinCall( @@ -18,6 +57,9 @@ export function lowerBuiltinCall( if (bi.module === "child_process" && bi.member === "execFile") { return lowerer.lowerExecFileCall(expr, loc); } + if (bi.module === "events" && bi.member === "addAbortListener") { + return lowerAddAbortListener(lowerer, expr, loc); + } if (bi.module === "timers") { const timersServed = lowerTimersMemberCall(lowerer, expr, bi.member, loc); if (timersServed) return timersServed; diff --git a/packages/compiler/src/frontend/lowering/builtins/numbers.ts b/packages/compiler/src/frontend/lowering/builtins/numbers.ts index c82509834..08d6cb349 100644 --- a/packages/compiler/src/frontend/lowering/builtins/numbers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/numbers.ts @@ -124,3 +124,24 @@ export function lowerNumberStaticProperty( if (value === undefined) return null; return { kind: "numLit", value, type: F64, loc: locOf(expr) }; } + +/** The Event phase constants (Event.NONE ... Event.BUBBLING_PHASE), + * which CustomEvent inherits through its constructor. */ +const EVENT_PHASES: Record = { + NONE: 0, + CAPTURING_PHASE: 1, + AT_TARGET: 2, + BUBBLING_PHASE: 3, +}; + +export function lowerEventStaticProperty( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): IrExpr | null { + const member = + lowerer.stdlibGlobalMember(expr, "Event") ?? lowerer.stdlibGlobalMember(expr, "CustomEvent"); + if (member === null) return null; + const value = own(EVENT_PHASES, member); + if (value === undefined) return null; + return { kind: "numLit", value, type: F64, loc: locOf(expr) }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index a9e549fb6..a67cd8b77 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -70,6 +70,7 @@ import { import { STREAM_FORCED_EVENT_NAMES, streamForcedTuple, streamSidesOf } from "./lower-stream.js"; import { boolLit, strLit, varRef } from "../../ir/build.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; +import { lowerAddAbortListener } from "./builtin-calls.js"; /** True when the class descends from (or is) the runtime emitter. */ export function emitterRooted(lowerer: Lowerer, info: ClassInfo | undefined | null): boolean { @@ -185,14 +186,17 @@ function emitterEvents(lowerer: Lowerer): Map { ); return sig; }; - // The two forced tuples (see the header comment). - table.set("error", { - tuple: [{ kind: "object", className: "%Error" }], - fromEmit: true, - conflict: null, - dynListener: false, - hasListener: false, - }); + // The forced tuples (see the header comment); errorMonitor listeners + // receive the 'error' payload. + for (const name of ["error", ERROR_MONITOR_EVENT]) { + table.set(name, { + tuple: [{ kind: "object", className: "%Error" }], + fromEmit: true, + conflict: null, + dynListener: false, + hasListener: false, + }); + } table.set("newListener", { tuple: [STRING], fromEmit: true, @@ -295,7 +299,12 @@ function emitterEvents(lowerer: Lowerer): Map { } const info = scannedEmitterInfo(lowerer, recvT); if (!info) return; - if (!nameT.isStringLiteralType()) { + const literal = isErrorMonitorRef(lowerer, arg0) + ? ERROR_MONITOR_EVENT + : nameT.isStringLiteralType() + ? nameT.value + : null; + if (literal === null) { const pattern = computedEventPatternOf(lowerer, arg0); if ( pattern && @@ -306,8 +315,8 @@ function emitterEvents(lowerer: Lowerer): Map { } return; } - const name = nameT.value; - if (META_EVENTS.has(name) || name === "error") return; // forced tuples + const name = literal; + if (META_EVENTS.has(name) || name === "error" || name === ERROR_MONITOR_EVENT) return; // forced tuples // Stream receivers' runtime-emitted events carry PER-BASE forced // tuples (lower-stream.ts) — their sites never join the program- // global table, so a stream's 'data' cannot collide with a user @@ -368,12 +377,69 @@ function computedPatternsOverlap(left: ComputedEventPattern, right: ComputedEven ); } -/** The compile-time name for a literal-only operation, or a pointed fence - * (string-literal TYPE, so const bindings and literal unions of one - * member count — the staticHeaderKeyOf stance). */ -function eventNameOf(lowerer: Lowerer, member: string, arg: ts.Expression): string { +/** The registry name of `events.errorMonitor`: a symbol in Node, so no + * string event can collide with it (the NUL never appears in a literal + * name the lowering admits as the same key — see literalEventName). */ +export const ERROR_MONITOR_EVENT = "\u0000events.errorMonitor"; + +/** `errorMonitor` / `EventEmitter.errorMonitor` / `events.errorMonitor`: + * the events module's unique symbol, by declaration provenance. */ +function isErrorMonitorRef(lowerer: Lowerer, arg: ts.Expression): boolean { + const isMonitorType = (t: ts.Type): boolean => { + if (!(t.flags & ts.TypeFlags.UniqueESSymbol)) return false; + const symbol = t.getSymbol(); + return symbol?.name === "errorMonitor" && lowerer.isStdlibSymbol(symbol); + }; + // The unique symbol TYPE names its declaration through any alias. + if (isMonitorType(lowerer.typeOf(arg))) return true; + // A const destructuring (`const { errorMonitor } = events`) widens the + // binding's type in JavaScript; its source property still names it. + if (!ts.isIdentifier(arg)) return false; + const symbol = lowerer.resolveValueSymbol(arg); + const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + if (!declaration || !ts.isBindingElement(declaration)) return false; + const pattern = declaration.parent; + const variable = pattern?.parent; + if ( + !pattern || + !variable || + !ts.isObjectBindingPattern(pattern) || + !ts.isVariableDeclaration(variable) || + !variable.initializer || + !(ts.getCombinedNodeFlags(variable) & ts.NodeFlags.Const) + ) + return false; + const key = declaration.propertyName ?? declaration.name; + if (!ts.isIdentifier(key)) return false; + const property = lowerer.checker.getPropertyOfType( + lowerer.typeOf(variable.initializer), + key.text, + ); + return property !== undefined && isMonitorType(lowerer.checker.getTypeOfSymbol(property)); +} + +/** A compile-time event name: a string-literal TYPE (const bindings and + * literal unions of one member count — the staticHeaderKeyOf stance) or + * the errorMonitor symbol. */ +function literalEventName(lowerer: Lowerer, arg: ts.Expression): string | null { + if (isErrorMonitorRef(lowerer, arg)) return ERROR_MONITOR_EVENT; const t = lowerer.typeOf(arg); - if (t.isStringLiteralType()) return t.value; + if (!t.isStringLiteralType()) return null; + // A string spelling of the reserved key stays a separate event in Node. + if (t.value === ERROR_MONITOR_EVENT) { + lowerer.noLowering( + "an event name spelling the errorMonitor key", + arg, + "the runtime reserves this name for events.errorMonitor", + ); + } + return t.value; +} + +/** The compile-time name for a literal-only operation, or a pointed fence. */ +function eventNameOf(lowerer: Lowerer, member: string, arg: ts.Expression): string { + const name = literalEventName(lowerer, arg); + if (name !== null) return name; lowerer.noLowering( `${member} with a non-literal event name`, arg, @@ -987,6 +1053,7 @@ export function lowerEmitterMethodCall( superRecv === undefined && lowerer.checker.getConstructSignatures(lowerer.typeOf(access.expression)).length > 0 ) { + if (member === "addAbortListener") return lowerAddAbortListener(lowerer, call, loc); if (member === "setMaxListeners" && args.length === 1) { if (lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind !== "f64") { // Node's runtime ladder over the dyn value ("setMaxListeners" is @@ -1080,7 +1147,7 @@ export function lowerEmitterMethodCall( ); } const registering = REGISTER_MEMBERS.has(member); - if (!lowerer.typeOf(args[0]!).isStringLiteralType()) { + if (literalEventName(lowerer, args[0]!) === null) { computedEventPattern(lowerer, info, member, args[0]!); const receiver = lowerReceiver(); const name = lowerer.lowerExprExpecting(args[0]!, STRING); @@ -1230,8 +1297,7 @@ export function lowerEmitterMethodCall( if (args.length === 0) { lowerer.noLowering("emit without an event name", call); } - const nameType = lowerer.typeOf(args[0]!); - if (!nameType.isStringLiteralType()) { + if (literalEventName(lowerer, args[0]!) === null) { const pattern = computedEventPatternOf(lowerer, args[0]!); const mayBeObserved = pattern === null || @@ -1363,7 +1429,11 @@ export function lowerEmitterMethodCall( } const all = args.length === 0; const receiver = lowerReceiver(); - const name = all ? strLit("", loc) : lowerer.lowerExprExpecting(args[0]!, STRING); + const name = all + ? strLit("", loc) + : isErrorMonitorRef(lowerer, args[0]!) + ? strLit(ERROR_MONITOR_EVENT, loc) + : lowerer.lowerExprExpecting(args[0]!, STRING); if (name.type.kind !== "string") { lowerer.noLowering( `removeAllListeners with a '${lowerer.fmt(name.type)}' event name`, @@ -1384,7 +1454,9 @@ export function lowerEmitterMethodCall( lowerer.noLowering(`listenerCount with ${args.length} arguments`, call); } const receiver = lowerReceiver(); - const name = lowerer.lowerExprExpecting(args[0]!, STRING); + const name = isErrorMonitorRef(lowerer, args[0]!) + ? strLit(ERROR_MONITOR_EVENT, loc) + : lowerer.lowerExprExpecting(args[0]!, STRING); if (name.type.kind !== "string") { lowerer.noLowering(`listenerCount with a '${lowerer.fmt(name.type)}' event name`, args[0]!); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 44f8c3f7f..b3ecf991e 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,4 +1,5 @@ import { generatorDrain } from "./iterator-adapters.js"; +import { lowerEventStaticProperty } from "./builtins/numbers.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; import { isOptionalProcessStreamProperty } from "./builtins/process.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; @@ -2147,6 +2148,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { lowerer.lowerJsonProperty(expr) ?? lowerer.lowerErrorCodeProperty(expr) ?? lowerer.lowerNumberStaticProperty(expr) ?? + lowerEventStaticProperty(lowerer, expr) ?? lowerer.lowerMathProperty(expr) ?? lowerer.lowerIntrinsicProperty(expr) ?? // The _readableState/_writableState scalar READS (the suite's @@ -11666,6 +11668,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return { kind: "libCall", fn: "dyn.dataViewIs", args: [value], type: BOOL, loc }; } + { + const eventKind = ["Event", "CustomEvent", "EventTarget"].findIndex((name) => + lowerer.isStdlibGlobal(expr.right, name), + ); + if (eventKind >= 0) { + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + const kind: IrExpr = { kind: "numLit", value: eventKind, type: F64, loc }; + return { kind: "libCall", fn: "event.is", args: [value, kind], type: BOOL, loc }; + } + } if (lowerer.isStdlibGlobal(expr.right, "SharedArrayBuffer")) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return { kind: "libCall", fn: "sharedArrayBuffer.is", args: [value], type: BOOL, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index d1db5da71..622de64b6 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -1771,7 +1771,7 @@ export function lowerStaticAbortSignalListenerCall( const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( - symbol?.name !== "AbortSignal" || + (symbol?.name !== "AbortSignal" && symbol?.name !== "EventTarget") || !lowerer.checker .declarationsOf(symbol) .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) @@ -1803,6 +1803,40 @@ export function lowerStaticAbortSignalListenerCall( ); } +/** `new EventTarget()`, `new Event(type, options?)` and `new + * CustomEvent(type, options?)`: native handles built from the checked + * argument vector (Node validates it in the constructor). */ +export function lowerEventTargetNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + const name = stdlibGlobalNameOf(lowerer, expr.expression); + if (name !== "EventTarget" && name !== "Event" && name !== "CustomEvent") return null; + const args = expr.arguments ?? []; + if (args.some(ts.isSpreadElement)) return null; + const loc = locOf(expr); + const values = args.map((arg) => + ts.isObjectLiteralExpression(arg) + ? lowerDynObjectLiteral(lowerer, arg) + : lowerer.lowerExprExpecting(arg, DYN), + ); + if (name === "EventTarget") { + const result: IrExpr = { kind: "libCall", fn: "event.targetNew", args: [], type: DYN, loc }; + if (values.length === 0) return result; + return { + kind: "seqExpr", + stmts: values.map((value): IrStmt => ({ kind: "exprStmt", expr: value, loc: value.loc })), + result, + type: DYN, + loc, + }; + } + return { + kind: "libCall", + fn: name === "Event" ? "event.new" : "event.customNew", + args: [{ kind: "dynArrLit", elems: values, type: DYN, loc }], + type: DYN, + loc, + }; +} + export function lowerAbortControllerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (!ts.isIdentifier(expr.expression) || expr.expression.text !== "AbortController") { return null; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 000a36198..480b7942c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -443,6 +443,7 @@ import { refuseUnsupportedApi, stdlibGlobalFnOf, lowerAbortControllerNew, + lowerEventTargetNew, lowerDynamicImportCall, lowerFetchCall, lowerFetchElementMethodCall, @@ -10670,6 +10671,7 @@ export class Lowerer { const builtin = lowerLifetimeRegionNew(this, expr) ?? lowerAbortControllerNew(this, expr) ?? + lowerEventTargetNew(this, expr) ?? lowerFetchWebNew(this, expr) ?? lowerResponseNew(this, expr) ?? lowerStaticReadableStreamNew(this, expr) ?? diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 93a923078..0995afbbc 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -127,7 +127,12 @@ export const FETCH_AMBIENT_TYPES = [ "ResponseInit", "RequestInit", "Event", + "CustomEvent", "EventTarget", + "AddEventListenerOptions", + "EventListenerOptions", + "EventInit", + "CustomEventInit", "AbortController", "AbortSignal", "Headers", @@ -2107,9 +2112,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } // The stdlib Console interface (the global console and node:console // Console instances) is a native handle carried by the checked-dynamic - // representation and consumed by the console method lowerings. + // representation and consumed by the console method lowerings. The + // stdlib Disposable (events.addAbortListener's result) is a checked + // object carrying its Symbol.dispose method. if ( - psym?.name === "Console" && + (psym?.name === "Console" || psym?.name === "Disposable") && checker .declarationsOf(psym) .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 7ad4d36f0..d9615a47e 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -507,6 +507,7 @@ async function compileExecutableNative( textDecoderLegacy: features.textDecoderLegacy, fileHandle: features.fileHandle, fetch: features.fetch, + ...(features.eventTarget ? { eventTarget: true } : {}), zlib: features.zlib, assert: features.assert, inspect: features.inspect, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 2deee7f1f..c1a483d61 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -88,6 +88,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fetch.abortAny", "abort.timeout", "abort.any", + "event.new", + "event.customNew", + "events.addAbortListener", "fetch.streamNew", "fetch.streamFrom", "num.toFixed", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index c014b148f..c96177ec6 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -64,6 +64,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "abort.controllerNew": { argTypes: [], result: DYN }, + /** EventTarget / Event / CustomEvent construction (scr_event_target.c): + * the Event constructors take the argument vector as one dyn array and + * throw Node's validation errors; `event.is` is `v instanceof Event` + * (kind 0), `CustomEvent` (1), `EventTarget` (2). addAbortListener + * answers the disposer (the Disposable's [Symbol.dispose]). */ + "event.targetNew": { argTypes: [], result: DYN }, + "event.new": { argTypes: [DYN], result: DYN }, + "event.customNew": { argTypes: [DYN], result: DYN }, + "event.is": { argTypes: [DYN, F64], result: BOOL }, + "events.addAbortListener": { argTypes: [DYN, DYN], result: DYN }, "abort.timeout": { argTypes: [DYN], result: DYN }, "abort.now": { argTypes: [DYN], result: DYN }, "abort.any": { argTypes: [DYN], result: DYN }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 26db4915b..e0a5ff0fa 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -9,6 +9,7 @@ export interface RuntimeFeatures { legacyTextDecoder: boolean; fileHandle: boolean; fetch: boolean; + eventTarget: boolean; processEvents: boolean; emitter: boolean; stream: boolean; @@ -78,6 +79,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run legacyTextDecoder: false, fileHandle: false, fetch: false, + eventTarget: false, processEvents: false, emitter: mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), @@ -124,6 +126,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run ) features.legacyTextDecoder = true; if (fn.startsWith("fetch.")) features.fetch = true; + if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true; if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; if (fn.startsWith("emitter.")) features.emitter = true; if ( diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index dd3299bef..1f9402b2f 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -142,6 +142,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr [features.stream, "the node:stream surface"], [features.tls, "the node:tls surface"], [features.fetch, "fetch"], + [features.eventTarget, "the EventTarget surface"], [features.nodeTest, "the node:test surface"], [features.dynAsync, "the checked-dynamic async surface"], [features.dc, "the diagnostics_channel surface"], diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 533b6a4ca..aa6f7d3ea 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -73,6 +73,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( systemLibraries: [], features: { workers: false, + eventTarget: false, regex: true, copying: false, textDecoderLegacy: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5130-event-target-dispatch.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5130-event-target-dispatch.js.json new file mode 100644 index 000000000..e5d765afa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5130-event-target-dispatch.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5130-event-target-dispatch.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5131-event-target-listener-errors.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5131-event-target-listener-errors.js.json new file mode 100644 index 000000000..3547bb593 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5131-event-target-listener-errors.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5131-event-target-listener-errors.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5132-event-target-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5132-event-target-typed.ts.json new file mode 100644 index 000000000..6f8aa1c7b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5132-event-target-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5132-event-target-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5133-abort-signal-event-target.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5133-abort-signal-event-target.ts.json new file mode 100644 index 000000000..a90a496b6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5133-abort-signal-event-target.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5133-abort-signal-event-target.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5134-events-error-monitor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5134-events-error-monitor.ts.json new file mode 100644 index 000000000..a573e44df --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5134-events-error-monitor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5134-events-error-monitor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5135-event-target-warnings.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5135-event-target-warnings.js.json new file mode 100644 index 000000000..20dd0893a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5135-event-target-warnings.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5135-event-target-warnings.js" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 1c3b33462..8db5e4402 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -52,6 +52,7 @@ const OPTIONAL = [ ["scr_console_native.c", "inspect"], ["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")], ["scr_dc.c", "dc"], + ["scr_event_target.c", any("eventTarget", "nativeFetch")], ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")], ["scr_zlib.c", "zlib"], ["scr_events.c", "events"], diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index 9b39ec263..86c9f05b9 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -3,10 +3,12 @@ import { fileURLToPath } from "node:url"; process.env.SCRIPTC_RUNTIME_PACK_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ platform: "wasi", - extraRuntimeSources: ["scr_abort.c"], + // AbortSignal is portable here and dispatches through the EventTarget unit. + extraRuntimeSources: ["scr_abort.c", "scr_event_target.c"], runtimeDefines: ["_GNU_SOURCE", "_WASI_EMULATED_SIGNAL", "_WASI_EMULATED_PROCESS_CLOCKS"], omitRuntimeSources: ["scr_child.c"], omitOptionalSources: [ + "scr_event_target.c", "scr_loop_kqueue.c", "scr_loop_epoll.c", "scr_loop_wsapoll.c", diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 24617f054..55220fb95 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -621,6 +621,7 @@ void scr_process_on_warning(ScrDyn *fn) { scr_throw_error_msg_code(SCR_ERR_TYPE, msg, strlen(msg), "ERR_INVALID_ARG_TYPE"); return; } + scr_warning_emit_hook = scr_emit_warning; if (scr_nwarn == scr_warn_cap) { scr_warn_cap = scr_warn_cap ? scr_warn_cap * 2 : 4; scr_warn_listeners = realloc(scr_warn_listeners, scr_warn_cap * sizeof *scr_warn_listeners); diff --git a/packages/runtime/src/scr_event_target.c b/packages/runtime/src/scr_event_target.c new file mode 100644 index 000000000..f26929417 --- /dev/null +++ b/packages/runtime/src/scr_event_target.c @@ -0,0 +1,1323 @@ +/* EventTarget, Event and CustomEvent over checked-dynamic values — Node's + * lib/internal/event_target.js, step for step (scr_runtime.h has the + * layout contract). + * + * Listeners for one event type form a linked list in registration order. + * Every node holds a reference on its `next` node and keeps that link when + * it is removed, exactly the JS objects Node walks: a dispatch caches the + * next node before a callback runs, skips nodes flagged removed, and + * continues from a removed node into whatever it last pointed at. Type + * roots are refcounted the same way, so a dispatch keeps walking a root + * that removeEventListener dropped from the target's map. + */ +#include "scr_runtime.h" + +#include +#include +#include + +#ifdef _WIN32 +#include +#define SCR_ET_PID() ((long)_getpid()) +#else +#include +#define SCR_ET_PID() ((long)getpid()) +#endif + +enum { + SCR_ET_ONCE = 1 << 0, + SCR_ET_CAPTURE = 1 << 1, + SCR_ET_PASSIVE = 1 << 2, + SCR_ET_RESIST = 1 << 3, /* runs after stopImmediatePropagation */ + SCR_ET_REMOVED = 1 << 4, +}; + +typedef struct ScrEtListener ScrEtListener; + +struct ScrEtListener { + size_t rc; + ScrEtListener *next; /* +1 */ + ScrEtListener *prev; /* weak; NULL when the root is the predecessor */ + ScrEtRoot *root; /* weak; the root outlives its live nodes */ + unsigned flags; + ScrDyn *listener; /* +1; NULL for a native listener */ + /* Native listeners: options.signal removal hooks and `on` + * attribute wrappers. */ + void (*native)(void *ctx, ScrDyn *self, ScrDyn *event); + void *ctx; + void (*ctx_release)(void *ctx); +}; + +struct ScrEtRoot { + size_t rc; + ScrEventTargetCore *core; /* weak; NULL once the root leaves the map */ + ScrEtRoot *map_next; /* the map's own reference order */ + ScrStr *type; + size_t size; + bool resist; + ScrEtListener *head; /* +1 */ +}; + +struct ScrEtHandler { + size_t rc; + ScrStr *type; + ScrDyn *handler; /* +1 or NULL (null) */ + ScrEtHandler *next; +}; + +/* `signal.addEventListener('abort', () => this.removeEventListener(type, + * listener, options), { once: true, [kWeakHandler]: this, ... })`: the + * signal's listener list owns the hook; the target is weak (its core + * clears `target` when it is dropped). */ +struct ScrEtHook { + size_t rc; + ScrEventTargetCore *target; /* weak */ + ScrEtHook *target_next; /* the target's hook registry */ + ScrDyn *type; /* +1, converted when the hook fires (Node's order) */ + ScrDyn *listener; /* +1 */ + bool capture; +}; + +static void scr_et_oom(void) { + fputs("scriptc: out of memory\n", stderr); + abort(); +} + +static void *scr_et_calloc(size_t n) { + void *p = calloc(1, n); + if (!p) scr_et_oom(); + return p; +} + +static bool scr_et_eq(const ScrStr *s, const char *want) { + return s->len == strlen(want) && memcmp(s->data, want, s->len) == 0; +} + +static void scr_et_type_error(const char *message, const char *code) { + scr_throw_error_msg_code(SCR_ERR_TYPE, message, strlen(message), code); +} + +/* ── teardown registries ───────────────────────────────────────────── + * Listener closures and events can reach their targets again (a closure + * capturing the target, an event's target), cycles the RC scheme does not + * collect. Cores with listeners and events holding references register + * here, and exit drops them before the RC audit counts. */ + +static SCR_TL ScrEventTargetCore *scr_et_live_cores; +static SCR_TL ScrEvent *scr_et_live_events; +static SCR_TL bool scr_et_teardown_registered; + +static void scr_et_teardown(void); + +static void scr_et_register_teardown(void) { + if (scr_et_teardown_registered) return; + scr_et_teardown_registered = true; + scr_atexit(scr_et_teardown); +} + +static void scr_et_core_track(ScrEventTargetCore *core) { + if (core->tracked) return; + scr_et_register_teardown(); + core->tracked = true; + core->tracked_prev = NULL; + core->tracked_next = scr_et_live_cores; + if (scr_et_live_cores) scr_et_live_cores->tracked_prev = core; + scr_et_live_cores = core; +} + +static void scr_et_core_untrack(ScrEventTargetCore *core) { + if (!core->tracked) return; + if (core->tracked_prev) core->tracked_prev->tracked_next = core->tracked_next; + else scr_et_live_cores = core->tracked_next; + if (core->tracked_next) core->tracked_next->tracked_prev = core->tracked_prev; + core->tracked = false; + core->tracked_prev = core->tracked_next = NULL; +} + +static void scr_event_track(ScrEvent *ev) { + if (ev->tracked) return; + scr_et_register_teardown(); + ev->tracked = true; + ev->tracked_prev = NULL; + ev->tracked_next = scr_et_live_events; + if (scr_et_live_events) scr_et_live_events->tracked_prev = ev; + scr_et_live_events = ev; +} + +static void scr_event_untrack(ScrEvent *ev) { + if (!ev->tracked) return; + if (ev->tracked_prev) ev->tracked_prev->tracked_next = ev->tracked_next; + else scr_et_live_events = ev->tracked_next; + if (ev->tracked_next) ev->tracked_next->tracked_prev = ev->tracked_prev; + ev->tracked = false; + ev->tracked_prev = ev->tracked_next = NULL; +} + +/* ── listener nodes and type roots ─────────────────────────────────── */ + +static ScrEtListener *scr_et_listener_retain(ScrEtListener *l) { + if (l) l->rc++; + return l; +} + +static void scr_et_listener_release(ScrEtListener *l) { + while (l && --l->rc == 0) { + ScrEtListener *next = l->next; + scr_dyn_release(l->listener); + if (l->ctx_release) l->ctx_release(l->ctx); + free(l); + l = next; /* the reference this node held */ + } +} + +static ScrEtRoot *scr_et_root_retain(ScrEtRoot *r) { + r->rc++; + return r; +} + +static void scr_et_root_release(ScrEtRoot *r) { + if (!r || --r->rc > 0) return; + scr_et_listener_release(r->head); + scr_str_release(r->type); + free(r); +} + +/* Listener.remove(): unlink, keep `next`, flag removed. */ +static void scr_et_listener_remove(ScrEtListener *l) { + ScrEtListener *next = l->next; + if (next) next->prev = l->prev; + l->flags |= SCR_ET_REMOVED; + /* The predecessor's link may be the last reference to l. */ + if (l->prev) { + l->prev->next = scr_et_listener_retain(next); + } else if (l->root) { + l->root->head = scr_et_listener_retain(next); + } else { + return; + } + scr_et_listener_release(l); +} + +static ScrEtRoot *scr_et_root_find(const ScrEventTargetCore *core, const ScrStr *type) { + for (ScrEtRoot *r = core->roots; r; r = r->map_next) { + if (r->type->len == type->len && memcmp(r->type->data, type->data, type->len) == 0) return r; + } + return NULL; +} + +/* this[kEvents].delete(type) */ +static void scr_et_root_unmap(ScrEventTargetCore *core, ScrEtRoot *root) { + for (ScrEtRoot **at = &core->roots; *at; at = &(*at)->map_next) { + if (*at == root) { + *at = root->map_next; + root->map_next = NULL; + root->core = NULL; + scr_et_root_release(root); + return; + } + } +} + +/* ── options.signal hooks ──────────────────────────────────────────── */ + +static void scr_et_hook_release(void *ctx) { + ScrEtHook *h = ctx; + if (--h->rc > 0) return; + if (h->target) { + for (ScrEtHook **at = &h->target->hooks; *at; at = &(*at)->target_next) { + if (*at == h) { + *at = h->target_next; + break; + } + } + } + scr_dyn_release(h->type); + scr_dyn_release(h->listener); + free(h); +} + +static void scr_et_remove_matching(ScrEventTargetCore *core, const ScrStr *type, + const ScrDyn *listener, bool capture); +static int scr_et_listener_check(const ScrDyn *listener); +static void scr_et_report(ScrCaught *caught); + +static void scr_et_hook_fire(void *ctx, ScrDyn *self, ScrDyn *event) { + (void)self; + (void)event; + ScrEtHook *h = ctx; + if (!h->target || scr_et_listener_check(h->listener) <= 0) { + if (scr_exc_pending()) scr_et_report(scr_exc_take()); + return; + } + ScrStr *type = scr_dyn_string_coerce_js(h->type); + if (!type) { + scr_et_report(scr_exc_take()); + return; + } + scr_et_remove_matching(h->target, type, h->listener, h->capture); + scr_str_release(type); +} + +/* ── listener values ───────────────────────────────────────────────── */ + +/* validateEventListener: true for a function or an object (handleEvent is + * read at dispatch), false for null/undefined, a TypeError otherwise. */ +static int scr_et_listener_check(const ScrDyn *listener) { + switch (listener->kind) { + case SCR_DYN_UNDEF: + case SCR_DYN_NULL: + return 0; + case SCR_DYN_FUNC: + case SCR_DYN_OBJ: + case SCR_DYN_ARR: + case SCR_DYN_BYTES: + case SCR_DYN_HANDLE: + case SCR_DYN_PROMISE: + case SCR_DYN_PROXY: + return 1; + case SCR_DYN_TYPED_REF: + if (scr_dyn_ref_typeof_is(listener, "function") || scr_dyn_ref_typeof_is(listener, "object")) + return 1; + break; + default: + break; + } + scr_dyn_arg_type_fail("listener", "an instance of EventListener", listener); + return -1; +} + +static bool scr_et_listener_same(const ScrDyn *a, const ScrDyn *b) { + return scr_dyn_strict_eq(a, b) || scr_dyn_obj_same_source(a, b); +} + +/* ── the warnings Node prints through process.emitWarning ───────────── */ + +static SCR_TL bool scr_et_trace_hint_shown; + +typedef struct { + char name[48]; + ScrStr *message; +} ScrEtWarning; + +static void *scr_et_warning_retain_v(void *p) { return p; } +static void scr_et_warning_release_v(void *p) { + ScrEtWarning *w = p; + scr_str_release(w->message); + free(w); +} + +static void scr_et_warn_tick(ScrClosure *cb) { + ScrEtWarning *w = scr_box_get_ref(cb->caps[0]); + if (scr_warning_emit_hook) { + scr_warning_emit_hook(w->name, NULL, w->message); + return; + } + fflush(stdout); + fprintf(stderr, "(node:%ld) %s: %.*s\n", SCR_ET_PID(), w->name, (int)w->message->len, + w->message->data); + if (!scr_et_trace_hint_shown) { + scr_et_trace_hint_shown = true; + fputs("(Use `node --trace-warnings ...` to show where the warning was created)\n", stderr); + } +} + +/* process.emitWarning: listeners and the default report run from a + * nextTick. */ +static void scr_et_warn(const char *name, const char *message) { + ScrEtWarning *w = scr_et_calloc(sizeof *w); + snprintf(w->name, sizeof w->name, "%s", name); + w->message = scr_str_new(message, strlen(message)); + ScrClosure *cb = scr_closure_new((void *)&scr_et_warn_tick, 1); + cb->caps[0] = scr_box_new_obj(&scr_et_warning_retain_v, &scr_et_warning_release_v, NULL); + scr_box_set_ref(cb->caps[0], w); + scr_next_tick(cb); +} + +/* this[kNewListener]: the one-time leak warning. */ +static void scr_et_new_listener(ScrEventTargetCore *core, ScrEtRoot *root) { + if (core->max_listeners <= 0 || (double)root->size <= core->max_listeners || core->warned) return; + core->warned = true; + char message[512]; + snprintf(message, sizeof message, + "Possible EventTarget memory leak detected. %zu %.*s listeners added to %s. " + "MaxListeners is %g. Use events.setMaxListeners() to increase limit", + root->size, (int)root->type->len, root->type->data, core->cls, core->max_listeners); + scr_et_warn("MaxListenersExceededWarning", message); +} + +/* ── the core ──────────────────────────────────────────────────────── */ + +void scr_et_core_init(ScrEventTargetCore *core, double max_listeners, const char *cls) { + memset(core, 0, sizeof *core); + core->max_listeners = max_listeners; + core->cls = cls; +} + +void scr_et_core_drop(ScrEventTargetCore *core) { + scr_et_core_untrack(core); + ScrEtRoot *roots = core->roots; + ScrEtHandler *handlers = core->handlers; + core->roots = NULL; + core->handlers = NULL; + /* Hooks aimed at this target stay registered on their signals and do + * nothing once fired (Node's weak handler). */ + while (core->hooks) { + ScrEtHook *h = core->hooks; + core->hooks = h->target_next; + h->target = NULL; + h->target_next = NULL; + } + while (roots) { + ScrEtRoot *next = roots->map_next; + roots->core = NULL; + roots->map_next = NULL; + scr_et_root_release(roots); + roots = next; + } + while (handlers) { + ScrEtHandler *next = handlers->next; + handlers->next = NULL; + if (--handlers->rc == 0) { + scr_str_release(handlers->type); + scr_dyn_release(handlers->handler); + free(handlers); + } else { + /* A wrapper still on a dispatch's stack keeps the slot; the handler + * value goes now so no cycle survives teardown. */ + ScrDyn *handler = handlers->handler; + handlers->handler = NULL; + scr_dyn_release(handler); + } + handlers = next; + } +} + +/* Appends a listener node to the type's list (Node's addEventListener + * tail). `listener` borrowed (retained in); for native nodes NULL. */ +static ScrEtListener *scr_et_append(ScrEventTargetCore *core, ScrStr *type, ScrDyn *listener, + unsigned flags, bool *duplicate) { + *duplicate = false; + ScrEtRoot *root = scr_et_root_find(core, type); + bool fresh = root == NULL; + ScrEtListener *previous = NULL; + if (fresh) { + root = scr_et_calloc(sizeof *root); + root->rc = 1; /* the map's reference */ + root->core = core; + root->type = scr_str_retain(type); + ScrEtRoot **tail = &core->roots; + while (*tail) tail = &(*tail)->map_next; + *tail = root; + } else if (listener) { + bool capture = (flags & SCR_ET_CAPTURE) != 0; + for (ScrEtListener *h = root->head; h; h = h->next) { + if (h->listener && ((h->flags & SCR_ET_CAPTURE) != 0) == capture && + scr_et_listener_same(h->listener, listener)) { + *duplicate = true; + return NULL; + } + previous = h; + } + } else { + for (ScrEtListener *h = root->head; h; h = h->next) previous = h; + } + ScrEtListener *l = scr_et_calloc(sizeof *l); + l->rc = 1; /* the predecessor's link */ + l->root = root; + l->prev = previous; + l->flags = flags; + l->listener = listener ? scr_dyn_retain(listener) : NULL; + if (previous) previous->next = l; + else root->head = l; + root->size = fresh ? 1 : root->size + 1; + if (flags & SCR_ET_RESIST) root->resist = true; + scr_et_core_track(core); + return l; +} + +static void scr_et_remove_matching(ScrEventTargetCore *core, const ScrStr *type, + const ScrDyn *listener, bool capture) { + ScrEtRoot *root = scr_et_root_find(core, type); + if (!root || !root->head) return; + for (ScrEtListener *h = root->head; h; h = h->next) { + if (h->listener && ((h->flags & SCR_ET_CAPTURE) != 0) == capture && + scr_et_listener_same(h->listener, listener)) { + scr_et_listener_remove(h); + root->size--; + if (root->size == 0) scr_et_root_unmap(core, root); + return; + } + } +} + +/* A property read on an options dictionary: typed records cross as live + * capsules, read through their materialized view. +1 or NULL. */ +static ScrDyn *scr_et_prop(const ScrDyn *object, const char *key) { + if (object->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(object); + ScrDyn *value = view ? scr_dyn_obj_read(view, key, strlen(key)) : NULL; + scr_dyn_release(view); + return value; + } + return scr_dyn_obj_read(object, key, strlen(key)); +} + +/* The listener options: a boolean is `capture`, null is no options, any + * object's once/capture/passive/signal are read in Node's order. */ +typedef struct { + bool once, capture, passive; + ScrDyn *signal; /* +1 or NULL */ +} ScrEtOptions; + +static bool scr_et_read_options(const ScrDyn *options, ScrEtOptions *out) { + memset(out, 0, sizeof *out); + if (!options || options->kind == SCR_DYN_UNDEF) return true; + if (options->kind == SCR_DYN_BOOL) { + out->capture = options->v.b; + return true; + } + if (options->kind == SCR_DYN_NULL) return true; + if (!scr_dyn_is_object(options) && options->kind != SCR_DYN_FUNC) { + scr_dyn_arg_type_fail("options", "of type object", options); + return false; + } + static const char *const keys[] = {"once", "capture", "passive"}; + bool *slots[] = {&out->once, &out->capture, &out->passive}; + for (size_t i = 0; i < 3; i++) { + ScrDyn *v = scr_et_prop(options, keys[i]); + if (!v && scr_exc_pending()) return false; + *slots[i] = v && scr_dyn_truthy(v); + scr_dyn_release(v); + } + ScrDyn *signal = scr_et_prop(options, "signal"); + if (!signal && scr_exc_pending()) return false; + if (signal && signal->kind == SCR_DYN_UNDEF) { + scr_dyn_release(signal); + signal = NULL; + } + out->signal = signal; + return true; +} + +SCR_TL ScrEventTargetCore *(*scr_et_signal_core_fn)(const ScrDyn *d, bool *aborted) = NULL; + +ScrDyn *scr_et_add_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc) { + if (argc < 2) { + scr_et_type_error("The \"type\" and \"listener\" arguments must be specified", "ERR_MISSING_ARGS"); + return NULL; + } + ScrEtOptions opts; + if (!scr_et_read_options(argc > 2 ? args[2] : NULL, &opts)) return NULL; + if (opts.signal) { + bool aborted = false; + ScrEventTargetCore *signal_core = + scr_et_signal_core_fn ? scr_et_signal_core_fn(opts.signal, &aborted) : NULL; + if (!signal_core) { + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", opts.signal); + scr_dyn_release(opts.signal); + return NULL; + } + scr_dyn_release(opts.signal); + if (aborted) return scr_dyn_retain(scr_dyn_undefined()); + ScrEtHook *h = scr_et_calloc(sizeof *h); + h->rc = 1; + h->type = scr_dyn_retain(args[0]); + h->listener = scr_dyn_retain(args[1]); + h->capture = opts.capture; + h->target = core; + h->target_next = core->hooks; + core->hooks = h; + ScrStr *abort = scr_str_new("abort", 5); + bool duplicate; + ScrEtListener *l = + scr_et_append(signal_core, abort, NULL, SCR_ET_ONCE | SCR_ET_RESIST, &duplicate); + scr_str_release(abort); + l->native = scr_et_hook_fire; + l->ctx = h; /* the creation reference moves in */ + l->ctx_release = scr_et_hook_release; + } + int valid = scr_et_listener_check(args[1]); + if (valid < 0) return NULL; + if (valid == 0) { + char message[96]; + snprintf(message, sizeof message, "addEventListener called with %s which has no effect.", + args[1]->kind == SCR_DYN_NULL ? "null" : "undefined"); + scr_et_warn("AddEventListenerArgumentTypeWarning", message); + return scr_dyn_retain(scr_dyn_undefined()); + } + ScrStr *type = scr_dyn_string_coerce_js(args[0]); + if (!type) return NULL; + unsigned flags = (opts.once ? SCR_ET_ONCE : 0) | (opts.capture ? SCR_ET_CAPTURE : 0) | + (opts.passive ? SCR_ET_PASSIVE : 0); + bool duplicate; + ScrEtListener *l = scr_et_append(core, type, args[1], flags, &duplicate); + if (l) scr_et_new_listener(core, l->root); + scr_str_release(type); + return scr_dyn_retain(scr_dyn_undefined()); +} + +ScrDyn *scr_et_remove_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc) { + if (argc < 2) { + scr_et_type_error("The \"type\" and \"listener\" arguments must be specified", "ERR_MISSING_ARGS"); + return NULL; + } + int valid = scr_et_listener_check(args[1]); + if (valid < 0) return NULL; + if (valid == 0) return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *type = scr_dyn_string_coerce_js(args[0]); + if (!type) return NULL; + /* `options?.capture === true`: a boolean options value reads no + * capture property. */ + bool capture = false; + if (argc > 2 && scr_dyn_is_object(args[2])) { + ScrDyn *v = scr_et_prop(args[2], "capture"); + if (!v && scr_exc_pending()) { + scr_str_release(type); + return NULL; + } + capture = v && v->kind == SCR_DYN_BOOL && v->v.b; + scr_dyn_release(v); + } + scr_et_remove_matching(core, type, args[1], capture); + scr_str_release(type); + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* ── listener errors ───────────────────────────────────────────────── */ + +static void *scr_et_caught_retain_v(void *p) { return scr_caught_retain((ScrCaught *)p); } +static void scr_et_caught_release_v(void *p) { scr_caught_release((ScrCaught *)p); } + +static void scr_et_rethrow_tick(ScrClosure *cb) { + ScrCaught *caught = scr_box_get_ref(cb->caps[0]); + scr_rethrow(caught); + scr_caught_release(caught); +} + +/* emitUncaughtException: process.nextTick(() => { throw err; }). */ +static void scr_et_report(ScrCaught *caught /* moves */) { + ScrClosure *cb = scr_closure_new((void *)&scr_et_rethrow_tick, 1); + cb->caps[0] = scr_box_new_obj(&scr_et_caught_retain_v, &scr_et_caught_release_v, NULL); + scr_box_set_ref(cb->caps[0], caught); + scr_next_tick(cb); +} + +static ScrDyn *scr_et_report_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)clo; + scr_dyn_throw(scr_dyn_retain(argc ? args[0] : scr_dyn_undefined())); + scr_et_report(scr_exc_take()); + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* addCatch(result): `then(undefined, report)`. A native promise reports + * from a continuation one turn after it rejects (the reaction's turn); + * `awaited` adds the handleEvent wrapper's own await, one turn more. */ +typedef struct { + ScrPromise *promise; + bool awaited; +} ScrEtCatch; + +static void scr_et_report_job(ScrClosure *job) { + ScrCaught *caught = scr_box_get_ref(job->caps[0]); + scr_et_report(caught); +} + +/* A reaction job one microtask from now that reports `caught`. */ +static void scr_et_report_later(ScrCaught *caught /* moves */) { + ScrClosure *job = scr_closure_new((void *)&scr_et_report_job, 1); + job->caps[0] = scr_box_new_obj(&scr_et_caught_retain_v, &scr_et_caught_release_v, NULL); + scr_box_set_ref(job->caps[0], caught); + scr_queue_microtask(job); +} + +static void scr_et_catch_entry(ScrFiber *self, void *argpack) { + (void)self; + ScrEtCatch *c = argpack; + if (!scr_promise_await_settled(c->promise)) { + ScrCaught *caught = scr_exc_take(); + if (c->awaited) scr_et_report_later(caught); + else scr_et_report(caught); + } + scr_promise_release(c->promise); + free(c); +} + +static void scr_et_catch_promise(ScrPromise *p, bool awaited) { + scr_promise_mark_handled(p); + ScrEtCatch *c = scr_et_calloc(sizeof *c); + c->promise = scr_promise_retain(p); + c->awaited = awaited; + ScrPromise *done = scr_async_spawn_after(p, scr_et_catch_entry, c); + scr_promise_release(done); +} + +static void scr_et_add_catch(ScrDyn *result) { + if (result->kind == SCR_DYN_PROMISE) { + scr_et_catch_promise(result->v.promise, false); + return; + } + if (!scr_dyn_is_object(result) && result->kind != SCR_DYN_FUNC) return; + ScrDyn *then = scr_dyn_obj_read(result, "then", 4); + if (then && scr_dyn_is_callable(then)) { + ScrDyn *report = scr_dyn_new_func(scr_closure_new((void *)&scr_et_report_call, 0), + scr_et_report_call, 1, "(err)", ""); + ScrDyn *call_args[2] = {scr_dyn_undefined(), report}; + scr_dyn_this_push_dyn(result); + ScrDyn *r = scr_dyn_call(then, call_args, 2, "then"); + scr_dyn_this_pop(); + scr_dyn_release(r); + scr_dyn_release(report); + } + scr_dyn_release(then); + if (scr_exc_pending()) scr_et_report(scr_exc_take()); +} + +/* The handleEvent wrapper is an async function: a synchronous throw + * rejects its promise at once, so addCatch's reaction is the next job. */ +static void scr_et_reject_through_catch(ScrCaught *caught /* moves */) { + scr_et_report_later(caught); +} + +/* Calls one listener with `this` = the target (functions) or the listener + * object (handleEvent). */ +static void scr_et_invoke(ScrEtListener *l, ScrDyn *self, ScrDyn *event) { + if (l->native) { + l->native(l->ctx, self, event); + return; + } + ScrDyn *listener = l->listener; + ScrDyn *args[1] = {event}; + if (listener->kind == SCR_DYN_FUNC || + (listener->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_typeof_is(listener, "function"))) { + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(listener, args, 1, "listener"); + scr_dyn_this_pop(); + if (!result) { + scr_et_report(scr_exc_take()); + return; + } + if (result->kind != SCR_DYN_UNDEF && result->kind != SCR_DYN_NULL) scr_et_add_catch(result); + scr_dyn_release(result); + return; + } + /* `if (listener.handleEvent) await ReflectApply(listener.handleEvent, + * listener, args)`. A typed record listener re-reads its live source, + * so handleEvent replacements on the original are seen here. */ + ScrDyn *current = listener->kind == SCR_DYN_OBJ && listener->v.obj.source_identity && + listener->v.obj.source_access + ? listener->v.obj.source_access(listener->v.obj.source_identity, true) + : scr_dyn_retain(listener); + ScrDyn *handle = NULL; + if (current->kind == SCR_DYN_OBJ || current->kind == SCR_DYN_PROXY || + current->kind == SCR_DYN_TYPED_REF) { + if (current->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(current); + handle = view ? scr_dyn_obj_read(view, "handleEvent", 11) : NULL; + scr_dyn_release(view); + } else if (current->kind == SCR_DYN_PROXY) { + ScrStr *key = scr_str_new("handleEvent", 11); + handle = scr_dyn_proxy_get(current, key); + scr_str_release(key); + } else { + handle = scr_dyn_obj_read(current, "handleEvent", 11); + } + } + if (!handle && scr_exc_pending()) { + scr_dyn_release(current); + scr_et_reject_through_catch(scr_exc_take()); + return; + } + if (handle && scr_dyn_truthy(handle)) { + if (!scr_dyn_is_callable(handle)) { + /* Reflect.apply's TypeError, V8's wording. */ + bool primitive = + handle->kind == SCR_DYN_NUM || handle->kind == SCR_DYN_STR || handle->kind == SCR_DYN_BOOL; + ScrStr *text = primitive ? scr_dyn_string_coerce_js(handle) : scr_str_new("#", 9); + const char *kind = handle->kind == SCR_DYN_NUM ? "a number" + : handle->kind == SCR_DYN_STR ? "a string" + : handle->kind == SCR_DYN_BOOL ? "a boolean" + : "an object"; + char message[320]; + snprintf(message, sizeof message, + "Function.prototype.apply was called on %.*s, which is %s and not a function", + text ? (int)text->len : 0, text ? text->data : "", kind); + scr_str_release(text); + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); + scr_et_reject_through_catch(scr_exc_take()); + } else { + ScrDyn *receiver = listener->kind == SCR_DYN_TYPED_REF ? listener : current; + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(handle, args, 1, "handleEvent"); + scr_dyn_this_pop(); + if (!result) { + scr_et_reject_through_catch(scr_exc_take()); + } else { + /* the wrapper awaits a returned promise and settles after it */ + if (result->kind == SCR_DYN_PROMISE) scr_et_catch_promise(result->v.promise, true); + scr_dyn_release(result); + } + } + } + scr_dyn_release(handle); + scr_dyn_release(current); +} + +/* ── dispatch (kHybridDispatch) ────────────────────────────────────── */ + +static ScrEvent *scr_event_of(const ScrDyn *d) { + if (d && d->kind == SCR_DYN_HANDLE && + (d->v.handle.tag == SCR_DYNH_EVENT || d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT)) { + return (ScrEvent *)d->v.handle.ptr; + } + return NULL; +} + +static void scr_event_set_target(ScrEvent *ev, ScrDyn *target) { + ScrDyn *old = ev->target; + ev->target = scr_dyn_retain(target); + scr_event_track(ev); + scr_dyn_release(old); +} + +void scr_et_dispatch(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *event) { + ScrEvent *ev = scr_event_of(event); + scr_dyn_retain(event); + scr_event_set_target(ev, self); + ev->dispatching = true; + ScrStr *type = scr_str_retain(ev->type); + ScrEtRoot *root = scr_et_root_find(core, type); + scr_str_release(type); + if (!root || !root->head) { + ev->dispatching = false; + scr_dyn_release(event); + return; + } + scr_et_root_retain(root); + ScrEtListener *handler = scr_et_listener_retain(root->head); + while (handler && (root->resist || (handler->flags & SCR_ET_PASSIVE) || !ev->stop)) { + /* Cache the next item in case this iteration removes the current one. */ + ScrEtListener *next = scr_et_listener_retain(handler->next); + if ((handler->flags & SCR_ET_REMOVED) || (ev->stop && !(handler->flags & SCR_ET_RESIST))) { + scr_et_listener_release(handler); + handler = next; + continue; + } + if (handler->flags & SCR_ET_ONCE) { + scr_et_listener_remove(handler); + root->size--; + } + bool passive = (handler->flags & SCR_ET_PASSIVE) != 0; + if (passive) ev->in_passive = true; + scr_et_invoke(handler, self, event); + ev->dispatching = false; + if (ev->in_passive) ev->in_passive = false; + scr_et_listener_release(handler); + handler = next; + } + scr_et_listener_release(handler); + ev->dispatching = false; + scr_et_root_release(root); + scr_dyn_release(event); +} + +ScrDyn *scr_et_dispatch_event(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *const *args, + size_t argc) { + if (argc < 1) { + scr_et_type_error("The \"event\" argument must be specified", "ERR_MISSING_ARGS"); + return NULL; + } + ScrEvent *ev = scr_event_of(args[0]); + if (!ev) { + scr_dyn_arg_type_fail("event", "an instance of Event", args[0]); + return NULL; + } + if (ev->dispatching) { + char message[256]; + snprintf(message, sizeof message, "The event \"%.*s\" is already being dispatched", + (int)ev->type->len, ev->type->data); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "ERR_EVENT_RECURSION"); + return NULL; + } + scr_et_dispatch(core, self, args[0]); + return scr_dyn_new_bool(!(ev->cancelable && ev->default_prevented)); +} + +/* ── event-handler attributes (defineEventHandler) ─────────────────── */ + +static void scr_et_handler_release(void *ctx) { + ScrEtHandler *h = ctx; + if (--h->rc > 0) return; + scr_str_release(h->type); + scr_dyn_release(h->handler); + free(h); +} + +/* The wrapper: `if (typeof handler !== 'function') return; return + * ReflectApply(handler, this, args)`. */ +static void scr_et_handler_fire(void *ctx, ScrDyn *self, ScrDyn *event) { + ScrEtHandler *h = ctx; + if (!h->handler || !scr_dyn_is_callable(h->handler)) return; + ScrDyn *handler = scr_dyn_retain(h->handler); + ScrDyn *args[1] = {event}; + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(handler, args, 1, "handler"); + scr_dyn_this_pop(); + scr_dyn_release(handler); + if (!result) { + scr_et_report(scr_exc_take()); + return; + } + if (result->kind != SCR_DYN_UNDEF && result->kind != SCR_DYN_NULL) scr_et_add_catch(result); + scr_dyn_release(result); +} + +static ScrEtHandler *scr_et_handler_find(ScrEventTargetCore *core, const char *type) { + for (ScrEtHandler *h = core->handlers; h; h = h->next) { + if (scr_et_eq(h->type, type)) return h; + } + return NULL; +} + +ScrDyn *scr_et_handler_get(ScrEventTargetCore *core, const char *type) { + ScrEtHandler *h = scr_et_handler_find(core, type); + return h && h->handler ? scr_dyn_retain(h->handler) : scr_dyn_new_null(); +} + +void scr_et_handler_set(ScrEventTargetCore *core, const char *type, const ScrDyn *value) { + ScrDyn *next = + value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF ? NULL : scr_dyn_retain((ScrDyn *)value); + ScrEtHandler *h = scr_et_handler_find(core, type); + if (h) { + /* Replacing keeps the wrapper's registration position; the listener + * count follows whether the handler is a function. */ + ScrEtRoot *root = scr_et_root_find(core, h->type); + bool was = h->handler && scr_dyn_is_callable(h->handler); + bool now = next && scr_dyn_is_callable(next); + ScrDyn *old = h->handler; + h->handler = next; + scr_dyn_release(old); + if (root) { + if (was) root->size--; + if (now) { + root->size++; + scr_et_new_listener(core, root); + } + } + return; + } + h = scr_et_calloc(sizeof *h); + h->rc = 1; /* the core's map */ + h->type = scr_str_new(type, strlen(type)); + h->handler = next; + h->next = core->handlers; + core->handlers = h; + bool duplicate; + ScrEtListener *l = scr_et_append(core, h->type, NULL, 0, &duplicate); + h->rc++; + l->native = scr_et_handler_fire; + l->ctx = h; + l->ctx_release = scr_et_handler_release; + scr_et_new_listener(core, l->root); +} + +/* ── Event handles ─────────────────────────────────────────────────── */ + +static ScrEvent *scr_event_alloc(ScrStr *type /* moves */, bool custom) { + ScrEvent *ev = scr_et_calloc(sizeof *ev); + ev->rc = 1; + ev->type = type; + ev->custom = custom; + ev->time_stamp = scr_perf_now(); + return ev; +} + +static void *scr_event_retain_v(void *p) { + ((ScrEvent *)p)->rc++; + return p; +} + +static void scr_event_drop_refs(ScrEvent *ev) { + scr_event_untrack(ev); + ScrDyn *target = ev->target; + ScrDyn *detail = ev->detail; + ev->target = NULL; + ev->detail = NULL; + scr_dyn_release(target); + scr_dyn_release(detail); +} + +static void scr_event_release_v(void *p) { + ScrEvent *ev = p; + if (--ev->rc > 0) return; + scr_event_drop_refs(ev); + scr_str_release(ev->type); + free(ev); +} + +static bool scr_et_key(const char *key, size_t len, const char *want) { + return strlen(want) == len && memcmp(key, want, len) == 0; +} + +static ScrDyn *scr_event_target_value(const ScrEvent *ev) { + return ev->target ? scr_dyn_retain(ev->target) : scr_dyn_new_null(); +} + +static ScrDyn *scr_event_get(void *ptr, const char *key, size_t len) { + ScrEvent *ev = ptr; + if (scr_et_key(key, len, "type")) return scr_dyn_new_str(ev->type); + if (scr_et_key(key, len, "target") || scr_et_key(key, len, "srcElement")) + return scr_event_target_value(ev); + if (scr_et_key(key, len, "currentTarget")) + return ev->dispatching ? scr_event_target_value(ev) : scr_dyn_new_null(); + if (scr_et_key(key, len, "cancelable")) return scr_dyn_new_bool(ev->cancelable); + if (scr_et_key(key, len, "defaultPrevented")) + return scr_dyn_new_bool(ev->cancelable && ev->default_prevented); + if (scr_et_key(key, len, "timeStamp")) return scr_dyn_new_num(ev->time_stamp); + if (scr_et_key(key, len, "returnValue")) + return scr_dyn_new_bool(!ev->cancelable || !ev->default_prevented); + if (scr_et_key(key, len, "bubbles")) return scr_dyn_new_bool(ev->bubbles); + if (scr_et_key(key, len, "composed")) return scr_dyn_new_bool(ev->composed); + if (scr_et_key(key, len, "eventPhase")) return scr_dyn_new_num(ev->dispatching ? 2 : 0); + if (scr_et_key(key, len, "cancelBubble")) return scr_dyn_new_bool(ev->propagation_stopped); + if (scr_et_key(key, len, "isTrusted")) return scr_dyn_new_bool(ev->trusted); + if (ev->custom && scr_et_key(key, len, "detail")) + return ev->detail ? scr_dyn_retain(ev->detail) : scr_dyn_new_null(); + return NULL; +} + +static bool scr_event_set(void *ptr, const char *key, size_t len, const ScrDyn *value) { + ScrEvent *ev = ptr; + if (scr_et_key(key, len, "returnValue")) { + if (!scr_dyn_truthy(value) && ev->cancelable && !ev->in_passive) ev->default_prevented = true; + return true; + } + if (scr_et_key(key, len, "cancelBubble")) { + if (scr_dyn_truthy(value)) ev->propagation_stopped = true; + return true; + } + return false; +} + +static void scr_et_not_function(const char *what) { + char message[256]; + snprintf(message, sizeof message, "%s is not a function", what); + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); +} + +static ScrDyn *scr_event_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + ScrEvent *ev = ptr; + (void)self; + if (strcmp(method, "preventDefault") == 0) { + if (ev->cancelable && !ev->in_passive) ev->default_prevented = true; + return scr_dyn_retain(scr_dyn_undefined()); + } + if (strcmp(method, "stopPropagation") == 0) { + ev->propagation_stopped = true; + return scr_dyn_retain(scr_dyn_undefined()); + } + if (strcmp(method, "stopImmediatePropagation") == 0) { + ev->propagation_stopped = true; + ev->stop = true; + return scr_dyn_retain(scr_dyn_undefined()); + } + if (strcmp(method, "composedPath") == 0) { + ScrDyn *path = scr_dyn_new_arr(); + if (ev->dispatching) scr_dyn_arr_push(path, scr_event_target_value(ev)); + return path; + } + if (strcmp(method, "initEvent") == 0) { + if (argc == 0) { + scr_et_type_error("The \"type\" argument must be specified", "ERR_MISSING_ARGS"); + return NULL; + } + if (ev->dispatching) return scr_dyn_retain(scr_dyn_undefined()); + ScrStr *type = scr_dyn_string_coerce_js(args[0]); + if (!type) return NULL; + scr_str_release(ev->type); + ev->type = type; + ev->bubbles = argc > 1 && scr_dyn_truthy(args[1]); + ev->cancelable = argc > 2 && scr_dyn_truthy(args[2]); + return scr_dyn_retain(scr_dyn_undefined()); + } + scr_et_not_function(what); + return NULL; +} + +/* Node's [kInspect]: `Event { type, defaultPrevented, cancelable, + * timeStamp }`. */ +static ScrDyn *scr_event_inspect_view(void *ptr) { + ScrEvent *ev = ptr; + ScrDyn *view = scr_dyn_new_obj(); + scr_dyn_obj_set(view, "type", 4, scr_dyn_new_str(ev->type)); + scr_dyn_obj_set(view, "defaultPrevented", 16, scr_dyn_new_bool(ev->default_prevented)); + scr_dyn_obj_set(view, "cancelable", 10, scr_dyn_new_bool(ev->cancelable)); + scr_dyn_obj_set(view, "timeStamp", 9, scr_dyn_new_num(ev->time_stamp)); + return view; +} + +static const ScrDynHandleOps scr_event_ops = { + .cls = "Event", + .retain = scr_event_retain_v, + .release = scr_event_release_v, + .invoke = scr_event_invoke, + .get = scr_event_get, + .set = scr_event_set, + .inspect_view = scr_event_inspect_view, +}; + +static const ScrDynHandleOps scr_custom_event_ops = { + .cls = "CustomEvent", + .retain = scr_event_retain_v, + .release = scr_event_release_v, + .invoke = scr_event_invoke, + .get = scr_event_get, + .set = scr_event_set, + .inspect_view = scr_event_inspect_view, +}; + +/* ── EventTarget handles ───────────────────────────────────────────── */ + +typedef struct ScrEventTarget { + size_t rc; + ScrEventTargetCore core; +} ScrEventTarget; + +static void *scr_event_target_retain_v(void *p) { + ((ScrEventTarget *)p)->rc++; + return p; +} + +static void scr_event_target_release_v(void *p) { + ScrEventTarget *t = p; + if (--t->rc > 0) return; + scr_et_core_drop(&t->core); + free(t); +} + +static ScrDyn *scr_event_target_invoke(void *ptr, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + ScrEventTarget *t = ptr; + if (strcmp(method, "addEventListener") == 0) return scr_et_add_listener(&t->core, args, argc); + if (strcmp(method, "removeEventListener") == 0) + return scr_et_remove_listener(&t->core, args, argc); + if (strcmp(method, "dispatchEvent") == 0) + return scr_et_dispatch_event(&t->core, self, args, argc); + scr_et_not_function(what); + return NULL; +} + +static ScrDyn *scr_event_target_get(void *ptr, const char *key, size_t len) { + (void)ptr; + (void)key; + (void)len; + return NULL; +} + +static bool scr_event_target_set(void *ptr, const char *key, size_t len, const ScrDyn *value) { + (void)ptr; + (void)key; + (void)len; + (void)value; + return false; +} + +static ScrDyn *scr_event_target_inspect_view(void *ptr) { + (void)ptr; + return scr_dyn_new_obj(); +} + +static const ScrDynHandleOps scr_event_target_ops = { + .cls = "EventTarget", + .retain = scr_event_target_retain_v, + .release = scr_event_target_release_v, + .invoke = scr_event_target_invoke, + .get = scr_event_target_get, + .set = scr_event_target_set, + .inspect_view = scr_event_target_inspect_view, +}; + +static void scr_et_install(void) { + static SCR_TL bool installed; + if (installed) return; + installed = true; + scr_dyn_handle_install(SCR_DYNH_EVENT, &scr_event_ops); + scr_dyn_handle_install(SCR_DYNH_CUSTOM_EVENT, &scr_custom_event_ops); + scr_dyn_handle_install(SCR_DYNH_EVENT_TARGET, &scr_event_target_ops); + scr_et_register_teardown(); +} + +static void scr_et_teardown(void) { + while (scr_et_live_cores) scr_et_core_drop(scr_et_live_cores); + while (scr_et_live_events) scr_event_drop_refs(scr_et_live_events); +} + +ScrDyn *scr_event_target_new(void) { + scr_et_install(); + ScrEventTarget *t = scr_et_calloc(sizeof *t); + t->rc = 1; + scr_et_core_init(&t->core, 10, "EventTarget"); + ScrDyn *out = scr_dyn_new_handle(t, SCR_DYNH_EVENT_TARGET); + scr_event_target_release_v(t); + return out; +} + +ScrDyn *scr_event_new_trusted(const char *type) { + scr_et_install(); + ScrEvent *ev = scr_event_alloc(scr_str_new(type, strlen(type)), false); + ev->trusted = true; + ScrDyn *out = scr_dyn_new_handle(ev, SCR_DYNH_EVENT); + scr_event_release_v(ev); + return out; +} + +/* The constructor's options: null/undefined are none, other non-objects + * throw; bubbles, cancelable and composed are read in order. */ +static ScrEvent *scr_event_construct(ScrDyn *args, bool custom) { + scr_et_install(); + size_t argc = args->kind == SCR_DYN_ARR ? args->v.arr.len : 0; + if (argc == 0) { + scr_et_type_error("The \"type\" argument must be specified", "ERR_MISSING_ARGS"); + return NULL; + } + ScrDyn *const *items = args->v.arr.items; + ScrDyn *options = argc > 1 ? items[1] : NULL; + bool has_options = options && options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL; + if (has_options && (!scr_dyn_is_object(options) || options->kind == SCR_DYN_ARR)) { + scr_dyn_arg_type_fail("options", "of type object", options); + return NULL; + } + bool flags[3] = {false, false, false}; + static const char *const keys[] = {"bubbles", "cancelable", "composed"}; + for (size_t i = 0; has_options && i < 3; i++) { + ScrDyn *v = scr_et_prop(options, keys[i]); + if (!v && scr_exc_pending()) return NULL; + flags[i] = v && scr_dyn_truthy(v); + scr_dyn_release(v); + } + ScrStr *type = scr_dyn_string_coerce_js(items[0]); + if (!type) return NULL; + ScrEvent *ev = scr_event_alloc(type, custom); + ev->bubbles = flags[0]; + ev->cancelable = flags[1]; + ev->composed = flags[2]; + return ev; +} + +ScrDyn *scr_event_new(ScrDyn *args) { + ScrEvent *ev = scr_event_construct(args, false); + if (!ev) return NULL; + ScrDyn *out = scr_dyn_new_handle(ev, SCR_DYNH_EVENT); + scr_event_release_v(ev); + return out; +} + +ScrDyn *scr_custom_event_new(ScrDyn *args) { + ScrEvent *ev = scr_event_construct(args, true); + if (!ev) return NULL; + /* `options?.detail ?? null`, read after the Event fields */ + ScrDyn *options = args->v.arr.len > 1 ? args->v.arr.items[1] : NULL; + if (options && options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_NULL) { + ScrDyn *detail = scr_et_prop(options, "detail"); + if (!detail && scr_exc_pending()) { + scr_event_release_v(ev); + return NULL; + } + if (detail && detail->kind != SCR_DYN_UNDEF && detail->kind != SCR_DYN_NULL) { + ev->detail = detail; + scr_event_track(ev); + } else { + scr_dyn_release(detail); + } + } + ScrDyn *out = scr_dyn_new_handle(ev, SCR_DYNH_CUSTOM_EVENT); + scr_event_release_v(ev); + return out; +} + +bool scr_event_is(const ScrDyn *v, double kind) { + if (v->kind != SCR_DYN_HANDLE) return false; + ScrDynHandleTag tag = v->v.handle.tag; + if (kind == 0) return tag == SCR_DYNH_EVENT || tag == SCR_DYNH_CUSTOM_EVENT; + if (kind == 1) return tag == SCR_DYNH_CUSTOM_EVENT; + return tag == SCR_DYNH_EVENT_TARGET || + (scr_et_signal_core_fn != NULL && tag == SCR_DYNH_ABORT_SIGNAL); +} + +/* ── events.addAbortListener ───────────────────────────────────────── */ + +static void *scr_et_dyn_retain_v(void *p) { return scr_dyn_retain((ScrDyn *)p); } +static void scr_et_dyn_release_v(void *p) { scr_dyn_release((ScrDyn *)p); } + +static ScrDyn *scr_et_box_get(ScrClosure *clo, size_t i) { + return (ScrDyn *)scr_box_get_ref(clo->caps[i]); +} + +static ScrBox *scr_et_dyn_box(ScrDyn *value) { + ScrBox *box = scr_box_new_obj(&scr_et_dyn_retain_v, &scr_et_dyn_release_v, NULL); + scr_box_set_ref(box, scr_dyn_retain(value)); + return box; +} + +static void scr_et_aborted_listener_job(ScrClosure *job) { + ScrDyn *listener = scr_et_box_get(job, 0); + ScrDyn *r = scr_dyn_call(listener, NULL, 0, "listener"); + scr_dyn_release(r); + scr_dyn_release(listener); +} + +/* [SymbolDispose]() { removeEventListener?.(); } */ +static ScrDyn *scr_et_abort_listener_dispose(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + if (clo->ncaps == 2) { + ScrDyn *signal = scr_et_box_get(clo, 0); + ScrDyn *listener = scr_et_box_get(clo, 1); + bool aborted; + ScrEventTargetCore *core = scr_et_signal_core_fn ? scr_et_signal_core_fn(signal, &aborted) : NULL; + if (core) { + ScrStr *abort = scr_str_new("abort", 5); + ScrDyn *type = scr_dyn_new_str(abort); + scr_str_release(abort); + ScrDyn *remove_args[2] = {type, listener}; + ScrDyn *r = scr_et_remove_listener(core, remove_args, 2); + scr_dyn_release(r); + scr_dyn_release(type); + } + scr_dyn_release(signal); + scr_dyn_release(listener); + } + return scr_dyn_retain(scr_dyn_undefined()); +} + +ScrDyn *scr_events_add_abort_listener(ScrDyn *signal, ScrDyn *listener) { + bool aborted = false; + ScrEventTargetCore *core = + signal && scr_et_signal_core_fn ? scr_et_signal_core_fn(signal, &aborted) : NULL; + if (!core) { + scr_dyn_arg_type_fail("signal", "an instance of AbortSignal", signal ? signal : scr_dyn_undefined()); + return NULL; + } + if (!listener || !scr_dyn_is_callable(listener)) { + scr_dyn_arg_type_fail("listener", "of type function", listener ? listener : scr_dyn_undefined()); + return NULL; + } + ScrClosure *dispose; + if (aborted) { + ScrClosure *job = scr_closure_new((void *)&scr_et_aborted_listener_job, 1); + job->caps[0] = scr_et_dyn_box(listener); + scr_queue_microtask(job); + dispose = scr_closure_new((void *)&scr_et_abort_listener_dispose, 0); + } else { + ScrStr *abort = scr_str_new("abort", 5); + bool duplicate; + ScrEtListener *l = + scr_et_append(core, abort, listener, SCR_ET_ONCE | SCR_ET_RESIST, &duplicate); + scr_str_release(abort); + (void)l; + dispose = scr_closure_new((void *)&scr_et_abort_listener_dispose, 2); + dispose->caps[0] = scr_et_dyn_box(signal); + dispose->caps[1] = scr_et_dyn_box(listener); + } + return scr_dyn_new_func(dispose, scr_et_abort_listener_dispose, 0, "()", "[Symbol.dispose]"); +} diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index ae9cdb4dd..98dc818fa 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -322,8 +322,30 @@ static bool scr_ee_emit_va(ScrEmitter *em, ScrStr *name, ...); /* Fires a meta event with the affected event's NAME as the argument * (listeners are fenced to arity <= 1, so the listener-function argument * Node passes second is never read). */ +/* events.errorMonitor's registry key (a symbol in Node; the compiler + * spells it with a leading NUL no admitted literal name carries). */ +static const char scr_ee_error_monitor[] = "\0events.errorMonitor"; +#define SCR_EE_ERROR_MONITOR_LEN (sizeof scr_ee_error_monitor - 1) + +static bool scr_ee_is_error_monitor(const ScrStr *name) { + return name->len == SCR_EE_ERROR_MONITOR_LEN && + memcmp(name->data, scr_ee_error_monitor, SCR_EE_ERROR_MONITOR_LEN) == 0; +} + +static void scr_ee_symbol_name_fence(const char *what) { + char message[160]; + snprintf(message, sizeof message, "%s with an events.errorMonitor listener is not supported yet", + what); + scr_throw_error(SCR_ERR_ERROR, scr_str_new(message, strlen(message))); +} + static void scr_ee_emit_meta(ScrEmitter *em, const char *meta, ScrStr *name) { if (!scr_ee_has(em->reg, meta)) return; + /* Node passes the symbol itself, which the string tuple cannot carry. */ + if (scr_ee_is_error_monitor(name)) { + scr_ee_symbol_name_fence(meta); + return; + } ScrStr *m = scr_str_new(meta, strlen(meta)); scr_ee_emit_va(em, m, name); scr_str_release(m); @@ -631,8 +653,26 @@ ScrEmitter *scr_emitter_remove_all(ScrEmitter *em, ScrStr *name, bool all) { * still holds it (the shared fired flag — Node's wrapper.fired). A * listener throw stops the pass (the exception stays pending for the * caller). Returns whether the bucket had listeners. */ +static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, + ScrDyn *const *flex_args, size_t flex_argc); + +/* emit('error', ...) first emits the same arguments to errorMonitor. */ +static void scr_ee_emit_error_monitor(ScrEmitter *em, va_list *ap_template, + ScrDyn *const *flex_args, size_t flex_argc) { + ScrEeBucket *b = + em->reg ? scr_ee_bucket_find(em->reg, scr_ee_error_monitor, SCR_EE_ERROR_MONITOR_LEN) : NULL; + if (!b || b->n == 0) return; + ScrStr *monitor = scr_str_new(scr_ee_error_monitor, SCR_EE_ERROR_MONITOR_LEN); + scr_ee_emit_core(em, monitor, ap_template, flex_args, flex_argc); + scr_str_release(monitor); +} + static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, ScrDyn *const *flex_args, size_t flex_argc) { + if (name->len == 5 && memcmp(name->data, "error", 5) == 0) { + scr_ee_emit_error_monitor(em, ap_template, flex_args, flex_argc); + if (scr_exc_pending()) return true; + } ScrEeBucket *b = em->reg ? scr_ee_bucket_find(em->reg, name->data, name->len) : NULL; if (!b || b->n == 0) return false; size_t n = b->n; @@ -719,6 +759,8 @@ bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, si * errors. Keep this separate from the statically typed Error ABI above. */ bool scr_emitter_emit_error_flex(ScrEmitter *em, ScrDyn *error) { if (!scr_ee_has(em->reg, "error")) { + scr_ee_emit_error_monitor(em, NULL, &error, 1); + if (scr_exc_pending()) return false; scr_dyn_throw(scr_dyn_retain(error)); return false; } @@ -731,8 +773,17 @@ bool scr_emitter_emit_error_flex(ScrEmitter *em, ScrDyn *error) { /* emit('error', err) — Node's special event: with no 'error' listener the * payload THROWS (catchable; uncaught exits 1 with the error report). * err is borrowed; the throw takes its own +1. */ +static void scr_ee_emit_error_monitor_va(ScrEmitter *em, ...) { + va_list ap; + va_start(ap, em); + scr_ee_emit_error_monitor(em, &ap, NULL, 0); + va_end(ap); +} + bool scr_emitter_emit_error(ScrEmitter *em, ScrStr *name, ScrError *err) { if (!scr_ee_has(em->reg, "error")) { + scr_ee_emit_error_monitor_va(em, err); + if (scr_exc_pending()) return false; scr_throw_obj(scr_error_retain(err), &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); return false; @@ -781,7 +832,14 @@ ScrArr *scr_emitter_event_names(ScrEmitter *em) { ScrArr *out = scr_arr_new(SCR_ELEM_STR, 0); if (em->reg) { for (ScrEeBucket *b = em->reg->head; b; b = b->next) { - if (b->n > 0) scr_arr_push_ref(out, scr_str_retain(b->name)); + if (b->n == 0) continue; + /* Node lists the errorMonitor symbol, which a string[] cannot hold. */ + if (scr_ee_is_error_monitor(b->name)) { + scr_arr_release(out); + scr_ee_symbol_name_fence("eventNames()"); + return scr_arr_new(SCR_ELEM_STR, 0); + } + scr_arr_push_ref(out, scr_str_retain(b->name)); } } return out; diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 27f0de01a..05bc29fee 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -26,7 +26,6 @@ static void sf_signal_install(void); typedef struct SfTransfer SfTransfer; typedef struct SfSignal SfSignal; typedef struct SfSignalWatch SfSignalWatch; -typedef struct SfEvent SfEvent; typedef struct SfStream SfStream; typedef struct SfReader SfReader; typedef struct SfResponse SfResponse; @@ -53,17 +52,6 @@ struct SfTypedRef { SfTypedRef *next; }; -typedef struct SfAbortListener { - ScrDyn *listener; - bool capture; - bool once; - size_t id; - SfSignal *target; /* weak; the target owns this listener */ - SfSignal *option_signal; /* owned unless it is the target */ - SfSignalWatch *signal_watch; - struct SfAbortListener *next; -} SfAbortListener; - struct SfSignalWatch { SfSignal *source; /* weak; NULL after the source fires */ void *owner; /* owner keeps source alive */ @@ -80,26 +68,12 @@ struct SfSignal { ScrDynHandleTag reason_self_tag; bool reason_tracked; SfSignal *reason_next; - ScrDyn *onabort; - size_t onabort_order; - SfAbortListener *listeners; - size_t next_listener_id; + ScrEventTargetCore core; /* listeners and the onabort attribute */ SfSignalWatch *watchers; SfSignal **sources; SfSignalWatch **source_watches; size_t source_count; double timer_id; - bool callbacks_tracked; - SfSignal *callbacks_next; -}; - -struct SfEvent { - size_t rc; - SfSignal *target; - double time_stamp; - bool dispatching; - bool stop_propagation; - bool stop_immediate; }; struct SfReader { @@ -248,7 +222,6 @@ static SfTransfer *sf_live; /* Weak registries: owners unlink themselves on destruction. They let the * fetch teardown sever opaque native→dyn edges before final cycle collection, * including otherwise-untraceable callback/reason→handle backedges. */ -static SfSignal *sf_callback_signals; static SfSignal *sf_reason_signals; #ifndef SCR_FETCH_SIGNAL_ONLY static SfStream *sf_callback_streams; @@ -340,26 +313,6 @@ static ScrDyn *sf_type_reason(const char *message, ScrError **error_out) { /* ── AbortSignal ─────────────────────────────────────────────────── */ -static void sf_signal_track_callbacks(SfSignal *s) { - if (s->callbacks_tracked) return; - s->callbacks_tracked = true; - s->callbacks_next = sf_callback_signals; - sf_callback_signals = s; -} - -static void sf_signal_untrack_callbacks(SfSignal *s) { - if (!s->callbacks_tracked) return; - for (SfSignal **at = &sf_callback_signals; *at; - at = &(*at)->callbacks_next) { - if (*at == s) { - *at = s->callbacks_next; - break; - } - } - s->callbacks_tracked = false; - s->callbacks_next = NULL; -} - static void sf_signal_track_reason(SfSignal *s) { if (s->reason_tracked) return; s->reason_tracked = true; @@ -414,6 +367,8 @@ static SfSignal *sf_signal_new(void) { SfSignal *s = calloc(1, sizeof *s); if (!s) sf_oom(); s->rc = 1; + /* AbortSignal sets kMaxEventTargetListeners to 0: no leak warning. */ + scr_et_core_init(&s->core, 0, "AbortSignal"); return s; } @@ -437,32 +392,6 @@ static void sf_watch_free(SfSignalWatch *w) { free(w); } -static void sf_abort_listener_free(SfAbortListener *l) { - if (!l) return; - sf_watch_free(l->signal_watch); - if (l->option_signal && l->option_signal != l->target) { - sf_signal_release(l->option_signal); - } - scr_dyn_release(l->listener); - free(l); -} - -static void sf_signal_drop_callbacks(SfSignal *s) { - SfAbortListener *listeners = s->listeners; - ScrDyn *onabort = s->onabort; - s->listeners = NULL; /* unlink before callback releases can re-enter */ - s->onabort = NULL; - s->onabort_order = 0; - sf_signal_untrack_callbacks(s); - while (listeners) { - SfAbortListener *next = listeners->next; - listeners->next = NULL; - sf_abort_listener_free(listeners); - listeners = next; - } - scr_dyn_release(onabort); -} - static void sf_signal_release(SfSignal *s) { if (!s || --s->rc > 0) return; for (size_t i = 0; i < s->source_count; i++) { @@ -471,7 +400,7 @@ static void sf_signal_release(SfSignal *s) { } free(s->source_watches); free(s->sources); - sf_signal_drop_callbacks(s); + scr_et_core_drop(&s->core); sf_signal_drop_reason(s); free(s); } @@ -483,148 +412,6 @@ static void sf_signal_release_v(void *p) { sf_signal_release((SfSignal *)p); } -static SfEvent *sf_event_new(SfSignal *target) { - SfEvent *event = calloc(1, sizeof *event); - if (!event) sf_oom(); - event->rc = 1; - event->target = sf_signal_retain(target); - event->time_stamp = scr_perf_now(); - event->dispatching = true; - return event; -} - -static SfEvent *sf_event_retain(SfEvent *event) { - event->rc++; - return event; -} - -static void sf_event_release(SfEvent *event) { - if (!event || --event->rc > 0) return; - sf_signal_release(event->target); - free(event); -} - -static void *sf_event_retain_v(void *ptr) { - return sf_event_retain((SfEvent *)ptr); -} - -static void sf_event_release_v(void *ptr) { - sf_event_release((SfEvent *)ptr); -} - -static void *sf_listener_error_retain_v(void *ptr) { - return scr_caught_retain((ScrCaught *)ptr); -} - -static void sf_listener_error_release_v(void *ptr) { - scr_caught_release((ScrCaught *)ptr); -} - -static void sf_listener_error_tick(ScrClosure *cb) { - ScrCaught *caught = scr_box_get_ref(cb->caps[0]); - scr_rethrow(caught); - scr_caught_release(caught); -} - -/* - * EventTarget reports listener exceptions as uncaught next-tick failures; - * dispatchEvent itself keeps running listeners and returns its boolean. - * Queue every failure separately so an uncaughtException handler that - * recovers from one still observes later listener failures in order. - */ -static void sf_defer_listener_error(ScrCaught *caught) { - ScrClosure *cb = - scr_closure_new((void *)&sf_listener_error_tick, 1); - cb->caps[0] = scr_box_new_obj( - &sf_listener_error_retain_v, &sf_listener_error_release_v, NULL); - scr_box_set_ref(cb->caps[0], caught); /* moves */ - scr_next_tick(cb); -} - -static ScrDyn *sf_event_invoke(void *ptr, ScrDyn *self, - const char *method, ScrDyn *const *args, - size_t argc, const char *what) { - SfEvent *event = ptr; - (void)self; - (void)args; - (void)argc; - if (strcmp(method, "preventDefault") == 0) { - /* Abort events are not cancelable. */ - return scr_dyn_retain(scr_dyn_undefined()); - } - if (strcmp(method, "stopPropagation") == 0) { - event->stop_propagation = true; - return scr_dyn_retain(scr_dyn_undefined()); - } - if (strcmp(method, "stopImmediatePropagation") == 0) { - event->stop_propagation = true; - event->stop_immediate = true; - return scr_dyn_retain(scr_dyn_undefined()); - } - if (strcmp(method, "composedPath") == 0) { - ScrDyn *path = scr_dyn_new_arr(); - if (event->dispatching) { - scr_dyn_arr_push( - path, - scr_dyn_new_handle(event->target, SCR_DYNH_ABORT_SIGNAL)); - } - return path; - } - sf_not_function(what); - return NULL; -} - -static ScrDyn *sf_event_get(void *ptr, const char *key, size_t len) { - SfEvent *event = ptr; - if (sf_name(key, len, "type")) { - ScrStr *type = scr_str_new("abort", 5); - ScrDyn *out = scr_dyn_new_str(type); - scr_str_release(type); - return out; - } - if (sf_name(key, len, "bubbles") || - sf_name(key, len, "cancelable") || - sf_name(key, len, "composed") || - sf_name(key, len, "defaultPrevented")) { - return scr_dyn_new_bool(false); - } - if (sf_name(key, len, "eventPhase")) { - return scr_dyn_new_num(event->dispatching ? 2 : 0); - } - if (sf_name(key, len, "target") || - sf_name(key, len, "srcElement")) { - return scr_dyn_new_handle(event->target, SCR_DYNH_ABORT_SIGNAL); - } - if (sf_name(key, len, "currentTarget")) { - return event->dispatching - ? scr_dyn_new_handle(event->target, SCR_DYNH_ABORT_SIGNAL) - : scr_dyn_new_null(); - } - if (sf_name(key, len, "isTrusted")) return scr_dyn_new_bool(true); - if (sf_name(key, len, "timeStamp")) { - return scr_dyn_new_num(event->time_stamp); - } - if (sf_name(key, len, "cancelBubble")) { - return scr_dyn_new_bool(event->stop_propagation); - } - if (sf_name(key, len, "returnValue")) return scr_dyn_new_bool(true); - return NULL; -} - -static bool sf_event_set(void *ptr, const char *key, size_t len, - const ScrDyn *value) { - SfEvent *event = ptr; - if (sf_name(key, len, "cancelBubble")) { - if (scr_dyn_truthy(value)) event->stop_propagation = true; - return true; - } - if (sf_name(key, len, "returnValue")) { - /* Setting false only prevents a cancelable event's default action. */ - return true; - } - return false; -} - static SfSignalWatch *sf_signal_watch(SfSignal *source, void *owner, void (*fire)(SfSignalWatch *, SfSignal *)) { @@ -638,199 +425,12 @@ static SfSignalWatch *sf_signal_watch(SfSignal *source, void *owner, return w; } -static void sf_listener_signal_abort(SfSignalWatch *w, - SfSignal *source) { - (void)source; - SfAbortListener *listener = w->owner; - SfSignal *target = listener->target; - for (SfAbortListener **at = &target->listeners; *at; - at = &(*at)->next) { - if (*at == listener) { - *at = listener->next; - sf_abort_listener_free(listener); - return; - } - } - sf_watch_free(w); -} - -static void sf_signal_dispatch_listeners(SfSignal *s, ScrDyn *provided_event) { - SfEvent *event_state; - ScrDyn *event; - if (provided_event) { - event = scr_dyn_retain(provided_event); - event_state = (SfEvent *)provided_event->v.handle.ptr; - SfSignal *old_target = event_state->target; - event_state->target = sf_signal_retain(s); - sf_signal_release(old_target); - event_state->dispatching = true; - } else { - event_state = sf_event_new(s); - event = scr_dyn_new_handle(event_state, SCR_DYNH_EVENT); - sf_event_release(event_state); - } - /* - * EventTarget dispatch snapshots the handlers present at the start: - * additions wait for the next event, removals before a listener's turn - * suppress it, and a listener may remove itself safely. `onabort` occupies - * the position where it was first assigned, just like an event-handler - * attribute in the DOM. Holding raw list nodes across the user callback is - * not safe because removeEventListener frees those nodes. - */ - size_t count = s->onabort && s->onabort->kind == SCR_DYN_FUNC ? 1 : 0; - for (SfAbortListener *l = s->listeners; l; l = l->next) count++; - typedef struct { - ScrDyn *listener; - bool once; - bool attribute; - size_t id; - } SfAbortDispatch; - SfAbortDispatch *dispatch = - count ? calloc(count, sizeof *dispatch) : NULL; - if (count && !dispatch) sf_oom(); - size_t i = 0; - if (s->onabort && s->onabort->kind == SCR_DYN_FUNC) { - dispatch[i].listener = scr_dyn_retain(s->onabort); - dispatch[i].attribute = true; - dispatch[i].id = s->onabort_order; - i++; - } - for (SfAbortListener *l = s->listeners; l; l = l->next) { - dispatch[i].listener = scr_dyn_retain(l->listener); - dispatch[i].once = l->once; - dispatch[i].id = l->id; - i++; - } - for (i = 1; i < count; i++) { - SfAbortDispatch entry = dispatch[i]; - size_t j = i; - while (j > 0 && dispatch[j - 1].id > entry.id) { - dispatch[j] = dispatch[j - 1]; - j--; - } - dispatch[j] = entry; - } - - for (i = 0; i < count && !event_state->stop_immediate; i++) { - if (dispatch[i].attribute) { - if (!s->onabort || s->onabort_order != dispatch[i].id) { - scr_dyn_release(dispatch[i].listener); - continue; - } - /* - * Assignment replaces the handler without moving its registration - * position. Invoke the current value if an earlier callback replaced - * it during this dispatch. - */ - ScrDyn *handler = scr_dyn_retain(s->onabort); - ScrDyn *args[1] = {event}; - scr_dyn_this_push(s, SCR_DYNH_ABORT_SIGNAL); - ScrDyn *r = scr_dyn_call(handler, args, 1, "signal.onabort"); - scr_dyn_this_pop(); - scr_dyn_release(r); - scr_dyn_release(handler); - scr_dyn_release(dispatch[i].listener); - if (scr_exc_pending()) { - sf_defer_listener_error(scr_exc_take()); - } - if (event_state->stop_immediate) { - i++; - break; - } - continue; - } - SfAbortListener **at = &s->listeners; - while (*at && (*at)->id != dispatch[i].id) { - at = &(*at)->next; - } - if (!*at) { - scr_dyn_release(dispatch[i].listener); - continue; - } - if (dispatch[i].once) { - SfAbortListener *l = *at; - *at = l->next; - sf_abort_listener_free(l); - } - ScrDyn *callable = dispatch[i].listener; - ScrDyn *current_listener = NULL; - ScrDyn *owned_handle = NULL; - ScrDyn *this_listener = NULL; - if (callable->kind == SCR_DYN_OBJ) { - /* - * EventListener is a Web IDL callback interface: handleEvent is read - * when the callback runs, not when it is registered. Typed records - * normally cross into dyn as value snapshots, so explicitly refresh - * an identity-carrying listener from its retained source here. - */ - current_listener = - callable->v.obj.source_identity && callable->v.obj.source_access - ? callable->v.obj.source_access( - callable->v.obj.source_identity, true) - : scr_dyn_retain(callable); - this_listener = current_listener; - const ScrDyn *handle = - scr_dyn_obj_get(current_listener, "handleEvent", 11); - if (!handle) { - scr_dyn_release(current_listener); - scr_dyn_release(dispatch[i].listener); - continue; - } - callable = (ScrDyn *)handle; - } else if (callable->kind == SCR_DYN_TYPED_REF) { - /* A live static record must remain the EventListener receiver so - * handleEvent mutations commit to the original source. Arrays and - * byte views are valid Web IDL objects but have no handleEvent in - * the statically modeled surface, so dispatching them is a no-op. */ - current_listener = scr_dyn_typed_ref_materialize(callable); - if (current_listener->kind != SCR_DYN_OBJ) { - scr_dyn_release(current_listener); - scr_dyn_release(dispatch[i].listener); - continue; - } - const ScrDyn *handle = - scr_dyn_obj_get(current_listener, "handleEvent", 11); - if (!handle) { - scr_dyn_release(current_listener); - scr_dyn_release(dispatch[i].listener); - continue; - } - this_listener = callable; - callable = (ScrDyn *)handle; - } else if (callable->kind != SCR_DYN_FUNC) { - /* Web IDL accepts every object as an EventListener. Native arrays, - * byte views, handles, and promises cannot carry an expando - * handleEvent in the static runtime, so a missing method is a no-op. */ - scr_dyn_release(dispatch[i].listener); - continue; - } - ScrDyn *args[1] = {event}; - if (this_listener) { - scr_dyn_this_push_dyn(this_listener); - } else { - scr_dyn_this_push(s, SCR_DYNH_ABORT_SIGNAL); - } - ScrDyn *r = - scr_dyn_call(callable, args, 1, "abort listener"); - scr_dyn_this_pop(); - scr_dyn_release(r); - scr_dyn_release(owned_handle); - scr_dyn_release(current_listener); - scr_dyn_release(dispatch[i].listener); - if (scr_exc_pending()) { - sf_defer_listener_error(scr_exc_take()); - } - if (event_state->stop_immediate) { - i++; - break; - } - } - while (i < count) { - scr_dyn_release(dispatch[i].listener); - i++; - } - event_state->dispatching = false; - free(dispatch); +/* runAbort: a trusted 'abort' Event through the signal's EventTarget. */ +static void sf_signal_dispatch_abort(SfSignal *s) { + ScrDyn *event = scr_event_new_trusted("abort"); + ScrDyn *self = scr_dyn_new_handle(s, SCR_DYNH_ABORT_SIGNAL); + scr_et_dispatch(&s->core, self, event); + scr_dyn_release(self); scr_dyn_release(event); } @@ -855,7 +455,7 @@ static void sf_signal_abort_full(SfSignal *s, ScrDyn *reason, w->source = NULL; w->fire(w, s); /* the owner may free w */ } - sf_signal_dispatch_listeners(s, NULL); + sf_signal_dispatch_abort(s); } static void sf_signal_abort_default(SfSignal *s, bool timeout) { @@ -879,6 +479,14 @@ static SfSignal *sf_signal_of(const ScrDyn *d, const char *where) { return (SfSignal *)d->v.handle.ptr; } +/* options.signal for the EventTarget engine. */ +static ScrEventTargetCore *sf_signal_core_of(const ScrDyn *d, bool *aborted) { + if (!d || d->kind != SCR_DYN_HANDLE || d->v.handle.tag != SCR_DYNH_ABORT_SIGNAL) return NULL; + SfSignal *s = (SfSignal *)d->v.handle.ptr; + *aborted = s->aborted; + return &s->core; +} + static void sf_signal_timer_fire(ScrClosure *cb) { SfSignal *s = (SfSignal *)scr_box_get_ref(cb->caps[0]); if (!s) return; @@ -1031,63 +639,6 @@ static ScrDyn *sf_abort_controller_get( return NULL; } -static bool sf_signal_listener_options( - ScrDyn *const *args, size_t argc, bool *capture, bool *once, - SfSignal **option_signal) { - *capture = false; - *once = false; - if (option_signal) *option_signal = NULL; - if (argc < 3) return true; - if (args[2]->kind == SCR_DYN_BOOL) { - /* EventTarget's boolean overload is `useCapture`, never `once`. */ - *capture = args[2]->v.b; - return true; - } - if (args[2]->kind != SCR_DYN_OBJ) return true; - const ScrDyn *capture_value = - scr_dyn_obj_get(args[2], "capture", 7); - const ScrDyn *once_value = scr_dyn_obj_get(args[2], "once", 4); - *capture = capture_value && scr_dyn_truthy(capture_value); - *once = once_value && scr_dyn_truthy(once_value); - if (option_signal) { - const ScrDyn *signal_value = - scr_dyn_obj_get(args[2], "signal", 6); - if (signal_value && signal_value->kind != SCR_DYN_UNDEF) { - *option_signal = sf_signal_of( - signal_value, - "AddEventListenerOptions.signal must be an AbortSignal"); - if (!*option_signal) return false; - } - } - return true; -} - -static bool sf_abort_listener_value(const ScrDyn *listener) { - /* Web IDL callback interfaces accept any object. handleEvent is looked up - * when the event is dispatched, where a non-callable value reports an - * asynchronous listener error and a missing value is a no-op. */ - switch (listener->kind) { - case SCR_DYN_FUNC: - case SCR_DYN_OBJ: - case SCR_DYN_ARR: - case SCR_DYN_BYTES: - case SCR_DYN_HANDLE: - case SCR_DYN_PROMISE: - return true; - - case SCR_DYN_TYPED_REF: - return scr_dyn_ref_typeof_is(listener, "function") || - scr_dyn_ref_typeof_is(listener, "object"); - default: - return false; - } -} - -static bool sf_abort_listener_equal( - const ScrDyn *a, const ScrDyn *b) { - return scr_dyn_strict_eq(a, b) || scr_dyn_obj_same_source(a, b); -} - static ScrDyn *sf_signal_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { @@ -1101,113 +652,11 @@ static ScrDyn *sf_signal_invoke(void *ptr, ScrDyn *self, const char *method, } return scr_dyn_retain(scr_dyn_undefined()); } - if (strcmp(method, "addEventListener") == 0) { - if (argc < 2) { - sf_type_error("AbortSignal.addEventListener requires an event name"); - return NULL; - } - ScrStr *event_name = scr_dyn_string_coerce_js(args[0]); - if (!event_name) return NULL; - bool is_abort_event = - sf_name(event_name->data, event_name->len, "abort"); - scr_str_release(event_name); - /* Web IDL's nullable EventListener argument: null is a no-op. */ - if (args[1]->kind == SCR_DYN_NULL || - args[1]->kind == SCR_DYN_UNDEF) { - return scr_dyn_retain(scr_dyn_undefined()); - } - if (!sf_abort_listener_value(args[1])) { - sf_type_error( - "AbortSignal.addEventListener listener must be a function, " - "an object with handleEvent, or null"); - return NULL; - } - bool capture = false; - bool once = false; - SfSignal *option_signal = NULL; - if (!sf_signal_listener_options( - args, argc, &capture, &once, &option_signal)) { - return NULL; - } - /* AbortSignal is an EventTarget: listeners for other event names are - * valid registrations even though this target only dispatches abort. */ - if (!is_abort_event) { - return scr_dyn_retain(scr_dyn_undefined()); - } - if (option_signal && option_signal->aborted) { - return scr_dyn_retain(scr_dyn_undefined()); - } - SfAbortListener **tail = &s->listeners; - while (*tail) { - if ((*tail)->capture == capture && - sf_abort_listener_equal((*tail)->listener, args[1])) { - return scr_dyn_retain(scr_dyn_undefined()); - } - tail = &(*tail)->next; - } - SfAbortListener *l = calloc(1, sizeof *l); - if (!l) sf_oom(); - l->listener = scr_dyn_retain(args[1]); - l->capture = capture; - l->once = once; - l->id = ++s->next_listener_id; - l->target = s; - l->option_signal = - option_signal && option_signal != s - ? sf_signal_retain(option_signal) - : option_signal; - *tail = l; - if (option_signal) { - l->signal_watch = - sf_signal_watch(option_signal, l, &sf_listener_signal_abort); - } - sf_signal_track_callbacks(s); - return scr_dyn_retain(scr_dyn_undefined()); - } - if (strcmp(method, "removeEventListener") == 0) { - if (argc < 2) { - sf_type_error( - "AbortSignal.removeEventListener requires an event name and listener"); - return NULL; - } - ScrStr *event_name = scr_dyn_string_coerce_js(args[0]); - if (!event_name) return NULL; - bool is_abort_event = - sf_name(event_name->data, event_name->len, "abort"); - scr_str_release(event_name); - if (is_abort_event) { - bool capture = false; - bool once = false; - (void)sf_signal_listener_options( - args, argc, &capture, &once, NULL); - (void)once; - for (SfAbortListener **at = &s->listeners; *at; at = &(*at)->next) { - if ((*at)->capture == capture && - sf_abort_listener_equal((*at)->listener, args[1])) { - SfAbortListener *l = *at; - *at = l->next; - sf_abort_listener_free(l); - break; - } - } - } - return scr_dyn_retain(scr_dyn_undefined()); - } - if (strcmp(method, "dispatchEvent") == 0) { - if (argc != 1 || args[0]->kind != SCR_DYN_HANDLE || - args[0]->v.handle.tag != SCR_DYNH_EVENT) { - sf_type_error("AbortSignal.dispatchEvent requires an Event"); - return NULL; - } - SfEvent *event = (SfEvent *)args[0]->v.handle.ptr; - if (event->dispatching) { - sf_type_error("The event is already being dispatched"); - return NULL; - } - sf_signal_dispatch_listeners(s, args[0]); - if (scr_exc_pending()) return NULL; - return scr_dyn_new_bool(true); - } + if (strcmp(method, "addEventListener") == 0) return scr_et_add_listener(&s->core, args, argc); + if (strcmp(method, "removeEventListener") == 0) + return scr_et_remove_listener(&s->core, args, argc); + if (strcmp(method, "dispatchEvent") == 0) + return scr_et_dispatch_event(&s->core, self, args, argc); (void)self; sf_not_function(what); return NULL; @@ -1220,9 +669,7 @@ static ScrDyn *sf_signal_get(void *ptr, const char *key, size_t len) { return s->aborted ? sf_signal_reason_ref(s) : scr_dyn_retain(scr_dyn_undefined()); } - if (sf_name(key, len, "onabort")) { - return s->onabort ? scr_dyn_retain(s->onabort) : scr_dyn_new_null(); - } + if (sf_name(key, len, "onabort")) return scr_et_handler_get(&s->core, "abort"); return NULL; } @@ -1230,19 +677,7 @@ static bool sf_signal_set(void *ptr, const char *key, size_t len, const ScrDyn *value) { SfSignal *s = ptr; if (!sf_name(key, len, "onabort")) return false; - bool had_handler = s->onabort && s->onabort->kind == SCR_DYN_FUNC; - scr_dyn_release(s->onabort); - if (value->kind != SCR_DYN_NULL && value->kind != SCR_DYN_UNDEF) { - s->onabort = scr_dyn_retain((ScrDyn *)value); - } else { - s->onabort = NULL; - } - if (value->kind == SCR_DYN_FUNC) { - if (!had_handler) s->onabort_order = ++s->next_listener_id; - sf_signal_track_callbacks(s); - } else { - s->onabort_order = 0; - } + scr_et_handler_set(&s->core, "abort", value); return true; } @@ -5627,9 +5062,6 @@ static const ScrDynHandleOps sf_response_ops = { static const ScrDynHandleOps sf_headers_ops = { "Headers", &sf_headers_retain_v, &sf_headers_release_v, &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL}; -static const ScrDynHandleOps sf_event_ops = { - "Event", &sf_event_retain_v, &sf_event_release_v, - &sf_event_invoke, &sf_event_get, &sf_event_set, NULL}; static void sf_teardown(void) { while (sf_live) { @@ -5647,9 +5079,6 @@ static void sf_teardown(void) { while (sf_callback_streams) { sf_stream_drop_source_callbacks(sf_callback_streams, true); } - while (sf_callback_signals) { - sf_signal_drop_callbacks(sf_callback_signals); - } while (sf_reason_signals) { sf_signal_drop_reason(sf_reason_signals); } @@ -5673,7 +5102,7 @@ void scr_fetch_install(void) { scr_dyn_handle_install(SCR_DYNH_FETCH_REQUEST, &sf_request_ops); scr_dyn_handle_install(SCR_DYNH_FETCH_RESPONSE, &sf_response_ops); scr_dyn_handle_install(SCR_DYNH_FETCH_HEADERS, &sf_headers_ops); - scr_dyn_handle_install(SCR_DYNH_EVENT, &sf_event_ops); + scr_et_signal_core_fn = sf_signal_core_of; scr_atexit(sf_teardown); } @@ -5689,11 +5118,7 @@ static const ScrDynHandleOps sf_signal_ops = { static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL }; -static const ScrDynHandleOps sf_event_ops = { - "Event", &sf_event_retain_v, &sf_event_release_v, - &sf_event_invoke, &sf_event_get, &sf_event_set, NULL }; static void sf_signal_teardown(void) { - while (sf_callback_signals) sf_signal_drop_callbacks(sf_callback_signals); while (sf_reason_signals) sf_signal_drop_reason(sf_reason_signals); } static void sf_signal_install(void) { @@ -5702,7 +5127,7 @@ static void sf_signal_install(void) { installed = true; scr_dyn_handle_install(SCR_DYNH_ABORT_SIGNAL, &sf_signal_ops); scr_dyn_handle_install(SCR_DYNH_ABORT_CONTROLLER, &sf_abort_controller_ops); - scr_dyn_handle_install(SCR_DYNH_EVENT, &sf_event_ops); + scr_et_signal_core_fn = sf_signal_core_of; scr_atexit(sf_signal_teardown); } diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 45e3bf3a4..7448609e4 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -967,6 +967,21 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER) { return scr_insp_array_buffer(d->v.handle.ptr, scr_dyn_handle_cls(d), recurse, depth); } + const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(d); + if (ops->inspect_view) { + /* Node's custom inspectors answer the bare class name beyond the + * depth limit (`depth < 0`). */ + if (recurse > depth) return scr_str_new(ops->cls, strlen(ops->cls)); + ScrDyn *view = ops->inspect_view(d->v.handle.ptr); + ScrStr *body = scr_insp_dyn(view, recurse, depth); + scr_dyn_release(view); + InspBuf out = {0}; + ib_cstr(&out, ops->cls); + ib_char(&out, ' '); + ib_bytes(&out, body->data, body->len); + scr_str_release(body); + return ib_take(&out); + } /* Node prints the class's full property dump (ServerResponse { * ... }) — internals we do not model. Fence loudly instead of a * silent-wrong shape (SEMANTICS.md). */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 89cc33eb2..3a80e2795 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4556,7 +4556,8 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { case SCR_DYN_OBJ: tag = "[object Object]"; break; case SCR_DYN_HANDLE: if ((d->v.handle.tag >= SCR_DYNH_ABORT_SIGNAL && - d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || + d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_EVENT_TARGET || + d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_DATE) { @@ -5093,7 +5094,8 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { return out; } if ((d->v.handle.tag >= SCR_DYNH_ABORT_SIGNAL && - d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP) { + d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_EVENT_TARGET || + d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "[object "); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index de6b37189..c1b66e35b 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -1892,6 +1892,7 @@ void scr_os_ifaddrs_free(ScrIfaddrs *s) { /* The events-unit hooks (scr_events.c fills them at install; NULL in * event-free binaries and in the standalone runtime C tests). */ SCR_TL void (*scr_process_exit_hook)(double code) = NULL; +SCR_TL void (*scr_warning_emit_hook)(const char *name, const char *code, ScrStr *message) = NULL; SCR_TL void (*scr_stdin_destroy_hook)(void) = NULL; void scr_process_set_exit_code(double code) { diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index a34562e75..22514ed31 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -4633,7 +4633,7 @@ typedef enum { SCR_DYNH_WEB_CONTROLLER, /* native ReadableStreamDefaultController */ SCR_DYNH_FETCH_RESPONSE, /* native static-fetch Response */ SCR_DYNH_FETCH_HEADERS, /* native static-fetch response Headers */ - SCR_DYNH_EVENT, /* native static-fetch abort Event */ + SCR_DYNH_EVENT, /* ScrEvent — new Event() and AbortSignal's abort events */ SCR_DYNH_ABORT_CONTROLLER, /* native static-fetch AbortController */ SCR_DYNH_CHILD, /* child_process.ChildProcess */ SCR_DYNH_FILE_HANDLE, /* fs/promises.FileHandle */ @@ -4658,6 +4658,8 @@ typedef enum { SCR_DYNH_MESSAGE_PORT, SCR_DYNH_SHARED_ARRAY_BUFFER, SCR_DYNH_CONSOLE_INSTANCE, /* node:console Console instance */ + SCR_DYNH_EVENT_TARGET, /* ScrEventTarget — new EventTarget() */ + SCR_DYNH_CUSTOM_EVENT, /* ScrEvent with detail — new CustomEvent() */ SCR_DYNH_COUNT, } ScrDynHandleTag; @@ -5406,9 +5408,81 @@ typedef struct ScrDynHandleOps { ScrDyn *(*iter_pack)(void *h); /* A live native iterator step; index advances only when a value is emitted. */ ScrDyn *(*iter_step)(void *h, double *index, unsigned selection); + /* util.inspect renders ` ` for the dyn object this answers + * (+1); NULL = the class has no inspection (the loud fence). */ + ScrDyn *(*inspect_view)(void *h); } ScrDynHandleOps; ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection); +/* ── EventTarget / Event / CustomEvent (scr_event_target.c) ─────────── + * Node's internal/event_target over checked-dynamic values. A target's + * listeners live in a ScrEventTargetCore: per event type, a linked list + * in registration order whose nodes keep their `next` link after removal, + * so a dispatch that cached a removed node continues where Node's does. + * Plain `new EventTarget()` values are SCR_DYNH_EVENT_TARGET handles; + * AbortSignal embeds a core of its own (MaxListeners 0, the `onabort` + * event-handler attribute). Events are ScrEvent handles (SCR_DYNH_EVENT, + * SCR_DYNH_CUSTOM_EVENT for CustomEvent). Listener exceptions, and + * rejections of a listener's returned promise or of a handleEvent call, + * are reported as uncaught exceptions from a nextTick, like Node. */ +typedef struct ScrEtRoot ScrEtRoot; +typedef struct ScrEtHandler ScrEtHandler; +typedef struct ScrEtHook ScrEtHook; +typedef struct ScrEventTargetCore { + ScrEtRoot *roots; /* per event type, in first-add order */ + ScrEtHandler *handlers; /* event-handler attributes (`onabort`) */ + ScrEtHook *hooks; /* options.signal removal hooks aimed here (weak) */ + double max_listeners; /* 0 = no leak warning (AbortSignal) */ + bool warned; + const char *cls; /* the warning's target name ("EventTarget") */ + bool tracked; /* in the exit teardown registry */ + struct ScrEventTargetCore *tracked_prev, *tracked_next; +} ScrEventTargetCore; + +typedef struct ScrEvent { + size_t rc; + ScrStr *type; + ScrDyn *target; /* +1, NULL until dispatched */ + ScrDyn *detail; /* CustomEvent's detail (+1); NULL for Event */ + double time_stamp; + bool custom, bubbles, cancelable, composed, trusted; + bool default_prevented, propagation_stopped, stop, dispatching, in_passive; + bool tracked; + struct ScrEvent *tracked_prev, *tracked_next; +} ScrEvent; + +void scr_et_core_init(ScrEventTargetCore *core, double max_listeners, const char *cls); +/* Drops every listener and handler (teardown and target destruction). */ +void scr_et_core_drop(ScrEventTargetCore *core); +/* The EventTarget methods over a core: `self` is the target's own dyn + * value (borrowed — `this` for listeners and the events' target), args + * borrowed. +1 result, or NULL with the exception pending. */ +ScrDyn *scr_et_add_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); +ScrDyn *scr_et_remove_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); +ScrDyn *scr_et_dispatch_event(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *const *args, size_t argc); +/* Dispatches an event handle (borrowed) without the argument checks. */ +void scr_et_dispatch(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *event); +/* `on` attributes (Node's defineEventHandler): get answers +1 (null + * when unset); set registers the wrapper listener on first assignment. */ +ScrDyn *scr_et_handler_get(ScrEventTargetCore *core, const char *type); +void scr_et_handler_set(ScrEventTargetCore *core, const char *type, const ScrDyn *value); +/* A trusted (isTrusted) Event handle of the given type, +1. */ +ScrDyn *scr_event_new_trusted(const char *type); +/* new EventTarget() / new Event(type, options) / new CustomEvent(type, + * options): `args` is the argument vector as one dyn array (borrowed). + * +1 handle, or NULL with the exception pending. */ +ScrDyn *scr_event_target_new(void); +ScrDyn *scr_event_new(ScrDyn *args); +ScrDyn *scr_custom_event_new(ScrDyn *args); +/* `v instanceof Event` (kind 0), `CustomEvent` (1), `EventTarget` (2). */ +bool scr_event_is(const ScrDyn *v, double kind); +/* The AbortSignal unit's view of its handles for options.signal: the + * signal's core and aborted flag, or NULL for a non-signal value. + * Installed by the AbortSignal unit; NULL when no signal can exist. */ +extern SCR_TL ScrEventTargetCore *(*scr_et_signal_core_fn)(const ScrDyn *d, bool *aborted); +/* events.addAbortListener(signal, listener): +1 disposable, or NULL. */ +ScrDyn *scr_events_add_abort_listener(ScrDyn *signal, ScrDyn *listener); + ScrDyn *scr_intl_segmenter_new(void); ScrStr *scr_process_builtin_id(ScrDyn *id, ScrArr *known); ScrDyn *scr_process_builtin_module(ScrStr *id, ScrDyn *getter); @@ -5973,6 +6047,10 @@ void scr_process_on_warning(ScrDyn *fn); void scr_process_off_warning(ScrDyn *fn); void scr_process_emit_warning(ScrDyn *args); void scr_emit_warning(const char *name, const char *code, ScrStr *message); +/* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the + * listener registration): units that cannot link the dyn-async unit route + * their warnings through it, and print the default report otherwise. */ +extern SCR_TL void (*scr_warning_emit_hook)(const char *name, const char *code, ScrStr *message); /* The promise-or-absent await's unit arm: one microtask hop, like JS's * await of any non-thenable (and like awaiting a settled promise). */ void scr_await_hop(void); diff --git a/tests/corpus/5130-event-target-dispatch.js b/tests/corpus/5130-event-target-dispatch.js new file mode 100644 index 000000000..a7c0b8fd6 --- /dev/null +++ b/tests/corpus/5130-event-target-dispatch.js @@ -0,0 +1,153 @@ +// EventTarget dispatch follows Node's linked-list walk: listeners run in +// registration order with the target as `this`, a listener appended while +// a later one is still pending runs in the same dispatch, a removed +// listener is skipped, `once` listeners leave before they run, and the +// (listener, capture) pair is the identity. After the first callback the +// event no longer reports itself as dispatching (eventPhase 0, null +// currentTarget), exactly Node's kIsBeingDispatched reset. +const t = new EventTarget(); + +function section(name) { + console.log("--", name); +} + +section("order and this"); +function first(e) { + console.log("first", e.type, this === t, e.eventPhase, e.currentTarget === t, e.target === t); + t.addEventListener("x", () => console.log("appended during dispatch")); + t.removeEventListener("x", third); +} +function second(e) { + console.log("second", e.eventPhase, e.currentTarget === null, e.composedPath().length); +} +function third() { + console.log("third (removed before its turn)"); +} +t.addEventListener("x", first); +t.addEventListener("x", second); +t.addEventListener("x", third); +t.addEventListener("x", first); // duplicate (listener, capture) is ignored +const ev = new Event("x"); +console.log("result", t.dispatchEvent(ev)); +console.log("after", ev.eventPhase, ev.currentTarget, ev.target === t, ev.srcElement === t); +console.log("again", t.dispatchEvent(ev)); + +section("tail append"); +const tail = new EventTarget(); +tail.addEventListener("y", () => { + console.log("only listener appends"); + tail.addEventListener("y", () => console.log("appended after the last node")); +}); +tail.dispatchEvent(new Event("y")); +tail.dispatchEvent(new Event("y")); + +section("once and capture"); +const o = new EventTarget(); +const shared = () => console.log("shared"); +o.addEventListener("o", () => console.log("once"), { once: true }); +o.addEventListener("o", shared, true); +o.addEventListener("o", shared, { capture: false }); +o.dispatchEvent(new Event("o")); +o.removeEventListener("o", shared, true); // a boolean options value reads no capture +o.dispatchEvent(new Event("o")); +o.removeEventListener("o", shared, { capture: true }); +o.dispatchEvent(new Event("o")); + +section("cancelable and passive"); +const c = new EventTarget(); +c.addEventListener("p", (e) => { + e.preventDefault(); + console.log("in passive", e.defaultPrevented); +}, { passive: true }); +c.addEventListener("c", (e) => { + e.preventDefault(); + console.log("prevented", e.defaultPrevented, e.returnValue); +}); +c.addEventListener("r", (e) => { + e.returnValue = false; +}); +console.log("passive result", c.dispatchEvent(new Event("p", { cancelable: true }))); +const cancel = new Event("c", { cancelable: true }); +console.log("cancel result", c.dispatchEvent(cancel), cancel.defaultPrevented); +console.log("not cancelable", c.dispatchEvent(new Event("c"))); +const rv = new Event("r", { cancelable: true }); +console.log("returnValue result", c.dispatchEvent(rv), rv.defaultPrevented); + +section("stop propagation"); +const s = new EventTarget(); +s.addEventListener("s", (e) => { + e.stopPropagation(); + console.log("stopPropagation keeps going", e.cancelBubble); +}); +s.addEventListener("s", (e) => { + e.stopImmediatePropagation(); + console.log("stopImmediatePropagation"); +}); +s.addEventListener("s", () => console.log("never")); +s.dispatchEvent(new Event("s")); +const cb = new Event("cb"); +cb.cancelBubble = true; +console.log("cancelBubble", cb.cancelBubble); + +section("handleEvent"); +const h = new EventTarget(); +const handler = { + tag: "obj", + handleEvent(e) { + console.log("handleEvent", this.tag, e.type, this === handler); + }, +}; +h.addEventListener("h", handler); +h.dispatchEvent(new Event("h")); +handler.handleEvent = (e) => console.log("replaced handleEvent", e.type); +h.dispatchEvent(new Event("h")); +h.addEventListener("missing", {}); +h.dispatchEvent(new Event("missing")); +h.removeEventListener("h", handler); +h.dispatchEvent(new Event("h")); + +section("validation"); +// Values the checker would reject at these call sites. +function loose(value) { + return value; +} +function attempt(label, fn) { + try { + fn(); + console.log(label, "ok"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +// @ts-expect-error -- the missing argument is the point +attempt("new Event()", () => new Event()); +attempt("bad options", () => new Event("x", loose(5))); +attempt("dispatch non-event", () => t.dispatchEvent(loose({}))); +// @ts-expect-error -- the missing argument is the point +attempt("dispatch nothing", () => t.dispatchEvent()); +attempt("listener number", () => t.addEventListener("x", loose(5))); +// @ts-expect-error -- the missing argument is the point +attempt("missing listener", () => t.addEventListener("x")); +attempt("options number", () => t.addEventListener("x", () => {}, loose(5))); +attempt("signal object", () => t.addEventListener("x", () => {}, { signal: loose(1) })); +const rec = new EventTarget(); +rec.addEventListener("rec", (e) => { + attempt("recursion", () => rec.dispatchEvent(e)); +}); +rec.dispatchEvent(new Event("rec")); + +section("event fields"); +const fresh = new Event("f", { bubbles: true, cancelable: true, composed: true }); +console.log(fresh.type, fresh.bubbles, fresh.cancelable, fresh.composed, fresh.isTrusted); +console.log(fresh.target, fresh.currentTarget, fresh.eventPhase, fresh.timeStamp >= 0); +console.log(Event.NONE, Event.CAPTURING_PHASE, Event.AT_TARGET, Event.BUBBLING_PHASE); +console.log(String(fresh), String(t), Object.prototype.toString.call(new CustomEvent("c"))); +fresh.initEvent("renamed", false, false); +console.log(fresh.type, fresh.bubbles, fresh.cancelable); +const custom = new CustomEvent("d", { detail: { n: 1 } }); +console.log(custom.detail.n, new CustomEvent("e").detail, custom instanceof Event, custom instanceof CustomEvent); +console.log(fresh instanceof CustomEvent, t instanceof EventTarget, loose({}) instanceof Event); +console.log(new Event(loose(42)).type, new Event(loose(null)).type); +// util.inspect: EventTarget prints its (empty) custom view; beyond the +// depth limit both print the bare class name. +console.log([t], { deep: { deeper: { target: t, event: fresh } } }); diff --git a/tests/corpus/5131-event-target-listener-errors.js b/tests/corpus/5131-event-target-listener-errors.js new file mode 100644 index 000000000..d84a43742 --- /dev/null +++ b/tests/corpus/5131-event-target-listener-errors.js @@ -0,0 +1,51 @@ +// A throwing EventTarget listener does not stop the dispatch: Node reports +// the exception as uncaught from a nextTick. A listener's returned promise +// is caught with then(), so its rejection is reported after the reaction +// runs; a handleEvent object goes through an async wrapper, so even its +// synchronous throw (or a non-callable handleEvent) arrives one microtask +// later. Each phase raises one failure and shows where its report lands +// among a microtask and a tick queued right after the dispatch. +process.on("uncaughtException", (err) => console.log("uncaught:", err.message)); + +async function phase(name, listener) { + console.log("--", name); + const t = new EventTarget(); + t.addEventListener("x", listener); + t.addEventListener("x", () => console.log("next listener still runs")); + console.log("dispatch result", t.dispatchEvent(new Event("x"))); + queueMicrotask(() => console.log("microtask")); + process.nextTick(() => console.log("tick")); + await new Promise((resolve) => setTimeout(resolve, 5)); +} + +async function main() { + await phase("throwing function", () => { + throw new Error("listener threw"); + }); + await phase("async function", async () => { + throw new Error("async listener rejected"); + }); + await phase("async function after an await", async () => { + await null; + throw new Error("late rejection"); + }); + await phase("throwing handleEvent", { + handleEvent() { + throw new Error("handleEvent threw"); + }, + }); + await phase("non-callable handleEvent", { handleEvent: 5 }); + await phase("async handleEvent", { + async handleEvent() { + await null; + throw new Error("async handleEvent rejected"); + }, + }); + await phase("thenable result", () => ({ + then(resolve, reject) { + console.log("then called", typeof resolve, typeof reject); + reject(new Error("thenable rejected")); + }, + })); +} +main(); diff --git a/tests/corpus/5132-event-target-typed.ts b/tests/corpus/5132-event-target-typed.ts new file mode 100644 index 000000000..ce3ebad98 --- /dev/null +++ b/tests/corpus/5132-event-target-typed.ts @@ -0,0 +1,79 @@ +// EventTarget, Event and CustomEvent from typed code: listeners typed +// `(event: Event) => void` or `{ handleEvent }` records, the dispatch +// result, the event's target and phase during and after dispatch, the +// instanceof tests, the phase constants, and CustomEvent's detail. +const bus = new EventTarget(); +const seen: string[] = []; + +function onPing(event: Event): void { + seen.push(`${event.type}:${event.eventPhase}:${event.target === bus}:${event.currentTarget === bus}`); +} + +bus.addEventListener("ping", onPing); +bus.addEventListener("ping", onPing, { once: true }); +const ping = new Event("ping", { cancelable: true }); +console.log(bus.dispatchEvent(ping), ping.eventPhase, ping.currentTarget === null, ping.target === bus); +console.log(seen.join(" ")); + +// Options dictionaries held in typed bindings. +const onceOptions: AddEventListenerOptions = { once: true }; +for (let i = 0; i < 3; i++) { + bus.addEventListener("round", () => seen.push(`round listener ${i}`), onceOptions); + bus.dispatchEvent(new Event("round")); +} +const captureOptions = { capture: true }; +const captured = (): void => { + seen.push("captured"); +}; +bus.addEventListener("cap", captured, captureOptions); +bus.removeEventListener("cap", captured); +bus.dispatchEvent(new Event("cap")); +bus.removeEventListener("cap", captured, captureOptions); +bus.dispatchEvent(new Event("cap")); +console.log(seen.splice(0).join(" ")); + +const canceller = (event: Event): void => { + event.preventDefault(); +}; +bus.addEventListener("save", canceller); +const save = new Event("save", { cancelable: true }); +console.log("save", bus.dispatchEvent(save), save.defaultPrevented, save.returnValue); +bus.removeEventListener("save", canceller); +console.log("save again", bus.dispatchEvent(new Event("save", { cancelable: true }))); + +interface Payload { + id: number; + tags: string[]; +} +const data = new CustomEvent("data", { detail: { id: 7, tags: ["a", "b"] } }); +bus.addEventListener("data", (event: Event) => { + if (event instanceof CustomEvent) { + const payload = event.detail as Payload; + console.log("detail", payload.id, payload.tags.join("+"), event.detail === data.detail); + } +}); +bus.dispatchEvent(data); + +const counter = { + hits: 0, + handleEvent(event: Event): void { + this.hits = this.hits + 1; + console.log("handleEvent", event.type, this.hits); + }, +}; +bus.addEventListener("count", counter); +bus.dispatchEvent(new Event("count")); +bus.dispatchEvent(new Event("count")); +console.log("hits", counter.hits); + +console.log(ping instanceof Event, data instanceof CustomEvent, ping instanceof CustomEvent); +console.log(bus instanceof EventTarget, Event.NONE, Event.AT_TARGET, Event.BUBBLING_PHASE); +console.log(ping.bubbles, ping.composed, ping.isTrusted, typeof ping.timeStamp); + +function relay(source: EventTarget, type: string): Promise { + return new Promise((resolve) => { + source.addEventListener(type, (event: Event) => resolve(event.type), { once: true }); + }); +} +relay(bus, "later").then((type) => console.log("relayed", type)); +setTimeout(() => bus.dispatchEvent(new Event("later")), 1); diff --git a/tests/corpus/5133-abort-signal-event-target.ts b/tests/corpus/5133-abort-signal-event-target.ts new file mode 100644 index 000000000..0a9eb31d7 --- /dev/null +++ b/tests/corpus/5133-abort-signal-event-target.ts @@ -0,0 +1,88 @@ +// AbortSignal is an EventTarget on the same engine: `onabort` takes the +// position of its first assignment, abort listeners and the removal hooks +// that options.signal registers run in registration order (the hooks run +// even after stopImmediatePropagation), any event type can be dispatched +// on a signal, and events.addAbortListener registers a once listener or, +// for an aborted signal, queues the call. +import { addAbortListener } from "node:events"; + +const log: string[] = []; + +// onabort is registered where it was first assigned, even as null. +const first = new AbortController(); +first.signal.onabort = null; +first.signal.addEventListener("abort", () => log.push("listener")); +first.signal.onabort = (event: Event) => log.push(`onabort ${event.type} ${event.isTrusted}`); +first.abort(); +console.log(log.splice(0).join(", ")); +console.log(first.signal.onabort !== null); + +// options.signal: the removal hook is an abort listener of its own. +const controller = new AbortController(); +const target = new EventTarget(); +controller.signal.addEventListener("abort", () => { + log.push("early abort listener dispatches"); + target.dispatchEvent(new Event("work")); +}); +target.addEventListener("work", () => log.push("work listener"), { signal: controller.signal }); +controller.signal.addEventListener("abort", (event: Event) => { + log.push("stops the rest"); + event.stopImmediatePropagation(); +}); +controller.signal.addEventListener("abort", () => log.push("never runs")); +target.dispatchEvent(new Event("work")); +controller.abort(); +target.dispatchEvent(new Event("work")); +console.log(log.splice(0).join(", ")); + +// An aborted signal registers nothing. +const done = new AbortController(); +done.abort(); +target.addEventListener("work", () => log.push("not registered"), { signal: done.signal }); +target.dispatchEvent(new Event("work")); +console.log("after aborted signal:", log.splice(0).join(", ") || "none"); + +// Signals dispatch any event type, with their own `this` and target. +const custom = new AbortController(); +custom.signal.addEventListener("custom", (event: Event) => { + log.push(`custom ${event.type} ${event.target === custom.signal} ${event.isTrusted}`); +}); +console.log(custom.signal.dispatchEvent(new Event("custom")), log.splice(0).join(", ")); +custom.signal.addEventListener("abort", (event: Event) => log.push(`manual ${event.isTrusted}`)); +custom.signal.dispatchEvent(new Event("abort")); +console.log(custom.signal.aborted, log.splice(0).join(", ")); + +// events.addAbortListener +const viaHelper = new AbortController(); +const disposable = addAbortListener(viaHelper.signal, (event: Event) => { + log.push(`helper ${event.type}`); +}); +viaHelper.signal.addEventListener("abort", (event: Event) => event.stopImmediatePropagation()); +viaHelper.signal.addEventListener("abort", () => log.push("stopped")); +viaHelper.abort(); +console.log(log.splice(0).join(", ")); +disposable[Symbol.dispose](); + +const disposed = new AbortController(); +const handle = addAbortListener(disposed.signal, () => log.push("disposed listener")); +handle[Symbol.dispose](); +disposed.abort(); +console.log("after dispose:", log.splice(0).join(", ") || "none"); + +const already = new AbortController(); +already.abort(); +addAbortListener(already.signal, () => console.log("aborted-signal listener runs later")); +queueMicrotask(() => console.log("microtask queued after it")); +console.log("sync end"); + +function attempt(label: string, fn: () => void): void { + try { + fn(); + console.log(label, "ok"); + } catch (e) { + const err = e as Error & { code?: string }; + console.log(label, err.name, err.code, err.message); + } +} +attempt("not a signal", () => addAbortListener(target as unknown as AbortSignal, () => {})); +attempt("not a function", () => addAbortListener(already.signal, 5 as unknown as () => void)); diff --git a/tests/corpus/5134-events-error-monitor.ts b/tests/corpus/5134-events-error-monitor.ts new file mode 100644 index 000000000..6e0c370d8 --- /dev/null +++ b/tests/corpus/5134-events-error-monitor.ts @@ -0,0 +1,38 @@ +// events.errorMonitor: listeners registered under the symbol see every +// 'error' emit first, with the same payload; they do not count as 'error' +// listeners, so an emit with none still throws after the monitors ran. +// Emitting the symbol itself reaches only the monitors. +import { EventEmitter, errorMonitor } from "node:events"; + +class Job extends EventEmitter { + readonly name: string; + constructor(name: string) { + super(); + this.name = name; + } +} + +const job = new Job("build"); +job.on(errorMonitor, (err: Error) => console.log("monitor A", err.message)); +job.once(EventEmitter.errorMonitor, (err: Error) => console.log("monitor once", err.message)); +job.on("error", (err: Error) => console.log("error listener", err.message)); +job.on(errorMonitor, (err: Error) => console.log("monitor B", err.message)); +console.log(job.emit("error", new Error("first"))); +console.log(job.emit("error", new TypeError("second"))); +console.log(job.listenerCount(errorMonitor), job.listenerCount("error")); + +const bare = new Job("bare"); +bare.on(errorMonitor, (err: Error) => console.log("bare monitor", err.message)); +try { + bare.emit("error", new RangeError("unhandled")); +} catch (err) { + console.log("thrown", (err as Error).name, (err as Error).message); +} +console.log(bare.emit(errorMonitor, new Error("direct"))); +bare.removeAllListeners(errorMonitor); +console.log(bare.listenerCount(errorMonitor)); +try { + bare.emit("error", new Error("no monitors left")); +} catch (err) { + console.log("thrown", (err as Error).message); +} diff --git a/tests/corpus/5135-event-target-warnings.js b/tests/corpus/5135-event-target-warnings.js new file mode 100644 index 000000000..63277c010 --- /dev/null +++ b/tests/corpus/5135-event-target-warnings.js @@ -0,0 +1,23 @@ +// @exit: 1 +// The process warnings EventTarget emits: the one-time leak warning past +// ten listeners of one type (none for AbortSignal, whose limit is 0) and +// the no-op warning for a null or undefined listener. Both arrive through +// process.emitWarning on a later tick; the 'warning' listener prints them +// (the stderr report carries the pid, so the @exit directive keeps it out +// of the comparison). +process.on("warning", (w) => console.log("warning:", w.name, "|", w.message)); + +const t = new EventTarget(); +for (let i = 0; i < 11; i++) t.addEventListener("x", () => {}); +for (let i = 0; i < 12; i++) t.addEventListener("x", () => {}); +for (let i = 0; i < 11; i++) t.addEventListener("y", () => {}); +t.addEventListener("z", null); +t.addEventListener("z", undefined); +const ac = new AbortController(); +for (let i = 0; i < 12; i++) ac.signal.addEventListener("abort", () => {}); +for (let i = 0; i < 12; i++) new EventTarget().addEventListener("w", () => {}, { signal: ac.signal }); +console.log("sync end"); +process.nextTick(() => console.log("tick queued after the warnings")); +setTimeout(() => { + process.exitCode = 1; +}, 1); diff --git a/tests/harness/fetch.test.ts b/tests/harness/fetch.test.ts index 76d162a03..f916956b6 100644 --- a/tests/harness/fetch.test.ts +++ b/tests/harness/fetch.test.ts @@ -74,14 +74,10 @@ async function runBinary( env?: NodeJS.ProcessEnv, timeout = 30_000, ): Promise { - // These fixtures intentionally exercise missing listeners, and some JS - // entries rely on Node's module detection. Neither warning is program output. + // Some JS entries rely on Node's module detection; that warning is not + // program output. if (cmd === "node") { - args = [ - "--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", - "--disable-warning=AddEventListenerArgumentTypeWarning", - ...args, - ]; + args = ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", ...args]; } try { const { stdout, stderr } = await execFileAsync(cmd, args, { @@ -98,13 +94,15 @@ async function runBinary( } } -// Keep sanitizer infrastructure noise separate from program stderr. +// Keep sanitizer infrastructure noise separate from program stderr; the +// process warnings carry each process's own pid. function expectCleanStderr(native: RunResult, node: RunResult): void { if (node.exitCode !== 0) return; + const pid = (text: string): string => text.replace(/^\(node:\d+\) /gm, "(node:PID) "); const stderr = native.stderr .toString("utf8") .replace(/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext.*\n/gm, ""); - expect(stderr).toBe(node.stderr.toString("utf8")); + expect(pid(stderr)).toBe(pid(node.stderr.toString("utf8"))); } async function build(entry: string, flavor = ""): Promise { diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index 2974dbffc..e884fb3ed 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -139,6 +139,12 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { "js-promise-all-values.js", "js-promise-catch-handler.js", "js-abort-portable.js", + "5130-event-target-dispatch.js", + "5131-event-target-listener-errors.js", + "5132-event-target-typed.ts", + "5133-abort-signal-event-target.ts", + "5134-events-error-monitor.ts", + "5135-event-target-warnings.js", "4031-event-emitter-long-tuples.ts", "2700-wasi-core.ts", "local-union-storage.ts",