Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created February 21, 2012 15:13
Show Gist options
  • Save bennadel/1876978 to your computer and use it in GitHub Desktop.
Save bennadel/1876978 to your computer and use it in GitHub Desktop.
ColdFusion 10 Beta - Closures And Function Expressions And Threads
<!---
Start a CFScript block. Closures can only be used inside
of CFScript due to the syntax required to define them.
--->
<cfscript>
// I am a factory for the thread callbacks.
function getThreadCallback(){
// Keep a running tally of how many times the log methods
// are called. We are doing this to see how Closure bindings
// are copied into the thread scope (copy vs. reference).
var callCount = 0;
// Define the log file for this demo -- we will need a log
// file since threads are async and we won't be able to
// write to the output.
var logFilePath = expandPath( "./thread.log" );
// Define the callback handler. This will use both a
// lexically-bound counter as well as a passed-in value for
// counter; this way, we can look at the various forms of
// attribute-copy.
var callback = function( metaCounter ){
// Open the file for appending.
var logFile = fileOpen( logFilePath, "append" );
fileWriteLine(
logFile,
"[#++callCount#][#metaCounter#] Finished with a success!"
);
// Clean up the file access.
fileClose( logFile );
};
// Return the callback.
return( callback );
}
// Get the callback closure (bound to the counter).
callback = getThreadCallback();
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Create a struct with a counter so that we can demonstrate that
// threads copy attributes by VALUE.
metaData = {
counter: 0
};
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Run a successful thread - pass in the callback handler and
// the meta count. Notice that these are both being passed-in
// via the attributes scope.
thread
name = "demoThread"
action="run"
util = metaData
onsuccess = callback
{
// Execute the success callback.
onsuccess( ++util.counter );
}
// Run a successful thread - pass in the callback handler and
// the meta count. Notice that these are both being passed-in
// via the attributes scope.
thread
name = "demoThread2"
action="run"
util = metaData
onsuccess = callback
{
// Execute the success callback.
onsuccess( ++util.counter );
}
// Run a successful thread - pass in the callback handler and
// the meta count. Notice that these are both being passed-in
// via the attributes scope.
thread
name = "demoThread3"
action="run"
util = metaData
onsuccess = callback
{
// Execute the success callback.
onsuccess( ++util.counter );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Wait for all the threads to finish running.
thread action="join";
// Debug the threads.
// writeDump( cfthread );
</cfscript>
<!---
Start a CFScript block. Closures can only be used inside
of CFScript due to the syntax required to define them.
--->
<cfscript>
// I accept N thread instances and a callback (the last
// argument). The callback is invoked when each one of the given
// threads has completed execution (success or failure). The
// thread object is echoed back in the callback arguments.
function threadsDone(){
// Extract the callback from the arguemnts.
var callback = arguments[ arrayLen( arguments ) ];
// Extract the thread objects - arguments[1..N-1].
var threads = arraySlice( arguments, 1, arrayLen( arguments ) - 1 );
// I am a utiltiy function that determine if a given thread
// is still running based on its status.
var isThreadRunning = function( threadInstance ){
// A thread can end with a success or an error, both of
// which lead to different final status values.
return(
(threadInstance.status != "TERMINATED") &&
(threadInstance.status != "COMPLETED")
);
};
// I am a utility function that determines if any of the
// given blogs are still running.
var threadsAreRunning = function(){
// Loop over each thread to look for at least ONE that is
// still running.
for (var threadInstance in threads){
// Check to see if this thread is still running. This
// is if it has not yet completed (successfully) or
// terminated (error).
if (isThreadRunning( threadInstance )){
// We don't need to continue searching; if this
// thread is running, that's good enough.
return( true );
}
}
// If we made it this far, then no threads are running.
return( false );
};
// I am utility function that invokes the callback with the
// given thread instances.
var invokeCallback = function(){
// All of the threads we are monitoring have stopped
// running. Now, we can invoke the callback. Let's build
// up an argument collection.
var callbackArguments = {};
// Translate the array-based arguments into a struct-
// based collection of arguments.
for (var i = 1 ; i < arrayLen( threads ) ; i++){
callbackArguments[ i ] = threads[ i ];
}
// Invoke the callback with the given threads.
callback( argumentCollection = callbackArguments );
};
// In order to check the threads, we need to launch an
// additional thread to act as a monitor. This will do
// nothing but sleep and check the threads.
//
// NOTE: We need to pass the two methods INTO the thread
// explicitly since the thread body does NOT have access
// to the local scope of the parent function.
thread
name = "threadsDone_#getTickCount()#"
action = "run"
threadsarerunning = threadsAreRunning
invokecallback = invokeCallback
{
// Check to see if the threads are running.
while (threadsAreRunning()){
// Sleep briefly to allow other threads to complete.
thread
action="sleep"
duration="10"
;
}
// If we made it this far, it means that the threads
// have all finished executing and the while-loop has
// been exited. Let's invoke the callback.
invokeCallback();
};
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Launch a thread - remember this is an ASYNCHRONOUS operation.
thread
name = "thread1"
action = "run" {
// Sleep the thread breifly.
sleep( randRange( 10, 100 ) );
}
// Launch a thread - remember this is an ASYNCHRONOUS operation.
thread
name = "thread2"
action = "run" {
// Sleep the thread breifly.
sleep( randRange( 10, 100 ) );
}
// Launch a thread - remember this is an ASYNCHRONOUS operation.
thread
name = "thread3"
action = "run" {
// In this one, let's throw an error to show that this works
// with failed threads as well as successful one.
//
// NOTE: Since this is an asynchronous process, this error
// does not kill the parent process.
throw(
type = "threadError",
message = "Something went wrong in the thread!"
);
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Wait for all threads to finish and the run the given
// callback.
//
// NOTE: For demo purposes, we're going to JOIN the threads so
// that we can write to the page output.
threadsDone(
cfthread.thread1,
cfthread.thread2,
cfthread.thread3,
function( thread1, thread2, thread3 ){
// Log the status of the completed threads.
writeOutput( "Threads have finished! #now()#<br />" );
writeOutput( "#thread1.name# - #thread1.status#<br />" );
writeOutput( "#thread2.name# - #thread2.status#<br />" );
writeOutput( "#thread3.name# - #thread3.status#<br />" );
}
);
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Wait for all threads to join so we can see the output.
thread action="join";
// Debug threads.
// writeDump( cfthread );
</cfscript>
<!---
Start a CFScript block. Closures can only be used inside
of CFScript due to the syntax required to define them.
--->
<cfscript>
// Create a closure that is bound to the primary page's
// output buffer.
writeToBrowser = function( value ){
// Get the current page context hash code.
var hashCode = getPageContext().hashCode();
// Write to the currently-bound output buffer.
writeOutput( "[#hashCode#] #value#" );
};
// Test the top-level output / hashCode binding.
writeToBrowser( "Top-level CONTROL write.<br />" );
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Run a thread and pass-in the parent-page-bound writer. When
// this thread is done executing, it will save a thread-bound
// writer to the THREAD scope.
thread
name = "test"
action = "run"
toplevelwriter = writeToBrowser
{
// Try writing to whatever output is currently defined.
writeOutput( "Direct output within thead.<br />" );
// Try writing to the closure-based writer.
topLevelWriter( "Top-level writer within thread.<br />" );
// Let's export a closure who's bound to the thread output
// buffer.
thread.writeToThread = function( value ){
// Get the current page context hash code.
var hashCode = getPageContext().hashCode();
// Write to currently-bound output buffer.
writeOutput( "[#hashCode#] #value#" );
};
// Execute the internal write.
thread.writeToThread( "Wrapped output within thread." );
// Sleep the thread to allow access to the exported function
// before the thread has actuall finished running.
sleep( 1000 );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// HOPEFULLY give the above thread enough time to export the
// writer method.
sleep( 500 );
// Try invoking the thread-exported writer.
cfthread.test.writeToThread(
"Thread-writer output - where's it gonna go?<br />"
);
// Join all the threads back to the page.
thread action = "join";
// Debug the threads.
writeDump( cfthread );
</cfscript>
<!---
Start a CFScript block. Closures can only be used inside
of CFScript due to the syntax required to define them.
--->
<cfscript>
// I create evented objects for publish and subscribe
// functionality. This factory creates objects that have the
// following method API:
//
// - on( eventType, callback )
// - off( eventType, callback )
// - trigger( eventType, data )
EventEmitter = function(){
// In order for the event bindings to work, we need to be
// able to bind and Unbind them. For this, we'll need to keep
// a unique ID for each data. And, in order to generate that
// ID, we'll need to store event-emitter meta data.
var eventEmitterMetaData = getMetaData( EventEmitter );
// Add the unique ID if it doesn't exist.
if (!structKeyExists( eventEmitterMetaData, "uuid" )){
// Initialize the uuid.
eventEmitterMetaData.uuid = 0;
}
// Create a cache of event bindings. Each event will be
// defined as a type and a set of callbacks.
var events = {};
// Create an instance of emitter.
var emitter = {};
// I add the given callback to the given event cache.
var addCallback = function( eventType, callback ){
// Check to see if this callback needs to be prepared
// for use with the event system.
if (!structKeyExists( getMetaData( callback ), "eventEmitterUUID" )){
// Add the UUID to the callback meta data.
//
// NOTE: Since this it is very likely (expected) that
// event callbacks will be created as Closrues, we
// can expect each closure to be unique and therefore
// to have its own proprietary meta data.
getMetaData( callback ).eventEmitterUUID = ++eventEmitterMetaData.uuid;
}
// Make sure we have a cache for this particular type of
// event before we try to access it.
if (!structKeyExists( events, eventType )){
// Create the cache.
events[ eventType ] = [];
}
// Add the callback to this collection.
arrayAppend( events[ eventType ], callback );
};
// I remove the callback from the given event cache.
var removeCallback = function( eventType, callback ){
// Make sure the callback has an event emitter UUID. If
// it doesn't, then we it shouldn't be part of the system.
if (!structKeyExists( getMetaData( callback ), "eventEmitterUUID" )){
throw( type = "InvalidCallback" );
}
// Check to make sure we have a cache for the given
// event type.
if (!structKeyExists( events, eventType )){
// No cache - nothing more to check.
return;
}
// Get the target UUID.
var uuid = getMetaData( callback ).eventEmitterUUID;
// Filter the callback out of the event type.
//
// NOTE: I am using an intermediary variable here for the
// result to get around some weird parsing bug.
var cache = arrayFilter(
events[ eventType ],
function( boundCallback ){
// Exclude if this method is bound.
if (getMetaData( boundCallback ).eventEmitterUUID == uuid){
return( false );
}
// Include this callback in the result.
return( true );
}
);
// Save the filtered cache of event callbacks.
//
// NOTE: See note from above (bug).
events[ eventType ] = cache;
};
// Create the bind method.
emitter.on = function( eventType, callback ){
// Add the callback to the appropriate callback cache.
addCallback( eventType, callback );
// Return "this" object for method chaining.
return( emitter );
};
// Create the unbind method.
emitter.off = function( eventType, callback ){
// Remove the callback from the appropriate callback cache.
removeCallback( eventType, callback );
// Return "this" object for method chaining.
return( emitter );
};
// Create the trigger method.
emitter.trigger = function( eventType, data ){
// Make sure we have callbacks for this event.
if (
!structKeyExists( events, eventType ) ||
!arrayLen( events[ eventType ] )
){
// No callbacks to trigger - exit out.
return;
}
// Loop over each callback to invoke it for the event.
arrayEach(
events[ eventType ],
function( callback ){
// Invoke the callback with the given data.
//
// NOTE: I am using the argumentCollection here
// to get around a strange named-argument bug.
callback( argumentCollection = { "1" = data } );
}
);
};
// Return the event emitter instance.
return( emitter );
};
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Create an instance of event emitter. This will have the
// publication and subscription methods for:
//
// - on()
// - off()
// - trigger()
evented = EventEmitter();
// Run a thread an pass in the event emitter. This will allow the
// the thread to announce events during its execution.
thread
name = "eventedThread"
action = "run"
beacon = evented
{
// Sleep this thread immediately to let the code AFTER the
// thread run and bind to the events.
sleep( 100 );
// Trigger start event.
beacon.trigger(
"threadStarted",
"The thread has started!"
);
sleep( 100 );
// Trigger end event.
beacon.trigger(
"threadEnded",
"The thread has ended!"
);
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Before we check the valid event emitting, we want to check to
// make sure that the UNBIND feature works.
tempCallback = function( message ){
writeOutput( "This should never be called.<br />" );
};
// Bind and UNBIND the callback. We want to make sure that it
// will NOT get called for the given event types.
evented
.on( "threadEnded", tempCallback )
.off( "threadEnded", tempCallback )
;
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Bind to the "Start" event on the thread.
evented.on(
"threadStarted",
function( message ){
writeOutput( message & "<br />" );
}
);
// Bind to the "End" event on the thread.
evented.on(
"threadEnded",
function( message ){
writeOutput( message & "<br />" );
}
);
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Halt the page until the thread has finished execution. This
// will cause the events to be triggered BEFORE this page has
// finished running.
thread action = "join";
// Debug threads.
writeDump( cfthread );
</cfscript>
<!---
Start a CFScript block. Closures can only be used inside
of CFScript due to the syntax required to define them.
--->
<cfscript>
// Run a thread that composes a function and a closure.
thread
name = "testComposition"
action = "run"
{
// Cannot use a FUNCTION DECLARATION inside of a CFThread
// since CFThread runs as a Function behind the scenes and
// function declarations cannot be nested.
//
// Error: Invalid CFML construct found on line X at column Y.
// The function innerFunction is incorrectly nested inside
// another function definition
// _cffunccfthread_cfthread42ecfm2828400851.
function innerFunction(){ };
// Define an inner closure. We *know* this will work.
var innerClosure = function(){ };
// Define an inner closure that, itself, defines a Function.
var innerComposite = function(){
function innerInnerFunction(){ }
innerInnerFunction();
};
// Try invoking composite closure.
innerComposite();
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Join thread back to page so we can debug the output.
thread action = "join";
// Debug the thread.
writeDump( cfthread );
</cfscript>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment