ความสามารถออฟไลน์บนแพลตฟอร์ม Apple

แอปพลิเคชัน Firebase ทำงานได้แม้ว่าแอปของคุณจะขาดการเชื่อมต่อเครือข่ายชั่วคราว นอกจากนี้ Firebase ยังมีเครื่องมือสำหรับการคงข้อมูลในเครื่อง การจัดการสถานะ และการจัดการเวลาแฝง

ความคงอยู่ของดิสก์

แอป Firebase จะจัดการการหยุดชะงักของเครือข่ายชั่วคราวโดยอัตโนมัติ ข้อมูลแคชพร้อมใช้งานขณะออฟไลน์ และ Firebase จะส่งการเขียนอีกครั้งเมื่อมีการกู้คืนการเชื่อมต่อเครือข่าย

เมื่อคุณเปิดใช้งานการคงอยู่ของดิสก์ แอปของคุณจะเขียนข้อมูลในเครื่องไปยังอุปกรณ์เพื่อให้แอปของคุณสามารถรักษาสถานะขณะออฟไลน์ แม้ว่าผู้ใช้หรือระบบปฏิบัติการจะรีสตาร์ทแอปก็ตาม

คุณสามารถเปิดใช้งานการคงอยู่ของดิสก์ด้วยโค้ดเพียงบรรทัดเดียว

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
Database.database().isPersistenceEnabled = true

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
[FIRDatabase database].persistenceEnabled = YES;

พฤติกรรมการคงอยู่

เมื่อเปิดใช้งานการคงอยู่ ข้อมูลใดๆ ที่ไคลเอ็นต์ฐานข้อมูลเรียลไทม์ของ Firebase จะซิงค์ในขณะที่ออนไลน์ยังคงอยู่ในดิสก์และพร้อมใช้งานแบบออฟไลน์ แม้ว่าผู้ใช้หรือระบบปฏิบัติการจะรีสตาร์ทแอปก็ตาม ซึ่งหมายความว่าแอปของคุณทำงานเหมือนออนไลน์โดยใช้ข้อมูลในเครื่องที่จัดเก็บไว้ในแคช การโทรกลับของผู้ฟังจะยังคงเริ่มทำงานสำหรับการอัปเดตในเครื่อง

ไคลเอ็นต์ฐานข้อมูลเรียลไทม์ของ Firebase จะเก็บคิวของการดำเนินการเขียนทั้งหมดที่ดำเนินการในขณะที่แอปของคุณออฟไลน์โดยอัตโนมัติ เมื่อเปิดใช้งานการคงอยู่ คิวนี้จะยังคงอยู่ในดิสก์ ดังนั้นการเขียนทั้งหมดของคุณจะพร้อมใช้งานเมื่อผู้ใช้หรือระบบปฏิบัติการรีสตาร์ทแอป เมื่อแอปสามารถเชื่อมต่อได้อีกครั้ง การดำเนินการทั้งหมดจะถูกส่งไปยังเซิร์ฟเวอร์ Firebase Realtime Database

หากแอปของคุณใช้การ ตรวจสอบสิทธิ์ Firebase ไคลเอ็นต์ฐานข้อมูลเรียลไทม์ของ Firebase จะคงโทเค็นการตรวจสอบสิทธิ์ของผู้ใช้ในการรีสตาร์ทแอป หากโทเค็นการตรวจสอบความถูกต้องหมดอายุในขณะที่แอปของคุณออฟไลน์ ไคลเอ็นต์จะหยุดการเขียนชั่วคราวจนกว่าแอปจะตรวจสอบสิทธิ์ผู้ใช้อีกครั้ง มิฉะนั้น การดำเนินการเขียนอาจล้มเหลวเนื่องจากกฎความปลอดภัย

การรักษาข้อมูลให้สดใหม่

ฐานข้อมูลเรียลไทม์ของ Firebase จะซิงโครไนซ์และจัดเก็บสำเนาข้อมูลในเครื่องสำหรับผู้ฟังที่ใช้งานอยู่ นอกจากนี้ คุณสามารถซิงค์สถานที่เฉพาะได้

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.keepSynced(true)

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[scoresRef keepSynced:YES];

