A modern, function-first Inversion of Control (IoC) container and reactive state management library anchored directly to HTMLElement and physical DOM nodes.
- Physical DOM as Scope Hierarchy: DOM nesting maps directly to IoC container scope hierarchies without virtual container trees.
- Zero-Leak Lifecycle Co-location: Automatic garbage collection and subscription cleanup tied to physical DOM attachment.
- Atomic Bidirectional Bridge: Synchronize JS reactive state and DOM properties (
dataset.*,style.*,aria-*,value) with loop guards. - Framework-Agnostic Lingua Franca: Built on native W3C Context Protocol (
ContextRequestEvent) and Custom Events, seamlessly bridging Astro, Islands, Web Components, and mixed frameworks. - Pure FP Two-Phase Architecture: Pure lazy blueprint declaration in Phase 1 + physical DOM mount execution in Phase 2.
npm install @sandlada/document-contextIn Astro or Islands architecture, independent <script is:inline> blocks bundled across different components can share state and services seamlessly through the physical DOM tree without sharing JavaScript heap memory references.
Define the pure blueprint and mount it to the root document element with persistent storage and DOM property synchronization:
<scriptis:inlinetype="module">import{createContext,pipe,withBridge,withStorage,mount}from'@sandlada/document-context'constthemeBlueprint=pipe(createContext({isDark: false}),withBridge({properties: {isDark: 'dataset.themeDark'}}),withStorage({adapter: 'localStorage',key: 'app-theme',hydrationStrategy: 'storageFirst'}))// Mount to document root elementwindow.__themeSession=mount(themeBlueprint)(document.documentElement)</script>An independent toggle button component modifies state using the curried update verb:
<scriptis:inlinetype="module">import{update}from'@sandlada/document-context'constbtn=document.getElementById('theme-toggle-btn')btn?.addEventListener('click',()=>{consttoggle=update((s)=>({isDark: !s.isDark}))toggle(window.__themeSession)})</script>Another independent consumer component subscribes to reactive state changes or reads snapshots via select / subscribe:
<scriptis:inlinetype="module">import{select,subscribe}from'@sandlada/document-context'conststatusEl=document.getElementById('theme-status-text')constsession=window.__themeSession// 1. Initial snapshot readconstisDark=select((s)=>s.isDark)(session)if(statusEl)statusEl.textContent=isDark ? '🌙 Dark' : '☀️ Light'// 2. Reactive subscriptionsubscribe((state)=>{if(statusEl){statusEl.textContent=state.isDark ? '🌙 Dark' : '☀️ Light'}})(session)</script>@sandlada/document-context provides modular subpath exports for clean tree-shaking:
@sandlada/document-context/core@sandlada/document-context/bridge@sandlada/document-context/storage@sandlada/document-context/dom@sandlada/document-context/signals
Creates a pure immutable blueprint seed. Zero DOM access, zero I/O side effects.
constblueprint=createContext({count: 0,theme: 'light'})Functional composition pipeline with progressive TypeScript generic type inference overloads.
constappBlueprint=pipe(createContext({count: 0}),withProvider('logger',()=>newConsoleLogger()),withBridge({properties: {count: 'dataset.count'}}))Registers a synchronous service provider on the blueprint.
lifecycle:'scoped'(default) |'singleton'|'transient'multi:boolean(defaultfalse)
withProvider('auth',(session)=>newAuthService(session),{lifecycle: 'scoped'})Registers an asynchronous service provider with lazy execution and Promise coalescing.
withAsyncProvider('user',async(session)=>{constres=awaitfetch('/api/user',{signal: session.abortSignal})returnres.json()})Attaches declarative lifecycle hooks ('mount', 'dispose', 'suspend', 'resuscitate', 'adopt').
withHook('mount',(session)=>{console.log('Mounted on',session.target)return()=>console.log('Cleanup on dispose')})Execution boundary that activates the runtime ISession on a physical HTMLElement.
constsession=mount(appBlueprint)(document.getElementById('app-root')!)Asynchronous mount boundary awaiting eager storage hydration and async initialization.
constsession=awaitmountAsync(appBlueprint)(document.getElementById('app-root')!)Curried synchronous state snapshot reader.
constgetCount=select((s: {count: number})=>s.count)constcurrentCount=getCount(session)Curried state transition verb. Accepts a partial state object or an updater function (prevState) => nextState. Returns boolean (false if session is disposed).
constincrement=update<{count: number}>((s)=>({count: s.count+1}))increment(session)Subscribes to reactive state transitions. Returns an unsubscribe teardown function.
constunsubscribe=subscribe((state)=>{console.log('New state:',state)})(session)Explicitly tears down the session, executing LIFO cleanups, poisoning the state store, and aborting session.abortSignal.
Returns an RxJS Observable<DocumentContextError> streaming non-fatal errors (e.g., storage parsing failures, hook exceptions).
Registers bidirectional property synchronization between JavaScript state and DOM element properties.
properties: Record of dot-paths (dataset.*,style.*,style.--*,aria-*,value,checked,hidden,elementInternals.value).events: Array of DOM events triggering DOM-to-state sync (default:['input', 'change']).batch: Microtask write batching (default:true).
withBridge({properties: {count: 'dataset.count',theme: 'dataset.theme',inputValue: {target: 'value',parse: Number}}})Configures persistent state storage, schema migrations, and cross-tab synchronization.
adapter:'localStorage'|'sessionStorage'| custom adapterkey: Storage string keyhydrationStrategy:'storageFirst'(default) |'domFirst'|'blueprintFirst'|'merge'crossTabSync: BroadcastChannel & Window storage sync (default:true)version: Schema version numbermigrate: Migration transition function(oldData, oldVersion) => newData
withStorage({adapter: 'localStorage',key: 'user-settings',hydrationStrategy: 'storageFirst',version: 2,migrate: (oldData: any,oldVersion)=>{if(oldVersion===1)return{ ...oldData,newField: 'default'}returnoldData}})Curried dependency injection verb. Dispatches standard ContextRequestEvent bubbling up the physical DOM tree across Shadow DOM boundaries.
// In a child Web Component or Elementconstauth=inject('auth')(this)Asynchronous dependency injection with Promise Coalescing, DFS circular dependency detection, and multi-caller abort isolation.
constuser=awaitinjectAsync('user',{signal: abortController.signal})(childElement)Multi-provider accumulation protocol collecting all matching services up the ancestor hierarchy.
direction:'bottomUp'(default) |'topDown'
constplugins=injectAll('plugin',{direction: 'topDown'})(childElement)Adapts a reactive state slice into a TC39 Signal-compatible object with a .get() method.
import{toSignal}from'@sandlada/document-context/signals'constcountSignal=toSignal(session,(s)=>s.count)console.log(countSignal.get())String tokens support 100% type safety and auto-completion across bundle boundaries via TypeScript declaration merging:
// types/context.d.tsimporttype{AuthService,Logger}from'./services'declare module '@sandlada/document-context'{interfaceServiceRegistry{'auth:service': AuthService'logger:service': Logger}}MIT