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.
When using a publishable key (frontend), include session (token from your backend) in every request.
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!'
},
options: {
timeout: 60000, // 60 seconds
retries: 3,
metadata: {
source: 'frontend',
triggeredBy: 'user-action'
}
}
});
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>
);
}
Complete Approval Feature Example
interface ApprovalRequest {
id: string;
title: string;
description: string;
requestedBy: string;
status: 'pending' | 'approved' | 'rejected';
}
function ApprovalFeature({ request }: { request: ApprovalRequest }) {
const [executionId, setExecutionId] = useState<string | null>(null);
const [featureStatus, setFeatureStatus] = useState<string>('idle');
// Start feature
const { mutate: startFeature, isLoading: isStarting } = useFeatureExecution({
onSuccess: (result) => {
setExecutionId(result.executionId);
setFeatureStatus('running');
}
});
// Send approval signal
const { mutate: sendSignal } = useFeatureSignal({
onSuccess: () => {
setFeatureStatus('completed');
}
});
// Monitor status
useFeatureSubscription({
executionId: executionId || '',
enabled: !!executionId,
onEvent: (event) => {
if (event.type === 'feature_completed') {
setFeatureStatus('completed');
} else if (event.type === 'feature_failed') {
setFeatureStatus('failed');
}
}
});
const handleStart = () => {
startFeature({
feature: 'approval-process',
input: {
requestId: request.id,
requestData: request
}
});
};
const handleApprove = () => {
if (!executionId) return;
sendSignal({
executionId,
signal: 'approve',
data: {
comments: 'Approved by manager',
timestamp: new Date().toISOString()
}
});
};
const handleReject = () => {
if (!executionId) return;
sendSignal({
executionId,
signal: 'reject',
data: {
reason: 'Does not meet requirements'
}
});
};
return (
<div className="approval-feature">
<div className="request-details">
<h3>{request.title}</h3>
<p>{request.description}</p>
<p>Requested by: {request.requestedBy}</p>
</div>
{featureStatus === 'idle' && (
<button onClick={handleStart} disabled={isStarting}>
{isStarting ? 'Starting...' : 'Start Approval Process'}
</button>
)}
{featureStatus === 'running' && executionId && (
<div className="feature-controls">
<FeatureStatus executionId={executionId} />
<div className="actions">
<button onClick={handleApprove} className="approve">
Approve
</button>
<button onClick={handleReject} className="reject">
Reject
</button>
</div>
</div>
)}
{featureStatus === 'completed' && (
<div className="success">
Approval process completed!
</div>
)}
{featureStatus === 'failed' && (
<div className="error">
Approval process failed. Please try again.
</div>
)}
</div>
);
}
Next Steps
Vue 3 composables for executing and managing Ductape Features.
useFeatureExecution
Execute a feature and track its progress.
Basic Feature Execution
<script setup lang="ts">
import { useFeatureExecution } from '@ductape/vue';
const { mutate, isLoading, data, error } = useFeatureExecution({
onSuccess: (result) => {
console.log('Feature completed:', result);
alert('Order processed successfully!');
}
});
const handleProcessOrder = (orderId: string) => {
mutate({
feature: 'process-order',
input: {
orderId,
priority: 'high'
}
});
};
</script>
<template>
<div>
<button @click="handleProcessOrder('order-123')" :disabled="isLoading">
{{ isLoading ? 'Processing...' : 'Process Order' }}
</button>
<p v-if="error" class="error">{{ error.message }}</p>
<p v-if="data">Result: {{ JSON.stringify(data.output) }}</p>
</div>
</template>
Feature with Real-time Status
<script setup lang="ts">
import { ref } from 'vue';
import { useFeatureExecution } from '@ductape/vue';
import FeatureStatus from './FeatureStatus.vue';
const executionId = ref<string | null>(null);
const { mutate, isLoading } = useFeatureExecution({
onSuccess: (result) => {
executionId.value = result.executionId;
}
});
const handleStart = () => {
mutate({
feature: 'data-pipeline',
input: {
source: 'database',
destination: 's3'
}
});
};
</script>
<template>
<div>
<button @click="handleStart" :disabled="isLoading">
Start Pipeline
</button>
<FeatureStatus v-if="executionId" :execution-id="executionId" />
</div>
</template>
useFeatureStatus
Monitor the status of a running feature.
<script setup lang="ts">
import { useFeatureStatus } from '@ductape/vue';
const props = defineProps<{ executionId: string }>();
const { data: status, isLoading } = useFeatureStatus(props.executionId, {
refetchInterval: 2000 // Poll every 2 seconds
});
</script>
<template>
<div v-if="isLoading">Loading status...</div>
<div v-else class="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>
<div v-if="status?.status === 'running'" class="progress-bar">
<div :style="{ width: `${status.progress}%` }" />
</div>
<div v-if="status?.status === 'completed'" class="success">
Feature completed successfully!
</div>
<div v-if="status?.status === 'failed'" class="error">
Error: {{ status.error }}
</div>
</div>
</template>
<style scoped>
.progress-bar {
height: 4px;
background: #e0e0e0;
margin-top: 1rem;
}
.progress-bar > div {
height: 100%;
background: #4CAF50;
transition: width 0.3s;
}
</style>
useFeatureSubscription
Subscribe to real-time feature events.
<script setup lang="ts">
import { ref } from 'vue';
import { useFeatureSubscription } from '@ductape/vue';
const props = defineProps<{ executionId: string }>();
const events = ref<any[]>([]);
useFeatureSubscription({
executionId: props.executionId,
onEvent: (event) => {
events.value.push(event);
if (event.type === 'step_completed') {
console.log(`Step ${event.step} completed`);
} else if (event.type === 'feature_completed') {
console.log('Feature finished!');
}
}
});
</script>
<template>
<div>
<h3>Feature Events</h3>
<ul>
<li v-for="(event, idx) in events" :key="idx">
<strong>{{ event.type }}:</strong> {{ event.message }}
<span class="timestamp">
{{ new Date(event.timestamp).toLocaleTimeString() }}
</span>
</li>
</ul>
</div>
</template>
useFeatureSignal
Send signals to a running feature.
<script setup lang="ts">
import { useFeatureSignal } from '@ductape/vue';
const props = defineProps<{ executionId: string }>();
const { mutate: sendSignal, isLoading } = useFeatureSignal({
onSuccess: () => {
alert('Signal sent successfully');
}
});
const handleApprove = () => {
sendSignal({
executionId: props.executionId,
signal: 'approve',
data: {
approvedBy: 'user-123',
timestamp: new Date().toISOString()
}
});
};
const handleReject = () => {
sendSignal({
executionId: props.executionId,
signal: 'reject',
data: { reason: 'Does not meet criteria' }
});
};
</script>
<template>
<div>
<button @click="handleApprove" :disabled="isLoading">
Approve
</button>
<button @click="handleReject" :disabled="isLoading">
Reject
</button>
</div>
</template>
useFeatureCancel
Cancel a running feature.
<script setup lang="ts">
import { useFeatureCancel } from '@ductape/vue';
const props = defineProps<{ 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: props.executionId });
}
};
</script>
<template>
<button @click="handleCancel" :disabled="isLoading" class="danger">
{{ isLoading ? 'Cancelling...' : 'Cancel Feature' }}
</button>
</template>
useFeatureHistory
View feature execution history.
<script setup lang="ts">
import { useFeatureHistory } from '@ductape/vue';
const { data, isLoading } = useFeatureHistory({
feature: 'process-order',
limit: 20
});
</script>
<template>
<div v-if="isLoading">Loading history...</div>
<div v-else>
<h2>Feature History</h2>
<table>
<thead>
<tr>
<th>Execution ID</th>
<th>Status</th>
<th>Started</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
<tr v-for="execution in data?.executions" :key="execution.id">
<td>{{ execution.id }}</td>
<td>
<span :class="`status-${execution.status}`">
{{ execution.status }}
</span>
</td>
<td>{{ new Date(execution.startedAt).toLocaleString() }}</td>
<td>{{ execution.duration }}ms</td>
</tr>
</tbody>
</table>
</div>
</template>
Complete Approval Feature Example
<script setup lang="ts">
import { ref } from 'vue';
import {
useFeatureExecution,
useFeatureSignal,
useFeatureSubscription
} from '@ductape/vue';
interface ApprovalRequest {
id: string;
title: string;
description: string;
requestedBy: string;
status: 'pending' | 'approved' | 'rejected';
}
const props = defineProps<{ request: ApprovalRequest }>();
const executionId = ref<string | null>(null);
const featureStatus = ref<string>('idle');
// Start feature
const { mutate: startFeature, isLoading: isStarting } = useFeatureExecution({
onSuccess: (result) => {
executionId.value = result.executionId;
featureStatus.value = 'running';
}
});
// Send approval signal
const { mutate: sendSignal } = useFeatureSignal({
onSuccess: () => {
featureStatus.value = 'completed';
}
});
// Monitor status
useFeatureSubscription({
executionId: executionId.value || '',
enabled: !!executionId.value,
onEvent: (event) => {
if (event.type === 'feature_completed') {
featureStatus.value = 'completed';
} else if (event.type === 'feature_failed') {
featureStatus.value = 'failed';
}
}
});
const handleStart = () => {
startFeature({
feature: 'approval-process',
input: {
requestId: props.request.id,
requestData: props.request
}
});
};
const handleApprove = () => {
if (!executionId.value) return;
sendSignal({
executionId: executionId.value,
signal: 'approve',
data: {
comments: 'Approved by manager',
timestamp: new Date().toISOString()
}
});
};
const handleReject = () => {
if (!executionId.value) return;
sendSignal({
executionId: executionId.value,
signal: 'reject',
data: {
reason: 'Does not meet requirements'
}
});
};
</script>
<template>
<div class="approval-feature">
<div class="request-details">
<h3>{{ request.title }}</h3>
<p>{{ request.description }}</p>
<p>Requested by: {{ request.requestedBy }}</p>
</div>
<button
v-if="featureStatus === 'idle'"
@click="handleStart"
:disabled="isStarting"
>
{{ isStarting ? 'Starting...' : 'Start Approval Process' }}
</button>
<div v-if="featureStatus === 'running' && executionId" class="feature-controls">
<FeatureStatus :execution-id="executionId" />
<div class="actions">
<button @click="handleApprove" class="approve">
Approve
</button>
<button @click="handleReject" class="reject">
Reject
</button>
</div>
</div>
<div v-if="featureStatus === 'completed'" class="success">
Approval process completed!
</div>
<div v-if="featureStatus === 'failed'" class="error">
Approval process failed. Please try again.
</div>
</div>
</template>
<style scoped>
.approval-feature {
padding: 1.5rem;
border: 1px solid #ddd;
border-radius: 8px;
}
.feature-controls {
margin-top: 1rem;
}
.actions {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.approve {
background: #4CAF50;
color: white;
}
.reject {
background: #f44336;
color: white;
}
</style>