npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

reactive-graph-engine

v1.0.0

Published

A reactive graph engine with dependency tracking, computed nodes, middleware, profiling, and persistence

Readme

Reactive Engine — @/lib/graph-engine/

Overview

Custom reactive computation engine untuk low-code IDE (Bandara DEO Sorong).
Engine ini mengelola state management berbasis signal → computed → async → query graph
dengan dependency tracking otomatis, circuit breaker, dan React integration.

Rating Review: ⭐ 4/5 (Februari 2026)


Architecture

┌─────────────────────────────────────────────────────────────┐
│                      Engine Core                             │
├─────────────────────────────────────────────────────────────┤
│  ┌──────────┐  ┌──────────┐  ┌───────────┐  ┌───────────┐  │
│  │  Signal   │  │ Computed │  │   Async   │  │   Query   │  │
│  │  (source) │→ │  (derived│→ │  (effect) │  │ (resource)│  │
│  └──────────┘  └──────────┘  └───────────┘  └───────────┘  │
│       │              │              │              │          │
│       └──────────────┴──────────────┴──────────────┘          │
│                          DepGraph                             │
├─────────────────────────────────────────────────────────────┤
│  MemoryManager │ Profiler │ ErrorRecovery │ CacheManager     │
│  PersistenceLayer │ ParallelExecutor │ TestingTools          │
└─────────────────────────────────────────────────────────────┘

Cara Pakai (React)

1. Define Nodes — nested()

// Client-side (no parser, lightweight):
import { nested } from "@/lib/graph-engine/nestedLite";

const nodeDefinition = nested({
  sidebar: {
    show:      { type: "signal", initial: true },
    activeTab: { type: "signal", initial: "pages" },
  },
  searchQuery: { type: "signal", initial: "" },
  filteredData: {
    type: "computed",
    expr: '{{ val("data").filter(x => x.name.includes(val("searchQuery"))) }}',
  },
});
// Flatten result: "sidebar.show", "sidebar.activeTab", "searchQuery", "filteredData"

2. Create Engine — useReactiveEngine()

import { useReactiveEngine, ReactiveEngineProvider } from "@/lib/graph-engine/useReactiveEngine";

function Layout({ children }) {
  const engine = useReactiveEngine(nodeDefinition, { helpers });
  if (!engine) return <Loading />;
  
  return (
    <ReactiveEngineProvider engine={engine}>
      {children}
    </ReactiveEngineProvider>
  );
}

3. Read/Write — useEngine(), useEngineValue(), engine.set()

import { useEngine, useEngineValue } from "@/lib/graph-engine/useReactiveEngine";
import { batch } from "@/lib/graph-engine/engineScheduler";

function Sidebar() {
  const engine = useEngine();
  const show = useEngineValue<boolean>(engine, "sidebar.show");
  const activeTab = useEngineValue<string>(engine, "sidebar.activeTab");

  const handleTabChange = (tab: string) => {
    batch(() => {
      engine.set("sidebar.show", true);
      engine.set("sidebar.activeTab", tab);
    });
  };

  if (!show) return null;
  return <div>Active: {activeTab}</div>;
}

API Reference

| API | Import | Deskripsi | |-----|--------|-----------| | nested() | @/lib/graph-engine/nested | Flatten nested node definitions ke dot-path | | useReactiveEngine() | @/lib/graph-engine/useReactiveEngine | Hook: create engine + registry + lifecycle | | ReactiveEngineProvider | @/lib/graph-engine/useReactiveEngine | Context provider | | useEngine() | @/lib/graph-engine/useReactiveEngine | Hook: get engine dari context (throws jika null) | | useEngineOptional() | @/lib/graph-engine/useReactiveEngine | Hook: get engine (safe, return null) | | useEngineValue<T>() | @/lib/graph-engine/useReactiveEngine | Hook: subscribe value via useSyncExternalStore | | useEngineState() | @/lib/graph-engine/useReactiveEngine | Hook: subscribe state (idle/pending/ready/error) | | useEngineNode<T>() | @/lib/graph-engine/useReactiveEngine | Hook: subscribe value + state sekaligus | | engine.val(path) | - | Baca nilai node (synchronous) | | engine.set(path, value) | - | Update signal value | | engine.state(path) | - | Baca state node | | engine.run(path) | - | Trigger run pada async/query node | | engine.getNode(path) | - | Ambil node instance | | batch(fn) | @/lib/graph-engine/engineScheduler | Group multiple set() ke satu flush |

⚠️ TIDAK ADA engine.get() — gunakan engine.val(path)


Node Types

Signal — Source of truth

{ type: "signal", initial: 0 }

Bisa di-set dari luar via engine.set("path", value).

Computed — Derived value

{ type: "computed", expr: '{{ val("price") * val("quantity") }}' }

Otomatis re-evaluate saat dependency berubah. Synchronous.

Async — Async computation

{ type: "async", expr: '{{ await fetch("/api/data") }}', auto: true, initialRun: true }

Untuk side-effect atau async operation. Punya state lifecycle (idle → pending → ready/error).

Query — Resource-based data fetching

{ 
  type: "query", 
  resource: "trigger_action_backend",
  params: '{{ ({ id: 123, data: { search: val("searchQuery") } }) }}',
  autoGet: true, 
  initialRun: true 
}

Delegate ke helper resources[resourceName](params, { signal }). Otomatis re-fetch saat params berubah.


useReactiveEngine vs createEngineAsync — Kapan Pakai Yang Mana

useReactiveEngine()GUNAKAN INI (untuk hampir semua kasus)

const engine = useReactiveEngine(nodeDefinition, { helpers });

Fitur built-in:

  • ✅ Engine Registry dengan reference counting
  • ✅ React Strict Mode double-mount handling
  • ✅ HMR signature-based rebuild
  • ✅ Automatic disposal on unmount
  • ✅ Synchronous — no loading state untuk engine creation

Cocok untuk: < 500 nodes, atau kapan saja engine creation < 100ms.

createEngineAsync()HANYA untuk skala besar (1000+ formula nodes)

const { engine, stats } = await createEngineAsync(appJson, helpers, {
  onProgress: (phase, current, total) => updateProgressBar(...),
  maxWorkers: 4,
  chunkSize: 500,
});

Fitur tambahan:

  • Worker Pool (parallel expression compilation)
  • Batch Processing (chunk 500 nodes)
  • Pre-compilation + AST caching
  • Progress callbacks untuk UI feedback
  • Memory optimization

Cocok untuk: 1000+ nodes dengan formula kompleks (spreadsheet-like).

⚠️ TIDAK punya React integration — kamu harus tulis sendiri engine lifecycle di useEffect.

Benchmark Reference

| Skenario | useReactiveEngine | createEngineAsync | |----------|------------------|-------------------| | 50 nodes (editor UI) | ✅ < 5ms | ❌ Overkill | | 200 nodes (dashboard) | ✅ < 20ms | ❌ Overkill | | 500 nodes (complex page) | ✅ < 50ms | 🟡 Opsional | | 1,000+ formula cells | 🟡 ~300ms+ | ✅ 8-10x faster | | 10,000 formula cells | ❌ ~41s freeze | ✅ ~3-5s + progress |

Cost per Node Type

| Node Type | Compile Cost | Keterangan | |-----------|-------------|------------| | Signal | Zero | Tidak ada expression, langsung initial value | | Query | Ringan | Hanya compile params expression | | Async | Medium | Compile code string sebagai function body | | Computed | Mahal | Compile + dependency analysis per expression |


Low-Code IDE: Preview Engine Node Count

Di preview (esbuild-wasm), buildBinding.ts menghasilkan engine definition dari Odoo DB:

Odoo page_components[] → per component:
  ├── x_studio_state[]  → signal nodes + async nodes
  └── x_studio_query[]  → query nodes
  
buildBinding() → nested({ ComponentA: {...}, ComponentB: {...} })
  → "ComponentA.stateName", "ComponentA.queryName", ...

Estimasi node count per page:

| Kompleksitas Page | Components | Nodes/Component | Total Nodes | Engine Method | |-------------------|-----------|-----------------|-------------|---------------| | Simple (form) | 5-10 | 3-5 | 15-50 | useReactiveEngine ✅ | | Medium (dashboard) | 10-20 | 5-8 | 50-160 | useReactiveEngine ✅ | | Complex (POS-like) | 20-30 | 5-10 | 100-300 | useReactiveEngine ✅ | | Heavy (spreadsheet) | 50+ | 10+ | 500-1000+ | Consider createEngineAsync 🟡 |

Catatan: buildBinding.ts saat ini hanya generate signal, async, dan query
BUKAN computed (yang paling mahal). Jadi bahkan di 500 nodes, mayoritas adalah signal
(zero compile cost) + query (satu compile per params expression).


File Structure

Core Files

| File | Lines | Deskripsi | |------|-------|-----------| | reactiveEngine.ts | ~4,300 | Engine class, MemoryManager, Profiler, Persistence, TestingTools | | engineNodes.ts | ~1,425 | SignalNode, ComputedNode, AsyncNode, QueryNode | | engineDepGraph.ts | ~1,078 | DepGraph dependency graph (topological sort, cycle detection) | | engineExpr.ts | ~608 | Expression compiler (compileExpr, sandbox) | | nested.ts | ~440 | nested() flatten + expression transformer (acorn parser, server-side only) | | nestedLite.ts | ~95 | Lightweight nested() — flatten only, NO parser (client-side) | | useReactiveEngine.tsx | ~440 | React hooks, Provider, Engine Registry | | engineTypes.ts | ~361 | TypeScript type definitions | | engineScheduler.ts | ~100 | batch(), microtask scheduler |

Enterprise/Optimization Files

| File | Lines | Deskripsi | |------|-------|-----------| | engineOptimizedBuilder.ts | ~804 | createEngineAsync, Worker Pool, batch compile | | engineHMR.ts | ~750 | Hot Module Replacement untuk engine nodes | | engineEnterpriseCompiler.ts | - | LRU cache + Web Worker expression compiler | | engineWorker.ts | - | Worker pool management | | engineRecovery.ts | - | Error recovery, circuit breaker |

Utils

| File | Deskripsi | |------|-----------| | utils/engineDynamicOps.ts | Dynamic node add/remove/rename | | utils/excelModel.ts | Excel-like cell reference helpers | | utils/batchOperations.ts | Batch node operations | | utils/astParser.ts | AST-based expression analysis | | utils/formulaIntelligence.ts | Formula autocomplete/intelligence |


Catatan Penting — Review Findings (Februari 2026)

✅ Kekuatan

  1. API Design (5/5)val(), set(), batch(), nested() sangat ergonomis
  2. React Integration (5/5)useSyncExternalStore zero-overhead, no tearing
  3. Feature Completeness (5/5) — Signal/Computed/Async/Query + DevTools + HMR + Profiler
  4. Error Handling (4/5) — Circuit breaker, graceful cycle handling, node-level isolation

🚀 Optimasi — Parser Split (B + A1)

Masalah: @babel/parser (511KB) di-import oleh nested.ts dan terbawa ke client bundle lewat 6 file node-engine.ts, padahal transformExpression() TIDAK dibutuhkan di client (semua sudah pakai val() eksplisit).

Solusi (2 langkah):

  1. Opsi B: Ganti @babel/parseracorn (ESTree-compatible, 236KB) — net saving 275KB di server
  2. Opsi A1: Buat nestedLite.ts — versi nested() tanpa parser, hanya flatten() logic (~95 baris)

Hasil:

  • Client bundle: -511KB (zero parser, pakai nestedLite.ts)
  • Server bundle: -275KB (acorn 236KB vs Babel 511KB, pakai nested.ts)
  • 6 client files → import dari nestedLite
  • 3 server files tetap pakai nested.ts: buildBinding.ts (×2), excelModel.ts

| File | Import | Parser | |------|--------|--------| | Client node-engine.ts (×6) | nestedLite | ❌ None | | Server buildBinding.ts (×2) | nested | ✅ acorn | | Server excelModel.ts | nested | ✅ acorn |

⚠️ Yang Perlu Diperhatikan

1. Fitur yang Belum Diimplementasi

// PersistenceLayer._serialize — compression flag ada tapi TIDAK jalan
private _serialize(state: any) {
    const raw = JSON.stringify({ v: 1, ts: now(), state });
    if (!this.compression) return raw;
    return raw;  // ← compression true atau false, hasilnya SAMA
}

Status: Flag compression ada, tapi kedua branch return raw. Jangan enable tanpa implementasi.

2. Dead Code — ParallelExecutor

// evaluateNode() return tanpa melakukan apa-apa untuk semua node type
private async evaluateNode(nodeId: string) {
    if ((node as any).kind === "computed") return;  // no-op
    if ((node as any).kind === "async") return;     // no-op
    if ((node as any).kind === "query") return;     // no-op
}

Status: ParallelExecutor ada tapi evaluateNode() sudah jadi no-op. Execution sebenarnya dilakukan oleh scheduler. Class ini bisa di-remove atau di-refactor.

3. Fitur Enterprise yang Tidak Dipakai di App

Fitur-fitur berikut ada di engine tapi 0 usage di codebase app:

| Fitur | ~Baris | Dipakai? | Catatan | |-------|--------|----------|---------| | MemoryManager (GC, histogram) | ~150 | ❌ | Untuk skenario ribuan node dengan data besar | | PersistenceLayer (versioning, migration) | ~80 | ❌ | enableAutoSave() tidak pernah dipanggil | | TestingTools (deterministic, snapshot) | ~65 | ❌ | Untuk unit testing engine sendiri | | createStateSyncPlugin (BroadcastChannel) | ~70 | ❌ | Sync state antar browser tab | | createEngineAsync (worker offload) | ~80 | ❌ | Untuk 1000+ formula nodes |

Ini BUKAN bug — fitur ini berguna untuk scale nanti. Tapi menambah ~500+ baris di core file.

4. Rekomendasi Refactor (Non-Urgent)

  • Extract TestingTools, createStateSyncPlugin, createEngineAsync ke graph-engine/extras/
  • Fix atau hapus compression dead branch di PersistenceLayer
  • Pertimbangkan remove ParallelExecutor yang sudah no-op
  • Split reactiveEngine.ts (4,300 lines) — pisahkan MemoryManager, Profiler, Persistence ke file terpisah

Rules — JANGAN DILANGGAR

  1. JANGAN gunakan engine.get() — method ini TIDAK ADA. Gunakan engine.val(path)
  2. JANGAN simpan server data di engine signals — gunakan React Query
  3. JANGAN pakai Zustand di editor module — gunakan Reactive Engine untuk UI state
  4. JANGAN pakai useState untuk data dari API — gunakan React Query
  5. JANGAN ubah urutan plugin di bundler.ts — order MATTERS
  6. SELALU gunakan batch() untuk multiple engine.set() calls
  7. SELALU gunakan useEngineValue() untuk subscribe — BUKAN polling
  8. SELALU handle engine === null state (loading) di component
  9. SELALU import nestedLite (bukan nested) di client-side node-engine.ts
  10. ⚠️ Import nested (dengan acorn parser) HANYA di server-side code yang butuh transformExpression()