LibJS: Implement the AsyncDisposableStack interface

This is very similar to the DisposableStack interface, except disposal
of resources is promise-based.
This commit is contained in:
Timothy Flynn 2025-01-17 10:09:01 -05:00 committed by Andreas Kling
parent 5ea0aa5f08
commit 26c2484c2f
Notes: github-actions[bot] 2025-01-17 19:47:28 +00:00
22 changed files with 873 additions and 0 deletions

View file

@ -41,6 +41,9 @@ set(SOURCES
Runtime/ArrayIterator.cpp
Runtime/ArrayIteratorPrototype.cpp
Runtime/ArrayPrototype.cpp
Runtime/AsyncDisposableStack.cpp
Runtime/AsyncDisposableStackConstructor.cpp
Runtime/AsyncDisposableStackPrototype.cpp
Runtime/AsyncFromSyncIterator.cpp
Runtime/AsyncFromSyncIteratorPrototype.cpp
Runtime/AsyncFunctionConstructor.cpp

View file

@ -21,6 +21,7 @@
__JS_ENUMERATE(AggregateError, aggregate_error, AggregateErrorPrototype, AggregateErrorConstructor, void) \
__JS_ENUMERATE(Array, array, ArrayPrototype, ArrayConstructor, void) \
__JS_ENUMERATE(ArrayBuffer, array_buffer, ArrayBufferPrototype, ArrayBufferConstructor, void) \
__JS_ENUMERATE(AsyncDisposableStack, async_disposable_stack, AsyncDisposableStackPrototype, AsyncDisposableStackConstructor, void) \
__JS_ENUMERATE(AsyncFunction, async_function, AsyncFunctionPrototype, AsyncFunctionConstructor, void) \
__JS_ENUMERATE(AsyncGeneratorFunction, async_generator_function, AsyncGeneratorFunctionPrototype, AsyncGeneratorFunctionConstructor, void) \
__JS_ENUMERATE(BigIntObject, bigint, BigIntPrototype, BigIntConstructor, void) \

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/AsyncDisposableStack.h>
namespace JS {
GC_DEFINE_ALLOCATOR(AsyncDisposableStack);
AsyncDisposableStack::AsyncDisposableStack(DisposeCapability dispose_capability, Object& prototype)
: Object(ConstructWithPrototypeTag::Tag, prototype)
, m_dispose_capability(move(dispose_capability))
{
}
void AsyncDisposableStack::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
m_dispose_capability.visit_edges(visitor);
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Object.h>
namespace JS {
class AsyncDisposableStack final : public Object {
JS_OBJECT(AsyncDisposableStack, Object);
GC_DECLARE_ALLOCATOR(AsyncDisposableStack);
public:
virtual ~AsyncDisposableStack() override = default;
enum class AsyncDisposableState {
Pending,
Disposed
};
[[nodiscard]] AsyncDisposableState async_disposable_state() const { return m_async_disposable_state; }
void set_disposed() { m_async_disposable_state = AsyncDisposableState::Disposed; }
[[nodiscard]] DisposeCapability const& dispose_capability() const { return m_dispose_capability; }
[[nodiscard]] DisposeCapability& dispose_capability() { return m_dispose_capability; }
private:
AsyncDisposableStack(DisposeCapability, Object& prototype);
virtual void visit_edges(Visitor& visitor) override;
DisposeCapability m_dispose_capability;
AsyncDisposableState m_async_disposable_state { AsyncDisposableState::Pending };
};
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/AsyncDisposableStack.h>
#include <LibJS/Runtime/AsyncDisposableStackConstructor.h>
namespace JS {
GC_DEFINE_ALLOCATOR(AsyncDisposableStackConstructor);
AsyncDisposableStackConstructor::AsyncDisposableStackConstructor(Realm& realm)
: NativeFunction(realm.vm().names.AsyncDisposableStack.as_string(), realm.intrinsics().function_prototype())
{
}
void AsyncDisposableStackConstructor::initialize(Realm& realm)
{
auto& vm = this->vm();
Base::initialize(realm);
// 12.4.2.1 AsyncDisposableStack.prototype, https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype
define_direct_property(vm.names.prototype, realm.intrinsics().async_disposable_stack_prototype(), 0);
define_direct_property(vm.names.length, Value(0), Attribute::Configurable);
}
// 12.4.1.1 AsyncDisposableStack ( ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack
ThrowCompletionOr<Value> AsyncDisposableStackConstructor::call()
{
auto& vm = this->vm();
// 1. If NewTarget is undefined, throw a TypeError exception.
return vm.throw_completion<TypeError>(ErrorType::ConstructorWithoutNew, vm.names.AsyncDisposableStack);
}
// 12.4.1.1 AsyncDisposableStack ( ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack
ThrowCompletionOr<GC::Ref<Object>> AsyncDisposableStackConstructor::construct(FunctionObject& new_target)
{
auto& vm = this->vm();
// 2. Let asyncDisposableStack be ? OrdinaryCreateFromConstructor(NewTarget, "%AsyncDisposableStack.prototype%", « [[AsyncDisposableState]], [[DisposeCapability]] »).
// 3. Set asyncDisposableStack.[[AsyncDisposableState]] to pending.
// 4. Set asyncDisposableStack.[[DisposeCapability]] to NewDisposeCapability().
// 5. Return asyncDisposableStack.
return TRY(ordinary_create_from_constructor<AsyncDisposableStack>(vm, new_target, &Intrinsics::async_disposable_stack_prototype, new_dispose_capability()));
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/NativeFunction.h>
namespace JS {
class AsyncDisposableStackConstructor final : public NativeFunction {
JS_OBJECT(AsyncDisposableStackConstructor, NativeFunction);
GC_DECLARE_ALLOCATOR(AsyncDisposableStackConstructor);
public:
virtual void initialize(Realm&) override;
virtual ~AsyncDisposableStackConstructor() override = default;
virtual ThrowCompletionOr<Value> call() override;
virtual ThrowCompletionOr<GC::Ref<Object>> construct(FunctionObject&) override;
private:
explicit AsyncDisposableStackConstructor(Realm&);
virtual bool has_constructor() const override { return true; }
};
}

View file

@ -0,0 +1,213 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/AsyncDisposableStackConstructor.h>
#include <LibJS/Runtime/AsyncDisposableStackPrototype.h>
#include <LibJS/Runtime/NativeFunction.h>
#include <LibJS/Runtime/PromiseCapability.h>
#include <LibJS/Runtime/PromiseConstructor.h>
namespace JS {
GC_DEFINE_ALLOCATOR(AsyncDisposableStackPrototype);
AsyncDisposableStackPrototype::AsyncDisposableStackPrototype(Realm& realm)
: PrototypeObject(realm.intrinsics().object_prototype())
{
}
void AsyncDisposableStackPrototype::initialize(Realm& realm)
{
Base::initialize(realm);
auto& vm = this->vm();
u8 attr = Attribute::Writable | Attribute::Configurable;
define_native_function(realm, vm.names.adopt, adopt, 2, attr);
define_native_function(realm, vm.names.defer, defer, 1, attr);
define_native_function(realm, vm.names.disposeAsync, dispose_async, 0, attr);
define_native_accessor(realm, vm.names.disposed, disposed_getter, {}, attr);
define_native_function(realm, vm.names.move, move_, 0, attr);
define_native_function(realm, vm.names.use, use, 1, attr);
// 12.4.3.7 AsyncDisposableStack.prototype [ @@asyncDispose ] (), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype-@@asyncDispose
define_direct_property(vm.well_known_symbol_async_dispose(), get_without_side_effects(vm.names.disposeAsync), attr);
// 12.4.3.8 AsyncDisposableStack.prototype [ @@toStringTag ], https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype-@@toStringTag
define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, vm.names.AsyncDisposableStack.as_string()), Attribute::Configurable);
}
// 12.4.3.1 AsyncDisposableStack.prototype.adopt( value, onDisposeAsync ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.adopt
JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::adopt)
{
auto& realm = *vm.current_realm();
auto value = vm.argument(0);
auto on_dispose_async = vm.argument(1);
// 1. Let asyncDisposableStack be the this value.
// 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]).
auto async_disposable_stack = TRY(typed_this_object(vm));
// 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, throw a ReferenceError exception.
if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed)
return vm.throw_completion<ReferenceError>(ErrorType::AsyncDisposableStackAlreadyDisposed);
// 4. If IsCallable(onDisposeAsync) is false, throw a TypeError exception.
if (!on_dispose_async.is_function())
return vm.throw_completion<TypeError>(ErrorType::NotAFunction, on_dispose_async);
// 5. Let closure be a new Abstract Closure with no parameters that captures value and onDisposeAsync and performs the following steps when called:
auto closure = [value, on_dispose_async](VM& vm) mutable -> ThrowCompletionOr<Value> {
// a. Return ? Call(onDisposeAsync, undefined, « value »).
return TRY(call(vm, on_dispose_async.as_function(), js_undefined(), value));
};
// 6. Let F be CreateBuiltinFunction(closure, 0, "", « »).
auto function = NativeFunction::create(realm, move(closure), 0, "");
// 7. Perform ? AddDisposableResource(asyncDisposableStack.[[DisposeCapability]], undefined, async-dispose, F).
TRY(add_disposable_resource(vm, async_disposable_stack->dispose_capability(), js_undefined(), Environment::InitializeBindingHint::AsyncDispose, function));
// 8. Return value.
return value;
}
// 12.4.3.2 AsyncDisposableStack.prototype.defer( onDisposeAsync ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.defer
JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::defer)
{
auto on_dispose_async = vm.argument(0);
// 1. Let asyncDisposableStack be the this value.
// 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]).
auto async_disposable_stack = TRY(typed_this_object(vm));
// 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, throw a ReferenceError exception.
if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed)
return vm.throw_completion<ReferenceError>(ErrorType::AsyncDisposableStackAlreadyDisposed);
// 4. If IsCallable(onDisposeAsync) is false, throw a TypeError exception.
if (!on_dispose_async.is_function())
return vm.throw_completion<TypeError>(ErrorType::NotAFunction, on_dispose_async);
// 5. Perform ? AddDisposableResource(asyncDisposableStack.[[DisposeCapability]], undefined, async-dispose, onDisposeAsync).
TRY(add_disposable_resource(vm, async_disposable_stack->dispose_capability(), js_undefined(), Environment::InitializeBindingHint::AsyncDispose, on_dispose_async.as_function()));
// 6. Return undefined.
return js_undefined();
}
// 12.4.3.3 AsyncDisposableStack.prototype.disposeAsync(), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.disposeAsync
JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::dispose_async)
{
auto& realm = *vm.current_realm();
// 1. Let asyncDisposableStack be the this value.
auto async_disposable_stack_value = vm.this_value();
// 2. Let promiseCapability be ! NewPromiseCapability(%Promise%).
auto promise_capability = MUST(new_promise_capability(vm, realm.intrinsics().promise_constructor()));
// 3. If asyncDisposableStack does not have an [[AsyncDisposableState]] internal slot, then
if (!async_disposable_stack_value.is_object() || !is<AsyncDisposableStack>(async_disposable_stack_value.as_object())) {
// a. Perform ! Call(promiseCapability.[[Reject]], undefined, « a newly created TypeError object »).
auto error = TypeError::create(realm, MUST(String::formatted(ErrorType::NotAnObjectOfType.message(), display_name())));
MUST(call(vm, *promise_capability->reject(), js_undefined(), error));
// b. Return promiseCapability.[[Promise]].
return promise_capability->promise();
}
auto& async_disposable_stack = static_cast<AsyncDisposableStack&>(async_disposable_stack_value.as_object());
// 4. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, then
if (async_disposable_stack.async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed) {
// a. Perform ! Call(promiseCapability.[[Resolve]], undefined, « undefined »).
MUST(call(vm, *promise_capability->resolve(), js_undefined(), js_undefined()));
// b. Return promiseCapability.[[Promise]].
return promise_capability->promise();
}
// 5. Set asyncDisposableStack.[[AsyncDisposableState]] to disposed.
async_disposable_stack.set_disposed();
// 6. Let result be DisposeResources(asyncDisposableStack.[[DisposeCapability]], NormalCompletion(undefined)).
// 7. IfAbruptRejectPromise(result, promiseCapability).
auto result = TRY_OR_REJECT(vm, promise_capability, dispose_resources(vm, async_disposable_stack.dispose_capability(), normal_completion(js_undefined())));
// 8. Perform ! Call(promiseCapability.[[Resolve]], undefined, « result »).
MUST(call(vm, *promise_capability->resolve(), js_undefined(), *result));
// 9. Return promiseCapability.[[Promise]].
return promise_capability->promise();
}
// 12.4.3.4 get AsyncDisposableStack.prototype.disposed, https://tc39.es/proposal-explicit-resource-management/#sec-get-asyncdisposablestack.prototype.disposed
JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::disposed_getter)
{
// 1. Let asyncDisposableStack be the this value.
// 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]).
auto async_disposable_stack = TRY(typed_this_object(vm));
// 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, return true.
if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed)
return true;
// 4. Otherwise, return false.
return false;
}
// 12.4.3.5 AsyncDisposableStack.prototype.move(), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.move
JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::move_)
{
auto& realm = *vm.current_realm();
// 1. Let asyncDisposableStack be the this value.
// 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]).
auto async_disposable_stack = TRY(typed_this_object(vm));
// 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, throw a ReferenceError exception.
if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed)
return vm.throw_completion<ReferenceError>(ErrorType::AsyncDisposableStackAlreadyDisposed);
// 4. Let newAsyncDisposableStack be ? OrdinaryCreateFromConstructor(%AsyncDisposableStack%, "%AsyncDisposableStack.prototype%", « [[AsyncDisposableState]], [[DisposeCapability]] »).
// 5. Set newAsyncDisposableStack.[[AsyncDisposableState]] to pending.
// 6. Set newAsyncDisposableStack.[[DisposeCapability]] to asyncDisposableStack.[[DisposeCapability]].
auto new_async_disposable_stack = TRY(ordinary_create_from_constructor<AsyncDisposableStack>(vm, realm.intrinsics().async_disposable_stack_constructor(), &Intrinsics::async_disposable_stack_prototype, move(async_disposable_stack->dispose_capability())));
// 7. Set asyncDisposableStack.[[DisposeCapability]] to NewDisposeCapability().
async_disposable_stack->dispose_capability() = new_dispose_capability();
// 8. Set asyncDisposableStack.[[AsyncDisposableState]] to disposed.
async_disposable_stack->set_disposed();
// 9. Return newAsyncDisposableStack.
return new_async_disposable_stack;
}
// 12.4.3.6 AsyncDisposableStack.prototype.use( value ), https://tc39.es/proposal-explicit-resource-management/#sec-asyncdisposablestack.prototype.use
JS_DEFINE_NATIVE_FUNCTION(AsyncDisposableStackPrototype::use)
{
auto value = vm.argument(0);
// 1. Let asyncDisposableStack be the this value.
// 2. Perform ? RequireInternalSlot(asyncDisposableStack, [[AsyncDisposableState]]).
auto async_disposable_stack = TRY(typed_this_object(vm));
// 3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, throw a ReferenceError exception.
if (async_disposable_stack->async_disposable_state() == AsyncDisposableStack::AsyncDisposableState::Disposed)
return vm.throw_completion<ReferenceError>(ErrorType::AsyncDisposableStackAlreadyDisposed);
// 4. Perform ? AddDisposableResource(asyncDisposableStack.[[DisposeCapability]], value, async-dispose).
TRY(add_disposable_resource(vm, async_disposable_stack->dispose_capability(), value, Environment::InitializeBindingHint::AsyncDispose));
// 5. Return value.
return value;
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/AsyncDisposableStack.h>
#include <LibJS/Runtime/PrototypeObject.h>
namespace JS {
class AsyncDisposableStackPrototype final : public PrototypeObject<AsyncDisposableStackPrototype, AsyncDisposableStack> {
JS_PROTOTYPE_OBJECT(AsyncDisposableStackPrototype, AsyncDisposableStack, AsyncDisposableStack);
GC_DECLARE_ALLOCATOR(AsyncDisposableStackPrototype);
public:
virtual void initialize(Realm&) override;
virtual ~AsyncDisposableStackPrototype() override = default;
private:
explicit AsyncDisposableStackPrototype(Realm&);
JS_DECLARE_NATIVE_FUNCTION(adopt);
JS_DECLARE_NATIVE_FUNCTION(defer);
JS_DECLARE_NATIVE_FUNCTION(dispose_async);
JS_DECLARE_NATIVE_FUNCTION(disposed_getter);
JS_DECLARE_NATIVE_FUNCTION(move_);
JS_DECLARE_NATIVE_FUNCTION(use);
};
}

View file

@ -127,6 +127,7 @@ namespace JS {
P(direction) \
P(disabledFeatures) \
P(disambiguation) \
P(disposeAsync) \
P(disposed) \
P(done) \
P(dotAll) \

View file

@ -13,6 +13,7 @@
M(AccessorValueOrWritable, "Accessor property descriptor cannot specify a value or writable key") \
M(AgentCannotSuspend, "Agent is not allowed to suspend") \
M(ArrayMaxSize, "Maximum array size exceeded") \
M(AsyncDisposableStackAlreadyDisposed, "AsyncDisposableStack is already disposed") \
M(BadArgCountMany, "{}() needs {} arguments") \
M(BadArgCountOne, "{}() needs one argument") \
M(BigIntBadOperator, "Cannot use {} operator with BigInt") \

View file

@ -18,6 +18,7 @@
#include <LibJS/Runtime/ArrayBufferConstructor.h>
#include <LibJS/Runtime/ArrayConstructor.h>
#include <LibJS/Runtime/ArrayPrototype.h>
#include <LibJS/Runtime/AsyncDisposableStackConstructor.h>
#include <LibJS/Runtime/AsyncFunctionConstructor.h>
#include <LibJS/Runtime/AsyncGeneratorFunctionConstructor.h>
#include <LibJS/Runtime/AsyncGeneratorPrototype.h>
@ -123,6 +124,7 @@ void set_default_global_bindings(Realm& realm)
global.define_intrinsic_accessor(vm.names.AggregateError, attr, [](auto& realm) -> Value { return realm.intrinsics().aggregate_error_constructor(); });
global.define_intrinsic_accessor(vm.names.Array, attr, [](auto& realm) -> Value { return realm.intrinsics().array_constructor(); });
global.define_intrinsic_accessor(vm.names.ArrayBuffer, attr, [](auto& realm) -> Value { return realm.intrinsics().array_buffer_constructor(); });
global.define_intrinsic_accessor(vm.names.AsyncDisposableStack, attr, [](auto& realm) -> Value { return realm.intrinsics().async_disposable_stack_constructor(); });
global.define_intrinsic_accessor(vm.names.BigInt, attr, [](auto& realm) -> Value { return realm.intrinsics().bigint_constructor(); });
global.define_intrinsic_accessor(vm.names.BigInt64Array, attr, [](auto& realm) -> Value { return realm.intrinsics().big_int64_array_constructor(); });
global.define_intrinsic_accessor(vm.names.BigUint64Array, attr, [](auto& realm) -> Value { return realm.intrinsics().big_uint64_array_constructor(); });

View file

@ -11,6 +11,8 @@
#include <LibJS/Runtime/ArrayConstructor.h>
#include <LibJS/Runtime/ArrayIteratorPrototype.h>
#include <LibJS/Runtime/ArrayPrototype.h>
#include <LibJS/Runtime/AsyncDisposableStackConstructor.h>
#include <LibJS/Runtime/AsyncDisposableStackPrototype.h>
#include <LibJS/Runtime/AsyncFromSyncIteratorPrototype.h>
#include <LibJS/Runtime/AsyncFunctionConstructor.h>
#include <LibJS/Runtime/AsyncFunctionPrototype.h>

View file

@ -0,0 +1,21 @@
test("constructor properties", () => {
expect(AsyncDisposableStack).toHaveLength(0);
expect(AsyncDisposableStack.name).toBe("AsyncDisposableStack");
});
describe("errors", () => {
test("called without new", () => {
expect(() => {
AsyncDisposableStack();
}).toThrowWithMessage(
TypeError,
"AsyncDisposableStack constructor must be called with 'new'"
);
});
});
describe("normal behavior", () => {
test("typeof", () => {
expect(typeof new AsyncDisposableStack()).toBe("object");
});
});

View file

@ -0,0 +1,9 @@
test("length is 0", () => {
expect(AsyncDisposableStack.prototype[Symbol.asyncDispose]).toHaveLength(0);
});
test("is the same as disposeAsync", () => {
expect(AsyncDisposableStack.prototype[Symbol.asyncDispose]).toBe(
AsyncDisposableStack.prototype.disposeAsync
);
});

View file

@ -0,0 +1,3 @@
test("basic functionality", () => {
expect(AsyncDisposableStack.prototype[Symbol.toStringTag]).toBe("AsyncDisposableStack");
});

View file

@ -0,0 +1,95 @@
test("length is 2", () => {
expect(AsyncDisposableStack.prototype.adopt).toHaveLength(2);
});
describe("basic functionality", () => {
test("adopted dispose method gets called when stack is disposed", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = 0;
let disposeArgument = undefined;
expect(disposedCalled).toBe(0);
const result = stack.adopt(null, arg => {
disposeArgument = arg;
++disposedCalled;
});
expect(result).toBeNull();
expect(disposedCalled).toBe(0);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
expect(disposeArgument).toBeNull();
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
});
test("can adopt any value", async () => {
const stack = new AsyncDisposableStack();
const disposed = [];
function dispose(value) {
disposed.push(value);
}
const values = [null, undefined, 1, "a", Symbol.dispose, () => {}, new WeakMap(), [], {}];
values.forEach(value => {
stack.adopt(value, dispose);
});
await stack.disposeAsync();
expect(disposed).toEqual(values.reverse());
});
test("adopted stack is already disposed", async () => {
const stack = new AsyncDisposableStack();
stack.adopt(stack, value => {
expect(stack).toBe(value);
expect(stack.disposed).toBeTrue();
});
await stack.disposeAsync();
});
});
describe("throws errors", () => {
test("if call back is not a function throws type error", () => {
const stack = new AsyncDisposableStack();
[
1,
1n,
"a",
Symbol.dispose,
NaN,
0,
{},
[],
{ f() {} },
{ [Symbol.dispose]() {} },
{
get [Symbol.dispose]() {
return () => {};
},
},
].forEach(value => {
expect(() => stack.adopt(null, value)).toThrowWithMessage(TypeError, "not a function");
});
expect(stack.disposed).toBeFalse();
});
test("adopt throws if stack is already disposed (over type errors)", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
[{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => {
expect(() => stack.adopt(value, () => {})).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
expect(() => stack.adopt(null, value)).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
});
});
});

View file

@ -0,0 +1,70 @@
test("length is 1", () => {
expect(AsyncDisposableStack.prototype.defer).toHaveLength(1);
});
describe("basic functionality", () => {
test("deferred function gets called when stack is disposed", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = 0;
expect(disposedCalled).toBe(0);
const result = stack.defer((...args) => {
expect(args.length).toBe(0);
++disposedCalled;
});
expect(result).toBeUndefined();
expect(disposedCalled).toBe(0);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
});
test("deferred stack is already disposed", async () => {
const stack = new AsyncDisposableStack();
stack.defer(() => {
expect(stack.disposed).toBeTrue();
});
await stack.disposeAsync();
});
});
describe("throws errors", () => {
test("if call back is not a function throws type error", () => {
const stack = new AsyncDisposableStack();
[
1,
1n,
"a",
Symbol.dispose,
NaN,
0,
{},
[],
{ f() {} },
{ [Symbol.dispose]() {} },
{
get [Symbol.dispose]() {
return () => {};
},
},
].forEach(value => {
expect(() => stack.defer(value)).toThrowWithMessage(TypeError, "not a function");
});
expect(stack.disposed).toBeFalse();
});
test("defer throws if stack is already disposed (over type errors)", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
[{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => {
expect(() => stack.defer(value)).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
});
});
});

