npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sandlada/document-context

v0.0.1-20260822.a

Published

A function-first, DOM-anchored IoC container and reactive state management library for modern browsers, Astro, Web Components, and micro-frontends.

Readme

@sandlada/document-context

npm version GitHub 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:

<script is:inline type="module">
    import { createContext, pipe, withBridge, withStorage, mount } from '@sandlada/document-context'

    const themeBlueprint = pipe(
        createContext({ isDark: false }),
        withBridge({
            properties: {
                isDark: 'dataset.themeDark'
            }
        }),
        withStorage({
            adapter: 'localStorage',
            key: 'app-theme',
            hydrationStrategy: 'storageFirst'
        })
    )

    // Mount to document root element
    window.__themeSession = mount(themeBlueprint)(document.documentElement)
</script>

2. State Mutator Component (ThemeToggle.astro)

An independent toggle button component modifies state using the curried update verb:

<script is:inline type="module">
    import { update } from '@sandlada/document-context'

    const btn = document.getElementById('theme-toggle-btn')
    btn?.addEventListener('click', () => {
        const toggle = 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:

<script is:inline type="module">
    import { select, subscribe } from '@sandlada/document-context'

    const statusEl = document.getElementById('theme-status-text')
    const session = window.__themeSession

    // 1. Initial snapshot read
    const isDark = select((s) => s.isDark)(session)
    if (statusEl) statusEl.textContent = isDark ? '🌙 Dark' : '☀️ Light'

    // 2. Reactive subscription
    subscribe((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.

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

pipe(source, ...operators)

Functional composition pipeline with progressive TypeScript generic type inference overloads.

const appBlueprint = pipe(
    createContext({ count: 0 }),
    withProvider('logger', () => new ConsoleLogger()),
    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) => new AuthService(session), { lifecycle: 'scoped' })

withAsyncProvider(token, asyncFactory, options?)

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

withAsyncProvider('user', async (session) => {
    const res = await fetch('/api/user', { signal: session.abortSignal })
    return res.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.

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

mountAsync(blueprint)(element)

Asynchronous mount boundary awaiting eager storage hydration and async initialization.

const session = await mountAsync(appBlueprint)(document.getElementById('app-root')!)

select(selector)(session)

Curried synchronous state snapshot reader.

const getCount = select((s: { count: number }) => s.count)
const currentCount = 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).

const increment = update<{ count: number }>((s) => ({ count: s.count + 1 }))
increment(session)

subscribe(listener)(session)

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

const unsubscribe = 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' }
        return oldData
    }
})

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 Element
const auth = inject('auth')(this)

injectAsync(token, options?)(target)

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

const user = await injectAsync('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'
const plugins = 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'

const countSignal = 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.ts
import type { AuthService, Logger } from './services'

declare module '@sandlada/document-context' {
    interface ServiceRegistry {
        'auth:service': AuthService
        'logger:service': Logger
    }
}

License

MIT