ไคลเอ็นต์ฐานข้อมูลเรียลไทม์ของ Firebase จะดาวน์โหลดข้อมูลที่ตำแหน่งเหล่านี้โดยอัตโนมัติและซิงค์ข้อมูลแม้ว่าข้อมูลอ้างอิงจะไม่มีการฟังที่ใช้งานอยู่ก็ตาม คุณสามารถปิดการซิงโครไนซ์กลับด้วยรหัสบรรทัดต่อไปนี้

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
scoresRef.keepSynced(false)

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
[scoresRef keepSynced:NO];

โดยค่าเริ่มต้น 10MB ของข้อมูลที่ซิงค์ก่อนหน้านี้จะถูกแคชไว้ ซึ่งน่าจะเพียงพอสำหรับการใช้งานส่วนใหญ่ หากแคชเกินขนาดที่กำหนดค่าไว้ ฐานข้อมูลเรียลไทม์ของ Firebase จะล้างข้อมูลที่ใช้ไปอย่างน้อยที่สุดเมื่อเร็วๆ นี้ ข้อมูลที่ซิงค์ไว้จะไม่ถูกล้างออกจากแคช

การสืบค้นข้อมูลออฟไลน์

ฐานข้อมูลเรียลไทม์ของ Firebase จัดเก็บข้อมูลที่ส่งคืนจากการสืบค้นเพื่อใช้เมื่อออฟไลน์ สำหรับการสืบค้นที่สร้างขณะออฟไลน์ ฐานข้อมูลเรียลไทม์ของ Firebase จะยังคงทำงานต่อไปสำหรับข้อมูลที่โหลดไว้ก่อนหน้านี้ หากยังไม่ได้โหลดข้อมูลที่ร้องขอ ฐานข้อมูลเรียลไทม์ของ Firebase จะโหลดข้อมูลจากแคชในเครื่อง เมื่อมีการเชื่อมต่อเครือข่ายอีกครั้ง ข้อมูลจะโหลดและจะแสดงข้อความค้นหา

ตัวอย่างเช่น โค้ดนี้จะค้นหาสี่รายการสุดท้ายในฐานข้อมูลคะแนนเรียลไทม์ของ Firebase

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.queryOrderedByValue().queryLimited(toLast: 4).observe(.childAdded) { snapshot in
  print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")")
}

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[[[scoresRef queryOrderedByValue] queryLimitedToLast:4]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
      NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value);
    }];

สมมติว่าผู้ใช้ขาดการเชื่อมต่อ ออฟไลน์ และรีสตาร์ทแอป ขณะที่ออฟไลน์ แอปจะสอบถามสองรายการสุดท้ายจากตำแหน่งเดียวกัน ข้อความค้นหานี้จะส่งคืนสองรายการสุดท้ายได้สำเร็จ เนื่องจากแอปได้โหลดทั้งสี่รายการในข้อความค้นหาด้านบนนี้

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
scoresRef.queryOrderedByValue().queryLimited(toLast: 2).observe(.childAdded) { snapshot in
  print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")")
}

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
[[[scoresRef queryOrderedByValue] queryLimitedToLast:2]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
      NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value);
    }];

ในตัวอย่างก่อนหน้านี้ ไคลเอ็นต์ฐานข้อมูลเรียลไทม์ของ Firebase จะเพิ่มเหตุการณ์ 'เด็กที่เพิ่ม' สำหรับไดโนเสาร์สองตัวที่มีคะแนนสูงสุด โดยใช้แคชที่คงอยู่ แต่จะไม่ทำให้เกิดเหตุการณ์ 'มูลค่า' เนื่องจากแอปไม่เคยดำเนินการค้นหานั้นขณะออนไลน์

หากแอปขอหกรายการสุดท้ายขณะออฟไลน์ แอปจะได้รับเหตุการณ์ 'เด็กที่เพิ่ม' สำหรับรายการที่แคชสี่รายการทันที เมื่ออุปกรณ์กลับมาออนไลน์อีกครั้ง ไคลเอ็นต์ฐานข้อมูลเรียลไทม์ของ Firebase จะซิงโครไนซ์กับเซิร์ฟเวอร์และรับเหตุการณ์ 'ลูกที่เพิ่ม' และ 'ค่า' สองรายการสุดท้ายสำหรับแอป

