Skip to main content

Persistence Architecture

Hyperscape implements a robust persistence layer with write-ahead logging, transactional saves, and crash recovery to prevent data loss during server crashes or network failures.
Persistence code lives in packages/server/src/persistence/ and packages/shared/src/systems/shared/character/.

Architecture Overview

The persistence system uses multiple layers of protection:
  1. Transactional Saves — Equipment and bank operations use database transactions
  2. Immediate Persistence — Critical operations (item pickup/drop) save immediately
  3. Auto-Save — Periodic saves every 5 seconds for inventory/equipment
  4. Write-Ahead Logging — Phase 2 scaffolding for trade/bank crash recovery
  5. Unified Payloads — Single source of truth for player data loading

Transactional Saves

Equipment Persistence

Equipment saves use database transactions to prevent data loss during crashes:
Atomicity Guarantee: Either both delete and insert succeed, or both are rolled back. No partial state is possible.

Bank Persistence

Bank saves also use transactions for atomic updates:
Breaking Change: savePlayerBankComplete replaces separate savePlayerItems and savePlayerTabs calls. Use the unified method for atomic bank saves.

Auto-Save System

Reduced Save Intervals

Auto-save intervals were reduced from 30 seconds to 5 seconds to minimize data loss:
Impact:
  • Before: Up to 30 seconds of progress could be lost on crash
  • After: Maximum 5 seconds of progress lost
  • Performance: Negligible overhead (6x more frequent saves with minimal DB load)

Immediate Persistence

Critical operations persist immediately to prevent duplication or loss:

Item Pickup

Item Drop

Why Immediate? If the server crashes after an item is picked up but before the next auto-save, the item could be lost. Immediate persistence ensures the database is updated before the operation completes.

Unified PLAYER_JOINED Payload

Single Source of Truth Pattern

The PLAYER_JOINED event now includes equipment and inventory data loaded from the database before the event is emitted. This eliminates race conditions where multiple systems query the database independently.

System Integration

Systems now load from the event payload instead of querying the database:
Backwards Compatibility: If the payload doesn’t include equipment/inventory data (old server version), systems fall back to querying the database directly.

EventBus Async Handler Tracking

The EventBus now tracks pending async handlers to enable graceful shutdown:
Graceful Shutdown: Call eventBus.waitForPendingHandlers() before server shutdown to ensure all database saves complete.

Write-Ahead Logging (Phase 2)

PersistenceService

The PersistenceService provides write-ahead logging for critical operations. Status: Phase 2 scaffolding — not yet integrated into game systems.

Operations Log Table

The operations_log table stores write-ahead log entries:
Operation Types:
  • trade_complete — Trade finalization
  • bank_deposit — Bank deposit transaction
  • bank_withdraw — Bank withdrawal transaction
  • inventory_add — Inventory item addition
  • inventory_remove — Inventory item removal
  • equipment_change — Equipment slot change

WAL Pattern

The write-ahead logging pattern ensures durability:
Phase 2 Status: PersistenceService is scaffolding for future integration. It is not currently wired up to TradingSystem or BankSystem. The operations_log table exists but is not populated.

Auto-Save Configuration

Equipment System

Inventory System

Performance: 5-second auto-save has negligible overhead. Database writes are batched and use transactions for efficiency.

Immediate Persistence

When to Persist Immediately

Immediate persistence is used for operations that could cause duplication or loss:

Implementation


Database Schema

Operations Log

Magic Skill Columns

Magic skill support was added in migration 0027_messy_dorian_gray.sql:
Columns:
  • magicLevel — Magic skill level (default: 1)
  • magicXp — Magic skill XP (default: 0)
  • selectedSpell — Autocast spell ID (null = no autocast)

Crash Recovery

Equipment Loading

Equipment is loaded from the database during character selection and passed via the PLAYER_JOINED event:
Race Condition Fix: Previously, both character-selection.ts and EquipmentSystem would query the database independently, causing race conditions. Now, character-selection loads the data once and passes it via the event payload.

Inventory Loading

Inventory follows the same pattern:

Performance Considerations

Batched Writes

The PersistenceService uses batched writes for performance:
Trade-offs:
  • Latency: Maximum 50ms delay before write
  • Throughput: Up to 100 operations per batch
  • Durability: Operations are logged immediately (durability point), batching only affects completion marking

Cleanup

Old completed operations are cleaned up periodically:
Recommendation: Run cleanup daily via cron job or scheduled task to prevent table growth.

Migration Guide

Updating to Transactional Saves

If you have custom persistence code, update to use transactions: Before:
After:

Using Unified Payloads

Update systems to load from event payloads instead of querying the database: Before:
After: