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 containercomputed(fn)— lazy, memoized derived signaleffect(fn)— side-effect with automatic dependency trackingbatch(fn)— coalesce multiple writes into a single notificationcreateScope()/runInScope()/serializeScope()— SSR isolation- Dependency graph internals (doubly-linked node lists)
- Equality checking (
Object.isdefault, custom comparators) Subscribable<T>interface definition
Does Not Own
- Any paradigm-specific API (no
createStore, noatom, noobservable) - 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 actionsset(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)
AtomScope—WeakMap-based value container for atom instances- Async atom resolution with
Promisesupport Atom<T>,WritableAtom<T>,ReadonlyAtom<T>type definitions
Does Not Own
- Suspense integration (that's in framework adapters)
- Framework hooks (
useAtomis 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 objectsnapshot(proxy)— create an immutable, structurally-shared snapshotobserve(fn)— auto-tracking side-effect (like MobXautorun)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 (
useSnapshotis in@stateloom/react) proxy-comparelibrary (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)—useSyncExternalStorebridge for raw signalsuseStore(store, selector)— store integration with selector memoizationuseAtom(atom)/useAtomValue(atom)/useSetAtom(atom)— atom hooksuseSnapshot(proxy)— proxy paradigm integrationScopeProvider/ScopeContext— SSR scope contextgetServerSnapshotimplementations for hydration safety
@stateloom/vue
Owns
useSignal(signal)—shallowRefbridge withonScopeDisposecleanupuseStore(store, selector)— store composable- Vue plugin for scope injection (
app.use(stateloomPlugin)) - Reactive ref wrappers
@stateloom/solid
Owns
useSignal(signal)—createSignalbridgeuseStore(store, selector)— store integration- Solid-specific scope management
@stateloom/svelte
Owns
- Svelte store contract compliance (signals natively satisfy
{ subscribe }) - Minimal adapter for
$storesyntax support - Scope management via Svelte context
@stateloom/angular
Owns
StateloomService— injectable servicetoObservable(signal)— RxJSObservablewrappertoSignal(observable)— Angular Signal bridge- Module/standalone component integration
Shared Rules for All Adapters
- Peer-depend on
@stateloom/coreand 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 persistencepartialize,merge,version,migrateconfiguration- Built-in storage adapters:
localStorage,sessionStorage,cookieStorage,indexedDB,memory StorageAdapterinterface for custom backends
@stateloom/tab-sync
broadcast(options)middleware for cross-tab syncBroadcastChannel-based with loop-prevention- Field-level filtering and conflict resolution
- Graceful degradation (no-op when
BroadcastChannelunavailable)
@stateloom/history
- Undo/redo support with two strategies
- Snapshot-based (simple, Immer structural sharing)
- Command-based (memory-efficient, explicit commands)
canUndo/canRedoas reactive signals
@stateloom/immer
- Enables mutable update syntax within
set()via Immer integration - Wraps the
setfunction to useproduce()
@stateloom/telemetry
- Analytics hooks for state change tracking
onStateChange,onErrorcallbacks 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-resetmockStore(overrides)— store mocking for component testsTestScopeProvider— 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
| Violation | Why It's Wrong | Where It Belongs |
|---|---|---|
Core importing localStorage | Core must be runtime-agnostic | @stateloom/persist |
| Store importing React hooks | Paradigms are framework-agnostic | @stateloom/react |
| React adapter implementing middleware logic | Adapters are thin bridges only | Dedicated middleware package |
| Persist middleware hardcoding Redis | Persist provides the interface | @stateloom/persist-redis |
| Any package importing from a higher layer | Dependencies flow downward only | Restructure the dependency |
| Atom importing from Store | Paradigms are independent siblings | Share via core primitives |
| Middleware importing framework hooks | Middleware is framework-agnostic | Framework 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
- Architecture Overview — full dependency graph and build order
- Design Philosophy — rationale for the layered architecture
- Core Design — what the core layer owns internally
- Middleware Overview — structural typing pattern that keeps middleware independent