การจัดการธุรกรรมออฟไลน์

ธุรกรรมใดๆ ที่ดำเนินการในขณะที่แอปออฟไลน์ จะถูกจัดคิว เมื่อแอปเชื่อมต่อเครือข่ายได้อีกครั้ง ธุรกรรมจะถูกส่งไปยังเซิร์ฟเวอร์ Realtime Database

การจัดการการแสดงตน

ในแอปพลิเคชันเรียลไทม์ มักจะมีประโยชน์ในการตรวจสอบเมื่อไคลเอ็นต์เชื่อมต่อและตัดการเชื่อมต่อ ตัวอย่างเช่น คุณอาจต้องการทำเครื่องหมายผู้ใช้เป็น 'ออฟไลน์' เมื่อไคลเอ็นต์ยกเลิกการเชื่อมต่อ

ไคลเอ็นต์ฐานข้อมูล Firebase จัดเตรียมข้อมูลพื้นฐานอย่างง่ายที่คุณสามารถใช้เพื่อเขียนไปยังฐานข้อมูลเมื่อไคลเอ็นต์ยกเลิกการเชื่อมต่อจากเซิร์ฟเวอร์ฐานข้อมูล Firebase การอัปเดตเหล่านี้เกิดขึ้นไม่ว่าไคลเอ็นต์จะยกเลิกการเชื่อมต่อทั้งหมดหรือไม่ก็ตาม คุณจึงวางใจได้ในการล้างข้อมูลแม้ว่าการเชื่อมต่อจะหลุดหรือไคลเอ็นต์ขัดข้อง การดำเนินการเขียนทั้งหมด รวมถึงการตั้งค่า การอัปเดต และการลบ สามารถทำได้เมื่อตัดการเชื่อมต่อ

ต่อไปนี้คือตัวอย่างง่ายๆ ของการเขียนข้อมูลเมื่อขาดการเชื่อมต่อโดยใช้ onDisconnect primitive:

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
let presenceRef = Database.database().reference(withPath: "disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnectSetValue("I disconnected!")

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
FIRDatabaseReference *presenceRef = [[FIRDatabase database] referenceWithPath:@"disconnectmessage"];
// Write a string when this client loses connection
[presenceRef onDisconnectSetValue:@"I disconnected!"];

วิธี onDisconnect ทำงาน

เมื่อคุณสร้างการดำเนินการ onDisconnect() การดำเนินการดังกล่าวจะอยู่บนเซิร์ฟเวอร์ฐานข้อมูลเรียลไทม์ของ Firebase เซิร์ฟเวอร์ตรวจสอบความปลอดภัยเพื่อให้แน่ใจว่าผู้ใช้สามารถดำเนินการเขียนเหตุการณ์ที่ร้องขอ และแจ้งให้แอปของคุณทราบหากไม่ถูกต้อง เซิร์ฟเวอร์จะตรวจสอบการเชื่อมต่อ หากเมื่อใดที่การเชื่อมต่อหมดเวลา หรือถูกปิดโดยไคลเอนต์ Realtime Database เซิร์ฟเวอร์จะตรวจสอบความปลอดภัยเป็นครั้งที่สอง (เพื่อให้แน่ใจว่าการดำเนินการยังคงถูกต้อง) จากนั้นเรียกใช้เหตุการณ์

แอปของคุณสามารถใช้การโทรกลับในการดำเนินการเขียนเพื่อให้แน่ใจว่าเชื่อมต่อ onDisconnect อย่างถูกต้อง:

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
presenceRef.onDisconnectRemoveValue { error, reference in
  if let error = error {
    print("Could not establish onDisconnect event: \(error)")
  }
}

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
[presenceRef onDisconnectRemoveValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *reference) {
  if (error != nil) {
    NSLog(@"Could not establish onDisconnect event: %@", error);
  }
}];

