LOOPE - Lingo Object Oriented Programming Environment by Irv Kalb

Previous Chapter

Table of Contents

Next Chapter

 

Section 3 - Section 3 - Behaviors and Objects Together

Chapter 14 - Building a Service Object

 

NetLingo

Lingo has a set of four built-in functions that are designed to allow programs to send and retrieve information over the internet. Collectively, these calls are commonly referred to as netLingo. The main calls are: getNetText, downloadNetText, postNetText, and preloadNetThing. (Additionally there are the support functions of netDone and netResult which we will explain later.) These four functions allow Lingo programmers to write applications that extend their applications beyond the confines of the desktop. However, these functions can be tricky to use correctly.

The goal of this chapter is to write a single "Net Manager" object whose job will be to manage any number of netLingo operations on behalf of a program. That is, any time you wish to perform a netLingo operation, rather than doing it in your own code, you will instead be able to make a call to a globally available Net Manager to do the work for you. This is a very object oriented approach because it allows you to "section off" all the code needed to deal with netLingo into a single parent script. Once that script is written and debugged, you never have to worry about netLingo operations again. The parent script will be re-usable across many projects.

When you write a parent script like this, it can be thought of as a service object. While it is still incorporated into the program, it provides services for the program. The parts of the main program that make calls to this netLingo manager will be considered the clients of this service object. A service object like this becomes a go-between. The program makes calls to the service object, and the service object handles these calls on behalf of the program and deals with the real target of the calls.

 

getNetText as an example

All the netLingo operations work in a similar way. We will choose to use getNetText as it provides the clearest example. As its name implies, the purpose of getNetText is to get the text that lives inside a file somewhere on the internet. Ideally, when we want to get the text of a file in the internet, we would like to write a statement like this:

sText = getNetText("http://www.someserver.com/somefilename.txt")

Unfortunately, it's not that simple. The problem is that any operation performed over the internet takes time to complete - real time that is perceptible to real users. Further, there is no way to know up front, how long a netLingo operation might take. The amount of time needed to complete its work is dependent on a number of factors: the amount of data being transferred, the speed of the internet connection, the speed of the disk on the server, the speed of the disk on the local computer, the speed of both processors, the amount of traffic on the internet, and many others.

All Lingo calls are defined to be either synchronous or asynchronous. Synchronous means that the program waits until the called routine is completed, and then execution continues at the next line of Lingo code. The called routine will complete its work - and if it is a function, it will return a result of the work it has accomplished. Most Lingo routines and functions are synchronous. In fact, unless the Lingo manual explicitly says differently, you can assume that a call is synchronous.

An asynchronous call works very differently. When you call an asynchronous routine, the called routine starts working immediately and returns to the caller quickly, but does not complete its work right away. Instead, the caller must check back with the routine from time to time to see if has completed its work. When it says that it has completed its work, then the caller can get the result of the routine.

Because the amount of time it may take to complete a netLingo operation, all four netLingo calls are asynchronous. If these calls were synchronous, then during the time that a netLingo operation was doing its work, the user would not be able to continue to interact with the program. This would be unacceptable.

To perform an asynchronous operation such as getNetText, you must make a call to getNetText to initiate the downloading of the text. Then you must repeatedly check to see when the operation has completed. Finally, once the operation has completed, you can assign the resulting text to a variable.

Here is an analogy. Lets say you want to get your shoes repaired. In the synchronous model, you take your shoes to the shoe repair shop, hand them over to the shoe repair person behind the counter, and then you wait. You wait at the shoe repair shop until your shoes are fixed. You have no way of knowing how long this will take. Further, while you are at the shoe repair shop you cannot carry on with the rest of your life.

The asynchronous model is very different. You bring your shoes to the shoe repair shop the same way. But rather than waiting for your shoes to get fixed, the shoe repair person hands you a ticket with a number that uniquely identifies your shoes. Now you can go home and continue on with whatever you want to do. Every once in a while, you go to or call the shoe repair shop and ask if your shoes, identified by the number on the ticket, are done yet. Until they are finished, the conversation goes like this, "Are my shoes, number 1234, done yet?". "No". (Some time goes by...) "Are my shoes, number 1234, done yet?". "No". (Some time goes by...) "Are my shoes, number 1234, done yet?". "No". (Some time goes by...) Eventually, the answer will be "Yes". When the answer is yes, then you can retrieve your shoes.

