Watervein DOM package


Keywords
framework, ui-framework, watervein, waterveinjs, typescript, javascript, dag, ecs, nes, engine, fast, fine-grained, frameworks, js, library, lightweight, no-build, no-compile, reactive, reactivity, ts, ui, web
Licenses
MIT/Apache-2.0
Install
npm install @watervein/dom@0.1.54-dev.2

Documentation

Watervein Logo

npm version License: MIT

Watervein

No component. No tree. A radical re-imagining of UI systems.

Stop Building Trees. Just Open the Valves.

Traditional frameworks force you to wrap your logic into lifecycle-heavy components. Watervein eliminates them. Look at how we express a simple decoupled reactive boundary without a single component scaffold:

// 1. Define raw reactive state anywhere—completely decoupled from UI
const count = createState(0);

// 2. Build the layout network using raw element expressions
const app = div({}, [
  button({ onclick: () => write(count, read(count) + 1) }, "Increment"),
  span({}, () => `Current value: ${read(count)}`) // Atomic edge hook
]);

// There are no re-rendering component lifecycles. 
// Clicking the button evaluates the DAG and updates ONLY the text inside the <span>.

Benchmark

Watervein is currently being integrated into the js-framework-benchmark suite. No published numbers exist yet — treat any performance claims in this README as architectural intent, not measured results, until benchmark data is published here.

Packages

Watervein is managed as a monorepo, split into core reactivity and DOM rendering layers:

  • @watervein/core — The core reactive engine powered by a Node Edge System (NES) and DAGs. Handles states, batches, and side effects.
  • @watervein/dom-core — The DOM core. Provides basic DOM manipulation functions.
  • @watervein/dom — The DOM rendering bindings. Provides graph-driven template utilities like For, Show, and element builders.

Note on tag coverage: @watervein/dom currently exposes a generic element() factory plus a growing set of tag shorthands (div, span, button, etc.). If you rely on a shorthand not yet exported (e.g. tr, td, a, input), fall back to element("tag", props, children). Missing a tag you need? Opening a PR to add tag shorthands to @watervein/dom is an excellent 'good first issue' for new contributors!


Getting Started

Installation

Install both the core reactive system and the DOM renderer via your preferred package manager:

pnpm add @watervein/core @watervein/dom-core @watervein/dom
# or npm install @watervein/core @watervein/dom-core @watervein/dom

Basic Setup

Create an index.html with a target element, and initialize your first Watervein graph in main.ts:

import { createState, read, write, UISystem } from '@watervein/core';
import { mount, span, button } from '@watervein/dom';

const count = createState(0);

const app = span({}, [
  button({ onclick: () => { write(count, read(count) + 1); UISystem.flush(); } }, "Click me"),
  span({}, () => ` Count: ${read(count)}`)
]);

mount(document.getElementById('app')!, app);

Mental Model: From Trees to Rivers

In Watervein, you don't build "components" that hold their own isolated state. Instead, your application is a network of data channels (Waterveins).

  1. States are Springs: Dynamic data sources.
  2. Computations are Rivers: Downstream functions reacting to the springs.
  3. DOM Elements are Ocean Mouths: Terminal endpoints where data finally shapes the UI.

No virtual trees to diff. No component functions to re-execute. Just targeted data flow.

Why Watervein?

  • Node Edge System (NES) & DAGs: Watervein abandons the traditional concepts of UI components and tree structures, replacing them with a model inspired by ECS (Entity Component System) and Directed Acyclic Graphs (DAGs).
  • Modern Developer Experience: While the underlying architecture is unconventional, it keeps a coding style familiar to users of React, SolidJS, and similar frameworks.
  • Extensible Architecture: You can register custom node types to extend the core's scheduling behavior.
  • Headless Core: On its own, core isn't even aware of the DOM. Only by adding packages like dom-core or dom can you actually render to a page. Because core has no DOM dependency, it's designed to eventually support other rendering backends, such as Canvas.

Conceptual Comparison: React vs. Vue vs. Svelte vs. SolidJS vs. Watervein vs. Vanilla JS

The table below describes each framework's intended architecture, not benchmarked outcomes. Complexity classes (e.g. "$O(N)$") describe the algorithmic design goal of Watervein's reconciliation loop, not a measured result.

Feature React Vue 3 Svelte 5 SolidJS Watervein Vanilla JS
Primary Architecture Component Tree (VNode) Component Tree + Reactive Proxies Compiler-driven Runes Component Scopes + Signals Decoupled Node Edge System (NES) / DAG Procedural / Imperative Execution
Component Execution Repeatedly (on every re-render) Triggered via dependency proxy track Run-once setup with dynamic updates Exactly once (during setup) No component abstraction exists N/A (No component abstraction)
State Mutation Scope Component/Subtree abstraction Block-tree inside component Signal-like Runes closure scope Signal closures Global topology network Manual mutation allocation
DOM Tracking Strategy Virtual DOM reconciliation Hybrid VNode + Static Hoisting Direct compiled DOM hydration/patch Fine-grained reactive closures Granular text/attribute edge hooks Manual pinpoint DOM targeting
Component Wrapper Requires Fiber / Virtual Parent VNode Fragment rendering Template Fragment Anchors Real DOM Fragment tracking display: contents sub-wrappers Native layout container blocks
Lifecycle Model Hook-driven (useEffect timeline) Hook/Option driven (onMounted) Rune effects / lifecycle helpers Component-mount scopes (onMount) Entity-bound, not lifecycle-bound Manual event/handle registration
Cleanup Mechanism Hook return functions Automated unmount tracking $effect tracking collection Explicit onCleanup registry Automatic via entity destruction (destroyEntity/destroyEntities) Manual memory reference disposal
Data Flow Direction Top-down unidirectional tree Unidirectional props / events Unidirectional signals model Reactive proxy propagation Topological DAG downstream Custom/arbitrary manual pipelines
Batching Mechanics Schedule-driven concurrent queues Microtask scheduler queue Microtask scheduler loop Microtask signal batch loops Synchronous topological flush() Synchronous mutation/immediate
Reordering Design Goal ($O$) $O(N)$ VNode diffing algorithm $O(N)$ patch-flag keyed diffing $O(N)$ block-list reconciliation $O(N)$ dynamic fragment index tracking $O(N)$ backwards-sweep reconciliation (design target) Custom optimized procedural loops

Componentization Patterns (UI Reuse Without Component Trees)

Since Watervein relies on flat entity structures instead of a hierarchy of stateful component definitions, UI parts are created using plain, pure JavaScript factory functions.

Instead of embedding runtime lifecycle magic into custom markup tags, you pass parameters and sub-graph configurations directly into functions that return raw elements:

import { createState, read, write, withEntity, createEntity } from '@watervein/core';
import { button } from '@watervein/dom';

// Pattern: Stateful Element Factory Function
export function createCounterButton(initialCount: number = 0) {
    const localEntityId = createEntity();

    // Lock internal reactive node state inside the allocated entity
    return withEntity(localEntityId, () => {
        const count = createState(initialCount);

        return button({
            class: "custom-btn",
            onclick: () => write(count, read(count) + 1)
        }, [
            () => `Clicks: ${read(count)}`
        ]);
    });
}

// Usage inside layout tree
const layout = element("div", {}, [
    createCounterButton(0),
    createCounterButton(10) // Independently allocated entity/memory
]);

The Golden Rules of Watervein Development:

  1. Functions, Not Custom Tags: Anything that looks like a component is just a plain function returning an HTMLElement.
  2. Encapsulate State via withEntity: If your reusable UI element holds internal state, wrap its signal creation inside withEntity(createEntity(), () => { ... }) so that destroying the entity cleans up all associated nodes.

Usage

Here's a look at how you define reactive states and mount a list using Watervein's DAG-based system:

import { createState, read, write, batch, UISystem } from '@watervein/core';
import { For, mount, element } from '@watervein/dom';

// 1. Define flat reactive states (NES / Data Layer)
const rowMap   = createState(new Map());
const rowOrder = createState([]);
const selected = createState(0);

// 2. Build the UI structure using graphs, not components
const list = For(
    rowOrder,
    (id) => id,
    (getId) => element("tr", {
        class: () => read(selected) === getId() ? "danger" : "",
    }, [
        element("td", { class: "col-md-1" }, () => `${getId()}`),
        element("td", { class: "col-md-4" }, [
            element("a", {
                onclick: () => write(selected, getId()),
            }, () => read(rowMap).get(getId())?.label ?? "")
        ])
    ])
);

// 3. Mount directly to the DOM
const tbody = document.querySelector("table.test-data")!;
mount(tbody, list);

// 4. Update state and flush the UI system
document.getElementById("run")!.addEventListener("click", () => {
    batch(() => {
        write(rowMap, new Map([[1, { id: 1, label: "Pretty Red Table" }]]));
        write(rowOrder, [1]);
    });
    UISystem.flush(); // Commit batched graph changes to the DOM
});

Architecture

ac_r

Mermeid Source Code
graph LR
    subgraph Traditional [Traditional Component Tree]
        direction TB
        App["&lt;App /&gt;"]
        Sidebar["&lt;Sidebar&gt;"]
        Main["&lt;Main&gt;"]
        Menu["&lt;Menu&gt;"]
        Card["&lt;Card&gt;"]
        State["[State]"]
        Update["[Update]"]
        Rerender(["Re-render entire tree"])

        App --> Sidebar
        App --> Main
        Sidebar --> Menu
        Main --> Card
        Menu --> State
        Card --> Update
        State --> Rerender
    end

    subgraph Watervein [Watervein Flattened DAG]
        direction TB
        StateA["StateA"]
        ComputeX["ComputeX"]
        EffectY["EffectY"]
        Mutation(["Pinpoint mutation via NES"])

        StateA --> ComputeX
        StateA --> EffectY
        ComputeX --> Mutation
        EffectY --> Mutation
    end

    Traditional -- Flatten --> Watervein

    style Rerender fill:#f9f,stroke:#333,stroke-width:2px,color:#000;
    style Mutation fill:#bbf,stroke:#333,stroke-width:2px,color:#000;
Loading

Traditional frameworks model your application as a Tree of Components, requiring virtual DOM diffing or template analysis to isolate mutations.

Watervein instead flattens UI logic into a decoupled Data Layer (NES Engine) and maps operations onto a Rendering Layer using DOM fragments.

1. Node Edge System (NES) & Graph Mechanics

At the engine level (@watervein/core), state (createState), derivations (createCompute), and side effects (createEffect) exist as plain nodes in a unified global Directed Acyclic Graph (DAG).

  • Entity Isolation: States and computations can be bound to flat entity IDs (createEntity). There are no lexical component scopes.
  • Downstream-Only Propagation: When a state changes via write(), the engine walks the graph edges downstream and marks dependent nodes as dirty. Only terminal nodes directly bound to a text block, property, or conditional block are scheduled for patching.

ac_1

Mermeid Source Code
graph LR
    State["[State Node]"]
    Compute["[Compute Node]"]
    DOM["[DOM Effect Node]"]
    
    State -- edge --> Compute
    Compute -- edge --> DOM

    Queue["[Dirty Queue]"]
    Native["Native Element"]

    State -- write triggers --> Queue
    DOM -- direct patch --> Native

    Queue ~~~ Native

    style Queue fill:#fff3cd,stroke:#ffc107,stroke-width:1px,color:#000
    style Native fill:#e2e3e5,stroke:#6c757d,stroke-width:1px,color:#000
Loading

2. Flushing Pipeline

Watervein decouples state changes from the browser's paint cycle. Multiple write() calls can be grouped inside batch(), which defers scheduling until the batch completes.

ac_2

Mermeid Source Code
graph LR
    Writes["[ Multiple Writes ]"]
    Recalc["[ NES Graph Recalculation ]"]
    Flush["[ UISystem.flush() ]"]
    Commit["[ Synchronous DOM Commit ]"]

    Writes --> Recalc
    Recalc --> Flush
    Flush --> Commit

    style Writes fill:#f8d7da,stroke:#dc3545,stroke-width:1px,color:#000
    style Recalc fill:#cce5ff,stroke:#004085,stroke-width:1px,color:#000
    style Flush fill:#e2e3e5,stroke:#383d41,stroke-width:1px,color:#000
    style Commit fill:#d4edda,stroke:#155724,stroke-width:1px,color:#000
Loading

UISystem.flush() performs a synchronous topological sweep across dirtied nodes, ordered by graph depth.

Current limitation: flush() does not currently isolate exceptions between nodes. If a compute or effect node throws during a flush, the exception propagates out of flush() and any remaining dirty nodes in that pass are left unprocessed. There is no errorBoundary API yet — see Roadmap. If your app logic can throw, wrap the relevant compute/effect callback in your own try/catch for now.

3. Layered DOM Decoupling

To keep the core engine free of DOM assumptions while still offering an ergonomic authoring experience, the repository splits the rendering pipeline into three packages:

ac_3

Mermeid Source Code
graph TD
    DSL["<strong>@watervein/dom</strong> (High-Level Developer DSL)<br>• Ergonomic tag factories and prop shorthands<br>• Reactive class/style prop parsing"]
    
    Core["<strong>@watervein/dom-core</strong> (Reactive Mutation Infrastructure)<br>• `display: contents` wrapper elements for `For` / `Show`<br>• Backwards-sweep DOM reconciliation for reordering"]
    
    Engine["<strong>@watervein/core</strong> (Pure Reactive Engine)<br>• Headless dependency-graph tracking<br>• Entity allocation and bulk destruction"]

    DSL -- "Lowering Properties" --> Core
    Core -- "Direct Invocations" --> Engine

    style DSL fill:#e8f4fd,stroke:#2b6cb0,stroke-width:1px,color:#000
    style Core fill:#edf2f7,stroke:#4a5568,stroke-width:1px,color:#000
    style Engine fill:#f7fafc,stroke:#718096,stroke-width:1px,color:#000
Loading
  • @watervein/dom: Provides declarative prop shorthands (e.g. { class: { active: someNode } }) on top of standard tag functions.
  • @watervein/dom-core: Implements the low-level element binding logic (element, Show, For, mount) that @watervein/dom wraps.

    💡 Why display: contents?: When rendering dynamic structures like lists (For) or conditionals (Show), Watervein wraps them in a real HTML element styled with display: contents. This tells the browser to ignore the wrapper for layout purposes, passing its children directly to the parent layout context, while still giving Watervein a stable DOM anchor to manage insertions and removals against.

  • @watervein/core: Contains no references to HTMLElement or document. It only computes dependency graphs, which is why it's designed to eventually support non-DOM rendering backends (e.g. Canvas).

Advanced State Management (The ECS Pattern)

In tree-based frameworks, passing data between distant components often requires prop drilling, Context APIs, or external state stores.

In Watervein, state nodes exist independently of any view layer, so you can define shared state as plain modules and reference it anywhere in your graph:

// stores/serverStore.ts
import { createState, createCompute, read } from '@watervein/core';

export const globalCpuLoad = createState(0);

export const isSystemCritical = createCompute(() => {
    return read(globalCpuLoad) > 85;
});
// app.ts
import { element } from '@watervein/dom';
import { read } from '@watervein/core';
import { isSystemCritical, globalCpuLoad } from './stores/serverStore.js';

const sidebar = element("div", {
    class: () => read(isSystemCritical) ? "bg-red" : "bg-gray"
}, []);

const mainPanel = element("span", {}, () => `Load: ${read(globalCpuLoad)}%`);

