buildSyncPayload method

Future<Uint8List> buildSyncPayload({
  1. String? peerNodeId,
})

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);
}