Layer Scoping

February 13, 2026 · View on GitHub

This document defines the exact responsibility boundary for each layer and package in StateLoom. Every package has a single, well-defined scope. If a feature does not fit within a package's scope, it belongs in a different package or a new one.

Layer Boundary Enforcement

Dependencies flow strictly downward. This diagram shows the allowed and prohibited import directions:

flowchart TB
    L1["Layer 1: Core"]
    L2["Layer 2: Paradigms"]
    L3["Layer 3: Adapters"]
    L4["Layer 4: Middleware"]
    L5["Layer 5: Backends"]

    L1 -->|"allowed"| L2
    L1 -->|"allowed"| L4
    L2 -->|"allowed"| L3
    L4 -->|"allowed"| L5

    L2 -.-x|"PROHIBITED"| L1
    L3 -.-x|"PROHIBITED"| L2
    L3 -.-x|"PROHIBITED"| L1
    L4 -.-x|"PROHIBITED"| L1
    L5 -.-x|"PROHIBITED"| L4

    style L1 fill:#3b82f6,color:#fff
    style L2 fill:#8b5cf6,color:#fff
    style L3 fill:#10b981,color:#fff
    style L4 fill:#f59e0b,color:#fff
    style L5 fill:#ef4444,color:#fff

::: tip Arrows show dependency direction (who imports whom). Dotted red lines show prohibited imports. Higher layers never import from lower layers. :::

Layer 1 — Reactive Core (@stateloom/core)

Scope

The reactive kernel. Provides the primitive building blocks that every other package builds on.

Owns

  • signal(value) — mutable reactive value container
  • computed(fn) — lazy, memoized derived signal
  • effect(fn) — side-effect with automatic dependency tracking
  • batch(fn) — coalesce multiple writes into a single notification
  • createScope() / runInScope() / serializeScope() — SSR isolation
  • Dependency graph internals (doubly-linked node lists)
  • Equality checking (Object.is default, custom comparators)
  • Subscribable<T> interface definition

Does Not Own

  • Any paradigm-specific API (no createStore, no atom, no observable)
  • Any framework-specific code (no React hooks, no Vue composables)
  • Any middleware or plugin system
  • Any persistence, devtools, or ecosystem features
  • Any platform-specific APIs (localStorage, BroadcastChannel, etc.)

Design Constraints

  • Zero platform-specific APIs — works in any JavaScript runtime
  • Target: ~1.5 KB gzipped
  • Uses only: closures, WeakMap, Set, Object.is, Promise, queueMicrotask

Layer 2 — Paradigm Adapters

Each paradigm adapter translates a familiar API pattern into operations on the reactive core.

@stateloom/store

Scope

Store-based state management, familiar to Zustand and Redux Toolkit users.

Owns

  • createStore(creator, options) — create a store with state and actions
  • set(partial) / set(updater) — state mutation via shallow merge
  • Selector support for derived reads
  • Middleware pipeline composition
  • StoreApi<T>, StateCreator<T> type definitions

