abortsignal util custom inspect

This commit is contained in:
pfg
2025-05-05 20:57:46 -07:00
parent c130d1bd69
commit 5a265ff619
3 changed files with 430 additions and 0 deletions

View File

@@ -48,6 +48,7 @@
#include <JavaScriptCore/SubspaceInlines.h>
#include <wtf/GetPtr.h>
#include <wtf/PointerPreparations.h>
#include "ErrorCode.h"
#include <wtf/URL.h>
namespace WebCore {
@@ -56,6 +57,7 @@ using namespace JSC;
// Functions
static JSC_DECLARE_HOST_FUNCTION(jsAbortControllerPrototypeFunction_abort);
static JSC_DECLARE_HOST_FUNCTION(jsAbortControllerPrototypeFunction_customInspect);
// Attributes
@@ -149,6 +151,7 @@ void JSAbortControllerPrototype::finishCreation(VM& vm)
{
Base::finishCreation(vm);
reifyStaticProperties(vm, JSAbortController::info(), JSAbortControllerPrototypeTableValues, *this);
this->putDirectNativeFunction(vm, this->globalObject(), builtinNames(vm).inspectCustomPublicName(), 2, jsAbortControllerPrototypeFunction_customInspect, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::Function | 0);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}
@@ -225,6 +228,78 @@ JSC_DEFINE_HOST_FUNCTION(jsAbortControllerPrototypeFunction_abort, (JSGlobalObje
return IDLOperation<JSAbortController>::call<jsAbortControllerPrototypeFunction_abortBody>(*lexicalGlobalObject, *callFrame, "abort");
}
static inline JSC::EncodedJSValue jsAbortControllerPrototypeFunction_customInspectBody(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame, typename IDLOperation<JSAbortController>::ClassParameter castedThis)
{
auto& vm = lexicalGlobalObject->vm();
auto throwScope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
JSValue depthValue = callFrame->argument(0);
JSValue optionsValue = callFrame->argument(1);
auto depth = depthValue.toNumber(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
if (depth < 0) {
return JSValue::encode(jsNontrivialString(vm, "[AbortController]"_s));
}
if (!depthValue.isUndefinedOrNull()) {
depthValue = jsNumber(depth - 1);
}
JSObject* options = optionsValue.toObject(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
PropertyNameArray optionsArray(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude);
options->getPropertyNames(lexicalGlobalObject, optionsArray, DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(throwScope, {});
JSObject* newOptions = constructEmptyObject(lexicalGlobalObject);
for (size_t i = 0; i < optionsArray.size(); i++) {
auto name = optionsArray[i];
JSValue value = options->get(lexicalGlobalObject, name);
RETURN_IF_EXCEPTION(throwScope, {});
newOptions->putDirect(vm, name, value, 0);
RETURN_IF_EXCEPTION(throwScope, {});
}
PutPropertySlot slot(newOptions);
newOptions->put(newOptions, lexicalGlobalObject, Identifier::fromString(vm, "depth"_s), depthValue, slot);
RETURN_IF_EXCEPTION(throwScope, {});
auto& impl = castedThis->wrapped();
JSObject* inputObj = constructEmptyObject(lexicalGlobalObject);
inputObj->putDirect(vm, Identifier::fromString(vm, "signal"_s), toJS<IDLInterface<AbortSignal>>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, impl.signal()), 0);
JSFunction* utilInspect = globalObject->utilInspectFunction();
auto callData = JSC::getCallData(utilInspect);
MarkedArgumentBuffer arguments;
arguments.append(inputObj);
arguments.append(newOptions);
auto inspectResult = JSC::profiledCall(globalObject, ProfilingReason::API, utilInspect, callData, inputObj, arguments);
RETURN_IF_EXCEPTION(throwScope, {});
auto* inspectString = inspectResult.toString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
auto inspectStringView = inspectString->view(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
JSValue result = jsString(vm, makeString("AbortController "_s, inspectStringView.data));
return JSValue::encode(result);
}
JSC_DEFINE_HOST_FUNCTION(jsAbortControllerPrototypeFunction_customInspect, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
return IDLOperation<JSAbortController>::call<jsAbortControllerPrototypeFunction_customInspectBody>(*lexicalGlobalObject, *callFrame, "inspect");
}
JSC::GCClient::IsoSubspace* JSAbortController::subspaceForImpl(JSC::VM& vm)
{
return WebCore::subspaceForImpl<JSAbortController, UseCustomHeapCellType::No>(

View File

@@ -52,6 +52,7 @@
#include <JavaScriptCore/SubspaceInlines.h>
#include <wtf/GetPtr.h>
#include <wtf/PointerPreparations.h>
#include "ErrorCode.h"
#include <wtf/URL.h>
namespace WebCore {
@@ -63,6 +64,7 @@ static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalConstructorFunction_abort);
static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalConstructorFunction_timeout);
static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalConstructorFunction_any);
static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalPrototypeFunction_throwIfAborted);
static JSC_DECLARE_HOST_FUNCTION(jsAbortSignalPrototypeFunction_customInspect);
// Attributes
@@ -159,6 +161,7 @@ void JSAbortSignalPrototype::finishCreation(VM& vm)
{
Base::finishCreation(vm);
reifyStaticProperties(vm, JSAbortSignal::info(), JSAbortSignalPrototypeTableValues, *this);
this->putDirectNativeFunction(vm, this->globalObject(), builtinNames(vm).inspectCustomPublicName(), 2, jsAbortSignalPrototypeFunction_customInspect, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::Function | 0);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}
@@ -339,6 +342,78 @@ JSC_DEFINE_HOST_FUNCTION(jsAbortSignalPrototypeFunction_throwIfAborted, (JSGloba
return IDLOperation<JSAbortSignal>::call<jsAbortSignalPrototypeFunction_throwIfAbortedBody>(*lexicalGlobalObject, *callFrame, "throwIfAborted");
}
static inline JSC::EncodedJSValue jsAbortSignalPrototypeFunction_customInspectBody(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame, typename IDLOperation<JSAbortSignal>::ClassParameter castedThis)
{
auto& vm = lexicalGlobalObject->vm();
auto throwScope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
JSValue depthValue = callFrame->argument(0);
JSValue optionsValue = callFrame->argument(1);
auto depth = depthValue.toNumber(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
if (depth < 0) {
return JSValue::encode(jsNontrivialString(vm, "[AbortSignal]"_s));
}
if (!depthValue.isUndefinedOrNull()) {
depthValue = jsNumber(depth - 1);
}
JSObject* options = optionsValue.toObject(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
PropertyNameArray optionsArray(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude);
options->getPropertyNames(lexicalGlobalObject, optionsArray, DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(throwScope, {});
JSObject* newOptions = constructEmptyObject(lexicalGlobalObject);
for (size_t i = 0; i < optionsArray.size(); i++) {
auto name = optionsArray[i];
JSValue value = options->get(lexicalGlobalObject, name);
RETURN_IF_EXCEPTION(throwScope, {});
newOptions->putDirect(vm, name, value, 0);
RETURN_IF_EXCEPTION(throwScope, {});
}
PutPropertySlot slot(newOptions);
newOptions->put(newOptions, lexicalGlobalObject, Identifier::fromString(vm, "depth"_s), depthValue, slot);
RETURN_IF_EXCEPTION(throwScope, {});
auto& impl = castedThis->wrapped();
JSObject* inputObj = constructEmptyObject(lexicalGlobalObject);
inputObj->putDirect(vm, Identifier::fromString(vm, "aborted"_s), jsBoolean(impl.aborted()), 0);
JSFunction* utilInspect = globalObject->utilInspectFunction();
auto callData = JSC::getCallData(utilInspect);
MarkedArgumentBuffer arguments;
arguments.append(inputObj);
arguments.append(newOptions);
auto inspectResult = JSC::profiledCall(globalObject, ProfilingReason::API, utilInspect, callData, inputObj, arguments);
RETURN_IF_EXCEPTION(throwScope, {});
auto* inspectString = inspectResult.toString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
auto inspectStringView = inspectString->view(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
JSValue result = jsString(vm, makeString("AbortSignal "_s, inspectStringView.data));
return JSValue::encode(result);
}
JSC_DEFINE_HOST_FUNCTION(jsAbortSignalPrototypeFunction_customInspect, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
{
return IDLOperation<JSAbortSignal>::call<jsAbortSignalPrototypeFunction_customInspectBody>(*lexicalGlobalObject, *callFrame, "inspect");
}
size_t JSAbortSignal::estimatedSize(JSC::JSCell* cell, JSC::VM& vm)
{
auto* thisObject = jsCast<JSAbortSignal*>(cell);

View File

@@ -0,0 +1,280 @@
// Flags: --expose-gc
'use strict';
require('../common');
const { inspect } = require('util');
const {
ok,
notStrictEqual,
strictEqual,
throws,
} = require('assert');
const {
test,
mock,
} = require('node:test');
const { setTimeout: sleep } = require('timers/promises');
// All of the the tests in this file depend on public-facing Node.js APIs.
// For tests that depend on Node.js internal APIs, please add them to
// test-abortcontroller-internal.js instead.
test('Abort is fired with the correct event type on AbortControllers', () => {
// Tests that abort is fired with the correct event type on AbortControllers
const ac = new AbortController();
ok(ac.signal);
let calls = 0;
const fn = (event) => {
ok(event);
strictEqual(event.type, 'abort');
calls++;
};
ac.signal.onabort = fn;
ac.signal.addEventListener('abort', fn);
ac.abort();
ac.abort();
ok(ac.signal.aborted);
strictEqual(calls, 2);
});
test('Abort events are trusted', () => {
// Tests that abort events are trusted
const ac = new AbortController();
let calls = 0;
const fn = (event) => {
ok(event.isTrusted);
calls++;
};
ac.signal.onabort = fn;
ac.abort();
strictEqual(calls, 1);
});
test('Abort events have the same isTrusted reference', () => {
// Tests that abort events have the same `isTrusted` reference
const first = new AbortController();
const second = new AbortController();
let ev1, ev2;
const ev3 = new Event('abort');
first.signal.addEventListener('abort', (event) => {
ev1 = event;
});
second.signal.addEventListener('abort', (event) => {
ev2 = event;
});
first.abort();
second.abort();
const firstTrusted = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(ev1), 'isTrusted').get;
const secondTrusted = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(ev2), 'isTrusted').get;
const untrusted = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(ev3), 'isTrusted').get;
strictEqual(firstTrusted, secondTrusted);
strictEqual(untrusted, firstTrusted);
});
test('AbortSignal is impossible to construct manually', () => {
// Tests that AbortSignal is impossible to construct manually
const ac = new AbortController();
throws(() => new ac.signal.constructor(), {
code: 'ERR_ILLEGAL_CONSTRUCTOR',
});
});
test('Symbol.toStringTag is correct', () => {
// Symbol.toStringTag
const toString = (o) => Object.prototype.toString.call(o);
const ac = new AbortController();
strictEqual(toString(ac), '[object AbortController]');
strictEqual(toString(ac.signal), '[object AbortSignal]');
});
test('AbortSignal.abort() creates an already aborted signal', () => {
const signal = AbortSignal.abort();
ok(signal.aborted);
});
test('AbortController properties and methods valiate the receiver', () => {
const acSignalGet = Object.getOwnPropertyDescriptor(
AbortController.prototype,
'signal'
).get;
const acAbort = AbortController.prototype.abort;
const goodController = new AbortController();
ok(acSignalGet.call(goodController));
acAbort.call(goodController);
const badAbortControllers = [
null,
undefined,
0,
NaN,
true,
'AbortController',
{ __proto__: AbortController.prototype },
];
for (const badController of badAbortControllers) {
throws(
() => acSignalGet.call(badController),
{ name: 'TypeError' }
);
throws(
() => acAbort.call(badController),
{ name: 'TypeError' }
);
}
});
test('AbortSignal properties validate the receiver', () => {
const signalAbortedGet = Object.getOwnPropertyDescriptor(
AbortSignal.prototype,
'aborted'
).get;
const goodSignal = new AbortController().signal;
strictEqual(signalAbortedGet.call(goodSignal), false);
const badAbortSignals = [
null,
undefined,
0,
NaN,
true,
'AbortSignal',
{ __proto__: AbortSignal.prototype },
];
for (const badSignal of badAbortSignals) {
throws(
() => signalAbortedGet.call(badSignal),
{ name: 'TypeError' }
);
}
});
test('AbortController inspection depth 1 or null works', () => {
const ac = new AbortController();
strictEqual(inspect(ac, { depth: 1 }),
'AbortController { signal: [AbortSignal] }');
strictEqual(inspect(ac, { depth: null }),
'AbortController { signal: AbortSignal { aborted: false } }');
});
test('AbortSignal reason is set correctly', () => {
// Test AbortSignal.reason
const ac = new AbortController();
ac.abort('reason');
strictEqual(ac.signal.reason, 'reason');
});
test('AbortSignal reasonable is set correctly with AbortSignal.abort()', () => {
// Test AbortSignal.reason
const signal = AbortSignal.abort('reason');
strictEqual(signal.reason, 'reason');
});
test('AbortSignal.timeout() works as expected', async () => {
// Test AbortSignal timeout
const signal = AbortSignal.timeout(10);
ok(!signal.aborted);
const { promise, resolve } = Promise.withResolvers();
const fn = () => {
ok(signal.aborted);
strictEqual(signal.reason.name, 'TimeoutError');
strictEqual(signal.reason.code, 23);
resolve();
};
setTimeout(fn, 20);
await promise;
});
test('AbortSignal.timeout() does not prevent the signal from being collected', async () => {
// Test AbortSignal timeout doesn't prevent the signal
// from being garbage collected.
let ref;
{
ref = new globalThis.WeakRef(AbortSignal.timeout(1_200_000));
}
await sleep(10);
globalThis.gc();
strictEqual(ref.deref(), undefined);
});
test('AbortSignal with a timeout is not collected while there is an active listener', async () => {
// Test that an AbortSignal with a timeout is not gc'd while
// there is an active listener on it.
let ref;
function handler() {}
{
ref = new globalThis.WeakRef(AbortSignal.timeout(1_200_000));
ref.deref().addEventListener('abort', handler);
}
await sleep(10);
globalThis.gc();
notStrictEqual(ref.deref(), undefined);
ok(ref.deref() instanceof AbortSignal);
ref.deref().removeEventListener('abort', handler);
await sleep(10);
globalThis.gc();
strictEqual(ref.deref(), undefined);
});
test('Setting a long timeout should not keep the process open', () => {
AbortSignal.timeout(1_200_000);
});
test('AbortSignal.reason should default', () => {
// Test AbortSignal.reason default
const signal = AbortSignal.abort();
ok(signal.reason instanceof DOMException);
strictEqual(signal.reason.code, 20);
const ac = new AbortController();
ac.abort();
ok(ac.signal.reason instanceof DOMException);
strictEqual(ac.signal.reason.code, 20);
});
test('abortSignal.throwIfAborted() works as expected', () => {
// Test abortSignal.throwIfAborted()
throws(() => AbortSignal.abort().throwIfAborted(), {
code: 20,
name: 'AbortError',
});
// Does not throw because it's not aborted.
const ac = new AbortController();
ac.signal.throwIfAborted();
});
test('abortSignal.throwIfAobrted() works as expected (2)', () => {
const originalDesc = Reflect.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted');
const actualReason = new Error();
Reflect.defineProperty(AbortSignal.prototype, 'aborted', { value: false });
throws(() => AbortSignal.abort(actualReason).throwIfAborted(), actualReason);
Reflect.defineProperty(AbortSignal.prototype, 'aborted', originalDesc);
});
test('abortSignal.throwIfAobrted() works as expected (3)', () => {
const originalDesc = Reflect.getOwnPropertyDescriptor(AbortSignal.prototype, 'reason');
const actualReason = new Error();
const fakeExcuse = new Error();
Reflect.defineProperty(AbortSignal.prototype, 'reason', { value: fakeExcuse });
throws(() => AbortSignal.abort(actualReason).throwIfAborted(), actualReason);
Reflect.defineProperty(AbortSignal.prototype, 'reason', originalDesc);
});