সূচিপত্র
- হাই-লেভেল আর্কিটেকচার
- ডেটা মডেল
- GraphQL API সারফেস
- পিয়ার চ্যাট: একটি বার্তা পাঠানো
- পিয়ার চ্যাট: একটি বার্তা গ্রহণ
- চ্যানেল চ্যাট: লিডার ইলেকশন ও ফ্যান-আউট
- চ্যানেল সিস্টেম বার্তা
- চ্যানেল লাইফসাইকেল
- পিয়ার ট্রান্সপোর্ট লেয়ার (LAN → Wi-Fi Aware → BLE)
- পিয়ার স্ট্যাটাস ও প্রেজেন্স
- ক্যাশিং লেয়ার
- ফাইল ডাউনলোড
- ডিজাইন প্যাটার্ন সারাংশ
হাই-লেভেল আর্কিটেকচার {#high-level-architecture}
PlainApp চ্যাট সার্ভারবিহীন। প্রতিটি ডিভাইস একটি এম্বেডেড Ktor HTTP সার্ভার চালায়, এবং ডিভাইসগুলো সরাসরি লোকাল নেটওয়ার্ক, Wi-Fi Aware (NAN), বা Bluetooth Low Energy-এর মাধ্যমে একে অপরের সাথে কথা বলে। এখানে কোনো রিলে সার্ভার নেই, কোনো ক্লাউড ইনবক্স নেই, কোনো ফোন-নম্বর-ভিত্তিক আইডেন্টিটি নেই। ডিভাইসগুলো একটি স্ব-উৎপাদিত clientId দ্বারা চিহ্নিত এবং pairing-এর সময় সম্পাদিত একটি Ed25519 + ECDH হ্যান্ডশেকের মাধ্যমে অথেনটিকেটেড হয়।
দুই ধরনের কনভার্সেশন বিদ্যমান:
| টাইপ | Constant | বিবরণ |
|---|---|---|
PEER | ChatTargetType.PEER | দুটি পেয়ার্ড ডিভাইসের মধ্যে ১-থেকে-১ ডাইরেক্ট চ্যাট। |
CHANNEL | ChatTargetType.CHANNEL | একটি ডিভাইসের মালিকানাধীন মাল্টি-পার্টি গ্রুপ চ্যাট; মেম্বাররা একে অপরকে বার্তা ফ্যান-আউট করে। |
একটি বিশেষ "local" টার্গেট হলো ডিভাইসের নিজস্ব স্ক্র্যাচপ্যাড (নিজের কাছে নোট) —
এটিতে পাঠানো ওয়্যারের উপর একটি no-op।
কম্পোনেন্ট ম্যাপ
আর্কিটেকচারটি ইচ্ছাকৃতভাবে স্তরিত:
১. 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 | উদ্দেশ্য |
|---|---|---|
chats | DChat | প্রতিটি বার্তায় একটি সারি (টেক্সট / ইমেজ / ফাইল)। |
chat_channels | DChatChannel | প্রতিটি গ্রুপ চ্যানেলে একটি সারি। |
peers | DPeer | প্রতিটি পরিচিত ডিভাইসে একটি সারি (পেয়ার্ড বা শুধু-চ্যানেল)। |
কিছু উল্লেখযোগ্য বিষয়:
- আইডেন্টিটি হলো
clientId, MAC নয়। Android প্রতিটি সংযোগে BLE MAC র্যান্ডমাইজ করে, তাই ডাটাবেস একটি স্থিতিশীল ১৩-অক্ষরের স্ব-উৎপাদিত আইডি ব্যবহার করে। আবিষ্কারের অনুমতি দিতে BLE-এ শুধুমাত্র একটি ৮-বাইট SHA-256 প্রিফিক্স (shortId) ব্রডকাস্ট করা হয়। status="channel"পিয়ারগুলো এমন একটি চ্যানেলের মেম্বার যার সাথে এই ডিভাইস কখনো সরাসরি পেয়ার করেনি। তাদেরkeyখালি — তারা পেয়ারওয়াইজ শেয়ার্ড কী-এর পরিবর্তে চ্যানেল কী ব্যবহার করে অথেনটিকেট করে।owner="me"হলো একটি সেন্টিনেল যা একটি নতুন ইনস্টল করা ডিভাইসকে তারclientIdস্থিতিশীল হওয়ার আগে মালিক হিসেবে কাজ করতে দেয়;isOwnedByMe()উভয়"me"এবংTempData.clientIdগ্রহণ করে।
GraphQL API সারফেস {#graphql-api-surface}
PlainApp দুটি GraphQL স্কিমা প্রকাশ করে:
১. Web GraphQL (addChatChannelSchema + addChatMessageSchemashared/src/commonMain/kotlin/com/ismartcoding/plain/httpserver/ এ)
— স্থানীয় Ktor সার্ভার দ্বারা ব্রাউজার UI এবং apitest/ হারনেসে পরিবেশিত। একটি
ChaCha20-এনক্রিপ্টেড টোকেন দ্বারা অথেনটিকেটেড।
২. Peer GraphQL (PeerGraphQLService.applyPeerSchema) — এনক্রিপ্টেড পিয়ার
ট্রান্সপোর্টের মাধ্যমে অন্য ডিভাইসের জন্য /peer_graphql-এ প্রকাশিত। Ed25519
স্বাক্ষর + ChaCha20 বডি এনক্রিপশন দ্বারা অথেনটিকেটেড।
দুটি স্কিমা একই বিজনেস লজিক সিঙ্গেলটন (ChannelManager, ChatMessageReceiver, …)
শেয়ার করে কিন্তু ভিন্ন সারফেস প্রকাশ করে কারণ ট্রাস্ট মডেল ভিন্ন: web GraphQL
স্থানীয় UI-কে বিশ্বাস করে, যেখানে peer GraphQL শুধুমাত্র ক্রিপ্টোগ্রাফিক্যালি
অথেনটিকেটেড পিয়ারদের বিশ্বাস করে।
Web GraphQL সারফেস (চ্যাট)
কোয়েরিজ: chatChannels (সমস্ত চ্যানেল তালিকাভুক্ত করুন), chatItems(id) (একটি
টার্গেটের জন্য বার্তা — id হলো "local", peer:<id>, বা channel:<id>), এবং
latestChatItems (সমস্ত চ্যাট জুড়ে প্রিভিউ)।
চ্যাট মিউটেশন: sendChatItem(toId, content), deleteChatItem(id),
deleteChatItems(query), এবং retryChatItem(id)।
চ্যানেল মিউটেশন: createChatChannel(name), updateChatChannel(id, name),
deleteChatChannel(id), leaveChatChannel(id), addChatChannelMember(id, peerId),
removeChatChannelMember(id, peerId), acceptChatChannelInvite(id), এবং
declineChatChannelInvite(id)।
Peer GraphQL সারফেস (ট্রান্সপোর্ট)
/peer_graphql-এ প্রকাশিত এবং Ed25519 স্বাক্ষর + ChaCha20 বডি এনক্রিপশন দ্বারা
অথেনটিকেটেড। মাত্র তিনটি মিউটেশন ট্রান্সপোর্ট বাউন্ডারি অতিক্রম করে:
createChatItem(content) (একটি ইনকামিং পিয়ার বার্তা), channelSystemMessage(type, payload) (চ্যানেল লাইফসাইকেল ইভেন্ট যেমন invite/leave), এবং startAware (পিয়ারকে
তার Wi-Fi Aware সার্ভিস শুরু করতে বলার জন্য একটি ধাক্কা, যাতে একটি দ্রুততর ট্রান্সপোর্ট
দায়িত্ব নিতে পারে)।
c-id HTTP হেডার প্রেরকের clientId বহন করে; c-cid হেডার একটি চ্যানেল আইডি বহন করে
যখন রিকোয়েস্টটি চ্যানেল-স্কোপড হয় (যাতে রিসিভার ডিক্রিপশনের জন্য পেয়ারওয়াইজ পিয়ার
কী-এর পরিবর্তে চ্যানেল কী বেছে নেয়)।
পিয়ার চ্যাট: একটি বার্তা পাঠানো {#peer-chat-sending-a-message}
যখন ব্যবহারকারী একটি পিয়ার কনভার্সেশনে Send ট্যাপ করেন, কল চেইনটি হলো:
প্রতিটি হপে এনফোর্স করা মূল ইনভেরিয়েন্ট:
১. ChatManager.createChatItem সর্বদা প্রথমে একটি সারি ইনসার্ট করে, তারপর
পাঠায়। এর অর্থ UI তাৎক্ষণিকভাবে একটি "pending" বাবল দেখে এবং ডেলিভারি এখনো না হলেও
বার্তাটি অ্যাপ ক্র্যাশে টিকে থাকে।
২. PeerGraphQLClient.buildSignedRequest signature|timestamp|requestJson ফর্মের
একটি এনভেলপ তৈরি করে। স্বাক্ষরটি "$timestamp$requestJson"-এর উপর Ed25519,
টাইমস্ট্যাম্পটি বডির সাথে বাইন্ড করে যাতে এটি একটি নতুন টাইমস্ট্যাম্প দিয়ে রিপ্লে করা
না যায়।
৩. PeerTransportRouter.send Lan → WifiAware → Ble ক্রমে ট্রান্সপোর্ট ইটারেট
করে। প্রতিটি ট্রান্সপোর্ট রাউটারকে পরবর্তীটি চেষ্টা করতে দিতে TransportUnavailable
থ্রো করতে পারে।
৪. রিসিভিং পক্ষে, PeerChatParser.decrypt টাইমস্ট্যাম্প ±5 min-এর মধ্যে আছে কিনা
চেক করে এবং GraphQL মিউটেশন কার্যকর হওয়ার আগেই Ed25519 স্বাক্ষর যাচাই করে।
৫. ChatMessageReceiver.receive "$fromPeerId|$signature|$timestamp" দ্বারা কীড
একটি seenSignatures সেট রাখে এবং ডুপ্লিকেটে ReplayedMessageException থ্রো করে
— অপরিহার্য কারণ ট্রান্সপোর্ট একই পেলোড দুবার ডেলিভার করতে পারে (LAN + BLE)।
যদি PeerChatSender.send একটি non-null এরর স্ট্রিং ফেরত দেয়, ChatSendertriggerPeerRediscovery(peerId) কল করে, যা একটি ডিরেক্টেড, এনক্রিপ্টেড DISCOVER
ব্রডকাস্ট ফায়ার করে যাতে পিয়ার তার বর্তমান IP/port পুনরায় ঘোষণা করতে পারে।
পিয়ার চ্যাট: একটি বার্তা গ্রহণ {#peer-chat-receiving-a-message}
ইনবাউন্ড রিকোয়েস্টগুলো স্থানীয় Ktor সার্ভারের /peer_graphql রাউটে পৌঁছায়, যা
PeerGraphQLService দ্বারা হ্যান্ডেল করা হয়:
নোটিফিকেশন
emitNotificationIfNeeded হলো চূড়ান্ত ধাপ। এটি নোটিফিকেশনটি দমন করে যখন
TempData.activeToId == targetId (অর্থাৎ ব্যবহারকারী বর্তমানে সেই কনভার্সেশন দেখছেন)
বা যখন canShowNotifications() false হয়। চ্যানেল নোটিফিকেশনগুলো প্রেরকের নাম দিয়ে
প্রিফিক্স করা হয়।
চ্যানেল চ্যাট: লিডার ইলেকশন ও ফ্যান-আউট {#channel-chat-leader-election--fan-out}
চ্যানেলগুলো মাল্টি-পার্টি কিন্তু সার্ভারবিহীন। প্রতিটি মেম্বার একই বার্তা N বার ফ্যান-আউট করা এড়াতে, সেন্ডার পক্ষ একজন একক লিডার নির্বাচন করে যার কাজ হলো সমস্ত যুক্ত মেম্বারদের কাছে ব্রডকাস্ট করা।
লিডার ইলেকশন অ্যালগরিদম (DChatChannel.electLeader)
১. বর্তমানে অনলাইন এমন যুক্ত মেম্বারদের ফিল্টার করুন (স্থানীয় ডিভাইস সর্বদা অনলাইন
বিবেচিত)।
২. যদি মালিক অনলাইন যুক্ত মেম্বারদের মধ্যে থাকেন → মালিক হলেন লিডার।
৩. অন্যথায়, লিডার হলেন সবচেয়ে ছোট clientId সহ অনলাইন যুক্ত মেম্বার
(ডিটারমিনিস্টিক টাইব্রেক, কোনো কোঅর্ডিনেশন প্রয়োজন নেই)।
৪. কোনো অনলাইন যুক্ত মেম্বার না থাকলে null ফেরত দেয়।
সেন্ড ফ্লো
কেন আদৌ একজন লিডার?
কল্পনা করুন একটি ৫-মেম্বার চ্যানেল যেখানে প্রত্যেকে অন্য সবাইকে ব্রডকাস্ট করে: একটি একক বার্তা ২০টি নেটওয়ার্ক রাউন্ড ট্রিপ এবং প্রতিটি মেম্বারে ৪টি ডুপ্লিকেট কপি তৈরি করবে। একজন লিডার নির্বাচন করে, শুধুমাত্র সেই ডিভাইসটি ফ্যান-আউট করে — সেন্ডার হয় নিজেই ফ্যান-আউট সম্পাদন করে (যদি সে লিডার হয়) বা লিডারের কাছে একটি একক কপি রিলে করে, যা তারপর ফ্যান-আউট করে।
যদি লিডার অফলাইন থাকে, সেন্ডার Result.NoLeader-এ ফিরে যায়, পিয়ার রিডিসকভারি
ট্রিগার করে (যাতে লিডারের IP পাওয়া যায়), এবং ব্যবহারকারীকে পুনঃপ্রচেষ্টা করতে দিতে
স্ট্যাটাস ক্লিয়ার করে।
চ্যানেল কী রাউটিং
চ্যানেল বার্তাগুলো চ্যানেলের ChaCha20 কী দিয়ে এনক্রিপ্ট করা হয়, পেয়ারওয়াইজ
পিয়ার কী দিয়ে নয়। এটিই এমন একজন মেম্বারকে বার্তা গ্রহণ করতে দেয় যে কেবল
চ্যানেলের মাধ্যমে অন্য মেম্বারদের সাথে দেখা করেছে (কখনো ১-থেকে-১ পেয়ার করেনি) —
তাদের peers সারিতে status="channel" এবং key="" আছে। সেন্ডার c-cid HTTP
হেডার চ্যানেল আইডিতে সেট করে; রিসিভার পেয়ারওয়াইজ কী-এর পরিবর্তে
ChannelCacher.getKeyBytes(channelId) আপ করে।
পার-রেসিপিয়েন্ট পুনঃপ্রচেষ্টা
প্রতিটি sendToMember একটি DMessageDeliveryResult ফেরত দেয়। অ্যাগ্রিগেটেড
DMessageStatusData চ্যাট আইটেমের status_data JSON হিসেবে পারসিস্ট করা হয়। UI
"Delivered to Alice, Bob; Failed for Carol" দেখায় এবং ব্যবহারকারীকে বিশেষভাবে
Carol-এর জন্য Retry ট্যাপ করতে দেয় — ChatManager.sendToChannelMembers
পুনঃপ্রচেষ্টা সাবসেটের জন্য sendToRecipients পুনরায় চালায় এবং বিদ্যমানগুলোর সাথে
নতুন ফলাফলগুলো মার্জ করে, শুধুমাত্র পুনঃপ্রচেষ্টা করা পিয়ারগুলো প্রতিস্থাপন
করে।
চ্যানেল সিস্টেম বার্তা {#channel-system-messages}
চ্যানেল কন্ট্রোল-প্লেন বার্তাগুলো (invite, accept, decline, update, kick, leave)
peer GraphQL channelSystemMessage মিউটেশনের মাধ্যমে বিনিময় করা হয়। এগুলো একটি
type স্ট্রিং দ্বারা টাইপ করা JSON পেলোড:
| Type | দিক | Signed? | উদ্দেশ্য |
|---|---|---|---|
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 হলো ইনভাইটি/কিক করা পিয়ার
আইডি (ব্রডকাস্ট 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 — প্রথম পছন্দ। HTTPS-এর উপর একটি ChaCha20 ক্রিপ্টো ইন্টারসেপ্টর
সহ OkHttp ব্যবহার করে। যখন peer.ip খালি থাকে সম্পূর্ণভাবে স্কিপ করা হয়
(ক্রস-সাবনেট পিয়ার যাকে আমরা এখনো আবিষ্কার করিনি)।
২. WifiAwareTransport (শুধুমাত্র Android 13+) — Wi-Fi Aware (NAN) ডেটা পাথ ব্যবহার
করে। যখন পিয়ারের awareRunning ফ্ল্যাগ false হয় ফাস্ট-স্কিপ (BLE প্রিওয়ার্মার
স্ক্যান দ্বারা রিফ্রেশ করা)। পিয়ারের IPv6 একটি কাস্টম DNS-এর মাধ্যমে রিজল্ভ করা হয়
যা হোস্টনেম plain-aware-peer-কে লিংক-লোকাল অ্যাড্রেসে ম্যাপ করে।
৩. BleTransport — যেকোনো পেয়ার্ড পিয়ারের জন্য গ্যারান্টিড ফলব্যাক। GATT-এর উপর
চাঙ্কড RPC স্ট্রিম করে। ধীর কিন্তু কোনো IP কানেক্টিভিটি ছাড়াই কাজ করে।
কেন এই ক্রম?
- LAN সবচেয়ে দ্রুত (একক HTTPS রাউন্ড ট্রিপ, ~১০ ms টাইমআউট)।
- Wi-Fi Aware মাঝারি (ডেটা-পাথ সেটআপ ~৫ সেকেন্ড, তারপর ~১০ ms রাউন্ড ট্রিপ) এবং
ক্রস-সাবনেটে কাজ করে (যেমন একটি ডিভাইস গেস্ট Wi-Fi-তে, অন্যটি IoT Wi-Fi-তে)। পিয়ারের
Aware সার্ভিস চলছে না হলে ফাস্ট স্কিপ করতে টিউন করা, একটি ১০ সেকেন্ডের
buildLinkটাইমআউট এড়ায়। - BLE সবচেয়ে ধীর কিন্তু কোনো IP কানেক্টিভিটি ছাড়াই কাজ করে — এমনকি Wi-Fi না থাকলেও, বার্তাটি এখনও পৌঁছায়। পেয়ার্ড পিয়ারদের জন্য গ্যারান্টিড ফলব্যাক হিসেবে ব্যবহৃত।
সার্কিট ব্রেকার নিশ্চিত করে যে একটি অস্থিতিশীল ট্রান্সপোর্ট (বিশেষত নেটওয়ার্ক চার্নের সময় Wi-Fi Aware) ২টি ব্যর্থতার পর ৩০ সেকেন্ডের জন্য স্কিপ করা হয়, তাই ফলব্যাক দ্রুত ঘটে পুনরাবৃত্ত ১০ সেকেন্ড টাইমআউটের জন্য অপেক্ষা না করে।
Wi-Fi Aware হ্যান্ডশেক
AwareSession একটি ডেটা পাথ খোলার আগে একটি দুই-বার্তা হ্যান্ডশেক করে:
MSG_HELLO(সাবস্ক্রাইবার → পাবলিশার): "আমি তোমাকে দেখছি, এটি আমার পিয়ার হ্যান্ডেল।"MSG_READY(পাবলিশার → সাবস্ক্রাইবার): "আমি আমার নেটওয়ার্ক স্পেসিফায়ার নিবন্ধন করেছি, তুমি এখনrequestNetworkকরতে পারো।"
এটি Android ফ্রেমওয়ার্কের ~৫০০ 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
সরাসরি কালেক্ট করে:
কেন copy-on-write?
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 চাঙ্কড RPC (16 KiB চাঙ্ক
GET /fs?id=…&offset=…&length=…-এর মাধ্যমে) একই ByteReadChannel-এর মাধ্যমে স্ট্রিম
করে। onClose কলব্যাক BLE-কে তার ব্যাকগ্রাউন্ড ডাউনলোড কোরুটিন বাতিল করতে দেয় যখন
কনজ্যুমার রেসপন্স তাড়াতাড়ি বন্ধ করে দেয় (যেমন পজে)।
ডিজাইন প্যাটার্ন সারাংশ {#design-patterns-recap}
| প্যাটার্ন | কোথায় | কেন |
|---|---|---|
| ফ্যাসাড | ChatManager | একক এন্ট্রি পয়েন্ট; কলাররা কখনো সরাসরি DB/ট্রান্সপোর্ট স্পর্শ করে না। |
| স্ট্র্যাটেজি + চেইন অফ রেসপ. | PeerTransportRouter + LanTransport/WifiAwareTransport/BleTransport | প্লাগযোগ্য ট্রান্সপোর্ট TransportUnavailable-কে ফল-থ্রু সিগন্যাল হিসেবে সহ। |
| সার্কিট ব্রেকার | PeerCircuitBreaker | ২ ব্যর্থ / ৩০ সেকেন্ড একটি (peer, transport) লেগ ওপেন করে যাতে Wi-Fi Aware ফলব্যাক ব্লক না করে। |
| স্টেট মেশিন | PeerStatusManager.PeerState, AwarePeerLink.LinkState | সকেট লাইফসাইকেল এবং NDP লিংক লাইফসাইকেলের জন্য স্পষ্ট ট্রানজিশন। |
| প্রোডিউসার/কনজ্যুমার + পুল | DownloadQueue (৩ ওয়ার্কার, Channel.BUFFERED) | ফাইল ডাউনলোডের জন্য বাউন্ডেড কনকারেন্সি। |
| অবজার্ভার / রিঅ্যাকটিভ | StateFlow সর্বত্র | Compose সরাসরি কালেক্ট করে; কোনো ম্যানুয়াল রিফ্রেশ নেই। |
| রিপ্লে প্রোটেকশন | ChatMessageReceiver.seenSignatures, PeerChatParser.MAX_TIMESTAMP_DIFF_MS | LAN+BLE ডুয়াল ডেলিভারি থেকে ডুপ্লিকেট ড্রপ; আউট-অফ-উইন্ডো টাইমস্ট্যাম্প প্রত্যাখ্যান। |
| এক্সপোনেনশিয়াল ব্যাকঅফ | PeerStatusManager.scheduleReconnect | min(60 s, 1 s × 2^min(n-1, 6)) — ৬৪ সেকেন্ডে ক্যাপ। |
| Copy-on-Write | PeerCacher.mutatePeer, ChannelCacher.mutateChannel | প্রতিটি মিউটেশনে ফায়ার করতে StateFlow.distinctUntilChanged কে বাধ্য করে। |
| স্বাক্ষরিত এনভেলপ | PeerGraphQLClient.buildSignedRequest | signature|timestamp|body — টাইমস্ট্যাম্পকে বডির সাথে বাইন্ড করে রিপ্লে রোধে। |
| ডিটারমিনিস্টিক রোল স্প্লিট | TempData.clientId < peer.id | WebSocket ক্লায়েন্ট বনাম সার্ভার, এবং Wi-Fi Aware সাবস্ক্রাইবার বনাম পাবলিশার সিদ্ধান্ত নেয়। |
| লেজি হাইড্রেশন | invite/update-এ ensureChannelPeer | অদেখা চ্যানেল মেম্বারদের জন্য peers সারি তৈরি করে যাতে ফ্যান-আউট রাউটিং কাজ করে। |
| এনক্রিপ্টেড আইডেন্টিটি | LANDiscoverManager.discoverSpecificDevice | ডিরেক্টেড DISCOVER টার্গেট আইডি পিয়ার কী দিয়ে এনক্রিপ্ট করে — শুধুমাত্র টার্গেট এটি চিনতে পারে। |
আরও পড়ার জন্য
- Pairing Flow — দুটি ডিভাইস কীভাবে বিশ্বাস স্থাপন করে এবং এই নিবন্ধের প্রতিটি ট্রান্সপোর্ট দ্বারা ব্যবহৃত শেয়ার্ড ChaCha20 কী বিনিময় করে।
apitest/groups/chat-messages.shএবংapitest/groups/chat-channels.sh— এক্সিকিউটেবল টেস্ট প্ল্যান যা প্রতিটি GraphQL মিউটেশন এন্ড-টু-এন্ড অনুশীলন করে।