There are fundamental differences between the roles of application code and framework library modules within an application. Framework libraries provide reusable services that encapsulate lower-level functionality in an abstraction that offers functionality with an easy to use API for higher level code within a variety of different applications. Libraries generally stay static within the development of an application (besides the occasional upgrade). On the other hand, application code utilizes the functionality provided by library modules to create a specific behavior for the logical interaction between users and a domain of data in a generally tighter-defined environment. Application code is completely dynamic within the application development process, it is constantly changing as the application evolves and develops. Recognizing and facilitating these differences is key to intelligently structuring a framework, and is a key principle behind the structure of Persevere.
Application Level
The application level is where abstract functionality is taken to a concrete realization. The application level is also where we focus more on interacting with our data model and define the structure of our information and how our actions affect that data. A common paradigm to use to describe applications is the model-view-controller (MVC) pattern. The model represents our domain-specific information and the encapsulation of the business logic and functionality that adds meaning to the raw stored data. The Persevere server primarily facilitates the model component of MVC applications, allowing the view and controller to be realized as the user interface with a clean separation from the model. Persevere is designed to allow the user interface to run directly in the client/browser for maximum interaction, or in other UI frameworks. Therefore, Persevere’s application model is structured in a way to facilitate adding meaning and functionality to raw data with an object-oriented approach to persistent data. Additionally, JavaScript classes are defined for each table of stored data.
Persevere also embraces the reality of the dynamic nature of the application code. When you are developing an application, the code is constantly changing as you add features, fix bugs, refactor, and make improvements. Many frameworks treat the application as a static set of code, but in Persevere the application code is treated as another set of persisted data. Persevere actually has a meta-class system, with a Class
table, where each instance is one of the table/class definitions. Persevere classes can be modified directly in their corresponding configuration files, or from the HTTP interface. They exist in declarative data structures that support modification with direct persistence back to their source files (as opposed to imperatively created structures that are made as a side-effect of the result of script execution, which retain reversibility in persisting modified objects). Classes also define the schema for data, specifying any desired constraints on the structure and format of the data. The entirety of the class, schema and methods, can be modified dynamically to support maximum efficiency in developing and evolving your application.
Application Libraries
Applications often do consist of more than only model classes, but may need their own library modules to provide reusable functionality that still is conceptually at the application level in the stack. Application libraries can simply be defined in JavaScript files that are placed in the WEB-INF/jslib directory, and they will be executed when Persevere starts up. Library modules are free to add functions to the global scope, or define namespaces for their functions. Persevere also provides a require
function that can define dependencies and loading order amongst libraries, and be used to define a set of modularized exports. For those familiar with Dojo, Persevere’s require
behaves very similar to dojo.require
.
Defining global functions is often considered taboo in JavaScript, but broad generalizations without consideration for the underlying purpose can often be counter-productive. In situations where there exists potential for independent parties to create conflicting names, global functions are indeed hazardous. Namespaces are often used for holding functions, so as to categorize a set of functions by their owner, and this is extremely important when code will be used in different environments in conjunction with unknown modules. However, application-level libraries can enjoy a much more deterministic environment. When developing in libraries with a known environment as a single developer or in a small group, creating namespaces for all functions is often completely excessive and unnecessary. To create a module that sets up a globally available function, we can simply define the global in a module:
// add.js
add = function(a,b){
return a + b;
}
This makes the add() function available to all server-side code in Persevere as a top level function. Global top-level functions are very simple to create and use, and finding ways to reduce complexity where it is not needed should always be welcome on projects. Persevere has the flexibility to allow application libraries to either define functions globally, in namespaces, or encapsulated in a set of exports (that we will look at below), so the most appropriate mechanism can be used at each layer in the application stack.
Framework Libraries
As we continue down the layers in the application stack, we are moving from the concrete application implementation into the abstract reusable functionality realm. Here we must be much more cautious in how functions are exposed. The require
provided by Persevere conforms to the ServerJS API draft specification and allows for safe encapsulated modules that do not touch the global scope at all. Modules are given an exports
object which they can use to assign functions to. The exports object is then returned whenever a require
call is made for that module. Modules are each executed in their own scope (a child of the global scope, so they can still assign to and access the global scope, as mentioned before). Therefore we can create completely encapsulated modules, for example:
// math.js
// multiply is defined as local/private to the module,
// since it is not assigned to a global variable
function multiply(a, b){
return a * b;
}
exports.factorial = function(x){
if(x == 1){
return 1;
}
return multiply(x, exports.factorial(x - 1));
}
We can now use this module with the require
function:
var math = require("math");
math.factorial(5) -> 120
This module has not modified the global scope at all (not even with namespaces). The ServerJS group is working on a standard set of libraries that will also be available as modules and accessible through the require when implemented.
Persevere also provides a means for defining single function/constructor modules (this is an extension not defined in the standard require specification). You can assign a function (or any value) directly to the exports object and that function will be returned by require calls. For example:
// delay.js
exports = function delay(func){
setTimeout(func,100);
}
// caller
var delay = require("delay.js");
delay(function(){
console.log("a little later");
});
In Persevere, all modules are loaded on startup. This allows all module startup code to execute in a single thread to set up the global scope. Once startup is finished, the global scope is frozen to prevent concurrent modification from different threads and open up the possibility of race conditions and non-deterministic behavior. The exports of modules are also frozen for the same reason. In combination with Persevere’s transactional handling of shared persistent data, this allows Persevere to maintain a thread-safe environment for application code, while still allowing multi-threaded request handling. Modules are only loaded once, each require call returns the exports object defined by the module, but the module is not re-loaded or executed.
Leveraging Java Libraries
Further down in the application stack, we have the foundational components that handle lower-level operations. Fortunately, Persevere is built on Rhino, which provides access to Java libraries. There is a virtually limitless selection of Java libraries for almost any type of low-level functionality needed, including sockets, encryption, various IO protocols, file system interaction, and so on. This ability to utilize Java libraries is an extremely powerful benefit of Persevere’s Rhino based infrastructure and opens up an enormous range of components that can be used. Often there is significant legacy functionality that can leveraged from Persevere through the Java bridge as well.
Often these lower-level components deal with the more processor intensive tasks and IO operations which may benefit being written in Java, enjoying the maturity of years of intense optimizations. While there are plenty of varying opinions out there about Java, one thing is relatively undisputed: Java is fast. Very fast. For performance sensitive components, being able to write cycle intense, low level components in a lower-level language like Java, while keeping the majority of application code in high-level JavaScript code can be highly beneficial. For example, suppose we needed a function to calculate prime numbers and we need the best performance possible. We could write the function in Java to maximize the speed:
package org.acme;
public class Prime {
public static long firstPrime(long start){
nextNum: for(long num = start; true; num++){
long numbersToCheck = (long) Math.sqrt(num);
for(int i = 2; i < numbersToCheck; i++){
if(num % i == 0){
continue nextNum;
}
}
return num;
}
}
}
And now we can utilize this from JavaScript, but have the performance benefit of the low-level Java code execution:
org.acme.Prime.firstPrime(100000000000000);
While Persevere is certainly designed for client-server applications to enjoy the consistency of a single language for end-to-end application code, that doesn’t mean that one should necessarily try to fit every layer in the stack into JavaScript. If it makes more sense to use a static lower level language in certain situations, then do so. The Rhino platform gives Persevere developers this freedom, allowing a powerful high-to-low level set of programming paradigms.
Application/Framework Distinction in Persevere
The Persevere server framework makes an intentional effort to create a separation between application code and library modules. Generally, languages and frameworks do not distinguish between the two, even though they can serve very different roles in an application, and have entirely different characteristics in the development cycle. With Persevere, a logical and distinct separation is made to harmonize with the different aspects of libraries and application code, and create an intuitive technological stack across the full spectrum from top to bottom.