sayance-matrix-native-module
Version:
Native Matrix client module for Expo using Rust SDK
323 lines (268 loc) • 8.67 kB
Markdown
# Native-Side Polling Implementation
## Problem Solved
The original implementation relied on Rust async tasks in a static library, which caused:
- ❌ No callbacks reaching JavaScript
- ❌ No visible logs from Rust
- ❌ Sync service not working properly
- ❌ Events not being delivered
## Solution: Native-Side Polling
We moved the **event loop management** from Rust to the native iOS/Android sides, creating a reliable polling system.
## Architecture Changes
### Before (Broken)
```
Rust Static Library → Async Tasks → Callbacks → JavaScript
❌ Async tasks don't run properly in static library
```
### After (Working)
```
iOS/Android Timer → Rust Polling Functions → Native Events → JavaScript
✅ Platform timers ensure reliable execution
```
## Implementation Details
### 1. Rust Changes (`rust/src/lib.rs`)
**Added New Functions:**
- `start_sync_non_blocking()` - Starts sync without blocking
- `poll_sync_updates()` - Returns pending events and clears queue
- `get_current_sync_state()` - Returns current sync status
- `has_pending_events()` - Quick check for pending events
- `get_poll_stats()` - Polling statistics and diagnostics
**Added Event Queue:**
```rust
static PENDING_EVENTS: Lazy<Arc<Mutex<Vec<MatrixEvent>>>> =
Lazy::new(|| Arc::new(Mutex::new(Vec::new())));
fn queue_pending_event(event: MatrixEvent) {
// Events are queued here for polling
}
```
**Comprehensive Logging:**
```rust
tracing::info!("🚨 STARTING NON-BLOCKING MATRIX SYNC SERVICE 🚨");
eprintln!("🚨 STARTING NON-BLOCKING MATRIX SYNC SERVICE 🚨");
```
### 2. iOS Implementation (`ios/MatrixModule.swift`)
**Timer-Based Polling:**
```swift
private func startPolling() {
syncTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.pollForUpdates()
}
}
private func pollForUpdates() {
let events = try pollSyncUpdates()
for event in events {
processMatrixEvent(event)
}
}
```
**Event Processing:**
```swift
private func processMatrixEvent(_ event: MatrixEvent) {
switch event {
case .syncUpdate(let state):
self.sendEvent("onSyncUpdate", [...])
case .timelineEvent(let roomId, let event):
self.sendEvent("onTimelineEvent", [...])
// ... other event types
}
}
```
### 3. Android Implementation (`android/.../MatrixModule.kt`)
**Handler-Based Polling:**
```kotlin
private fun startPolling() {
syncHandler = Handler(Looper.getMainLooper())
syncRunnable = object : Runnable {
override fun run() {
pollForUpdates()
syncHandler?.postDelayed(this, 1000)
}
}
syncHandler?.post(syncRunnable!!)
}
```
**Event Processing:**
```kotlin
private fun processMatrixEvent(event: MatrixEvent) {
when (event) {
is MatrixEvent.SyncUpdate -> {
sendEvent("onSyncUpdate", mapOf(...))
}
is MatrixEvent.TimelineEvent -> {
sendEvent("onTimelineEvent", mapOf(...))
}
// ... other event types
}
}
```
### 4. TypeScript Interface (`src/index.ts`)
**Enhanced Logging:**
```typescript
async startSync(): Promise<void> {
console.log('🚨 [JS] Starting Matrix sync...');
try {
await MatrixModule.startSync();
console.log('✅ [JS] Matrix sync started successfully');
} catch (error) {
console.error('❌ [JS] Failed to start Matrix sync:', error);
throw error;
}
}
```
**New Functions:**
```typescript
async getPollStats(): Promise<any> {
const stats = await MatrixModule.getPollStats();
console.log('📊 [JS] Poll stats:', stats);
return stats;
}
```
## Logging Strategy
### Log Levels and Emojis
- 🚨 **Critical operations** (start/stop sync)
- 🔍 **Polling activity** (every poll cycle)
- 📊 **Statistics and diagnostics**
- 💬 **Message events**
- 📨 **Room invites**
- 👤 **Membership changes**
- ✅ **Success operations**
- ❌ **Errors and failures**
- ⚠️ **Warnings**
### Multi-Platform Logging
- **Rust**: `tracing::info!()` + `eprintln!()` for immediate visibility
- **iOS**: `NSLog()` for Xcode console
- **Android**: `Log.d()` for adb logcat
- **JavaScript**: `console.log()` for Metro/Hermes
## Testing Instructions
### 1. Build the Module
```bash
# Full build
./build.sh
# Or step by step
cd rust && cargo build --release
cd ../ios && ./build.sh # macOS only
cd ../android && ./gradlew build
cd .. && npm run build
```
### 2. Run Tests
```bash
# Test module availability
node test_polling.js
# Test in Expo app
expo run:ios
# or
expo run:android
```
### 3. Monitor Logs
**iOS (Xcode Console):**
```
🚨 [iOS] Starting sync with polling...
🔍 [iOS] Polling for updates...
📊 [iOS] Polled 2 events
💬 [iOS] Timeline Event - Room: !abc:matrix.org
```
**Android (adb logcat):**
```bash
adb logcat | grep MatrixModule
```
Output:
```
🚨 [Android] Starting sync with polling...
🔍 [Android] Polling for updates...
📊 [Android] Polled 2 events
💬 [Android] Timeline Event - Room: !abc:matrix.org
```
**JavaScript (Metro):**
```
🚨 [JS] Starting Matrix sync...
✅ [JS] Matrix sync started successfully
📊 [JS] Poll stats: {pending_events: 0, ...}
```
## Performance Characteristics
### Polling Frequency
- **Default**: 1 second intervals
- **Configurable**: Can be adjusted per platform
- **Efficient**: Only polls when events are pending
### Memory Management
- **Event Queue**: Limited to 1000 events (prevents memory leaks)
- **Stats Tracking**: Lightweight counters
- **Timer Cleanup**: Proper cleanup on stop
### Battery Optimization
- **iOS**: Compatible with background app refresh
- **Android**: Uses main looper for efficiency
- **Smart Polling**: Checks `has_pending_events()` before full poll
## Expected Log Output
### Successful Initialization
```
🚀 [Rust] Starting Matrix client initialization
📁 [iOS] Using storage path: /Documents/matrix_storage
✅ [iOS] Matrix client initialized successfully
🚨 [iOS] Starting sync with polling...
🔄 [iOS] Starting polling with interval: 1.0 seconds
✅ [iOS] Polling timer created and scheduled
```
### Active Polling
```
🔍 [iOS] Polling for updates...
📊 [Rust] POLLED 3 PENDING EVENTS
🔄 Sync Update: SYNCING
💬 Timeline Event: $event123 in !room:matrix.org
📨 Room Invite: !newroom:matrix.org from @user:matrix.org
🎯 [iOS] Processing event: SyncUpdate
🔄 [iOS] Sync Update - State: SYNCING, Rooms: 5
```
### Statistics Output
```javascript
{
"pending_events": 0,
"last_poll_seconds_ago": 1,
"sync_state": "SYNCING",
"ios_polling": true,
"ios_poll_interval": 1.0,
"ios_stats": {
"start_time": 1699123456,
"total_polls": 147,
"events_received": 23,
"last_poll_time": 1699123503
}
}
```
## Troubleshooting
### No Logs Appearing
1. **Check build**: Ensure `./build.sh` completed successfully
2. **Check linking**: Verify native module is properly linked in your app
3. **Check console**: Look in the right console (Xcode/Metro/adb)
### No Events Received
1. **Check authentication**: Verify access token and user ID
2. **Check network**: Ensure homeserver is reachable
3. **Check rooms**: User must be in active rooms
4. **Check polling**: Verify `getPollStats()` shows active polling
### Performance Issues
1. **Adjust poll interval**: Increase from 1 second if needed
2. **Monitor memory**: Check for event queue growth
3. **Check battery usage**: Monitor background activity
## Migration from Old Implementation
### Code Changes Required
1. **Replace `startSync()`**: No code changes needed (same API)
2. **Monitor logs**: Look for new emoji-based logs
3. **Check stats**: Use `getPollStats()` for diagnostics
4. **Event handling**: Existing event listeners continue to work
### Verification Steps
1. ✅ Logs appear in native console
2. ✅ Events reach JavaScript listeners
3. ✅ Sync state updates properly
4. ✅ Messages and invites work
5. ✅ Polling stats show activity
## Benefits of New Implementation
### Reliability
- ✅ **Platform timers** ensure execution
- ✅ **Visible debugging** through comprehensive logs
- ✅ **Predictable behavior** across iOS/Android
### Performance
- ✅ **Lower latency** (1-second polling vs async delays)
- ✅ **Better battery life** (optimized polling)
- ✅ **Memory safety** (bounded event queues)
### Maintainability
- ✅ **Clear separation** of concerns
- ✅ **Easy debugging** with emoji logs
- ✅ **Platform-specific optimization**
The implementation successfully resolves the original async runtime issues by moving event loop management to the native platform side where it belongs in a static library architecture.