Building a DOM in JavaScript: Ownership, X-Refs, and Copy Semantics

building-a-dom-in-javascript:-ownership,-x-refs,-and-copy-semantics

This is the second article in my DOM-handling series.

If you haven’t read the first part – Which Language Handles DOM-Like Models Best? – you might want to start there for context.

JavaScript’s garbage collection frees you from explicit memory frees, but it won’t save you from “dangling” references to semi-destroyed objects, multiparenting, or topology corruption in complex object graphs.

Building a DOM-ish model (Document → Card → CardItem) with predictable behavior means making manual decisions about ownership, weak references, and deep copying.

Design Decisions

Full JS solution (~330 LOC) here: 🔗 https://jsfiddle.net/0tv5yj37/4/

Ownership and Reference Tracking

  • Parent pointers (parent field) enforce single ownership of a node at runtime.
  • Registration sets (inboundButtons, inboundConnectors) simulate weak references:

    • When an object is detached, it actively clears inbound links.
    • This prevents dangling references after deletion.
  • Detachment cascade ensures that deleting a node cleans up all references to it before it’s gone.

Handling / Avoidance of Shared Mutable State

  • Shared Style and Bitmap instances are protected with Object.freeze() to enforce immutability at runtime.
  • Copy-on-write is achieved via mutateStyle() – editing a shared resource automatically clones it.
  • No shared mutable structures exist in the graph; all shared data is immutable.

Trade-Offs

  • Manual enforcement: Because JavaScript won’t stop multiparenting or cycles, we must check on every addItem() and setParent().
  • Two-phase deep copy: Required to preserve shared topology while creating new mutable nodes.
  • Cognitive overhead: The model spans ~330 LOC, much of it boilerplate for deepCopy() and resolve() methods.

Safety Guarantees

From the Language

  • Garbage collection eliminates leaks from unreferenced objects (eventually).
  • No raw memory access, so undefined behavior from freed memory is impossible.

From the Design

  • Ghosting prevention via explicit unlinking (detach()): On object removal, cross-references are broken to ensure no part of the remaining object hierarchy can continue accessing the removed object in a “post-detached” non-working state.
  • Runtime safety: Invariant checks (Card already has a parent, Loop detected) ensure structural integrity.
  • Topology safety: Connectors and Buttons x-refs are nulled on detach and rewired on copy.

Code Size & Cognitive Overhead

  • Size: ~330 LOC, with most complexity in repetitive deep copy/resolve patterns.
  • Cognitive load: Developers must understand the lifecycle of every object and call detach() correctly; missing one leads to “ghost” references until GC eventually runs.

Usage

//// This code throws exception - style is immutable
// doc.cards[0].items[0].style.font = "Helvetica";

// unshare on mutate:
myText.mutateStyle(style => {
   style.font = "Helvetica";
});

// Drop X-refs
doc.cards[0].removeItem(doc.cards[0].items[0]);
console.log("after item deleted and link lost", doc);
// Result shows that "hello" text is removed both from card and connector.
// Cross-references are explicitly broken to prevent ghosting -
// no remaining part of the object hierarchy can access the removed item.

// Copy
const newDoc = deepCopy(doc);
console.log("after copy, preserving button and connector links", newDoc);
// Result: newDoc is structurally and data-wise identical to doc,
// though consisting of newly allocated objects.

// Safety net against multiparenting and cycles:
// The following code successfully compiles but crashes
// because of multiparenting attempt
doc.addCard(newDoc.cards[0]);

// This code compiles and runs
// because copy operation prevents multiparenting
doc.addCard(deepCopy(newDoc.cards[0]));

Evaluation Table

Criterion Description Verdict
Memory safety Avoids unsafe access patterns ⚠️ Safe from UAF/null deref by design; GC handles deallocation, though stack references can see partially destructed (detached) objects
Leak prevention Avoids memory leaks ⚠️ GC works eventually, with memory overhead and causes sudden pauses
Ownership clarity Are ownership relations clear and enforced? ⚠️ Implemented in code, enforced only at runtime via exceptions
Copy semantics Are copy/clone operations predictable and correct? ⚠️ Manual
Weaks handling Survives partial deletions and dangling refs? ⚠️ Manual, via registration sets and detach cascade; detached objects remain visible to stack references but in a post-detached, non-working state
Runtime resilience Can DOM ops crash app? ⚠️ Yes, if invariants violated (multiparent) – throws exception
Code expressiveness Concise and maintainable? ⚠️ Verbose; every class needs boilerplate deep copy and detach logic
Ergonomic trade-offs Difficulty of enforcing invariants? ❌ High; language offers no compile-time guarantees

Verdict

JavaScript can host a reasonably safe DOM-like graph if:

  • You manually enforce ownership,
  • Treat “weak” links as opt-in and track them explicitly,
  • Accept that deep copy logic will be verbose,
  • And rely on runtime exceptions rather than compiler errors.

It’s not fully safe, but it’s miles better than a manual memory handling.

These conclusions can be applied to all GC-driven languages, which is why I won’t test Java, Kotlin, C#, Python, Dart, Go, or TypeScript – although I welcome any examples in these languages proving that they have extra features helping with DOM handling.

Next stop – C++

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
laravel-lusophone-–-agora-com-documentacao-oficial

Laravel Lusophone – Agora com Documentação Oficial

Next Post
golf.com:-reverse-routing:-playing-the-old-course-backwards

Golf.com: Reverse Routing: Playing The Old Course Backwards

Related Posts