Integration
9. Framework Integration (Tauri Example)
Section titled “9. Framework Integration (Tauri Example)”9.1 Rust Side
Section titled “9.1 Rust Side”use tauri::{ plugin::{Builder, TauriPlugin}, Manager, Runtime, State,};
pub fn init<R: Runtime>() -> TauriPlugin<R> { Builder::new("sync") .invoke_handler(tauri::generate_handler![ sync_enable, sync_disable, sync_create_invite, sync_join_invite, sync_push, sync_pull, sync_status, sync_last_cursor, ]) .setup(|app, api| { // Initialize sync state app.manage(SyncState::new(api.config())); Ok(()) }) .build()}
#[tauri::command]async fn sync_enable(state: State<'_, SyncState>) -> Result<(), String>;
#[tauri::command]async fn sync_create_invite(state: State<'_, SyncState>) -> Result<Invite, String>;
#[tauri::command]async fn sync_join_invite( state: State<'_, SyncState>, invite: String,) -> Result<(), String>;
#[tauri::command]async fn sync_push( state: State<'_, SyncState>, data: Vec<u8>,) -> Result<PushResult, String>;
#[tauri::command]async fn sync_pull( state: State<'_, SyncState>, after_cursor: Option<u64>,) -> Result<PullResult, String>;
#[tauri::command]fn sync_status(state: State<'_, SyncState>) -> ConnectionStatus;
#[tauri::command]fn sync_last_cursor(state: State<'_, SyncState>) -> u64;9.2 JavaScript API
Section titled “9.2 JavaScript API”// @0k-sync/tauri-plugin
export interface SyncPlugin { enable(): Promise<void>; disable(): Promise<void>; createInvite(): Promise<Invite>; joinInvite(invite: string): Promise<void>; push(data: Uint8Array): Promise<PushResult>; pull(afterCursor?: number): Promise<PullResult>; status(): ConnectionStatus; lastCursor(): number;
// Event listeners on(event: 'connected', handler: (info: ConnectionInfo) => void): void; on(event: 'disconnected', handler: (reason: string) => void): void; on(event: 'blob-available', handler: (blob: BlobInfo) => void): void; on(event: 'error', handler: (error: SyncError) => void): void;}
export interface Invite { qrCode: string; // Data URL for QR image shortCode: string; // XXXX-XXXX-XXXX-XXXX expiresAt: number; // Unix timestamp}
export interface PushResult { blobId: string; cursor: number;}
export interface PullResult { blobs: Blob[]; hasMore: boolean; maxCursor: number;}
export interface Blob { id: string; cursor: number; sender: string; data: Uint8Array; timestamp: number;}9.3 Usage Example
Section titled “9.3 Usage Example”import { sync } from '@0k-sync/tauri-plugin';
// Initialize syncawait sync.enable();
// Create invite for new deviceconst invite = await sync.createInvite();showQRCode(invite.qrCode);
// Or join existing sync groupawait sync.joinInvite('XXXX-XXXX-XXXX-XXXX');
// Push dataconst blob = new TextEncoder().encode(JSON.stringify(myData));const result = await sync.push(blob);console.log(`Synced at cursor ${result.cursor}`);
// Pull dataconst { blobs, hasMore } = await sync.pull(lastKnownCursor);for (const blob of blobs) { const data = JSON.parse(new TextDecoder().decode(blob.data)); await mergeIntoLocalDb(data);}
// Listen for real-time updatessync.on('blob-available', async (info) => { const { blobs } = await sync.pull(info.cursor - 1); // Process new blob});10. Mobile Lifecycle Considerations
Section titled “10. Mobile Lifecycle Considerations”10.1 The “Mobile Exit” Problem
Section titled “10.1 The “Mobile Exit” Problem”⚠️ Critical Reality: On iOS and modern Android, background network connections (including QUIC/iroh) are killed within ~30 seconds of the app being backgrounded. You cannot rely on persistent connections for sync.
iOS applicationWillTerminate does NOT guarantee enough execution time to:
- Establish an iroh connection
- Perform a Noise handshake
- Upload a blob
The OS watchdog will kill your app before completing.
Design Assumption: Data generated while offline or just before closing will not sync until the next app launch. The UI must reflect this.
10.2 Sync Status Indicators
Section titled “10.2 Sync Status Indicators”The UI must show sync state to avoid user confusion:
| Indicator | Meaning | User Action |
|---|---|---|
| ☁️✓ | Synced to relay | None needed |
| ☁️⏳ | Pending sync (will sync on next launch) | None; automatic |
| ☁️✗ | Sync failed (will retry) | Check connection |
| 📴 | Offline (changes saved locally) | Restore connection |
10.3 Stranded Commits
Section titled “10.3 Stranded Commits”Definition: Local changes that exist only on the device and haven’t synced to the relay.
Rules:
- Never block UI trying to sync on exit — you will annoy users or trigger OS watchdog
- Assume stranded commits — local changes may not sync until next launch
- Local-first always — save to local DB first, sync is opportunistic
10.4 Optimistic Local Updates Pattern
Section titled “10.4 Optimistic Local Updates Pattern”The UI should never wait for sync to complete before showing changes:
// In your local-first appasync fn save_transaction(tx: Transaction, sync: &SyncClient, db: &LocalDb) { // 1. Save locally FIRST (instant, always succeeds) db.insert(&tx).await?;
// 2. Update UI immediately (optimistic) emit_to_frontend("transaction_saved", &tx);
// 3. Queue for sync (fire-and-forget) let blob = serialize(&tx); match sync.push(&blob).await { Ok(result) => { // Mark as synced in local DB db.mark_synced(tx.id, result.cursor).await?; emit_to_frontend("sync_status", SyncStatus::Synced); } Err(_) => { // Will retry on next app launch emit_to_frontend("sync_status", SyncStatus::Pending); } }}Key Principle: The local database is the source of truth. Sync is opportunistic. Users should never lose data because sync failed.
10.5 Sync Trigger Points
Section titled “10.5 Sync Trigger Points”Sync should be triggered on these application lifecycle events:
// Pseudocode - adapt to your framework (Tauri, Electron, React Native, etc.)
// On app startupasync fn on_app_start(sync_client: &SyncClient) { // Initial sync on app start sync_client.connect_and_pull().await;}
// On app resumed (came to foreground)async fn on_app_resume(sync_client: &SyncClient) { // App came to foreground - pull new data, push pending sync_client.sync_pending_then_pull().await;}
// On app closingasync fn on_app_close(sync_client: &SyncClient) { // App closing - DO NOT block waiting for sync! // Just mark pending items; they'll sync next launch sync_client.mark_pending_for_next_launch();
// Fire-and-forget attempt (may not complete) let _ = tokio::time::timeout( std::time::Duration::from_millis(500), sync_client.quick_flush() ).await;}10.6 Sync Strategy Summary
Section titled “10.6 Sync Strategy Summary”| Event | Action | Blocking? |
|---|---|---|
| App launch | Connect + push pending + pull new | No (background) |
| App resume (foreground) | Push pending + pull new | No (background) |
| App pause (background) | Mark state, fire-and-forget flush | No (500ms max) |
| User action (save, etc.) | Save local, queue for sync, attempt push | No |
| Manual “Sync Now” button | Full sync cycle | Yes (with spinner) |
10.7 What We Don’t Support (Yet)
Section titled “10.7 What We Don’t Support (Yet)”| Feature | Reason | Future Possibility |
|---|---|---|
| Background sync | iOS/Android kill background connections | Push notifications |
| Push notifications for new data | Requires APNS/FCM integration | Optional plugin |
| Always-on sync | Not possible on mobile without OS support | None |
| Guaranteed sync on exit | OS doesn’t allow it | None |