Does Not Own

  • The reactive primitives themselves (delegates to core)
  • Framework bindings (no useStore — that's in @stateloom/react)
  • Specific middleware implementations (those are in dedicated packages)

@stateloom/atom

Scope

Bottom-up atomic state composition, familiar to Jotai users.

Owns

  • atom(initialValue) — create a base atom config
  • Derived atoms (read-only, write, async)
  • AtomScopeWeakMap-based value container for atom instances
  • Async atom resolution with Promise support
  • Atom<T>, WritableAtom<T>, ReadonlyAtom<T> type definitions

Does Not Own

  • Suspense integration (that's in framework adapters)
  • Framework hooks (useAtom is in @stateloom/react)
  • Store-based features

@stateloom/proxy

Scope

Proxy-based mutable state with transparent tracking, familiar to Valtio and MobX users.

Owns

  • observable(obj) — create a deeply-proxied mutable state object
  • snapshot(proxy) — create an immutable, structurally-shared snapshot
  • observe(fn) — auto-tracking side-effect (like MobX autorun)
  • ref(value) — opt a value out of proxying (DOM elements, class instances)
  • Two-layer proxy architecture (write-tracking source + read-tracking snapshot)

Does Not Own

  • Framework bindings (useSnapshot is in @stateloom/react)
  • proxy-compare library (it's a dependency, not owned code)
  • Store or atom features

Layer 3 — Framework Adapters

Each adapter is a thin bridge (50–200 lines) connecting the Subscribable<T> contract to framework-specific reactivity.

@stateloom/react

Owns

  • useSignal(signal)useSyncExternalStore bridge for raw signals
  • useStore(store, selector) — store integration with selector memoization
  • useAtom(atom) / useAtomValue(atom) / useSetAtom(atom) — atom hooks
  • useSnapshot(proxy) — proxy paradigm integration
  • ScopeProvider / ScopeContext — SSR scope context
  • getServerSnapshot implementations for hydration safety

@stateloom/vue

Owns

  • useSignal(signal)shallowRef bridge with onScopeDispose cleanup
  • useStore(store, selector) — store composable
  • Vue plugin for scope injection (app.use(stateloomPlugin))
  • Reactive ref wrappers

@stateloom/solid

Owns

  • useSignal(signal)createSignal bridge
  • useStore(store, selector) — store integration
  • Solid-specific scope management

@stateloom/svelte

Owns

  • Svelte store contract compliance (signals natively satisfy { subscribe })
  • Minimal adapter for $store syntax support
  • Scope management via Svelte context

@stateloom/angular

Owns

  • StateloomService — injectable service
  • toObservable(signal) — RxJS Observable wrapper
  • toSignal(observable) — Angular Signal bridge
  • Module/standalone component integration

Shared Rules for All Adapters

  • Peer-depend on @stateloom/core and the framework
  • Auto-cleanup subscriptions on component unmount
  • Provide SSR-safe snapshot implementations
  • No business logic — pure bridging code

Layer 4 — Middleware & Ecosystem

Each middleware package implements the Middleware<T> interface and adds a specific cross-cutting concern.

@stateloom/devtools

  • Redux DevTools Extension bridge (standard protocol)
  • Custom inspector API for building tools
  • Time-travel debugging via action log replay
  • Action name inference from function names

@stateloom/persist

  • persist(options) middleware for storage persistence
  • partialize, merge, version, migrate configuration
  • Built-in storage adapters: localStorage, sessionStorage, cookieStorage, indexedDB, memory
  • StorageAdapter interface for custom backends

@stateloom/tab-sync

  • broadcast(options) middleware for cross-tab sync
  • BroadcastChannel-based with loop-prevention
  • Field-level filtering and conflict resolution
  • Graceful degradation (no-op when BroadcastChannel unavailable)

@stateloom/history

  • Undo/redo support with two strategies
  • Snapshot-based (simple, Immer structural sharing)
  • Command-based (memory-efficient, explicit commands)
  • canUndo / canRedo as reactive signals

@stateloom/immer

  • Enables mutable update syntax within set() via Immer integration
  • Wraps the set function to use produce()

@stateloom/telemetry

  • Analytics hooks for state change tracking
  • onStateChange, onError callbacks with metadata
  • Duration measurement for state transitions

@stateloom/server

  • createServerScope(options) — TTL-based, LRU-evicting server scope
  • Per-request scope forking
  • Memory-bounded scope management for long-running servers

@stateloom/testing

  • createTestScope() — isolated scope per test with auto-reset
  • mockStore(overrides) — store mocking for component tests
  • TestScopeProvider — test harness for framework adapter testing

Layer 5 — Platform Backends

Storage backends for specific platforms. Each depends on @stateloom/persist.

@stateloom/persist-redis

  • HTTP + TCP Redis adapters
  • Edge-compatible via HTTP protocol (Upstash)
  • TTL support

Import Direction Rules

This diagram illustrates what each layer is allowed to import, with specific examples:

flowchart LR
    subgraph "What Core CAN use"
        C_OK["closures, WeakMap, Set,<br/>Object.is, Promise,<br/>queueMicrotask"]
    end

    subgraph "What Core CANNOT use"
        C_NO["window, localStorage,<br/>BroadcastChannel, document,<br/>React, Vue, fetch"]
    end

    subgraph "What Paradigms CAN import"
        P_OK["@stateloom/core:<br/>signal, computed, effect,<br/>batch, createScope"]
    end

    subgraph "What Paradigms CANNOT import"
        P_NO["@stateloom/react,<br/>@stateloom/persist,<br/>@stateloom/store (from atom)"]
    end

Boundary Violations to Watch For

ViolationWhy It's WrongWhere It Belongs
Core importing localStorageCore must be runtime-agnostic@stateloom/persist
Store importing React hooksParadigms are framework-agnostic@stateloom/react
React adapter implementing middleware logicAdapters are thin bridges onlyDedicated middleware package
Persist middleware hardcoding RedisPersist provides the interface@stateloom/persist-redis
Any package importing from a higher layerDependencies flow downward onlyRestructure the dependency
Atom importing from StoreParadigms are independent siblingsShare via core primitives
Middleware importing framework hooksMiddleware is framework-agnosticFramework adapter handles integration

Package Selection Guide

Use this decision tree to determine which packages a consumer needs:

flowchart TB
    Start["What state pattern<br/>do you prefer?"] --> Q1{Single object<br/>with actions?}
    Start --> Q2{Independent atoms<br/>composed together?}
    Start --> Q3{Mutable proxy<br/>with direct assignment?}
    Start --> Q4{Raw signals<br/>only?}

    Q1 -->|Yes| Store["@stateloom/store"]
    Q2 -->|Yes| Atom["@stateloom/atom"]
    Q3 -->|Yes| Proxy["@stateloom/proxy"]
    Q4 -->|Yes| Core["@stateloom/core"]

    Store --> FW{Which framework?}
    Atom --> FW
    Proxy --> FW
    Core --> FW

    FW -->|React| React["+ @stateloom/react"]
    FW -->|Vue| Vue["+ @stateloom/vue"]
    FW -->|Solid| Solid["+ @stateloom/solid"]
    FW -->|Svelte| Svelte["+ @stateloom/svelte"]
    FW -->|Angular| Angular["+ @stateloom/angular"]
    FW -->|None| Vanilla["No adapter needed"]

    React --> MW{Need persistence<br/>or devtools?}
    Vue --> MW
    Solid --> MW
    Svelte --> MW
    Angular --> MW
    Vanilla --> MW

    MW -->|Persistence| Persist["+ @stateloom/persist"]
    MW -->|DevTools| DT["+ @stateloom/devtools"]
    MW -->|Both| Both["+ persist + devtools"]
    MW -->|No| Done["Done"]

Cross-References