-
Notifications
You must be signed in to change notification settings - Fork 1
WeakCollections
This page documents how Asynkron.JsEngine implements JavaScript weak collections - data structures that hold weak references to objects, allowing garbage collection.
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
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 referenceAll 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
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)
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();
}| 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;
}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()
};
}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();
}| 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 _);
}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"]
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 allows holding a weak reference to a single object:
Files:
StdLib/WeakRef/WeakRefConstructor.csStdLib/WeakRef/WeakRefPrototype.cs
[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;
}[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
_targetas a regular property (strong reference). For true weak semantics, this would need to useSystem.WeakReference<T>internally.
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]);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
Key Points:
- ConditionalWeakTable holds keys weakly
- When key is garbage collected, entry is automatically removed
- No manual cleanup required
- Cannot detect when entries are removed (no "weak" events)
| 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 |
const privateData = new WeakMap();
class Person {
constructor(name, ssn) {
this.name = name;
privateData.set(this, { ssn }); // Truly private
}
getSSN() {
return privateData.get(this).ssn;
}
}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
}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 neededconst seen = new WeakSet();
function detectCycle(obj) {
if (seen.has(obj)) return true;
seen.add(obj);
// ... traverse children
seen.delete(obj);
return false;
}ConditionalWeakTable provides built-in thread safety:
- Multiple threads can read/write concurrently
- No explicit locking needed
- Safe for use in async/await patterns
| 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 |
- JsObject-and-Properties - Regular object storage
- Pooling-Deep-Dive - Memory management patterns
- Performance-Patterns - Optimization techniques