@offline-protocol/mesh-sdk 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,36 @@
1
1
  # @offline-protocol/mesh-sdk
2
2
 
3
- Offline-first mesh networking SDK with intelligent transport switching for React Native. Built with Rust for maximum performance and reliability.
3
+ Offline-first mesh networking SDK for React Native. Enables peer-to-peer messaging over BLE, WiFi Direct, and Internet with intelligent transport switching.
4
4
 
5
- ## Features
5
+ ## Table of Contents
6
6
 
7
- - **Offline-First**: Messages delivered even without internet connectivity
8
- - **Intelligent Transport Switching**: DORS automatically selects the best transport (Internet, BLE, WiFi Direct)
9
- - **Mesh Networking**: Multi-hop routing with automatic relay selection
10
- - **Cross-Platform**: Works on iOS and Android
11
- - **Type-Safe**: Full TypeScript support
7
+ - [Requirements](#requirements)
8
+ - [Installation](#installation)
9
+ - [Platform Setup](#platform-setup)
10
+ - [Quick Start](#quick-start)
11
+ - [Protocol Lifecycle](#protocol-lifecycle)
12
+ - [Configuration](#configuration)
13
+ - [API Reference](#api-reference)
14
+ - [Events](#events)
15
+ - [Types](#types)
16
+ - [DORS (Transport Switching)](#dors-dynamic-offline-relay-switch)
17
+ - [Mesh Networking](#mesh-networking)
18
+ - [Reliability Layer](#reliability-layer)
19
+ - [File Transfer](#file-transfer)
20
+ - [Troubleshooting](#troubleshooting)
21
+
22
+ ---
23
+
24
+ ## Requirements
25
+
26
+ | Platform | Version |
27
+ |----------|---------|
28
+ | React Native | >= 0.70.0 |
29
+ | iOS | >= 13.0 |
30
+ | Android | >= API 26 (Android 8.0) |
31
+ | Node.js | >= 16 |
32
+
33
+ ---
12
34
 
13
35
  ## Installation
14
36
 
@@ -16,15 +38,63 @@ Offline-first mesh networking SDK with intelligent transport switching for React
16
38
  npm install @offline-protocol/mesh-sdk
17
39
  ```
18
40
 
19
- ### iOS Setup
41
+ ### iOS
20
42
 
21
43
  ```bash
22
44
  cd ios && pod install
23
45
  ```
24
46
 
25
- ### Android Setup
47
+ ### Android
48
+
49
+ Pre-built native libraries are included. No additional setup required.
50
+
51
+ ---
52
+
53
+ ## Platform Setup
54
+
55
+ ### iOS Permissions
56
+
57
+ Add to `Info.plist`:
58
+
59
+ ```xml
60
+ <key>NSBluetoothAlwaysUsageDescription</key>
61
+ <string>Required for offline mesh communication</string>
26
62
 
27
- No additional setup needed. Pre-built libraries are included.
63
+ <key>NSBluetoothPeripheralUsageDescription</key>
64
+ <string>Required for offline mesh communication</string>
65
+
66
+ <key>UIBackgroundModes</key>
67
+ <array>
68
+ <string>bluetooth-central</string>
69
+ <string>bluetooth-peripheral</string>
70
+ </array>
71
+ ```
72
+
73
+ ### Android Permissions
74
+
75
+ Add to `AndroidManifest.xml`:
76
+
77
+ ```xml
78
+ <!-- Bluetooth (Android 12+) -->
79
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
80
+ <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
81
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
82
+
83
+ <!-- Bluetooth (Android 11 and below) -->
84
+ <uses-permission android:name="android.permission.BLUETOOTH" />
85
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
86
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
87
+
88
+ <!-- WiFi Direct -->
89
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
90
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
91
+ <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
92
+
93
+ <!-- Internet -->
94
+ <uses-permission android:name="android.permission.INTERNET" />
95
+ ```
96
+
97
+ ---
28
98
 
29
99
  ## Quick Start
30
100
 
@@ -47,62 +117,404 @@ const messageId = await protocol.sendMessage({
47
117
  content: 'Hello!',
48
118
  priority: MessagePriority.High,
49
119
  });
120
+
121
+ await protocol.stop();
122
+ await protocol.destroy();
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Protocol Lifecycle
128
+
129
+ ### Complete Flow Example
130
+
131
+ ```typescript
132
+ import {
133
+ OfflineProtocol,
134
+ MessagePriority,
135
+ ProtocolEvent,
136
+ MessageReceivedEvent,
137
+ MessageDeliveredEvent,
138
+ NeighborDiscoveredEvent,
139
+ } from '@offline-protocol/mesh-sdk';
140
+
141
+ // 1. CREATE PROTOCOL INSTANCE
142
+ const protocol = new OfflineProtocol({
143
+ appId: 'my-chat-app',
144
+ userId: 'alice-device-001',
145
+ });
146
+
147
+ // 2. REGISTER EVENT LISTENERS (before starting)
148
+
149
+ // Track discovered peers
150
+ const discoveredPeers = new Map<string, number>(); // peerId -> rssi
151
+
152
+ protocol.on('neighbor_discovered', (event: NeighborDiscoveredEvent) => {
153
+ console.log(`[PEER FOUND] ${event.peer_id} via ${event.transport}, RSSI: ${event.rssi}`);
154
+ discoveredPeers.set(event.peer_id, event.rssi ?? -100);
155
+ });
156
+
157
+ protocol.on('neighbor_lost', (event) => {
158
+ console.log(`[PEER LOST] ${event.peer_id}`);
159
+ discoveredPeers.delete(event.peer_id);
160
+ });
161
+
162
+ // Track outgoing messages
163
+ const pendingMessages = new Map<string, { recipient: string; content: string }>();
164
+
165
+ protocol.on('message_sent', (event) => {
166
+ console.log(`[SENT] Message ${event.message_id} to ${event.recipient}`);
167
+ pendingMessages.set(event.message_id, {
168
+ recipient: event.recipient,
169
+ content: event.content,
170
+ });
171
+ });
172
+
173
+ protocol.on('message_delivered', (event: MessageDeliveredEvent) => {
174
+ console.log(`[DELIVERED] Message ${event.message_id} in ${event.latency_ms}ms, ${event.hop_count} hops`);
175
+ pendingMessages.delete(event.message_id);
176
+ });
177
+
178
+ protocol.on('message_failed', (event) => {
179
+ console.log(`[FAILED] Message ${event.message_id}: ${event.reason} (${event.retry_count} retries)`);
180
+ pendingMessages.delete(event.message_id);
181
+ });
182
+
183
+ // Handle incoming messages
184
+ protocol.on('message_received', (event: MessageReceivedEvent) => {
185
+ console.log(`[RECEIVED] From ${event.sender}: ${event.content}`);
186
+ console.log(` - Message ID: ${event.message_id}`);
187
+ console.log(` - Hop count: ${event.hop_count}`);
188
+ console.log(` - Transport: ${event.transport}`);
189
+
190
+ // Process the message in your app
191
+ handleIncomingMessage(event);
192
+ });
193
+
194
+ // Monitor transport changes
195
+ protocol.on('transport_switched', (event) => {
196
+ console.log(`[TRANSPORT] Switched from ${event.from} to ${event.to}: ${event.reason}`);
197
+ });
198
+
199
+ // 3. START THE PROTOCOL
200
+ await protocol.start();
201
+ // At this point:
202
+ // - BLE scanning begins (discovers nearby devices)
203
+ // - BLE advertising begins (makes this device discoverable)
204
+ // - neighbor_discovered events will start firing as peers are found
205
+
206
+ // 4. WAIT FOR PEERS (optional helper)
207
+ async function waitForPeer(peerId: string, timeoutMs = 30000): Promise<boolean> {
208
+ if (discoveredPeers.has(peerId)) return true;
209
+
210
+ return new Promise((resolve) => {
211
+ const timeout = setTimeout(() => resolve(false), timeoutMs);
212
+
213
+ const handler = (event: NeighborDiscoveredEvent) => {
214
+ if (event.peer_id === peerId) {
215
+ clearTimeout(timeout);
216
+ protocol.off('neighbor_discovered', handler);
217
+ resolve(true);
218
+ }
219
+ };
220
+
221
+ protocol.on('neighbor_discovered', handler);
222
+ });
223
+ }
224
+
225
+ // 5. SEND A MESSAGE
226
+ async function sendChatMessage(recipientId: string, text: string) {
227
+ try {
228
+ const messageId = await protocol.sendMessage({
229
+ recipient: recipientId,
230
+ content: text,
231
+ priority: MessagePriority.High,
232
+ });
233
+ console.log(`Message queued with ID: ${messageId}`);
234
+ return messageId;
235
+ } catch (error) {
236
+ console.error('Failed to send message:', error);
237
+ throw error;
238
+ }
239
+ }
240
+
241
+ // 6. CLEANUP ON APP EXIT
242
+ async function cleanup() {
243
+ await protocol.stop();
244
+ await protocol.destroy();
245
+ }
246
+ ```
247
+
248
+ ### Event Sequence Timeline
249
+
250
+ ```
251
+ ┌─────────────────────────────────────────────────────────────────────┐
252
+ │ PROTOCOL LIFECYCLE │
253
+ ├─────────────────────────────────────────────────────────────────────┤
254
+ │ │
255
+ │ new OfflineProtocol(config) │
256
+ │ │ │
257
+ │ ▼ │
258
+ │ protocol.on('...', handler) ← Register all event listeners │
259
+ │ │ │
260
+ │ ▼ │
261
+ │ await protocol.start() │
262
+ │ │ │
263
+ │ ├──► BLE advertising starts (device becomes discoverable) │
264
+ │ ├──► BLE scanning starts (looking for other devices) │
265
+ │ │ │
266
+ │ ▼ │
267
+ │ ┌─────────────────────────────────────────────────────────────┐ │
268
+ │ │ PEER DISCOVERY PHASE │ │
269
+ │ │ │ │
270
+ │ │ neighbor_discovered { peer_id, transport, rssi } │ │
271
+ │ │ neighbor_discovered { peer_id, transport, rssi } │ │
272
+ │ │ ... │ │
273
+ │ │ │ │
274
+ │ │ MeshController evaluates peers, establishes connections │ │
275
+ │ │ (MEMBER for same cluster, BRIDGE for different clusters) │ │
276
+ │ └─────────────────────────────────────────────────────────────┘ │
277
+ │ │ │
278
+ │ ▼ │
279
+ │ ┌─────────────────────────────────────────────────────────────┐ │
280
+ │ │ MESSAGING PHASE │ │
281
+ │ │ │ │
282
+ │ │ protocol.sendMessage({ recipient, content, priority }) │ │
283
+ │ │ │ │ │
284
+ │ │ ▼ │ │
285
+ │ │ message_sent { message_id, recipient, content, ... } │ │
286
+ │ │ │ │ │
287
+ │ │ ├──► [SUCCESS] message_delivered { message_id, ... } │ │
288
+ │ │ │ │ │
289
+ │ │ └──► [FAILURE] message_failed { message_id, reason } │ │
290
+ │ │ │ │
291
+ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │
292
+ │ │ │ │
293
+ │ │ INCOMING: message_received { sender, content, ... } │ │
294
+ │ └─────────────────────────────────────────────────────────────┘ │
295
+ │ │ │
296
+ │ │ (peers may come and go) │
297
+ │ │ │
298
+ │ ▼ │
299
+ │ neighbor_lost { peer_id } │
300
+ │ neighbor_discovered { peer_id, ... } ← new peer appears │
301
+ │ │ │
302
+ │ ▼ │
303
+ │ await protocol.stop() │
304
+ │ │ │
305
+ │ ├──► BLE scanning stops │
306
+ │ ├──► BLE advertising stops │
307
+ │ ├──► All connections closed │
308
+ │ │ │
309
+ │ ▼ │
310
+ │ await protocol.destroy() ← Clean up resources │
311
+ │ │
312
+ └─────────────────────────────────────────────────────────────────────┘
313
+ ```
314
+
315
+ ### What Happens Under the Hood
316
+
317
+ #### On `protocol.start()`
318
+
319
+ 1. **Protocol core starts** in Rust
320
+ 2. **BLE Manager initializes**:
321
+ - Starts scanning for devices advertising the Offline Protocol service UUID
322
+ - Starts advertising this device with mesh metadata (degree, free slots, battery, uptime)
323
+ 3. **Process timer starts** - polls for outgoing fragments every 100ms
324
+
325
+ #### On Peer Discovery
326
+
327
+ 1. **BLE scan detects advertisement** from another device
328
+ 2. **MeshController.shouldInitiateOutbound()** evaluates the candidate:
329
+ - Checks connection budget (default max: 4)
330
+ - Calculates peer score (RSSI, availability, battery, uptime, stability, load)
331
+ - Determines if this is a cluster bridge opportunity
332
+ 3. **If accepted**: BLE connection established, `neighbor_discovered` fires
333
+ 4. **If at capacity**: May evict a lower-scoring peer to make room
334
+
335
+ #### On `protocol.sendMessage()`
336
+
337
+ 1. **Message created** with unique ID, TTL, timestamp, priority
338
+ 2. **message_sent event** fires immediately
339
+ 3. **Message queued** for transmission
340
+ 4. **DORS selects transport** (BLE, WiFi Direct, or Internet)
341
+ 5. **Message sent** to connected peers
342
+ 6. **ACK tracking begins** (default 5s timeout)
343
+ 7. **On ACK received**: `message_delivered` event fires
344
+ 8. **On timeout/max retries**: `message_failed` event fires
345
+
346
+ #### On Incoming Message
347
+
348
+ 1. **BLE fragment received** from peer
349
+ 2. **Deduplication check** - skip if message ID already seen
350
+ 3. **If addressed to this device**: `message_received` event fires
351
+ 4. **ACK sent back** to sender
352
+ 5. **Hop count incremented** for metrics
353
+
354
+ #### On `protocol.stop()`
355
+
356
+ 1. **BLE Manager stops** scanning and advertising
357
+ 2. **All peer connections closed**
358
+ 3. **neighbor_lost events** fire for each disconnected peer
359
+ 4. **Protocol core stops**
360
+
361
+ ### Diagnostic Events
362
+
363
+ The SDK emits diagnostic events for debugging:
364
+
365
+ ```typescript
366
+ protocol.on('diagnostic', (event) => {
367
+ console.log(`[${event.level.toUpperCase()}] ${event.message}`, event.context);
368
+ });
50
369
  ```
51
370
 
371
+ ---
372
+
52
373
  ## Configuration
53
374
 
375
+ ### ProtocolConfig
376
+
54
377
  ```typescript
55
378
  interface ProtocolConfig {
56
379
  appId: string;
57
380
  userId: string;
58
- transport?: {
59
- bleEnabled?: boolean; // default: true
60
- wifiDirectEnabled?: boolean; // default: true
61
- internetEnabled?: boolean; // default: true
381
+ transports?: TransportsConfig;
382
+ dors?: DorsConfig;
383
+ network?: NetworkConfig;
384
+ reliability?: ReliabilityConfig;
385
+ fileTransfer?: FileTransferConfig;
386
+ path?: PathConfig;
387
+ }
388
+ ```
389
+
390
+ ### TransportsConfig
391
+
392
+ ```typescript
393
+ interface TransportsConfig {
394
+ ble?: {
395
+ enabled: boolean; // default: true
62
396
  };
63
- dors?: {
64
- preferOnline?: boolean; // default: false
65
- switchHysteresis?: number; // default: 15.0
66
- switchCooldownSecs?: number; // default: 20
397
+ internet?: {
398
+ enabled: boolean; // default: false
399
+ serverAddress?: string; // WebSocket URL
400
+ autoReconnect?: boolean; // default: true
401
+ reconnectDelay?: number; // ms
67
402
  };
68
- relay?: {
69
- allowRelay?: boolean; // default: true
70
- minBatteryForRelay?: number; // default: 30
71
- relayThreshold?: number; // default: 3
403
+ wifiDirect?: {
404
+ enabled: boolean; // default: false (Android only)
405
+ deviceName?: string;
406
+ autoAccept?: boolean;
407
+ groupOwnerIntent?: number; // 0-15
72
408
  };
73
- network?: {
74
- initialTtl?: number; // default: 8
409
+ }
410
+ ```
411
+
412
+ ### DorsConfig
413
+
414
+ Controls transport switching behavior.
415
+
416
+ ```typescript
417
+ interface DorsConfig {
418
+ preferOnline?: boolean; // default: false
419
+ switchHysteresis?: number; // default: 15.0
420
+ switchCooldownSecs?: number; // default: 20
421
+ bleToWifiRetryThreshold?: number; // default: 2
422
+ rssiSwitchThreshold?: number; // default: -85 dBm
423
+ congestionQueueThreshold?: number; // default: 50
424
+ stabilityWindowSecs?: number; // default: 8
425
+ poorSignalDurationSecs?: number; // default: 10
426
+ ttlEscalationThreshold?: number; // default: 2
427
+ congestionDurationSecs?: number; // default: 10
428
+ ttlEscalationHoldSecs?: number; // default: 20
429
+ historyWindowSize?: number; // default: 10
430
+ queueRecoveryRatio?: number; // default: 0.5
431
+ }
432
+ ```
433
+
434
+
435
+ ### NetworkConfig
436
+
437
+ ```typescript
438
+ interface NetworkConfig {
439
+ initialTtl?: number; // default: 8
440
+ }
441
+ ```
442
+
443
+ ### ReliabilityConfig
444
+
445
+ ```typescript
446
+ interface ReliabilityConfig {
447
+ ack?: {
448
+ defaultTimeoutMs?: number; // default: 5000
449
+ maxPendingAcks?: number; // default: 1000
75
450
  };
451
+ retry?: {
452
+ maxRetries?: number; // default: 5
453
+ initialDelayMs?: number; // default: 1000
454
+ maxDelayMs?: number; // default: 30000
455
+ backoffMultiplier?: number; // default: 2.0
456
+ outboxMaxLifetimeMs?: number; // default: 3600000
457
+ };
458
+ dedup?: {
459
+ maxTrackedMessages?: number; // default: 10000
460
+ retentionTimeSecs?: number; // default: 3600
461
+ };
462
+ }
463
+ ```
464
+
465
+ ### PathConfig
466
+
467
+ ```typescript
468
+ interface PathConfig {
469
+ forwardToTopK?: number; // default: 3
470
+ maxCongestionLevel?: number; // default: 0.8
471
+ }
472
+ ```
473
+
474
+ ### FileTransferConfig
475
+
476
+ ```typescript
477
+ interface FileTransferConfig {
478
+ chunkSize?: number; // default: 32768 (32KB)
479
+ maxFileSize?: number; // default: 104857600 (100MB)
76
480
  }
77
481
  ```
78
482
 
79
- ## API
483
+ ---
80
484
 
81
- ### Methods
485
+ ## API Reference
82
486
 
83
- - `start(): Promise<void>` - Start the protocol
84
- - `stop(): Promise<void>` - Stop the protocol
85
- - `sendMessage(params): Promise<string>` - Send a message
86
- - `on(eventType, listener)` - Register event listener
87
- - `off(eventType, listener)` - Remove event listener
88
- - `destroy(): Promise<void>` - Clean up resources
487
+ ### Constructor
488
+
489
+ ```typescript
490
+ new OfflineProtocol(config: ProtocolConfig)
491
+ ```
89
492
 
90
- ### Events
493
+ ### Lifecycle Methods
91
494
 
92
- **Message Events:**
93
- - `message_sent` - Message was sent
94
- - `message_received` - Message was received
95
- - `message_delivered` - Message was delivered (ACK received)
96
- - `message_failed` - Message delivery failed
495
+ | Method | Returns | Description |
496
+ |--------|---------|-------------|
497
+ | `start()` | `Promise<void>` | Start the protocol and all enabled transports |
498
+ | `stop()` | `Promise<void>` | Stop the protocol and disconnect all peers |
499
+ | `pause()` | `Promise<void>` | Pause for background mode |
500
+ | `resume()` | `Promise<void>` | Resume from paused state |
501
+ | `destroy()` | `Promise<void>` | Clean up all resources |
502
+ | `getState()` | `Promise<ProtocolState>` | Get current state (`Stopped`, `Running`, `Paused`) |
97
503
 
98
- **Network Events:**
99
- - `transport_switched` - Transport changed (BLE/WiFi/Internet)
100
- - `neighbor_discovered` - New neighbor found
101
- - `neighbor_lost` - Neighbor disconnected
504
+ ### Messaging
102
505
 
103
- ### Message Priority
506
+ | Method | Returns | Description |
507
+ |--------|---------|-------------|
508
+ | `sendMessage(params: SendMessageParams)` | `Promise<string>` | Send message, returns message ID |
509
+ | `receiveMessage()` | `Promise<MessageReceivedEvent \| null>` | Poll for next received message |
104
510
 
105
511
  ```typescript
512
+ interface SendMessageParams {
513
+ recipient: string;
514
+ content: string;
515
+ priority?: MessagePriority; // default: Medium
516
+ }
517
+
106
518
  enum MessagePriority {
107
519
  Low = 0,
108
520
  Medium = 1,
@@ -111,85 +523,530 @@ enum MessagePriority {
111
523
  }
112
524
  ```
113
525
 
114
- ## How It Works
526
+ ### Transport Management
527
+
528
+ | Method | Returns | Description |
529
+ |--------|---------|-------------|
530
+ | `getActiveTransports()` | `Promise<TransportType[]>` | Get list of active transports |
531
+ | `enableTransport(type, config?)` | `Promise<void>` | Enable a transport |
532
+ | `disableTransport(type)` | `Promise<void>` | Disable a transport |
533
+ | `forceTransport(type)` | `Promise<void>` | Force specific transport (override DORS) |
534
+ | `releaseTransportLock()` | `Promise<void>` | Release forced transport, let DORS decide |
535
+ | `getTransportMetrics(type)` | `Promise<TransportMetrics \| null>` | Get transport statistics |
536
+
537
+ ```typescript
538
+ type TransportType = 'ble' | 'internet' | 'wifiDirect';
539
+
540
+ interface TransportMetrics {
541
+ packetsSent: number;
542
+ packetsReceived: number;
543
+ bytesSent: number;
544
+ bytesReceived: number;
545
+ errorRate: number;
546
+ avgLatencyMs: number;
547
+ }
548
+ ```
549
+
550
+ ### Bluetooth
551
+
552
+ | Method | Returns | Description |
553
+ |--------|---------|-------------|
554
+ | `isBluetoothEnabled()` | `Promise<boolean>` | Check if Bluetooth is enabled |
555
+ | `requestEnableBluetooth()` | `Promise<boolean>` | Request to enable Bluetooth (Android only) |
556
+ | `getBLePeerCount()` | `Promise<number>` | Get number of discovered BLE peers |
557
+
558
+ ### Network Topology
559
+
560
+ | Method | Returns | Description |
561
+ |--------|---------|-------------|
562
+ | `getTopology()` | `Promise<NetworkTopology>` | Get network topology snapshot |
563
+ | `getMessageStats()` | `Promise<MessageDeliveryStats[]>` | Get message delivery statistics |
564
+ | `getDeliverySuccessRate()` | `Promise<number>` | Get delivery success rate (0-1) |
565
+ | `getMedianLatency()` | `Promise<number \| null>` | Get median latency in ms |
566
+ | `getMedianHops()` | `Promise<number \| null>` | Get median hop count |
567
+
568
+ ### Battery
115
569
 
116
- ### DORS (Dynamic Offline Relay Switch)
570
+ | Method | Returns | Description |
571
+ |--------|---------|-------------|
572
+ | `setBatteryLevel(level)` | `Promise<void>` | Set battery level (0-100) for mesh decisions |
573
+ | `getBatteryLevel()` | `Promise<number \| null>` | Get current battery level |
117
574
 
118
- DORS automatically selects the best transport (Internet, BLE, or WiFi Direct) based on:
119
- - Signal strength (RSSI)
120
- - Bandwidth and congestion
121
- - Energy efficiency
122
- - Reliability and proximity
575
+ ### DORS Configuration
123
576
 
124
- ### Mesh Network
577
+ | Method | Returns | Description |
578
+ |--------|---------|-------------|
579
+ | `updateDorsConfig(config)` | `Promise<void>` | Update DORS settings at runtime |
580
+ | `getDorsConfig()` | `Promise<DorsConfig>` | Get current DORS configuration |
581
+ | `shouldEscalateToWifi()` | `Promise<boolean>` | Check if DORS recommends WiFi escalation |
125
582
 
126
- The SDK implements a cluster-based mesh network where devices organize into clusters and form connections based on mesh topology:
583
+ ### Reliability Configuration
127
584
 
128
- **Cluster Architecture:**
129
- - **MEMBER Role**: Devices within the same cluster (intra-cluster connections)
130
- - **BRIDGE Role**: Devices connecting different clusters (inter-cluster connections)
131
- - **Connection Budget**: Each device maintains up to 4 active connections (configurable)
585
+ | Method | Returns | Description |
586
+ |--------|---------|-------------|
587
+ | `updateAckConfig(config)` | `Promise<void>` | Update ACK settings |
588
+ | `updateRetryConfig(config)` | `Promise<void>` | Update retry settings |
589
+ | `updateDedupConfig(config)` | `Promise<void>` | Update deduplication settings |
590
+ | `getDedupStats()` | `Promise<DedupStats>` | Get deduplication statistics |
591
+ | `getPendingAckCount()` | `Promise<number>` | Get pending ACK count |
592
+ | `getRetryQueueSize()` | `Promise<number>` | Get retry queue size |
132
593
 
133
- **Connection Decision Process:**
134
- 1. Devices discover each other via BLE advertisements containing mesh metadata
135
- 2. MeshController evaluates connection candidates based on:
136
- - Available connection slots
137
- - Peer scores (RSSI, battery, uptime, stability, load)
138
- - Cluster membership and free slot estimates
139
- 3. Connection intent determines role:
140
- - `INTRA_CLUSTER` → MEMBER role (same cluster)
141
- - `INTER_CLUSTER` → BRIDGE role (different clusters)
142
- 4. When connection budget is full, the system can evict the worst peer to make room for better connections
594
+ ### Gradient Routing
143
595
 
144
- **Message Routing:**
145
- - Direct delivery when recipient is a connected peer (1 hop)
146
- - Multi-hop routing through cluster members and bridges (up to TTL hops, default 8)
147
- - Automatic path selection based on cluster topology and peer quality
148
- - Messages traverse clusters via bridge connections when needed
596
+ | Method | Returns | Description |
597
+ |--------|---------|-------------|
598
+ | `learnRoute(destination, nextHop, hopCount, quality)` | `Promise<void>` | Learn a route from incoming message |
599
+ | `getBestRoute(destination)` | `Promise<RouteEntry \| null>` | Get best route to destination |
600
+ | `getAllRoutes(destination)` | `Promise<RouteEntry[]>` | Get all routes to destination |
601
+ | `hasRoute(destination)` | `Promise<boolean>` | Check if route exists |
602
+ | `removeNeighborRoutes(neighborId)` | `Promise<void>` | Remove routes through neighbor |
603
+ | `cleanupExpiredRoutes()` | `Promise<void>` | Clean up expired routes |
604
+ | `getRoutingStats()` | `Promise<RoutingStats>` | Get routing table statistics |
605
+ | `updateRoutingConfig(config)` | `Promise<void>` | Update routing configuration |
149
606
 
150
- ## Example
607
+ ### Event Listeners
608
+
609
+ | Method | Returns | Description |
610
+ |--------|---------|-------------|
611
+ | `on(eventType, listener)` | `this` | Register event listener |
612
+ | `off(eventType, listener)` | `this` | Remove event listener |
613
+ | `once(eventType, listener)` | `this` | Register one-time listener |
614
+ | `removeAllListeners(eventType?)` | `this` | Remove all listeners |
615
+
616
+ ---
617
+
618
+ ## Events
619
+
620
+
621
+ ### Message Events
622
+
623
+ #### message_sent
151
624
 
152
625
  ```typescript
153
- import React, { useEffect, useState } from 'react';
154
- import { OfflineProtocol, MessagePriority } from '@offline-protocol/mesh-sdk';
626
+ interface MessageSentEvent {
627
+ type: 'message_sent';
628
+ message_id: string;
629
+ sender: string;
630
+ recipient: string;
631
+ content: string;
632
+ priority: 'low' | 'medium' | 'high' | 'critical';
633
+ requires_ack: boolean;
634
+ timestamp: number;
635
+ }
636
+ ```
155
637
 
156
- function ChatScreen({ userId, recipientId }) {
157
- const [protocol, setProtocol] = useState(null);
158
- const [messages, setMessages] = useState([]);
638
+ #### message_received
159
639
 
160
- useEffect(() => {
161
- const proto = new OfflineProtocol({
162
- appId: 'chat-app',
163
- userId,
164
- });
640
+ ```typescript
641
+ interface MessageReceivedEvent {
642
+ type: 'message_received';
643
+ message_id: string;
644
+ sender: string;
645
+ recipient: string;
646
+ content: string;
647
+ hop_count: number;
648
+ transport: string;
649
+ timestamp: number;
650
+ }
651
+ ```
165
652
 
166
- proto.on('message_received', (event) => {
167
- if (event.sender === recipientId) {
168
- setMessages((prev) => [...prev, {
169
- id: event.message_id,
170
- text: event.content,
171
- sender: event.sender,
172
- timestamp: event.timestamp,
173
- }]);
174
- }
175
- });
653
+ #### message_delivered
176
654
 
177
- proto.start();
178
- setProtocol(proto);
655
+ ```typescript
656
+ interface MessageDeliveredEvent {
657
+ type: 'message_delivered';
658
+ message_id: string;
659
+ latency_ms: number;
660
+ hop_count: number;
661
+ transport: string;
662
+ }
663
+ ```
179
664
 
180
- return () => proto.destroy();
181
- }, [userId, recipientId]);
665
+ #### message_failed
182
666
 
183
- const sendMessage = async (text) => {
184
- if (protocol) {
185
- await protocol.sendMessage({
186
- recipient: recipientId,
187
- content: text,
188
- priority: MessagePriority.High,
189
- });
190
- }
191
- };
667
+ ```typescript
668
+ interface MessageFailedEvent {
669
+ type: 'message_failed';
670
+ message_id: string;
671
+ reason: string;
672
+ retry_count: number;
673
+ }
674
+ ```
675
+
676
+ ### Network Events
677
+
678
+ #### transport_switched
679
+
680
+ ```typescript
681
+ interface TransportSwitchedEvent {
682
+ type: 'transport_switched';
683
+ from: string | null;
684
+ to: string;
685
+ reason: string;
686
+ }
687
+ ```
688
+
689
+ #### neighbor_discovered
690
+
691
+ ```typescript
692
+ interface NeighborDiscoveredEvent {
693
+ type: 'neighbor_discovered';
694
+ peer_id: string;
695
+ transport: string;
696
+ rssi?: number;
697
+ }
698
+ ```
699
+
700
+ #### neighbor_lost
701
+
702
+ ```typescript
703
+ interface NeighborLostEvent {
704
+ type: 'neighbor_lost';
705
+ peer_id: string;
706
+ }
707
+ ```
708
+
709
+ #### network_metrics
710
+
711
+ ```typescript
712
+ interface NetworkMetricsEvent {
713
+ type: 'network_metrics';
714
+ neighbor_count: number;
715
+ relay_count: number;
716
+ delivery_ratio: number;
717
+ avg_latency_ms: number;
718
+ }
719
+ ```
720
+
721
+
722
+ ### File Events
723
+
724
+ #### file_progress
725
+
726
+ ```typescript
727
+ interface FileProgressEvent {
728
+ type: 'file_progress';
729
+ file_id: string;
730
+ chunks_sent: number;
731
+ total_chunks: number;
732
+ percentage: number;
733
+ }
734
+ ```
735
+
736
+ #### file_received
737
+
738
+ ```typescript
739
+ interface FileReceivedEvent {
740
+ type: 'file_received';
741
+ file_id: string;
742
+ file_name: string;
743
+ file_size: number;
744
+ sender: string;
745
+ }
746
+ ```
192
747
 
193
- return (/* Your UI */);
748
+ ### Diagnostic Events
749
+
750
+ ```typescript
751
+ interface DiagnosticEvent {
752
+ type: 'diagnostic';
753
+ level: 'info' | 'warning' | 'error';
754
+ message: string;
755
+ context?: Record<string, unknown>;
194
756
  }
195
757
  ```
758
+
759
+ ---
760
+
761
+ ## Types
762
+
763
+ ### NetworkTopology
764
+
765
+ ```typescript
766
+ interface NetworkTopology {
767
+ timestamp: number;
768
+ local_user_id: string;
769
+ nodes: NetworkNode[];
770
+ links: NetworkLink[];
771
+ stats: NetworkStats;
772
+ }
773
+
774
+ interface NetworkNode {
775
+ user_id: string;
776
+ role: string; // 'Normal' or 'Relay' from topology API
777
+ connection_count: number;
778
+ battery_level?: number;
779
+ last_seen: number;
780
+ transports: TransportType[];
781
+ }
782
+
783
+ interface NetworkLink {
784
+ from: string;
785
+ to: string;
786
+ quality: number; // 0.0 - 1.0
787
+ transport: TransportType;
788
+ rssi?: number;
789
+ }
790
+
791
+ interface NetworkStats {
792
+ total_nodes: number;
793
+ relay_nodes: number; // Count of nodes with 'Relay' role in topology
794
+ total_connections: number;
795
+ avg_link_quality: number;
796
+ network_diameter?: number;
797
+ }
798
+ ```
799
+
800
+ ### MessageDeliveryStats
801
+
802
+ ```typescript
803
+ interface MessageDeliveryStats {
804
+ message_id: string;
805
+ sender: string;
806
+ recipient: string;
807
+ sent_at: number;
808
+ delivered_at?: number;
809
+ hop_count: number;
810
+ transport?: TransportType;
811
+ retry_count: number;
812
+ latency_ms?: number;
813
+ }
814
+ ```
815
+
816
+ ### RouteEntry
817
+
818
+ ```typescript
819
+ interface RouteEntry {
820
+ nextHop: string;
821
+ hopCount: number;
822
+ quality: number; // 0.0 - 1.0
823
+ lastSeenMs: number;
824
+ }
825
+ ```
826
+
827
+ ### RoutingStats
828
+
829
+ ```typescript
830
+ interface RoutingStats {
831
+ destinationCount: number;
832
+ routeCount: number;
833
+ }
834
+ ```
835
+
836
+ ### DedupStats
837
+
838
+ ```typescript
839
+ interface DedupStats {
840
+ totalTracked: number;
841
+ recentTracked: number;
842
+ capacityUsedPercent: number;
843
+ mode: 'HashMap' | 'BloomFilter';
844
+ }
845
+ ```
846
+
847
+ ### FileProgress
848
+
849
+ ```typescript
850
+ interface FileProgress {
851
+ file_id: string;
852
+ file_name: string;
853
+ file_size: number;
854
+ chunks_completed: number;
855
+ total_chunks: number;
856
+ percentage: number;
857
+ }
858
+ ```
859
+
860
+ ### ProtocolState
861
+
862
+ ```typescript
863
+ enum ProtocolState {
864
+ Stopped = 0,
865
+ Running = 1,
866
+ Paused = 2,
867
+ }
868
+ ```
869
+
870
+ ---
871
+
872
+ ## DORS (Dynamic Offline Relay Switch)
873
+
874
+ DORS automatically selects the optimal transport based on real-time conditions.
875
+
876
+ ### Scoring Factors
877
+
878
+ | Factor | Description |
879
+ |--------|-------------|
880
+ | Signal Strength | RSSI for BLE/WiFi (-50 to -100 dBm) |
881
+ | Proximity | Hop count to destination |
882
+ | Bandwidth | Transport throughput capability |
883
+ | Congestion | Queue depth and backlog |
884
+ | Energy | Battery impact of transport |
885
+ | Reliability | Historical delivery success rate |
886
+ | Load | Current processing capacity |
887
+
888
+ ### Transport Weights
889
+
890
+ **BLE**: Optimized for energy efficiency and mesh scenarios
891
+ - Signal: 30%, Energy: 30%, Congestion: 15%, Proximity: 15%
892
+
893
+ **WiFi Direct**: Optimized for high throughput
894
+ - Bandwidth: 35%, Proximity: 20%, Congestion: 20%, Reliability: 15%
895
+
896
+ **Internet**: Optimized for server connectivity
897
+ - Bandwidth: 35%, Reliability: 30%, Congestion: 15%, Energy: 10%
898
+
899
+ ### Switching Safeguards
900
+
901
+ | Safeguard | Default | Description |
902
+ |-----------|---------|-------------|
903
+ | Hysteresis | 15 points | Minimum score improvement to switch |
904
+ | Cooldown | 20 seconds | Wait time between switches |
905
+ | Stability Window | 8 seconds | Transport must be stable before switching |
906
+
907
+ ---
908
+
909
+ ## Mesh Networking
910
+
911
+ ### Cluster Architecture
912
+
913
+ Devices organize into **clusters** (groups of nearby connected peers). Connections between clusters are handled by **bridge** connections.
914
+
915
+ **Connection Roles:**
916
+ - `MEMBER` - Intra-cluster connection (devices in same neighborhood)
917
+ - `BRIDGE` - Inter-cluster connection (bridges different neighborhoods)
918
+
919
+ ### How It Works
920
+
921
+ 1. **Discovery**: Devices broadcast BLE advertisements with mesh metadata (degree, free slots, battery, uptime)
922
+ 2. **Cluster Detection**: Each device computes a cluster signature from connected peer hashes
923
+ 3. **Connection Decisions**: MeshController evaluates candidates - prioritizes bridging different clusters
924
+ 4. **Rebalancing**: Periodically swaps lower-quality peers for better candidates or bridge opportunities
925
+ 5. **Delivery**: Messages sent to connected peers
926
+
927
+ ### Connection Budget
928
+
929
+ - Default: 4 connections per device
930
+ - Minimum: 1 connection maintained
931
+ - Connections are scored and rebalanced every ~15 seconds
932
+ - Bridge candidates get priority when clusters need unifying
933
+
934
+ ### Peer Scoring
935
+
936
+ | Factor | Weight | Description |
937
+ |--------|--------|-------------|
938
+ | RSSI | 35% | Signal strength to peer |
939
+ | Availability | 20% | Free connection slots |
940
+ | Uptime | 15% | How long peer has been active |
941
+ | Battery | 15% | Peer's battery level |
942
+ | Stability | 10% | Connection reliability history |
943
+ | Load | 5% | Current processing load |
944
+
945
+ **Bridge Favor**: Candidates from different clusters get a score bonus (`bridgeFavor: 0.1`) to encourage network unification.
946
+
947
+ ### Message TTL
948
+
949
+ - Default: 8 hops
950
+ - Messages are dropped when TTL reaches 0
951
+ - Prevents infinite message circulation
952
+
953
+ ---
954
+
955
+ ## Reliability Layer
956
+
957
+ ### Acknowledgments
958
+
959
+ - Messages require ACK for delivery confirmation
960
+ - Default timeout: 5 seconds
961
+ - `message_delivered` event fires on ACK receipt
962
+
963
+ ### Retry Queue
964
+
965
+ - Failed messages are retried with exponential backoff
966
+ - Initial delay: 1 second
967
+ - Maximum delay: 30 seconds
968
+ - Maximum retries: 5
969
+
970
+ ### Deduplication
971
+
972
+ Prevents duplicate message processing:
973
+
974
+ - **Bloom Filter Mode**: Space-efficient, ~1% false positive rate
975
+ - **HashMap Mode**: Exact tracking, configurable capacity
976
+
977
+ ---
978
+
979
+ ## File Transfer
980
+
981
+ ### Sending Files
982
+
983
+ ```typescript
984
+ const fileId = await protocol.sendFile({
985
+ filePath: '/path/to/file.pdf',
986
+ recipient: 'user456',
987
+ fileName: 'document.pdf', // optional
988
+ });
989
+
990
+ protocol.on('file_progress', (event) => {
991
+ console.log(`${event.percentage}% complete`);
992
+ });
993
+ ```
994
+
995
+ ### Managing Transfers
996
+
997
+ ```typescript
998
+ const progress = await protocol.getFileProgress(fileId);
999
+ await protocol.cancelFileTransfer(fileId);
1000
+ ```
1001
+
1002
+ ---
1003
+
1004
+ ## Troubleshooting
1005
+
1006
+ ### Messages Not Delivering
1007
+
1008
+ 1. Verify both devices have protocol started
1009
+ 2. Check they're within BLE range (~10-30m)
1010
+ 3. Ensure TTL is sufficient for network size
1011
+ 4. Monitor `message_failed` events for retry information
1012
+
1013
+ ### No Peers Discovered
1014
+
1015
+ 1. Verify Bluetooth is enabled: `await protocol.isBluetoothEnabled()`
1016
+ 2. Check permissions are granted
1017
+ 3. Ensure background modes enabled (iOS)
1018
+ 4. Verify devices are within range
1019
+
1020
+ ### Frequent Disconnections
1021
+
1022
+ 1. Check signal strength via `neighbor_discovered` RSSI
1023
+ 2. Increase `stabilityWindowSecs` in DORS config
1024
+ 3. Reduce `rebalanceInterval` frequency
1025
+ 4. Check for BLE interference
1026
+
1027
+ ### High Battery Drain
1028
+
1029
+ 1. Reduce connection count (native mesh config)
1030
+ 2. Verify DORS is selecting BLE over WiFi Direct
1031
+ 3. Check for excessive retry activity
1032
+ 4. Use `setBatteryLevel()` to inform mesh decisions
1033
+
1034
+ ### Transport Not Switching
1035
+
1036
+ 1. Verify transport is enabled in config
1037
+ 2. Check hysteresis threshold isn't too high
1038
+ 3. Ensure cooldown period has elapsed
1039
+ 4. Use `forceTransport()` to test manually
1040
+
1041
+ ### Linking Error
1042
+
1043
+ If you see the linking error message:
1044
+ 1. Run `pod install` (iOS)
1045
+ 2. Rebuild the app after installing
1046
+ 3. Verify not using Expo Go (native modules required)
1047
+
1048
+ ---
1049
+
1050
+ ## License
1051
+
1052
+ ISC
@@ -113,6 +113,8 @@ class BleManager(
113
113
  private var gattServer: BluetoothGattServer? = null
114
114
  private var messageCharacteristic: BluetoothGattCharacteristic? = null
115
115
  private var deviceIdCharacteristic: BluetoothGattCharacteristic? = null
116
+ @Volatile private var isGattServiceReady = false
117
+ @Volatile private var pendingAdvertiseReason: String? = null
116
118
 
117
119
  // Connection registry keeps track of client/server links and desired roles.
118
120
  private val connections = MeshConnectionRegistry()
@@ -412,6 +414,8 @@ class BleManager(
412
414
  // Close GATT server
413
415
  gattServer?.close()
414
416
  gattServer = null
417
+ isGattServiceReady = false
418
+ pendingAdvertiseReason = null
415
419
 
416
420
  updateState(TransportState.STOPPED)
417
421
  protocol.bleStatusChanged(false)
@@ -508,6 +512,9 @@ class BleManager(
508
512
 
509
513
  private fun setupGattServer() {
510
514
  try {
515
+ // Reset flag - service registration is asynchronous
516
+ isGattServiceReady = false
517
+
511
518
  gattServer = bluetoothManager.openGattServer(context, gattServerCallback)
512
519
 
513
520
  // Create message characteristic (write without response + notify)
@@ -530,10 +537,11 @@ class BleManager(
530
537
  service.addCharacteristic(messageCharacteristic)
531
538
  service.addCharacteristic(deviceIdCharacteristic)
532
539
 
533
- // Add service to GATT server
540
+ // Add service to GATT server (asynchronous - callback in onServiceAdded)
534
541
  gattServer?.addService(service)
535
542
 
536
- Log.i(TAG, "GATT server configured")
543
+ Log.i(TAG, "GATT server setup initiated, waiting for service registration callback...")
544
+ emitDiagnostic("info", "GATT server setup initiated")
537
545
  } catch (e: SecurityException) {
538
546
  Log.e(TAG, "Permission denied while setting up GATT server", e)
539
547
  throw e
@@ -644,6 +652,16 @@ class BleManager(
644
652
  private fun startAdvertising(reason: String = "manual") {
645
653
  if (isAdvertising) return
646
654
 
655
+ // Wait for GATT service to be ready before advertising
656
+ if (!isGattServiceReady) {
657
+ pendingAdvertiseReason = reason
658
+ if (logThrottler.shouldLog("advert_waiting_gatt", intervalMs = 5000)) {
659
+ Log.i(TAG, "Waiting for GATT service to be ready before advertising (reason: $reason)")
660
+ emitDiagnostic("info", "Waiting for GATT service registration", mapOf("reason" to reason))
661
+ }
662
+ return
663
+ }
664
+
647
665
  try {
648
666
  val settings = AdvertiseSettings.Builder()
649
667
  .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
@@ -1456,6 +1474,33 @@ class BleManager(
1456
1474
  // MARK: - GATT Server Callback
1457
1475
 
1458
1476
  private val gattServerCallback = object : BluetoothGattServerCallback() {
1477
+ override fun onServiceAdded(status: Int, service: BluetoothGattService?) {
1478
+ if (status == BluetoothGatt.GATT_SUCCESS) {
1479
+ Log.i(TAG, "✅ GATT service added successfully: ${service?.uuid}")
1480
+ emitDiagnostic("info", "GATT service registered successfully", mapOf(
1481
+ "serviceUUID" to (service?.uuid?.toString() ?: "unknown")
1482
+ ))
1483
+ isGattServiceReady = true
1484
+
1485
+ // Start advertising now that the service is ready
1486
+ val reason = pendingAdvertiseReason
1487
+ if (reason != null) {
1488
+ pendingAdvertiseReason = null
1489
+ Log.i(TAG, "📡 Starting deferred advertising after GATT service ready")
1490
+ mainHandler.post {
1491
+ startAdvertising("gatt_service_ready")
1492
+ }
1493
+ }
1494
+ } else {
1495
+ Log.e(TAG, "❌ Error adding GATT service: status=$status")
1496
+ emitDiagnostic("error", "Error adding GATT service", mapOf(
1497
+ "status" to status,
1498
+ "serviceUUID" to (service?.uuid?.toString() ?: "unknown")
1499
+ ))
1500
+ isGattServiceReady = false
1501
+ }
1502
+ }
1503
+
1459
1504
  override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
1460
1505
  when (newState) {
1461
1506
  BluetoothProfile.STATE_CONNECTED -> {
@@ -121,6 +121,8 @@ public class BleManager: NSObject, TransportManager {
121
121
  private var isAdvertising = false
122
122
  private var centralReady = false
123
123
  private var peripheralReady = false
124
+ private var isGattServiceReady = false
125
+ private var pendingAdvertiseAfterServiceReady = false
124
126
  private var subscribedCentrals: Set<UUID> = []
125
127
  private var lastMeshAdvertisement: MeshAdvertisementData?
126
128
 
@@ -397,6 +399,8 @@ public class BleManager: NSObject, TransportManager {
397
399
 
398
400
  centralReady = false
399
401
  peripheralReady = false
402
+ isGattServiceReady = false
403
+ pendingAdvertiseAfterServiceReady = false
400
404
 
401
405
  updateState(.stopped)
402
406
  emitDiagnostic("info", "BLE transport stopped")
@@ -705,6 +709,16 @@ public class BleManager: NSObject, TransportManager {
705
709
 
706
710
  setupGattServer()
707
711
 
712
+ // Wait for GATT service to be ready before advertising
713
+ guard isGattServiceReady else {
714
+ pendingAdvertiseAfterServiceReady = true
715
+ if logThrottler.shouldLog(key: "advert_waiting_gatt", interval: 5) {
716
+ print("[BleManager] Waiting for GATT service to be ready before advertising (reason: \(reason))")
717
+ emitDiagnostic("info", "Waiting for GATT service registration", context: ["reason": reason])
718
+ }
719
+ return
720
+ }
721
+
708
722
  let meshData = meshController.advertisement()
709
723
  lastMeshAdvertisement = meshData
710
724
  var advertisementData: [String: Any] = [
@@ -763,10 +777,13 @@ public class BleManager: NSObject, TransportManager {
763
777
 
764
778
  private func setupGattServer() {
765
779
  guard let peripheral = peripheralManager else { return }
766
- if messageCharacteristic != nil && deviceIdCharacteristic != nil {
780
+ if messageCharacteristic != nil && deviceIdCharacteristic != nil && isGattServiceReady {
767
781
  return
768
782
  }
769
783
 
784
+ // Reset flag - service registration is asynchronous
785
+ isGattServiceReady = false
786
+
770
787
  // Create message characteristic (write without response + notify)
771
788
  messageCharacteristic = CBMutableCharacteristic(
772
789
  type: MESSAGE_CHAR_UUID,
@@ -788,10 +805,10 @@ public class BleManager: NSObject, TransportManager {
788
805
  let service = CBMutableService(type: SERVICE_UUID, primary: true)
789
806
  service.characteristics = [messageCharacteristic!, deviceIdCharacteristic!]
790
807
 
791
- // Add service to peripheral manager
808
+ // Add service to peripheral manager (asynchronous - callback in peripheralManager(_:didAdd:error:))
792
809
  peripheral.add(service)
793
- print("[BleManager] GATT server configured")
794
- emitDiagnostic("info", "GATT server configured")
810
+ print("[BleManager] GATT server setup initiated, waiting for service registration callback...")
811
+ emitDiagnostic("info", "GATT server setup initiated")
795
812
  }
796
813
 
797
814
  private func startFragmentPolling() {
@@ -1523,7 +1540,20 @@ extension BleManager: CBCentralManagerDelegate {
1523
1540
  pruneMeshObservations(now: now)
1524
1541
  meshController.observeAdvertisement(meshMetadata, rssi: Int(rssiValue))
1525
1542
 
1526
- let decision = meshController.shouldInitiateOutbound(metadata: meshMetadata, rssi: Int(rssiValue))
1543
+ // When there's no metadata (iOS/Android advertising without service data),
1544
+ // still try to connect - metadata will be exchanged via GATT after connection
1545
+ let decision: MeshController.MeshDecision
1546
+ if meshMetadata == nil {
1547
+ // No metadata in advertisement - allow basic connection to exchange info via GATT
1548
+ decision = MeshController.MeshDecision(
1549
+ intent: .intraCluster,
1550
+ reason: "no_metadata_in_advert",
1551
+ evictPeerId: nil
1552
+ )
1553
+ } else {
1554
+ decision = meshController.shouldInitiateOutbound(metadata: meshMetadata, rssi: Int(rssiValue))
1555
+ }
1556
+
1527
1557
  guard decision.intent != .rejected else {
1528
1558
  if logThrottler.shouldLog(key: "mesh_skip_\(peripheral.identifier.uuidString)", interval: 15) {
1529
1559
  print("[BleManager] Skipping \(peripheral.identifier) due to \(decision.reason)")
@@ -1892,6 +1922,32 @@ extension BleManager: CBPeripheralManagerDelegate {
1892
1922
  }
1893
1923
  }
1894
1924
 
1925
+ public func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
1926
+ if let error = error {
1927
+ print("[BleManager] ❌ Error adding GATT service: \(error)")
1928
+ emitDiagnostic("error", "Error adding GATT service", context: [
1929
+ "error": error.localizedDescription,
1930
+ "serviceUUID": service.uuid.uuidString
1931
+ ])
1932
+ isGattServiceReady = false
1933
+ return
1934
+ }
1935
+
1936
+ print("[BleManager] ✅ GATT service added successfully: \(service.uuid)")
1937
+ emitDiagnostic("info", "GATT service registered successfully", context: [
1938
+ "serviceUUID": service.uuid.uuidString
1939
+ ])
1940
+
1941
+ isGattServiceReady = true
1942
+
1943
+ // Start advertising now that the service is ready
1944
+ if pendingAdvertiseAfterServiceReady {
1945
+ pendingAdvertiseAfterServiceReady = false
1946
+ print("[BleManager] 📡 Starting deferred advertising after GATT service ready")
1947
+ startAdvertising(reason: "gatt_service_ready")
1948
+ }
1949
+ }
1950
+
1895
1951
  public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
1896
1952
  for request in requests {
1897
1953
  print("[BleManager] 📨 GATT WRITE REQUEST from \(request.central.identifier), char: \(request.characteristic.uuid), size: \(request.value?.count ?? 0)")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@offline-protocol/mesh-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Offline-first mesh networking SDK with intelligent transport switching for React Native",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",