Tutorial: Synchronize time

Tips and Tricks Synchronize time

How to synchronize client with server time

The Webcom SDK makes it possible to retrieve the time difference (offset) between the user device and the Webcom server:

In JavaScript, the ServerlessDb Service maintains the currentState property in realtime that returns an instance of ServerlessDb.State.

// const app = Webcom.App("<your-app>"); // UNCOMMENT if you haven't yet an instance of your app!
const database = app.serverlessDb;
const timeOffsetMillis = database.currentState.connection.serverTimeOffsetMs;
const serverTime = new Date(Date.now() + timeOffsetMillis);
console.log("The server time is", serverTime);

If you need to be notified in realtime of the changes of the currentState, you can subscribe to the virtual ".info" data node!

In Kotlin, the Datasync Service maintains the state property in realtime that returns an instance of DatasyncState.

val myApp = WebcomApplication.default // the app defined by the 'webcom.properties' asset file
val datasync = myApp.datasyncService // this service is the entry point for all datasync-related features
datasync.state.connectionState.backendTimeOffsetMilliseconds?.let { timeOffsetMilliseconds ->
    val serverTime = Date(System.currentTimeMillis() + timeOffsetMilliseconds)
    println("The server time is: $serverTime")
}

If you need to be notified in realtime of the changes of the Datasync Service state, you can subscribe to its value using the subscribeToStateChange() method!

let datasyncManager = Webcom.defaultApplication.datasyncService.createManager()
datasyncManager.subscribeToStateChange { state in
    guard let backEndTimeOffsetMS = state.connectionState.backEndTimeOffsetMS else {
        return
    }
    print("server time offset changed:", backEndTimeOffsetMS, "ms")
    let backEndTimeOffsetSeconds: TimeInterval = Double(backEndTimeOffsetMS) / 1000
    let backEndDate = Date(timeIntervalSinceNow: backEndTimeOffsetSeconds)
    print("new server time is", backEndDate)
}

Write data with server timestamp

You can use the special variable "Webcom.ServerValue.TIMESTAMP" to store timestamp managed by the Webcom server. This is useful if you want to ensure consistent representation of time in your data. When you read (or listen) values written with this constant, you will receive an integer timestamp (representing the number of milliseconds elapsed since Unix Epoch). You can use the serverTimeOffset described above to display the local time of your client.

For example (replace “<your-app>” with your actual application identifier):

// const app = Webcom.App("<your-app>"); // UNCOMMENT if you haven't yet an instance of your app!
const database = app.serverlessDb;
const dataNode = database.rootNode.relativeNode("mypath");
dataNode
    .set({
        value: "test",
        time: Webcom.ServerValue.TIMESTAMP
    })
    .then(() => dataNode.get())
    .then(snapshot => {
        const data = snapshot.val();
        console.log(`Value '${data.value}' changed at ${new Date(data.time).toLocaleString()}`);
    });

When written into the Webcom database, the special TimeStampDate.Server object is substituted (on server side) with the actual timestamp of the Webcom back end. Of course, when read from the database as a TimeStampDate object, it is converted into the actual date time written by the server. This objet is useful when you need to ensure consistent representation of time within your data. You can also use the serverTimeOffset described above to compare it with the local time of the client device.

import com.orange.webcom.dessert.TimeStampDate

@Serializable
data class MyData(val value: String, val time: TimeStampDate)

val myApp = WebcomApplication.default // the app defined by the 'webcom.properties' asset file
val datasync = myApp.datasyncService // this service is the entry point for all datasync-related features
val manager = datasync.createManager()
val node = manager / "mypath"

node.set(MyData("test", TimeStampDate.Server)) {
    node.get {
        it.getOrNull()?.asA(MyData::class)?.let { data ->
            println("Value '${data.value}' changed at ${data.time}")
        }
    }
}

A WebcomDate value encapsulates a standard Date value. The type also provides a special .backEndCurrentDate instance that represents the Webcom server current date.

You can use the serverTimeOffset described above to display the local time of your client.

struct MyData: Codable {
    let value: String
    let time: WebcomDate
}

let datasyncManager = Webcom.defaultApplication.datasyncService.createManager()
let node = datasyncManager / "mypath"
node? <- MyData(value: "test", time: .backEndCurrentDate)
node?.subscribe(to: .valueChange) { event in
    guard let data: MyData = event.value.decoded() else {
        return
    }
    print("Value '\(data.value)' changed at '\(data.time.value)'")
}

Full example for a simple chat application

In the following code, we use a server timestamp to store the timestamp of new messages. When we display the message to the user we add the 'timeOffset', so that all messages seems to be timestamped from the local device clock (replace “<your-app>” with your actual application identifier):

// const app = Webcom.App("<your-app>"); // UNCOMMENT if you haven't yet an instance of your app!
const database = app.serverlessDb;
const messagesNode = database.rootNode.relativeNode("messages");

// Function called to push message into Webcom
function addMessage(username, message) {
    messagesNode.push({
      text: message,
      name: username,
      time: Webcom.ServerValue.TIMESTAMP
    });
}

messagesNode.subscribe(
    Webcom.Event.Child.Addition,
    Webcom.Callback(snapshot => {
        const {text: message, name: username = "anonymous", time} = snapshot.val();
        // Retrieve the device date corresponding to the message date
        const date = new Date(time - database.currentState.connection.serverTimeOffsetMs);

        // Now you just have to append the new message in you page...
    })
);
import com.orange.webcom.dessert.TimeStampDate
import com.orange.webcom.sdk.datasync.subscription.SubscribableEvent.*

@Serializable
data class Message(val text: String, val name: String, val time: TimeStampDate)

val myApp = WebcomApplication.default // the app defined by the 'webcom.properties' asset file
val datasync = myApp.datasyncService // this service is the entry point for all datasync-related features
val manager = datasync.createManager()
val messagesNode = manager.rootNode

// Function called to push message into Webcom
fun addMessage(username: String, message: String) {
    messagesNode.push(Message(message, username, TimeStampDate()))
}

messagesNode.subscribe(Child.Added::class) { // it: Notification<Child.Added>
    if (it is Child.Added) {
        it.value.asA(Message::class)
            // Retrieve the device date corresponding to the message date
            . run { copy(time = TimeStampDate(time - (datasync.state.connectionState.backendTimeOffsetMilliseconds ?: 0L))) }
            . let { message ->
                // Now you just have to append the new message in you page...
            }
    }
}
struct Message: Codable {
    let username: String
    let message: String
    let date: WebcomDate
}

let app = Webcom.defaultApplication
let manager = app.datasyncService.createManager()
let node = manager.rootNode

// Function called to push message into Webcom
func addMessage(username: String, message: String) {
    node.push(Message(username: username, message: message, date: .backEndCurrentDate))
}

node.subscribe(to: .childAddition) { event in
    guard let message: Message = event.value.decoded() else {
        return
    }
    // Retrieve the device date corresponding to the message date
    let deviceDate = app.datasyncService.state.connectionState.deviceDate(for: message.date)
    // Now you just have to append the new message in you page...
}