(वैकल्पिक) फायरबेस लोकल एमुलेटर सूट के साथ प्रोटोटाइप और परीक्षण
आपका ऐप रीयलटाइम डेटाबेस से कैसे पढ़ता और लिखता है, इस बारे में बात करने से पहले, आइए टूल का एक सेट पेश करते हैं जिसका उपयोग आप प्रोटोटाइप और रियलटाइम डेटाबेस कार्यक्षमता का परीक्षण करने के लिए कर सकते हैं: फायरबेस लोकल एमुलेटर सूट। यदि आप अलग-अलग डेटा मॉडल आज़मा रहे हैं, अपने सुरक्षा नियमों का अनुकूलन कर रहे हैं, या बैक-एंड के साथ इंटरैक्ट करने का सबसे किफायती तरीका खोजने के लिए काम कर रहे हैं, तो लाइव सेवाओं को तैनात किए बिना स्थानीय रूप से काम करने में सक्षम होना एक अच्छा विचार हो सकता है।
एक रीयलटाइम डेटाबेस इम्यूलेटर लोकल इम्यूलेटर सूट का हिस्सा है, जो आपके ऐप को आपके एमुलेटेड डेटाबेस कंटेंट और कॉन्फ़िगरेशन के साथ-साथ वैकल्पिक रूप से आपके एमुलेटेड प्रोजेक्ट संसाधनों (फ़ंक्शंस, अन्य डेटाबेस और सुरक्षा नियम) के साथ इंटरैक्ट करने में सक्षम बनाता है।
रीयलटाइम डेटाबेस एम्यूलेटर का उपयोग करने में बस कुछ ही चरण शामिल हैं:
- एमुलेटर से कनेक्ट करने के लिए अपने ऐप के टेस्ट कॉन्फ़िगरेशन में कोड की एक पंक्ति जोड़ना।
- आपकी स्थानीय प्रोजेक्ट निर्देशिका की जड़ से,
firebase emulators:start
। - सामान्य रूप से रीयलटाइम डेटाबेस प्लेटफ़ॉर्म SDK का उपयोग करके या रीयलटाइम डेटाबेस REST API का उपयोग करके अपने ऐप के प्रोटोटाइप कोड से कॉल करना।
रीयलटाइम डेटाबेस और क्लाउड फ़ंक्शंस से संबंधित एक विस्तृत पूर्वाभ्यास उपलब्ध है। आपको स्थानीय इम्यूलेटर सुइट परिचय पर भी एक नज़र डालनी चाहिए।
एक FIRDatabaseReference प्राप्त करें
डेटाबेस से डेटा पढ़ने या लिखने के लिए, आपको FIRDatabaseReference
की एक आवृत्ति की आवश्यकता है:
तीव्र
var ref: DatabaseReference! ref = Database.database().reference()
उद्देश्य सी
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
डेटा लिखें
यह दस्तावेज़ फायरबेस डेटा को पढ़ने और लिखने की मूल बातें शामिल करता है।
फायरबेस डेटा एक Database
संदर्भ के लिए लिखा गया है और एक अतुल्यकालिक श्रोता को संदर्भ में संलग्न करके पुनर्प्राप्त किया गया है। डेटा की प्रारंभिक स्थिति के लिए श्रोता को एक बार ट्रिगर किया जाता है और फिर कभी भी डेटा में परिवर्तन होता है।
मूल लेखन कार्य
मूल लेखन कार्यों के लिए, आप किसी निर्दिष्ट संदर्भ में डेटा को सहेजने के लिए, उस पथ पर किसी भी मौजूदा डेटा को प्रतिस्थापित करने के लिए setValue
उपयोग कर सकते हैं। आप इस विधि का उपयोग इसके लिए कर सकते हैं:
- पास प्रकार जो उपलब्ध JSON प्रकारों के अनुरूप हैं:
-
NSString
-
NSNumber
-
NSDictionary
-
NSArray
-
उदाहरण के लिए, आप एक उपयोगकर्ता को setValue
के साथ निम्नानुसार जोड़ सकते हैं:
तीव्र
self.ref.child("users").child(user.uid).setValue(["username": username])
उद्देश्य सी
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
इस तरह से setValue
उपयोग निर्दिष्ट स्थान पर डेटा को अधिलेखित कर देता है, जिसमें कोई भी चाइल्ड नोड शामिल है। हालाँकि, आप अभी भी पूरे ऑब्जेक्ट को फिर से लिखे बिना बच्चे को अपडेट कर सकते हैं। यदि आप उपयोगकर्ताओं को अपनी प्रोफ़ाइल अपडेट करने की अनुमति देना चाहते हैं, तो आप उपयोगकर्ता नाम को निम्नानुसार अपडेट कर सकते हैं:
तीव्र
self.ref.child("users/\(user.uid)/username").setValue(username)
उद्देश्य सी
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
डेटा पढ़ें
महत्वपूर्ण घटनाओं को सुनकर डेटा पढ़ें
पथ पर डेटा पढ़ने और परिवर्तनों को सुनने के लिए, FIRDataEventTypeValue
ईवेंट का निरीक्षण करने के लिए FIRDatabaseReference
के observeEventType:withBlock
उपयोग करें।
घटना प्रकार | विशिष्ट उपयोग |
---|---|
FIRDataEventTypeValue | पथ की संपूर्ण सामग्री में परिवर्तनों को पढ़ें और सुनें। |
आप दिए गए पथ पर डेटा को पढ़ने के लिए FIRDataEventTypeValue
ईवेंट का उपयोग कर सकते हैं, क्योंकि यह ईवेंट के समय मौजूद होता है। यह विधि एक बार चालू हो जाती है जब श्रोता जुड़ा होता है और फिर से हर बार डेटा, किसी भी बच्चे सहित, बदल जाता है। ईवेंट कॉलबैक एक snapshot
पारित किया जाता है जिसमें चाइल्ड डेटा सहित उस स्थान पर सभी डेटा होते हैं। यदि कोई डेटा नहीं है, तो जब आप exists()
कॉल करते हैं और जब आप इसकी value
प्रॉपर्टी पढ़ते हैं तो स्नैपशॉट false
nil
जाएगा।
निम्नलिखित उदाहरण डेटाबेस से एक पोस्ट के विवरण को पुनः प्राप्त करने वाले एक सामाजिक ब्लॉगिंग एप्लिकेशन को प्रदर्शित करता है:
तीव्र
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
उद्देश्य सी
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
श्रोता एक FIRDataSnapshot
प्राप्त करता है जिसमें घटना के समय डेटाबेस में निर्दिष्ट स्थान पर इसकी value
संपत्ति में डेटा होता है। आप मूल्यों को उचित मूल प्रकार, जैसे NSDictionary
असाइन कर सकते हैं। यदि स्थान पर कोई डेटा मौजूद नहीं है, तो value
nil
है।
एक बार डेटा पढ़ें
GetData () का उपयोग करके एक बार पढ़ें
एसडीके को डेटाबेस सर्वर के साथ इंटरैक्शन प्रबंधित करने के लिए डिज़ाइन किया गया है, चाहे आपका ऐप ऑनलाइन हो या ऑफलाइन।
आम तौर पर, आपको बैकएंड से डेटा के अपडेट की सूचना प्राप्त करने के लिए डेटा पढ़ने के लिए ऊपर वर्णित मूल्य ईवेंट तकनीकों का उपयोग करना चाहिए। वे तकनीक आपके उपयोग और बिलिंग को कम करती हैं, और आपके उपयोगकर्ताओं को ऑनलाइन और ऑफ़लाइन होने पर सर्वोत्तम अनुभव देने के लिए अनुकूलित होती हैं।
यदि आपको केवल एक बार डेटा की आवश्यकता है, तो आप डेटाबेस से डेटा का स्नैपशॉट प्राप्त करने के लिए getData()
उपयोग कर सकते हैं। यदि किसी कारण से getData()
सर्वर मान वापस करने में असमर्थ है, तो क्लाइंट स्थानीय संग्रहण कैश की जांच करेगा और यदि मान अभी भी नहीं मिला है तो एक त्रुटि लौटाएगा।
निम्न उदाहरण डेटाबेस से एक बार उपयोगकर्ता के सार्वजनिक-सामना करने वाले उपयोगकर्ता नाम को पुनर्प्राप्त करने का प्रदर्शन करता है:
तीव्र
ref.child("users/\(uid)/username").getData(completion: { error, snapshot in guard error == nil else { print(error!.localizedDescription) return; } let userName = snapshot.value as? String ?? "Unknown"; });
उद्देश्य सी
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid]; [[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) { if (error) { NSLog(@"Received an error %@", error); return; } NSString *userName = snapshot.value; }];
getData()
का अनावश्यक उपयोग बैंडविड्थ के उपयोग को बढ़ा सकता है और प्रदर्शन के नुकसान का कारण बन सकता है, जिसे ऊपर दिखाए गए रीयलटाइम श्रोता का उपयोग करके रोका जा सकता है।
एक बार पर्यवेक्षक के साथ डेटा पढ़ें
कुछ मामलों में आप चाहते हैं कि सर्वर पर अपडेट किए गए मान की जांच करने के बजाय स्थानीय कैश से मान तुरंत लौटाया जाए। उन मामलों में आप तुरंत स्थानीय डिस्क कैश से डेटा प्राप्त करने के लिए observeSingleEventOfType
उपयोग कर सकते हैं।
यह उस डेटा के लिए उपयोगी है जिसे केवल एक बार लोड करने की आवश्यकता होती है और जिसके बार-बार बदलने या सक्रिय रूप से सुनने की आवश्यकता नहीं होती है। उदाहरण के लिए, पिछले उदाहरणों में ब्लॉगिंग ऐप उपयोगकर्ता के प्रोफ़ाइल को लोड करने के लिए इस विधि का उपयोग करता है जब वे एक नई पोस्ट लिखना शुरू करते हैं:
तीव्र
let userID = Auth.auth().currentUser?.uid ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in // Get user value let value = snapshot.value as? NSDictionary let username = value?["username"] as? String ?? "" let user = User(username: username) // ... }) { error in print(error.localizedDescription) }
उद्देश्य सी
NSString *userID = [FIRAuth auth].currentUser.uid; [[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { // Get user value User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]]; // ... } withCancelBlock:^(NSError * _Nonnull error) { NSLog(@"%@", error.localizedDescription); }];
डेटा को अपडेट करना या हटाना
विशिष्ट क्षेत्रों को अद्यतन करें
साथ ही अन्य चाइल्ड नोड्स को अधिलेखित किए बिना एक नोड के विशिष्ट चिल्ड्रेन को लिखने के लिए, updateChildValues
विधि का उपयोग करें।
updateChildValues
कॉल करते समय, आप कुंजी के लिए पथ निर्दिष्ट करके निचले स्तर के बाल मूल्यों को अपडेट कर सकते हैं। यदि डेटा को बेहतर पैमाने पर रखने के लिए कई स्थानों पर संग्रहीत किया जाता है, तो आप डेटा फैन-आउट का उपयोग करके उस डेटा के सभी उदाहरणों को अपडेट कर सकते हैं। उदाहरण के लिए, एक सोशल ब्लॉगिंग ऐप एक पोस्ट बनाना चाहता है और साथ ही इसे हाल की गतिविधि फ़ीड और पोस्ट करने वाले उपयोगकर्ता की गतिविधि फ़ीड में अपडेट करना चाहता है। ऐसा करने के लिए, ब्लॉगिंग एप्लिकेशन इस तरह कोड का उपयोग करता है:
तीव्र
guard let key = ref.child("posts").childByAutoId().key else { return } let post = ["uid": userID, "author": username, "title": title, "body": body] let childUpdates = ["/posts/\(key)": post, "/user-posts/\(userID)/\(key)/": post] ref.updateChildValues(childUpdates)
उद्देश्य सी
NSString *key = [[_ref child:@"posts"] childByAutoId].key; NSDictionary *post = @{@"uid": userID, @"author": username, @"title": title, @"body": body}; NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post, [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post}; [_ref updateChildValues:childUpdates];
यह उदाहरण /posts/$postid
पर सभी उपयोगकर्ताओं के लिए पोस्ट युक्त नोड में एक पोस्ट बनाने के लिए childByAutoId
उपयोग करता है और साथ ही getKey()
के साथ कुंजी को पुनः प्राप्त करता है। तब कुंजी का उपयोग उपयोगकर्ता के पोस्ट में /user-posts/$userid/$postid
पर दूसरी प्रविष्टि बनाने के लिए किया जा सकता है।
इन रास्तों का उपयोग करके, आप JSON ट्री में एक ही कॉल के साथ JSON updateChildValues
में कई स्थानों पर एक साथ अपडेट कर सकते हैं, जैसे कि यह उदाहरण दोनों स्थानों पर नई पोस्ट कैसे बनाता है। इस तरह से किए गए एक साथ अद्यतन परमाणु हैं: या तो सभी अद्यतन सफल होते हैं या सभी अद्यतन विफल होते हैं।
एक पूर्णता ब्लॉक जोड़ें
यदि आप जानना चाहते हैं कि आपका डेटा कब सबमिट किया गया है, तो आप एक पूर्णता ब्लॉक जोड़ सकते हैं। दोनों setValue
और updateChildValues
एक वैकल्पिक समापन ब्लॉक लेते हैं जिसे तब कहा जाता है जब डेटाबेस को लिखने के लिए प्रतिबद्ध किया गया हो। यह श्रोता ट्रैक रखने के लिए उपयोगी हो सकता है कि कौन सा डेटा सहेजा गया है और कौन सा डेटा अभी भी सिंक्रनाइज़ किया जा रहा है। यदि कॉल असफल होती है, तो श्रोता को एक एरर ऑब्जेक्ट पास किया जाता है जो दर्शाता है कि विफलता क्यों हुई।
तीव्र
ref.child("users").child(user.uid).setValue(["username": username]) { (error:Error?, ref:DatabaseReference) in if let error = error { print("Data could not be saved: \(error).") } else { print("Data saved successfully!") } }
उद्देश्य सी
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { if (error) { NSLog(@"Data could not be saved: %@", error); } else { NSLog(@"Data saved successfully."); } }];
डेटा हटाएं
डेटा को हटाने का सबसे सरल तरीका उस डेटा के स्थान के संदर्भ में removeValue
कॉल करना है।
आप setValue
या updateChildValues
जैसे किसी अन्य लेखन ऑपरेशन के लिए nil
मान के रूप में निर्दिष्ट करके भी हटा सकते हैं। आप एक एपीआई कॉल में कई बच्चों को हटाने के लिए इस तकनीक का उपयोग updateChildValues
के साथ कर सकते हैं।
श्रोताओं को अलग करें
जब आप ViewController
छोड़ते हैं तो पर्यवेक्षक स्वचालित रूप से डेटा सिंक करना बंद नहीं करते हैं। यदि एक पर्यवेक्षक को ठीक से हटाया नहीं जाता है, तो यह डेटा को स्थानीय मेमोरी में सिंक करना जारी रखता है। जब एक पर्यवेक्षक की आवश्यकता नहीं रह जाती है, तो संबंधित FIRDatabaseHandle
removeObserverWithHandle
विधि से पास करके इसे हटा दें।
जब आप किसी संदर्भ में कॉलबैक ब्लॉक जोड़ते हैं, तो एक FIRDatabaseHandle
वापस आ जाता है। कॉलबैक ब्लॉक को हटाने के लिए इन हैंडल का उपयोग किया जा सकता है।
यदि एक डेटाबेस संदर्भ में कई श्रोताओं को जोड़ा गया है, तो प्रत्येक श्रोता को तब बुलाया जाता है जब कोई ईवेंट उठाया जाता है। उस स्थान पर डेटा सिंक करना बंद करने के लिए, आपको किसी स्थान पर सभी पर्यवेक्षकों को removeAllObservers
विधि को कॉल करके निकालना होगा।
श्रोता पर removeObserverWithHandle
या removeAllObservers
कॉल करने से उसके चाइल्ड नोड्स पर पंजीकृत श्रोताओं को स्वचालित रूप से नहीं हटाया जाता है; आपको उन्हें निकालने के लिए उन संदर्भों या हैंडल का भी ट्रैक रखना चाहिए।
डेटा को लेनदेन के रूप में सहेजें
डेटा के साथ काम करते समय जो समवर्ती संशोधनों, जैसे वृद्धिशील काउंटरों द्वारा दूषित हो सकता है, आप लेनदेन ऑपरेशन का उपयोग कर सकते हैं। आप इस ऑपरेशन को दो तर्क देते हैं: एक अपडेट फ़ंक्शन और एक वैकल्पिक पूर्णता कॉलबैक। अपडेट फ़ंक्शन डेटा की वर्तमान स्थिति को एक तर्क के रूप में लेता है और नई वांछित स्थिति देता है जिसे आप लिखना चाहते हैं।
उदाहरण के लिए, उदाहरण के लिए सोशल ब्लॉगिंग ऐप में, आप उपयोगकर्ताओं को पोस्ट को तारांकित और अतारांकित करने की अनुमति दे सकते हैं और ट्रैक कर सकते हैं कि किसी पोस्ट को कितने सितारे प्राप्त हुए हैं:
तीव्र
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in if var post = currentData.value as? [String: AnyObject], let uid = Auth.auth().currentUser?.uid { var stars: [String: Bool] stars = post["stars"] as? [String: Bool] ?? [:] var starCount = post["starCount"] as? Int ?? 0 if let _ = stars[uid] { // Unstar the post and remove self from stars starCount -= 1 stars.removeValue(forKey: uid) } else { // Star the post and add self to stars starCount += 1 stars[uid] = true } post["starCount"] = starCount as AnyObject? post["stars"] = stars as AnyObject? // Set value and report transaction success currentData.value = post return TransactionResult.success(withValue: currentData) } return TransactionResult.success(withValue: currentData) }) { error, committed, snapshot in if let error = error { print(error.localizedDescription) } }
उद्देश्य सी
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { NSMutableDictionary *post = currentData.value; if (!post || [post isEqual:[NSNull null]]) { return [FIRTransactionResult successWithValue:currentData]; } NSMutableDictionary *stars = post[@"stars"]; if (!stars) { stars = [[NSMutableDictionary alloc] initWithCapacity:1]; } NSString *uid = [FIRAuth auth].currentUser.uid; int starCount = [post[@"starCount"] intValue]; if (stars[uid]) { // Unstar the post and remove self from stars starCount--; [stars removeObjectForKey:uid]; } else { // Star the post and add self to stars starCount++; stars[uid] = @YES; } post[@"stars"] = stars; post[@"starCount"] = @(starCount); // Set value and report transaction success currentData.value = post; return [FIRTransactionResult successWithValue:currentData]; } andCompletionBlock:^(NSError * _Nullable error, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { // Transaction completed if (error) { NSLog(@"%@", error.localizedDescription); } }];
लेन-देन का उपयोग करना स्टार काउंट को गलत होने से रोकता है यदि कई उपयोगकर्ता एक ही पोस्ट को एक ही समय में स्टार करते हैं या क्लाइंट के पास पुराना डेटा है। FIRMutableData
क्लास में निहित मान प्रारंभ में पथ के लिए क्लाइंट का अंतिम ज्ञात मान है, या यदि कोई नहीं है तो nil
। सर्वर इसके वर्तमान मूल्य के खिलाफ प्रारंभिक मूल्य की तुलना करता है और यदि मान मेल खाता है, या इसे अस्वीकार करता है तो लेनदेन को स्वीकार करता है। यदि लेन-देन अस्वीकार कर दिया जाता है, तो सर्वर क्लाइंट को वर्तमान मान लौटाता है, जो अद्यतन मूल्य के साथ लेनदेन को फिर से चलाता है। यह तब तक दोहराता है जब तक लेन-देन स्वीकार नहीं किया जाता या बहुत अधिक प्रयास नहीं किए जाते।
परमाणु सर्वर-साइड वेतन वृद्धि
उपरोक्त उपयोग के मामले में हम डेटाबेस के लिए दो मान लिख रहे हैं: उस उपयोगकर्ता की आईडी जो पोस्ट को स्टार/अनस्टार करता है, और बढ़ी हुई स्टार गिनती। अगर हम पहले से ही जानते हैं कि उपयोगकर्ता पोस्ट को तारांकित कर रहा है, तो हम लेन-देन के बजाय परमाणु वृद्धि ऑपरेशन का उपयोग कर सकते हैं।
तीव्र
let updates = [ "posts/\(postID)/stars/\(userID)": true, "posts/\(postID)/starCount": ServerValue.increment(1), "user-posts/\(postID)/stars/\(userID)": true, "user-posts/\(postID)/starCount": ServerValue.increment(1) ] as [String : Any] Database.database().reference().updateChildValues(updates);
उद्देश्य सी
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1], [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]}; [[[FIRDatabase database] reference] updateChildValues:updates];
यह कोड एक लेन-देन ऑपरेशन का उपयोग नहीं करता है, इसलिए यदि कोई परस्पर विरोधी अपडेट है तो यह स्वचालित रूप से फिर से नहीं चलता है। हालाँकि, चूंकि इंक्रीमेंट ऑपरेशन सीधे डेटाबेस सर्वर पर होता है, इसलिए विरोध की कोई संभावना नहीं है।
यदि आप एप्लिकेशन-विशिष्ट विरोधों का पता लगाना और उन्हें अस्वीकार करना चाहते हैं, जैसे कि कोई उपयोगकर्ता किसी ऐसी पोस्ट को तारांकित करता है जिसे वे पहले ही तारांकित कर चुके हैं, तो आपको उस उपयोग मामले के लिए कस्टम सुरक्षा नियम लिखने चाहिए।
डेटा के साथ ऑफ़लाइन काम करें
यदि कोई क्लाइंट अपना नेटवर्क कनेक्शन खो देता है, तो आपका ऐप ठीक से काम करना जारी रखेगा।
फायरबेस डेटाबेस से जुड़ा प्रत्येक क्लाइंट किसी भी सक्रिय डेटा का अपना आंतरिक संस्करण बनाए रखता है। जब डेटा लिखा जाता है, तो इसे पहले इस स्थानीय संस्करण में लिखा जाता है। फायरबेस क्लाइंट तब उस डेटा को दूरस्थ डेटाबेस सर्वर और अन्य क्लाइंट के साथ "सर्वश्रेष्ठ-प्रयास" के आधार पर सिंक्रनाइज़ करता है।
नतीजतन, सर्वर पर किसी भी डेटा को लिखे जाने से पहले, सभी डेटाबेस को लिखते हैं, तुरंत स्थानीय घटनाओं को ट्रिगर करते हैं। इसका मतलब है कि आपका ऐप नेटवर्क विलंबता या कनेक्टिविटी की परवाह किए बिना उत्तरदायी बना रहता है।
एक बार कनेक्टिविटी फिर से स्थापित हो जाने के बाद, आपके ऐप को ईवेंट का उपयुक्त सेट प्राप्त होता है ताकि क्लाइंट बिना किसी कस्टम कोड को लिखे वर्तमान सर्वर स्थिति के साथ सिंक हो सके।
हम ऑनलाइन और ऑफ़लाइन क्षमताओं के बारे में और जानें में ऑफ़लाइन व्यवहार के बारे में और बात करेंगे.