Features
- @ductape/client
- React
- Vue 3
Features allow you to execute complex, multi-step processes with built-in durability, retries, and state management. Execute backend features directly from your frontend application.
For protected operations, configure the backend-issued session with ductape.setSession(token) or pass session explicitly per call. The backend remains responsible for authorization; login should use a backend endpoint or an explicitly permitted pre-login operation.
Feature execution preserves the runtime receipt (feature_id, execution_time, completed_steps, and optional failure/rollback details). executionId aliases feature_id; timestamps are present only when supplied by the server. Failed, rolled-back, timed-out, or cancelled execution rejects with FEATURE_EXECUTION_FAILED and the receipt on error.result. HTTP 200 is not itself a successful execution.
The client does not automatically replay mutations after network failures. Reconcile an uncertain result before retrying. Create a new client when changing product/environment after accessing services; existing connections retain their original scope. Changing sessions closes realtime streams until connect() is called again.
Executing Features
Basic Execution
const sessionToken = getSessionFromYourBackend();
const execution = await ductape.features.execute({
feature: 'process-order',
input: {
orderId: 'order-123',
userId: 'user-456'
},
session: sessionToken,
});
console.log('Execution ID:', execution.executionId);
console.log('Status:', execution.status);
Execute with Options
const execution = await ductape.features.execute({
feature: 'send-notification',
input: {
userId: 'user-123',
message: 'Your order has shipped!'
},
product: 'my-product',
env: 'dev',
session: sessionToken,
});
Checking Feature Status
Get Current Status
const status = await ductape.features.getStatus({
executionId: 'exec-123'
});
console.log('Status:', status.status); // 'running', 'completed', 'failed', 'cancelled'
console.log('Result:', status.result);
console.log('Error:', status.error);
Poll for Completion
async function waitForCompletion(executionId: string): Promise<any> {
while (true) {
const status = await ductape.features.getStatus({ executionId });
if (status.status === 'completed') {
return status.result;
}
if (status.status === 'failed') {
throw new Error(status.error);
}
if (status.status === 'cancelled') {
throw new Error('Feature was cancelled'');
}
// Wait 1 second before checking again
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// Usage
const execution = await ductape.features.execute({
feature: 'data-processing',
input: { dataId: '123' }
});
try {
const result = await waitForCompletion(execution.executionId);
console.log('Feature completed':', result);
} catch (error) {
console.error('Feature failed':', error);
}
Feature History
Get the execution history to see all steps:
const history = await ductape.features.getHistory({
executionId: 'exec-123'
});
history.events.forEach(event => {
console.log(`[${event.timestamp}] ${event.type}:`, event.data);
});
Sending Signals
Send signals to running features:
// Start a long-running feature
const execution = await ductape.features.execute({
feature: 'approval-process',
input: { requestId: 'req-123' }
});
// Later: send an approval signal
await ductape.features.signal({
executionId: execution.executionId,
signal: 'approve',
data: {
approvedBy: 'user-456',
comments: 'Looks good'
}
});
Cancelling Features
await ductape.features.cancel({
executionId: 'exec-123',
reason: 'User cancelled the operation'
});
Subscribing to Feature Updates
Get real-time updates on feature execution:
// Connect to WebSocket first
await ductape.connect();
const subscription = ductape.features.subscribe({
executionId: 'exec-123',
onChange: (event) => {
console.log('Feature event':', event);
if (event.type === 'status_changed') {
console.log('New status:', event.data.status);
}
if (event.type === 'step_completed') {
console.log('Step completed:', event.data.step);
}
if (event.type === 'completed') {
console.log('Feature completed':', event.data.result);
}
if (event.type === 'failed') {
console.error('Feature failed':', event.data.error);
}
}
});
// Later: unsubscribe
subscription.unsubscribe();
Listing Features
Get a list of feature executions:
const result = await ductape.features.list({
feature: 'process-order',
status: 'running',
limit: 20,
offset: 0
});
console.log(`Found ${result.count} running executions`);
result.executions.forEach(exec => {
console.log(`- ${exec.executionId}: ${exec.status}`);
});
Complete Order Processing Example
class OrderProcessor {
private ductape: Ductape;
constructor(ductape: Ductape) {
this.ductape = ductape;
}
async processOrder(orderId: string) {
// Show loading state
this.showLoading(true);
try {
// Execute feature
const execution = await this.ductape.features.execute({
feature: 'process-order',
input: {
orderId: orderId,
timestamp: new Date().toISOString()
}
});
// Subscribe to updates
const subscription = this.ductape.features.subscribe({
executionId: execution.executionId,
onChange: (event) => {
this.handleFeatureEvent(event);
}
});
// Wait for completion
const result = await this.waitForCompletion(execution.executionId);
// Cleanup
subscription.unsubscribe();
this.showLoading(false);
// Show success
this.showSuccess('Order processed successfully!');
return result;
} catch (error) {
this.showLoading(false);
this.showError('Failed to process order: ' + error.message);
throw error;
}
}
private async waitForCompletion(executionId: string): Promise<any> {
while (true) {
const status = await this.ductape.features.getStatus({ executionId });
if (status.status === 'completed') {
return status.result;
}
if (status.status === 'failed') {
throw new Error(status.error);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
private handleFeatureEvent(event: any) {
switch (event.type) {
case 'status_changed':
this.updateStatus(event.data.status);
break;
case 'step_completed':
this.updateProgress(event.data.step);
break;
case 'completed':
console.log('Feature completed':', event.data.result);
break;
case 'failed':
console.error('Feature failed':', event.data.error);
break;
}
}
private showLoading(show: boolean) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
private updateStatus(status: string) {
document.getElementById('status').textContent = `Status: ${status}`;
}
private updateProgress(step: string) {
const progress = document.getElementById('progress');
const item = document.createElement('li');
item.textContent = `✓ ${step}`;
progress.appendChild(item);
}
private showSuccess(message: string) {
alert(message);
}
private showError(message: string) {
alert(message);
}
}
// Usage
const processor = new OrderProcessor(ductape);
await processor.processOrder('order-123');
Approval Feature Example
class ApprovalFeature {
private ductape: Ductape;
private executionId: string | null = null;
constructor(ductape: Ductape) {
this.ductape = ductape;
}
async submitForApproval(data: any) {
const execution = await this.ductape.features.execute({
feature: 'approval-process',
input: {
data: data,
submittedBy: 'current-user-id',
submittedAt: new Date().toISOString()
}
});
this.executionId = execution.executionId;
// Subscribe to updates
this.ductape.features.subscribe({
executionId: this.executionId,
onChange: (event) => {
if (event.type === 'completed') {
this.onApproved(event.data.result);
} else if (event.type === 'failed') {
this.onRejected(event.data.error);
}
}
});
return execution;
}
async approve(comments: string) {
if (!this.executionId) {
throw new Error('No active feature');
}
await this.ductape.features.signal({
executionId: this.executionId,
signal: 'approve',
data: {
approvedBy: 'current-user-id',
comments: comments,
approvedAt: new Date().toISOString()
}
});
}
async reject(reason: string) {
if (!this.executionId) {
throw new Error('No active feature');
}
await this.ductape.features.signal({
executionId: this.executionId,
signal: 'reject',
data: {
rejectedBy: 'current-user-id',
reason: reason,
rejectedAt: new Date().toISOString()
}
});
}
async cancel() {
if (!this.executionId) {
throw new Error('No active feature');
}
await this.ductape.features.cancel({
executionId: this.executionId,
reason: 'Cancelled by user'
});
}
private onApproved(result: any) {
console.log('Approved:', result);
alert('Request approved!');
}
private onRejected(reason: string) {
console.log('Rejected:', reason);
alert('Request rejected: ' + reason);
}
}
// Usage
const approval = new ApprovalFeature(ductape);
// Submit for approval
await approval.submitForApproval({
type: 'expense',
amount: 1000,
description: 'New laptop'
});
// Later: approve
await approval.approve('Approved for purchase');
// Or: reject
// await approval.reject('Budget exceeded');
Multi-Step Feature with Progress
class MultiStepFeature {
private ductape: Ductape;
private steps: string[] = [
'Validating input',
'Processing data',
'Generating report',
'Sending notifications',
'Cleanup'
];
private currentStep: number = 0;
constructor(ductape: Ductape) {
this.ductape = ductape;
}
async execute(input: any) {
const execution = await this.ductape.features.execute({
feature: 'multi-step-process',
input: input
});
// Subscribe to step updates
const subscription = this.ductape.features.subscribe({
executionId: execution.executionId,
onChange: (event) => {
if (event.type === 'step_completed') {
this.currentStep++;
this.updateProgress();
}
}
});
try {
const result = await this.waitForCompletion(execution.executionId);
subscription.unsubscribe();
return result;
} catch (error) {
subscription.unsubscribe();
throw error;
}
}
private async waitForCompletion(executionId: string): Promise<any> {
while (true) {
const status = await this.ductape.features.getStatus({ executionId });
if (status.status === 'completed') {
return status.result;
}
if (status.status === 'failed') {
throw new Error(status.error);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
private updateProgress() {
const percentage = (this.currentStep / this.steps.length) * 100;
const currentStepName = this.steps[this.currentStep - 1] || 'Starting...';
document.getElementById('progress-bar').style.width = `${percentage}%`;
document.getElementById('progress-text').textContent =
`${currentStepName} (${this.currentStep}/${this.steps.length})`;
}
}
// Usage
const feature = new MultiStepFeature(ductape);
const result = await feature.execute({ dataId: '123' });
Feature Dashboard
class FeatureDashboard {
private ductape: Ductape;
constructor(ductape: Ductape) {
this.ductape = ductape;
}
async loadFeatures() {
const [running, completed, failed] = await Promise.all([
this.ductape.features.list({ status: 'running', limit: 10 }),
this.ductape.features.list({ status: 'completed', limit: 10 }),
this.ductape.features.list({ status: 'failed', limit: 10 })
]);
this.renderSection('running', running.executions);
this.renderSection('completed', completed.executions);
this.renderSection('failed', failed.executions);
}
private renderSection(status: string, executions: any[]) {
const container = document.getElementById(`${status}-features`);
container.innerHTML = '';
executions.forEach(exec => {
const item = document.createElement('div');
item.className = 'feature-item';
item.innerHTML = `
<div class="feature-info">
<strong>${exec.feature}</strong>
<span>${exec.executionId}</span>
<span>${new Date(exec.startedAt).toLocaleString()}</span>
</div>
<button onclick="dashboard.viewDetails('${exec.executionId}')">
View Details
</button>
`;
container.appendChild(item);
});
}
async viewDetails(executionId: string) {
const [status, history] = await Promise.all([
this.ductape.features.getStatus({ executionId }),
this.ductape.features.getHistory({ executionId })
]);
console.log('Status:', status);
console.log('History:', history);
// Show in modal or details panel
this.showDetailsModal(status, history);
}
private showDetailsModal(status: any, history: any) {
// Implementation for showing details
console.log('Showing details:', status, history);
}
}
// Usage
const dashboard = new FeatureDashboard(ductape);
await dashboard.loadFeatures();
Best Practices
- Handle errors gracefully: Always wrap feature execution in try-catch
- Use subscriptions: Subscribe to feature updates for real-time feedback
- Implement timeouts: Set reasonable timeouts for feature execution
- Store execution IDs: Save execution IDs in your database for tracking
- Show progress: Provide visual feedback during long-running features
- Handle cancellation: Allow users to cancel long-running operations
- Retry logic: Implement retry logic for transient failures
- Log events: Log feature events for debugging and monitoring
Next Steps
React hooks for executing and managing Ductape Features.
When using a publishable key, include session in every feature call. Example: import { session } from './config'.
useFeatureExecution
Execute a feature and track its progress.
Basic Feature Execution
import { useFeatureExecution } from '@ductape/react';
import { session } from './config';
function OrderProcessor() {
const { mutate, isLoading, data, error } = useFeatureExecution({
onSuccess: (result) => {
console.log('Feature completed:', result);
alert('Order processed successfully!');
},
onError: (err) => {
console.error('Feature failed:', err);
}
});
const handleProcessOrder = (orderId: string) => {
mutate({
feature: 'process-order',
input: {
orderId,
priority: 'high'
},
session,
});
};
return (
<div>
<button onClick={() => handleProcessOrder('order-123')} disabled={isLoading}>
{isLoading ? 'Processing...' : 'Process Order'}
</button>
{error && <p className="error">{error.message}</p>}
{data && <p>Result: {JSON.stringify(data.output)}</p>}
</div>
);
}
Feature with Real-time Status
import { session } from './config';
function FeatureRunner() {
const [executionId, setExecutionId] = useState<string | null>(null);
const { mutate, isLoading } = useFeatureExecution({
onSuccess: (result) => {
setExecutionId(result.executionId);
}
});
const handleStart = () => {
mutate({
feature: 'data-pipeline',
input: {
source: 'database',
destination: 's3'
},
session,
});
};
return (
<div>
<button onClick={handleStart} disabled={isLoading}>
Start Pipeline
</button>
{executionId && <FeatureStatus executionId={executionId} />}
</div>
);
}
useFeatureStatus
Monitor the status of a running feature.
import { useFeatureStatus } from '@ductape/react';
function FeatureStatus({ executionId }: { executionId: string }) {
const { data: status, isLoading } = useFeatureStatus(executionId, {
refetchInterval: 2000 // Poll every 2 seconds
});
if (isLoading) return <div>Loading status...</div>;
return (
<div className="feature-status">
<h3>Feature Status</h3>
<div>
<strong>Status:</strong> {status?.status}
</div>
<div>
<strong>Current Step:</strong> {status?.currentStep}
</div>
<div>
<strong>Progress:</strong> {status?.progress}%
</div>
{status?.status === 'running' && (
<div className="progress-bar">
<div style={{ width: `${status.progress}%` }} />
</div>
)}
{status?.status === 'completed' && (
<div className="success">
Feature completed successfully!
</div>
)}
{status?.status === 'failed' && (
<div className="error">
Error: {status.error}
</div>
)}
</div>
);
}
useFeatureSubscription
Subscribe to real-time feature events.
import { useFeatureSubscription } from '@ductape/react';
function LiveFeatureMonitor({ executionId }: { executionId: string }) {
const [events, setEvents] = useState<any[]>([]);
useFeatureSubscription({
executionId,
onEvent: (event) => {
setEvents(prev => [...prev, event]);
if (event.type === 'step_completed') {
console.log(`Step completed`);
} else if (event.type === 'feature_completed') {
console.log('Feature finished!');
}
}
});
return (
<div>
<h3>Feature Events</h3>
<ul>
{events.map((event, idx) => (
<li key={idx}>
<strong>{event.type}:</strong> {event.message}
<span className="timestamp">
{new Date(event.timestamp).toLocaleTimeString()}
</span>
</li>
))}
</ul>
</div>
);
}
useFeatureSignal
Send signals to a running feature.
import { useFeatureSignal } from '@ductape/react';
function FeatureController({ executionId }: { executionId: string }) {
const { mutate: sendSignal, isLoading } = useFeatureSignal({
onSuccess: () => {
alert('Signal sent successfully');
}
});
const handleApprove = () => {
sendSignal({
executionId,
signal: 'approve',
data: {
approvedBy: 'user-123',
timestamp: new Date().toISOString()
}
});
};
const handleReject = () => {
sendSignal({
executionId,
signal: 'reject',
data: { reason: 'Does not meet criteria' }
});
};
return (
<div>
<button onClick={handleApprove} disabled={isLoading}>
Approve
</button>
<button onClick={handleReject} disabled={isLoading}>
Reject
</button>
</div>
);
}
useFeatureCancel
Cancel a running feature.
import { useFeatureCancel } from '@ductape/react';
function CancelButton({ executionId }: { executionId: string }) {
const { mutate: cancel, isLoading } = useFeatureCancel({
onSuccess: () => {
alert('Feature cancelled');
}
});
const handleCancel = () => {
if (confirm('Are you sure you want to cancel this feature?')) {
cancel({ executionId });
}
};
return (
<button onClick={handleCancel} disabled={isLoading} className="danger">
{isLoading ? 'Cancelling...' : 'Cancel Feature'}
</button>
);
}
useFeatureHistory
View feature execution history.
import { useFeatureHistory } from '@ductape/react';
function FeatureHistory() {
const { data, isLoading } = useFeatureHistory({
feature: 'process-order',
limit: 20
});
if (isLoading) return <div>Loading history...</div>;
return (
<div>
<h2>Feature History</h2>
<table>
<thead>
<tr>
<th>Execution ID</th>
<th>Status</th>
<th>Started</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{data?.executions.map(execution => (
<tr key={execution.id}>
<td>{execution.id}</td>
<td>
<span className={`status-${execution.status}`}>
{execution.status}
</span>
</td>
<td>{new Date(execution.startedAt).toLocaleString()}</td>
<td>{execution.duration}ms</td>
</tr>
))}
</tbody>
</table>
</div>
);
}