Commit bc88d541 authored by Bas Lijnse's avatar Bas Lijnse

Changed the websocket protocol to add ping messages and allowing exceptions to be

sent back to the client. Server still needs to send more information back.
parent ea6d613b
# The Web Client Runtime
## Introduction
The web-based user interfaces of iTasks are generated by applications, not directly as html, css and javascript, but as abstract user interface descriptions. These interface descriptions are interpreted by a generic run-time system, written in javascript that runs in a web browser and manages one or multiple iTasks 'viewports' in a web page. iTask applications don't have to use the full browser window, they can be just as easily embedded as part of another web-page.
## The basic idea
TODO
## The connection pool
TODO
## The Task-UI Protocol
In the current web run-time, websockets are used to communicate between web-browsers and iTask applications. Over these sockets a custom protocol is implemented that let's browsers create and attach to task instances.
The protocol mixes two communication styles. To control the connection, there a number of RPC-like commands. These are initiated by the client framework and the server responds with an acknowledgement or a response. Once the connection is set up, the server also pushes messages to the client framework.
These are used to synchronize the UI and to notify clients of server-side exceptions
### Commands
All commands are send as a triplet encoded in a JSON array. The first part is a unique command identifier (> 0). This is followed by the command name. The last part is a JSON object with parameters.
For example:
```
[42,"new",{}]
```
The server responds with a similar triplet. The first two parts match the command. The last part contains potential response data.
For example:
```
[42,"new",{instanceNo: 23423, instanceKey: 'asdfasdfasuasdfasdf'}]
```
If a command fails for some reason, the server responds with a message that has the same command identifier but has the string ```"exception"``` as second part, followed by an object with a description of the exception.
For example:
```
[42,"exception",{description: "Failed to create task instance"}]
```
The following commands are possible:
| Command | Description | Parameters | Example | Response attributes |
| ---------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | --------------------------- |
| `new` | Create a new session task instance. | none | `[23,"new",{}]` | `instanceNo`, `instanceKey` |
| `attach` | Link a task instance to the current connection. Each task instance can only be attached to one connection at a time. When a task instance is attached to a connection, that connection will accept events and receives UI changes. | `instanceNo`,`instanceKey` | `[18,"attach",{instanceNo: 364}]` | none |
| `detach` | Unlink a task instance from this connection | `instanceNo` | `[58,"detach",{instanceNo: 628}]` | none |
| `ui-event` | Send a UI event for one of the attached task instances. | `instanceNo` , `taskNo`, `action`, `edit`,`value` | `[58,"ui-event",{"instanceNo": 34, "taskNo": 5,"edit": "0-23-4-2","value": null}]` | none |
| `ping` | Let the server know that the attached instances are still alive. If the client framework has no events and does not send a ping event at regular intervals, the server will remove session task instances | none | `[67,"ping",{}]` | none |
### Notifications
Notifications have the same format as responses to commands. The only difference is that the command identifier is always `0`. Because they are not responses to specific commands, there is no identifier.
The following notifications are possible:
| Notification | Description | Attributes | Example |
| ------------ | ---------------------------------------- | -------------------------- | ---------------------------------------- |
| `ui-change` | Update the UI of one of the attached task instances | `instanceNo`, `change` | `[0,"ui-change",{"instanceNo": 395,change: []}]` |
| `exception` | One of the task had an uncaught exception. | `instanceNo`,`description` | `[0,"exception",{"instanceNo":65,"description":"Could ot read file x"}]` |
| `detach` | A task instance has been attached to another session, and is therefore detached from the current connection. | `instanceNo` | `[0,"detach",{"instanceNo": 47}]` |
......@@ -254,7 +254,6 @@ where
:: ChangeQueues :== Map InstanceNo (Queue UIChange)
import StdMisc
taskUIService :: ![PublishedTask] -> WebService ChangeQueues ChangeQueues
taskUIService taskUrls = { urlMatchPred = matchFun [url \\ {PublishedTask|url} <-taskUrls]
, completeRequest = True
......@@ -297,41 +296,76 @@ where
= ([wsockCloseMsg msg], True, instances, iworld)
handleEvent (WSPing msg) instances iworld
= ([wsockPongMsg msg], False, instances, iworld)
handleEvent (WSTextMessage msg) instances iworld //Process events TODO:Send confirmation messages after 'void' events (attach,detach,event)
handleEvent (WSTextMessage msg) instances iworld //Process commands
= case fromString msg of
// - new session
(JSONArray [JSONInt reqId,JSONString "new"])
(JSONArray [JSONInt commandId, JSONString "new" :_])
= case createTaskInstance` req taskUrls iworld of
(Error (_,err), iworld)
= abort err
// # json = JSONArray [JSONInt reqId,JSONString "ERROR",JSONString err]
// = (wsockTextMsg (toString json),False, instances,iworld)
# json = JSONArray [JSONInt commandId, JSONString "exception",JSONObject [("description",JSONString err)]]
= (wsockTextMsg (toString json),False, instances,iworld)
(Ok (instanceNo,instanceKey),iworld)
# json = JSONArray [JSONInt reqId, JSONObject [("instanceNo",JSONInt instanceNo),("instanceKey",JSONString instanceKey)]]
# json = JSONArray [JSONInt commandId, JSONString "new", JSONObject [("instanceNo",JSONInt instanceNo),("instanceKey",JSONString instanceKey)]]
= (wsockTextMsg (toString json),False, instances, iworld)
// - attach existing instance
(JSONArray [JSONString "attach",JSONInt instanceNo,JSONString instanceKey])
//Clear all io and queue a Reset event to make sure we start with a fresh GUI
# iworld = attachViewport instanceNo iworld
# (_,iworld) = updateInstanceConnect clientname [instanceNo] iworld
= ([],False, [(instanceNo,instanceKey):instances], iworld)
(JSONArray [JSONInt commandId, JSONString "attach", args=:(JSONObject _)])
= case (jsonQuery "instanceNo" args, jsonQuery "instanceKey" args) of
(Just instanceNo, Just instanceKey)
//Clear all io and queue a Reset event to make sure we start with a fresh GUI
= case updateInstanceConnect clientname [instanceNo] iworld of
(Error (_,err),iworld)
# json = JSONArray [JSONInt commandId, JSONString "exception",JSONObject [("description",JSONString err)]]
= (wsockTextMsg (toString json),False, instances, iworld)
(Ok (), iworld)
# iworld = attachViewport instanceNo iworld
# json = JSONArray [JSONInt commandId, JSONString "attach", JSONObject []]
= (wsockTextMsg (toString json),False, [(instanceNo,instanceKey):instances], iworld)
_
# json = JSONArray [JSONInt commandId, JSONString "exception", JSONObject [("description",JSONString "Missing command parameters")]]
= (wsockTextMsg (toString json),False, instances, iworld)
// - detach instance
(JSONArray [JSONString "detach",JSONInt instanceNo])
# iworld = detachViewport instanceNo iworld
# (_,iworld) = updateInstanceDisconnect [instanceNo] iworld
= ([],False, filter (((==) instanceNo) o fst) instances, iworld)
(JSONArray [JSONString "event",JSONInt instanceNo,JSONArray [JSONString taskId,JSONNull,JSONString actionId]]) //Action event
# iworld = queueEvent instanceNo (ActionEvent (fromString taskId) actionId) iworld
= ([],False, instances,iworld)
(JSONArray [JSONString "event",JSONInt instanceNo,JSONArray [JSONString taskId,JSONString name,value]]) //Edit event
# iworld = queueEvent instanceNo (EditEvent (fromString taskId) name value) iworld
= ([],False, instances,iworld)
(JSONArray [JSONInt commandId, JSONString "detach", args=:(JSONObject _)])
= case (jsonQuery "instanceNo" args) of
(Just instanceNo)
= case updateInstanceDisconnect [instanceNo] iworld of
(Error (_,err),iworld)
# json = JSONArray [JSONInt commandId, JSONString "exception",JSONObject [("description",JSONString err)]]
= (wsockTextMsg (toString json),False, instances, iworld)
(Ok (), iworld)
# iworld = detachViewport instanceNo iworld
# json = JSONArray [JSONInt commandId, JSONString "detach", JSONObject []]
= ([],False, filter (((==) instanceNo) o fst) instances, iworld)
_
# json = JSONArray [JSONInt commandId, JSONString "exception", JSONObject [("description",JSONString "Missing command parameters")]]
= (wsockTextMsg (toString json),False, instances, iworld)
// - UI events
(JSONArray [JSONInt commandId, JSONString "ui-event", args=:(JSONObject _)])
= case parseEvent args of
(Just (instanceNo,event))
# iworld = queueEvent instanceNo event iworld
# json = JSONArray [JSONInt commandId, JSONString "ui-event", JSONObject []]
= (wsockTextMsg (toString json), False, instances, iworld)
_
# json = JSONArray [JSONInt commandId, JSONString "exception", JSONObject [("description",JSONString "Missing event parameters")]]
= (wsockTextMsg (toString json),False, instances, iworld)
// - Pings
(JSONArray [JSONInt commandId, JSONString "ping",_])
//TODO: Update timeout data for all instances
# json = JSONArray [JSONInt commandId, JSONString "ping", JSONObject []]
= (wsockTextMsg (toString json),False, instances, iworld)
//Unknown message
e
# json = JSONArray [JSONString "ERROR",JSONString "Unknown event"]
# json = JSONArray [JSONInt 0, JSONString "exception", JSONObject [("description",JSONString "Unknown command")]]
= (wsockTextMsg (toString json),False, instances, iworld)
parseEvent json = case (jsonQuery "instanceNo" json, jsonQuery "taskNo" json, jsonQuery "action" json) of
(Just i,Just t,Just a) = Just (i, ActionEvent (TaskId i t) a)
(Just i,Just t,_) = case (jsonQuery "edit" json, jsonQuery "value" json) of
(Just (JSONString e),Just v) = (Just (i, EditEvent (TaskId i t) e v))
_ = Nothing
_ = Nothing
shareChangeFun _ _ connState iworld = ([], False, connState, Nothing, iworld)
onTick req output (clientname,state,instances) iworld
......@@ -343,8 +377,9 @@ where
[] = ([],False,(clientname,state,instances),Nothing,iworld)
changes
# (_,iworld) = updateInstanceLastIO (map fst instances) iworld
# msgs = [wsockTextMsg (toString (JSONObject [("instance",JSONInt instanceNo)
,("change",encodeUIChange change)])) \\ (instanceNo,change) <- changes]
# msgs =
[wsockTextMsg (toString (JSONArray [JSONInt 0,JSONString "ui-change"
,JSONObject [("instanceNo",JSONInt instanceNo),("change",encodeUIChange change)]])) \\ (instanceNo,change) <- changes]
= (flatten msgs,False, (clientname,state,instances),Just output,iworld)
disconnectFun _ _ (clientname,state,instances) iworld = (Nothing, snd (updateInstanceDisconnect (map fst instances) iworld))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment