Understanding the Executor
The Executor is a core component that ensures database operations are executed safely and in the correct order. It prevents race conditions by serializing operations on the same collection.
Why You Need an Executor
Database adapters often cannot handle concurrent operations safely. Without an executor:
// Race condition without executor
await Promise.all([
db.users.add({ name: "Alice" }),
db.users.add({ name: "Bob" })
]);
// Both operations might interfere with each other
With an executor, operations are queued and executed sequentially:
// Safe with executor
await Promise.all([
db.users.add({ name: "Alice" }),
db.users.add({ name: "Bob" })
]);
// Operations are queued: Alice first, then Bob
Executor Types
Executor (Simple)
A basic FIFO queue. All operations are executed one after another, regardless of collection.
import { Executor, ValtheraClass } from "@wxn0brp/db-core";
const db = new ValtheraClass({
adapter: myAdapter,
executor: new Executor()
});
Characteristics:
- Single queue for all operations
- Strict sequential execution
- Simple and predictable
- Lower concurrency (bottleneck on single queue)
When to use:
- Simple adapters that cannot handle any concurrency
- When you want predictable, linear execution
- Testing and debugging
SmartExecutor (Advanced)
Per-collection queues with automatic cleanup. Operations on different collections can run in parallel.
import { SmartExecutor, ValtheraClass } from "@wxn0brp/db-core";
const db = new ValtheraClass({
adapter: myAdapter,
executor: new SmartExecutor(
ttl, // default: 300000 (5 min)
aware // default: true
)
});
Characteristics:
- Separate queue for each collection
- Parallel execution across collections
- Automatic cleanup of idle queues
- Higher concurrency
Parameters:
ttl(default:300000): Time-to-live for inactive queues (milliseconds)aware(default:true): Whether to isolate queues per collection
When to use:
- Most production scenarios
- Applications with multiple collections
- When you need better concurrency
How SmartExecutor Works
Queue Isolation
// These operations run in parallel (different collections)
await Promise.all([
db.users.add({ name: "Alice" }), // Queue: "users"
db.posts.add({ title: "Hello" }), // Queue: "posts"
db.comments.add({ text: "Nice!" }) // Queue: "comments"
]);
// These operations run sequentially (same collection)
await Promise.all([
db.users.add({ name: "Alice" }), // Queue: "users" - first
db.users.add({ name: "Bob" }), // Queue: "users" - second
db.users.find({}) // Queue: "users" - third
]);
Automatic Cleanup
SmartExecutor automatically removes idle queues after the TTL period:
const executor = new SmartExecutor(60000); // 1 minute TTL
// Operation creates a queue for "users"
await db.users.add({ name: "Alice" });
// Queue remains active for 1 minute
// If no operations on "users" for 1 minute, queue is removed
// Next operation on "users" creates a new queue
aware Mode
When aware = true, queues are isolated per collection:
const executor = new SmartExecutor(300000, true);
// "users" -> separate queue
// "posts" -> separate queue
// "comments" -> separate queue
When aware = false, all operations share a single queue:
const executor = new SmartExecutor(300000, false);
// All operations -> single shared queue
// Behaves like simple Executor but with TTL cleanup
Which default applies?
- A standalone
new SmartExecutor()usesaware = true(per-collection queues). ValtheraClasswithout an explicit executor createsnew SmartExecutor(undefined, false)- a single queue. It switches to per-collection queues duringinit()only if the adapter signalssmartExecutor = true(andexecutorAwareis not disabled).
Adapter Integration
Adapters can signal support for SmartExecutor:
class MyAdapter extends ActionsBase {
smartExecutor = true; // Signals support for per-collection queuing
// ... adapter methods
}
When an adapter has smartExecutor = true, ValtheraClass automatically enables aware mode on the SmartExecutor - but only when no explicit executor was passed to the constructor.
executorAware Option
By default, ValtheraClass sets executorAware: true in its options. This means:
- During
init(), if no explicitexecutorwas passed, the adapter signalssmartExecutor = true, and the default executor is aSmartExecutor, the executor'sawaremode is automatically enabled. - This allows per-collection queuing without manual configuration.
If you pass your own SmartExecutor, its constructed aware value is used; executorAware does not change it.
You can disable the automatic activation by setting executorAware: false:
const db = new ValtheraClass({
adapter: myAdapter,
executorAware: false // Prevent automatic aware mode activation
});
When to disable:
- When you want full control over executor behavior
- When debugging concurrency issues
- When your adapter signals smartExecutor but you prefer a single queue
Custom Executor
You can implement your own executor:
import { ExecutorInterface } from "@wxn0brp/db-core";
class MyCustomExecutor implements ExecutorInterface {
async addOp(func: Function, query?: any, collection?: string): Promise<any> {
// Your custom logic here
// Must return a promise that resolves with the operation result
return await new Promise((resolve, reject) => {
// Queue the operation
// Call func(query) when ready
func(query).then(resolve).catch(reject);
});
}
}
const db = new ValtheraClass({
adapter: myAdapter,
executor: new MyCustomExecutor()
});
Performance Considerations
Executor vs SmartExecutor
| Scenario | Executor | SmartExecutor |
|---|---|---|
| Single collection | Same | Same |
| Multiple collections | Sequential | Parallel |
| Memory usage | Low | Higher (multiple queues) |
| Concurrency | Low | High |
| Complexity | Simple | Moderate |