mirror of
https://github.com/oven-sh/bun
synced 2026-02-19 07:12:24 +00:00
abortsignal util custom inspect
This commit is contained in:
@@ -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>(
|
||||
|
||||
@@ -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);
|
||||
|
||||
280
test/js/node/test/parallel/test-abortcontroller.js
Normal file
280
test/js/node/test/parallel/test-abortcontroller.js
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user