Entry Processor
Entry Processor executes user-defined logic atomically on the server, solving the classic read-modify-write race condition. Instead of reading data, modifying it locally, and writing it back (risking conflicts), you send a processor function that runs where the data lives.
Atomic Operations
Read-modify-write in a single atomic operation on the server
Server-Side Execution
Logic runs in secure sandbox on the server
Built-in Processors
Common operations like INCREMENT, PUT_IF_ABSENT ready to use
The Problem
Without atomic operations, concurrent updates can silently lose data:
// WITHOUT Entry Processor - Race condition!
// Client A Client B
// read(key) → 10
// read(key) → 10
// write(key, 10 + 5)
// write(key, 10 + 3)
// Result: 13 (expected: 18) - One update lost!
// WITH Entry Processor - Atomic operations
// Client A Client B
// executeOnKey(add(5))
// executeOnKey(add(3))
// Server executes atomically:
// value = 10 → 15 → 18
// Result: 18 (correct!) Basic Usage
import { TopGunClient } from '@topgunbuild/client';
import { BuiltInProcessors } from '@topgunbuild/core';
const client = new TopGunClient({
serverUrl: 'ws://localhost:8080',
});
// Increment a counter atomically
const result = await client.executeOnKey(
'stats', // map name
'pageViews', // key
BuiltInProcessors.INCREMENT(1)
);
console.log('New value:', result.newValue); // 1
console.log('Success:', result.success); // true Built-in Processors
TopGun provides common processors out of the box:
import { BuiltInProcessors } from '@topgunbuild/core';
// INCREMENT - Add to numeric value
await client.executeOnKey('stats', 'views',
BuiltInProcessors.INCREMENT(5)
);
// DECREMENT_FLOOR - Subtract but never go below 0
const result = await client.executeOnKey('inventory', 'stock',
BuiltInProcessors.DECREMENT_FLOOR(1)
);
if (result.result?.wasFloored) {
console.log('Stock is now at zero!');
}
// PUT_IF_ABSENT - Set only if key doesn't exist
await client.executeOnKey('users', 'user-123',
BuiltInProcessors.PUT_IF_ABSENT({ name: 'Alice', role: 'user' })
);
// DELETE_IF_EQUALS - Delete only if value matches
await client.executeOnKey('sessions', 'session-abc',
BuiltInProcessors.DELETE_IF_EQUALS({ token: 'old-token' })
);
// ARRAY_PUSH - Append to array
await client.executeOnKey('posts', 'post-1',
BuiltInProcessors.ARRAY_PUSH('new-comment-id')
);
// SET_PROPERTY - Update nested property
await client.executeOnKey('users', 'user-123',
BuiltInProcessors.SET_PROPERTY('profile.avatar', 'new-url.jpg')
); Available Built-in Processors
| Processor | Description |
|---|---|
INCREMENT(delta) | Add to numeric value (default: 1) |
DECREMENT(delta) | Subtract from numeric value |
DECREMENT_FLOOR(delta) | Subtract but never go below 0, returns wasFloored |
MULTIPLY(factor) | Multiply numeric value |
PUT_IF_ABSENT(value) | Set only if key doesn’t exist |
REPLACE(newValue) | Replace existing value |
REPLACE_IF_EQUALS(expected, newValue) | Replace only if current value matches |
DELETE_IF_EQUALS(expected) | Delete only if value matches |
ARRAY_PUSH(item) | Append to array |
ARRAY_POP() | Remove last array element |
ARRAY_REMOVE(item) | Remove item from array |
SET_PROPERTY(path, value) | Update nested property |
DELETE_PROPERTY(path) | Delete nested property |
GET() | Read value without modification |
Custom Processors
For complex business logic, write custom processor code:
// Custom processor for complex logic
const result = await client.executeOnKey('inventory', 'product-abc', {
name: 'reserve_item',
code: `
if (!value || value.stock <= 0) {
return {
value,
result: { success: false, reason: 'out_of_stock' }
};
}
const newValue = {
...value,
stock: value.stock - 1,
reserved: [...(value.reserved || []), args.userId],
};
return {
value: newValue,
result: { success: true, remaining: newValue.stock }
};
`,
args: { userId: 'user-456' }
});
if (result.result?.success) {
console.log(`Reserved! ${result.result.remaining} left`);
} else {
console.log('Out of stock');
} Processor Code Structure
Your processor code receives three variables:
value- Current value at the key (orundefinedif not exists)key- The key being processedargs- Arguments passed from the client
Must return an object with:
value- New value to store (orundefinedto delete)result- Custom result to return to client
Batch Operations
Process multiple keys in one request:
// Execute on multiple keys at once
const results = await client.executeOnKeys(
'counters',
['views', 'clicks', 'shares'],
BuiltInProcessors.INCREMENT(1)
);
for (const [key, result] of results) {
if (result.success) {
console.log(`${key}: ${result.newValue}`);
}
} React Hook
The useEntryProcessor hook provides a reactive interface for React components.
import { useEntryProcessor } from '@topgunbuild/react';
import { BuiltInProcessors } from '@topgunbuild/core';
function LikeButton({ postId }) {
const { execute, executing, lastResult, error } = useEntryProcessor(
'likes',
{ name: 'increment', code: 'return { value: (value ?? 0) + 1, result: (value ?? 0) + 1 };' }
);
const handleLike = async () => {
const result = await execute(postId);
if (result.success) {
console.log('New count:', result.result);
}
};
return (
<button onClick={handleLike} disabled={executing}>
{executing ? '...' : 'Like'}
</button>
);
} Using Built-in Processors with React
import { useEntryProcessor } from '@topgunbuild/react';
import { BuiltInProcessors } from '@topgunbuild/core';
import { useMemo } from 'react';
function StockButton({ productId }) {
// Memoize processor definition
const processor = useMemo(
() => BuiltInProcessors.DECREMENT_FLOOR(1),
[]
);
const { execute, executing, lastResult } = useEntryProcessor(
'inventory',
processor
);
const handlePurchase = async () => {
const result = await execute(productId);
if (result.result?.wasFloored) {
alert('Out of stock!');
}
};
return (
<button onClick={handlePurchase} disabled={executing}>
{executing ? 'Processing...' : 'Buy Now'}
</button>
);
} Hook Return Values
| Property | Type | Description |
|---|---|---|
execute | (key: string, args?: unknown) => Promise<EntryProcessorResult> | Execute on single key |
executeMany | (keys: string[], args?: unknown) => Promise<Map<string, EntryProcessorResult>> | Execute on multiple keys |
executing | boolean | True while operation is in progress |
lastResult | EntryProcessorResult | null | Most recent execution result |
error | Error | null | Last error encountered |
reset | () => void | Clear lastResult and error |
Hook Options
| Option | Type | Default | Description |
|---|---|---|---|
retries | number | 0 | Number of retry attempts on failure |
retryDelayMs | number | 100 | Delay between retries (doubles with each retry) |
Use Cases
Inventory Management
// Reserve stock atomically - no overselling
const result = await client.executeOnKey('products', 'sku-123',
BuiltInProcessors.DECREMENT_FLOOR(1)
);
if (result.result?.wasFloored) {
throw new Error('Out of stock');
}
Rate Limiting
// Atomic increment with limit check
const result = await client.executeOnKey('rate-limits', `user:${userId}`, {
name: 'rate_check',
code: `
const count = (value ?? 0) + 1;
if (count > 100) {
return { value, result: { allowed: false, count } };
}
return { value: count, result: { allowed: true, count } };
`
});
if (!result.result?.allowed) {
throw new Error('Rate limit exceeded');
}
Optimistic Locking
// Optimistic locking with version check
const result = await client.executeOnKey('documents', 'doc-1', {
name: 'conditional_update',
code: `
if (!value || value.version !== args.expectedVersion) {
return {
value,
result: { updated: false, conflict: true }
};
}
return {
value: {
...value,
data: args.newData,
version: value.version + 1,
},
result: { updated: true, conflict: false },
};
`,
args: {
expectedVersion: 5,
newData: 'Updated content',
},
});
if (result.result?.conflict) {
console.log('Conflict detected, please refresh and retry');
} Game Scores
// Add score with bounds checking
await client.executeOnKey('leaderboard', 'player-123', {
name: 'add_score',
code: `
const current = value ?? { score: 0, highScore: 0 };
const newScore = current.score + args.points;
return {
value: {
score: newScore,
highScore: Math.max(current.highScore, newScore)
},
result: { newScore, isHighScore: newScore > current.highScore }
};
`,
args: { points: 100 }
});
Security
Sandboxed Execution: Processor code runs in an isolated sandbox using isolated-vm.
It has no access to Node.js APIs, filesystem, network, or globals. Only value, key, and args are available.
Security Features
| Feature | Protection |
|---|---|
| Memory Limit | 8MB per isolate prevents memory bombs |
| CPU Timeout | 100ms max execution prevents infinite loops |
| No I/O Access | No require, fs, fetch, XMLHttpRequest |
| No Globals | No eval, Function, process, global |
| Code Validation | Dangerous patterns are blocked before execution |
Need network access? Entry Processors are sandboxed and cannot call external APIs. For ML inference, webhooks, or external service integration, use Interceptors instead.
API Reference
client.executeOnKey()
executeOnKey<V, R>(
mapName: string,
key: string,
processor: EntryProcessorDef<V, R>
): Promise<EntryProcessorResult<R>>
client.executeOnKeys()
executeOnKeys<V, R>(
mapName: string,
keys: string[],
processor: EntryProcessorDef<V, R>
): Promise<Map<string, EntryProcessorResult<R>>>
EntryProcessorResult
interface EntryProcessorResult<R> {
success: boolean; // Whether operation succeeded
result?: R; // Custom result from processor
error?: string; // Error message if failed
newValue?: unknown; // New value after processing
}