Services and Flows

Introduction

Services are script entry points that one process may invoke on another. Often the cooperating processes are the Client and its associated User process however in the Chat example User processes in the server invoke services on each other to send chat messages and channel invitations.

In this section we discuss the services we need to implement the chat system and their invocation flows.

Login() - The Server Login Service

It is appropriate at this point to mention Inq's package structure and its relation to client login. Like other languages, Inq uses the package directive at the head of a parse module to define the package in which unqualified references to functions, services and typedefs will be resolved. A module's package defines the namespace in which those functions, services and typedefs it defines reside. For Chat we have no need to sub-divide the application so we put everything into the package examples.chat.

In addition, when a client logs into a server it specifies the package it wants to login to. Inq's login window looks like this:

Login Window

Inq expects to find the service examples.chat:Login. If it does not an exception is thrown and login is unsuccessful. The Login service is passed the user name and encrypted password and performs authentication of these credentials.

Note
Inq will include an authorisation and privilege package, yet to be released. Full details of the login protocol will be covered there.

The Login service must call either of the predefined functions system:LoginOK() or system:LoginDenied(). For Chat we don't do anything other than call system:LoginOK() as shown:

/**
 * The Login service for the Chat package is run
 * when the client logs in as this package.
 * For demo purposes it doesn't do anything other
 * than respond with the client source.
 */
service Login(string loginName, string passwd)
{
  call system:LoginOK(url="chat.inq");
}

The url argument is required and tells the Inq server the source required to start the client. If relative, as here, it is resolved with respect to the current module. The url is resolved and read on the server, so the client does not require (and likely should not have) access to this url.

Inq Client Initialisation

The Inq script is returned to and parsed in the client. As well as defining functions and services, it will necessarily contain a fragment that, being outside any such definition, is executed immediately to show the initial GUI. Our example includes this script at the end of chat.inq


if ($process.package == "\p")
{
  // When running as the chat package itself, create a top-level window
  // and create the GUI within it.
  // Alternatively, the chatWindow function can be called by other
  // applications providing a container for the GUI.
  gWindow chat;
  call chatWindow(chat);

  // Load utility dialogs
  exec("classpath:///inq/gui/questiondialog.inq");
  exec("classpath:///inq/gui/messagedialog.inq");
  exec("classpath:///inq/gui/textPrompt.inq");

  // Create the utility dialogs for later use
  call inq.gui.util:createMessageDialog(parent = chat);
  call inq.gui.util:createTextPromptDialog(parent = chat);
  call inq.gui.util:createQuestionDialog(parent = chat);

  // Pop up the window
  show (chat);
}

Once logged in, the login package is available at $process.package and given that the string constant \p expands to the package of the current parse module, we can make this fragment conditional on having logged in as examples.chat. This allows the chat utility to be used stand-alone or integrated into larger applications having their own startup fragments that can just call the public function examples.chat:chatWindow() providing a suitable GUI parent.

After login, the initial client script runs at the context $root and chatWindow() is called to build the initial GUI. It creates the menus and a tab pane to hold each conversation and sets up its MVC and property bindings as discussed in GUI Basics. In client-server applications it is common to want to invoke a service at this time, for example to fetch initial model data. However, the client-side context is still being built, so Inq provides a component event when this part of initialisation is complete. The chatWindow() function contains this gEvent usage:

// As the layout is performed and the GUI node structure added to
// the node space, contextEstablished() is called with $this as $root.chat
gEvent(channels, call contextEstablished(), event=(gContext));

The event handler function for the gContext event type conventionally has the name contextEstablished() and is called when the component (the tab pane channels in this example) is

  1. placed into the Inq hierarchy beneath a component whose contextNode property is set to true;
  2. the GUI context is placed into the process node space.

In chatWindow() the following lines perform step 1:

// The chat top-level GUI container is a context node. It becomes $this
// in any component event callbacks. See gContext event handler below.
chat.properties.contextNode = true;
  .
  .
  .
// Layout the window - only the tab to add
layout(., chat, "channels");

The event handler was placed on the channels component although we could have used any of those created and laid-out by chatWindow(), because they all end up residing as children of chat. Accomplishing step 2, the chat node itself is placed into the process node space by the line:

// $this is currently $root. Add the chat window to the node space
any $this.chat = chat;

initChat() - Signing into Chat

Having established a context of $root.chat the client invokes the initChat service in the server. Referring to chatserv.inq this service performs the following:

  1. calls the local function deriveChatName() to create a suitable value for OnLine.OnLine. If a user logs into the chat system more than once a new presence is created by suffixing the login name with _1, _2 and so on.
  2. calls the local function register() passing such arguments so that the stack contains the necessary values for the OnLine construct expression.

