আপনার অ্যাপ সাময়িকভাবে নেটওয়ার্ক সংযোগ হারিয়ে ফেললেও Firebase অ্যাপ্লিকেশনগুলি কাজ করে৷ এছাড়াও, ফায়ারবেস স্থানীয়ভাবে ডেটা বজায় রাখা, উপস্থিতি পরিচালনা এবং লেটেন্সি পরিচালনা করার জন্য সরঞ্জাম সরবরাহ করে।
ডিস্কের অধ্যবসায়
Firebase অ্যাপগুলি স্বয়ংক্রিয়ভাবে অস্থায়ী নেটওয়ার্ক বাধাগুলি পরিচালনা করে। অফলাইনে থাকা অবস্থায় ক্যাশ করা ডেটা পাওয়া যায় এবং নেটওয়ার্ক সংযোগ পুনরুদ্ধার করা হলে Firebase যেকোনো লেখা পুনরায় পাঠায়।
আপনি যখন ডিস্কের স্থিরতা সক্ষম করেন, তখন আপনার অ্যাপটি ডিভাইসে স্থানীয়ভাবে ডেটা লেখে যাতে আপনার অ্যাপ অফলাইনে থাকা অবস্থায় স্থিতি বজায় রাখতে পারে, এমনকি ব্যবহারকারী বা অপারেটিং সিস্টেম অ্যাপটি পুনরায় চালু করলেও।
আপনি কোডের একটি লাইন দিয়ে ডিস্কের স্থিরতা সক্ষম করতে পারেন।
Kotlin+KTX
Firebase.database.setPersistenceEnabled(true)
Java
FirebaseDatabase.getInstance().setPersistenceEnabled(true);
অধ্যবসায় আচরণ
অধ্যবসায় সক্ষম করার মাধ্যমে, ফায়ারবেস রিয়েলটাইম ডেটাবেস ক্লায়েন্ট অনলাইনে ডিস্কে থাকাকালীন সিঙ্ক করবে এমন যেকোন ডেটা এবং অফলাইনে উপলব্ধ, এমনকি ব্যবহারকারী বা অপারেটিং সিস্টেম অ্যাপটি পুনরায় চালু করলেও। এর অর্থ হল আপনার অ্যাপটি ক্যাশে সঞ্চিত স্থানীয় ডেটা ব্যবহার করে অনলাইনে কাজ করে। স্থানীয় আপডেটের জন্য শ্রোতা কলব্যাক চালু থাকবে।
Firebase রিয়েলটাইম ডেটাবেস ক্লায়েন্ট স্বয়ংক্রিয়ভাবে সমস্ত লেখার ক্রিয়াকলাপের একটি সারি রাখে যা আপনার অ্যাপ অফলাইনে থাকাকালীন সঞ্চালিত হয়। যখন অধ্যবসায় সক্ষম করা হয়, তখন এই সারিটি ডিস্কে স্থির থাকে যাতে ব্যবহারকারী বা অপারেটিং সিস্টেম অ্যাপটি পুনরায় চালু করলে আপনার সমস্ত লেখা উপলব্ধ থাকে। অ্যাপটি কানেক্টিভিটি ফিরে পেলে, সমস্ত ক্রিয়াকলাপ Firebase রিয়েলটাইম ডেটাবেস সার্ভারে পাঠানো হয়।
যদি আপনার অ্যাপ Firebase প্রমাণীকরণ ব্যবহার করে, Firebase রিয়েলটাইম ডেটাবেস ক্লায়েন্ট অ্যাপ রিস্টার্ট জুড়ে ব্যবহারকারীর প্রমাণীকরণ টোকেন বজায় রাখে। আপনার অ্যাপ অফলাইনে থাকাকালীন যদি প্রমাণীকরণ টোকেনের মেয়াদ শেষ হয়ে যায়, ক্লায়েন্ট আপনার অ্যাপ ব্যবহারকারীকে পুনরায় প্রমাণীকরণ না করা পর্যন্ত লেখার ক্রিয়াকলাপ থামিয়ে দেয়, অন্যথায় সুরক্ষা নিয়মের কারণে লেখার ক্রিয়াকলাপ ব্যর্থ হতে পারে।
ডাটা ফ্রেশ রাখা
Firebase রিয়েলটাইম ডেটাবেস সক্রিয় শ্রোতাদের জন্য ডেটার একটি স্থানীয় অনুলিপি সিঙ্ক্রোনাইজ করে এবং সঞ্চয় করে। এছাড়াও, আপনি সুনির্দিষ্ট অবস্থানগুলিকে সিঙ্কে রাখতে পারেন।
Kotlin+KTX
val scoresRef = Firebase.database.getReference("scores") scoresRef.keepSynced(true)
Java
DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores"); scoresRef.keepSynced(true);
ফায়ারবেস রিয়েলটাইম ডেটাবেস ক্লায়েন্ট স্বয়ংক্রিয়ভাবে এই অবস্থানগুলিতে ডেটা ডাউনলোড করে এবং রেফারেন্সের কোনও সক্রিয় শ্রোতা না থাকলেও এটি সিঙ্কে রাখে। আপনি নিম্নলিখিত কোডের লাইন দিয়ে সিঙ্ক্রোনাইজেশন বন্ধ করতে পারেন।
Kotlin+KTX
scoresRef.keepSynced(false)
Java
scoresRef.keepSynced(false);
ডিফল্টরূপে, পূর্বে সিঙ্ক করা 10MB ডেটা ক্যাশে করা হয়। এটি বেশিরভাগ অ্যাপ্লিকেশনের জন্য যথেষ্ট হওয়া উচিত। যদি ক্যাশে কনফিগার করা আকারকে ছাড়িয়ে যায়, ফায়ারবেস রিয়েলটাইম ডেটাবেস এমন ডেটা পরিষ্কার করে যা সম্প্রতি ব্যবহার করা হয়েছে। সিঙ্কে রাখা ডেটা ক্যাশে থেকে পরিষ্কার করা হয় না।
ডেটা অফলাইনে অনুসন্ধান করা হচ্ছে
Firebase রিয়েলটাইম ডেটাবেস অফলাইনে ব্যবহারের জন্য একটি প্রশ্ন থেকে ফিরে আসা ডেটা সঞ্চয় করে। অফলাইন থাকাকালীন তৈরি করা প্রশ্নের জন্য, ফায়ারবেস রিয়েলটাইম ডেটাবেস পূর্বে লোড করা ডেটার জন্য কাজ করতে থাকে। অনুরোধ করা ডেটা লোড না হলে, ফায়ারবেস রিয়েলটাইম ডেটাবেস স্থানীয় ক্যাশে থেকে ডেটা লোড করে৷ নেটওয়ার্ক সংযোগ আবার উপলব্ধ হলে, ডেটা লোড হয় এবং ক্যোয়ারী প্রতিফলিত হবে।
উদাহরণস্বরূপ, এই কোডটি স্কোরগুলির একটি Firebase রিয়েলটাইম ডেটাবেসের শেষ চারটি আইটেমের জন্য প্রশ্ন করে
Kotlin+KTX
val scoresRef = Firebase.database.getReference("scores") scoresRef.orderByValue().limitToLast(4).addChildEventListener(object : ChildEventListener { override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) { Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}") } // ... })
Java
DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores"); scoresRef.orderByValue().limitToLast(4).addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) { Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue()); } // ... });
অনুমান করুন যে ব্যবহারকারী সংযোগ হারান, অফলাইনে যান এবং অ্যাপটি পুনরায় চালু করেন। অফলাইনে থাকাকালীন, অ্যাপ একই অবস্থান থেকে শেষ দুটি আইটেমের জন্য প্রশ্ন করে। এই ক্যোয়ারীটি সফলভাবে শেষ দুটি আইটেম ফিরিয়ে দেবে কারণ অ্যাপটি উপরের ক্যোয়ারীতে চারটি আইটেম লোড করেছে।
Kotlin+KTX
scoresRef.orderByValue().limitToLast(2).addChildEventListener(object : ChildEventListener { override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) { Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}") } // ... })
Java
scoresRef.orderByValue().limitToLast(2).addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) { Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue()); } // ... });
পূর্ববর্তী উদাহরণে, ফায়ারবেস রিয়েলটাইম ডেটাবেস ক্লায়েন্ট স্থায়ী ক্যাশে ব্যবহার করে সর্বোচ্চ স্কোরকারী দুটি ডাইনোসরের জন্য 'শিশু যোগ করা' ইভেন্টগুলি উত্থাপন করে। কিন্তু এটি একটি 'মান' ইভেন্ট বাড়াবে না, যেহেতু অ্যাপটি অনলাইনে থাকাকালীন কখনই সেই প্রশ্নটি কার্যকর করেনি।
যদি অ্যাপটি অফলাইনে থাকা অবস্থায় শেষ ছয়টি আইটেম অনুরোধ করে, তাহলে এটি সরাসরি চারটি ক্যাশ করা আইটেমের জন্য 'শিশু যুক্ত' ইভেন্ট পাবে। ডিভাইসটি অনলাইনে ফিরে আসলে, ফায়ারবেস রিয়েলটাইম ডেটাবেস ক্লায়েন্ট সার্ভারের সাথে সিঙ্ক্রোনাইজ করে এবং অ্যাপের জন্য চূড়ান্ত দুটি 'চাইল্ড অ্যাড' এবং 'মান' ইভেন্ট পায়।
অফলাইনে লেনদেন পরিচালনা করা
অ্যাপটি অফলাইনে থাকাকালীন সঞ্চালিত যেকোনো লেনদেন সারিবদ্ধ। অ্যাপটি একবার নেটওয়ার্ক সংযোগ ফিরে পেলে, লেনদেনগুলি রিয়েলটাইম ডেটাবেস সার্ভারে পাঠানো হয়।
উপস্থিতি পরিচালনা
রিয়েলটাইম অ্যাপ্লিকেশনগুলিতে ক্লায়েন্ট কখন সংযোগ এবং সংযোগ বিচ্ছিন্ন করে তা সনাক্ত করা প্রায়শই দরকারী। উদাহরণস্বরূপ, আপনি একজন ব্যবহারকারীকে 'অফলাইন' হিসাবে চিহ্নিত করতে চাইতে পারেন যখন তাদের ক্লায়েন্ট সংযোগ বিচ্ছিন্ন হয়ে যায়।
ফায়ারবেস ডাটাবেস ক্লায়েন্ট সাধারণ আদিম প্রদান করে যা আপনি ডাটাবেসে লিখতে ব্যবহার করতে পারেন যখন একটি ক্লায়েন্ট ফায়ারবেস ডেটাবেস সার্ভার থেকে সংযোগ বিচ্ছিন্ন করে। ক্লায়েন্ট পরিষ্কারভাবে সংযোগ বিচ্ছিন্ন করুক বা না করুক এই আপডেটগুলি ঘটবে, তাই একটি সংযোগ ড্রপ বা ক্লায়েন্ট ক্র্যাশ হলেও আপনি ডেটা পরিষ্কার করার জন্য তাদের উপর নির্ভর করতে পারেন। সমস্ত লেখার ক্রিয়াকলাপ, সেটিং, আপডেট এবং অপসারণ সহ, সংযোগ বিচ্ছিন্ন করার পরে সঞ্চালিত হতে পারে।
onDisconnect
আদিম ব্যবহার করে সংযোগ বিচ্ছিন্ন হওয়ার পরে ডেটা লেখার একটি সহজ উদাহরণ এখানে:
Kotlin+KTX
val presenceRef = Firebase.database.getReference("disconnectmessage") // Write a string when this client loses connection presenceRef.onDisconnect().setValue("I disconnected!")
Java
DatabaseReference presenceRef = FirebaseDatabase.getInstance().getReference("disconnectmessage"); // Write a string when this client loses connection presenceRef.onDisconnect().setValue("I disconnected!");
কিভাবে অন ডিসকানেক্ট কাজ করে
আপনি একটি onDisconnect()
অপারেশন স্থাপন করলে, অপারেশনটি Firebase রিয়েলটাইম ডেটাবেস সার্ভারে থাকে। ব্যবহারকারী অনুরোধ করা লেখার ইভেন্টটি সম্পাদন করতে পারে তা নিশ্চিত করতে সার্ভার নিরাপত্তা পরীক্ষা করে এবং এটি অবৈধ হলে আপনার অ্যাপকে জানায়। সার্ভার তারপর সংযোগ নিরীক্ষণ. যদি কোনো সময়ে সংযোগের সময় শেষ হয়ে যায়, বা রিয়েলটাইম ডেটাবেস ক্লায়েন্ট দ্বারা সক্রিয়ভাবে বন্ধ হয়ে যায়, সার্ভারটি দ্বিতীয়বার নিরাপত্তা পরীক্ষা করে (অপারেশনটি এখনও বৈধ কিনা তা নিশ্চিত করতে) এবং তারপর ইভেন্টটি আহ্বান করে।
onDisconnect
সঠিকভাবে সংযুক্ত করা হয়েছে তা নিশ্চিত করতে আপনার অ্যাপটি রাইট অপারেশনে কলব্যাক ব্যবহার করতে পারে:
Kotlin+KTX
presenceRef.onDisconnect().removeValue { error, reference -> error?.let { Log.d(TAG, "could not establish onDisconnect event: ${error.message}") } }
Java
presenceRef.onDisconnect().removeValue(new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError error, @NonNull DatabaseReference reference) { if (error != null) { Log.d(TAG, "could not establish onDisconnect event:" + error.getMessage()); } } });
একটি onDisconnect
ইভেন্টও .cancel()
কল করে বাতিল করা যেতে পারে:
Kotlin+KTX
val onDisconnectRef = presenceRef.onDisconnect() onDisconnectRef.setValue("I disconnected") // ... // some time later when we change our minds // ... onDisconnectRef.cancel()
Java
OnDisconnect onDisconnectRef = presenceRef.onDisconnect(); onDisconnectRef.setValue("I disconnected"); // ... // some time later when we change our minds // ... onDisconnectRef.cancel();
সংযোগের অবস্থা সনাক্ত করা হচ্ছে
উপস্থিতি-সম্পর্কিত অনেক বৈশিষ্ট্যের জন্য, আপনার অ্যাপটি কখন অনলাইন বা অফলাইনে আছে তা জানার জন্য এটি উপযোগী। ফায়ারবেস রিয়েলটাইম ডেটাবেস /.info/connected
এ একটি বিশেষ অবস্থান প্রদান করে যা প্রতিবার Firebase রিয়েলটাইম ডেটাবেস ক্লায়েন্টের সংযোগের অবস্থা পরিবর্তনের সময় আপডেট করা হয়। এখানে একটি উদাহরণ:
Kotlin+KTX
val connectedRef = Firebase.database.getReference(".info/connected") connectedRef.addValueEventListener(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val connected = snapshot.getValue(Boolean::class.java) ?: false if (connected) { Log.d(TAG, "connected") } else { Log.d(TAG, "not connected") } } override fun onCancelled(error: DatabaseError) { Log.w(TAG, "Listener was cancelled") } })
Java
DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected"); connectedRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { boolean connected = snapshot.getValue(Boolean.class); if (connected) { Log.d(TAG, "connected"); } else { Log.d(TAG, "not connected"); } } @Override public void onCancelled(@NonNull DatabaseError error) { Log.w(TAG, "Listener was cancelled"); } });
/.info/connected
হল একটি বুলিয়ান মান যা রিয়েলটাইম ডেটাবেস ক্লায়েন্টের মধ্যে সিঙ্ক্রোনাইজ করা হয় না কারণ মানটি ক্লায়েন্টের অবস্থার উপর নির্ভর করে। অন্য কথায়, যদি একজন ক্লায়েন্ট /.info/connected
কে মিথ্যা বলে পড়ে, তাহলে আলাদা ক্লায়েন্ট যে মিথ্যা পড়বে তার কোন গ্যারান্টি নেই।
অ্যান্ড্রয়েডে, ব্যান্ডউইথ এবং ব্যাটারি ব্যবহার কমাতে Firebase স্বয়ংক্রিয়ভাবে সংযোগের অবস্থা পরিচালনা করে। যখন কোনো ক্লায়েন্টের কোনো সক্রিয় শ্রোতা না থাকে, কোনো মুলতুবি লেখা বা onDisconnect
অপারেশন না থাকে, এবং goOffline
পদ্ধতি দ্বারা স্পষ্টভাবে সংযোগ বিচ্ছিন্ন না হয়, তখন Firebase 60 সেকেন্ড নিষ্ক্রিয়তার পর সংযোগটি বন্ধ করে দেয়।
লেটেন্সি হ্যান্ডলিং
সার্ভার টাইমস্ট্যাম্প
ফায়ারবেস রিয়েলটাইম ডেটাবেস সার্ভারগুলি ডেটা হিসাবে সার্ভারে উত্পন্ন টাইমস্ট্যাম্পগুলি সন্নিবেশ করার জন্য একটি প্রক্রিয়া সরবরাহ করে। এই বৈশিষ্ট্যটি, onDisconnect
এর সাথে মিলিত, একটি রিয়েলটাইম ডেটাবেস ক্লায়েন্ট সংযোগ বিচ্ছিন্ন হওয়ার সময়টি নির্ভরযোগ্যভাবে নোট করার একটি সহজ উপায় প্রদান করে:
Kotlin+KTX
val userLastOnlineRef = Firebase.database.getReference("users/joe/lastOnline") userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)
Java
DatabaseReference userLastOnlineRef = FirebaseDatabase.getInstance().getReference("users/joe/lastOnline"); userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);
ঘড়ি নৈকতলীয়
যদিও firebase.database.ServerValue.TIMESTAMP
অনেক বেশি নির্ভুল, এবং বেশিরভাগ পঠন/লেখার ক্রিয়াকলাপের জন্য অগ্রাধিকারযোগ্য, এটি মাঝে মাঝে Firebase রিয়েলটাইম ডেটাবেসের সার্ভারের ক্ষেত্রে ক্লায়েন্টের ঘড়ির তির্যক অনুমান করতে উপযোগী হতে পারে। আপনি অবস্থান /.info/serverTimeOffset
এ একটি কলব্যাক সংযুক্ত করতে পারেন মান পেতে, মিলিসেকেন্ডে, যে ফায়ারবেস রিয়েলটাইম ডেটাবেস ক্লায়েন্টরা সার্ভারের সময় অনুমান করতে স্থানীয় রিপোর্ট করা সময় (মিলিসেকেন্ডে যুগের সময়) যোগ করে। নোট করুন যে এই অফসেটের নির্ভুলতা নেটওয়ার্কিং লেটেন্সি দ্বারা প্রভাবিত হতে পারে, এবং তাই এটি প্রাথমিকভাবে ঘড়ির সময়ের মধ্যে বড় (> 1 সেকেন্ড) অসঙ্গতিগুলি আবিষ্কার করার জন্য দরকারী।
Kotlin+KTX
val offsetRef = Firebase.database.getReference(".info/serverTimeOffset") offsetRef.addValueEventListener(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val offset = snapshot.getValue(Double::class.java) ?: 0.0 val estimatedServerTimeMs = System.currentTimeMillis() + offset } override fun onCancelled(error: DatabaseError) { Log.w(TAG, "Listener was cancelled") } })
Java
DatabaseReference offsetRef = FirebaseDatabase.getInstance().getReference(".info/serverTimeOffset"); offsetRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { double offset = snapshot.getValue(Double.class); double estimatedServerTimeMs = System.currentTimeMillis() + offset; } @Override public void onCancelled(@NonNull DatabaseError error) { Log.w(TAG, "Listener was cancelled"); } });
নমুনা উপস্থিতি অ্যাপ
সংযোগের অবস্থা পর্যবেক্ষণ এবং সার্ভার টাইমস্ট্যাম্পের সাথে সংযোগ বিচ্ছিন্ন অপারেশনগুলিকে একত্রিত করে, আপনি একটি ব্যবহারকারীর উপস্থিতি সিস্টেম তৈরি করতে পারেন। এই সিস্টেমে, একটি রিয়েলটাইম ডেটাবেস ক্লায়েন্ট অনলাইন আছে কিনা তা নির্দেশ করার জন্য প্রতিটি ব্যবহারকারী ডেটাবেস অবস্থানে ডেটা সঞ্চয় করে। ক্লায়েন্টরা এই অবস্থানটিকে সত্য হিসাবে সেট করে যখন তারা অনলাইনে আসে এবং একটি টাইমস্ট্যাম্প যখন তারা সংযোগ বিচ্ছিন্ন করে। এই টাইমস্ট্যাম্প নির্দেশ করে যে প্রদত্ত ব্যবহারকারী শেষবার অনলাইনে ছিলেন।
নোট করুন যে ব্যবহারকারীকে অনলাইনে চিহ্নিত করার আগে আপনার অ্যাপটি সংযোগ বিচ্ছিন্ন করার ক্রিয়াকলাপগুলিকে সারিবদ্ধ করা উচিত, উভয় কমান্ড সার্ভারে পাঠানোর আগে ক্লায়েন্টের নেটওয়ার্ক সংযোগ হারিয়ে যাওয়ার ক্ষেত্রে যে কোনও রেস পরিস্থিতি এড়াতে।
এখানে একটি সাধারণ ব্যবহারকারী উপস্থিতি সিস্টেম আছে:
Kotlin+KTX
// Since I can connect from multiple devices, we store each connection instance separately // any time that connectionsRef's value is null (i.e. has no children) I am offline val database = Firebase.database val myConnectionsRef = database.getReference("users/joe/connections") // Stores the timestamp of my last disconnect (the last time I was seen online) val lastOnlineRef = database.getReference("/users/joe/lastOnline") val connectedRef = database.getReference(".info/connected") connectedRef.addValueEventListener(object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { val connected = snapshot.getValue<Boolean>() ?: false if (connected) { val con = myConnectionsRef.push() // When this device disconnects, remove it con.onDisconnect().removeValue() // When I disconnect, update the last time I was seen online lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP) // Add this device to my connections list // this value could contain info about the device or a timestamp too con.setValue(java.lang.Boolean.TRUE) } } override fun onCancelled(error: DatabaseError) { Log.w(TAG, "Listener was cancelled at .info/connected") } })
Java
// Since I can connect from multiple devices, we store each connection instance separately // any time that connectionsRef's value is null (i.e. has no children) I am offline final FirebaseDatabase database = FirebaseDatabase.getInstance(); final DatabaseReference myConnectionsRef = database.getReference("users/joe/connections"); // Stores the timestamp of my last disconnect (the last time I was seen online) final DatabaseReference lastOnlineRef = database.getReference("/users/joe/lastOnline"); final DatabaseReference connectedRef = database.getReference(".info/connected"); connectedRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { boolean connected = snapshot.getValue(Boolean.class); if (connected) { DatabaseReference con = myConnectionsRef.push(); // When this device disconnects, remove it con.onDisconnect().removeValue(); // When I disconnect, update the last time I was seen online lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP); // Add this device to my connections list // this value could contain info about the device or a timestamp too con.setValue(Boolean.TRUE); } } @Override public void onCancelled(@NonNull DatabaseError error) { Log.w(TAG, "Listener was cancelled at .info/connected"); } });