Skip to content

Adstream.data api

MaxMotovilov edited this page Nov 29, 2012 · 43 revisions

adstream.data API: Classes and Functions

Namespace adstream.data

dojo.require( 'adstream.data.Service' );

connect()

var root = adstream.data.connect( endpoint_url, options, schema );

The connect() function returns the root of the tree-like data structure corresponding to the URL layout. A service object encapsulating the data and functionality related to the network protocol is also created and can be accessed with the service() method on any of the data objects within the cached hierarchy including the root object.

The optional argument options can be used to pass in a dictionary with miscellaneous parameters affecting the operation of the network transport. Currently the following options are supported:

  • acceptContentTypes: an array of MIME types acceptable as a server response. The default includes application/json, text/javascript and other, more obscure, alternatives and probably does not need to be overridden;
  • rejectContentTypes: if present, instructs the transport to accept all types except those in this list; the HTTP Accept: header of the request is still populated from the list in acceptContentTypes.

Argument options can be omitted entirely.

Service

The Service object is normally created by the adstream.data.connect() and its instance can be obtained by calling the service() method on any of the data objects.

root

A property pointing to the root object of the cache.

catchAll()

service.catchAll( callback );

Sets a global (for the service) error handler which will be called for all errors in communication with the service or interpretation of the received data. The callback function receives a single argument: the exception object thrown at the point of failure.

The exceptions passed into the catch-all handler as well as into the error handlers associated with individual requests will contain the following properties provided that the HTTP request has completed rather than aborted or timed out:

  • status: HTTP status returned by the server;
  • responseText: response body as a text string;
  • responseHeaders: a dictionary with all response headers, where keys have capitalized first letters in each word (e.g. “Content-Type”) and the values are either strings (single instance of a header) or arrays of strings (multiple instances);
  • ioargs: Dojo ioargs dictionary that includes various transport-level parameters as well as the xhr object used to execute the request.

push()

service.push( data, rel_url );

Processes a server-initiated update received by means external to the data service layer (presumably, a push-mode transport channel). Argument data should contain a well-formed deserialized server response packet; the optional argument rel_url can be used to simulate response to the request to a specific URL within the service.

pause()

service.pause( everything );

Pauses the stream of GET requests generated by the watch instances with automatic refresh until resume() is called. If true is passed in place of the optional argument everything, this method also cancels all active GET requests and prevents any new explicit GET requests from executing while the service is paused. All cancelled requests as well as all blocked explicit requests are accumulated by the service object so that they may be restarted by a call to resume().

resume()

service.resume( discard_pending );

Resumes normal execution of GET requests including the automatic refresh processing. Unless true is passed in place of the optional argument discard_pending, this method also immediately restarts the GET requests that have been cancelled by the prior call to pause(true) or blocked from execution by same.

Namespace adstream.data.schema

dojo.require( 'adstream.data.schema' );

The schema passed to the connect() function describes the tree of data objects in the cache as well as the tree of the URL space associated with the service. It consists of objects that will serve as prototypes for the instances that the framework creates automatically. For example:

var root = adstream.data.connect(
  '/myservice',
  new adstream.data.schema.Node( {
    books: new adstream.data.schema.Container( {
      item: new adstream.data.schema.Object(),
      view: { offset:0, count: 30 }
    } ),
    authors: new adstream.data.schema.Container( {
      item: new adstream.data.schema.Object(),
      view: { offset:0 }
    } )
  } )
);

connects to a service providing two containers /books and /authors at its root, each of which contains data objects without any further URL structure.

Node

Represents a placeholder object/URL: the service provides no data or functionality associated with a node. Node serves only to structure the URL space (and the client-side representation). Typically the root of the tree would be the one and only Node in the schema.

new Node( subschema, options, prototype )

Constructor of the Node accepts up to three dictionaries (object literals):

  • Subschema: each property corresponds to a nested node, object or container. The name of the property serves as the URL component.
  • Options: a provision to pass options to schema entities derived from the Node. One option applies to all container items, including Nodeitself:
    • deleteViaParent: a true-like value which, when set, prevents the library from issuing HTTP DELETEs on this URL, using HTTP PUT (batch deletion) on its container instead.
  • Prototype: content of this dictionary is injected into the prototype of the schema entity making it possible to implement client-side methods working with data content supplied by the server.