The declaration for register() is:

local function register(OnLine, OnLine.LoginName)

The arguments are declared pass-by-value using the type reference form, including the shorthand syntax for OnLine.OnLine. The next sections explain what the register() function is doing.

Creating the OnLine Instance

The combination of new() and create() respectively makes a new prototype OnLine instance and submits it for creation as a managed instance.

transaction
{
  create(new(OnLine));
}

The construct expression (defined with the typedef in OnLine.inq) executes to initialise the prototype. This process involves copying from the stack, which for managed instance prototypes means assigning the matching fields OnLine and LoginName, passed as arguments to register().

We don't initialise the Context field in this way so as things stand the constructor always sets it to the current context path, returned by $path. When sending service requests to another chat user's process, as we see shortly we use the @context argument in the send invocation. You may realise at this point that everyone's context is the same and will be $root.chat. This is certainly true if all users logged in to chat as its native package examples.chat however this may not be the case if chat has been integrated into a larger application. That application may well establish a different context of, say, $root.myLargeApp.chat. By storing the context in the user's OnLine instance we don't need to know what it is when invoking services in another's User process.

Lastly, the ProcessId field is initialised. In Inq, the only way to access another process is via its id property. The process can be obtained using getprocess(id).

Setting Up Event-Live Lists

We would like to maintain lists of the users logged into chat and the public chat rooms available, so we can select them in the client GUI. To do this we use a technique that creates a list in our server-side node space, returns it to the client and keeps it up to date as items within it are created and destroyed. We have two similar lists in this case, so a helper function readChatWithList is scripted.

// Set up two lists in our node space. The publicList is the available
// public chat rooms, that is the instances of ChatChannel whose IsPublic
// field is true. The privateList is all the registered users, that is
// all the OnLine instances.
call readChatWithList(IsPublic=true, listAt=path($this.vars.publicList));
call readChatWithList(IsPublic=false, listAt=path($this.vars.privateList));

This function follows the pattern for building an event-live node set described in Building Node Structures. Event-live structures propagate events arising within them upwards through the node space to $root, discussed for update events in Event Flows Between Processes. This is true also when such a structure is added to the node space. In readChatWithList() this line is important:

add(remove(nodeSet.list), listAt);

This places the structure rooted at list and based on the seeded map type nodeSet at the given path. The same result would be achieved with the line

any {listAt} = remove(nodeSet.list);

Except that, unlike an anonymous declaration, the add function generates an event on the root of an event-live structure. This event propagates to the peer client carrying the node set and its path, so it can be similarly placed in the client node space. In this pattern, structures are created and maintained in the server while being automatically reflected in the client. There is no need to return them with an explicit reverse service invocation. The following diagram depicts most of what has been discussed so far when Alice logs into chat:

Initialisation

Establishing Creation Listeners

While a node set is placed into a process's node space will propagate update and delete events to the client, creation events must be explicitly solicted and processed. Inq raises create events on a typedef's meta data and these emanate from the system catalog. The following script arranges to receive these events for the OnLine typedef:

// Listen for new instances of OnLine so we can maintain the private list.
// The listen() function returns a token that represents the listen itself.
// We save this at $this.listeners.newOnLine so we can unlisten() later
// if we want to.
any $this.listeners.newOnLine = listen (unlisten($catalog, $this.listeners.newOnLine),
                                        func f = call onLineCreated(OnLine = @eventData),
                                        event   = (create),
                                        typedef = OnLine);

The opposite of listen is unlisten but unlisten doesn't mind if the listener token is unresolved.

The second argument is a func whose expression (which does not have to be a call statement) is executed when the event fires. At this point, the stack is initialised with certain well-known paths. For the create event the path @eventData is the instance just created. The use of func as opposed to cfunc is important because func encapsulates the context in which the event handler expression will run.

The typedef argument ensures the listener only fires when instances of examples.chat.OnLine are created. Here is the script for onLineCreated:

/**
 * Called when a new OnLine instance is created, that is when a new user
 * registers with Chat.
 */
local function onLineCreated(any OnLine)
{
  // Put the instance in the private list
  any k = getuniquekey(OnLine);
  add(OnLine, path($this.vars.privateList.{k}.OnLine));
}

From the discussion of the node set structure the event handler follows the pattern of:

  1. obtaining the primary key of the new instance using getuniquekey() and
  2. using the add() function to place the new instance in the node space accordingly and raising an event

The event flow is as shown:

Create Events

In a similar way, a listener is set up to fire when new ChatChannel instances are created, except that in this case we are only interested in those having IsPublic equal to true. To support filtering of create events, Inq allows an arbitrary value, known as the create data, to be specified in the listener and at the point the instance is submitted for creation. Here is the script to set up the listener:

// Listen for new instances of ChatChannel so we can maintain the public list.
// In this case we discriminate events of interest by specifying the "create"
// argument. This is any value that must compare equals with the value
// used in create() for the listener to be fired. We only have one value
// which is boolean true. We don't have to use a map but doing so just
// makes things a little clearer.
any createPublic.IsPublic = true;
any $this.listeners.newChatChannel = listen (unlisten($catalog,
                                                      $this.listeners.newChatChannel),
                                          func f  = call chatChannelCreated(ChatChannel = @eventData),
                                          event   = (create),
                                          create  = createPublic,
                                          typedef = ChatChannel);

The listener will fire if the data provided in the create statement compares equals with the create argument in the listen statement. At the bottom of chatserv.inq there is some script to create a public chatroom:

// Create a test chat room
any cc = new(ChatChannel);
cc.ChatChannel = "TestInq";
cc.IsPublic = true;
cc.IsConference = false; // only relevant when IsPublic = false
cc.PartyCount = 0;
cc.BackChatCount = 10;

// Make the create data so that this (public) channel fires the listsner
// established in register()
any createPublic.IsPublic = true;
create(cc, createPublic);

This code would fire the listener because its create data is equal to the listener's. The other case where ChatChannel instances are created, private conferences and one-to-one channels, is the following

// specify create data (cf. the "TestInq" Channel at the
// bottom of this file). If a listener for create events is
// established then no create data means wild-card.
any createPublic.IsPublic = false;
create(any c = new(ChatChannel), createPublic);

If create data is used at all it must be used everywhere, as not specifying it will fire all listeners.

Initialisation Response

In chat we choose to respond to the initial service request by invoking the initResponse service in the client:

// Set up the icons we will use and pass them back in initResponse
smap icons;

any icons.win       = image("images/chatinq.jpg");
any icons.iwin      = image("images/ichatinq.jpg");
any icons.clear     = image("images/clear16.gif");
any icons.rxfocus   = image("images/rxfocus.gif");
any icons.rxnofocus = image("images/rxnofocus.gif");

// Send back the response
send initResponse(icons, OnLine);

We have chosen to return some icons to the client. In the same way that source scripts are generally not directly accessible by the client, so other resources need not be either. Inq applications may be operating through specific firewall ports and no assumptions about http:// access need be made using this pattern.

Other Services

In the rest of this section we cover more briefly the other major services. Reference can be made to the source files to see how these and the more minor ones are scripted in detail.

joinChannel() and joinChannelConfirm()

The joinChannel service adds the caller as a new participant in the given channel. In fact, because this operation is also performed within the server it just calls the local function joinChannel, which does the actual work. If completed successfully, the joinChannelConfirm service is invoked in the client.

The joinChannelConfirm service creates a GUI subtree to support the client's participation in the channel. In the same way as initialisation used the gContext event during client initialisation, so the client defines the root of the GUI subtree as a context and uses the same event to complete its setup.

Join Channel

sendChat()

The sendChat service dispatches a chat message to all current participants. Earlier in this example we discussed the node space requirements. As we now see, at the second-level context we have the participantsList established in setupChatContext() that allows to do this straight away in a loop:

// Send the message to existing channel participants
foreach($this.chatInsts.participantsList)
{
  // Protect against exceptions in case any of the users coincidentally
  // go off line.
  try
  {
    any p = getprocess($this.OnLine.ProcessId);
    send receivedChatMsg(ChatChannel,
                         BackChat,
                         @channel = p.ichannel,
                         @context = $this.OnLine.Context);
  }
  catch {}
}

Inq's built-in event handling means that this list is automatically maintained when participants leave the channel, however if we are unlucky enough to send to a (now) non-existent process we simply ignore the exception that would arise.

Send Chat

Notice that in the send statement we specify the well-known argument of @context. Recall from earlier that this is the context of the chat system as a whole, not the channel we are currently dealing with. In the server this does not matter because its only action is to relay the message to the client. The issue of switching to the correct context is handled in the client-side service chatReceived() with the following usage of xfunc:

/**
 * We have received a chat message. This service is invoked in the
 * top-level chat context (originally established when initChat was
 * invoked). In this context we've saved our character styles and other
 * useful things, and the funcs for each active ChatChannel, of course.
 */
service chatReceived(any ChatChannel, any BackChat)
{
  any k = getuniquekey(ChatChannel);

  xfunc($this.vars.{k}.tabFuncs.chatReceived,
        ChatChannel,
        BackChat,
        $this.vars.baseStyle,
        $this.vars.urlRE,
        $this.properties.active);
}

nextpage