View file

@ -0,0 +1,88 @@
test("length is 0", () => {
expect(AsyncDisposableStack.prototype.disposeAsync).toHaveLength(0);
});
describe("basic functionality", () => {
test("make the stack marked as disposed", async () => {
const stack = new AsyncDisposableStack();
const result = await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
expect(result).toBeUndefined();
});
test("call dispose on objects in stack when called", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = false;
stack.use({
[Symbol.asyncDispose]() {
disposedCalled = true;
},
});
expect(disposedCalled).toBeFalse();
const result = await stack.disposeAsync();
expect(disposedCalled).toBeTrue();
expect(result).toBeUndefined();
});
test("disposed the objects added to the stack in reverse order", async () => {
const disposed = [];
const stack = new AsyncDisposableStack();
stack.use({
[Symbol.asyncDispose]() {
disposed.push("a");
},
});
stack.use({
[Symbol.asyncDispose]() {
disposed.push("b");
},
});
expect(disposed).toEqual([]);
const result = await stack.disposeAsync();
expect(disposed).toEqual(["b", "a"]);
expect(result).toBeUndefined();
});
test("does not dispose anything if already disposed", async () => {
const disposed = [];
const stack = new AsyncDisposableStack();
stack.use({
[Symbol.asyncDispose]() {
disposed.push("a");
},
});
expect(stack.disposed).toBeFalse();
expect(disposed).toEqual([]);
let result = await stack.disposeAsync();
expect(result).toBeUndefined();
expect(stack.disposed).toBeTrue();
expect(disposed).toEqual(["a"]);
result = await stack.disposeAsync();
expect(result).toBeUndefined();
expect(stack.disposed).toBeTrue();
expect(disposed).toEqual(["a"]);
});
test("throws if dispose method throws", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = false;
stack.use({
[Symbol.asyncDispose]() {
disposedCalled = true;
expect().fail("fail in dispose");
},
});
expect(async () => await stack.disposeAsync()).toThrowWithMessage(
ExpectationError,
"fail in dispose"
);
});
});