Knowing that you need to continually check the status, you might be tempted to write something like the code below, (which is actually incorrect):

property pNetID

on mouseUp me
  pNetID = getNetText("http://www.someserver.com/somefile.txt")
  repeat
while netDone(pNetID) = FALSE
    nothing

  end
repeat
  sText =
netTextResult(pNetID)
  -- do something with sText
end

In fact, many people who are just starting to write code to deal with netLingo operations try to use a repeat loop to do the checking. However, because of the way Lingo handles repeat loops, this code will not work. Repeat loops are optimized so that no external events can happen while a repeat loop is executing. Because the getNetText operation doesn't really get time to execute, the operation will never complete. And therefore, the repeat loop will never exit.

Instead, we must recode the above so that we check every once in a while. In the following discussion, let's assume that the program is running in a frame with a standard "go to the frame" frame script behavior. Given that we are constantly going to the same frame, we can do the checking in an exitFrame method of a behavior attached to a sprite, as follows:

property pNetID

on beginSprite me
  pNetID = VOID
end

on mouseUp me
  pNetID = getNetText("http://www.someserver.com/somefile.txt")
end

on exitFrame me
  if
voidp(pNetID) then
    return
  end
if

  if netDone(pNetID) then
    sText = netTextResult(pNetID)
    -- do something with sText
    pNetID = VOID
-- reset pNetID
  end
if
end

Assuming that this behavior is attached to a sprite, and the program is in a frame with a standard "go to the frame" behavior, this code will work correctly. When you click on this sprite, it makes a request to get the text from a file at the given URL. The operation returns a network ID, (often referred to as a NetID), that uniquely identifies this operation. The on exitFrame method of this behavior will continuously be called. When the sprite is clicked, pNetID is assigned the value returned from the call to getNetText. Then the exitFrame method starts checking for completion by calling the support routine netDone, while passing in the NetID obtained from the original call. This process is repeated until the operation has completed. This type of continuous checking is commonly referred to as polling. When the operation is finished, netDone returns TRUE. When that happens, the program then calls the other support routine, netTextResult, to get the actual text. You can then do what ever you want to do with the text that has been retrieved. Finally, we set the NetID back to VOID so that the exitFrame method will stop checking.

In our shoe repair store analogy, the above approach corresponds to you checking in with the shoe store every once in a while to find out if your shoe repairs have been completed. Now, let's extend our analogy by making our shoe repair shop have better customer service. Wouldn't it be more pleasant if the shoe store delivered the shoes back to your house when they were fixed? And at the door of your choice: front door, side door, back door. In computer terminology, this concept is referred to as a callback. You initiate an operation - and instead of polling for completion, the operation calls you back when it is done and passes back any information you requested.

If we wanted our shoe store to do this, the shoe store would need two pieces of information. When you drop off your shoes, the shoe repair person would ask you for your address, and might ask you at which door you want your shoes left. The address is needed because the shoe store wants to drop the shoes off at your house. By default, they would deliver them to your front door, but if you had given special instructions to deliver them to the back door, the shoe store would be more than happy to comply. With this type of service, you simply drop off the shoes, give some information about yourself, and when the shoes are done they show up at your house at the door of your choice.

Now, how do we implement something like this in Lingo? First, we'll look at this from the "client" point of view - that is, the place in your program where you want to issue a netLingo request. Then we will start to build a generic Net Manager that will handle all the details. To build the client side, you must assume that the Net Manager already exists. In this way, we can start to build up the API of what we will need the Net Manager to provide for us. We'll assume that the program has instantiated a global Net Manager called goNetMgr. Here's how we would replace our earlier code with a call to our new Net Manager:

 

global goNetMgr

on mouseUp me
  
goNetMgr.mGetNetText("http://www.someserver.com/somefile.txt", me)
end

on mNetEvent me, sText
  -- do something with sText
end

 

In this behavior, when the user clicks on the sprite, we call a method of the Net Manager to do the real work for us. Note that you don't even need to receive back a NetID because the Net Manager will handle all such details internally. (We could choose to build the Net Manager is a way so that it returns the NetID, in case we want to allow for the client to do it's own polling, but we will ignore this for now.) Also note that we are passing in the value of "me". The value of "me" is always the current object. In the shoe store analogy, we are passing in our address so the shoe store will know where to deliver our shoes.

