Skip to content

Repository files navigation

@sandlada/document-context

npm versionGitHub License

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.

Installation

npm install @sandlada/document-context

Usage (Astro Multi-Component Inline Scripts)

In 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.

1. Root Context Definition (Layout.astro)

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>

2. State Mutator Component (ThemeToggle.astro)

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>

3. Reactive Consumer Component (ThemeDisplay.astro)

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>

API Reference

@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

1. Core Module (@sandlada/document-context/core)

createContext(initialState)

Creates a pure immutable blueprint seed. Zero DOM access, zero I/O side effects.

constblueprint=createContext({count: 0,theme: 'light'})

pipe(source, ...operators)

Functional composition pipeline with progressive TypeScript generic type inference overloads.

constappBlueprint=pipe(createContext({count: 0}),withProvider('logger',()=>newConsoleLogger()),withBridge({properties: {count: 'dataset.count'}}))

withProvider(token, factory, options?)

Registers a synchronous service provider on the blueprint.

  • lifecycle: 'scoped' (default) | 'singleton' | 'transient'
  • multi: boolean (default false)
withProvider('auth',(session)=>newAuthService(session),{lifecycle: 'scoped'})

withAsyncProvider(token, asyncFactory, options?)

Registers an asynchronous service provider with lazy execution and Promise coalescing.

withAsyncProvider('user',async(session)=>{constres=awaitfetch('/api/user',{signal: session.abortSignal})returnres.json()})

withHook(event, handler)

Attaches declarative lifecycle hooks ('mount', 'dispose', 'suspend', 'resuscitate', 'adopt').

withHook('mount',(session)=>{console.log('Mounted on',session.target)return()=>console.log('Cleanup on dispose')})

mount(blueprint)(element)

Execution boundary that activates the runtime ISession on a physical HTMLElement.

constsession=mount(appBlueprint)(document.getElementById('app-root')!)

mountAsync(blueprint)(element)

Asynchronous mount boundary awaiting eager storage hydration and async initialization.

constsession=awaitmountAsync(appBlueprint)(document.getElementById('app-root')!)

select(selector)(session)

Curried synchronous state snapshot reader.

constgetCount=select((s: {count: number})=>s.count)constcurrentCount=getCount(session)

update(updater)(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)

subscribe(listener)(session)

Subscribes to reactive state transitions. Returns an unsubscribe teardown function.

constunsubscribe=subscribe((state)=>{console.log('New state:',state)})(session)

dispose(session)

Explicitly tears down the session, executing LIFO cleanups, poisoning the state store, and aborting session.abortSignal.

readErrorStream(session)

Returns an RxJS Observable<DocumentContextError> streaming non-fatal errors (e.g., storage parsing failures, hook exceptions).


2. Bridge Module (@sandlada/document-context/bridge)

withBridge(options)

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}}})

3. Storage Module (@sandlada/document-context/storage)

withStorage(options)

Configures persistent state storage, schema migrations, and cross-tab synchronization.

  • adapter: 'localStorage' | 'sessionStorage' | custom adapter
  • key: Storage string key
  • hydrationStrategy: 'storageFirst' (default) | 'domFirst' | 'blueprintFirst' | 'merge'
  • crossTabSync: BroadcastChannel & Window storage sync (default: true)
  • version: Schema version number
  • migrate: 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}})

4. DOM & W3C Protocol Module (@sandlada/document-context/dom)

inject(token)(target)

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)

injectAsync(token, options?)(target)

Asynchronous dependency injection with Promise Coalescing, DFS circular dependency detection, and multi-caller abort isolation.

constuser=awaitinjectAsync('user',{signal: abortController.signal})(childElement)

injectAll(token, options?)(target) / injectAllAsync(token, options?)(target)

Multi-provider accumulation protocol collecting all matching services up the ancestor hierarchy.

  • direction: 'bottomUp' (default) | 'topDown'
constplugins=injectAll('plugin',{direction: 'topDown'})(childElement)

5. Signals Module (@sandlada/document-context/signals)

toSignal(session, selector)

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())

6. TypeScript ServiceRegistry Type Augmentation

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}}

License

MIT

About

A modern, function-first Inversion of Control (IoC) container and reactive state management library anchored directly to HTMLElement and physical DOM nodes.

Topics

Resources

Code of conduct

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages