Skip to content

Client API Library

shz edited this page Aug 8, 2011 · 12 revisions

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.

Contents

Environment Requirements

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 (parse and stringify)
  • console.log - Standard functionality
  • setTimeout - Standard functionality
  • localStorage - This may just be a plain object; no methods are used, simply basic assignment and delete
  • XMLHttpRequest or XDomainRequest - An XHR-like object capable of cross-domain request

Initialization

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

Logging can be configured on a fairly granular basis:

  • zz.logging.waiting - Logs waiting state changes (see Waiting State below); default true
  • zz.logging.connection - Logs raw connection connect/disconnect; default true
  • zz.logging.responses - Setting this to true causes responses to logged outgoing messages to be logged (i.e. when you're logging outgoing sub messages, you'll also see the relevant responses to those subs); default true
  • zz.logging.incoming.* - Enables logging for an incoming message type; all default to false
  • zz.logging.outgoing.* - Enables logging for an outgoing message type; all default to false

Waiting State

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')

Error Reporting

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.

Authentication

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's User model
  • name - The display name of the user. This field will automatically update when the authenticated user's User model is changed.
  • avatar - A URL to the user's avatar image. This field will automatically update when the authenticated user's User model 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

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

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 Objects

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.

Modifying Data Objects

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

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 Models

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

Getting Related Models

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) { ... });

Handling Live Data

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 .hot variable
  • Don't put yourself in a situation where you may need to .freeze()/.heat() more than once

Data Class Hierarchy

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

Model Documentation

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.

Extra Fields

  • hot - Boolean indicating if the Model is hot or frozen. See above for details about what this means.

Methods

  • 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

ModelList Documentation

As stated above, ModelList is a subclass of Array, and as such has all the same methods and properties as you'd expect.

Extra Fields

  • hot - Boolean indicating if the ModelList is hot or frozen. See above for details about what this mean.
  • sorted - Boolean indicating if this ModelList is maintaining its sorted state. See documentation on the sort method below.

Methods

  • heat() - Makes the ModelList hot if it isn't already; see above
  • freeze() - Freezes the object if it isn't already frozen; see above
  • sort(comparator) - Sorts the ModelList. Note that comparator, 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 as Models are added or removed from hot ModelListss. This does incur a performance hit as elements must be inserted in order, so unsort() should be used it sorted order is no longer required. Finally, a caveat: because ModelLists are Arrays, it is possible for user code to insert/remove elements. If this is done on a sorted ModelList it will break sorted order.
  • unsort() - Stops sorting on hot updates

Queries

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 Query Object

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. If callback isn'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.

Examples

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) { ... });

Clone this wiki locally