Skip to main content
Version: 2.6

Polling Reaktors

Overview​

Polling reaktors are specialized reaktors that execute on a schedule and can signal whether more work exists through a boolean return value. This enables efficient batch processing and queue-based workflows where the reaktor continues processing until all work is completed.

When to Use Polling Reaktors​

Use polling reaktors when you need to:

  • Process queues or batches: Handle work items one at a time until the queue is empty
  • Scheduled background tasks: Execute periodic tasks like data synchronization, cleanup, or monitoring
  • Event-driven polling: Check for new events/messages and process them immediately
  • Continuous processing: Process work as fast as possible when available, with periodic checks when idle

Comparison with Standard Reaktors​

AspectStandard ReaktorPolling Reaktor
ExecutionOn-demand (API call, chat message, email)Scheduled (interval-based)
Return ValueAny typeBoolean (true = more work, false = wait)
ControlExternal triggersREST API (start/stop/status)
StateStatelessPersistent state (running/stopped)
Use CaseRequest/response workflowsBackground processing, queues

Registration API​

import { registerPollingReaktor } from '@operaide/aktor';

registerPollingReaktor({
reaktorDefinitionId: string, // Unique identifier
interval: string, // '2m', '5s', '1h', '30ms', '1d'
autoStart?: boolean, // Default: false
label?: string, // Optional UI label
description?: string, // Optional description
createReaktor: () => Aktor<boolean>
});

Configuration Options​

reaktorDefinitionId (required)​

Unique identifier for this polling reaktor. Used in REST API endpoints and internal tracking.

reaktorDefinitionId: 'coffee-queue-processor'

interval (required)​

Execution interval as a human-readable duration string:

  • '30ms' - 30 milliseconds
  • '5s' - 5 seconds
  • '2m' - 2 minutes
  • '1h' - 1 hour
  • '1d' - 1 day

Note: There is a minimum interval of 3 seconds enforced by the distributed timer system for server coordination.

autoStart (optional, default: false)​

Whether to start the reaktor automatically on server startup. Useful for critical background tasks that should always be running.

autoStart: true  // Starts on server init

label and description (optional)​

Human-readable metadata displayed in UIs and documentation.

label: 'Coffee Queue Processor',
description: 'Processes coffee orders from the queue one at a time'

createReaktor (required)​

Factory function that returns an Aktor<boolean>. The boolean value controls execution flow.

Boolean Return Contract​

The reaktor's return value controls its execution behavior:

true - More Work Exists​

  • Reaktor executes again immediately (no interval wait)
  • Continues until false is returned
  • Enables fast batch processing

false - No Work Available​

  • Reaktor waits for the configured interval
  • Next execution happens after interval expires
  • Saves resources when queue is empty

Example Pattern​

createReaktor() {
return aktorIfElse({
if_: aktorCheckQueue({}), // Check if work exists
then_: aktorProcessItem({}), // Process & return true/false
else_: aktorConst(false) // No work, wait for interval
});
}

Implementing Polling Reaktors​

Pattern 1: Queue Processing​

Process items from a queue until empty:

import {
registerPollingReaktor,
createAktorFunctionAsync,
aktorIfElse,
aktorConst
} from '@operaide/aktor';

// Check if queue has items
const aktorHasOrders = createAktorFunction('aktorHasOrders', hasOrders);
function hasOrders() {
return queue.length > 0;
}

// Process one order (async operation)
const aktorProcessOrder = createAktorFunctionAsync('aktorProcessOrder', processOrder);
async function processOrder() {
if (queue.length === 0) {
return false;
}

const order = queue.shift();
await processOrderAsync(order); // Your async logic

return queue.length > 0; // More work?
}

registerPollingReaktor({
reaktorDefinitionId: 'order-processor',
interval: '5s',
createReaktor() {
return aktorIfElse({
if_: aktorHasOrders({}),
then_: aktorProcessOrder({}), // Returns true if more orders
else_: aktorConst(false) // Queue empty, wait 5s
});
}
});

Pattern 2: Batch Processing with Settings​

Use aktorSetting for configurable batch sizes:

import { aktorSetting } from '@operaide/aktor';
import { z } from 'zod';

const aktorBatchProcessor = createAktorFunctionAsync('aktorBatchProcessor', processBatch);
async function processBatch({ batchSize }: { batchSize: number }) {
const items = await fetchNextBatch(batchSize);

if (items.length === 0) {
return false; // No more items
}

await Promise.all(items.map(processItem));

return items.length === batchSize; // True if full batch (likely more items)
}

registerPollingReaktor({
reaktorDefinitionId: 'batch-worker',
interval: '1m',
createReaktor() {
const batchSize = aktorSetting(
z.number().min(1).max(100).describe('Items per batch'),
10
);

return aktorBatchProcessor({ batchSize });
}
});

Pattern 3: Conditional Processing​

Use aktorIfElse to branch based on conditions:

const aktorCheckAndProcess = createAktorFunctionAsync('aktorCheckAndProcess', checkAndProcess);
async function checkAndProcess() {
const hasNewEmails = await checkForNewEmails();

if (!hasNewEmails) {
return false; // No emails, wait for interval
}

const processed = await processEmails();
return processed > 0; // Continue if we processed emails
}

registerPollingReaktor({
reaktorDefinitionId: 'email-processor',
interval: '30s',
createReaktor() {
return aktorCheckAndProcess({});
}
});

REST API Control​

For each registered polling reaktor, the following endpoints are automatically generated:

Start a Polling Reaktor​

POST /api/v1/polling/{reaktorId}/start

Example:

curl -X POST http://localhost:8811/api/v1/polling/coffee-queue-processor/start \
-H "X-Operaide-Token: your-auth-token"

Response:

{
"message": "Polling reaktor \"coffee-queue-processor\" started successfully"
}

Stop a Polling Reaktor​

POST /api/v1/polling/{reaktorId}/stop

Example:

curl -X POST http://localhost:8811/api/v1/polling/coffee-queue-processor/stop \
-H "X-Operaide-Token: your-auth-token"

Response:

{
"message": "Polling reaktor \"coffee-queue-processor\" stopped successfully"
}

Get Reaktor Status​

GET /api/v1/polling/{reaktorId}/status

Example:

curl http://localhost:8811/api/v1/polling/coffee-queue-processor/status \
-H "X-Operaide-Token: your-auth-token"

Response:

{
"state": "running",
"interval": "5s",
"lastRun": "2024-01-15T10:30:00Z",
"nextRun": "2024-01-15T10:30:05Z"
}

State Persistence​

Polling reaktor state is persisted in MongoDB and survives server restarts.

State Fields​

  • state: "running" or "stopped"
  • interval: Configured interval string
  • lastRun: ISO timestamp of last execution
  • nextRun: ISO timestamp of next scheduled execution
  • updatedAt: Last state change timestamp

Restart Behavior​

With autoStart: true​

  • Reaktor automatically restarts if it was running before server restart
  • Preserves the running state across deployments

With autoStart: false (default)​

  • Reaktor state is reset to "stopped" on server restart
  • Must be manually started via REST API
  • Prevents unexpected execution after deployment

This ensures non-critical polling reaktors don't automatically resume after server restart, giving operators control over when background processing begins.

Architecture and Implementation​

Dependency Injection Pattern​

Polling reaktors follow the same dependency injection pattern as registerReaktorDefinition:

@operaide/aktor (Pure TypeScript)
├─ PollingReaktor.ts
│ └─ registerPollingReaktor() → calls _registerPollingReaktor
└─ RegisterPollingReaktorPrivate.ts
├─ _registerPollingReaktor (no-op by default)
└─ setRegisterPollingReaktorFunction() (for injection)

Meteor Extension (/imports/extensions/polling-reaktor)
└─ registerInjector.ts
└─ Injects Meteor implementation into @operaide/aktor

Benefits​

  • Clean Separation: @operaide/aktor has zero Meteor dependencies
  • Testability: Can mock the implementation for testing
  • Flexibility: Other frameworks can inject custom implementations
  • Consistency: Same pattern as standard reaktors

