buildSyncPayload method
Packages the local routing table and syncable messages into a
compressed bytes payload ready to send via Nearby.sendBytesPayload.
If peerNodeId is provided, uses per-peer dedup to exclude messages
already delivered to that peer. Otherwise sends all syncable messages.
Returns the gzip-compressed JSON bytes.
Implementation
Future<Uint8List> buildSyncPayload({String? peerNodeId}) async {
// 1. Gather local state
final allNodes = await _db.getAllNodes();
final allMessages = peerNodeId != null
? await _db.getMessagesForSync(peerNodeId)
: await _db.getAllSyncableMessages();
// 2. Build self-entries — one per subscribed group so multi-hop peers
// learn about ALL of our group memberships.
final now = DateTime.now().millisecondsSinceEpoch;
final selfEntries = localGroupIds.isNotEmpty
? localGroupIds
.map(
(gid) => SyncNodeEntry(
nodeId: localNodeId,
role: localRole.value,
groupId: gid,
lastSeen: now,
hopCount: 0,
displayName: localDisplayName,
batteryLevel: localBatteryLevel,
hasInternetAccess: localHasInternetAccess,
),
)
.toList()
: [
SyncNodeEntry(
nodeId: localNodeId,
role: localRole.value,
groupId: null,
lastSeen: now,
hopCount: 0,
displayName: localDisplayName,
batteryLevel: localBatteryLevel,
hasInternetAccess: localHasInternetAccess,
),
];
// 3. Build the sync envelope
final payload = SyncPayload(
protocolVersion: kAirpassProtocolVersion,
senderNodeId: localNodeId,
syncTimestamp: now,
nodes: [
// Include self-entries for all subscribed groups
...selfEntries,
// Include all known nodes with incremented hop count
...allNodes.map(
(n) => SyncNodeEntry(
nodeId: n.nodeId,
role: n.role,
groupId: n.groupId,
lastSeen: n.lastSeen,
hopCount: n.hopCount + 1, // One more hop from us
displayName: n.displayName,
batteryLevel: n.batteryLevel,
hasInternetAccess: n.hasInternetAccess,
),
),
],
messages: allMessages
.map((m) {
// Sign outgoing messages that don't have a signature yet
final sig =
m.signature ??
_signer.signMessage(
messageId: m.messageId,
senderId: m.senderId,
targetId: m.targetId,
payload: m.payload,
createdAt: m.createdAt,
);
return SyncMessageEntry(
messageId: m.messageId,
senderId: m.senderId,
targetId: m.targetId,
payloadBase64: base64Encode(m.payload),
status: m.status,
createdAt: m.createdAt,
ttl: m.ttl - 1, // Decrement TTL on relay
signature: sig,
// Media metadata — propagates through the mesh
mediaType: m.mediaType,
mediaFileName: m.mediaFileName,
mediaMimeType: m.mediaMimeType,
mediaFileSize: m.mediaFileSize,
mediaHash: m.mediaHash,
thumbnailBase64: m.mediaThumbnail != null
? base64Encode(m.mediaThumbnail!)
: null,
);
})
.where((m) => m.ttl > 0) // Don't send expired messages
.toList(),
);
// 4. Mark included messages as SENT (transition from PENDING → SENT)
final sentIds = payload.messages.map((m) => m.messageId).toList();
if (sentIds.isNotEmpty) {
await _db.markMessagesAsSent(sentIds);
}
// 5. Serialize → compress
final jsonString = jsonEncode(payload.toJson());
final jsonBytes = utf8.encode(jsonString);
final compressed = gzip.encode(jsonBytes);
return Uint8List.fromList(compressed);
}