service()

Returns the service object associated with the cache.

url()

var url = obj.url();

Returns the endpoint-relative URL of the object.

id()

var id = obj.id();

Returns the last part of the endpoint-relative URL of the object which corresponds to the item identifier for immediate children of a Container node.

get()

dojo.when( obj.get( relative_url, depth, force_request ), on_success, on_error );

Retrieves a child object of the node or the node itself, with its representation loaded. Returns the value immediately if available in cache or a dojo.Deferred “promise” that can be used to asynchronously call a callback function using dojo.when(). Optional argument depth can be used to also retrieve the representation of object’s properties or contained items down to specific nesting depth in the same request. When not specified, depth defaults to 0 for nodes and objects and to 1 for containers.

The optional argument force_request can be used to request data from the server and update the cached copy of the object even when it appears valid and up-to-date.

del()

dojo.when( obj.del(), on_success, on_error );

Deletes the object from the server-side data model and from the client cache after receiving a confirmation. The on_success callback is called without an argument. No request to the server is generated and the callback is called synchronously when the object being deleted is a newly created item (i.e. it has never been submitted to the service).

This method is only present in objects and nodes that are items in a container according to the schema.

watch()

obj.watch( callback, relative_url, options );

Associates a callback function with a node or a trimmed subtree. The callback will be called every time the specified node and/or its children are modified as a result of synchronizing with the server. Argument options is a dictionary that may contain the following properties:

  • maxDepth: limits the depth of the watched subtree, beginning at the specified node. The default value for maxDepth is 0 for objects and 1 for containers (same as the default depth in get());
  • minDepth: excludes top levels of the subtree from the watched set. Defaults to 0 (exclude nothing);
  • refreshRate: requests that the watched subtree (up to specified maxDepth) should be refreshed automatically by issuing an appropriate get() request after specified number of milliseconds has passed since its last update. By default there is no automatic refresh. To minimize the demands on browser resources the refresh logic is very conservative: large number of refresh requests and/or slow server responses may result in significantly lower refresh rates than specified.

Deletion or creation of an item in the container (i.e. detected change in the currently tracked set of items — see filter() and view()) triggers the callbacks for this container. Change in the version metadata property on an Object (or change in a read-only object’s data representation) triggers the callbacks for this object.

All arguments of watch() except callback are optional.

The callback function is called on the node determined by the relative_url passed into watch(), even when the alert was actually triggered by a change in one of its descendants (subject to minDepth and maxDepth settings). It receives two arguments: present state of the node (after update) and the prior state of the node (before update). The latter is a temporary object that will not be retained by the library after callback activation and should not be modified or stored; it does however have a complete interface of the corresponding node and can be used to examine the state as it was before the update. If the node in question is a container, the second argument may also have up to two additional properties defined:

  • created: an array containing IDs of items that have been created in the container as a result of the update;
  • deleted: an array containing IDs of items that have been deleted from the container as a result of the update.

Neither of these properties will contain an empty array: the property is just omitted from the object entirely if no corresponding IDs are present. Also note that renaming the newly created items from @N in the server-side confirmation is treated as a pair of changes: one creation and one deletion.

ignore()

obj.ignore( relative_url );

Removes all watcher callbacks from an object, or one of its descendants if the optional argument relative_url is specified.

save()

dojo.when( obj.save( depth ), on_success, on_error );

Submits modifications to the internal state of the object and its properties down to the level specified by the optional argumentdepth, which defaults to 0, to the server. Always returns a promise object that will pass the object to the on_success callback after the update has been completed and the object re-synchronized. Nodes and read-only objects do not participate in the update themselves but provide this method as a shortcut to saving all or part of their read-write properties.

Containers also provide this method and participate in updates, but do not honor the depth argument (see below for details). The container-specific update logic also affects saving of newly created items (see Container.save() below).

Object

Represents a data object with internal representation, accessible as object’s properties in the cache, that can be obtained from the service and updated. May contain other nodes, objects or containers that will also be accessible as parent object’s properties in the cache but are maintained as cached object instances in their own right.

new Object( subschema, options, prototype )

Same as the Node’s constructor. Object-specific options are:

  • readOnly: a true-like value, indicates that the object’s internal representation should never be sent back to the server as part of an update and that changes in the object will be detected by the data access layer based on its data content rather than the version metadata field.

Instances of Object inherit all of the Node methods.

Container

Represents a container of items (nodes, objects or containers), cached on the client either completely or partially. Every item has an identical schema, determined from the argument passed to the Container’s constructor. Items are accessible as properties of the container in the cache. Containers have no associated internal representation; the service may optionally associate additional data with the container (such as total number of items) by including it in the extra metadata object.

The ‘subschema’ argument is a dictionary containing at least one named property, item. The value of item determines the schema of contained items. Other named properties of the argument specify the default metadata values associated with the container and are optional:

  • view: specifies metadata values associated with paging
  • filter: specifies metadata values associated with filtering (or searching)
  • extra: specifies additional metadata associated with the container

These values may be replaced with the ones returned by the service or overridden by the user (except extra, which is read-only). Note that sub-properties of view and filter become URL parameters in the requests issued to the service.

Container options:

  • readOnly: analogous to Object — the container will not participate in save();
  • saveWithParent: normally, deep save() does not propagate into containers because of the difference in their semantics (POST vs. PUT). This flag will override the default behavior, making the container submit its new items (if any) along with parent object’s changes. Note that the server-side implementation should be capable of detecting POST-like subpackets (newly created container items) in the PUT request directed to the parent object and processing them correctly (i.e. creating items and returning them in the response with replaces metadata field set) in order to support this functionality;
  • saveAllItems: overrides the default save() behavior of the Container making it always send back all of its items. Note that calling save() on the container itself will always result in a POST, even if no new items are present. This flag mixes PUT and POST semantics by including PUT-like data into a POST request and the server-side implementation should be capable of supporting this.

In addition to contained items, instances of Container may also have associated metadata exposed via the view(), filter() and extra() methods. The metadata is organized into dictionaries (view, filter and extra, respectively) that may contain scalar-valued properties. The values of the metadata properties may come from the server as part of the object representation or from the schema (defaults). The read-write metadata dictionaries — view and filter — also allow user to modify their properties or reset them to default values; their properties are also submitted to the server as part of the GET requests involving the container. While the data access layer does not use any of the metadata internally, leaving the interpretation of all properties up to the service and the user, care must be exercised when requesting multiple containers in a deep get() request as the metadata property names are not qualified in any way when added to the URL string as parameters and a conflict will result in a failure to initiate the request.

create()

var temp = obj.create();
// Modify internal state of temp in some way
dojo.when( temp.save(), on_success, on_error );

Synchronously creates and returns a new item of the type stored in the container. The object is immediately placed into the container as an item with a synthetic name starting with the '@' character — service implementations must avoid item names beginning with '@' to prevent clashes.

When save() is called on a newly created item, it is submitted in a POST request to the service with all of its properties, regardless of the depth argument. Calling save() on a child object of a newly created item is an error.

del()

dojo.when( obj.del(), on_success, on_error );

This method call works the same as the one provided by the Node — that is, deletes the Container instance it is invoked upon — and is only available on container instances that are themselves items of another container.

dojo.when( obj.del( what ), on_success, on_error );

This method call deletes one or more items from the container, depending on the value of the argument what:

  1. If what is a scalar value (normally a string or a number) then it is expected to be an ID of the item in the container. If the item in question is newly created, it is deleted locally and the method returns null, otherwise a DELETE request on the item is submitted to the backend service.
  2. If what is an array of scalar values (that is, item IDs) then all of those items will be deleted in a single request which will be a DELETE request on a single item if there is only one or a PUT request on the container is two or more items are to be deleted. Note that newly created items are still deleted locally and the method returns null if it doesn’t find a single pre-existing item among the ones specified by what.
  3. If what is a function then it will be called for each item currently in the container and the items for which it returns a truthy value will be processed as in the previous case.

