-
Notifications
You must be signed in to change notification settings - Fork 2
Client API Library
This library is served by the server itself, in order to easily propagate bugfixes and protocol changes without required client updates. It's the gateway to the entirety of the server's functionality.
The client API library uses the namespace zz in Javascript. To load this library, simply fetch /api-library.js from the API server.
- Environment Requirements
- Initialization
- Logging
- Waiting State
- Error Reporting
- Authentication
- Presence
- Notifications
- Creating Data Objects
- Modifying Data Objects
- The Data Read Layer
- Queries
The api library is served with its own dependencies, and should work out of the box in any browser environment. However, if it's being run in a different environment (say, Node.js), it won't function without a few dependencies. These are listed here.
-
JSON- Standard functionality (parseandstringify) -
console.log- Standard functionality - setTimeout - Standard functionality
- localStorage - This may just be a plain object; no methods are used, simply basic assignment and
delete -
XMLHttpRequestorXDomainRequest- An XHR-like object capable of cross-domain request
To initialize the library, call zz.init. This function may be passed a callback, which will be fired when the initialization is complete. Similar to jQuery's .ready() function, if zz.init is called after the initialization has completed, the callback will be fired immediately.
Logging can be configured on a fairly granular basis:
-
zz.logging.waiting- Logs waiting state changes (see Waiting State below); defaulttrue -
zz.logging.connection- Logs raw connection connect/disconnect; defaulttrue -
zz.logging.responses- Setting this to true causes responses to logged outgoing messages to be logged (i.e. when you're logging outgoingsubmessages, you'll also see the relevant responses to thosesubs); defaulttrue -
zz.logging.incoming.*- Enables logging for an incoming message type; all default tofalse -
zz.logging.outgoing.*- Enables logging for an outgoing message type; all default tofalse
When a message is sent, and no response is received within a timely fashion, zz will fire the waiting event. This will be fired once only, regardless of the number of tardy responses out in the air. Once these responses are all received, zz will fire the done event.
"A timely fashion" is defined by the zz.waitThreshold variable, which should be set to the desired number of milliseconds (by default, 1000).
Example timeline:
Message 1 sent
(500 ms pass)
zz.emit('waiting');
Message 2 sent
(500 ms pass)
Response 1 received
(100 ms pass)
Response 2 received
zz.emit('done')
The Hipsell server is capable of recording client errors for analysis and bugfixing. To push these errors to the server, use zz.recordError(err), and pass and object in. That err argument will be saved verbatim to the server for future use.
Clients can authenticate using the zz.auth(email, password, callback) function. The arguments are self explanatory, but the password argument itself may be either a raw password, or the hashed form of a password -- the function will detect which form the argument is, and handle it properly.
To deauthenticate a client, call zz.auth.deauth().
When authentication state changes, the change event will be fired on zz.auth.
Information on the current authentication state can be fetched via the zz.auth.curUser() function. This will return a Model-like object, containing four fields:
-
email- Authenticated user's email address -
password- Hashed form of the authenticated user's password -
_id- The ID of the authenticated user'sUsermodel -
name- The display name of the user. This field will automatically update when the authenticated user'sUsermodel is changed. -
avatar- A URL to the user's avatar image. This field will automatically update when the authenticated user'sUsermodel is changed.
This object will also fire events when the name and avatar fields receive updates. These events are named for the relevant field, e.g. zz.auth.curUser().on('name', function() { ... })) will listen for changes on the name field.
Authentication state will be preserved automatically between client sessions. Any callback on zz.init will not be fired until the authentication state is properly set up -- additionally, the ready state won't be fired on zz.auth for this initial authentication.
To changed the currently authenticated user's password, call zz.auth.changePassword(old, new, callback). This should be passed the current raw password for the user through old, and the desired new password through new. The callback is, as usual, optional. It should take a single parameter, err, that will be set to an Error object if an error occurred while attempting to perform the password change.
Presence information can be watched for both the current user and other users, and can be controlled for the current user. To subscribe to presence information for a user, listen on the zz.presence object. The event to listen on should be set to the id of the required user, or 'me' to listen on the current user's presence. For any value other than 'me', listening on presence will result in a server subscription, so these handlers should be used sparingly if possible, and cleaned up as soon as they're not needed.
Handlers for presence events take a single argument, status, that describes the user's presence status. It will be one of:
'online''away''offline'
Note: This is somewhat different from convention, but whenever a new listener is registered on a user (other than 'me'), the handler will be fired once with the user's presence state, regardless of whether a presence update was received or not.
The current user may control their presence status by making use of the zz.presence.online(), zz.presence.offline(), and zz.presence.away() functions. In addition to notifying the server of the user's presence status change, these functions can control the underlying server connection to optimize network usage. For example, calling zz.presence.offline() may result in the connection being shut down entirely until zz.presence.online() is called.
Notifications from the server are handled by listening on zz's notification event. Notification handlers should take two arguments -- the first being the notification message, and the second being the key of the relevant model.
Creating data is fairly straightforward, and simply involves calling the appropriate data creation function, defined as a member of zz.create. For example, if I wanted to create a listing, I'd use zz.create.listing(). The names of these functions are simply the relevant model names (see the bottom for a list of these) with the first letter lowercased.
These data creation functions take two arguments. The first is the data to create the object with, and the second is an optional callback that will be fired when the creation is finished.
If a callback is supplied, it will be called with a single argument, with the ID of the created object. If there was an error during creation (including a validation error), the callback will not be fired and an exception will be raised.
Example:
zz.create.convo({listing: 'listing/1'}, function(id) {
console.log('Created convo with id', id);
});
Note: The data passed to zz.create will not necessarily be the data that exists on the model itself. Do not make this assumption. The zz.create function is more like a constructor than a plain setData command.
Data modification happens through members of zz.update (for example, zz.update.listing). These update functions are similar to their equivalents in zz.create, but take three arguments rather than two. The first is the ID of the model to update, or alternatively an instance of that model (from which the _id field will be used). The second argument is an object containing the data to update with, and the third is an optional callback that will be fired when the update is complete. This callback will not be passed any arguments.
If an error occurs during the update process (whether validation or otherwise), and exception will be raised and the callback won't be fired.
Example:
zz.update.convo('convo/54321deadbeef', {listing: 'listing/3'}, function() {
console.log('Successfully updated convo');
});
Note: The data passed to zz.update will not necessarily be the data that exists on the model itself. Do not make this assumption.
The data read layer is the most complicated component in zz, due largely to the fact that it's designed to be used in a "live" fashion, with real-time data updates being pushed from the server.
Fetching individual models is done through the member functions of zz.data, similar to zz.update and zz.create. The names of these functions are simply the relevant model names (see the bottom for a list of these) with the first letter lowercased.
Each of these functions requires that the final argument passed by a callback function. When the fetch is complete, this callback will be fired with a single argument: null, if the data didn't exist, otherwise an instance of the appropriate model.
These fetcher functions have a more flexible interface than the equivalent zz.update and zz.create functions, and essentially operate in two modes.
The first of these is a simple fetch by id. For example:
TODO - the rest
Related models may be fetched from a Model instance by calling the appropriate related* method. For example, to fetch a model's related User models, I'd call m.relatedUsers, and to fetch related inquiries I'd call m.relatedInquiries. These method take an optional field argument (discussed below), as well as a required callback as the last argument.
When the related models have been fetched, the callback will be fired with a ModelList instance passed as the sole argument. This ModelList (described in detail below) is an Array-like object that will contain all the relevant Models. It's also capable of live updates (described below), just like Models themselves.
Just what these relevant models are depends on the arguments passed to the related* method. By default, the "query" used to fetch them is
All * models where [modelname] = [model._id]
So, if we wanted the following:
All Convo models where listing = listing._id
we'd do:
[l is a Listing]
l.relatedConvos(function(ml) { ... });
If the field name default isn't suitable, it can be overridden via the field argument. For example, if I wanted:
All Convo models where creator = user._id
we'd do:
[u is a User]
u.relatedConvos('creator', function(ml) { ... });
Driven by the need to not leak memory, our Models and ModelLists are a little weird.
On a high level, these objects can either be hot or frozen. A frozen model is a straightforward beast -- merely a collection of data at the time it was frozen. A hot model, on the other hand, will update automatically to reflect changes made by other clients, and allows events to fire on those changes. The .freeze() method can be used to make a hot object frozen, and will also release any and all event handlers registered on the object. Similarly, the .heat() method can be used to make a frozen object hot. Note that trying to .heat() an object that is already hot -- and similarly, trying to .freeze() an already frozen object -- will result in an exception being thrown.
By default, Models and ModelLists returned from the ORM are frozen, as a safety precaution.
The trick with hot objects is that they're reference counted, and if not disposed of properly will leak memory. Internally, the library maintains a subscription that will only be released when its referee count reaches zero; if even a single hot object remains unfrozen that subscription will remain active for the lifetime of the application. Additionally, any hot object will never be garbage collected even if it goes out of the application's scope, due to the library maintaining event handlers pointing to its methods.
In short, if not handled properly, hot objects will leak, causing network, memory, and processing congestion. As such it's crucial that they're released properly.
Follow these tips and you'll be ok:
-
DO NOT
.heat()a model unless the updating functionality is required - Whatever calls
.heat()is responsible for also calling.freeze()at the appropriate time - If something cannot guarantee that
.freeze()will be called, it should not.heat() - You can check the status of a model by reading the
.hotvariable - Don't put yourself in a situation where you may need to
.freeze()/.heat()more than once
All models inherit from zz.models.Model; all lists inherit from zz.models.ModelList, which itself inherits from Array. This is all set up via a prototype chain, so methods can be added/overwritten as needed.
zz.models.Model
|-- zz.models.Listing
|-- zz.models.Inquiry
|-- zz.models.Convo
|-- zz.models.User
|-- zz.models.Offer
|-- zz.models.Message
Array
|-- zz.models.ModelList
|-- zz.models.ListingList
|-- zz.models.InquiryList
|-- zz.models.ConvoList
|-- zz.models.UserList
|-- zz.models.OfferList
|-- zz.models.MessageList
Long story short, Models are designed such that the only fields available when iterating with .hasOwnProperty() are the actual model data fields. Any extra fields defined below won't interfere with that.
-
hot- Boolean indicating if theModelis hot or frozen. See above for details about what this means.
-
heat()- Makes the model hot if isn't already; see above -
freeze()- Freezes the object if it isn't already frozen; see above -
related*( ... )- See the section on relations above
As stated above, ModelList is a subclass of Array, and as such has all the same methods and properties as you'd expect.
-
hot- Boolean indicating if theModelListis hot or frozen. See above for details about what this mean. -
sorted- Boolean indicating if thisModelListis maintaining its sorted state. See documentation on thesortmethod below.
-
heat()- Makes theModelListhot if it isn't already; see above -
freeze()- Freezes the object if it isn't already frozen; see above -
sort(comparator)- Sorts theModelList. Note thatcomparator, unlike the native sort, is required. Other than that this behaves exactly the same as Javascript's built-in sort method, but has the additional property of maintaining sorted order asModels are added or removed from hotModelListss. This does incur a performance hit as elements must be inserted in order, sounsort()should be used it sorted order is no longer required. Finally, a caveat: becauseModelLists areArrays, it is possible for user code to insert/remove elements. If this is done on a sortedModelListit will break sorted order. -
unsort()- Stops sorting on hot updates
The query mechanism exists to allow more complicated searches to be performed than allowed by the basic related mechanism. Queries are defined on the server, and accessed by name through zz.
A query(name) function is provided for each data type, accessed via zz.data.[type].query. This query function takes a single optional parameter that is the name of the query to run; if the name is not specified, no query is performed, but limit, offset, and sort still operate.
If query is passed a function as the second parameter, it will execute immediately. Otherwise, it will return a Query object, described below. This object can be used to set additional options on the query, and to run the query and await results.
Note: Running a nonexistent query will not result in an error being thrown; instead, the callback will simply be called with an empty list.
The only public properties on the Query object are methods. Each one of these methods, if called with a function as the last argument, will run the query and return undefined. Otherwise, every method will return the Query itself, so options can be chained.
The callback function takes a single argument, ids, which will be an array of object ids returned by the query.
-
offset(n, [callback])- Sets the offset. -
limit(n, [callback])- Sets the maximum number of ids to fetch. This defaults to, and cannot be greater than, 100. -
sort(s, [callback])- Sets the sort field. This may be prefixed with a+or-to specify ascending or descending (defaults to descending) -
params(d, [callback])- Sets the parameters. This should be a flat object mapping query parameters to values. Note that this will only take strings or integers as values. Anything else will cause an error. -
run([callback])- Runs the query. Ifcallbackisn't set, this method does nothing.
Note: The above methods, save .run(), will override the effect of the previous call if executed more than once. For example, calling .limit(10).limit(5) will result in a limit of 5.
zz.data.listing.query().limit(10).sort('+created', function(ids) { ... });
zz.data.listing.query('foobar', function(ids) { ... });
zz.data.listing.query('foobar').offset(20).run(function(ids) { ... });