weapon

Crates.ioweapon
lib.rsweapon
version0.1.1
created_at2025-08-04 06:19:03.260167+00
updated_at2025-09-27 01:57:27.79998+00
descriptionCross-device sync engine
homepage
repository
max_upload_size
id1780384
size164,332
Andre Popovitch (anchpop)

documentation

README

Weapon - A Local-First Event Sourcing & Cross-Device Sync Engine

Weapon is a Rust library that enables local-first applications with cross-device synchronization. It implements event sourcing patterns with support for offline usage, real-time sync, and multi-device collaboration. It is designed primarily to be compiled to WASM and used with React applications. That said, isn't react-specific in any way and would probably work in a Dioxus app (or similar) as well. I made it for Yap.Town, a language learning app I work on sometimes.

Core Concept

Weapon uses an event-sourcing architecture where:

  • User actions generate "events" with unique IDs and timestamps
  • Application state is derived from "replaying" the chronological sequence of events
  • Events are stored locally first (using OPFS in browsers)
  • Synchronization simply merges events from all devices

Key Features

  1. Local-First Architecture

    • Users can use your app without logging in
    • All data is stored locally using browser storage (OPFS)
    • Works fully offline with zero network dependency (great for PWAs!)
  2. Seamless Authentication Transition

    • When users log in, their local data automatically syncs to the cloud
    • Logged-out user data gets imported into their account
    • No data loss during authentication state changes
  3. Real-Time Cross-Device Sync

    • Changes sync instantly across all devices
    • Any postgres server can be used for cloud persistence
    • With supabase, supports real-time subscriptions for lower-latency sync
    • Supports both push (real-time) and pull (periodic) synchronization
  4. Event Sourcing

    • Complete audit trail of all changes
    • Time-travel debugging capabilities
    • Conflict-free merging of concurrent edits
    • Ability to replay events to rebuild state

Event sourcing enables fixing bugs retroactively. When you fix a bug in your state computation logic, users will replay all historical events through the corrected code to regenerate a bug-free state. This effectively "rewrites history" as if the bug never existed.

For example, in a budgeting app, if you discover floating-point rounding errors and switch to fixed-precision arithmetic, replaying all events will recalculate every transaction with the correct precision, fixing all historical calculation errors automatically.

Architecture

Event Model

Events are the atomic units of change in Weapon. Each event:

  • Has a unique timestamp and device-specific index
  • Is immutable once created
  • Can be versioned for backward compatibility
  • Is serializable to JSON for storage/transmission
pub trait Event: Sized + PartialOrd + Ord + Clone + Eq {
    fn to_json(&self) -> Result<serde_json::Value, serde_json::Error>;
    fn from_json(json: &serde_json::Value) -> Result<Self, serde_json::Error>;
}

State Management

Application state is computed by applying events in chronological order:

pub trait PartialAppState: Sized {
    type Event: Event;
    type Partial: Sized;
    
    // Process events incrementally
    fn process_event(partial: Self::Partial, event: &Timestamped<Self::Event>) -> Self::Partial;
    
    // Compute derived state once after all events
    fn finalize(partial: Self::Partial) -> Self;
}

Storage Layers

Weapon supports multiple storage backends:

  • OPFS (Origin Private File System) - Browser storage
  • Supabase - Cloud persistence and sync
  • Memory - For testing and temporary state

Real-World Usage Example

Here's how Weapon is used in Yap.Town for managing language learning state:

1. Define Your Events

#[derive(Clone, Debug, Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq)]
pub enum DeckEvent {
    CardReviewed { 
        card_id: String, 
        rating: u8 
    },
    CardAdded { 
        card_id: String, 
        content: CardContent 
    },
    SettingChanged { 
        key: String, 
        value: serde_json::Value 
    },
}

// Version your events for future compatibility
pub enum VersionedDeckEvent {
    V1(DeckEvent),
}

impl Event for DeckEvent {
    fn to_json(&self) -> Result<serde_json::Value, serde_json::Error> {
        let versioned = VersionedDeckEvent::V1(self.clone());
        serde_json::to_value(versioned)
    }
    
    fn from_json(json: &serde_json::Value) -> Result<Self, serde_json::Error> {
        let versioned: VersionedDeckEvent = serde_json::from_value(json.clone())?;
        Ok(match versioned {
            VersionedDeckEvent::V1(event) => event,
        })
    }
}

2. Define Your State

pub struct DeckState {
    cards: HashMap<String, Card>,
    settings: HashMap<String, serde_json::Value>,
    // Derived state (computed in finalize)
    due_cards: Vec<String>,
    statistics: DeckStatistics,
}

impl PartialAppState for DeckState {
    type Event = DeckEvent;
    type Partial = PartialDeckState;
    
