DocsGuidesEntry Processor

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:

Race Condition Problem
// 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

Atomic Counter Increment
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:

Built-in Processors
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

ProcessorDescription
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 - Inventory Reservation
// 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 (or undefined if not exists)
  • key - The key being processed
  • args - Arguments passed from the client

Must return an object with:

  • value - New value to store (or undefined to delete)
  • result - Custom result to return to client

Batch Operations

Process multiple keys in one request:

Batch Operations
// 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.

components/LikeButton.tsx
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

components/StockButton.tsx
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

PropertyTypeDescription
execute(key: string, args?: unknown) => Promise<EntryProcessorResult>Execute on single key
executeMany(keys: string[], args?: unknown) => Promise<Map<string, EntryProcessorResult>>Execute on multiple keys
executingbooleanTrue while operation is in progress
lastResultEntryProcessorResult | nullMost recent execution result
errorError | nullLast error encountered
reset() => voidClear lastResult and error

Hook Options

OptionTypeDefaultDescription
retriesnumber0Number of retry attempts on failure
retryDelayMsnumber100Delay 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

Version-based Conditional Update
// 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

FeatureProtection
Memory Limit8MB per isolate prevents memory bombs
CPU Timeout100ms max execution prevents infinite loops
No I/O AccessNo require, fs, fetch, XMLHttpRequest
No GlobalsNo eval, Function, process, global
Code ValidationDangerous 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
}