Skip to content

WeakCollections

Roger Johansson edited this page Jan 14, 2026 · 1 revision

WeakCollections (WeakMap, WeakSet, WeakRef)

This page documents how Asynkron.JsEngine implements JavaScript weak collections - data structures that hold weak references to objects, allowing garbage collection.

Architecture Overview

flowchart TB
    subgraph JS["JavaScript API"]
        WeakMap["new WeakMap()"]
        WeakSet["new WeakSet()"]
        WeakRef["new WeakRef(target)"]
    end
    
    subgraph Runtime["Runtime Classes"]
        JsWeakMap["JsWeakMap"]
        JsWeakSet["JsWeakSet"]
        WeakRefObj["WeakRef Object"]
    end
    
    subgraph DotNet[".NET Foundation"]
        CWT["ConditionalWeakTable<object, object?>"]
        GC["Garbage Collector"]
    end
    
    WeakMap --> JsWeakMap
    WeakSet --> JsWeakSet
    WeakRef --> WeakRefObj
    
    JsWeakMap --> CWT
    JsWeakSet --> CWT
    CWT --> GC
Loading

Why Weak References?

Weak collections solve the problem of memory leaks from metadata storage:

// Problem: this Map prevents obj from being garbage collected
const metadata = new Map();
let obj = { data: "important" };
metadata.set(obj, { timestamp: Date.now() });
obj = null;  // obj still in memory - Map holds strong reference!

// Solution: WeakMap allows GC
const metadata = new WeakMap();
let obj = { data: "important" };
metadata.set(obj, { timestamp: Date.now() });
obj = null;  // obj can be collected - WeakMap holds weak reference

ConditionalWeakTable Foundation

All weak collections use .NET's ConditionalWeakTable<TKey, TValue>:

flowchart LR
    subgraph CWT["ConditionalWeakTable"]
        Entry1["Key (weak) → Value"]
        Entry2["Key (weak) → Value"]
        Entry3["Key (weak) → Value"]
    end
    
    subgraph GC["When Key is Collected"]
        Remove["Entry auto-removed"]
    end
    
    Entry1 -.->|key collected| Remove
Loading

Why ConditionalWeakTable?

  • Keys are held weakly - GC can collect them
  • Entries auto-remove when key is collected
  • Thread-safe by design
  • Cannot be enumerated (matches JS spec)

WeakMap Implementation

Core Class

File: JsTypes/JsWeakMap.cs

public sealed class JsWeakMap : IJsObjectLike, IPropertyDefinitionHost, 
    IExtensibilityControl, IPrototypeAccessorProvider, IAsJsValue
{
    // Use ConditionalWeakTable for weak reference semantics
    // Keys must be objects, values stored as object? (boxing unavoidable)
    private readonly ConditionalWeakTable<object, object?> _entries = new();
}

Methods

Method Description
set(key, value) Associates value with key, returns WeakMap for chaining
get(key) Returns value or undefined
has(key) Returns true if key exists
delete(key) Removes entry, returns true if existed
public JsWeakMap Set(JsValue key, JsValue value)
{
    var keyObj = JsWeakCollectionHelpers.ExtractWeakKeyObject(key);
    
    // WeakMap only accepts objects as keys
    if (keyObj == null)
    {
        throw new Exception("Invalid value used as weak map key");
    }
    
    // Use Remove + Add pattern for update semantics
    _entries.Remove(keyObj);
    _entries.Add(keyObj, ExtractValueObject(value));
    return this;
}

public JsValue Get(JsValue key)
{
    var keyObj = JsWeakCollectionHelpers.ExtractWeakKeyObject(key);
    if (keyObj == null) return JsValue.Undefined;
    
    if (_entries.TryGetValue(keyObj, out var storedValue))
    {
        return WrapValueObject(storedValue);
    }
    return JsValue.Undefined;
}

Value Storage

Values must be stored as object? due to ConditionalWeakTable constraints:

private static object? ExtractValueObject(JsValue value)
{
    return value.Kind switch
    {
        JsValueKind.Undefined => Symbol.Undefined,  // Special sentinel
        JsValueKind.Null => null,
        JsValueKind.Boolean => value.NumberValue != 0,  // Box boolean
        JsValueKind.Number => value.NumberValue,        // Box double
        JsValueKind.String => value.ObjectValue ?? string.Empty,
        JsValueKind.Symbol => value.ObjectValue,
        JsValueKind.BigInt => value.ObjectValue,
        JsValueKind.Object => value.ObjectValue,
        _ => throw new InvalidOperationException()
    };
}

