• log out

Designing your own stream types

Streams can be thought of as a "base class" that implements quite a bit of functionaity for a piece of data. You may want to create new types of streams (such as Chess/game) and new types of messages (such as Chess/move) when you develop new apps and plugins. First, you would name your stream type with a namespace, such as "Chess/game". You would enter it in your app.json or plugin.json file:

{    
  "Streams": {
    "types": {
      "Chess/game": {
        /* a client is able to create
           streams of this type by via a
           POST to Streams/stream
		   and can set content and title
		   attributes */
        "create": ["content", "title"],
		
        /* a client is able to edit
           streams of this type via a
           PUT to Streams/stream,
		   but can only change the
		   content and title attributes */
        "edit": ["content", "title"],
		
        /* default field values */
        "defaults": {
          "title": "Untitled Chess Game",
		  "icon": "Chess/game",
		  "readLevel": 40
        },
		
        /* information about messages */
        "messages": {
          "Chess/move": {
            /* a client is able to post
               messags of this type via
               POST to Streams/message */
            "edit": true,
			 
            /* describes what this is */
            "description": "A chess move",
			
            /* email subject for notif. */
            "subject": "A move was made"
          }
        }
      }
    }
  }
}

Normally, the icon, title, content fields, and ability to store custom attributes, should be enough for most purposes. The field title can hold up to 255 characters, content can hold up to 1023, and attributes can hold JSON up to 1023 characters. Sometimes, though, you need to store larger data, in which case you might want an auxiliary database table. You'd make an update script for your app or plugin, with SQL something like this:

// SQL to create table:
CREATE TABLE `chess_game` (
  `publisherId` varchar(31),
  `streamName` varchar(255),
  `userId` varchar(31) NOT NULL,
  `moves` text NOT NULL,
  PRIMARY KEY (`publisherId`,`streamName`)
);

After running scripts/Q/models to autogenerate classes for the database model, you'd add an additional field into the config:

{
  "Streams": {
    "types": {
      "Chess/game": {
        /* Supply the name of a
           PHP class representing
           the table which extends
           Chess/game streams */
        "extend": ["Chess_Game"]
	  }
	}
  }
}	

Behind the scenes, every time Streams::fetch( ) gets a "Chess/game" stream from the database, it also fetches a corresponding row from the chess_game table. Given a $stream object, you'd simply access the corresponding Chess_Game object as $stream->Chess_Game. Extended fields are assigned and managed just as the core fields like "icon" and "title" are:

// fetch a chess game
$stream = Streams::fetchOne(null, $pubId, $name);
$stream->moves .= "\ne2-e4";
$stream->save(); // updates Chess_Game table
$stream->post(array(
  'type' => 'Chess/move',
  'content' => '',
  'instructions' => array(
    'move' => 'e2-e4',
    'color' => 'white'
  )
));
// get associated Chess_Game object
$game = $stream->get('Chess_Game');
$game->resign(); // call some method on it
// or vice versa:
$stream = Streams_Stream::extendedBy($game);

Client-side constructors

On the client side, you would want to define a Javascript constructor and handlers for this stream's messages, similar to how you do for tools:

Q.Streams.define("Chess/game",
function (fields) {
  // set some extended fields
  this.fields.moves = fields.moves;
  
  // DO NOT add events to the stream:
  this.onSomething = new Q.Event();
  // ^^ do not do this, see below
  
  // let's subscribe to some messages
  this.onMessage('Chess/move').set(
  function (stream, message) {
    // The stream variable here
	// refers to the cached local
	// representation of the stream,
	// whose state we'll now update:
    stream.moves += 
	  ((message.color === 'white')
	  ? "\n" : "\t")
	  + message.move;
  }, 'Chess/game');
});

The code above illustrates a fundamental convention for implementing new types of streams: when a new message is posted, the local representation of the stream should be updated to match what it would be if the stream was refreshed. Said another way, part of handling messages client-side is "applying diff patches" to the state of the stream they are posted on.

Your new stream type is ready to be used alongside other streams by both the client and the server.

When defining new stream types, do not add events like you would to tools. That's because, when streams are refreshed, entirely new stream objects might be created and replaced in the cache.

Adding more hooks

If the above functionality is not enough for you for some reason, you can also add hooks to extend the retrieving and saving of the streams, posting of messages, etc.

{"Q": {"handlersBeforeEvent":

/* before saving a Chess/game stream */
"Streams/Stream/save/Chess/game":
["Chess/before/Streams_Stream_save_Chess_game"],

/* before posting in Chess/game streams: */
"Streams/post/Chess/game":
["Chess/before/Streams_post_Chess_game"]

/* before posting Chess/move messages: */
"Streams/message/Chess/move":
["Chess/before/Streams_Stream_save_Chess_game"]

}, "handlersAfterEvent": {

/* after fetching Chess/game streams: */
"Streams/Stream/fetch/Chess/game":
[Chess/after/Streams_Stream_fetch_Chess_game]

/* after posting in Chess/game streams: */
"Streams/post/Chess/game":
["Chess/after/Streams_post_Chess_game"]

/* after posting Chess/move messages: */
"Streams/message/Chess/move":
["Chess/after/Streams_Stream_save_Chess_game"]

}}

and implement them, something like this:

function
Chess_before_Streams_Stream_save_Chess_game
($params)
{
  $stream = $params['stream'];
  $retrieved = $stream->wasRetrieved();
  
  if ($retrieved
  and !$stream->wasModified('foo')) {
    return;
  }
  
  // do stuff here
}

function
Chess_after_Streams_Stream_fetch_Chess_game
($params, &$streams)
{
  if (!$params['retrieved']) {
    // all the streams were already cached
    return;
  }
  
  // do stuff here
}