| Crates.io | weapon |
| lib.rs | weapon |
| version | 0.1.1 |
| created_at | 2025-08-04 06:19:03.260167+00 |
| updated_at | 2025-09-27 01:57:27.79998+00 |
| description | Cross-device sync engine |
| homepage | |
| repository | |
| max_upload_size | |
| id | 1780384 |
| size | 164,332 |
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.
Weapon uses an event-sourcing architecture where:
Local-First Architecture
Seamless Authentication Transition
Real-Time Cross-Device Sync
Event Sourcing
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.
Events are the atomic units of change in Weapon. Each event:
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>;
}
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;
}
Weapon supports multiple storage backends:
Here's how Weapon is used in Yap.Town for managing language learning state:
#[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,
})
}
}
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,
}
}
}
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);
}
}
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>;
}
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);
}
};
Weapon implements a simple synchronization strategy:
The sync protocol ensures:
Weapon is currently in active development and used in production by Yap.Town. While functional, the API may evolve significantly.