Cettia 1.0.0-Alpha2 released

wrote this on

I’m pleased to announce that Cetita Protocol 1.0.0-Alpha2, Cettia JavaScript Client 1.0.0-Alpha2 and Cettia Java Server 1.0.0-Alpha2 have been released. The theme of this release is offline application, which provides a flexible way to deal with disconnection making it highly annoying to build real-time application.

In the real world, losing connection is not uncommon so that it is pretty important to make applications relying on full-duplex connection functional while offline in some way. For example, users lose connection every time they navigate from one page to another page and may lose network while on the move. Even if the time between disconnection and reconnection is very short, it’s true that any message can’t be sent and received after every disconnection and authentication which is a process to verify a user is who they say they are should be done and messages which couldn’t be sent and received meanwhile should be synchronized after every reconnection. Also if reconnection doesn’t occur for a long time, these messages might have to be sent through each user’s email.

To solve such problems, it has been required to design and implement application’s own protocol on top of the full duplex connection. With this feature, you can handle sockets regardless of their online/offline state as well as deal with such issues with ease just by handling some reserved socket events. Moreover, a reference to socket is not affected by disconnection and reconnection at all and doing authentication once is enough for socket. Of course, it applies to every transport not just HTTP based ones.

Let’s take a look at the new feature through code snippet.

JavaScript Client

var socket = cettia.open(uri, {
    // 'name' option allows for the socket of the next page to inherit the lifecycle of the socket of the current page
    // This means the server can cache events which couldn't be sent to the socket of the previous page 
    // due to temporary disconnection during page navigation and send them to the socket of the next page on reconnection
    // With this option, you don't need to follow single page application model to workaround such issues
    name: "main"
});

// A queue containing events the client couldn't send to the server while disconnection
var cache = [];

// Fired when the server issues a new id for this socket as the beginning of the new lifecycle and the end of the old lifecycle
// The 'open' event always follows this event but not vice versa
socket.on("new", function() {
    // You can reset resources having been used for the old lifecycle for the new lifecycle here
    cache.length = 0;
});

// Fired when the underlying transport establishes a connection
socket.on("open", function() {
    // Now that communication is possible, you can flush the cache
    while(socket.state() === "opened" && cache.length) {
        // Removes the first event from the cache and sends it to the server one by one
        var args = cache.shift();
        socket.send.apply(socket, args);
    }
});

// Fired if some event is sent via this socket while there is no connection
socket.on("cache", function(args) {
    // You can determine whether or not to cache this arguments used to call the 'send' method
    // For example, in some cases, you may want to avoid caching to deliver live data in time
    cache.push(args);
});

// You don't need to pay attention to socket's online/offline state at all
// Internally, it will be delegated to the underlying transport while online and the 'cache' event while offline
socket.send("event", data);

Java Server

// Fired when a socket has been created as the beginning of the lifecycle
// However the handshake is not performed yet and it is not allowed to exchange events
server.onsocket(new Action<ServerSocket>() {
    @Override
    public void on(final ServerSocket socket) {
        // You can authenticate a given socket here e.g. using token-based approach or cookie-based approach
        final Map<String, String> authentication = new TokenVerifier().verify(new Uri(socket.uri()).parameter("token"));

        // Once is enough because this reference is unaffected by disconnection and reconnection
        socket.tag(authentication.get("username"));
        // Then, it's possible to send events to a specific user who may have used multiple devices by username
        // The given socket represents a specific device like desktop, phone, tablet and so on
        // e.g. server.byTag("flowersinthesand", socket -> socket.send("directmessage", "Hello there!"));
        
        // A queue containing events the server couldn't send to the client while disconnection
        final Queue<Object[]> cache = new ConcurrentLinkedQueue<>();
        
        // Fired when the handshake is performed successfully
        socket.onopen(new VoidAction() {
            @Override
            public void on() {
                // Now that communication is possible, you can flush the cache
                while (socket.state() == ServerSocket.State.OPENED && !cache.isEmpty()) {
                    // Removes the first event from the cache and sends it to the client one by one
                    Object[] args = cache.poll();
                    socket.send((String) args[0], args[1], (Action<?>) args[2], (Action<?>) args[3]);
                }
            }
        });

        // Fired if some event is sent via this socket while there is no connection
        socket.oncache(new Action<Object[]>() {
            @Override
            public void on(Object[] args) {
                // You can determine whether or not to cache this arguments used to call the 'send' method
                // For example, in some cases, you may want to avoid caching to deliver live data in time
                cache.offer(args);
            }
        });

        // Fired if the socket has been closed for a long time i.e. 1 minute and deleted from the server as the end of the lifecycle
        // A deleted socket can't be and shouldn't be used
        socket.ondelete(new VoidAction() {
            @Override
            public void on() {
                // If the cache is not empty, that is to say, there are still some messages user should receive
                // you can send an email to notify user or store them to database for user to check on next logging in
                if (!cache.isEmpty()) {
                    Account account = Account.findByUsername(authentication.get("username"));
                    // Assumes this method checks if user have not really received the unread messages
                    // or some of them through other devices or other sockets and sends an email 
                    // to notify user of the final unread messages if they exist
                    account.notifyUnreadMessages(cache);
                }
            }
        });        
    }
});
    
// You can pass a socket action to server at any time not paying attention to given socket's online/offline state
server.all(new Action<ServerSocket>() {
    @Override
    public void on(final ServerSocket socket) {
        // It will be delegated to the underlying transport while online or the cache event while offline
        socket.send("event", data);
    }
});

For your information, TokenVerifier, Uri and Account are imaginary classes to help describe a scenario matching with this feature well. As you can see that the above code snippets are just boilerplate, we have agonized over introducing helper classes to remove these boilerplate but have decided to wait for the community’s feedback. So don’t hesitate to give us your thought about this.

Here’s the full changelog:

As always, please let us, Cettia Groups, know if you have any question or feedback.