เหตุการณ์ onDisconnect สามารถยกเลิกได้โดยการโทร .cancel() :

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
presenceRef.onDisconnectSetValue("I disconnected")
// some time later when we change our minds
presenceRef.cancelDisconnectOperations()

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
[presenceRef onDisconnectSetValue:@"I disconnected"];
// some time later when we change our minds
[presenceRef cancelDisconnectOperations];

กำลังตรวจจับสถานะการเชื่อมต่อ

สำหรับฟีเจอร์ที่เกี่ยวข้องกับการแสดงตนจำนวนมาก แอปของคุณจะรู้ว่าแอปออนไลน์หรือออฟไลน์เมื่อใดจะมีประโยชน์ ฐานข้อมูลเรียลไทม์ของ Firebase มีตำแหน่งพิเศษที่ /.info/connected ซึ่งอัปเดตทุกครั้งที่สถานะการเชื่อมต่อของไคลเอ็นต์ Firebase Realtime Database เปลี่ยนแปลง นี่คือตัวอย่าง:

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
let connectedRef = Database.database().reference(withPath: ".info/connected")
connectedRef.observe(.value, with: { snapshot in
  if snapshot.value as? Bool ?? false {
    print("Connected")
  } else {
    print("Not connected")
  }
})

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:@".info/connected"];
[connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  if([snapshot.value boolValue]) {
    NSLog(@"connected");
  } else {
    NSLog(@"not connected");
  }
}];

/.info/connected เป็นค่าบูลีนที่ไม่ได้ซิงโครไนซ์ระหว่างไคลเอนต์ Realtime Database เนื่องจากค่าจะขึ้นอยู่กับสถานะของไคลเอนต์ กล่าวอีกนัยหนึ่ง ถ้าไคลเอนต์หนึ่งอ่าน /.info/connected เป็นเท็จ ก็ไม่รับประกันว่าไคลเอนต์แยกต่างหากจะอ่านเท็จด้วย

เวลาในการตอบสนองการจัดการ

เวลาประทับของเซิร์ฟเวอร์

เซิร์ฟเวอร์ฐานข้อมูลเรียลไทม์ของ Firebase มีกลไกในการแทรกการประทับเวลาที่สร้างบนเซิร์ฟเวอร์เป็นข้อมูล คุณลักษณะนี้เมื่อรวมกับ onDisconnect จะเป็นวิธีที่ง่ายในการจดบันทึกเวลาที่ไคลเอ็นต์ Realtime Database ยกเลิกการเชื่อมต่อ:

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
let userLastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline")
userLastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
FIRDatabaseReference *userLastOnlineRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/lastOnline"];
[userLastOnlineRef onDisconnectSetValue:[FIRServerValue timestamp]];

นาฬิกาเอียง

แม้ว่า firebase.database.ServerValue.TIMESTAMP จะมีความแม่นยำมากกว่า และดีกว่าสำหรับการดำเนินการอ่าน/เขียนส่วนใหญ่ แต่บางครั้งอาจเป็นประโยชน์ในการประมาณการนาฬิกาของไคลเอ็นต์เอียงเมื่อเทียบกับเซิร์ฟเวอร์ของฐานข้อมูลเรียลไทม์ของ Firebase คุณสามารถแนบการเรียกกลับไปยังตำแหน่ง /.info/serverTimeOffset เพื่อรับค่าในหน่วยมิลลิวินาทีที่ไคลเอนต์ Firebase Realtime Database เพิ่มไปยังเวลาที่รายงานในเครื่อง (เวลาของยุคในหน่วยมิลลิวินาที) เพื่อประมาณเวลาของเซิร์ฟเวอร์ โปรดทราบว่าความแม่นยำของออฟเซ็ตนี้อาจได้รับผลกระทบจากเวลาแฝงของเครือข่าย และดังนั้นจึงมีประโยชน์ในเบื้องต้นสำหรับการค้นหาความคลาดเคลื่อนขนาดใหญ่ (> 1 วินาที) ในเวลานาฬิกา

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
let offsetRef = Database.database().reference(withPath: ".info/serverTimeOffset")
offsetRef.observe(.value, with: { snapshot in
  if let offset = snapshot.value as? TimeInterval {
    print("Estimated server time in milliseconds: \(Date().timeIntervalSince1970 * 1000 + offset)")
  }
})

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
FIRDatabaseReference *offsetRef = [[FIRDatabase database] referenceWithPath:@".info/serverTimeOffset"];
[offsetRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  NSTimeInterval offset = [(NSNumber *)snapshot.value doubleValue];
  NSTimeInterval estimatedServerTimeMs = [[NSDate date] timeIntervalSince1970] * 1000.0 + offset;
  NSLog(@"Estimated server time: %0.3f", estimatedServerTimeMs);
}];

ตัวอย่างการแสดงตน App

ด้วยการรวมการดำเนินการยกเลิกการเชื่อมต่อเข้ากับการตรวจสอบสถานะการเชื่อมต่อและการประทับเวลาของเซิร์ฟเวอร์ คุณสามารถสร้างระบบการแสดงตนของผู้ใช้ได้ ในระบบนี้ ผู้ใช้แต่ละรายจัดเก็บข้อมูลที่ตำแหน่งฐานข้อมูลเพื่อระบุว่าไคลเอ็นต์ Realtime Database ออนไลน์อยู่หรือไม่ ลูกค้าตั้งค่าตำแหน่งนี้เป็นจริงเมื่อออนไลน์และประทับเวลาเมื่อยกเลิกการเชื่อมต่อ การประทับเวลานี้ระบุเวลาล่าสุดที่ผู้ใช้ที่ระบุออนไลน์

โปรดทราบว่าแอปของคุณควรจัดคิวการดำเนินการยกเลิกการเชื่อมต่อก่อนที่ผู้ใช้จะถูกทำเครื่องหมายว่าออนไลน์ เพื่อหลีกเลี่ยงสภาวะการแข่งขันในกรณีที่การเชื่อมต่อเครือข่ายของไคลเอ็นต์ขาดหายไปก่อนที่จะส่งคำสั่งทั้งสองไปยังเซิร์ฟเวอร์

นี่คือระบบการแสดงตนของผู้ใช้อย่างง่าย:

Swift

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
// 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
let myConnectionsRef = Database.database().reference(withPath: "users/morgan/connections")

// stores the timestamp of my last disconnect (the last time I was seen online)
let lastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline")

let connectedRef = Database.database().reference(withPath: ".info/connected")

connectedRef.observe(.value, with: { snapshot in
  // only handle connection established (or I've reconnected after a loss of connection)
  guard let connected = snapshot.value as? Bool, connected else { return }

  // add this device to my connections list
  let con = myConnectionsRef.childByAutoId()

  // when this device disconnects, remove it.
  con.onDisconnectRemoveValue()

  // The onDisconnect() call is before the call to set() itself. This is to avoid a race condition
  // where you set the user's presence to true and the client disconnects before the
  // onDisconnect() operation takes effect, leaving a ghost user.

  // this value could contain info about the device or a timestamp instead of just true
  con.setValue(true)

  // when I disconnect, update the last time I was seen online
  lastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())
})

วัตถุประสงค์-C

หมายเหตุ: ผลิตภัณฑ์ Firebase นี้ไม่มีในเป้าหมาย App Clip
// 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
FIRDatabaseReference *myConnectionsRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/connections"];

// stores the timestamp of my last disconnect (the last time I was seen online)
FIRDatabaseReference *lastOnlineRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/lastOnline"];

FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:@".info/connected"];
[connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  if([snapshot.value boolValue]) {
    // connection established (or I've reconnected after a loss of connection)

    // add this device to my connections list
    FIRDatabaseReference *con = [myConnectionsRef childByAutoId];

    // when this device disconnects, remove it
    [con onDisconnectRemoveValue];

    // The onDisconnect() call is before the call to set() itself. This is to avoid a race condition
    // where you set the user's presence to true and the client disconnects before the
    // onDisconnect() operation takes effect, leaving a ghost user.

    // this value could contain info about the device or a timestamp instead of just true
    [con setValue:@YES];


    // when I disconnect, update the last time I was seen online
    [lastOnlineRef onDisconnectSetValue:[FIRServerValue timestamp]];
  }
}];