Layered Architecture​

  1. Types Layer - TypeScript types and Zod schemas
  2. Storage Layer - MongoDB collection for state persistence
  3. Registry Layer - In-memory registration of polling reaktors
  4. Execution Layer - Scheduling using distributed timers
  5. API Layer - REST endpoints for control

Best Practices​

1. Use Async Functions Correctly​

Always use createAktorFunctionAsync for async functions:

// ✅ CORRECT
const aktorProcess = createAktorFunctionAsync('aktorProcess', processAsync);
async function processAsync() {
await someAsyncOperation();
return hasMoreWork;
}

// ❌ WRONG
const aktorProcess = createAktorFunction('aktorProcess', processAsync);

2. Handle Empty Queues Gracefully​

Always check for empty queues before processing:

async function processOrder() {
if (queue.length === 0) {
return false; // Stop processing
}

const order = queue.shift();
await process(order);

return queue.length > 0; // Continue if more items
}

3. Set Appropriate Intervals​

Choose intervals based on your use case:

  • Fast processing: '5s' to '30s' - for responsive queue processing
  • Periodic tasks: '5m' to '1h' - for scheduled maintenance
  • Background sync: '1h' to '1d' - for low-priority sync tasks

4. Use autoStart Carefully​

Only set autoStart: true for critical background tasks that must always run. For most cases, manual start via API provides better control.

5. Monitor Execution​

Use the status endpoint to monitor reaktor health:

# Check status
curl http://localhost:8811/api/v1/polling/my-reaktor/status

# If stuck, restart
curl -X POST http://localhost:8811/api/v1/polling/my-reaktor/stop
curl -X POST http://localhost:8811/api/v1/polling/my-reaktor/start

6. Add Delays for Visibility (Demo Only)​

For demo/testing purposes, add artificial delays to make processing visible:

async function processOrder() {
const order = queue.shift();

// Add delay for demo visibility (remove in production!)
await new Promise(resolve => setTimeout(resolve, 2000));

await actualProcessing(order);
return queue.length > 0;
}

Advanced Patterns​

Combining Multiple Aktors​

Use aktorParallel to execute multiple checks:

import { aktorParallel } from '@operaide/aktor';

const aktorCheckMultipleSources = createAktorFunctionAsync(
'aktorCheckMultipleSources',
checkMultipleSources
);

async function checkMultipleSources({ sources }: { sources: string[] }) {
const checks = await Promise.all(sources.map(checkSource));
return checks.some(hasWork => hasWork);
}

createReaktor() {
const sources = aktorConst(['email', 'api', 'database']);
return aktorCheckMultipleSources({ sources });
}

Error Handling​

Wrap processing in try-catch to prevent reaktor crashes:

async function processWithErrorHandling() {
try {
const item = queue.shift();
await processItem(item);
return queue.length > 0;
} catch (error) {
logger.error('Processing failed:', error);
return false; // Stop on error
}
}

Rate Limiting​

Implement rate limiting within the reaktor:

let lastProcessTime = 0;
const MIN_INTERVAL_MS = 1000;

async function processWithRateLimit() {
const now = Date.now();
const timeSinceLastProcess = now - lastProcessTime;

if (timeSinceLastProcess < MIN_INTERVAL_MS) {
await new Promise(resolve =>
setTimeout(resolve, MIN_INTERVAL_MS - timeSinceLastProcess)
);
}

lastProcessTime = Date.now();
return await actualProcessing();
}

Troubleshooting​

Reaktor Not Starting​

  1. Check server logs for errors
  2. Verify reaktor is registered: GET /api/v1/polling
  3. Check configuration syntax
  4. Ensure createReaktor() returns Aktor<boolean>

Reaktor Stops Unexpectedly​

  1. Check for errors in reaktor execution
  2. Verify async functions use createAktorFunctionAsync
  3. Check if reaktor returned false (expected behavior)
  4. Review server logs for exceptions

State Mismatch After Restart​

If the UI shows "running" but reaktor isn't executing:

  1. Server restart resets non-autoStart reaktors to "stopped"
  2. Click "Stop" then "Start" to resynchronize
  3. Or set autoStart: true if it should survive restarts

See Also​