Serializing JavaScript Functions

2022-03-01 06:44:44

Serializing JavaScript Functions

or what happens when the client asks for the moon and is willing to ruin the tides to have it.

It is largely well known that JavaScript is a language that specializes in the bizarre and wonderful.  Perhaps no other language will give you quite as much rope to hang yourself with so readily.  When dealing JavaScript, many of us have heard the expression "JavaScript is a LISP."  There's some truth to that in JavaScript's core feature: almost everything is an object.  Much akin to everything being an S-expression?  This may not sound out of the ordinary at first but take the following example:

>> var func1 = function(name) {
console.log("Hello," name || func1._name);
>> func1._name = "world";
>> func1();
Hello, world
>> func1("John");
Hello, John

As show above, even functions can be assigned additional member fields and yes, you can reference them warning free before you have ever declared or defined them.  This behavior stems from functions being objects within the JavaScript runtime and by extension they have all of the core object methods that JavaScript provides.

The Client and the Burden of the "Nice to Have"

The above preface hints at the subject of this entry.  I am, as of the time of writing this, a software engineering consultant for a particularly overgrown business dominated tech stack for a large consumer service and fleet maintenance corporation in the United States.  While much of my job is putting out small fires in the code, cultivating more complete understanding in more junior engineers, and hunting down business and technical requirements because so often "TBD" is not sufficient to build a product, I am occasionally blessed with an opportunity to sink my teeth into a fun feature.  The client who shall not be named for their sake as well as my own wanted a custom sorting comparator in an Angular data-grid.  This is a simple enough feature request in a vacuum.  The data itself is the usual train wreck one comes to expect from business-first, technology-never clients but the catch is the pitfall of all good intentions.  We have stared down that fantastic generic implementation that business one day wanted a special change in behavior for this one single case.  We have all looked upon horrors that make Java's reflections API seem reasonable and pinched our noses in the name of re-usability.  We have all lived to regret ever creating such an elegant solution and shirking the ever-wise practice of Geordi time estimates a la Star Trek only to find a determined estimate that is unreal when small changes in user-visible behavior mean huge changes to how the code under the hood works.  The client wants a single column on a single view to sort using a multiple case conditional and a regular expression for parsing instead of string comparison functions.  The code-base in question is shared by multiple teams and packing your portion's of the business demands into a nasty series of conditionals in the user interface code is not going to get approval anytime soon and more importantly maybe it should not be approved.  There is rarely a true "one and done" case.  Before long the generic solution has all the nasty edges and trappings of a generic data-grid supplier and all the headaches of hard-coded formatting and sorting rules if one assumes that special cases are a one time happening.  There are a few painful solutions:

  • Write a DSL and parser/handler system for arbitrary sorting rules to provide to the user interface
  • Ask the client why a string needs such complex sorting and parsing in a data-grid when neither the underlying database nor any disconnected utilities such as a spreadsheet application could easily replicate the behavioral outcome
  • Write a JavaScript serialization/de-serialization workflow to pass executable code from the back-end service layer to the user interface

The first two options are obviously far more correct than the final option.  Passing executable code over an HTTP technology is asking for security issues.  Man-in-the-middle attacks, cross-platform compatibility, and many other issues enter the room as soon as JavaScript is utilizing code it does not see until invoked and troubleshooting errors becomes a headache that few junior engineers will catch because the source code is now disjoint from the production assembly.  That all being said, I went ahead and implemented the third option.☺  Rarely do I get a chance to push back on the absurdity of the technical nature of the request with this client and I find little support beyond "take it or leave it" ultimatums within my current employer or my advocate between myself and the client.

What is an Eval?

A miserable pile of risks.  But enough jokes, this is how it is done.  Enter the ECMAScript specification for Functions [here].  By creating a function via the Function constructor, strings can become executable.  The only issue is that syntax highlighting and validation is nice.  I wanted to make sure that I did not get called back in on a Friday evening because someone pushed bad code as block text so I decided that the back-end service layer would hold these custom comparator functions as a proper function.  From here, the idea of functions being objects becomes key.  The "toString" member method of the comparator function-object is invoked and transmitted to the user interface where it is captured and transformed into a proper function again.  Given that functions cannot be parsed back together, I utilized the Function constructor with an internal wrapper to extract the serialized-as-string comparator function.

>> var func1String = func1.toString();
>> var func2 = new Function("return "+func1String)();
>> func2("John");
Hello, John

As shown above, func2 is the result of creating a new function which returns our serialized function.  As a small benefit to this approach over traditional eval-driven methodologies, the Function constructor only has access to the global scope and at least according to the Mozilla documentation is more performant.  All the same, this is terrible and should be avoided.  I enjoyed exploring the rarely used Function constructor and hope this proves useful to someone in the future.

-- Devin

Tagged with:

javascriptlong postwork

Edit 🔒