save()

dojo.when( obj.save( what, depth ), on_success, on_error );

The save() method on containers can be used to submit newly created or changed items to the backend service either individually or in bulk depending on the value of the argument what:

  1. If save() is called without arguments, all newly created items are submitted in a single POST request. If saveAllItems was specified among container’s options at creation time, all pre-existing items will also be submitted in this request. Note that it will still be a POST rather than a PUT request as in cases (3) and (4) below.
  2. If what is a scalar value (normally a string or a number) then it is expected to be an ID of the item in the container and the call to container.save(what,depth) is fully equivalent to container[what].save(depth) where the depth argument is optional.
  3. If what is an array of scalar values (that is, item IDs) then all of those items will be saved in a single request. Depending on the exact mix of items, the request may end up being a POST on the container (one or more newly created items alone), a PUT on the item (a single modified item by itself) or a PUT on the container (a mix of newly created and modified items). The optional depth applies only to modified items.
  4. If what is a function then it will be called for each item currently in the container and the items for which it returns a truthy value will be processed as in the previous case.

Note that when a container is saved as part of a deep save initiated at one of its parents, it behaves as follows:

  • If the save() was called on a parent container, and the container instance in question is, therefore, a newly created item or a part thereof, it will save all of its newly created items, if any;
  • Otherwise the container will not save anything.

In either case the depth passed into the original call does not influence the actions of the container. This is the default behavior which can be overridden using the schema options saveWithParent and saveAllItems described above.

view()

Manages the metadata associated with the current view of a container: that is, the range of items that are being preserved in cache and synchronized with the server. Note that this association is a matter of convention only: nothing in the code of the data access layer uses the information in this metadata dictionary or imposes any requirements on its property names or values. Containers do, however, reset their content completely when properties in view returned by the service change from their prior state.

var view = obj.view();

Synchronously retrieves current view metadata. Will return an empty object if the view is not specified in the schema and no settings to view have been previously made. Please note that the object thus returned may be modified directly by changing its properties without affecting the defaults stored in the schema.

obj.view( view_settings );

Modifies current view dictionary using the value provided by the user:

  • If view_settings is null, then reset to default values;
  • Otherwise view_settings should be a dictionary whose properties will be interpreted as follows:
    • If property value is null, reset its value to default or remove completely if not in the schema;
    • Otherwise set the property of the view to specified value.

This method call returns the resulting view metadata. A change to view metadata invalidates the cached copy of the container (the next call to get() will not complete synchronously). The user would usually execute the following sequence to effect a change in a container:

obj.filter( filter_settings );
obj.view( view_settings );
dojo.when( obj.get(), on_success, on_error );

Note that this way of effecting the view settings is not substantially different from directly modifying the object returned by a call to view() without parameters.

Suggested content of the view metadata dictionary is:

  • offset: zero-based offset from the beginning of the list in current sorting order, as determined by the nature of the container or currently specified filter;
  • count: number of items in the view, which should never be greater than what is requested by the user and smaller only if the end of the list is reached.

filter()

Manages the metadata that is, by convention, associated with the current filter or search pattern on a container. Fully identical to the view() method otherwise.

extra()

var extra = obj.extra();

Returns the extra metadata dictionary, which should not be modified by the user. The service would normally use this metadata section to pass back any data of general interest to the users of the container, e.g. the total number of items in a container.

Connector

Connector node is a provision for linked backend services that appear to the client as separate service endpoints but in reality operate with the same data and affect each other’s object space. Where a modification (POST, PUT, DELETE) request to one of the linked services may need to update the client-side objects belonging to the other service, it may do so by directing the other service’s part of the response packet to a Connector object which will, in turn, forward it to the rightful destination using the push() method on the associated adstream.data.Service object.

new Connector()

Constructor of the Connector object has no arguments; these objects also do not provide get(), save(), watch() or ignore() methods.

connect()

 connector.connect( node, rel_path );

This method should be called on instances of Connector to establish response forwarding to a specific linked service (determined from node.service()) and relative path for the response within the URL space of the linked service (a combination of node.url() and the optional argument rel_path).

Namespace adstream.data — Extras

Additional functional units also exposed via the namespace adstream.data are available from separate modules:

Watcher

dojo.require( 'adstream.data.Watcher' );

The Watcher mixin can be used to simplify the setup and cleanup of watcher callbacks within widgets (codebehind objects) that may be repeatedly created and destroyed during the lifetime of the application. The following example assumes that data field of the myApp.myWidget instance is connected to the appropriate data object as part of template instantiation:

dojo.declare( 'myApp.myWidget', [ dijit._Widget, adstream.data.Watcher ], {
  
   data: null,

   startup: function() {
      this.watch( 'onDataChanged', data );
   },

   onDataChanged: function() {
      // Do something
   }
} );

When the instance of myApp.myWidget is being overwritten and destroyed, the watcher it established during initialization will be automatically disconnected.

watch()

  this.watch( method, data_object, relative_url, options );

This method connects the watcher callback method which could be either name or actual function to the specified sub-object(s) of the data_object. Arguments relative_url and options are optional and are interpreted identically to the corresponding arguments of the adstream.data.schema.Node.watch() method. The method callback will be called with the same scope argument (this) as the original call to watch().

ignore()

  this.ignore( data_object, relative_url );

Disconnects the watcher from the data object explicitly in the same way as data_object.ignore( relative_url ) would. Note that explicit disconnection is provided for convenience and is generally not required as the watchers will be automatically disconnected during the destruction sequence of the object inheriting from adstream.data.Watcher.

Limitations of the current version

  1. No two instances of adstream.data.Watcher should connect to the same data URL (after the relative_url is taken into account) within the same service. A single instance may connect multiple watchers to the same URL within the same service without a problem.
  2. A single instance of adstream.data.Watcher should not connect to the same data URL on two different services.

Extensions

Extensions are a form of dynamic mixins that can be used to modify the behavior of adstream.date node objects. An extension is instantiated separately from the node it is intended to modify, then it can be installed on the node object, overriding some of its methods. When no longer needed, the extension can be uninstalled, restoring the original functionality.

At the moment, installing and uninstalling multiple extensions in arbitrary order on the same node may lead to unspecified behavior. It is recommended to use no more than a single extension object per node or, if absolutely necessary, install and uninstall multiple extension in a strict last-in-first-out manner.

install()

  extension_object.install( data_object );

Modifies data_object by installing overrides provided by the extension_object. A single extension can be associated with at most one adstream.data node.

uninstall()

  extension_object.uninstall();

Disassociates extension_object from the adstream.data node and restores the methods of the latter to their original state.

extensions.IncrementalContainer

dojo.require( 'adstream.data.extensions.IncrementalContainer' );

The IncrementalContainer is an extension that lets an adstream.data.schema.Container object accumulate consecutively received pages of items without resetting the content every time a new page is received. The extension keeps track of the view and filter metadata objects and preserves the old content only if pages are truly consecutive: the new page fits either directly in front or directly behind the existing content in the current sorting order and the filter settings do not change.

var extension_object = new adstream.data.extensions.IncrementalContainer( overrides );
extension_object.install( container_object );

The constructor argument is a dictionary which could override any of the following methods:

extendView()

overrides.extendView = function( old, add ){...}

This method takes two instances of the view metadata objects: old reflects the state of the container before the update and add contains the metadata received from the server. The method determines if current view can indeed be extended with the data just received and, in case of success, directly modifies the old object and returns true; otherwise it leaves old unchanged and returns false.

The default implementation of this method uses (and modifies) properties offset and count of the view metadata object; it also ensures that if the metadata object contains properties in addition to these two, their values do not change with the update.

compatibleFilter()

overrides.compatibleFilter = function( old, updated ){...}

This method takes two instances of the filter metadata objects: old reflects the state of the container before the update and updated contains the metadata received from the server. The method determines if the two states of the filter are compatible in the sense that replacing old with updated need not reset the container’s content and returns true or false accordingly.

The default implementation of this method returns true if and only if updated does not contain any new or changed properties compared to old: it may contain fewer properties and still satisfy the condition.