Imported from makerprism/solid-ml (
AGENTS.md). Install upstream withnpx skills add makerprism/solid-ml. Copyright stays with the author.
Agent Guidelines for solid-ml-server
Hard Constraints
NEVER use opam commands or access .opam or _opam folders, except inside examples/ssr_api_app/Dockerfile when installing Melange for the Docker build.
This project builds with /home/sabine/.opam/landing-pages-build/bin/dune (dune 3.20.2) which is configured to work with the system OCaml installation. Do not use opam commands or reference any packages in .opam/ or _opam/ directories, except for the Dockerfile exception above. Always use the dune binary specified in the Makefile's DUNE variable.
Git workflow: Always use rebase (not merge) when integrating remote changes before pushing.
Branch/PR workflow: Work in a feature branch (not main) and open a pull request for each session's feature/fix. Do not stack PRs: keep exactly one active PR per session and wait for it to be merged (or explicitly closed) before starting another PR branch.
Examples policy: When adding a user-facing feature, update all relevant examples in the same session/PR to demonstrate the new capability. Do not defer example updates to a later stacked PR.
Product Stage Policy
This project is still in an early stage. Prioritize DX and simplicity over API stability.
- Prefer simple, obvious, ergonomic solutions over complex compatibility layers.
- It is acceptable to change APIs when it meaningfully improves usability.
- Do not block improvements for backward compatibility concerns at this stage.
- When you introduce a breaking change, update docs/tests in the same change so migration is clear.
Project Overview
solid-ml-server is an OCaml framework for building reactive web applications with SSR, inspired by SolidJS.
Repository: github.com/makerprism/solid-ml
License: MIT
Documentation
- Architecture:
docs/A-01-architecture.md- Full design document with API specs, SSR flow, and development phases
Package Structure
| Package | Purpose | Status |
|---|---|---|
solid-ml-internal |
Shared reactive core (functor-based, DO NOT use directly) | Complete |
solid-ml-server |
Server-side reactive framework (OCaml 5 with DLS) | Complete |
solid-ml-ssr |
Server-side rendering to HTML strings | Complete |
solid-ml-browser |
Browser-side reactive framework (Melange) | Complete |
solid-ml-router |
SSR-aware routing with data loaders | Complete |
Build Commands
# Build all packages
dune build
# Run tests
dune runtest
# Clean build artifacts
dune clean
# Build browser examples (requires Node.js for esbuild bundling)
make browser-examples
# Run browser tests
make browser-tests
Architecture
Shared Core (solid-ml-internal)
The project uses a functor-based architecture to share reactive algorithms between server and browser:
solid-ml-internal/
├── types.ml # Internal types (Obj.t for type erasure, phantom types for safety)
├── backend.ml # Backend.S module type + Backend.Global implementation
├── reactive_functor.ml # Make(Backend) functor with all reactive algorithms
└── solid_ml_internal.ml
Key insight: The only platform difference is how the current runtime is stored:
- Server: Domain-local storage (thread-safe isolation per domain)
- Browser: Global ref (safe in single-threaded JS)
Server (solid-ml-server)
(* Defines DLS backend and instantiates functor *)
module Backend_DLS : Internal.Backend.S = struct
let runtime_key = Domain.DLS.new_key (fun () -> None)
let get_runtime () = Domain.DLS.get runtime_key
let set_runtime rt = Domain.DLS.set runtime_key rt
let handle_error exn _ = raise exn
end
module R = Internal.Reactive_functor.Make(Backend_DLS)
Browser (solid-ml-browser)
(* Defines global ref backend with console.error *)
module Backend_Browser : Internal.Backend.S = struct
let current_runtime = ref None
let get_runtime () = !current_runtime
let set_runtime rt = current_runtime := rt
let handle_error exn context =
console_error ("solid-ml-server: Error in " ^ context ^ ": " ^ Printexc.to_string exn)
end
module R = Internal.Reactive_functor.Make(Backend_Browser)
Type Safety
The internal types use Obj.t for type erasure (needed for heterogeneous collections), but expose type-safe APIs via phantom types:
(* Internal: untyped *)
type signal_state = { mutable value: Obj.t; ... }
(* Public: typed via phantom type parameter *)
type 'a signal = signal_state (* 'a is phantom *)
let create_typed_signal (type a) ?equals (initial : a) : a signal =
{ value = Obj.repr initial; ... }
let read_typed_signal (type a) (s : a signal) : a =
Obj.obj s.value
Current Development Phase
Phase 1: Reactive Core (complete)
- Signal.create, get, set, update, peek
- Dependency tracking via execution context
- Effect.create with auto-tracking
- Effect.create_with_cleanup
- Effect.untrack
- Memo.create, create_with_equals (eager evaluation like SolidJS)
- Batch.run with Signal integration
- Owner.create_root, run_with_owner, on_cleanup, dispose
- Context.create, provide, use
- Comprehensive test suite (31 reactive + 23 HTML + 36 SolidJS compat = 90 tests)
- Counter example in native OCaml
- SolidJS compatibility test suite
- Shared functor-based core for server and browser
Phase 2: Server Rendering (complete)
- HTML element functions (div, p, input, etc.)
- render_to_string function
- Hydration markers for reactive text
- Attribute escaping and boolean attributes
- Comprehensive test suite (23 tests)
Phase 3: Client Runtime (complete)
- Set up Melange build configuration (dune 3.16+, melange 0.1)
- DOM API bindings (
lib/solid-ml-browser/dom.ml) - HTML element functions mirroring solid-ml-ssr
- Reactive DOM primitives
- Browser reactive core using shared functor
- Event handling system
- Render function (client-side from scratch)
- Hydrate function with text node adoption via hydration markers
- Browser counter example (
examples/browser_counter/) - Browser reactive tests (14 tests via Node.js)
Phase 4: Router (complete)
- Route matching (static, params, wildcards)
- Route pattern parsing
- URL parsing (path, query, hash)
- Query string parsing
- Router context and state management
- Router.navigate for programmatic navigation
- Link component for navigation
- NavLink with active class support
- Outlet component for rendering matched routes
- RouterProvider for initializing router context
- Comprehensive test suite (91 tests)
- History API integration (browser)
- Client-side navigation without reload (browser)
- Scroll restoration
- Loading/error states (Resource module)
Phase 5: Suspense & Error Handling (complete)
- Suspense.boundary for async loading states
- ErrorBoundary.make for catching errors with reset
- Resource.read_suspense with ~default parameter
- Counter-based Suspense (matches SolidJS)
- Nested Suspense boundaries
- Multiple concurrent resources
- Unique_id.create for SSR hydration
- Comprehensive test suite (13 tests)
Test Summary: 208 tests total (31 reactive + 23 HTML + 36 SolidJS compat + 91 router + 14 browser + 13 suspense)
Code Style
- Use OCaml standard library where possible
- Document public APIs with odoc comments
- Write tests for all new functionality
- Keep implementations simple - optimize later
Key Files
| File | Purpose |
|---|---|
lib/solid-ml-internal/types.ml |
Internal types with Obj.t + phantom type wrappers |
lib/solid-ml-internal/backend.ml |
Backend.S module type + Global implementation |
lib/solid-ml-internal/reactive_functor.ml |
Main functor with all reactive algorithms |
lib/solid-ml/reactive.ml |
Server instantiation with DLS backend |
lib/solid-ml/signal.ml |
Type-safe Signal API |
lib/solid-ml/effect.ml |
Effect API |
lib/solid-ml/memo.ml |
Memo API |
lib/solid-ml/batch.ml |
Batched updates |
lib/solid-ml/owner.ml |
Ownership and disposal tracking |
lib/solid-ml/context.ml |
Component context |
lib/solid-ml-ssr/html.ml |
HTML element functions for SSR |
lib/solid-ml-ssr/render.ml |
Render components to HTML strings |
lib/solid-ml-browser/reactive_core.ml |
Browser instantiation with global ref backend |
lib/solid-ml-browser/dom.ml |
Melange FFI bindings for browser DOM |
lib/solid-ml-browser/html.ml |
DOM element functions |
lib/solid-ml-browser/reactive.ml |
Reactive DOM bindings (text, attr, etc.) |
lib/solid-ml-browser/hydration.ml |
Hydration state and marker parsing |
lib/solid-ml-browser/event.ml |
Event handling utilities |
lib/solid-ml-browser/render.ml |
Client-side render and hydrate functions |
lib/solid-ml-router/route.ml |
Route pattern matching and params |
lib/solid-ml-router/router.ml |
Router state and navigation |
lib/solid-ml-router/components.ml |
Link, NavLink, Outlet components |
lib/solid-ml/suspense.ml |
Suspense boundaries for async loading |
lib/solid-ml/error_boundary.ml |
Error boundaries with reset |
lib/solid-ml/unique_id.ml |
Generate unique IDs for hydration |
lib/solid-ml-browser/suspense.ml |
Browser Suspense implementation |
lib/solid-ml-browser/error_boundary.ml |
Browser ErrorBoundary |
test/test_reactive.ml |
Test suite for reactive primitives (31 tests) |
test/test_html.ml |
Test suite for HTML rendering (23 tests) |
test/test_solidjs_compat.ml |
SolidJS compatibility tests (36 tests) |
test/test_router.ml |
Router tests (91 tests) |
test/test_suspense.ml |
Suspense and ErrorBoundary tests (13 tests) |
test_browser/test_reactive.ml |
Browser reactive core tests (14 tests) |
examples/counter/counter.ml |
Counter example demonstrating reactive features |
examples/todo/todo.ml |
Todo list with SSR rendering |
examples/router/router.ml |
Router example with routes, params, NavLink |
examples/browser_counter/ |
Browser counter + todo (Melange) |
examples/parallel/parallel.ml |
OCaml 5 domain parallelism demo |
examples/ssr_server/server.ml |
Dream server SSR demo (requires dream) |
Design Principles
- Fine-grained reactivity - No virtual DOM, signals update only what depends on them
- Automatic tracking - Reading a signal inside an effect/memo auto-subscribes
- Server/client isomorphism - Same component code works on server (HTML) and client (DOM)
- Type safety - Full OCaml type checking for UI code
- Shared core - One implementation of reactive algorithms, platform-specific backends
- MLX syntax - JSX-like templates via the mlx package
Known Limitations
- Browser: Multiple independent apps on same page share global state
- Server: Each
Runtime.runis fully isolated
Important: Signals should not be shared across runtimes or domains.
Differences from SolidJS
solid-ml-server aims to match SolidJS semantics closely. Here are the known differences:
Implemented Features Matching SolidJS
| Feature | solid-ml-server | SolidJS | Notes |
|---|---|---|---|
| Signal equality | Physical (!=) |
Reference (===) |
Matches - both skip updates for same reference |
| Memo equality | Structural (=) |
Reference (===) |
solid-ml-server uses structural by default (customizable) |
| createSelector | create_selector |
createSelector |
Returns (k -> bool) with auto-cleanup |
| Effect.on | Effect.on |
on() |
Explicit deps, untracked body, ~defer option |
| catchError | Owner.catch_error |
catchError |
Sync error handling (no setter reset) |
| Batch | Batch.run |
batch |
Groups updates, defers effects |
| Context | Context.create/provide/use |
createContext/useContext |
Owner-tree based lookup |
| Suspense | Suspense.boundary |
<Suspense> |
Counter-based, shows fallback while loading |
| ErrorBoundary | ErrorBoundary.make |
<ErrorBoundary> |
Catches errors with reset capability |
| createUniqueId | Unique_id.create |
createUniqueId |
SSR-safe unique ID generation |
Features NOT Implemented (Intentionally Omitted)
| Feature | Reason |
|---|---|
createResource |
Use Resource module in router for async data |
createDeferred |
No microtask scheduling in OCaml |
createReaction |
Use Effect.on for explicit tracking |
createRenderEffect |
Effects run synchronously already |
| Transitions API | Requires concurrent rendering not available in OCaml |
startTransition |
No concurrent mode |
useTransition |
No concurrent mode |
SuspenseList |
Experimental in SolidJS, doesn't fully support SSR |
children helper |
Use direct children access |
lazy |
Use OCaml's native lazy |
| Event delegation | Implemented globally (matches SolidJS behavior) |
Semantic Differences
-
Effect scheduling: solid-ml-server effects run synchronously during
run_updates. SolidJS sometimes defers to microtasks. This rarely matters in practice. -
Error boundaries:
Owner.catch_errorcatches synchronously thrown exceptions. SolidJS'scatchErrorprovides a setter to reset; solid-ml-server returns the fallback value directly since OCaml exceptions are sync. -
Context ID generation: Not thread-safe (uses
ref, notAtomic). Create contexts at module init time before spawning domains. -
No JSX compiler: solid-ml-server requires manual DOM/HTML construction or use of MLX syntax. There's no Babel-like transform.
-
Memo evaluation: solid-ml-server memos are eager (computed immediately on creation). SolidJS memos are also eager but may defer in some cases.
Browser-specific Notes
create_selectorauto-cleans up when the owning computation is disposed (viaOwner.on_cleanup)- No manual
unsubscribeneeded (unlike earlier versions) - Multiple apps on same page share the global runtime ref