Skip to content

Latest commit

 

History

History
360 lines (294 loc) · 12.3 KB

File metadata and controls

360 lines (294 loc) · 12.3 KB

Architecture Overview

StateLoom is organized as a layered monorepo. Each layer has a clear responsibility boundary, explicit dependency direction, and independent versioning under the @stateloom/* npm scope.

Layer Diagram

graph TB
    subgraph L1["Layer 1 — Reactive Core"]
        Core["@stateloom/core<br/>signal · computed · effect · batch · scope<br/>~1.5 KB gzipped"]
    end

    subgraph L2["Layer 2 — Paradigm Adapters"]
        Store["@stateloom/store<br/>createStore · setState · selectors"]
        Atom["@stateloom/atom<br/>atom · derived · async atoms"]
        Proxy["@stateloom/proxy<br/>observable · snapshot · observe"]
    end

    subgraph L3["Layer 3 — Framework Adapters"]
        React["@stateloom/react"]
        Vue["@stateloom/vue"]
        Solid["@stateloom/solid"]
        Svelte["@stateloom/svelte"]
        Angular["@stateloom/angular"]
    end

    subgraph L4["Layer 4 — Middleware & Ecosystem"]
        Devtools["@stateloom/devtools"]
        Persist["@stateloom/persist"]
        TabSync["@stateloom/tab-sync"]
        History["@stateloom/history"]
        Immer["@stateloom/immer"]
        Telemetry["@stateloom/telemetry"]
        Server["@stateloom/server"]
        Testing["@stateloom/testing"]
    end

    subgraph L5["Layer 5 — Platform Backends"]
        PersistRedis["@stateloom/persist-redis"]
    end

    Core --> Store
    Core --> Atom
    Core --> Proxy

    Store --> React
    Store --> Vue
    Store --> Solid
    Store --> Svelte
    Store --> Angular
    Atom --> React
    Atom --> Vue
    Proxy --> React
    Proxy --> Vue

    Core --> Devtools
    Core --> Persist
    Core --> TabSync
    Core --> History
    Core --> Immer
    Core --> Telemetry
    Core --> Server
    Core --> Testing

    Persist --> PersistRedis
Loading

Dependency Rules

  1. Downward only: Dependencies flow strictly downward. A lower layer never imports from a higher layer.
  2. Core is the only shared dependency: All packages depend on @stateloom/core as a peer dependency.
  3. Framework adapters peer-depend on their framework: @stateloom/react peer-depends on react, @stateloom/vue on vue, etc.
  4. Paradigm packages are independent: @stateloom/store, @stateloom/atom, and @stateloom/proxy do not depend on each other.
  5. Middleware packages are independent: Each middleware package depends only on core. They do not depend on each other or on paradigm packages.
  6. Platform backends depend on their parent: @stateloom/persist-redis depends on @stateloom/persist.

Package Dependency Graph

graph LR
    Core["@stateloom/core"]

    Core --> Store["@stateloom/store"]
    Core --> Atom["@stateloom/atom"]
    Core --> Proxy["@stateloom/proxy"]

    Core --> Devtools["@stateloom/devtools"]
    Core --> Persist["@stateloom/persist"]
    Core --> TabSync["@stateloom/tab-sync"]
    Core --> History["@stateloom/history"]
    Core --> Immer["@stateloom/immer"]
    Core --> Telemetry["@stateloom/telemetry"]
    Core --> Server["@stateloom/server"]
    Core --> Testing["@stateloom/testing"]

    Persist --> PersistRedis["@stateloom/persist-redis"]

    Store --> ReactA["@stateloom/react"]
    Atom --> ReactA
    Proxy --> ReactA

    Store --> VueA["@stateloom/vue"]
    Store --> SolidA["@stateloom/solid"]
    Store --> SvelteA["@stateloom/svelte"]
    Store --> AngularA["@stateloom/angular"]
Loading

Build Order

Turborepo resolves this automatically via "dependsOn": ["^build"]:

Phase Packages Dependencies
1 @stateloom/core None
2 store, atom, proxy core
3 devtools, persist, tab-sync, history, immer, telemetry, server, testing core
4 react, vue, solid, svelte, angular core + paradigms
5 persist-redis persist
6 examples/* all packages

Phases 2 and 3 execute in parallel. Turborepo's caching means only changed packages and their dependents rebuild.

Package Naming Convention

All packages are published under the @stateloom npm organization:

Category Pattern Examples
Core @stateloom/core @stateloom/core
Paradigms @stateloom/<paradigm> store, atom, proxy
Framework adapters @stateloom/<framework> react, vue, solid, svelte, angular
Middleware @stateloom/<feature> devtools, persist, tab-sync, history
Platform backends @stateloom/persist-<platform> persist-redis
Utilities @stateloom/<utility> server, testing, immer, telemetry

Monorepo Layout

stateloom/
├── packages/
│   ├── core/                  # Layer 1
│   ├── store/                 # Layer 2
│   ├── atom/                  # Layer 2
│   ├── proxy/                 # Layer 2
│   ├── react/                 # Layer 3
│   ├── vue/                   # Layer 3
│   ├── solid/                 # Layer 3
│   ├── svelte/                # Layer 3
│   ├── angular/               # Layer 3
│   ├── devtools/              # Layer 4
│   ├── persist/               # Layer 4
│   ├── persist-redis/         # Layer 5
│   ├── tab-sync/              # Layer 4
│   ├── history/               # Layer 4
│   ├── immer/                 # Layer 4
│   ├── telemetry/             # Layer 4
│   ├── server/                # Layer 4
│   └── testing/               # Layer 4
├── examples/
├── docs/
├── scripts/
└── [root config files]

Reactive Data Flow

This sequence diagram shows how a signal write propagates through the system from a user action to a framework re-render:

sequenceDiagram
    participant App as User Action
    participant S as signal.set()
    participant G as Dependency Graph
    participant C as computed (pull)
    participant E as effect (flush)
    participant FA as Framework Adapter
    participant UI as Component Re-render

    App->>S: set(newValue)
    S->>G: startBatch + propagateChange
    G->>G: Mark subscribers DIRTY / MAYBE_DIRTY
    S->>S: endBatch
    Note over S: Flush notifications + effects
    S->>E: effect.run()
    E->>C: computed.get() [pull phase]
    C->>C: Recompute if stale
    C->>E: Return fresh value
    E->>FA: useSyncExternalStore / shallowRef
    FA->>UI: Schedule re-render
Loading

Build Pipeline

Turborepo resolves build order automatically via "dependsOn": ["^build"]. Phases 2 and 3 execute in parallel:

flowchart LR
    subgraph "Phase 1"
        Core["@stateloom/core"]
    end

    subgraph "Phase 2 (parallel)"
        Store["store"]
        Atom["atom"]
        Proxy["proxy"]
    end

    subgraph "Phase 3 (parallel)"
        DT["devtools"]
        PS["persist"]
        TS["tab-sync"]
        HI["history"]
        IM["immer"]
        TM["telemetry"]
        SV["server"]
        TST["testing"]
    end

    subgraph "Phase 4 (parallel)"
        React["react"]
        Vue["vue"]
        Solid["solid"]
        Svelte["svelte"]
        Angular["angular"]
    end

    subgraph "Phase 5"
        PR["persist-redis"]
    end

    Core --> Store & Atom & Proxy
    Core --> DT & PS & TS & HI & IM & TM & SV & TST
    Store & Atom & Proxy --> React & Vue & Solid & Svelte & Angular
    PS --> PR
Loading

Turborepo's caching means only changed packages and their dependents rebuild.

Key Interfaces

The entire architecture rests on a small number of shared interfaces:

Subscribable<T> — The Universal Contract

interface Subscribable<T> {
  get(): T;
  subscribe(callback: (value: T) => void): () => void;
}

Every signal, computed, store, and atom implements this. Framework adapters bridge this single interface to framework-specific reactivity.

Middleware<T> — The Extension Point

interface Middleware<T> {
  name: string;
  init?: (api: MiddlewareAPI<T>) => void;
  onSet?: (api: MiddlewareAPI<T>, next: SetFn<T>, partial: Partial<T>) => void;
  onGet?: (api: MiddlewareAPI<T>, key: keyof T) => void;
  onSubscribe?: (api: MiddlewareAPI<T>, listener: Listener<T>) => Listener<T>;
  onDestroy?: (api: MiddlewareAPI<T>) => void;
}

All cross-cutting concerns (persistence, devtools, tab-sync, history) implement this interface.

Scope — SSR Isolation

interface Scope {
  fork(): Scope;
  get<T>(subscribable: Subscribable<T>): T;
  set<T>(signal: Signal<T>, value: T): void;
  serialize(): Record<string, unknown>;
}

Scopes provide per-request isolation for SSR environments.

Interface Relationships

This class diagram shows how the key interfaces connect across layers:

classDiagram
    class Subscribable~T~ {
        +get() T
        +subscribe(cb) () => void
    }

    class Signal~T~ {
        +get() T
        +set(value) void
        +update(fn) void
        +subscribe(cb) () => void
    }

    class ReadonlySignal~T~ {
        +get() T
        +subscribe(cb) () => void
    }

    class StoreApi~T~ {
        +get() T
        +getState() T
        +setState(partial) void
        +subscribe(listener) () => void
        +destroy() void
    }

    class Middleware~T~ {
        +name string
        +init(api) void
        +onSet(api, next, partial) void
        +onDestroy(api) void
    }

    class Scope {
        +fork() Scope
        +get(sub) T
        +set(signal, value) void
        +serialize() Record
    }

    Signal~T~ --|> Subscribable~T~
    ReadonlySignal~T~ --|> Subscribable~T~
    StoreApi~T~ --|> Subscribable~T~
    Middleware~T~ ..> StoreApi~T~ : intercepts
    Scope ..> Subscribable~T~ : scopes
Loading

When to Use Each Layer

Need Layer Package
Raw reactive primitives Core @stateloom/core
Zustand-like single-object store Paradigm @stateloom/store
Jotai-like bottom-up atoms Paradigm @stateloom/atom
Valtio-like mutable proxy Paradigm @stateloom/proxy
React hooks for stores/atoms/proxies Framework @stateloom/react
Vue composables Framework @stateloom/vue
Persist to localStorage/Redis Middleware @stateloom/persist + backend
Time-travel debugging Middleware @stateloom/devtools
Undo/redo Middleware @stateloom/history
Cross-tab sync Middleware @stateloom/tab-sync
SSR per-request isolation Core + Adapter @stateloom/core + framework adapter

Cross-References