View file

@ -0,0 +1,24 @@
test("is getter without setter", () => {
const property = Object.getOwnPropertyDescriptor(AsyncDisposableStack.prototype, "disposed");
expect(property.get).not.toBeUndefined();
expect(property.set).toBeUndefined();
expect(property.value).toBeUndefined();
});
describe("basic functionality", () => {
test("is not a property on the object itself", () => {
const stack = new AsyncDisposableStack();
expect(Object.hasOwn(stack, "disposed")).toBeFalse();
});
test("starts off as false", () => {
const stack = new AsyncDisposableStack();
expect(stack.disposed).toBeFalse();
});
test("becomes true after being disposed", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
});
});

View file

@ -0,0 +1,62 @@
test("length is 0", () => {
expect(AsyncDisposableStack.prototype.move).toHaveLength(0);
});
describe("basic functionality", () => {
test("stack is disposed after moving", () => {
const stack = new AsyncDisposableStack();
const newStack = stack.move();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeFalse();
});
test("move does not dispose resource but only move them", async () => {
const stack = new AsyncDisposableStack();
let disposeCalled = false;
stack.defer(() => {
disposeCalled = true;
});
expect(disposeCalled).toBeFalse();
expect(stack.disposed).toBeFalse();
const newStack = stack.move();
expect(disposeCalled).toBeFalse();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeFalse();
await stack.disposeAsync();
expect(disposeCalled).toBeFalse();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeFalse();
await newStack.disposeAsync();
expect(disposeCalled).toBeTrue();
expect(stack.disposed).toBeTrue();
expect(newStack.disposed).toBeTrue();
});
test("can add stack to itself", async () => {
const stack = new AsyncDisposableStack();
stack.move(stack);
await stack.disposeAsync();
});
});
describe("throws errors", () => {
test("move throws if stack is already disposed (over type errors)", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
expect(() => stack.move()).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
});
});

