tendryl
Version: 
A workflow orchestrator with chainable async interfaces, ledger integration for operation tracking, and race condition detection
278 lines (203 loc) • 6.69 kB
Markdown
# Tendryl
A workflow orchestrator with chainable async interfaces, ledger integration for operation tracking, and race condition detection.
## Features
- **Chain**: Class-based and function-based chainable async workflows
- **Ledger**: Automatic operation tracking with collision detection
- **Logger**: Structured logging with Pino integration
- **Chain with Ledger**: Integrated chain and ledger functionality
## Installation
```bash
npm install tendryl
```
## Usage
### Core Chain
The core chain provides a simple way to create chainable async workflows:
```typescript
import { ChainStep, chainstep } from 'tendryl';
// Class-based approach
class FetchData extends ChainStep<{ url: string; data?: any }> {
  async execute(state) {
    const response = await fetch(state.url);
    state.data = await response.json();
    return state;
  }
}
class ProcessData extends ChainStep<{ data?: any; processed?: any }> {
  async execute(state) {
    state.processed = state.data.map(item => ({ ...item, processed: true }));
    return state;
  }
}
// Chain them together
FetchData.then(ProcessData);
// Execute the chain
const result = await FetchData.invoke({ url: 'https://api.example.com/data' });
```
### Function-based approach
```typescript
import { chainstep } from 'tendryl';
const fetchData = chainstep(async (state) => {
  const response = await fetch(state.url);
  state.data = await response.json();
  return state;
}, 'FetchData');
const processData = chainstep(async (state) => {
  state.processed = state.data.map(item => ({ ...item, processed: true }));
  return state;
}, 'ProcessData');
// Chain them together
fetchData.then(processData);
// Execute
const result = await fetchData.invoke({ url: 'https://api.example.com/data' });
```
### Ledger System
The ledger system tracks all operations on an object and detects race conditions:
```typescript
import { createLedger, getLedgerStats, printLedger } from 'tendryl/ledger';
// Create a ledger-wrapped object
const state = createLedger({ count: 0, name: 'test' });
// All operations are tracked
state.count = 1;
state.name = 'updated';
const currentCount = state.count;
// Get statistics
const stats = getLedgerStats(state);
console.log(stats); // { totalOperations: 3, totalReads: 1, totalWrites: 2, collisions: 0, duration: 123 }
// Print detailed ledger
printLedger(state);
```
### Logger
Structured logging with Pino:
```typescript
import { logger, createChildLogger, setLogLevel } from 'tendryl/logger';
// Use the main logger
logger.info('Application started');
// Create a child logger for specific context
const userLogger = createChildLogger('user-service');
userLogger.info('User created', { userId: 123 });
// Set log level
setLogLevel('debug');
```
### Chain with Ledger
Combines chain functionality with automatic ledger tracking:
```typescript
import { ChainStep } from 'tendryl/chain-with-ledger';
class UpdateUser extends ChainStep<{ userId: number; user?: any }> {
  async execute(state) {
    // All operations on state are automatically tracked
    state.user = { id: state.userId, name: 'John Doe' };
    state.user.lastUpdated = new Date();
    return state;
  }
}
class ValidateUser extends ChainStep<{ user?: any; valid?: boolean }> {
  async execute(state) {
    state.valid = state.user && state.user.name;
    return state;
  }
}
// Chain with ledger enabled
UpdateUser.then(ValidateUser);
// Execute with ledger tracking
const result = await UpdateUser.invoke(
  { userId: 123 },
  { enableLedger: true }
);
// Access ledger information
console.log(result.ledger); // Ledger log
console.log(result.reconciliation); // Conflict detection results
```
## API Reference
### Chain
- `ChainStep<S>` - Abstract base class for chain steps
- `chainstep(fn, name?)` - Create chainable steps from functions
- `.then(next)` - Chain steps sequentially
- `.when(condition, target, label?)` - Conditional branching
- `.retries(n)` - Set retry attempts
- `.catch(errorStep)` - Error handling
- `.invoke(initialState)` - Execute the chain
- `.graph(options?)` - Generate Mermaid flowchart
### Ledger
- `createLedger(initial, options?)` - Create ledger-wrapped object
- `getLedger(proxy)` - Get ledger from proxy
- `getLedgerStats(proxy)` - Get operation statistics
- `getCollisions(proxy)` - Get detected collisions
- `reconcileLedger(proxy)` - Reconcile pending operations
- `printLedger(proxy)` - Print detailed ledger report
### Logger
- `logger` - Main Pino logger instance
- `createChildLogger(name)` - Create child logger
- `setLogLevel(level)` - Set log level
- `getLogLevel()` - Get current log level
## Examples
### Workflow with Error Handling
```typescript
import { ChainStep } from 'tendryl/chain-with-ledger';
class FetchData extends ChainStep<{ url: string; data?: any }> {
  async execute(state) {
    const response = await fetch(state.url);
    if (!response.ok) throw new Error('Fetch failed');
    state.data = await response.json();
    return state;
  }
}
class HandleError extends ChainStep<{ error?: string }> {
  async execute(state) {
    state.error = 'Data fetch failed, using fallback';
    state.data = [{ fallback: true }];
    return state;
  }
}
FetchData.retries(3).catch(HandleError);
```
### Parallel Execution
```typescript
import { ChainStep } from 'tendryl/chain-with-ledger';
class ProcessA extends ChainStep<{ resultA?: string }> {
  async execute(state) {
    state.resultA = 'Processed A';
    return state;
  }
}
class ProcessB extends ChainStep<{ resultB?: string }> {
  async execute(state) {
    state.resultB = 'Processed B';
    return state;
  }
}
class Start extends ChainStep<{}> {
  async execute(state) {
    return state;
  }
}
// Execute ProcessA and ProcessB in parallel
Start.then([ProcessA, ProcessB]);
```
## Development
### GitHub Actions
This repository includes GitHub Actions workflows for automated testing and publishing:
- **Test Workflow** (`test.yml`): Runs on every push to main and pull request
  - Type checking
  - Unit tests
  - Build verification
- **Publish Workflow** (`publish.yml`): Runs when a new release is published
  - Automatically publishes to npm
  - Requires `NPM_TOKEN` secret to be configured
### Setting up NPM Token
To enable automatic publishing, you need to:
1. Create an npm access token at https://www.npmjs.com/settings/tokens
2. Add the token as a repository secret named `NPM_TOKEN` in your GitHub repository settings
3. Create a new release on GitHub to trigger the publish workflow
### Local Development
```bash
# Install dependencies
pnpm install
# Run tests
pnpm test
# Build package
pnpm build
# Type check
pnpm type-check
```
## License
MIT