Query Cancellation in DuckDbConnector
The DuckDbConnector now supports query cancellation through a unified QueryHandle
interface with full composability support. All query methods (execute
, query
, queryJson
) now return a QueryHandle
that provides immediate access to cancellation functionality and signal composability.
QueryHandle Interface
typescript
interface QueryOptions {
signal?: AbortSignal; // Optional external abort signal
}
interface QueryHandle<T = any> {
result: Promise<T>; // Promise that resolves with query results
cancel: () => Promise<void>; // Method to cancel the query
signal: AbortSignal; // Read-only access to the abort signal for composability
}
Usage Examples
Basic Query with Cancellation
typescript
import {createWasmDuckDbConnector} from './connectors/createDuckDbConnector';
const connector = createWasmDuckDbConnector();
await connector.initialize();
// Start a query and get immediate access to cancellation
const queryHandle = connector.query('SELECT * FROM large_table');
console.log('Query started');
// Cancel the query if needed (e.g., user clicks cancel button)
setTimeout(() => {
queryHandle.cancel();
}, 5000);
try {
const result = await queryHandle.result;
console.log('Query completed:', result.numRows);
} catch (error) {
console.log('Query was cancelled or failed:', error.message);
}
Composable Cancellation - Multiple Queries with Shared Controller
typescript
// Create a master abort controller for a series of operations
const masterController = new AbortController();
// Start multiple queries that can all be cancelled together
const query1 = connector.query('SELECT COUNT(*) FROM table1', {
signal: masterController.signal,
});
const query2 = connector.query('SELECT AVG(price) FROM products', {
signal: masterController.signal,
});
const query3 = connector.queryJson('SELECT * FROM users LIMIT 100', {
signal: masterController.signal,
});
// Cancel all queries at once
setTimeout(() => {
console.log('Cancelling all queries...');
masterController.abort(); // This cancels all three queries
}, 3000);
try {
const results = await Promise.allSettled([
query1.result,
query2.result,
query3.result,
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Query ${index + 1} completed`);
} else {
console.log(`Query ${index + 1} failed:`, result.reason.message);
}
});
} catch (error) {
console.log('Error in query execution:', error.message);
}
Integration with Other Cancellable Operations
typescript
// Create a shared abort controller for the entire operation
const operationController = new AbortController();
async function performComplexOperation() {
try {
// Step 1: Run a query
const queryHandle = connector.query(
'SELECT id, data FROM source_table WHERE condition = ?',
{signal: operationController.signal},
);
const queryResult = await queryHandle.result;
// Step 2: Make HTTP requests using the same signal
const response = await fetch('/api/process-data', {
method: 'POST',
body: JSON.stringify(queryResult),
signal: operationController.signal, // Same signal!
});
// Step 3: Another query with the same cancellation
const finalQuery = connector.execute(
'INSERT INTO results SELECT * FROM processed_data',
{signal: operationController.signal},
);
await finalQuery.result;
console.log('Complex operation completed');
} catch (error) {
if (error.name === 'AbortError') {
console.log('Operation was cancelled');
} else {
console.log('Operation failed:', error.message);
}
}
}
// Start the operation
performComplexOperation();
// Cancel the entire operation (queries + HTTP requests) after 10 seconds
setTimeout(() => {
operationController.abort();
}, 10000);
Advanced Signal Composition
typescript
// Create timeout-based cancellation
function createTimeoutSignal(ms: number): AbortSignal {
const controller = new AbortController();
setTimeout(() => controller.abort(), ms);
return controller.signal;
}
// Combine multiple signals
function combineSignals(...signals: AbortSignal[]): AbortSignal {
const controller = new AbortController();
signals.forEach((signal) => {
if (signal.aborted) {
controller.abort();
} else {
signal.addEventListener('abort', () => controller.abort());
}
});
return controller.signal;
}
// Usage: Query with both user cancellation and timeout
const userController = new AbortController();
const timeoutSignal = createTimeoutSignal(30000); // 30 second timeout
const combinedSignal = combineSignals(userController.signal, timeoutSignal);
const queryHandle = connector.query('SELECT * FROM very_large_table', {
signal: combinedSignal,
});
// User can still cancel manually
document.getElementById('cancel-btn').onclick = () => {
userController.abort();
};
try {
const result = await queryHandle.result;
console.log('Query completed within timeout');
} catch (error) {
console.log('Query cancelled or timed out:', error.message);
}
Listening to Cancellation Events
typescript
const queryHandle = connector.query('SELECT * FROM table');
// Listen for cancellation
queryHandle.signal.addEventListener('abort', () => {
console.log('Query was cancelled');
// Update UI, clean up resources, etc.
});
// Check if already cancelled
if (queryHandle.signal.aborted) {
console.log('Query was already cancelled');
}
// Cancel after some condition
if (someCondition) {
await queryHandle.cancel();
}
Migration from Old API
Before (Old API)
typescript
const {data, qid} = await connector.query('SELECT * FROM table');
console.log('Query ID:', qid);
console.log('Results:', data.numRows);
After (New API)
typescript
// Simple usage (no external signal)
const queryHandle = connector.query('SELECT * FROM table');
console.log('Query started');
const data = await queryHandle.result;
console.log('Results:', data.numRows);
// With external cancellation control
const controller = new AbortController();
const queryHandle = connector.query('SELECT * FROM table', {
signal: controller.signal,
});
// controller.abort() to cancel
const data = await queryHandle.result;
Implementation Notes
- Hybrid Approach: Combines the simplicity of
.cancel()
with the composability ofAbortSignal
- Optional External Control: Pass your own
AbortSignal
for coordinated cancellation across multiple operations - Automatic Internal Management: If no signal is provided, one is created internally
- Signal Chaining: External signals are chained to internal controllers for proper cleanup
- Web Standards Compliant: Uses standard
AbortController
/AbortSignal
APIs - Composable: Signals can be shared across queries, HTTP requests, and other cancellable operations
- Event-Driven: Listen for abort events to update UI or perform cleanup