Next, you see a method in the behavior called mNetEvent. When the Net Manager is finished with the operation, it will call this method of this behavior and pass back the text it has retrieved. But why mNetEvent? There is nothing special about this name. It could be any name. However, the client needs to know the name of the callback routine that the Net Manager will call. The name of this method is the equivalent of our shoe store dropping off the shoes at our front door by default. So we must know what that default method is. If you want to be able to use a different method for the callback, then we need to change the API of our Net Manager to allow us to optionally specify the alternative method name as an additional parameter in the mGetNetText call. For example, if you would rather have your callback set to mMyNetCallback, then we could add an additional parameter, (passed as a symbol), in the initial call like this:

global goNetMgr

on mouseUp me
  goNetMgr.mGetNetText("http://www.someserver.com/somefile.txt", me, #mMyNetCallBack)
end

on mMyNetCallback me, sText
  -- do something with sText
end


Given such a call, when the Net Manager has finished the network operation, it would instead call back using the mMyNetCallBack method in this behavior.

 

Building the Net Manager

Now let's look at how we can build a basic Net Manager. As we said, we will instantiate a single global Net Manager object from a Net Manager parent script when the program starts up. The code will start out simple enough:

-- NetMgr #1

property pNetID
property poCaller

on new me
  return
me
end

on mGetNetText me, sURL, oCaller
  poCaller = oCaller -- save away caller's object reference
  pNetID = getNetText(sURL) -- initiate the real netLingo call
end

on mSomeHandlerThatGetsCalledPeriodically me
  if
voidp(pNetID) then
    return
-- nothing to do
  end
if

  if netDone(pNetID) then
    sText = netTextResult(pNetID)
    poCaller.mNetResult(sText) -- callback with results
    pNetID = VOID
-- clear out
  end
if
end

Most of the code here should look very familiar. We have moved most of the logic from the earlier behavior into this parent script. In the mGetNextText method you can see how we save a copy of the caller's object reference into the property variable, poCaller. We do this so we can call back the caller when the operation is complete. Then, we initiate the real netLingo operation using the supplied URL. Finally, the NetID which was returned by the operation is stored into the pNetID property variable.

Now look at the method appropriately named, mSomeHandlerThatGetsCalledPeriodically. The fundamental idea here is that the responsibility for doing the polling for completion of the operation, has been moved from the behavior into this object. Given this shift, you can now write a number of different client behaviors that make use of a Net Manager, where each behavior is much simpler, just a call and a callback. Each such behavior never has to worry about calling netDone and netTextResult.

But now we have a problem. We need a way to have the method of object get called periodically. This is one way that behaviors are different from objects. Behaviors constantly receive "exitFrame" events, but objects do not. You'll recall that in our earlier behavior version of this script, we simply had an "on exitFrame" method where we did the polling. As written, the mSomeHandlerThatGetsCalledPeriodically in this script will never actually be called. We can't do it this way with an object.

 

The Actorlist

Director maintains a special list of objects that would like to receive messages once every frame, (just like behaviors get exitFrame messages). This special list is called the actorList and is a built-in global list variable. When you start up Director, if you go to the message window and type:

-- Welcome to Director --
put the actorList
-- []


You see that this is an empty list. Any object that wants to receive notification of frame events can either add itself, or can be added to the actorList. The actorList works like any other Lingo list, therefore, you can add an object by using the add command. If an object wants to add itself to the actorList, you can write this:

add(the actorList, me)

Or if you want to add an object from "outside" the object using an object reference, you do it like this:

add(the actorList, oSomeObjectRef)

Once an object has been added to the actorList, the object will receive a callback every frame in a special method called "on stepFrame". The object can now do incremental work the same way that a behavior can do incremental work in its exitFrame method. If we think of the on stepFrame method just like the on exitFrame method in a behavior, now we have a way that our Net Manager can poll to see when the net operation is completed. Here is a rewrite of our previous code into a real working form:

-- NetMgr #2

property pNetID
property poCaller

on new me
  add(the
actorList, me)
  return
me
end

on mGetNetText me, sURL, oCaller
  poCaller = oCaller -- save away caller's object reference
  pNetID = getNetText(sURL) -- initiate the real netLingo call
end

on stepFrame me
  if
voidp(pNetID) then
    return
-- nothing to do
  end
if

  if netDone(pNetID) then
    sText = netTextResult(pNetID)
    poCaller.mNetResult(sText) -- callback with results
    pNetID = VOID
-- clear out
  end
if
end

on mCleanUp me
  deleteOne(the
actorList, me)
end

When the Net Manager is instantiated, it adds itself to the actorList. It will then receive calls to its stepFrame method every frame event. The polling for completion of the netLingo operation is then done in the on stepFrame method.

If and when an object no longer wishes to receive stepFrame events, it can be removed from the actorList. If you want to remove the object from the actorList from inside the object, you do it like this:

deleteOne(the actorList, me)


In the script above, the Net Manager removes itself from the actorList when it is cleaned up. If you want to remove an object from "outside" the object using an object reference, you do it like this:

deleteOne(the actorList, oSomeObjectRef)

Warning: You should never remove an object from the actorList in its own on stepFrame handler. This may lead to unexpected errors or crashes. This because by deleting an object from the actorList changes the actorList itself. If the object being deleted is not at the end of the actorList, then Director will miss calling the next object in the actorList.

 

There is another approach to the problem of having an object have a method that is called repeatedly. Instead of using the actorList, we could have set up a timeout object. Timeout objects are very powerful and are specifically designed for calling methods at periodic intervals. This approach would work just as well. We have decided to not go into detail about timeout objects here, as it might tend to confuse the central issue of building a Net Manager.

 

When writing code that makes asynchronous calls (such as to a Net Manager), care must be taken with the user interface of the program. When you make an asynchronous call, you don't know how long it will take to get the answer back. Instead of the program waiting until the answer is available, the program keeps running. As a programmer, you must decide how to deal with this. For example, you must decide if the program must effectively wait until the results are available. If so, then you may need to change the cursor, put up a status string ("Getting data ..."), show an animation, or even put a dialog box of some sort. If, however, the program can continue on without the immediate need for the data, then you may need to put up an alert when the data is ready. User interface becomes a more complex issue because the program must accommodate for real time while waiting for information. This mindset is extremely important in designing the overall flow of the program.

 

Managing multiple operations

As we have written it so far, the Net Manager can only handle one operation at a time. In fact, this code has an inherent error. Assume that you had a program that had two different buttons to get text from two different URLs. Further assume that each button had a behavior which made a call to the Net Manager asking it to retrieve the associated text. Think about what would happen if the user clicked on one button, then while the Net Manager was processing that operation (polling for its completion), the user clicked on the other button to initiate a second operation from the second URL. In this case, when mGetNetText is called, the new NetID would overwrite the current one, and the new object reference of the caller will replace the one in the first call. The code in the stepFrame method would switch to poll for the second request only. The first request would effectively be lost! We need a general way to address this problem.

The solution lies in building a queue of requests where only one operation is active at any time. In computerese, this is known as a FIFO (First In First Out) queue. Using this approach, whenever a request to perform an operation comes in, we always add the new request to the end of a queue of requests to be processed. If this is the only request in the queue, then we activate it right away. Every time we finish an operation, we remove the current operation from the queue, move all queued operations down by 1 position in the queue, and activate the one at the beginning of the queue. Here is a visual representation of such a queue:

In this first picture a single operation has been requested. The operation which we'll call A, has been added to the end of the queue. Because the queue started out empty, the operation winds up in position 1. And because it is in position 1 in the queue this operation is activated (shown in green).

Here, while still processing the A operation, the program has issued five more operation requests. Each has been added in turn to the end of the queue, we'll call these B, C, D,E, and F. They are shown in red to signify that they are not active.

Now operation A finishes and is eliminated from the front of the queue. When A is eliminated from the queue, all remaining queued operations shift their position down to the next lower position number. Because operation B is now in position 1, it is activated.

Here, operation B has finished, all pending operations are moved to the next lower number, and operation C is activated.

Now another request comes in (G). We add this operation to the end of the queue and continue processing the operation in position 1.

Lingo lists are perfect for building and maintaining such a queue. Each element in our list will represent one queued netLingo operation request. Each element will actually be a property list that remembers the key information that we need in order to perform the operation. For now, each operation will remember the URL and the caller's object reference. Each operation will be represented by a Lingo property list that looks like this: [#URL: sURL, #caller: oCallBack].

-- NetMgr #3

property pNetID
property pllQ -- list of lists of queued operations

on new me
  pllQ = [] -- initialize to the empty list
  add(the
actorList, me)
  return
me
end

on mGetNetText me, sURL, oCaller
  lOperation = [#URL: sURL, #caller: oCaller]
  append(pllQ, lOperation)
  if
count(pllQ) = 1 then
    me.mActivateOperation()
  end
if
end

on mActivateOperation me
  lOperation = pllQ[1] -- always use the first operation in the Q
  sURL = lOperation[#URL]
  -- initiate the real netLingo operation
  pNetID = getNetText(sURL)
end

on stepFrame me
  if
voidp(pNetID) then
    return
-- nothing to do
  end
if

  if netDone(pNetID) then
    sText = netTextResult(pNetID)
    -- Find out who the caller was
    lOperation = pllQ[1]
    oCaller = lOperation[#caller]
    oCaller.mNetEvent(sText) -- callback with results
    pNetID = VOID
-- clear out
    deleteAt(pllQ, 1) -- eliminate the actiive operation
    
    -- If there are more operations queue, activate the top one
    if
count(pllQ) <> 0 then
      me.mActivateOperation()
    end
if
  end
if
end

on mCleanUp me
  deleteOne(the
actorList, me)
end

In the code above, only the operation residing in position one is ever active. When the client calls mGetNetText, the Net Manager builds an operation (represented as a property list), and always adds it to the end of the queue of operations. After adding it to queue, it checks to see if there is only one operation in the queue (the current operation). If so, it activates the operation immediately. If there were already one or more operations in the queue, the new operation would just be added to the end of the queue. Whenever an operation is completed, the property list representing that operation is removed from the front of the queue (deleted from position one), any and all queued operations are moved to a lower position number. If there are operations left in the queue, then the one in position one is activated.

Notice that although we have added more complexity to the processing of these operations, the interface from the client hasn't changed at all. This technique of queuing requests and processing them over time is extremely powerful. We have demonstrated it in building a Net Manager here, but this approach can be used for any type of asynchronous requests. I have used a similar structure of managing queues for sending email messages (using the DirectEMail XTRA) and sending and receiving files over ftp (using the DirectFTP XTRA).

 

Alternative callback method

Earlier we talked about the way that the Net Manager calls back the requestor. We said that by default, when the operation is completed, the Net Manager would call back the caller using the mNetEvent method. But now we might like to be able to allow the caller to be called back at a different method for different calls. In order to do this, we added an additional optional parameter in the client's call to mGetNetText. As a reminder, we said that the client would be allowed to call mGetNetText with an optional third parameter like this:

goNetManager.mGetNetText(sURL, me, #mSomeOtherCallbackMethod)

This allows the caller to specify the name of a method that should be called back when the operation is completed. In our shoe repair example, this is the equivalent of specifying at which door our shoes should be left. We said that the shoe repair shop would leave the shoes at the front door (the mNetEvent method), unless we specified that they should be left somewhere else (e.g., mSomeOtherCallbackMethod).

In this case, when the operation is completed, the Net Manager will call back the caller using the caller's mSomeOtherCallbackMethod. To implement this in the Net Manager, we only need to make three minor changes. First, we change the mGetNetText to take an additional optional parameter. Second, in the same method we need to add the name of the method to call back to the property list of things that we remember for each operation. The modified code would look like this:

on mGetNetText me, sURL, oCaller, symCallback
  if
voidp(symCallBack) then
    symCallBack = #mNetEvent
  end
if
  lOperation = [#URL: sURL, #caller: oCaller, #callback:symCallback]
  append(pllQ, lOperation)
  if
count(pllQ) = 1 then
    me.mActivateOperation()
  end
if
end

The final change deals with the way we call back the caller. When the operation is completed, our earlier code called back the caller like this:

oCaller.mNetEvent(sText) -- callback with results

But now, the caller should be called back using the name of the method that that was saved in the property list representing the operation. To call back a method "on the fly", we can use the Lingo call statement. The Lingo call statement takes a method name (as a symbol), an object reference, and values to be sent. Therefore, we can change the above line to this:

symCallBack = lOperation[#callback] -- get the name of the callback method
call(symCallBack, oCaller, sText) -- callback with results

 

Managing concurrent operations

It turns out that with netLingo operations, Macromedia currently claims that you can actually carry on up to four netLingo operations at the same time. We wrote the earlier code assuming that only one operation could be active. We can now modify the code to allow for concurrent operations. (Because the number of concurrent operations could get larger in the future, we'll make this value be a property variable, pnkConcurrentOps, rather than a hard-coded constant. But to make the discussion easier, we will use the number 4) This is a good exercise in pure Lingo. We will still maintain a list of queued operations, but instead of only activating operation number one, we will now allow the first four operations in the queue to be active. Whenever an operation is placed in positions 1, 2, 3, or 4, it will be immediately activated (shown in green below). If an operation is placed into positions 5 or above, the request will simply be queued, but not activated (shown in red below). Here is a visual representation of the approach.

In the picture above, a request for netLingo operation A comes in. We find that position 1 of the queue is empty so the request in put in position 1. Because the request is in one of the first four positions, we activate it immediately. Then a second request, operation B, comes in. Similarly, we find that position 2 is open. We place the request in position 2 and activate it immediately.

 

Now, four additional requests (C, D, E, F) come in over time. Request C is placed into empty slot 3 and is activated. Request D is placed into empty slot 4 and is activated. Now request E comes in. Because all of the first four slots are filled, request E gets added tot the end of the queue in position 5, but is not activated. Similarly Request F is placed at the end of the queue in slot 6 and is not activated.

Eventually, operation A has finishes. When it does, its slot (position 1) is emptied. When an active operation (in the first four slots) finishes, we look to see if there is a queued operation in slot 5. We find that operation E is queued there, so we move request E from its place in the queue to the newly emptied position (slot 1), and immediately activate it.

 

Eventually operation B has finishes. In a similar way, operation F is moved into the newly emptied slot (slot 2). When Operation C finishes, there are no more queued operations waiting, and slot 3 is left empty (a zero in the queue, and in the same position in the list of net IDs).

And here is the code to implement this:

-- NetMgr #5

property pnkMaxConcurrentOps -- constant, max number of concurrent IDs
property plNetIDs -- list of active NetIDs
property pllQ -- list of lists of queued operations
property pnActiveOps -- number of active operations

on new me
  pllQ = [] -- initialize to the empty list
  pnActiveOps = 0
  pnkMaxConcurrentOps = 4
  plNetIDs = []
  repeat
with i = 1 to pnkMaxConcurrentOps
    append(plNetIDs, 0) -- zero means empty
    append(pllQ, 0)
  end
repeat
  add(the
actorList, me)
  return
me
end

on mGetNetText me, sURL, oCaller, symCallback
  if
voidp(symCallBack) then
    symCallBack = #mNetEvent
  end
if
  lOperation = [#URL: sURL, #caller: oCaller, #callback:symCallback]

  if pnActiveOps < pnkMaxConcurrentOps then -- activate right away
    me.mActivateOperation(lOperation)
    
  else
-- queue this operation
    append(pllQ, lOperation)
  end
if
end

on mActivateOperation me, lOperation
  -- Initiate the net op
  sURL = lOperation[#URL]
  -- there is an empty slot, find it and activate
  where = getOne(plNetIDs, 0)
  -- store this operation into the new place in the queue
  pllQ[where] = lOperation

  -- initiate the real netLingo operation
  NetID = getNetText(sURL)
  plNetIDs[where] = NetID

  -- increment the number of active operations
  pnActiveOps = pnActiveOps + 1
end

on stepFrame me
  if pnActiveOps = 0
then
    return
-- nothing to do
  end
if

  repeat with queueNumber = 1 to pnkMaxConcurrentOps
    NetID = plNetIDs[queueNumber]
    if NetID > 0
then
      fDone = NetDone(NetID)
      if fDone then
        sText = netTextResult(NetID)
        
        -- Find out who the caller was and where they want to be called
        lOperation = pllQ[queueNumber]
        oCaller = lOperation[#caller]
        symCallback = lOperation[#callback]
        call(symCallback, oCaller, sText) -- callback with results
        
        plNetIDs[queueNumber] = 0
-- clear out old NetID
        pllQ[queueNumber] = 0
-- clear out actiive operation
        pnActiveOps = pnActiveOps - 1
        
        -- If there are more operations queue, activate the top one
        if
count(pllQ) > pnkMaxConcurrentOps then
          -- grab the next queued op from the queue, and activate it
          lNextOperation = pllQ[pnkMaxConcurrentOps + 1]
          deleteAt(pllQ, (pnkMaxConcurrentOps + 1))
          me.mActivateOperation(lNextOperation)
        end
if
      end
if
    end
if
  end
repeat
end

on mCleanUp me
  deleteOne(the
actorList, me)
end


In the stepFrame method, instead of checking a single operation, we now check up to 4 concurrent operations. In order to make this checking fast, we have added a list of NetIDs, (the property variable plNetIDs), which is maintained parallel to the list of the first 4 operations in the queue. In this list we store the NetIDs of the currently active operations and use them to check for completion. Whenever an operation completes, we empty the associated slot in the queue and the associated NetID in the plNetIDs list (set both to zero). Then we check to see if there is a fifth element in the queue. If there is, we move the property list representing that operation into the empty slot left by the completed operation, and activate it.

 

Allowing for all four netLingo operations

Now that we have the basic structure of our NetManager object, we can add in the other three netLingo operations. This addition will require changes to three areas of our code. First we will need to add methods to handle each new operation (mPostNetText, mPreloadNetText, and mDownloadNetThing). Second we need to expand the list of information saved for each netLingo operation to include an operation type. To do this, we add an #opType to each list representing an operation. We will also need to save some additional information for the postNetText and downloadNetThing calls. Third, when we are ready to activate the operation, based on the saved operation, we execute the appropriate netLingo call.

-- NetMgr #6

property pnkMaxConcurrentOps -- constant, max number of concurrent IDs
property plNetIDs -- list of active NetIDs
property pllQ -- list of lists of queued operations
property pnActiveOps -- number of active operations

on new me
  pllQ = [] -- initialize to the empty list
  pnActiveOps = 0
  pnkMaxConcurrentOps = 4
  plNetIDs = []
  repeat
with i = 1 to pnkMaxConcurrentOps
    append(plNetIDs, 0) -- zero means empty
    append(pllQ, 0)
  end
repeat
  add(the
actorList, me)
  return
me
end

on mGetNetText me, sURL, oCaller, symCallback
  if
voidp(symCallBack) then
    symCallBack = #mNetEvent
  end
if
  lOperation = [#URL: sURL, #caller: oCaller, #callback:symCallback, #opType:#getNetText]
  me.imQueueOperation(lOperation)
end

on mPostNetText me, sURL, oCaller, lDataORsData, symCallback
  if
voidp(symCallBack) then
    symCallBack = #mNetEvent
  end
if
  lOperation = [#URL: sURL, #caller: oCaller, #callback:symCallback, #opType:#postNetText, #data: lDataORsData]
  me.imQueueOperation(lOperation)
end


on mPreloadNetThing me, sURL, oCaller, symCallback
  if
voidp(symCallBack) then
    symCallBack = #mNetEvent
  end
if
  lOperation = [#URL: sURL, #caller: oCaller, #callback:symCallback, #opType:#preloadNetThing]
  me.imQueueOperation(lOperation)
end

on mDownloadNetThing me, sURL, oCaller, sLocalFileName, symCallback
  if
voidp(symCallBack) then
    symCallBack = #mNetEvent
  end
if
  lOperation = [#URL: sURL, #caller: oCaller, #callback:symCallback, #opType:#downLoadNetThing, #localFilename:sLocalFileName]
  me.imQueueOperation(lOperation)
end

on imQueueOperation me, lOperation
  if pnActiveOps < pnkMaxConcurrentOps then
-- activate right away
    me.mActivateOperation(lOperation)
    
  else
-- queue this operation
    append(pllQ, lOperation)
  end
if
end

on mActivateOperation me, lOperation
  -- Initiate the net op
  sURL = lOperation[#URL]
  -- there is an empty slot, find it and activate
  where = getOne(plNetIDs, 0)
  -- store this operation into the new place in the queue
  pllQ[where] = lOperation
  symOpType = lOperation[#opType]

  -- initiate the real netLingo operation, based on the remembered type
  -- pull out additional saved info for postNetText and downloadNetThing
  case symOpType of
    #getNetText:
      NetID = getNetText(sURL)
      
    #postNetText:
      theData = lOperation[#theData]
      NetID = postNetText(sURL, thedata)
      
    #preloadNetThing:
      NetID = preloadNetThing(sURL)
      
    #downloadNetThing:
      sLocalFileName = lOperation[#localFileName]
      NetID = downloadNetThing(sURL, sLocalFileName)
  end
case

  -- Save the returned NetID
  plNetIDs[where] = NetID

  -- increment the number of active operations
  pnActiveOps = pnActiveOps + 1
end

on stepFrame me
  if pnActiveOps = 0
then
    return
-- nothing to do
  end
if

  repeat with queueNumber = 1 to pnkMaxConcurrentOps
    NetID = plNetIDs[queueNumber]
    if NetID > 0
then
      if
netDone(NetID) then
        
        -- Check for network error
        theNetError = netError(NetID)
        if theNetError <> "OK"
then
          errorReturnValue = theNetError
        else
          errorReturnValue = 0
        end
if
        
        -- Find out: the caller, the operation, and where to call back
        lOperation = pllQ[queueNumber]
        symOperation = lOperation[#opType]
        oCaller = lOperation[#caller]
        symCallback = lOperation[#callback]
        if (symOperation = #getNetText) or (symOperation = #postNetText) then

          sText = netTextResult(NetID)
        else

          sText = ""
-- no real text to return
        end
if
        
        -- Call back with results and errorCode
        call(symCallback, oCaller, sText, errorReturnValue)
        
        plNetIDs[queueNumber] = 0
-- clear out old NetID
        pllQ[queueNumber] = 0
-- clear out actiive operation
        pnActiveOps = pnActiveOps - 1
-- decrement number of active ops
        
        -- If there are more operations queue, activate the next one
        if
count(pllQ) > pnkMaxConcurrentOps then
          -- grab the next queued op from the queue, and activate it
          lNextOperation = pllQ[pnkMaxConcurrentOps + 1]
          deleteAt(pllQ, (pnkMaxConcurrentOps + 1))
          me.mActivateOperation(lNextOperation)
        end
if
      end
if
    end
if
  end
repeat
end

on mCleanUp me
  deleteOne(the
actorList, me)
end


We did make one more significant change in this version. In the on stepFrame method, once we see that an operation is done, we make an additional call to another support routine, netError. This routine tells us if the netLingo operation was successful. If the operation completed correctly, netError returns the string "OK". If there was an error, then netError would return the error code of the network error. We then want to return an errorcode as an additional parameter in the callback. To do this cleanly, we change an "OK" from netError to the value of zero meaning everything went well. The client call backs then need an additional parameter of an errorcode to check for. Here is an example:

global goNetMgr

on mouseUp me
  goNetMgr.mGetNetText("http://someServerName.someFileName", me)
end

on mNetEvent me, sText, errorCode
  if errorCode <> 0
then
    alert("Error:"
&& errorCode)
  else
    -- Do whatever you want to with sText
  end
if
end

 

Our Net Manager is now fully functional. There are only two minor features that would be helpful additions to our Net Manager. First, it would be good if each operation checked for a time out. That is, when the operation was activated, we could record the starting time (in milliseconds). Then as we are checking for completion, we could check the time to find out if a connection was dropped while performing the operation. We could pick an arbitrary, but significant amount of time, for example 30 seconds. If the operation lasted longer than this time period, we could then report a timeout as an error.

The other addition we could make could be to allow the client to assign their own identifier (ID) to each operation. The client would send in an identifier of their choosing with each request to the Net Manager. The Net Manager would just store this ID in the property list that represents the operation. When the Net Manager calls back the client, it could then pass the ID back to the client. Using an ID like this would make it much easier for the client to match up calls and callbacks, especially in the event of an error.

But these additions (as they say) will be left as an exercise for the reader.

 

Summary

This has been a very ambitious chapter. We have introduced a large number of new concepts including: netLingo, polling, synchronous/asynchronous events, call backs, the actorList and the stepFrame method FIFO queues, and the call statement. We have used these concepts to meet our goal of building a Net Manager that can handle any number of all types of netLingo operations. A key concept here is that by building a service object such as a Net Manager, the program is freed from having to worry about low level details. The Net Manager takes care of all this for us. Once we have built the Net Manager, we can then make use of the Net Manager's functionality with a simple call and callback scheme.



Previous Chapter

Table of Contents

Next Chapter