From be81949979afb8d8efc92dc1651e24d1d38c2003 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 13 Nov 2026 02:28:08 -0300 Subject: [PATCH 1/3] perf(runtime): give NSArray wrappers Symbol.iterator through the prototype Arrays returned from native methods got a per-instance Symbol.iterator function. Installed once on NSArray's prototype template instead, every array wrapper inherits it, whichever path built it, and none carries an own property: under the canary wrapper lifetime mode that property was state, one state object and associated token per array. --- NativeScript/runtime/Interop.mm | 6 ------ NativeScript/runtime/MetadataBuilder.mm | 4 ++++ NativeScript/runtime/SymbolIterator.h | 3 ++- NativeScript/runtime/SymbolIterator.mm | 27 +++++++------------------ 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index 05b65de34..6c594a9e7 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -18,7 +18,6 @@ #include "Reference.h" #include "Runtime.h" #include "RuntimeConfig.h" -#include "SymbolIterator.h" #include "SymbolLoader.h" #include "UnmanagedType.h" #include "robin_hood.h" @@ -1399,11 +1398,6 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, LocalPrototypeTemplate()->Set( tns::ToV8String(isolate, "toString"), FunctionTemplate::New(isolate, MetadataBuilder::ToStringFunctionCallback)); + if (meta->type() == MetaType::Interface && strcmp(meta->name(), "NSArray") == 0) { + SymbolIterator::Install(isolate, ctorFuncTemplate->PrototypeTemplate()); + } if (meta->type() == MetaType::Interface) { const InterfaceMeta* interfaceMeta = static_cast(meta); diff --git a/NativeScript/runtime/SymbolIterator.h b/NativeScript/runtime/SymbolIterator.h index 6cf66fc06..800da55bb 100644 --- a/NativeScript/runtime/SymbolIterator.h +++ b/NativeScript/runtime/SymbolIterator.h @@ -7,7 +7,8 @@ namespace tns { class SymbolIterator { public: - static void Set(v8::Local context, v8::Local object); + // On NSArray's prototype template, so every array wrapper inherits it. + static void Install(v8::Isolate* isolate, v8::Local prototypeTemplate); private: static v8::Local CreateIteratorObject(v8::Local context, v8::Local object); static void NextCallback(const v8::FunctionCallbackInfo& args); diff --git a/NativeScript/runtime/SymbolIterator.mm b/NativeScript/runtime/SymbolIterator.mm index 33c940838..123b4e269 100644 --- a/NativeScript/runtime/SymbolIterator.mm +++ b/NativeScript/runtime/SymbolIterator.mm @@ -7,26 +7,13 @@ namespace tns { -void SymbolIterator::Set(Local context, Local object) { - Isolate* isolate = v8::Isolate::GetCurrent(); - - // Iterates its receiver rather than capturing `object`: in the canary - // wrapper lifetime mode this property lives on the object's state, which a - // reference back to the object would keep alive until native death. - Local iterator; - bool success = v8::Function::New(context, - [](const FunctionCallbackInfo& args) { - Isolate* isolate = args.GetIsolate(); - Local context = isolate->GetCurrentContext(); - Local result = - CreateIteratorObject(context, args.This()); - args.GetReturnValue().Set(result); - }) - .ToLocal(&iterator); - tns::Assert(success, isolate); - - Local symbolIteratorKey = Symbol::GetIterator(isolate); - success = object.As()->Set(context, symbolIteratorKey, iterator).FromMaybe(false); +void SymbolIterator::Install(Isolate* isolate, Local prototypeTemplate) { + prototypeTemplate->Set( + Symbol::GetIterator(isolate), + FunctionTemplate::New(isolate, [](const FunctionCallbackInfo& args) { + Local context = args.GetIsolate()->GetCurrentContext(); + args.GetReturnValue().Set(CreateIteratorObject(context, args.This())); + })); } Local SymbolIterator::CreateIteratorObject(Local context, Local object) { From 03d1438f60f389487233fb1d920fd5453cebce6b Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 13 Nov 2026 02:28:08 -0300 Subject: [PATCH 2/3] feat(runtime): weak collections key native objects by lifetime in canary mode In canary mode a native object's wrapper dies with its JS references while the object lives on, so WeakMap/WeakSet entries, WeakRef targets and FinalizationRegistry registrations keyed on the wrapper were lost with it. A canary-only builtin replaces the four globals with subclasses that key a native object by an opaque per-object key (frozen, null-prototype), held by the object's state and so alive exactly until the object is deallocated; WeakRef.prototype.deref (and the legacy get) return its live wrapper, a new one, or undefined once the object is gone. Values that are not canaries pass through unchanged, and other modes keep the built-ins. The key and its reverse lookup (a private on the key naming the object, checked against the registry, then an ObjC weak load so a deallocating object is never retained) are internal to the builtin. --- NativeScript/runtime/WeakRef.cpp | 14 ++ NativeScript/runtime/WrapperLifetimeCanary.h | 5 + NativeScript/runtime/WrapperLifetimeCanary.mm | 125 +++++++++++++- NativeScript/runtime/js/weak-collections.js | 98 +++++++++++ .../WrapperLifetimeWeakCollectionsTests.js | 154 ++++++++++++++++++ TestRunner/app/tests/index.js | 1 + tools/js2c-inputs.xcfilelist | 1 + 7 files changed, 393 insertions(+), 5 deletions(-) create mode 100644 NativeScript/runtime/js/weak-collections.js create mode 100644 TestRunner/app/tests/WrapperLifetimeWeakCollectionsTests.js diff --git a/NativeScript/runtime/WeakRef.cpp b/NativeScript/runtime/WeakRef.cpp index de04d3acc..7831b1571 100644 --- a/NativeScript/runtime/WeakRef.cpp +++ b/NativeScript/runtime/WeakRef.cpp @@ -2,6 +2,8 @@ #include "BuiltinLoader.h" #include "Helpers.h" +#include "WrapperLifetime.h" +#include "WrapperLifetimeCanary.h" using namespace v8; @@ -18,6 +20,18 @@ void WeakRef::Init(Local context) { tns::LogError(isolate, tc); } tns::Assert(success, isolate); + + if (GetWrapperLifetimeMode() != WrapperLifetimeMode::kCanary) { + return; + } + Local binding = Object::New(isolate); + success = WrapperLifetimeCanary::RegisterWrapFunctions(context, binding) && + BuiltinLoader::RunBuiltin(context, BuiltinId::kWeakCollections, binding) + .ToLocal(&result); + if (!success && tc.HasCaught()) { + tns::LogError(isolate, tc); + } + tns::Assert(success, isolate); } } // namespace tns diff --git a/NativeScript/runtime/WrapperLifetimeCanary.h b/NativeScript/runtime/WrapperLifetimeCanary.h index 255859e24..fb73612d6 100644 --- a/NativeScript/runtime/WrapperLifetimeCanary.h +++ b/NativeScript/runtime/WrapperLifetimeCanary.h @@ -47,6 +47,11 @@ class WrapperLifetimeCanary { // the mode is canary. static void ConfigureInstanceTemplate(v8::Local instanceTemplate); + // Adds `unwrap` (a canary's native object -> its weak-collection key) and + // `wrap` (key -> live or new canary, or undefined once the object is gone) + // to `binding`, for the weak-collections builtin. Canary mode only. + static bool RegisterWrapFunctions(v8::Local context, v8::Local binding); + // The record of an object that already has state in this isolate, for a new // canary to share. Null otherwise. static std::shared_ptr Find(v8::Isolate* isolate, id object); diff --git a/NativeScript/runtime/WrapperLifetimeCanary.mm b/NativeScript/runtime/WrapperLifetimeCanary.mm index cf44e239b..674c9ab87 100644 --- a/NativeScript/runtime/WrapperLifetimeCanary.mm +++ b/NativeScript/runtime/WrapperLifetimeCanary.mm @@ -7,6 +7,7 @@ #include #include #include +#include "ArgConverter.h" #include "Caches.h" #include "DataWrapper.h" #include "EventLoop.h" @@ -32,6 +33,7 @@ #endif extern "C" id objc_initWeakOrNil(id* location, id value); +extern "C" id objc_loadWeakRetained(id* location); extern "C" void objc_destroyWeak(id* location); using namespace v8; @@ -51,8 +53,10 @@ // tells, when a sweep reads it. bool reportsDeath = false; // An ObjC weak reference to the object, zeroed by the runtime when it is - // deallocated, on any thread. Set for records nothing reports. + // deallocated, on any thread. Set for records nothing reports, and for any + // record whose object became a weak-collection key (Wrap reads it). id object = nil; + bool weakSet = false; ~CanaryRecord() { objc_destroyWeak(&object); } @@ -90,6 +94,10 @@ bool ObjectAlive() const { struct CanaryState { std::shared_ptr registry = std::make_shared(); + // The weak collections' key for a native object hangs off its state, and + // names the object it stands for. + Global keyOfState; + Global objectOfKey; }; void DrainDead(const std::shared_ptr& registry) { @@ -320,10 +328,13 @@ bool StateForWrite(Isolate* isolate, Local holder, Local* out) { if (slot == nullptr) { auto record = std::make_shared(); record->reportsDeath = extended || NS_CANARY_DEATH_SIGNAL == NS_CANARY_DEATH_ASSOCIATION; - if (!record->reportsDeath && objc_initWeakOrNil(&record->object, object) == nil) { - // The class refuses weak references. - registry->records.erase((__bridge void*)object); - return false; + if (!record->reportsDeath) { + if (objc_initWeakOrNil(&record->object, object) == nil) { + // The class refuses weak references. + registry->records.erase((__bridge void*)object); + return false; + } + record->weakSet = true; } record->state.Reset(isolate, Object::New(isolate, Null(isolate), nullptr, nullptr, 0)); slot = std::move(record); @@ -502,6 +513,98 @@ void PostSweepAfterGC(Isolate* isolate, GCType, GCCallbackFlags) { } #endif +// The weak collections' key for a canary's native object: a frozen, +// null-prototype object that lives exactly as long as the object's state, that +// is, until the native object is deallocated. The holder itself for anything +// that is not a canary. +void UnwrapCallback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + Local value = info[0]; + info.GetReturnValue().Set(value); + CanaryState* canaryState = Caches::StateFor(isolate); + Local state; + if (canaryState == nullptr || !value->IsObject() || + !StateForWrite(isolate, value.As(), &state)) { + return; + } + Local context = isolate->GetCurrentContext(); + Local keyOfState = canaryState->keyOfState.Get(isolate); + Local key; + if (state->GetPrivate(context, keyOfState).ToLocal(&key) && key->IsObject()) { + info.GetReturnValue().Set(key); + return; + } + ObjectWeakCallbackState* canary = CanaryOf(isolate, value.As()); + CanaryRecord* record = canary->canaryRecord_.get(); + if (!record->weakSet) { + if (objc_initWeakOrNil(&record->object, canary->lifetimeTarget_) == nil) { + return; + } + record->weakSet = true; + } + Local newKey = Object::New(isolate, Null(isolate), nullptr, nullptr, 0); + uint64_t address = reinterpret_cast((__bridge void*)canary->lifetimeTarget_); + if (!newKey + ->SetPrivate(context, canaryState->objectOfKey.Get(isolate), + BigInt::NewFromUnsigned(isolate, address)) + .FromMaybe(false) || + !newKey->SetIntegrityLevel(context, IntegrityLevel::kFrozen).FromMaybe(false) || + !state->SetPrivate(context, keyOfState, newKey).FromMaybe(false)) { + return; + } + info.GetReturnValue().Set(newKey); +} + +// The object a key stands for: its live canary or a new one, or undefined once +// the native object is gone. Anything that is not a key passes through. +void WrapCallback(const FunctionCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + Local value = info[0]; + info.GetReturnValue().Set(value); + CanaryState* canaryState = Caches::StateFor(isolate); + if (canaryState == nullptr || !value->IsObject()) { + return; + } + Local context = isolate->GetCurrentContext(); + Local key = value.As(); + Local address; + if (!key->GetPrivate(context, canaryState->objectOfKey.Get(isolate)).ToLocal(&address) || + !address->IsBigInt()) { + return; + } + info.GetReturnValue().SetUndefined(); + std::shared_ptr record; + { + std::lock_guard lock(canaryState->registry->mutex); + auto it = canaryState->registry->records.find( + reinterpret_cast(static_cast(address.As()->Uint64Value()))); + if (it == canaryState->registry->records.end()) { + return; + } + record = it->second; + } + if (!Usable(record) || !record->weakSet) { + return; + } + // A record at that address may belong to a later object: it must be the + // key's own. + Local recordKey; + Local state = Local::New(isolate, record->state); + if (!state->GetPrivate(context, canaryState->keyOfState.Get(isolate)).ToLocal(&recordKey) || + !recordKey->StrictEquals(key)) { + return; + } + id object = objc_loadWeakRetained(&record->object); + if (object == nil) { + return; + } + ObjCDataWrapper* wrapper = new ObjCDataWrapper(object); + Local result = ArgConverter::ConvertArgument(context, wrapper); + tns::DeleteWrapperIfUnused(isolate, result, wrapper); + [object release]; + info.GetReturnValue().Set(result); +} + } // namespace void WrapperLifetimeCanary::Init(Isolate* isolate) { @@ -513,6 +616,8 @@ void PostSweepAfterGC(Isolate* isolate, GCType, GCCallbackFlags) { return; } state->registry->isolate = isolate; + state->keyOfState.Reset(isolate, Private::New(isolate)); + state->objectOfKey.Reset(isolate, Private::New(isolate)); if (Runtime* runtime = Runtime::GetRuntime(isolate)) { state->registry->loop = runtime->GetEventLoop(); } @@ -526,6 +631,16 @@ void PostSweepAfterGC(Isolate* isolate, GCType, GCCallbackFlags) { #endif } +bool WrapperLifetimeCanary::RegisterWrapFunctions(Local context, Local binding) { + Isolate* isolate = v8::Isolate::GetCurrent(); + Local unwrap; + Local wrap; + return v8::Function::New(context, UnwrapCallback).ToLocal(&unwrap) && + v8::Function::New(context, WrapCallback).ToLocal(&wrap) && + binding->Set(context, tns::ToV8String(isolate, "unwrap"), unwrap).FromMaybe(false) && + binding->Set(context, tns::ToV8String(isolate, "wrap"), wrap).FromMaybe(false); +} + void WrapperLifetimeCanary::InstallDeallocHook(Class extendedClass) { if (GetWrapperLifetimeMode() != WrapperLifetimeMode::kCanary) { return; diff --git a/NativeScript/runtime/js/weak-collections.js b/NativeScript/runtime/js/weak-collections.js new file mode 100644 index 000000000..34ed856e4 --- /dev/null +++ b/NativeScript/runtime/js/weak-collections.js @@ -0,0 +1,98 @@ +"use strict"; + +// Canary wrapper lifetime mode only (run by WeakRef.cpp): WeakMap, WeakSet, +// WeakRef and FinalizationRegistry key a native object by its lifetime key +// instead of its wrapper. The wrapper is a canary that dies with its JS +// references while the native object lives on, and a later wrapper for the same +// object is a new JS object; keyed by the wrapper, entries and targets would be +// lost with it. Every other value passes through unchanged. + +const { unwrap, wrap } = binding; +const { + FinalizationRegistry: NativeFinalizationRegistry, + WeakMap: NativeWeakMap, + WeakRef: NativeWeakRef, + WeakSet: NativeWeakSet, +} = globalThis; + +class WeakMap extends NativeWeakMap { + get(key) { + return super.get(unwrap(key)); + } + + set(key, value) { + super.set(unwrap(key), value); + return this; + } + + has(key) { + return super.has(unwrap(key)); + } + + delete(key) { + return super.delete(unwrap(key)); + } +} + +class WeakSet extends NativeWeakSet { + add(value) { + super.add(unwrap(value)); + return this; + } + + has(value) { + return super.has(unwrap(value)); + } + + delete(value) { + return super.delete(unwrap(value)); + } +} + +class WeakRef extends NativeWeakRef { + constructor(target) { + super(unwrap(target)); + } + + // The live wrapper, a new one if the old one died while the native object + // lived, or undefined once the native object is gone. + deref() { + return wrap(super.deref()); + } +} + +// weak-ref.js gave the native prototype a `get` alias of `deref`; inherited +// as is, it would hand out the lifetime key. +Object.defineProperty(WeakRef.prototype, "get", { + value: WeakRef.prototype.deref, + writable: true, + configurable: true, +}); + +class FinalizationRegistry extends NativeFinalizationRegistry { + register(target, heldValue, unregisterToken) { + super.register( + unwrap(target), + heldValue, + unregisterToken === undefined ? undefined : unwrap(unregisterToken), + ); + } + + unregister(unregisterToken) { + return super.unregister(unwrap(unregisterToken)); + } +} + +for (const [name, value] of [ + ["WeakMap", WeakMap], + ["WeakSet", WeakSet], + ["WeakRef", WeakRef], + ["FinalizationRegistry", FinalizationRegistry], +]) { + Object.defineProperty(globalThis, name, { + value, + writable: true, + configurable: true, + enumerable: false, + }); +} diff --git a/TestRunner/app/tests/WrapperLifetimeWeakCollectionsTests.js b/TestRunner/app/tests/WrapperLifetimeWeakCollectionsTests.js new file mode 100644 index 000000000..d019f4314 --- /dev/null +++ b/TestRunner/app/tests/WrapperLifetimeWeakCollectionsTests.js @@ -0,0 +1,154 @@ +// WeakMap, WeakSet, WeakRef and FinalizationRegistry keyed on native objects. +// The expectations hold in every wrapper lifetime mode; under "canary" they +// depend on the weak collections keying a native object by its lifetime key +// rather than by its current wrapper. +describe("Weak collections of native objects", function () { + + var Extended = (function (_super) { + __extends(Extended, _super); + function Extended() { + return (_super !== null && _super.apply(this, arguments)) || this; + } + return Extended; + })(TNSAllocLog); + + function scrub(n) { + return n > 0 ? scrub(n - 1) + n : 0; + } + + function settle(rounds, callback) { + scrub(300); + __collect(); + __collect(); + setTimeout(function () { + NSObject.new(); + if (rounds > 1) { + settle(rounds - 1, callback); + } else { + callback(); + } + }, 10); + } + + it("keeps a WeakMap entry for a native object across a wrapper change", function (done) { + var owner = NSMutableArray.new(); + var map = new WeakMap(); + (function () { + var obj = Extended.new(); + map.set(obj, "value"); + owner.addObject(obj); + })(); + settle(2, function () { + var obj = owner.objectAtIndex(0); + expect(map.get(obj)).toBe("value"); + expect(map.has(obj)).toBe(true); + expect(map.delete(obj)).toBe(true); + expect(map.has(obj)).toBe(false); + done(); + }); + }); + + it("keeps a WeakSet member for a native object across a wrapper change", function (done) { + var owner = NSMutableArray.new(); + var set = new WeakSet(); + (function () { + var obj = Extended.new(); + set.add(obj); + owner.addObject(obj); + })(); + settle(2, function () { + expect(set.has(owner.objectAtIndex(0))).toBe(true); + done(); + }); + }); + + it("dereferences a WeakRef to a natively owned object while the object lives", function (done) { + var owner = NSMutableArray.new(); + var ref; + (function () { + var obj = Extended.new(); + obj.tag = "kept"; + ref = new WeakRef(obj); + owner.addObject(obj); + })(); + settle(2, function () { + var target = ref.deref(); + expect(target).toBe(owner.objectAtIndex(0)); + expect(target.tag).toBe("kept"); + expect(ref.get()).toBe(target); + done(); + }); + }); + + it("clears a WeakRef once the native object is gone", function (done) { + var owner = NSMutableArray.new(); + var ref; + (function () { + var obj = Extended.new(); + ref = new WeakRef(obj); + owner.addObject(obj); + })(); + settle(2, function () { + owner.removeAllObjects(); + var attempts = 10; + (function poll() { + settle(1, function () { + if (ref.deref() === undefined || --attempts === 0) { + expect(ref.deref()).toBeUndefined(); + done(); + return; + } + poll(); + }); + })(); + }); + }); + + it("runs a FinalizationRegistry callback once the native object is gone", function (done) { + var owner = NSMutableArray.new(); + var held = []; + var registry = new FinalizationRegistry(function (value) { + held.push(value); + }); + (function () { + var obj = Extended.new(); + registry.register(obj, "held"); + owner.addObject(obj); + })(); + settle(2, function () { + expect(held).toEqual([]); + owner.removeAllObjects(); + var attempts = 15; + (function poll() { + settle(1, function () { + if (held.length > 0 || --attempts === 0) { + expect(held).toEqual(["held"]); + done(); + return; + } + poll(); + }); + })(); + }); + }); + + it("keeps working as the standard collections for other keys", function () { + var key = {}; + var map = new WeakMap([[key, 1]]); + expect(map.get(key)).toBe(1); + expect(map instanceof WeakMap).toBe(true); + expect(new WeakSet([key]).has(key)).toBe(true); + expect(new WeakRef(key).deref()).toBe(key); + }); + + it("iterates NSArray wrappers through the prototype", function () { + var array = NSArray.arrayWithArray([1, 2, 3]); + var seen = []; + for (var value of array) { + seen.push(value); + } + expect(seen).toEqual([1, 2, 3]); + expect([].concat(Array.from(NSMutableArray.arrayWithArray(["a"])))).toEqual(["a"]); + expect(Object.getOwnPropertySymbols(array).length).toBe(0); + }); +}); diff --git a/TestRunner/app/tests/index.js b/TestRunner/app/tests/index.js index 37388515a..4fca8cd89 100644 --- a/TestRunner/app/tests/index.js +++ b/TestRunner/app/tests/index.js @@ -136,6 +136,7 @@ require("./NsInteropTests"); require("./GCFinalizerTests"); require("./WrapperLifetimeTests"); require("./WrapperLifetimeModesTests"); +require("./WrapperLifetimeWeakCollectionsTests"); require("./BlockCacheRaceTests"); require("./WorkerConcurrentStartupTests"); require("./WorkerOptionsTests"); diff --git a/tools/js2c-inputs.xcfilelist b/tools/js2c-inputs.xcfilelist index dd339c974..ca3913c1d 100644 --- a/tools/js2c-inputs.xcfilelist +++ b/tools/js2c-inputs.xcfilelist @@ -30,5 +30,6 @@ $(SRCROOT)/NativeScript/runtime/js/require-factory.js $(SRCROOT)/NativeScript/runtime/js/structured-clone.js $(SRCROOT)/NativeScript/runtime/js/text-encoding.js $(SRCROOT)/NativeScript/runtime/js/ts-helpers.js +$(SRCROOT)/NativeScript/runtime/js/weak-collections.js $(SRCROOT)/NativeScript/runtime/js/weak-ref.js $(SRCROOT)/NativeScript/runtime/js/worker-events.js From 767c3229ca8ae75fea58d9b36fd1a046051f782e Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 13 Nov 2026 02:28:48 -0300 Subject: [PATCH 3/3] docs(runtime): weak collections under canary mode --- docs/knowledge/v8-resurrecting-finalizers.md | 8 ++++++-- types/ns-runtime.d.ts | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/knowledge/v8-resurrecting-finalizers.md b/docs/knowledge/v8-resurrecting-finalizers.md index 0e4bbbcf3..cef1f3cac 100644 --- a/docs/knowledge/v8-resurrecting-finalizers.md +++ b/docs/knowledge/v8-resurrecting-finalizers.md @@ -243,8 +243,12 @@ rooted at canary death instead, because a state reachable only through the canar the same collection. The cost: state that references its own object (a closure over `this`) keeps it alive. A canary built after a death is a new object, but `===` cannot tell: only one canary lives at a time, and anything still referencing the old one would have kept it alive. -What does notice is whatever keys on the canary without keeping it: `WeakMap` keys and `WeakRef`s -of the old canary, and native `#private` fields, which are branded on it. Other +What could notice is whatever keys on the canary without keeping it. The weak collections +don't: in canary mode a builtin (`js/weak-collections.js`) replaces `WeakMap`, `WeakSet`, +`WeakRef` and `FinalizationRegistry` with subclasses that key a native object by an opaque, +frozen key held by its state, so entries and targets live exactly as long as the native object, +and `WeakRef.prototype.deref` returns the live canary, a new one, or `undefined` once the object +is gone. Native `#private` fields are branded on the canary and do not carry over. Other limits: `Object.freeze`, `seal` and `preventExtensions` throw on a canary (V8 refuses them on objects with interceptors); index-keyed properties are not forwarded and die with the canary; a write through an object whose prototype is a canary lands in the canary's state, since diff --git a/types/ns-runtime.d.ts b/types/ns-runtime.d.ts index 3d4730d56..a29776ed9 100644 --- a/types/ns-runtime.d.ts +++ b/types/ns-runtime.d.ts @@ -54,8 +54,9 @@ declare module "ns:runtime" { * - `"canary"`: no resurrection. A wrapper in scope dies with its JS * references; JS properties set on it live on a state object kept until * the native object is deallocated, and a wrapper built for the object - * later has them again. Weak references to the old wrapper (WeakMap keys, - * WeakRef) and native `#private` fields do not carry over. + * later has them again. WeakMap, WeakSet, WeakRef and FinalizationRegistry + * key native objects by their lifetime in this mode, so they carry over + * too; native `#private` fields do not. * State that references its own object keeps that object alive. */ wrapperLifetimeMode: "resurrecting" | "rooted" | "canary";