विषय-सूची
- उच्च-स्तरीय आर्किटेक्चर
- डेटा मॉडल
- GraphQL API सतह
- पीयर चैट: एक संदेश भेजना
- पीयर चैट: एक संदेश प्राप्त करना
- चैनल चैट: लीडर चुनाव और फ़ैन-आउट
- चैनल सिस्टम संदेश
- चैनल जीवनचक्र
- पीयर ट्रांसपोर्ट परत (LAN → Wi-Fi Aware → BLE)
- पीयर स्टेटस और प्रेज़ेंस
- कैशिंग परत
- फ़ाइल डाउनलोड
- डिज़ाइन पैटर्न सारांश
उच्च-स्तरीय आर्किटेक्चर {#high-level-architecture}
PlainApp चैट सर्वररहित है। हर डिवाइस एक एम्बेडेड Ktor HTTP सर्वर चलाता है, और डिवाइस स्थानीय नेटवर्क, Wi-Fi Aware (NAN), या Bluetooth Low Energy पर सीधे एक-दूसरे से बात करते हैं। कोई रिले सर्वर नहीं, कोई क्लाउड इनबॉक्स नहीं, कोई फ़ोन-नंबर-आधारित पहचान नहीं। डिवाइस एक स्व-जनित clientId द्वारा पहचाने जाते हैं और पेयरिंग के दौरान किए गए Ed25519 + ECDH हैंडशेक के माध्यम से प्रमाणित होते हैं।
दो प्रकार की बातचीत मौजूद हैं:
| Type | Constant | Description |
|---|---|---|
PEER | ChatTargetType.PEER | दो पेयर्ड डिवाइस के बीच 1-से-1 सीधा चैट। |
CHANNEL | ChatTargetType.CHANNEL | एक डिवाइस द्वारा स्वामित्व वाला बहु-पक्षीय समूह चैट; सदस्य एक-दूसरे को संदेश फैलाते हैं। |
एक विशेष "local" लक्ष्य डिवाइस का अपना स्क्रैचपैड है (खुद को नोट्स) — इसे भेजना वायर पर एक नो-ऑप है।
कंपोनेंट मानचित्र
आर्किटेक्चर जानबूझकर स्तरित है:
- UI / GraphQL एंट्री पॉइंट कभी ट्रांसपोर्ट या DB को सीधे नहीं छूते।
ChatManagerएक फ़ैकड़ है — हर कॉलर (UI, GraphQL रिज़ॉल्वर, पीयर रिसीवर) इससे गुज़रता है।ChatSenderएक डिस्पैचर है जोChatTargetTypeपर शाखित होता है और पीयर या चैनल प्रेषकों को सौंपता है।- ट्रांसपोर्ट परत सर्किट ब्रेकिंग के साथ एक प्लग करने योग्य रणनीति श्रृंखला है, इसलिए एक अविश्वसनीय Wi-Fi Aware लिंक कभी उस संदेश को ब्लॉक नहीं करता जो BLE पर जा सकता है।
डेटा मॉडल {#data-model}
ChatTarget
राउटिंग की सबसे छोटी इकाई एक ChatTarget है — एक (toId, type) जोड़ा जहाँ type या तो PEER या CHANNEL है। यह एक encodedToId (peer:<id> या channel:<id>) उजागर करता है जिसका UI एक स्थिर राउटिंग कुंजी के रूप में उपयोग करता है (उदा. TempData.activeToId ताकि रिसीवर जान सके कि नोटिफ़िकेशन उत्सर्जित करना है या नहीं), एक isLocal() जाँच (toId == "local"), और एक parseId कम्पेनियन जो संग्रहीत स्ट्रिंग से लक्ष्य का पुनर्निर्माण करता है।
डेटाबेस टेबल्स
सभी दृढ़ता Room उपयोग करती है। चैट के लिए तीन टेबल्स महत्वपूर्ण हैं:
| Table | Entity | Purpose |
|---|---|---|
chats | DChat | प्रति संदेश एक पंक्ति (टेक्स्ट / छवि / फ़ाइल)। |
chat_channels | DChatChannel | प्रति समूह चैनल एक पंक्ति। |
peers | DPeer | प्रति ज्ञात डिवाइस एक पंक्ति (पेयर्ड या केवल-चैनल)। |
कुछ बातें ध्यान देने योग्य:
- पहचान
clientIdहै, कभी MAC नहीं। Android हर कनेक्शन पर BLE MAC को यादृच्छिक बनाता है, इसलिए डेटाबेस एक स्थिर 13-अक्षर स्व-जनित id का उपयोग करता है। केवल एक 8-बाइट SHA-256 उपसर्ग (shortId) डिस्कवरी की अनुमति देने के लिए BLE पर प्रसारित होता है। status="channel"पीयर एक चैनल के सदस्य हैं जिनके साथ इस डिवाइस ने कभी सीधे पेयर नहीं किया। उनकीkeyखाली है — वे एक जोड़ीदार साझा कुंजी के बजाय चैनल कुंजी उपयोग करके प्रमाणित होते हैं।owner="me"एक सेंटिनल है जो एक नए-स्थापित डिवाइस को अपनेclientIdस्थिर होने से पहले स्वामी के रूप में कार्य करने देता है;isOwnedByMe()दोनों"me"औरTempData.clientIdको स्वीकार करता है।
GraphQL API सतह {#graphql-api-surface}
PlainApp दो GraphQL स्कीमा उजागर करता है:
- Web GraphQL (
addChatChannelSchema+addChatMessageSchema) — स्थानीय Ktor सर्वर द्वारा ब्राउज़र UI औरapitest/हार्नेस को सर्व किया जाता है। ChaCha20-कूटलेखित टोकन द्वारा प्रमाणित। - Peer GraphQL (
PeerGraphQLService.applyPeerSchema) —/peer_graphqlपर अन्य डिवाइस के लिए कूटलेखित पीयर ट्रांसपोर्ट पर उजागर। Ed25519 हस्ताक्षर + ChaCha20 बॉडी एन्क्रिप्शन द्वारा प्रमाणित।
दोनों स्कीमा एक ही बिज़नेस लॉजिक सिंगलटन (ChannelManager, ChatMessageReceiver, …) साझा करते हैं लेकिन अलग सतह उजागर करते हैं क्योंकि ट्रस्ट मॉडल भिन्न है: वेब GraphQL स्थानीय UI पर भरोसा करता है, जबकि पीयर GraphQL केवल क्रिप्टोग्राफ़िक रूप से प्रमाणित पीयर पर भरोसा करता है।
वेब GraphQL सतह (चैट)
Queries: chatChannels (सभी चैनल सूचीबद्ध करें), chatItems(id) (किसी लक्ष्य के लिए संदेश — id "local", peer:<id>, या channel:<id> है), और latestChatItems (सभी चैट में पूर्वावलोकन)।
चैट mutations: sendChatItem(toId, content), deleteChatItem(id), deleteChatItems(query), और retryChatItem(id)।
चैनल mutations: createChatChannel(name), updateChatChannel(id, name), deleteChatChannel(id), leaveChatChannel(id), addChatChannelMember(id, peerId), removeChatChannelMember(id, peerId), acceptChatChannelInvite(id), और declineChatChannelInvite(id)।
पीयर GraphQL सतह (ट्रांसपोर्ट)
/peer_graphql पर उजागर और Ed25519 हस्ताक्षर + ChaCha20 बॉडी एन्क्रिप्शन द्वारा प्रमाणित। केवल तीन mutations ट्रांसपोर्ट सीमा पार करते हैं: createChatItem(content) (एक आने वाला पीयर संदेश), channelSystemMessage(type, payload) (चैनल जीवनचक्र घटनाएँ जैसे invite/leave), और startAware (पीयर को उसकी Wi-Fi Aware सेवा शुरू करने के लिए एक नज पूछना ताकि एक तेज़ ट्रांसपोर्ट काबू ले सके)।
c-id HTTP हेडर भेजने वाले का clientId ले जाता है; c-cid हेडर एक चैनल id ले जाता है जब अनुरोध चैनल-स्कोपड हो (ताकि रिसीवर डिक्रिप्शन के लिए जोड़ीदार पीयर कुंजी के बजाय चैनल कुंजी चुने)।
पीयर चैट: एक संदेश भेजना {#peer-chat-sending-a-message}
जब उपयोगकर्ता एक पीयर बातचीत में Send टैप करता है, तो कॉल श्रृंखला है:
हर हॉप पर लागू मुख्य अपरिवर्तनीयताएँ:
ChatManager.createChatItemहमेशा पहले एक पंक्ति डालता है, फिर भेजता है। इसका अर्थ है कि UI को तुरंत एक "pending" बुलबुला दिखता है और संदेश ऐप क्रैश से बचा रहता है भले ही वितरण अभी तक न हुआ हो।PeerGraphQLClient.buildSignedRequestsignature|timestamp|requestJsonरूप का एक एनवलप बनाता है। हस्ताक्षर"$timestamp$requestJson"पर Ed25519 है, टाइमस्टैम्प को बॉडी से बाँधता है ताकि इसे एक नए टाइमस्टैम्प के साथ रीप्ले न किया जा सके।PeerTransportRouter.sendट्रांसपोर्ट कोLan → WifiAware → Bleक्रम में पुनरावृत्त करता है। हर ट्रांसपोर्ट राउटर को अगला आज़माने देने के लिएTransportUnavailableफेंक सकता है।- प्राप्त करने वाले पक्ष पर,
PeerChatParser.decryptजाँचता है कि टाइमस्टैम्प±5 minके भीतर है और GraphQL mutation कार्यान्वित होने से पहले Ed25519 हस्ताक्षर सत्यापित करता है। ChatMessageReceiver.receiveएकseenSignaturesसमूह रखता है जो"$fromPeerId|$signature|$timestamp"द्वारा की-ड है और डुप्लिकेट परReplayedMessageExceptionफेंकता है — आवश्यक क्योंकि ट्रांसपोर्ट एक ही पेलोड दो बार वितरित कर सकता है (LAN + BLE)।
यदि PeerChatSender.send एक ग़ैर-शून्य त्रुटि स्ट्रिंग लौटाता है, तो ChatSender triggerPeerRediscovery(peerId) को कॉल करता है, जो एक डायरेक्टेड, कूटलेखित DISCOVER ब्रॉडकास्ट फायर करता है ताकि पीयर अपना वर्तमान IP/port पुनः-घोषित कर सके।
पीयर चैट: एक संदेश प्राप्त करना {#peer-chat-receiving-a-message}
इनबाउंड अनुरोध स्थानीय Ktor सर्वर के /peer_graphql रूट पर आते हैं, जिन्हें PeerGraphQLService संभालता है:
नोटिफ़िकेशन
emitNotificationIfNeeded अंतिम चरण है। यह नोटिफ़िकेशन को दबाता है जब TempData.activeToId == targetId (यानी उपयोगकर्ता वर्तमान में उस बातचीत को देख रहा है) या जब canShowNotifications() ग़लत हो। चैनल नोटिफ़िकेशन भेजने वाले के नाम के साथ उपसर्ग होते हैं।
चैनल चैट: लीडर चुनाव और फ़ैन-आउट {#channel-chat-leader-election--fan-out}
चैनल बहु-पक्षीय लेकिन सर्वररहित हैं। हर सदस्य का एक ही संदेश N बार फैलाने से बचने के लिए, प्रेषक पक्ष एक एकल लीडर चुनता है जिसका काम सभी जुड़े सदस्यों को प्रसारित करना है।
लीडर चुनाव एल्गोरिदम (DChatChannel.electLeader)
- उन जुड़े सदस्यों पर फ़िल्टर करें जो वर्तमान में ऑनलाइन हैं (स्थानीय डिवाइस हमेशा ऑनलाइन माना जाता है)।
- यदि स्वामी ऑनलाइन जुड़े सदस्यों में है → स्वामी लीडर है।
- अन्यथा, लीडर वह ऑनलाइन जुड़ा सदस्य है जिसका सबसे छोटा
clientIdहै (निर्धारक टाईब्रेक, किसी समन्वय की आवश्यकता नहीं)। - यदि कोई ऑनलाइन जुड़ा सदस्य नहीं है तो
nullलौटाता है।
प्रेषण प्रवाह
आख़िर लीडर क्यों?
कल्पना करें एक 5-सदस्यीय चैनल जहाँ हर कोई हर दूसरे को प्रसारित करता है: एक एकल संदेश 20 नेटवर्क राउंड ट्रिप और हर सदस्य पर 4 डुप्लिकेट प्रतिलिपि उत्पन्न करेगा। एक लीडर चुनकर, केवल वही डिवाइस फ़ैन-आउट करता है — प्रेषक या तो फ़ैन-आउट स्वयं करता है (यदि वह लीडर है) या लीडर को एक एकल प्रतिलिपि रिले करता है, जो तब फैलाता है।
यदि लीडर ऑफ़लाइन है, तो प्रेषक Result.NoLeader पर वापस आता है, पीयर पुनः-खोज ट्रिगर करता है (ताकि लीडर का IP मिल सके), और उपयोगकर्ता को पुनः प्रयास करने देने के लिए स्टेटस साफ़ करता है।
चैनल कुंजी राउटिंग
चैनल संदेश जोड़ीदार पीयर कुंजी के बजाय चैनल की ChaCha20 कुंजी से कूटलेखित होते हैं। यही कारण है कि एक सदस्य जिसने दूसरे सदस्यों से केवल चैनल के माध्यम से मुलाकात की है (कभी 1-से-1 पेयर नहीं) संदेश प्राप्त कर सकता है — उसकी peers पंक्ति में status="channel" और key="" है। प्रेषक c-cid HTTP हेडर को चैनल id पर सेट करता है; रिसीवर जोड़ीदार कुंजी के बजाय ChannelCacher.getKeyBytes(channelId) खोजता है।
प्रति-प्राप्तकर्ता पुनः प्रयास
हर sendToMember एक DMessageDeliveryResult लौटाता है। सकल DMessageStatusData चैट आइटम के status_data JSON के रूप में स्थायी किया जाता है। UI "Alice, Bob को वितरित; Carol के लिए विफल" दिखाता है और उपयोगकर्ता को विशेष रूप से Carol के लिए Retry टैप करने देता है — ChatManager.sendToChannelMembers पुनः प्रयास सबसेट के लिए sendToRecipients को पुनः-चलाता है और मौजूदा परिणामों के साथ नए परिणामों को मिलाता है, केवल पुनः प्रयास किए गए पीयर को प्रतिस्थापित करता है।
चैनल सिस्टम संदेश {#channel-system-messages}
चैनल नियंत्रण-तल संदेश (invite, accept, decline, update, kick, leave) पीयर GraphQL channelSystemMessage mutation पर आदान-प्रदान होते हैं। वे एक type स्ट्रिंग द्वारा टाइप किए गए JSON पेलोड हैं:
| Type | Direction | Signed? | Purpose |
|---|---|---|---|
channel_invite | स्वामी → आमंत्रित व्यक्ति | Yes | एक पीयर को आमंत्रित करें; चैनल कुंजी + सदस्य ले जाता है। |
channel_invite_accept | आमंत्रित व्यक्ति → स्वामी | No | स्वीकृति; स्वीकार करने वाले की सार्वजनिक कुंजी ले जाता है। |
channel_invite_decline | आमंत्रित व्यक्ति → स्वामी | No | अस्वीकार; स्वामी सदस्य हटाता है। |
channel_update | स्वामी → सभी सदस्य | Yes | सदस्यता/नाम परिवर्तन प्रसारण। |
channel_kick | स्वामी → निकाले गए पीयर | Yes | लक्षित किक; चैनल हटाने पर भी प्रसारित। |
channel_leave | सदस्य → स्वामी | No | सदस्य-प्रेरित छोड़ने की सूचना। |
हस्ताक्षरित पेलोड फ़ॉर्मैट
तीन हस्ताक्षरित प्रकार (invite, update, kick) एक विहित पाइप-सीमांकित स्ट्रिंग उपयोग करते हैं: "$channelId|$version|$action|$target", जहाँ action invite, update, kick में से एक है, और target आमंत्रित/निकाले गए पीयर id है (प्रसारण kick के लिए खाली)।
स्वामी इस स्ट्रिंग को अपनी Ed25519 कुंजी से हस्ताक्षरित करता है। रिसीवर किसी भी संदेश को अस्वीकार करते हैं जहाँ channel.owner != fromId इससे पहले कि हस्ताक्षर जाँचा भी जाए, और ChannelUpdate पेलोड अस्वीकार करते हैं जिनका version स्थानीय संस्करण से ≤ है (अव्यवस्थित वितरण के विरुद्ध पुराने-संस्करण गार्ड)।
आलसी पीयर हाइड्रेशन
ChannelInvite और ChannelUpdate एक memberPeers: List<MemberPeerInfo> सूची ले जाते हैं — हर सदस्य के लिए हल्की पीयर जानकारी (id, name, publicKey, deviceType, ip, port)। रिसीवर का ensureChannelPeer किसी भी सदस्य के लिए status="channel" वाली एक DPeer पंक्ति बनाता है जिसे उसने पहले कभी नहीं देखा। यह महत्वपूर्ण है क्योंकि फ़ैन-आउट राउटिंग को संदेश भेजने के लिए हर सदस्य की पीयर रिकॉर्ड चाहिए।
चैनल जीवनचक्र {#channel-lifecycle}
पीयर ट्रांसपोर्ट परत (LAN → Wi-Fi Aware → BLE) {#peer-transport-layer-lan--wi-fi-aware--ble}
PeerTransportRouter सर्किट ब्रेकिंग के साथ एक रणनीति श्रृंखला है। ट्रांसपोर्ट की क्रमित सूची है:
LanTransport— पहली पसंद। OkHttp को HTTPS पर ChaCha20 क्रिप्टो इंटरसेप्टर के साथ उपयोग करता है। जबpeer.ipखाली हो तो पूरी तरह छोड़ दिया जाता है (क्रॉस-सबनेट पीयर जिसे हमने अभी तक खोजा नहीं)।WifiAwareTransport(केवल Android 13+) — Wi-Fi Aware (NAN) डेटा पथ उपयोग करता है। जब पीयर काawareRunningफ़्लैग ग़लत हो तो फास्ट-स्किप (BLE प्रीवार्मर स्कैन द्वारा रिफ़्रेश)। पीयर का IPv6 एक कस्टम DNS के माध्यम से हल होता है जो होस्टनामplain-aware-peerको लिंक-लोकल पते पर मैप करता है।BleTransport— किसी भी पेयर्ड पीयर के लिए गारंटीड फ़ॉलबैक। GATT पर चंक्ड RPC स्ट्रीम करता है। धीमा लेकिन बिना किसी IP कनेक्टिविटी के काम करता है।
यह क्रम क्यों?
- LAN सबसे तेज़ है (एकल HTTPS राउंड ट्रिप, ~10 ms टाइमआउट)।
- Wi-Fi Aware मध्यम है (डेटा-पथ सेटअप ~5 सेकंड, फिर ~10 ms राउंड ट्रिप) और क्रॉस-सबनेट काम करता है (उदा. एक डिवाइस गेस्ट Wi-Fi पर, दूसरा IoT Wi-Fi पर)। तेज़ी से छोड़ने के लिए ट्यून किया गया जब पीयर की Aware सेवा नहीं चल रही, 10 सेकंड के
buildLinkटाइमआउट से बचता है। - BLE सबसे धीमा है लेकिन बिना किसी IP कनेक्टिविटी के काम करता है — बिना Wi-Fi के भी, संदेश फिर भी पहुँच जाता है। पेयर्ड पीयर के लिए गारंटीड फ़ॉलबैक के रूप में उपयोग।
सर्किट ब्रेकर सुनिश्चित करता है कि एक अविश्वसनीय ट्रांसपोर्ट (विशेष रूप से नेटवर्क चर्न के दौरान Wi-Fi Aware) 2 विफलताओं के बाद 30 सेकंड के लिए छोड़ दिया जाता है, इसलिए फ़ॉलबैक जल्दी होता है बजाय बार-बार 10 सेकंड के टाइमआउट की प्रतीक्षा के।
Wi-Fi Aware हैंडशेक
AwareSession एक डेटा पथ खोलने से पहले दो-संदेश हैंडशेक करता है:
MSG_HELLO(सब्सक्राइबर → पब्लिशर): "मैं तुम्हें देख रहा हूँ, यह मेरा पीयर हैंडल है।"MSG_READY(पब्लिशर → सब्सक्राइबर): "मैंने अपना नेटवर्क स्पेसिफ़ायर पंजीकृत कर लिया है, तुम अबrequestNetworkकर सकते हो।"
यह Android फ़्रेमवर्क की ~500 ms विंडो के भीतर दोनों पक्षों के connectivityManager.requestNetwork(...) कॉल को सिंक्रोनाइज़ करता है। सब्सक्राइबर वह पक्ष है जिसका clientId छोटा है (निर्धारक भूमिका विभाजन — दोनों पक्ष बिना समन्वय के सहमत होते हैं), और यह पुनः प्रयास लूप का स्वामी है।
पीयर स्टेटस और प्रेज़ेंस {#peer-status--presence}
प्रेज़ेंस लंबे जीवित WebSocket कनेक्शन के माध्यम से ट्रैक होती है। प्रत्येक जोड़े का केवल एक पक्ष सॉकेट खोलता है — निर्धारक नियम TempData.clientId < peer.id द्वारा तय। दूसरा पक्ष /peer_status पर इनबाउंड कनेक्शन स्वीकार करता है।
PeerCacher.onlineMap प्रेज़ेंस के लिए सत्य का स्रोत है। यह onlinePeerIds: StateFlow<Set<String>> के रूप में उजागर है, जिसे चैनल लीडर चुनाव (electLeader(onlinePeerIds, myId)) उपभोग करता है।
कैशिंग परत {#caching-layer}
दो कैश डेटाबेस टेबल्स को मेमोरी में प्रतिबिंबित करते हैं और StateFlow उजागर करते हैं जिन्हें Compose सीधे एकत्रित करता है:
कॉपी-ऑन-राइट क्यों?
Kotlin का MutableStateFlow.distinctUntilChanged संरचनात्मक समानता उपयोग करता है। यदि हम DPeer को जगह पर ही बदलते, तो व्युत्पन्न pairedPeers सूची में पहले और बाद में वही DPeer संदर्भ होता, और distinctUntilChanged कोई अंतर नहीं देखता और उत्सर्जन दबा देता। एंटिटी को पहले कॉपी करके, कॉपी को बदलकर, और मानचित्र प्रविष्टि को एक नए PeerRuntime/ChannelRuntime से प्रतिस्थापित करके, व्युत्पन्न सूची को नई-संदर्भों-की-नई-सूची मिलती है और फ़्लो फायर करता है।
फ़ाइल डाउनलोड {#file-downloads}
इनबाउंड फ़ाइल/छवि संदेश एक सीमित वर्कर पूल द्वारा स्वतः डाउनलोड होते हैं। हर डाउनलोड उपलब्ध किसी भी ट्रांसपोर्ट (PeerTransportRouter.downloadFile) से स्ट्रीम होता है और एक टेम्प फ़ाइल में लिखता है, फिर ऐप के मीडिया स्टोर में आयात करता है और चैट आइटम के uri फ़ील्ड को पैच करता है।
ट्रांसपोर्ट-अज्ञेयवादी स्ट्रीमिंग
DownloadedResponse(status, ByteReadChannel, onClose): AutoCloseable एब्सट्रैक्शन LAN और Wi-Fi Aware को लाइव HTTP बॉडी स्ट्रीम करने देता है, जबकि BLE उसी ByteReadChannel के माध्यम से चंक्ड RPC (16 KiB चंक्स GET /fs?id=…&offset=…&length=… के माध्यम से) स्ट्रीम करता है। onClose कॉलबैक BLE को अपनी पृष्ठभूमि डाउनलोड कोरूटीन को रद्द करने देता है जब उपभोक्ता प्रतिक्रिया को जल्दी बंद करता है (उदा. pause पर)।
डिज़ाइन पैटर्न सारांश {#design-patterns-recap}
| Pattern | Where | Why |
|---|---|---|
| Façade | ChatManager | एकल एंट्री पॉइंट; कॉलर कभी DB/ट्रांसपोर्ट को सीधे नहीं छूते। |
| Strategy + Chain of Resp. | PeerTransportRouter + LanTransport/WifiAwareTransport/BleTransport | TransportUnavailable को गिरने के संकेत के रूप में उपयोग करते हुए प्लग करने योग्य ट्रांसपोर्ट। |
| Circuit Breaker | PeerCircuitBreaker | 2 विफलताएँ / 30 सेकंड एक (peer, transport) लेग खोलते हैं ताकि Wi-Fi Aware फ़ॉलबैक को ब्लॉक न करे। |
| State Machine | PeerStatusManager.PeerState, AwarePeerLink.LinkState | सॉकेट जीवनचक्र और NDP लिंक जीवनचक्र के लिए स्पष्ट संक्रमण। |
| Producer/Consumer + Pool | DownloadQueue (3 workers, Channel.BUFFERED) | फ़ाइल डाउनलोड के लिए सीमित समवर्तीता। |
| Observer / Reactive | हर जगह StateFlow | Compose सीधे एकत्रित करता है; कोई मैन्युअल रिफ़्रेश नहीं। |
| Replay Protection | ChatMessageReceiver.seenSignatures, PeerChatParser.MAX_TIMESTAMP_DIFF_MS | LAN+BLE दोहरी वितरण से डुप्लिकेट हटाएँ; विंडो-बाह के टाइमस्टैम्प अस्वीकार करें। |
| Exponential Backoff | PeerStatusManager.scheduleReconnect | min(60 s, 1 s × 2^min(n-1, 6)) — 64 s पर सीमित। |
| Copy-on-Write | PeerCacher.mutatePeer, ChannelCacher.mutateChannel | हर बदलाव पर StateFlow.distinctUntilChanged को फायर करने पर मजबूर। |
| Signed Envelope | PeerGraphQLClient.buildSignedRequest | signature|timestamp|body — टाइमस्टैम्प को बॉडी से बाँधता है ताकि रीप्ले रोका जा सके। |
| Deterministic Role Split | TempData.clientId < peer.id | WebSocket क्लाइंट बनाम सर्वर, और Wi-Fi Aware सब्सक्राइबर बनाम पब्लिशर तय करता है। |
| Lazy Hydration | invite/update पर ensureChannelPeer | अनदेखे चैनल सदस्यों के लिए peers पंक्तियाँ बनाता है ताकि फ़ैन-आउट राउटिंग काम करे। |
| Encrypted Identity | LANDiscoverManager.discoverSpecificDevice | डायरेक्टेड DISCOVER लक्ष्य id को पीयर कुंजी से कूटलेखित करता है — केवल लक्ष्य इसे पहचानता है। |
आगे पढ़ने के लिए
- Pairing Flow — दो डिवाइस विश्वास कैसे स्थापित करते हैं और इस लेख में हर ट्रांसपोर्ट द्वारा उपयोग की जाने वाली साझा ChaCha20 कुंजी का आदान-प्रदान कैसे करते हैं।
apitest/groups/chat-messages.shऔरapitest/groups/chat-channels.sh— निष्पादन योग्य टेस्ट प्लान जो हर GraphQL mutation को अंत-से-अंत अभ्यास करता है।