-
Notifications
You must be signed in to change notification settings - Fork 120
Routing and WebSockets
@todo Diagram of how the whole app works (from Tower's perspective), similar to the older version of http://www.heroku.com/how (the one heroku has now is unintuitive).
Tower routes are modeled after Rails' routes. Here's how you might write a few for a blogging app:
Tower.Route.draw ->
@match "/login", to: "sessions#new", via: "get", as: "login"
@resources "posts", ->
@resources "comments"
@namespace "admin", ->
@resources "posts", ->
@resources "comments", only: "index"
@resources "users", ->
@resources "comments", only: "index"
@namespace "mobile", constraints: {subdomain: /^mobile$/}, ->
@root "/", to: "mobile#index"
@match "(/*path)", to: "application#index"
Tower.Route.draw ->
# this...
@resources "posts"
# is equivalent to this...
@match "/posts", to: "posts#index", via: "get"
@match "/posts/new", to: "posts#new", via: "get"
@match "/posts", to: "posts#create", via: "post"
@match "/posts/:id", to: "posts#show", via: "get"
@match "/posts/:id/edit", to: "posts#edit", via: "get"
@match "/posts/:id", to: "posts#update", via: "put"
@match "/posts/:id", to: "posts#destroy", via: "delete"
Note: Still figuring out what to call this... transaction/batch-request/etc.
When you save a model, the data eventually goes to Tower.notifyConnections
. The connections on the client should have a way of writing the data to the server. This means the "connection" object needs to know how to serialize create
, update
and delete
actions on a record into a URL dynamically. This means we have to rethink how routes work.
If you have a route to postsController#index
called /articles
, you might define that like this:
class App.Post extends Tower.Model
class App.PostsController extends Tower.Controller
Tower.Route.draw ->
@resources 'posts', as: 'article'
But, what if you start having nested controllers, such as App.Admin.PostsController, with route
/admin/posts. You might actually have two different names for the same model: the internal
/admin/posts, and the external
/articles. You may want this because, internally, you're okay with reading tables of the models as they actually are, but your client may have a specific niche market where
postdoesn't make sense but
articledoes. So how do you dynamically build these two urls given a single
@post` model from the within the store class?
You can either:
- manually specify the URL when you save the model, maybe
@post.save(url: '/admin/posts')
- or pass the controller/context with the model to the save method:
@post.save(controllerName: this.constructor.className())
.
Without passing those custom urls, the url will always be /articles
given a @post
object, because of the route definitions. It has no way of distinguishing which route should be used to build the name.
I think it should try to read the url from the model, as a method, and it also passes in the controller it's being called from. But this will take some time to flesh out because we don't want to start wiring the controller to the model.
Maybe you could tell the current controller that just called the save
method on the model somehow?
For now, it will just use the default /posts
route and map to the controller, until we can figure out how to make the dynamics more dynamic :).
The end goal is to make the client send data transparently to server-side controllers, whether through Ajax or WebSockets. This means that web socket params must be "routed" to the controller actions the same as basic HTTP and Ajax requests do. This means we'll have to wire web sockets into the express middleware pipeline somehow. It also means we can instantiate one controller per user/connection per controller class, which will be a decent performance-boosting optimization since you can cache things like the currentUser
.
To do that, we're also going to have to make logging look the same for each of the different methods.
Finally, if that were implemented, we're only one step away from having a "batch request api". This way you could update multiple records, even across different controllers, is a single HTTP request:
batchRequest [
{method: 'POST', url: '/posts', data: {title: 'New Post'}},
{method: 'PUT', url: '/posts/10', data: {title: 'Changed title on this one'}}
]
Once we have the method of passing everything through the express middleware layer this will be easy.
Request -> Connection -> Middleware -> Router -> Controller (that's cached in the Connection for the specific user/session)
We'll have to manage the server-side controllers more clearly, knowing that any variable that was set before an async function may have been changed by the time the async function finishes. So, use callbacks and don't set any variables.
For now, we'll just instantiate a new controller for each request, no matter what it is. Later we'll see if we can start reusing the controllers on a per-user basis.
Web sockets will work by creating a Tower.Net.Connection
for each user. Each connection will store an instance of each controller and actions the user has access to. It will instantiate these controllers when first accessed using computed properties. Then route matching the socket will act as if it's a request and pass through the controller action:
class Tower.Net.Connection
# finds route, then passes to each connection
@handle: (request) ->
route = Tower.Route.find(request)
return unless route
for connection in @all
connection.handle(route)
@all: []
constructor: (currentUser) ->
@currentUser = currentUser
@controllers
# already has route matching, so it can update all connections
handle: (request) ->
There is a "store" and a "stream" or "connection". The stream/connection defines how data is sent back and forth between client and server. These include:
- Ajax
- Web Socket
Thus, a store has a connection. Or more specifically, a store has a set of prioritized connections that fallback to less desirable methods of syncing data if the connection method is not available. (Really, this means any database connection is just a property of a store).
- Will pass through the already instantiated controller for the specific user, which is slightly more optimized, rather than an Ajax request that has to create a new controller for each request.
- Won't update all the connected clients if they have a cursor matching, because they should also be updated if there is a record created through the terminal, for example. So it needs to be lower level. Here's how that works. When a record is modified (created/updated/deleted), it passes through all connections on the server and passes it through the matching connections' matching cursors. On the client since there is only 1 connection it doesn't need the connection-cursor construct, just the cursor.
If a cursor is defined on a controller, it will ask the controller to match a record against its cursors rather than having the system internally just deal with the cursors. This provides you a space to access the currentUser
.
Exactly the same as one, but it should bulk update each connection. So if a User
and Post
are created in one transaction, it should send one request back to the client that looks something like this: {users: [User], posts: [Post]}
. If the connection doesn't allow a specific user to see that other users' posts, then there would be no update to that client.
- controller scope
- model scope
- after update
- store
- tower.net.connection
- update
- controllers
- scopes
- test
- scopes
- write to client
- controllers
- update
- tower.net.connection
- store
- client
- create
- connection
- update (beginPropertyChanges)
- controllers
- scopes
- test
- scopes
- write to server
- callback with status and validation errors
- controllers
- update (beginPropertyChanges)
- connection
- create
- Now, content negotiation implies that we can request a list of acceptable representations. What if I say “I want your feeds in ATOM first. If you don’t speak ATOM, gimme XML!”.
HTTPS is a combination of Hypertext Transfer Protocol (HTTP) with SSL/TLS protocol.