A state machine for managing DuckDB operations in web applications. This library provides a type-safe interface for database initialization, query execution, transaction management, and table catalog operations.
- State Management: Full XState integration for predictable database state management
- DuckDB Integration: Built on top of
@duckdb/duckdb-wasmfor browser-based analytics - Transaction Support: Complete transaction lifecycle management (begin, execute, commit, rollback)
- Table Catalog: Dynamic table loading, versioning, and management
- Type Safety: Full TypeScript support with comprehensive type definitions
- Multiple Data Formats: Support for Arrow IPC and JSON data formats
- Compression: Built-in support for data compression (zlib)
- Real-time Updates: Subscription-based table change notifications
npm install @jr200-labs/xstate-duckdb
# or
yarn add @jr200-labs/xstate-duckdb
# or
pnpm add @jr200-labs/xstate-duckdb@duckdb/duckdb-wasm, apache-arrow, and @opentelemetry/api are declared as peer dependencies and must be installed directly by the consumer. This guarantees a single resolved version across the dependency tree -- preventing the class of bug where a transitive copy of DuckDB-wasm diverges from the .wasm assets the consumer actually ships.
pnpm add @duckdb/duckdb-wasm apache-arrow @opentelemetry/apiSupported ranges:
| Peer | Range |
|---|---|
@duckdb/duckdb-wasm |
>=1.33.1-dev42.0 <2 |
apache-arrow |
>=21 <22 |
@opentelemetry/api |
^1.9.0 |
The duckdbMachine has the following states:
idle: Initial state, waiting for configurationconfigured: Database configured, ready to connectinitializing: Database initialization in progressconnected: Database connected and ready for operationsdisconnected: Database disconnectederror: Error state
CONFIGURE: Configure database parameters and catalogRESET: Reset to initial state
CONNECT: Initialize and connect to databaseDISCONNECT: Disconnect from database
QUERY.EXECUTE: Execute a one-shot query with auto-commit
TRANSACTION.BEGIN: Start a new transactionTRANSACTION.EXECUTE: Execute a query within a transactionTRANSACTION.COMMIT: Commit the current transactionTRANSACTION.ROLLBACK: Rollback the current transaction
CATALOG.SUBSCRIBE: Subscribe to table changes with a subscription objectCATALOG.UNSUBSCRIBE: Unsubscribe from table changes using subscription IDCATALOG.LIST_TABLES: List all loaded tablesCATALOG.LOAD_TABLE: Load data into a tableCATALOG.DROP_TABLE: Drop a tableCATALOG.LIST_DEFINITIONS: Get catalog configuration
This library emits OpenTelemetry spans for DuckDB lifecycle, query, transaction, and catalog operations. DuckDB runs in-process (WASM) so there is no cross- process context propagation — spans attach to the ambient OTel context so they nest correctly under any caller-provided parent span.
| Span name | Emitted by | Attributes |
|---|---|---|
xstate.duckdb.init |
initDuckDb |
duckdb.version |
xstate.duckdb.close |
closeDuckDb |
— |
xstate.duckdb.query |
duckdbRunQuery / queryDuckDb |
query.description, result.type, result.row_count |
xstate.duckdb.tx.begin |
beginTransaction |
— |
xstate.duckdb.tx.commit |
commitTransaction |
— |
xstate.duckdb.tx.rollback |
rollbackTransaction |
— |
xstate.duckdb.load_table |
loadTableIntoDuckDb |
table.spec, payload.type, payload.compression, table.instance |
xstate.duckdb.prune |
pruneTableVersions |
pruned.instances, kept.versions |
All error paths record exceptions on the active span, set span status to
ERROR, and emit an xstate.duckdb.error event with a truncated stack.
@opentelemetry/api is a peer dependency — the consumer controls the installed
version and registers the SDK. If no provider is registered all telemetry calls
become no-ops. Minimal setup:
import { trace, propagation, context } from '@opentelemetry/api'
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
import { W3CTraceContextPropagator } from '@opentelemetry/core'
import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
const provider = new BasicTracerProvider({
spanProcessors: [
/* your exporter */
],
})
trace.setGlobalTracerProvider(provider)
propagation.setGlobalPropagator(new W3CTraceContextPropagator())
const ctxMgr = new AsyncLocalStorageContextManager()
ctxMgr.enable()
context.setGlobalContextManager(ctxMgr)import { duckdbMachine } from '@jr200-labs/xstate-duckdb'
import { useActor } from '@xstate/react'
function DatabaseComponent() {
const [state, send] = useActor(duckdbMachine)
const initializeDB = () => {
send({
type: 'CONFIGURE',
dbInitParams: {
logLevel: LogLevel.INFO,
config: {},
},
catalogConfig: {},
})
send({ type: 'CONNECT' })
}
const runQuery = () => {
send({
type: 'QUERY.EXECUTE',
queryParams: {
sql: 'SELECT 1 as test',
callback: (result) => console.log(result),
description: 'test_query',
resultType: 'json',
},
})
}
return (
<div>
<button onClick={initializeDB}>Initialize DB</button>
<button onClick={runQuery}>Run Query</button>
</div>
)
}const handleTransaction = () => {
// Begin transaction
send({ type: 'TRANSACTION.BEGIN' })
// Execute queries within transaction
send({
type: 'TRANSACTION.EXECUTE',
queryParams: {
sql: 'INSERT INTO users (name) VALUES ("John")',
callback: (result) => console.log('Insert result:', result),
description: 'insert_user',
resultType: 'json',
},
})
// Commit or rollback
send({ type: 'TRANSACTION.COMMIT' })
// or send({ type: 'TRANSACTION.ROLLBACK' })
}const handleTableOperations = () => {
// Load a table with Arrow data
send({
type: 'CATALOG.LOAD_TABLE',
tableName: 'my_table',
tablePayload: arrowDataBase64,
payloadType: 'b64ipc',
payloadCompression: 'zlib',
callback: (tableInstanceName, error) => {
if (error) console.error('Load error:', error)
else console.log('Table loaded:', tableInstanceName)
},
})
// List all tables
send({
type: 'CATALOG.LIST_TABLES',
callback: (tables) => console.log('Tables:', tables),
})
// Subscribe to table changes with enhanced subscription object
send({
type: 'CATALOG.SUBSCRIBE',
subscription: {
tableSpecName: 'my_table',
onSubscribe: (id: string, tableSpecName: string) => {
console.log(`Subscribed to ${tableSpecName} with ID: ${id}`)
},
onChange: (tableInstanceName: string, tableVersionId: number) => {
console.log(`Table updated: ${tableInstanceName}, version: ${tableVersionId}`)
},
},
})
// Unsubscribe using the subscription ID
send({
type: 'CATALOG.UNSUBSCRIBE',
id: 'subscription_id_here',
})
}The subscription system provides real-time notifications when tables are updated:
// Create a subscription with custom callbacks
const subscription = {
tableSpecName: 'users',
onSubscribe: (id: string, tableSpecName: string) => {
console.log(`Successfully subscribed to ${tableSpecName} with ID: ${id}`)
// Store the subscription ID for later unsubscription
setSubscriptionId(id)
},
onChange: (tableInstanceName: string, tableVersionId: number) => {
console.log(`Table ${tableSpecName} updated to version ${tableVersionId}`)
// Handle table updates - e.g., refresh UI, fetch new data
refreshTableData(tableInstanceName)
},
}
send({
type: 'CATALOG.SUBSCRIBE',
subscription,
})
// Later, unsubscribe using the stored ID
send({
type: 'CATALOG.UNSUBSCRIBE',
id: subscriptionId,
})- Node.js 18+
- pnpm (recommended)
# Install dependencies
pnpm install
# Build the project
pnpm build
# Run tests
pnpm test
# Start development mode
pnpm devThe project includes a React example in examples/react-test/:
cd examples/react-test
pnpm install
pnpm devThis will start a development server with a comprehensive UI for testing all database operations.
Contributions welcome!
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
MIT License - see LICENSE file for details.