@statedelta/chain
v0.1.1
Published
Tail resolver for StateDelta - navigates inheritance chains via partial headers
Maintainers
Readme
@statedelta/chain
Tail resolver for StateDelta - navigates inheritance chains via partial headers.
Overview
@statedelta/chain is a focused, high-performance library for resolving inheritance chains (tails) in document hierarchies. It navigates through extends references using partial header fetches (~4KB), enabling maximum parallelism for orchestrators.
manifest.json ──extends──▶ step-1.json ──extends──▶ genesis.json
│ │ │
▼ ▼ ▼
HEAD ... ROOTFeatures
- Fast - Uses partial headers (~4KB) instead of full files
- Agnostic - Doesn't know your DSL, receives
extractRefsfunction - Event-Driven - Emits events for each node (enables parallel processing)
- Type-Safe - Full TypeScript with strict mode
- Cycle Detection - Detects circular dependencies with visualization
- Cancellable - Supports
AbortSignalfor timeout/cancellation - Disposable - One instance per resolution (stateless)
- Relative Paths - Passes
referrercontext for relative path resolution
Installation
pnpm add @statedelta/chainPeer Dependency:
pnpm add @statedelta/gatewayQuick Start
import { createChain, defaultExtractRefs } from '@statedelta/chain';
import { createGateway, FileProvider } from '@statedelta/gateway';
// 1. Create Gateway
const gateway = createGateway({
providers: [new FileProvider()],
});
// 2. Create Chain
const chain = createChain({
gateway,
extractRefs: defaultExtractRefs, // extracts `extends` field
});
// 3. Listen to events (optional but powerful)
chain.on('node:loaded', (node) => {
console.log(`Loaded: ${node.header.name} (depth: ${node.depth})`);
});
// 4. Resolve
const result = await chain.resolve('./manifest.json');
console.log(`Tail: ${result.depth} nodes`);
console.log(`Root: ${result.root.header.name}`);
console.log(`Head: ${result.head.header.name}`);API
createChain(options)
Creates a new Chain instance.
const chain = createChain({
// Required
gateway: GatewayLike, // Gateway with loadHeader()
extractRefs: ExtractRefs, // Function to extract parent ref
// Optional
maxDepth?: number, // Default: 100
validateHeader?: HeaderValidator,
});chain.resolve(source, options?)
Resolves the inheritance chain.
const result = await chain.resolve('./manifest.json', {
signal?: AbortSignal, // For cancellation
});Returns: Promise<ChainResult>
interface ChainResult {
tail: ChainNode[]; // [root, ..., head]
root: ChainNode; // First node (no parent)
head: ChainNode; // Last node (entry point)
depth: number; // Number of nodes
source: string; // Original source
duration: number; // Resolution time (ms)
}
interface ChainNode {
source: string; // URL/path
depth: number; // 0 = head, increases toward root
isRoot: boolean; // true if no parent
header: CollectedHeader;
}Events
chain.on('node:loaded', (node: ChainNode) => {
// Called for each node as it's discovered
// Perfect for starting parallel work!
});
chain.on('tail:complete', (result: ChainResult) => {
// Called when resolution completes
});
chain.on('error', (error: Error) => {
// Called on any error
});defaultExtractRefs
Default extractor for StateDelta Document Protocol:
import { defaultExtractRefs } from '@statedelta/chain';
// Extracts parent from `extends` field
const refs = defaultExtractRefs(header);
// { parent: header.extends }createMockGateway(headers)
Creates a mock gateway for testing:
import { createMockGateway } from '@statedelta/chain';
const gateway = createMockGateway({
'./a.json': { name: 'a', extends: './b.json' },
'./b.json': { name: 'b' },
});Usage Examples
With Orchestrator (Maximum Parallelism)
const chain = createChain({ gateway, extractRefs });
// React to each node immediately
chain.on('node:loaded', (node) => {
// Start full content download in parallel
orchestrator.hydrate(node.source);
// Start deps download in parallel
if (node.header.deps) {
orchestrator.loadDeps(node.header.deps);
}
});
const result = await chain.resolve('./manifest.json');
// By now, orchestrator may have already downloaded everything!Multiple Chains in Parallel
// Gateway is shared - cache benefits all chains
const [result1, result2, result3] = await Promise.all([
createChain({ gateway, extractRefs }).resolve('./a.json'),
createChain({ gateway, extractRefs }).resolve('./b.json'),
createChain({ gateway, extractRefs }).resolve('./c.json'),
]);Custom Validation
const chain = createChain({
gateway,
extractRefs,
validateHeader: (header, ctx) => {
// Reject old versions
if (header.version && !semver.satisfies(header.version, '^2.0.0')) {
return { valid: false, error: `Version ${header.version} not supported` };
}
// Limit depth for this use case
if (ctx.depth > 50) {
return { valid: false, error: 'Chain too deep' };
}
return { valid: true };
},
});With Timeout
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // 5s timeout
try {
const result = await chain.resolve('./manifest.json', {
signal: controller.signal,
});
} catch (error) {
if (isAbortError(error)) {
console.log('Resolution timed out');
}
}Custom ExtractRefs
// For a different DSL that uses `parent` instead of `extends`
const extractRefs = (header) => ({
parent: header.parent || header.base || header.inherits,
});
const chain = createChain({ gateway, extractRefs });Relative Paths
Chain supports relative paths in extends by passing the referrer context to Gateway:
// head.json: { "extends": "./delta.json" }
// delta.json: { "extends": "../base/state.json" }
const result = await chain.resolve('/fixtures/head.json');
// Chain passes referrer to Gateway:
// loadHeader("./delta.json", { referrer: "/fixtures/head.json" })
// Gateway/FileProvider resolves to: "/fixtures/delta.json"
// Then:
// loadHeader("../base/state.json", { referrer: "/fixtures/delta.json" })
// Gateway/FileProvider resolves to: "/fixtures/base/state.json"Note: Chain does NOT resolve paths itself. It passes the referrer context, and the Gateway/Provider resolves the relative path. This keeps Chain agnostic to filesystem details.
Errors
All errors extend ChainError and include:
code- Programmatic error codesource- Source that caused the errortoJSON()- Serialization support
import {
ChainError,
CircularDependencyError,
MaxDepthError,
HeaderValidationError,
ResolutionError,
AbortError,
// Type guards
isChainError,
isCircularDependencyError,
isMaxDepthError,
isHeaderValidationError,
isResolutionError,
isAbortError,
} from '@statedelta/chain';CircularDependencyError
try {
await chain.resolve('./a.json');
} catch (error) {
if (isCircularDependencyError(error)) {
console.log('Cycle:', error.cycle);
// ['a.json', 'b.json', 'c.json', 'a.json']
console.log(error.visualize());
// a.json
// ↓
// b.json
// ↓
// c.json
// ↓
// a.json ← cycle!
}
}MaxDepthError
if (isMaxDepthError(error)) {
console.log(`Depth ${error.depth} exceeded max ${error.maxDepth}`);
console.log('Path:', error.path);
}Architecture
┌─────────────────────────────────────────────────────────────────┐
│ ORCHESTRATOR │
│ (Loader or Assembler) │
│ │
│ - Provides extractRefs (knows DSL) │
│ - Listens to Chain events │
│ - Starts parallel work on node:loaded │
└─────────────────────────────────────────────────────────────────┘
│ │
│ on('node:loaded') │ gateway.load()
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────┐
│ CHAIN │ │ GATEWAY │
│ │ │ │
│ - Navigates tail │ headers │ - loadHeader() partial │
│ - EventEmitter │─────────▶│ - load() full │
│ - Cycle detection │ │ - Cache │
│ - One-shot │ │ - Integrity validation │
└──────────────────────┘ └──────────────────────────────┘What Chain Does
- ✅ Navigates inheritance chain via
gateway.loadHeader() - ✅ Emits events for each node
- ✅ Returns
Promise<ChainResult>with complete tail - ✅ Detects circular dependencies
- ✅ Supports
AbortSignalcancellation - ✅ Custom header validation
What Chain Does NOT Do
- ❌ Know your DSL (receives
extractRefs) - ❌ Direct I/O (Gateway does it)
- ❌ Parse content (Gateway does it)
- ❌ Store content (stateless)
- ❌ Manage deps (Orchestrator does it)
- ❌ Cache (Gateway does it)
- ❌ Resolve paths (passes
referrer, Provider resolves)
Performance
| Operation | Without Chain | With Chain | |-----------|---------------|------------| | Discover 10-node tail | ~500ms (full files) | ~50ms (headers only) | | Bytes per node | ~500KB | ~4KB | | Start parallel work | After complete | After 1st node |
Type Guards
import {
isChainNode,
isChainResult,
isCollectedHeader,
isGatewayLike,
} from '@statedelta/chain';
if (isChainNode(value)) {
// value is ChainNode
}Testing
# Run tests
pnpm test
# Watch mode
pnpm test:watch
# Coverage
pnpm test:coverage
# UI
pnpm test:uiCoverage: 99%+ statements, 97%+ functions
Documentation
- PROPOSAL.md - Specification
- ARCHITECTURE.md - Architecture details
- DESIGN-DECISIONS.md - 21 design decisions
Related Packages
- @statedelta/gateway - I/O gateway (required peer dependency)
- @statedelta/assembler - State assembler (consumer)
- @statedelta/loader - Orchestrator (consumer)
License
MIT
