diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index c677708c4e..869ddb5de0 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -75,6 +75,7 @@ namespace JS { P(ceil) \ P(charAt) \ P(charCodeAt) \ + P(cleanupSome) \ P(clear) \ P(clz32) \ P(concat) \ @@ -310,6 +311,7 @@ namespace JS { P(undefined) \ P(unescape) \ P(unicode) \ + P(unregister) \ P(unshift) \ P(value) \ P(valueOf) \ @@ -321,6 +323,7 @@ struct CommonPropertyNames { PropertyName catch_ { "catch", PropertyName::StringMayBeNumber::No }; PropertyName delete_ { "delete", PropertyName::StringMayBeNumber::No }; PropertyName for_ { "for", PropertyName::StringMayBeNumber::No }; + PropertyName register_ { "register", PropertyName::StringMayBeNumber::No }; PropertyName return_ { "return", PropertyName::StringMayBeNumber::No }; PropertyName throw_ { "throw", PropertyName::StringMayBeNumber::No }; #define __ENUMERATE(x) PropertyName x { #x, PropertyName::StringMayBeNumber::No }; diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index edff2b734c..2d6057d47f 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -26,6 +26,7 @@ M(DescWriteNonWritable, "Cannot write to non-writable property '{}'") \ M(DetachedArrayBuffer, "ArrayBuffer is detached") \ M(DivisionByZero, "Division by zero") \ + M(FinalizationRegistrySameTargetAndValue, "Target and held value must not be the same") \ M(GetCapabilitiesExecutorCalledMultipleTimes, "GetCapabilitiesExecutor was called multiple times") \ M(InOperatorWithObject, "'in' operator must be used on an object") \ M(InstanceOfOperatorBadPrototype, "'prototype' property of {} is not an object") \ diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp index 13bf816a87..6993a08bb9 100644 --- a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.cpp @@ -17,6 +17,11 @@ void FinalizationRegistryPrototype::initialize(GlobalObject& global_object) { auto& vm = this->vm(); Object::initialize(global_object); + u8 attr = Attribute::Writable | Attribute::Configurable; + + define_native_function(vm.names.cleanupSome, cleanup_some, 0, attr); + define_native_function(vm.names.register_, register_, 2, attr); + define_native_function(vm.names.unregister, unregister, 1, attr); // 26.2.3.4 FinalizationRegistry.prototype [ @@toStringTag ], https://tc39.es/ecma262/#sec-finalization-registry.prototype-@@tostringtag define_property(vm.well_known_symbol_to_string_tag(), js_string(global_object.heap(), vm.names.FinalizationRegistry.as_string()), Attribute::Configurable); @@ -38,4 +43,68 @@ FinalizationRegistry* FinalizationRegistryPrototype::typed_this(VM& vm, GlobalOb return static_cast(this_object); } +// @STAGE 2@ FinalizationRegistry.prototype.cleanupSome ( [ callback ] ), https://github.com/tc39/proposal-cleanup-some/blob/master/spec/finalization-registry.html +JS_DEFINE_NATIVE_FUNCTION(FinalizationRegistryPrototype::cleanup_some) +{ + auto* finalization_registry = typed_this(vm, global_object); + if (!finalization_registry) + return {}; + + auto callback = vm.argument(0); + if (vm.argument_count() > 0 && !callback.is_function()) { + vm.throw_exception(global_object, ErrorType::NotAFunction, callback.to_string_without_side_effects()); + return {}; + } + + finalization_registry->cleanup(callback.is_undefined() ? nullptr : &callback.as_function()); + + return js_undefined(); +} + +// 26.2.3.2 FinalizationRegistry.prototype.register ( target, heldValue [ , unregisterToken ] ), https://tc39.es/ecma262/#sec-finalization-registry.prototype.register +JS_DEFINE_NATIVE_FUNCTION(FinalizationRegistryPrototype::register_) +{ + auto* finalization_registry = typed_this(vm, global_object); + if (!finalization_registry) + return {}; + + auto target = vm.argument(0); + if (!target.is_object()) { + vm.throw_exception(global_object, ErrorType::NotAnObject, target.to_string_without_side_effects()); + return {}; + } + + auto held_value = vm.argument(1); + if (same_value(target, held_value)) { + vm.throw_exception(global_object, ErrorType::FinalizationRegistrySameTargetAndValue); + return {}; + } + + auto unregister_token = vm.argument(2); + if (!unregister_token.is_object() && !unregister_token.is_undefined()) { + vm.throw_exception(global_object, ErrorType::NotAnObject, unregister_token.to_string_without_side_effects()); + return {}; + } + + finalization_registry->add_finalization_record(target.as_cell(), held_value, unregister_token.is_undefined() ? nullptr : &unregister_token.as_object()); + + return js_undefined(); +} + +// 26.2.3.3 FinalizationRegistry.prototype.unregister ( unregisterToken ), https://tc39.es/ecma262/#sec-finalization-registry.prototype.unregister +JS_DEFINE_NATIVE_FUNCTION(FinalizationRegistryPrototype::unregister) +{ + auto* finalization_registry = typed_this(vm, global_object); + if (!finalization_registry) + return {}; + + auto unregister_token = vm.argument(0); + if (!unregister_token.is_object()) { + vm.throw_exception(global_object, ErrorType::NotAnObject, unregister_token.to_string_without_side_effects()); + return {}; + } + + return Value(finalization_registry->remove_by_token(unregister_token.as_object())); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.h b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.h index a03d96aaee..963489cad0 100644 --- a/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/FinalizationRegistryPrototype.h @@ -20,4 +20,10 @@ public: private: static FinalizationRegistry* typed_this(VM&, GlobalObject&); + + JS_DECLARE_NATIVE_FUNCTION(cleanup_some); + JS_DECLARE_NATIVE_FUNCTION(register_); + JS_DECLARE_NATIVE_FUNCTION(unregister); +}; + } diff --git a/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.prototype.cleanupSome.js b/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.prototype.cleanupSome.js new file mode 100644 index 0000000000..7553d96b82 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.prototype.cleanupSome.js @@ -0,0 +1,35 @@ +test("length is 0", () => { + expect(FinalizationRegistry.prototype.cleanupSome).toHaveLength(0); +}); + +function registerInDifferentScope(registry) { + registry.register({}, {}); +} + +test("basic functionality", () => { + var registry = new FinalizationRegistry(() => {}); + + var count = 0; + var increment = () => { + count++; + }; + + registry.cleanupSome(increment); + + expect(count).toBe(0); + + registerInDifferentScope(registry); + gc(); + + registry.cleanupSome(increment); + + expect(count).toBe(1); +}); + +test("errors", () => { + var registry = new FinalizationRegistry(() => {}); + + expect(() => { + registry.cleanupSome(5); + }).toThrowWithMessage(TypeError, "is not a function"); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.prototype.register.js b/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.prototype.register.js new file mode 100644 index 0000000000..04daa86ad3 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.prototype.register.js @@ -0,0 +1,35 @@ +test("length is 2", () => { + expect(FinalizationRegistry.prototype.register).toHaveLength(2); +}); + +test("basic functionality", () => { + var registry = new FinalizationRegistry(() => {}); + + var target1 = {}; + var heldValue1 = {}; + + registry.register(target1, heldValue1); + + var target2 = {}; + var heldValue2 = {}; + var token = {}; + + registry.register(target2, heldValue2, token); +}); + +test("errors", () => { + var registry = new FinalizationRegistry(() => {}); + + expect(() => { + registry.register(5, {}); + }).toThrowWithMessage(TypeError, "is not an object"); + + expect(() => { + var a = {}; + registry.register(a, a); + }).toThrowWithMessage(TypeError, "Target and held value must not be the same"); + + expect(() => { + registry.register({}, {}, 5); + }).toThrowWithMessage(TypeError, "is not an object"); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.prototype.unregister.js b/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.prototype.unregister.js new file mode 100644 index 0000000000..b67c9c9f69 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/FinalizationRegistry/FinalizationRegistry.prototype.unregister.js @@ -0,0 +1,25 @@ +test("length is 2", () => { + expect(FinalizationRegistry.prototype.unregister).toHaveLength(1); +}); + +test("basic functionality", () => { + var registry = new FinalizationRegistry(() => {}); + + var target = {}; + var heldValue = {}; + var token = {}; + + registry.register(target, heldValue, token); + + expect(registry.unregister({})).toBe(false); + expect(registry.unregister(token)).toBe(true); + expect(registry.unregister(token)).toBe(false); +}); + +test("errors", () => { + var registry = new FinalizationRegistry(() => {}); + + expect(() => { + registry.unregister(5); + }).toThrowWithMessage(TypeError, "is not an object"); +});