    fn process_event(mut partial: Self::Partial, event: &Timestamped<DeckEvent>) -> Self::Partial {
        match &event.event {
            DeckEvent::CardReviewed { card_id, rating } => {
                // Update card with review
                partial.update_card_review(card_id, *rating, event.timestamp);
            }
            DeckEvent::CardAdded { card_id, content } => {
                partial.cards.insert(card_id.clone(), Card::new(content.clone()));
            }
            DeckEvent::SettingChanged { key, value } => {
                partial.settings.insert(key.clone(), value.clone());
            }
        }
        partial
    }
    
    fn finalize(partial: Self::Partial) -> Self {
        // Compute derived state like due cards and statistics
        let due_cards = partial.compute_due_cards();
        let statistics = partial.compute_statistics();
        
        DeckState {
            cards: partial.cards,
            settings: partial.settings,
            due_cards,
            statistics,
        }
    }
}

3. Initialize Weapon (Rust/WASM)

use weapon::data_model::{EventStore, EventType};

pub struct WeaponInstance {
    store: RefCell<EventStore<String, String>>,
    device_id: String,
    user_id: Option<String>,
}

impl WeaponInstance {
    pub async fn new(user_id: Option<String>) -> Result<Self, Error> {
        // Get or create device ID
        let device_id = get_or_create_device_id(&user_id).await?;
        
        // Initialize event store
        let mut store = EventStore::default();
        
        // Register sync callback for when events change
        store.register_listener(move |listener_id, stream_id| {
            // Trigger sync with cloud
            sync_with_supabase(stream_id).await;
        });
        
        Ok(Self {
            store: RefCell::new(store),
            device_id,
            user_id,
        })
    }
    
    pub fn add_event(&self, stream_id: String, event: DeckEvent) {
        let mut store = self.store.borrow_mut();
        let stream = store.get_or_insert_default::<EventType<DeckEvent>>(
            stream_id, 
            None
        );
        stream.add_event(event);
    }
}

4. React Integration

import { Weapon } from 'weapon-wasm';

function WeaponProvider({ userId, children }) {
    const [weapon, setWeapon] = useState(null);
    
    useEffect(() => {
        async function init() {
            // Initialize Weapon with sync callback
            const weaponInstance = await new Weapon(
                userId,
                async (listenerId, streamId) => {
                    // Sync when events change
                    await weaponInstance.sync(streamId, accessToken);
                }
            );
            setWeapon(weaponInstance);
        }
        init();
    }, [userId]);
    
    // Subscribe to stream changes
    useEffect(() => {
        if (!weapon) return;
        
        const unsubscribe = weapon.subscribe_to_stream('deck_events', () => {
            // React to changes
            setDeckState(weapon.get_deck_state());
        });
        
        return () => weapon.unsubscribe(unsubscribe);
    }, [weapon]);
    
    return (
        <WeaponContext.Provider value={weapon}>
            {children}
        </WeaponContext.Provider>
    );
}

// Usage in components
function DeckComponent() {
    const weapon = useWeapon();
    
    const handleCardReview = (cardId, rating) => {
        // Add event - automatically syncs
        weapon.add_deck_event({
            type: 'CardReviewed',
            card_id: cardId,
            rating: rating
        });
    };
    
    return <div>...</div>;
}

5. Cross-Tab Synchronization

Weapon supports synchronization between browser tabs using BroadcastChannel:

// Automatically handled by Weapon - tabs notify each other of changes
const channel = new BroadcastChannel('weapon-opfs-sync');

channel.onmessage = (event) => {
    if (event.data?.type === 'opfs-written') {
        // Reload affected stream from local storage
        weapon.load_from_local_storage(event.data.stream_id);
    }
};

Sync Strategy

Weapon implements a simple synchronization strategy:

  1. Event Generation: User actions create timestamped events
  2. Device Identification: Each device gets a unique ID
  3. Local Storage: Events are immediately persisted locally
  4. Cloud Sync: Events sync to cloud when online
  5. Conflict Resolution: Events merge chronologically by timestamp
  6. Real-time Updates: Changes propagate instantly via WebSockets

The sync protocol ensures:

  • No data loss during offline periods
  • Eventual consistency across all devices
  • Minimal sync overhead (only new events transfer)
  • Automatic conflict resolution via timestamps

Benefits

  • Instant UI Response: No network latency for user actions
  • Offline Capable: Full functionality without internet
  • Cross-Device Sync: Seamless experience across devices
  • Data Portability: Export/import entire event history
  • Time Travel: Replay events to any point in time
  • Audit Trail: Complete history of all changes
  • Conflict-Free: Automatic merging of concurrent edits

Status

Weapon is currently in active development and used in production by Yap.Town. While functional, the API may evolve significantly.

Commit count: 0

cargo fmt