WeakSet Implementation

Core Class

File: JsTypes/JsWeakSet.cs

public sealed class JsWeakSet : IJsObjectLike, IPropertyDefinitionHost,
    IExtensibilityControl, IPrototypeAccessorProvider, IAsJsValue
{
    // Use ConditionalWeakTable to track object membership
    // We use null as value since we only care about key presence
    private readonly ConditionalWeakTable<object, object?> _values = new();
}

Methods

Method Description
add(value) Adds object, returns WeakSet for chaining
has(value) Returns true if object is in set
delete(value) Removes object, returns true if existed
public JsWeakSet Add(JsValue value)
{
    var obj = JsWeakCollectionHelpers.ExtractWeakKeyObject(value);
    
    // WeakSet only accepts objects as values
    if (obj == null)
    {
        throw new Exception("Invalid value used in weak set");
    }
    
    // Add if not already present
    if (!_values.TryGetValue(obj, out _))
    {
        _values.Add(obj, null);  // null value - only presence matters
    }
    
    return this;
}

public bool Has(JsValue value)
{
    var obj = JsWeakCollectionHelpers.ExtractWeakKeyObject(value);
    return obj != null && _values.TryGetValue(obj, out _);
}

Key Validation

Both WeakMap and WeakSet reject primitive keys/values:

File: JsTypes/JsWeakCollectionHelpers.cs

flowchart TD
    Input["ExtractWeakKeyObject(value)"]
    
    Input --> PrimitiveCheck{Is primitive?}
    PrimitiveCheck -->|null/undefined| Reject1["return null"]
    PrimitiveCheck -->|string/number/boolean| Reject2["return null"]
    PrimitiveCheck -->|no| ObjCheck
    
    ObjCheck{Object type?}
    ObjCheck -->|JsSymbol| Reject3["return null"]
    ObjCheck -->|Value type| Reject4["return null"]
    ObjCheck -->|Reference type| Accept["return object"]
Loading
internal static class JsWeakCollectionHelpers
{
    public static object? ExtractWeakKeyObject(JsValue value)
    {
        // Reject primitives immediately
        if (value.IsNull || value.IsUndefined || value.IsString || 
            value.IsNumber || value.IsBoolean)
        {
            return null;
        }

        var obj = value.ObjectValue;

        // Unwrap nested JsValue wrappers
        while (obj is JsValue nested)
        {
            obj = nested.ObjectValue;
        }

        return IsWeakCollectionObject(obj) ? obj : null;
    }

    private static bool IsWeakCollectionObject(object? value)
    {
        switch (value)
        {
            case null:
            case Symbol sym when ReferenceEquals(sym, Symbol.Undefined):
            case string:
                return false;
        }

        // Reject value types (primitives boxed)
        if (value.GetType().IsValueType)
        {
            return false;
        }

        // Reject JsSymbol as keys
        return value is not JsSymbol;
    }
}

Valid Keys:

  • Plain objects {}
  • Arrays []
  • Functions
  • Class instances
  • Any reference type

Invalid Keys:

  • null, undefined
  • number, string, boolean
  • Symbol
  • Boxed value types

WeakRef Implementation

WeakRef allows holding a weak reference to a single object:

Files:

  • StdLib/WeakRef/WeakRefConstructor.cs
  • StdLib/WeakRef/WeakRefPrototype.cs

Constructor

[JsHostMethod("WeakRef", Length = 1d)]
private JsValue RequireTargetObject(IReadOnlyList<JsValue> args)
{
    var target = args.GetArgument(0);
    if (!target.IsObject || target.AsObject() is null)
    {
        throw ThrowTypeError("WeakRef target must be an object", realm: Realm);
    }
    return target;
}

private void InitializeWeakRef(JsObject instance, JsValue target)
{
    instance.SetProperty("_target", target);
    instance.RealmState ??= Realm;
}

deref() Method

[JsHostMethod("deref", Length = 0d)]
public JsValue Deref(JsValue thisValue, IReadOnlyList<JsValue> _)
{
    if (thisValue.AsObject() is { } obj && obj.TryGetProperty("_target", out var stored))
    {
        return stored;
    }
    return JsValue.Undefined;
}

Note: The current implementation stores _target as a regular property (strong reference). For true weak semantics, this would need to use System.WeakReference<T> internally.

Constructor Initialization