Memory Management Strategy

  • Persistent state: Define nodes at the module level for data that should live for the entire application session (user sessions, themes, etc.).
  • Ephemeral state: Use createEntity() and withEntity() inside factory functions for local UI state (modals, dropdowns, list rows) so that destroying the entity (DestructionSystem.destroyEntity / destroyEntities) also prunes its associated graph nodes and edges.

Strict TypeScript Integration

Watervein is written in strict TypeScript. Because it doesn't rely on a JSX compilation step, tag properties are typed directly against standard HTMLElement interfaces:

import { button } from '@watervein/dom';

const safeButton = button({
    id: "submit-action",
    className: "btn-primary",

    // Type error: 'onclick' expects a function, not a string
    onclick: "alert('clicked')" // ❌ TS Error
}, ["Submit"]);

Reactive Prop Polymorphism

Most properties accept either a static value or a reactive hook (a function or a Node):

const reactiveInput = input({
    value: () => read(usernameState),
    disabled: isFormSubmitting // static boolean, or a Node<boolean>
});

Testing Strategy

Because Watervein separates the pure reactive engine (@watervein/core) from the DOM environment, you can test application logic without a browser runtime, then layer DOM-level tests on top.

1. Headless Graph Testing (Zero-DOM)

Verify state mutations, batching, and side effects against the raw engine:

import { describe, it, expect } from 'vitest';
import { createState, createCompute, createEffect, read, write, UISystem } from '@watervein/core';

describe('Counter Graph Logic', () => {
    it('propagates state changes through computed nodes', () => {
        const count = createState(0);
        const doubled = createCompute(() => read(count) * 2);

        write(count, 5);
        UISystem.flush();

        expect(read(doubled)).toBe(10);
    });
});

2. Granular Patch Testing (DOM Layer)

To test actual browser interactions, use happy-dom or jsdom to verify pinpoint text/attribute updates:

// @vitest-environment happy-dom
import { describe, it, expect } from 'vitest';
import { mount } from '@watervein/dom';
import { createCounterButton } from './counter.js';

describe('Counter DOM Mutations', () => {
    it('should patch individual text nodes seamlessly', () => {
        const container = document.createElement('div');
        const btn = createCounterButton(5);
        mount(container, btn);

        const targetButton = container.querySelector('button')!;
        expect(targetButton.textContent).toBe('Clicks: 5');

        targetButton.dispatchEvent(new MouseEvent('click'));

        expect(targetButton.textContent).toBe('Clicks: 6');
    });
});

Contributing

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. We're looking for developers to help push this architecture forward. Whether it's optimizing DAG propagation, implementing new custom node types, or fixing bugs, contributions are welcome.

Local Development Setup

  1. Fork and clone the repository.
  2. Install dependencies and build the packages:
pnpm install
pnpm build
pnpm test

Pull Request Guidelines

  • Please open an issue to discuss significant architectural changes before submitting a PR.
  • Ensure all tests pass before submitting.

License

This project is licensed under either of:

at your option.

Future Roadmap & Ecosystem Vision

Watervein's core (@watervein/core) is headless and decoupled from any rendering runtime. The DOM rendering layer (@watervein/dom) is the first concrete backend built on top of it.

Planned/under-consideration work includes:

  • @watervein/canvas: A 2D/WebGL rendering backend powered by the same NES graph engine.
  • @watervein/router: A reactive, graph-integrated client-side router that treats route updates as node transitions.
  • @watervein/compiler: An optional build-time plugin (Vite / Rollup) to pre-parse static object properties into direct JavaScript assignments, reducing runtime factory-call overhead.
  • Server-side rendering / hydration: Streaming string compilation mapped onto the existing DAG dependency resolution.
  • Extensions: We'll make it possible to create extensions.

None of the above are implemented yet. This section describes direction, not current capability — please don't rely on any roadmap item until it has a corresponding package and tests.

Implemented

  • Scope: Addition of Scope Functionality.
  • Error boundaries: An errorBoundary API for @watervein/dom that isolates exceptions thrown inside a sub-graph during flush(), rendering a fallback UI instead of letting the exception propagate and stall the rest of the flush pass.