View file

@ -0,0 +1,96 @@
test("length is 1", () => {
expect(AsyncDisposableStack.prototype.use).toHaveLength(1);
});
describe("basic functionality", () => {
test("added objects dispose method gets when stack is disposed", async () => {
const stack = new AsyncDisposableStack();
let disposedCalled = 0;
const obj = {
[Symbol.dispose]() {
++disposedCalled;
},
};
expect(disposedCalled).toBe(0);
const result = stack.use(obj);
expect(result).toBe(obj);
expect(disposedCalled).toBe(0);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
await stack.disposeAsync();
expect(disposedCalled).toBe(1);
});
test("can add null and undefined", async () => {
const stack = new AsyncDisposableStack();
expect(stack.use(null)).toBeNull();
expect(stack.use(undefined)).toBeUndefined();
expect(stack.disposed).toBeFalse();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
});
test("can add stack to itself", async () => {
const stack = new AsyncDisposableStack();
stack.use(stack);
await stack.disposeAsync();
});
});
describe("throws errors", () => {
test("if added value is not an object or null or undefined throws type error", () => {
const stack = new AsyncDisposableStack();
[1, 1n, "a", Symbol.dispose, NaN, 0].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(TypeError, "not an object");
});
expect(stack.disposed).toBeFalse();
});
test("if added object does not have a dispose method throws type error", () => {
const stack = new AsyncDisposableStack();
[{}, [], { f() {} }].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(
TypeError,
"does not have dispose method"
);
});
expect(stack.disposed).toBeFalse();
});
test("if added object has non function dispose method it throws type error", () => {
const stack = new AsyncDisposableStack();
let calledGetter = false;
[
{ [Symbol.dispose]: 1 },
{
get [Symbol.dispose]() {
calledGetter = true;
return 1;
},
},
].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(TypeError, "is not a function");
});
expect(stack.disposed).toBeFalse();
expect(calledGetter).toBeTrue();
});
test("use throws if stack is already disposed (over type errors)", async () => {
const stack = new AsyncDisposableStack();
await stack.disposeAsync();
expect(stack.disposed).toBeTrue();
[{ [Symbol.dispose]() {} }, 1, null, undefined, "a", []].forEach(value => {
expect(() => stack.use(value)).toThrowWithMessage(
ReferenceError,
"AsyncDisposableStack already disposed values"
);
});
});
});

View file

@ -10,6 +10,7 @@ AnimationPlaybackEvent
AnimationTimeline
Array
ArrayBuffer
AsyncDisposableStack
Attr
Audio
AudioBuffer