LibJS: Requeue aborted FinalizationRegistry cleanup

When an automatic cleanup job aborts after invoking a callback, requeue
the registry if records with empty targets remain. This keeps later held
values from being stranded until another target dies.

Add test-js coverage that drains the automatic cleanup queue after two
records die and the first cleanup callback throws.
This commit is contained in:
Andreas Kling 2026-05-14 16:20:34 +02:00 committed by Andreas Kling
parent 0e8bc451a7
commit 841fae0d57
5 changed files with 39 additions and 1 deletions

View file

@ -51,6 +51,15 @@ bool FinalizationRegistry::remove_by_token(Cell& unregister_token)
return removed;
}
bool FinalizationRegistry::has_empty_cells() const
{
for (auto& record : m_records) {
if (!record.target)
return true;
}
return false;
}
void FinalizationRegistry::remove_dead_cells(Badge<GC::Heap>)
{
auto any_cells_were_removed = false;

View file

@ -30,6 +30,7 @@ public:
void add_finalization_record(Cell& target, Value held_value, Cell* unregister_token);
bool remove_by_token(Cell& unregister_token);
ThrowCompletionOr<void> cleanup(GC::Ptr<JobCallback> = {});
bool has_empty_cells() const;
virtual Cell const& owner_cell(Badge<GC::Heap>) const override { return *this; }
virtual void remove_dead_cells(Badge<GC::Heap>) override;

View file

@ -488,7 +488,9 @@ void VM::run_queued_finalization_registry_cleanup_jobs()
while (!m_finalization_registry_cleanup_jobs.is_empty()) {
auto registry = m_finalization_registry_cleanup_jobs.take_last();
// FIXME: Handle any uncatched exceptions here.
(void)registry->cleanup();
auto result = registry->cleanup();
if (result.is_error() && registry->has_empty_cells())
m_finalization_registry_cleanup_jobs.append(registry);
}
}

View file

@ -70,6 +70,26 @@ test("cleanup can process multiple dead records from one garbage collection", ()
expect(heldValues).toEqual(["first", "second"]);
});
test("automatic cleanup continues after a callback throws", () => {
var heldValues = [];
var registry = new FinalizationRegistry(value => {
heldValues.push(value);
if (value === "first") throw new Error("expected abrupt completion");
});
evaluateSource("var __finalizationRegistryCleanupAfterThrowTarget1 = {};");
evaluateSource("var __finalizationRegistryCleanupAfterThrowTarget2 = {};");
registry.register(globalThis.__finalizationRegistryCleanupAfterThrowTarget1, "first");
registry.register(globalThis.__finalizationRegistryCleanupAfterThrowTarget2, "second");
markAsGarbage("__finalizationRegistryCleanupAfterThrowTarget1");
markAsGarbage("__finalizationRegistryCleanupAfterThrowTarget2");
gc();
runQueuedFinalizationRegistryCleanupJobs();
expect(heldValues).toEqual(["first", "second"]);
});
test("cleanup helper errors", () => {
var registry = new FinalizationRegistry(() => {});

View file

@ -53,6 +53,12 @@ TESTJS_GLOBAL_FUNCTION(run_queued_promise_jobs, runQueuedPromiseJobs)
return JS::js_undefined();
}
TESTJS_GLOBAL_FUNCTION(run_queued_finalization_registry_cleanup_jobs, runQueuedFinalizationRegistryCleanupJobs)
{
vm.run_queued_finalization_registry_cleanup_jobs();
return JS::js_undefined();
}
TESTJS_GLOBAL_FUNCTION(get_weak_set_size, getWeakSetSize)
{
auto object = TRY(vm.argument(0).to_object(vm));