Both WeakMap and WeakSet support initialization from iterables:

File: StdLib/MapSet/WeakMapConstructor.cs

protected override void PopulateInstance(JsWeakMap instance, IReadOnlyList<JsValue> args)
{
    if (args.Count == 0 || args[0].IsNull || args[0].IsUndefined)
        return;

    if (!args[0].TryGetObject<JsArray>(out var entries))
        return;

    foreach (var entry in entries.Items)
    {
        if (!entry.TryGetObject<JsArray>(out var pair) || pair.Items.Count < 2)
            continue;

        instance.Set(pair.GetElement(0), pair.GetElement(1));
    }
}
// Usage
const wm = new WeakMap([
    [obj1, "value1"],
    [obj2, "value2"]
]);

const ws = new WeakSet([obj1, obj2, obj3]);

GC Interaction

sequenceDiagram
    participant JS as JavaScript Code
    participant WM as WeakMap
    participant CWT as ConditionalWeakTable
    participant GC as Garbage Collector
    
    JS->>WM: set(obj, value)
    WM->>CWT: Add(obj, value)
    Note over CWT: obj held weakly
    
    JS->>JS: obj = null
    Note over JS: No more references to obj
    
    GC->>GC: Collection cycle
    GC->>CWT: Finalize obj
    CWT->>CWT: Auto-remove entry
    
    JS->>WM: has(obj)
    WM->>CWT: TryGetValue
    CWT-->>WM: false (not found)
    WM-->>JS: false
Loading

Key Points:

  1. ConditionalWeakTable holds keys weakly
  2. When key is garbage collected, entry is automatically removed
  3. No manual cleanup required
  4. Cannot detect when entries are removed (no "weak" events)

Differences from Regular Collections

Feature Map/Set WeakMap/WeakSet
Key types Any value Objects only
Iteration for...of, .forEach() Not iterable
.size property Yes No
Prevents GC Yes (strong refs) No (weak refs)
Memory leaks Possible Auto-cleanup

Common Use Cases

1. Private Data / Encapsulation

const privateData = new WeakMap();

class Person {
    constructor(name, ssn) {
        this.name = name;
        privateData.set(this, { ssn });  // Truly private
    }
    
    getSSN() {
        return privateData.get(this).ssn;
    }
}

2. DOM Node Metadata

const nodeData = new WeakMap();

function processNode(node) {
    if (!nodeData.has(node)) {
        nodeData.set(node, { processedAt: Date.now() });
    }
    // When node is removed from DOM and GC'd, metadata is auto-cleaned
}

3. Memoization Cache

const cache = new WeakMap();

function expensive(obj) {
    if (cache.has(obj)) return cache.get(obj);
    const result = /* expensive computation */;
    cache.set(obj, result);
    return result;
}
// Cache entries auto-clean when objects are no longer needed

4. Object Identity Tracking

const seen = new WeakSet();

function detectCycle(obj) {
    if (seen.has(obj)) return true;
    seen.add(obj);
    // ... traverse children
    seen.delete(obj);
    return false;
}

Thread Safety

ConditionalWeakTable provides built-in thread safety:

  • Multiple threads can read/write concurrently
  • No explicit locking needed
  • Safe for use in async/await patterns

Key Files

Category File Path
WeakMap Core src/Asynkron.JsEngine/JsTypes/JsWeakMap.cs
WeakSet Core src/Asynkron.JsEngine/JsTypes/JsWeakSet.cs
Key Validation src/Asynkron.JsEngine/JsTypes/JsWeakCollectionHelpers.cs
WeakMap Constructor src/Asynkron.JsEngine/StdLib/MapSet/WeakMapConstructor.cs
WeakMap Prototype src/Asynkron.JsEngine/StdLib/MapSet/WeakMapPrototype.cs
WeakSet Constructor src/Asynkron.JsEngine/StdLib/MapSet/WeakSetConstructor.cs
WeakSet Prototype src/Asynkron.JsEngine/StdLib/MapSet/WeakSetPrototype.cs
WeakRef Constructor src/Asynkron.JsEngine/StdLib/WeakRef/WeakRefConstructor.cs
WeakRef Prototype src/Asynkron.JsEngine/StdLib/WeakRef/WeakRefPrototype.cs
Tests tests/Asynkron.JsEngine.Tests/WeakMapTests.cs
Tests tests/Asynkron.JsEngine.Tests/WeakSetTests.cs

See Also

Clone this wiki locally