• Search form is empty!

  • routeConfig.js

    routeConfig.js


    http://robertdunaway.github.io

    The Mashup is a learning tool that also serves as a bootstrap project for line-of-business applications.

    https://github.com/MashupJS/MashupJS

    The routeConfig.js file holds routing information for the Mashup.

    Multiple files implementation

    There is one routeConfig.js file per application. This offers a little independence to the developer and application if it is deployed as a mobile application.

    Additionally, for large applications, separating route configurations makes it easy to exclude routes the user doesn’t need, reducing the number of routes the system must interrogate between state changes.
    Applications are defined by each directory in the core/apps directory. 
    Examples:
    App1 - root/core/apps/app1
    App2 - root/core/apps/app2
    Mashup - root/core/apps/mashup

    root/config/rootConfig.js

    This is the initial route configuration file that is always loaded. It contains the default route “/” and more importantly the code for starting up the session.

    The loadCompleted() function is run by every route requiring an authenticated user. It gets user information and puts it into the sessionService, then logs the route for instrumentation and analysis.
    The “getUserInfo()” function may vary from application to application but the pattern is in place and works for anyone who wishes to verify authentication before executing a route.

    The “60” in getUserInfo(60) means the user’s information is cached and that cache doesn’t become stale for 60 minutes. If the user information cache is not stale then the cache is used rather than making a call to the authentication server, AuthApiADSP.
    NOTE: AuthApiADSP is the authentication server used by the Mashup but any custom implementation will work.
    The ADSP means the AuthApi uses Active Directory for authentication and Stored Procedures for data access as opposed to oData or EF.

    Lazy Loading

    Typically a SPA application written with Angular will have many files referenced in the Index.html that load when the application starts. To improve this, many files can be concatenated into one and then minified.

    Lazy Loading takes this one step further. Files are still minified and an application may choose to concatenate its own files but the files are never loaded until the first time they are needed and then they are cached for subsequent loads.

    This release of Angular (1.3) does not support lazy loading but I expect it will soon be available with the work Angular 2.0 is doing and much of that will be done in the router and ported back to Angular 1.3.

    How we are implementing lazy loading, until Angular supports it, is via ocLazyLoad.

    mashupApp.config(function ($routeProvider) {
    
        $routeProvider
    
            .when('/mashupExamples/angularExamplesMain', {
                templateUrl: 'apps/mashupExamples/angularExamplesMain/angularExamplesMain.html',
                controller: 'angularExamplesMainController'
                , resolve: {
                    loadMyCtrl: ['$ocLazyLoad', function ($ocLazyLoad) {
                        return $ocLazyLoad.load({
                            name: 'mashupApp',
                            files: ['apps/mashupExamples/angularExamplesMain/angularExamplesMainController.js']
                        });
                    }]
                 , sessionLoad: function ($route, sessionLoad) { return sessionLoad.loadCompleted(); }
                }
            })
    NOTE: Notice the file property is an array. Here you can link to any and all js files your module needs and if it has already been requested, that will be used and the load never occurs.
    Here is the Network tab in Chrome during the first load of a module and the next image is during the second call for the same module.

    First load


    Second load


    NOTE: There is a bug in ocLazyLoad where you must tell it what modules you’ve already loaded. ocLazyLoad 0.3.9 fixes this for everything except Angular Bootstrap which the mashup uses.
    https://github.com/ocombe/ocLazyLoad/issues/71#issuecomment-61446335
    Added to address the ocLazyLoad issue.
    // ----------------------------------------------------------------------------------------------
    // This configures ocLazyLoadProvider and let's it know that some modules have already been
    // loaded.  Without this the Menu dialog would not work because some directive was loaded twice.
    // https://github.com/ocombe/ocLazyLoad/issues/71
    // ----------------------------------------------------------------------------------------------
    angular.module('mashupApp').config(['$ocLazyLoadProvider', function ($ocLazyLoadProvider) {
    
        $ocLazyLoadProvider.config({
            loadedModules: ['ngRoute', 'ui.bootstrap', 'ngSanitize', 'oc.lazyLoad']
        });
    
    }]);
    

    Resolve

    The resolve function is very powerful and allows you to perform functions required before a route is executed.

    The Mashup makes use of Resolve to verify the user has been authenticated, after which the user session is created.

    Here is the configuration for the /about route.

        $routeProvider
            .when('/about', {
                templateUrl: 'apps/mashup/about/about.html',
                controller: 'aboutController',
                resolve: {
                    loadMyCtrl: ['$ocLazyLoad', function ($ocLazyLoad) {
                        // you can lazy load files for an existing module
                        return $ocLazyLoad.load({
                            name: 'mashupApp',
                            files: ['apps/mashup/about/aboutController.js', 'apps/mashup/~appServices/dataService.js']
                        });
                    }]
                    , sessionLoad: function ($route, sessionLoad) { return sessionLoad.loadCompleted(); }
                }
    
            })
     
    When the /about route is triggered, the templateUrl is set as well as the controller. Then you’ll have a resolve. Everything in the resolve must be complete before the route can execute. In this case we have two functions. First the loadMyCtrl function must complete and then the sessionLoad: function. The session load calls sessionLoad.loadCompleted() function.

    Here is the factory implementation. You’ll see that every attempt is made to short-circuit the load event if it can be determined the user has been detected recently. Slowness in this function will be perceived by the user.
    
    mashupApp.factory('sessionLoad', function ($log, $q, $timeout, $location, $interval, sessionService, mashupDataService, utility) {
    
        var userInfoCacheDuration = 1;
        var userInfoLastChecked = 0;
    
        var logRouteInstrumentation = function () {
            // -------------------------------------------------------------------
            // Instrumenting the application so we can track what pages get used.
            // -------------------------------------------------------------------
            var logObject = utility.getLogObject("Instr", "MashupCoreUI", "sessionLoad", "loadComplete", "UI-Routing", sessionService);
            // Additional or custom properties for logging.
            logObject.absUrl = $location.absUrl();
            logObject.url = $location.url();
            $log.log("UI-Routing to [ " + $location.url() + " ]", logObject);
            // -------------------------------------------------------------------
            // -------------------------------------------------------------------
        };
    
        var loadCompleted = function () {
    
            var defer = $q.defer();
    
            (function () {
                // This controller only loads once when this site with any route is reloaded.
                // For now this is where I'll put code that needs to load once.
    
                // Attempt to shortcircuit call to getUserInfo if it has been called within the cache duration.
                var duration = (new Date().getTime() - userInfoLastChecked) / (1000 * 60); // 1000 is one second and 60 is one minute.
                if (duration > userInfoCacheDuration) {
    
                    mashupDataService.getUserInfo(60).then(function (data) {
    
                        if (data === null || data === undefined || data === '' || data.length === 0) {
    
                            // TODO: add this to a log that is transmitted immediately  will need to implement the log transmission module first.
                            $log.info('The getUserInfo returned an empty array.  Just thought you should know.');
    
                        } else {
                            // saving session information for the rest of the mashup to use.
                            userInfoLastChecked = new Date().getTime();
                            sessionService.setUserSession(data[0]);
                        }
    
                        logRouteInstrumentation();
    
                        // TODO: Might be a good place to put some AuthR code.  Anything we do here needs to also be done at the server.
                        // The client side is to easily manipulated.
                        defer.resolve(true);
                    }),
                        function () {
                            // if userInfoLastChecked is not 0 then the user has been authenticated at some point.
                            // if an error occurs then we should not prevent the user from navigating to the next page.
                            if (userInfoLastChecked) {
                                defer.resolve(true);
                            }
                        };
                } else {
                    // short circuit because we are within the duration threshold
                    logRouteInstrumentation();
                    defer.resolve(true);
                }
            })();
    
            return defer.promise;
        };
    
        return {
    
            // Good jsfiddle on how this works. http://jsfiddle.net/JYvsZ/
    
            loadCompleted: loadCompleted
        };
    });

    WebApi CORS problem

    WebApi CORS problem


    http://robertdunaway.github.io

    The Mashup is a learning tool that also serves as a bootstrap project for line-of-business applications.

    http://mashupjs.github.io

    Following the basic WebApi CORS setup, you will get good results. When the client server and WebApi server know about each other, i.e., the client server is configured in the WebApi server as Access-Control-Allow-Origin, everything works.

    The problem comes into play when the client is not hosted by a server at all as is true with hybrid mobile apps. In this case there isn’t an origin.

    Chrome rejects a wild card on Access-Control-Allow-Origin, making the solution nearly impossible unless you hijack the “Preflight” process and respond to the client with the client added to the Access-Control-Allow-Origin.

    Solution

    Here is the code to make this work.

    using System;
    using System.Web.Http;
    
    namespace AuthApiADSP
    {
        public class WebApiApplication : System.Web.HttpApplication
        {
            protected void Application_Start()
            {
                GlobalConfiguration.Configure(WebApiConfig.Register);
            }
    
            protected void Application_BeginRequest(object sender, EventArgs e)
            {
                if (Context.Request.Path.Contains("api/") && Context.Request.HttpMethod == "OPTIONS")
                {
    
                    Context.Response.AddHeader("Access-Control-Allow-Origin", Context.Request.Headers["Origin"]);
                    Context.Response.AddHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
                    Context.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST PUT, DELETE, OPTIONS");
                    Context.Response.AddHeader("Access-Control-Allow-Credentials", "true");
                    Context.Response.End();
                }
            } 
        }
    }
     
    For completeness, my WebApiConfig.cs looks like this.

    using System.Net.Http.Headers;
    using System.Web.Http;
    using System.Web.Http.Cors;
    
    namespace AuthApiADSP
    {
        public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                // Web API configuration and services
                // Enabling CORS Globally
                var cors = new EnableCorsAttribute("*", "accept,content-type,origin,x-my-header", "*") { SupportsCredentials = true };
                config.EnableCors(cors);
                //config.EnableCors();
    
                // Web API routes
                config.MapHttpAttributeRoutes();
    
                config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional }
                );
    
                // By default Web Api wants to return XML.  (This is confusing because RestFul is based on JSON)
                // This line changes the default return type to JSON.
                config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
    
            }
        }
    }
    

    Background Info

    The challenge is all around CORS and its requirements for ‘Access-Control-Allow-Origin’. The server must maintain a list of allowed server urls and provide that list in the origin header attribute. Chrome looks at this header and if it doesn’t see itself in the list then it self imposes a restriction. This all happens in the preflight before the actual request for data occurs.

    Here is a better description of what is going on:
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS


    Because hybrid applications don’t have a server based origin, your first attempt to address this problem might be a wildcard setting Access-Control-Allow-Origin = “*”.

    This works fine in IE because IE ignores Access-Control-Allow-Origin completely but Chrome and Firefox don’t have it. Also, in Chrome and Firefox GET requests and POST without data all work. It isn’t until a POST has data that a preflight is initiated.

    NOTE: Another good reason to develop in Chrome. Had I developed solely in IE, I would never have discovered this problem until we went into production.

    Another good resource for learning about CORS:
    http://www.html5rocks.com/en/tutorials/cors/#toc-making-a-cors-request

    Ultimately the only solution that addressed all my requirements was hijacking the preflight, “old school,” and returning the client as “origin,” satisfying Chrome and Firefox.

    I plan to update this later. Something about the solution I’ve come up with just doesn’t feel right. It feels too much like a hack. I’m always open to suggestions and new ideas so feel free to email me. robertdunaway@usa.net

    detectService

    detectService

    http://robertdunaway.github.io

    The Mashup is a learning tool that also serves as a bootstrap project for line-of-business applications.

    https://github.com/MashupJS/MashupJS

    The Mashup is home to a service called detectService.

    Introduction

    The detectService tracks the connectivity state of the application preventing unnecessary attempts to call for data and instead encouraging the client to retrieve cached data if available.

    A failed connection attempt to a WebApi takes 3 or 4 seconds to fail. With 3 or 4 WebApi calls required to load a page, this can be punishing to the user.

    The detectService short circuits failed attempts.

    detect()

    detect() is a function the cachedService calls before every attempt to retrieve data from a Restful service.

    The detect() never attempts to detect a remote service but uses any information collected to determine if a connection is available. If no attempt to connect to a particular service has been attempted then the default response is “true,” i.e., the detectService makes a positive assumption.

    When the detect() function is called, the remote service is added to a Heartbeat Monitor where it is periodically checked.

    failed()

    failed() is a function the cache calls when a remote service attempt fails. This tells the detectService that a service has failed and updates its status in case another call to the remote service is attempted. The remote service is added to a Code Blue List and a process begins checking the remote service for a heart beat.

    Internals

    Here are some of the internals and how they work. You shouldn’t have to deal with them but it might be useful to understand how they are intended to work.

    Heartbeat Monitor

    The Heartbeat Monitor tracks connectivity once a resource is accessed. If a connection attempt fails, then the connection is added to the Code Blue Monitor list and if not already started, the Code Blue Monitor starts.

    Successful heartbeats are logged to the console but not to IndexedDB.

    The log is checked every two hours and any logs more than a week old are removed.

    enter image description here

    Every 2 hours the log is checked and any logs over 1 week old are removed.

    Code Blue Monitor

    The Code Blue Monitor checks resources for connectivity much like the Heartbeat Monitor does but more frequently. The results, pass or fail, are recorded to the console window and IndexedDB.


    Battery level

    If available, the battery level is checked. If the battery level is less than or equal to 30%, all monitors slow down.

    logService

    logService

    Credit

    I first learned about the $log service and reasons to use it in Adam Freeman’s, “Pro AngularJS”.

    I stumbled across an easy to understand example and explanation in Vinny Linck’s “AngularJS: How to override $log implementation”: http://vinnylinck.tumblr.com/post/58833687265/angularjs-how-to-override-log-implementation

    The logService is significantly different than the work that kick-started it’s creation but it’s good go give credit whenever possible and these guys did a great job helping me along my way.

    Introduction

    The Mashup is a learning tool that also serves as a bootstrap project for line-of-business applications.
    https://github.com/MashupJS/MashupJS

    The Mashup is home to a service called logService.

    The logService overrides the $log service to extend it’s functionality.
    Overriding $log gives us
    • Console.[log, info, warn, error]
    • Instrumentation
    • Error logging
    • Debugging clients
    • Custom logging actions
    • Mobile friendly resource economy

    Console.[log, info, warn, error]

    Vinny Linck’s article, referenced above, probably explains this better. When overriding the logserviceadelegatetothelog service is provided via delegate.ThisisusedtorestoretheConsoleimplementationoflog.
    $delegate.log(argument);

    Instrumentation

    This implementation of logService decorates a logObject with several user session and environmental variables. Within the logObject itself are subject, app, mod (module), func, and status. Using these properties and calling $log the task of instrumenting applications is easy.

    The routeConfig uses the resolve function of each route to call the logRouteInstrumentation. Each route the user takes is logged to the indexedDB database and can be retrieved to see what parts of the application is used.

        var logRouteInstrumentation = function () {
            // -------------------------------------------------------------------
            // Instrumenting the application so we can track what pages get used.
            // -------------------------------------------------------------------
            var logObject = utility.getLogObject("Instr", "MashupCoreUI", "sessionLoad", "loadComplete", "UI-Routing", sessionService);
            // Additional or custom properties for logging.
            logObject.absUrl = $location.absUrl();
            logObject.url = $location.url();
            $log.log("UI-Routing to [ " + $location.url() + " ]", logObject);
            // -------------------------------------------------------------------
            // -------------------------------------------------------------------
        };
    

    Custom logging actions

    Error and Debug are custom implementations. The CodeBlueMonitor, shown in the code below, is an implementation of custom logging.

    Error logging
    There is, currently, no special implementation for Error logging but this is easily remedied once you’ve decided how you want your errors logged. You can “roll your own” solution or use a Log4net type implementation.

    Using $log to log errors is as simple as getting a logObject as shown above and setting the subject to “Error”.

    Below is the portion of the logService where you determine the action taken by subject.

    var subject = logServiceObj.subject;
    
    switch (subject) {
    
        case "Perf":
            {
    
                break;
            }
        case "HeartBeatFail":
        case "CodeBlueMonitor":
            {
                logDb.put({ name: 'heartbeat' }, logServiceObj);
                break;
            }
        case "Debug":
            {
                // This is a space for doing anything you need to do with debug data.
                // This can be saved to a separate IndexedDB database or table.
                // This can be sent to a WebApi or file.
                // Using the "subject" property you can add any custom behavior you need.
                break;
            }
        case "Error":
            {
                // Do something.
                break;
            }
    }
    
    
    Debugging clients
    Using the same approach as above with the subject set to “Debug”.

    Mobile friendly

    Environment
    The sessionService collects environmental information including the machine type (mobile/desktop), what kind of browser, version of browser, operating system, and where possible battery level.

    Battery level
    The battery level is not supported by all browsers but where supported it is collected, displayed, and used for throttling.

    Throttling
    Using the information collected about the environment certain tasks are throttled.

    When the environment is a mobile device the size the log is reduced in size.

    When the client machine is using a battery the battery’s level of charge determines some functionality. When the battery is 30% or lower then log management significantly decreases and the tolerance for stale data is increased. Reducing the amount of work the client must perform will help preserve battery power until the device can be recharged.

    How to use

    To get the same behavior of logdonothingdifferent.log will operate as before with the addition of the log entry being saved to the indexedDB database logServiceDB in the log table.

    To leverage the logService call the utility.getLogObject function passing in information you would like to see in a log. The object you get back will have the information you passed along with environmental and session information.

    Additionally, any property you add to the returned logObject will be included in logging.
    Example, the logRouteInstrumentation function uses all the standard logObject properties and adds two of it’s own.

        var logRouteInstrumentation = function () {
            // -------------------------------------------------------------------
            // Instrumenting the application so we can track what pages get used.
            // -------------------------------------------------------------------
            var logObject = utility.getLogObject("Instr", "MashupCoreUI", "sessionLoad", "loadComplete", "UI-Routing", sessionService);
            // Additional or custom properties for logging.
            logObject.absUrl = $location.absUrl();
            logObject.url = $location.url();
            $log.log("UI-Routing to [ " + $location.url() + " ]", logObject);
            // -------------------------------------------------------------------
            // -------------------------------------------------------------------
        };
    
    
    Looking at the code above you can see a simple pattern.

    1 . Call the utility.getLogObject function passing in the following:
    - subject
    - app (application name, you might have many applications)
    - mod (module name)
    - func (function name)
    - status (this can be any status value that makes sense. Often set to true/false but not limited.)

    var logObject = utility.getLogObject("Instr", "MashupCoreUI", "sessionLoad", "loadComplete", "UI-Routing", sessionService);
    Here is the interface for getLogObject
    mashupApp.service('utility_LogHelper', function () {
    
        var getLogObject = function (subject, app, mod, func, status, sessionService) {
    
    
    You might have noticed that this function is implemented in utility_LogHelper. To reduce dependency injection into your module a number of utilities, like this, will be exposed by the utility module.

    This implementation is straight forward and described here: https://github.com/MashupJS/mashupjs.docs/blob/master/docs/mashupCore/services/utilityService/utilityService.md

    2 . If you have any properties you’d like to add, do this next.
    logObject.absUrl = $location.absUrl();
    logObject.url = $location.url();
    
    
    3 . Call the $log([message], [log object]);
    $log.log("UI-Routing to [ " + $location.url() + " ]", logObject);

    Loosley coupled

    The goal of the logService is to be loosely coupled. Errors that occur in the logService should not negatively impact the user. Also, adding the logService to an application should be as simple as adding the script to the application. The problem is dependencies so when adding logService be sure to include the dependency modules.

    Removing the logService should have zero impact on an application so in that regard it is loosely coupled.

    sessionService

    sessionService

    http://robertdunaway.github.io 

    The Mashup is a learning tool that also serves as a bootstrap project for line-of-business applications.

    The sessionService shares session information between modules.

    Purpose

    A great way to share information between modules, in Angular, is via services.

    The sessionService provides, to modules, any information they need about the user, environment, and any other session specific data modules might need.

    userSession

    Exposed by the sessionService, the userSession holds whatever information is returned by the AuthN/AuthR system chosen. The expected properties are not defined but rather inherited by whatever system is providing AuthN/AuthR services.

    var userSession = {};
     
    When this user information is retrieved by the application, the setUserSession function is called.

    The sessionService is not tasked with retrieving this information but only holding it for modules to consume.

    envSession

    Environment information is exposed by the sessionService via envSession.

    Retrieving environment information doesn’t require external access to databases or WebApi(s) so the separation of responsibilities is not too badly abused when the sessionService retrieves environmental information on its own.

    envSession is a good candidate for being modularized by a utlity_ module.

    Some user/environment session properties include:
    • browser: “Chrome 38.0.2125.101”
    • deviceType: “desktop”
    • osName: “Windows”
    • battery level
    • battery status
    • userId: “user name”
    • ADDomain
    • AuthenticationType
    • IsAnonymous
    • IsAuthenticated
    • Groups[]
    • Roles[]
    • Privileges[]
    • ActiveStatus
    • FirstName
    • LastName
    • FullName
    • Email
    • CreatedDateTime
    • CreatedBy
    • UpdatedDateTime
    • UpdatedBy
    More properties can be added as needed.

    cacheService

    cacheService


    Mashup’s (cacheService)

    http://robertdunaway.github.io


    I’m still learning Angular and JavaScript, so please feel free to comment below or go to GitHub and log bugs or just make the code better. Thanks!

    https://github.com/MashupJS/MashupJS


    Introduction

    One of the benefits of SPA applications is the decoupling of dependence on external resources. The composition of the view and controller is done locally and page processing, in AngularJS, is local where it once occurred on a web server.

    Taking this decoupling one step further with data independence gives you an offline capability. When offline, the application will continue to function properly while gracefully handling connection drops and re-connects.

    Who is this for?

    If adopting an Offline-First or building an application that must be resilient when connectivity is inconsistent, you need a variety of caching solutions to fit a variety of use case scenarios.

    Use case

    This article covers the use case where the data used is long lived and stable. This data might be a very large dataset that is expensive to retrieve or it might be a list of data used to populate list controls. Any data element that does not change often or is expensive to retrieve will benefit from this particular caching approach. Also, the application must not notice when it’s online or offline.

    Angular-cache-resource (ACR)

    This option, at first glance, seems the obvious choice. Angular-cache-resource is a micro-library whose functionality will be adopted in the next release of Angular version 2.0.

    In a nutshell, this micro-library is a one-to-one replacement for Angular’s $resource. What it adds is a valuable caching model that promotes Progressive Enhancement, the practice of managing the perceived performance by displaying information as it becomes available. Some common practices are placing JavaScript files at the end of an HTML page, allowing the content to render while the application continues to process.

    How ACR works is that each request for data is cached and associated with the URL used to retrieve it. When the same data is requested, ACR will return the cached data to the client immediately and then fetch the data from the source and pass it back to the client. The user sees data immediately and that data is likely unchanged, but if it has been altered, the new changes will appear within moments.

    cacheService

    The cacheService does not address the same use case as ACR and in fact doesn’t try to. When working with cacheService, only one result is returned. Either the cached data is returned or, if the cache is stale, new data is fetched. The benefit here is a round trip to the server is never performed unless the cache becomes stale and this leads to lower network and back-end server utilization.

    Solution Overview

    This solution uses the YDN-DB micro-library to wrap up the complexity of IndexedDB and is a simple service injected into your Angular module, cacheService. Using IndexedDB as the caching source avoids the 5mb limitation most browsers have on LocalStorage. IndexedDB is a complex library with limited built in ability to query. YDN-DB has rich capabilities built on top of IndexedDB and offers a commonly understood SQL interface for accessing data.

    Here are a list of technologies used and links to learn more about them.

    IndexedDB

    http://www.html5rocks.com/en/tutorials/indexeddb/todo/
    http://www.w3.org/TR/IndexedDB/
    http://msdn.microsoft.com/en-us/library/ie/hh673548%28v=vs.85%29.aspx

    YDN-DB

    http://dev.yathit.com/ydn-db/getting-started.html
    http://git.yathit.com/ydn-db/wiki/Home
    https://github.com/yathit/ydn-db

    Angular Services

    https://docs.angularjs.org/guide/services
    http://blog.pluralsight.com/angularjs-step-by-step-services

    Pros and Cons

    When choosing an approach I looked at the pros and cons of a few approaches and technologies.
    Approach Pros Cons
    IndexedDB Very large storage capacity Difficult/cumbersome to work with
    TaffyDB Simple/powerful capabilities
      Referenced by Angular docs as a good model to follow
      No built in persistence model
    TaffyDB + IndexedDB Would allow the power of TaffyDB data manipulation with the storage capacity of IndexedDB Added complexity with IndexedDB
      Little current activity on the library
    LocalStorage Simple to use Limited query ability
      Limited storage capacity
    YDN-DB Current activity on the library Nothing
      Easy to use
      Allows commonly known SQL syntax
      Seamless integration with IndexedDB but falls back on Web SQL and LocalStorage when IndexedDB is not supported

    Implementation Details/How To

    This section walks through the cacheService solution.

    1 . Download the production version of YDN-DB.
    http://dev.yathit.com/ydn-db/downloads.html
    2 . Add the cachedService.js to your project.

    This code can be found in the Mashup. It has a couple dependencies you can choose to include or re-factor out including the utilityService, detectService, and sessionService.
    • utilityService - provides various general functions such as UTC conversions and a logging helper function.
    • detectService - provides detection services so the cacheService doesn’t make unnecessary trips to the WebApi. If the WebApi is unavailable then it takes 3 or 4 seconds to fail. If three or four of these calls are required for a page, then a disconnected status could lead to a 10- or 12-second delay. The detectService prevents this.
    • sessionService - provides common session data shared among all modules and pages.
    These dependencies can be removed and code modified to create a drop in cacheService module.
    Most of what you need to know is well documented in the comments.

    The public function is getData. The caller of this function doesn’t know if that application is online or offline. All the client knows is it will receive data.

    The isCacheStale determines whether or not a call to retrieve data is required. The client passes in “staleMinutes,” which is then used by isCacheStale to make this determination. If you only want the fresh data, you can pass in a staleMinutes value of “0” (zero) which will always return as stale and fetch fresh data. You can also take the opposite approach if you know the cache exists from another part of the application and you do not wish for the freshest data. In this case pass “9999” or some other unreasonable number of stale minutes.

    This is a very simple api that will likely become more complex and rich as more developers use and improve it.

    NOTE: On clearing out old cache on a global scale,when cacheService first runs, it not only defines a set of functions but executes code to check how old the cache is in general. If the cache is too old, as defined by “staleMinutes,” then everything is removed. The default is one week, which is 10,080 minutes, but this can be changed to any amount of time you desire. The first call of each subject after the cache is removed is a stale response and fresh data is fetched.

    3 . Now that we have the cacheService installed we can inject it directly into any component we wish.

    mashupApp.service('cacheService', function ($http, $q, $log, utility, detectService, sessionService) {
     
    4 . Client code for calling the getData method

    mashupApp.service('mashupExamplesItemDataService', function ($http, $q, $log, cacheService) {
    
        return {
            // Directly calling the web api. Not using the mashup cache but can leverage 
            // the caching of angular-cached-resource
    
            getItems1: function () {
                return $http.get("http://localhost:50000/api/MashupExamples/Items/", { withCredentials: true });
            },
    
            getExample2items: function (staleMinutes) {
    
                var cacheName = 'MashupExamples_example2items';
                var schema = { name: cacheName, keyPath: 'id' };
                var webApiUrl = 'http://localhost:50000/api/MashupExamples/Items/';
    
                return cacheService.getData(cacheName, schema, webApiUrl, staleMinutes);
            },
     
    You’ll notice all calls are asynchronous allowing the client to continue working.

    Here you see the $q injected service that provides the deferred, promise, and resolve methods. On the receiving end of this is the “then” function that promises to execute as soon as the deferred promise is resolved.

    $scope.example2items_click = function() {
        mashupExamplesDataService.getExample2items($scope.example2items_cache).then(function(data) {
            $scope.example2items = data;
        });
    };

    The getData() process

    The getData() process

    getData api

    getData: function (cacheName
                        , schema
                        , webApiUrl
                        , staleMinutes
                        , useConvention
                        , heartBeatUrl
                        , heartBeatName)
    cacheName
    The name of the cache you give to the type of data you are retrieving. If the name already exists in the cache and is not yet stale then you receive the cache data and no call to the WebApi will be made.
    webApiUrl
    The webApiUrl includes the full URL and the URL properties.

    Example:
    http://localhost:50004/api/ExampleData/Items/1
    staleMinutes
    The number of minutes until the cache is considered stale. The cache will remain until updated in case the WebApi is unavailable.
    useHeartBeatConvention
    WebApi(s) can offer a HeartBeat function that allows the Mashup to know if the WebApi is available. This might also serve to track system performance.

    The HeartBeatUrl is the endpoint to the HeartBeat.

    You can specify the URL explicitly or use the built in convention.

    The convention is the base URL of the webApiUrl and “/api/HeartBeat/“.
    heartBeatUrl
    The heartBeatUrl that will be used if the useHeartBeatConvention is false.
    heartBeatName
    The name of the heart beat used in logs.

    If nothing is provided then the webApiUrl is used.

    falsy but no heartBeatUrl provided




    This means you did not provide a value for useHeartBeatConvention, which the system interprets as falsy or you explicitly passed in false but did not follow up and provide the heartBeatUrl.

    This is just a warning message but it does indicate the heart beat monitor will not work correctly or at all.

    Notes

    On MahsupJS benefits:

    MashupJS provides the plumbing so that all applications built using the Mashup can take advantage of it. The cacheService is available to all but if another approach is better for your application or for all your applications, that approach can be used instead.

    The benefit of MashupJS is not only the shared plumbing, but also the ability for the plumbing to evolve and improve. When a new version of jQuery or YDN-DB is released it is updated in the MashupJS and all applications benefit. Before this approach it was easy for applications to start diverging and before you know it, you’re supporting three or four versions of the same micro-libraries.

    This caching model is new and with many other caching models available and coming out, this will either be improved or replaced. The goal of the Mashup is to show and host best of breed practices and technology, maybe not on day one but it will eventually mature.

    On async alerts

    You’ll notice commented alert code. For example:

    setTimeout(function () { alert('cache data'); }, 1);

    This is an async approach to raising alerts, allowing your page to finish loading while you test the cache function. Otherwise, the alert() method is synchronous and would halt all execution.

    Tip: Working with YDN-DB async

    When testing or debugging calls to YDN-DB, remember to place log statements inside the “then” function because YDN-DB is async.

    In this example, you’ll notice the log message “SELECT statement start” is nowhere near the select statement. The log function runs immediately, then you’ll notice several other log results and even the “SELECT statement end” long before you see the results of the YDN-DB.

    Example: Here is sample code.



    Here is the console. Notice that the logs do not come in the order you might expect.



    Tip: YDN-DB database name

    Be careful when creating YDN-DB objects. You will not receive an error if you use a name for your object that was already created but you will be confused to find the data that you expected to be there doesn’t exist.

    Tip: The IndexedDB database must be loaded

    It’s very easy to pop in a bit of code to grab data from the local cache. I added a function to retrieve cache metadata. Because this was not my normal process for retrieving cached data, I wrote code to go directly against IndexedDB. As a result I got inconsistent results. When refreshing the screen, I noticed that everything worked about half the time.

    The caching code is well tested and accounts for waiting for the database to become ready.

    So, this tip is to always make sure your local cache database is ready before you access it. We listen for the “onReady” event and set a variable to true. This variable is global and can be used by any module that needs access to cached data.

    var dbCache = new ydn.db.Storage('mashCache');
    // This value "dbCacheReady" allows the rest of the application to know when the database is ready
    // to use. Specifically, cacheService needs this.
    var dbCacheReady = false;
    
    dbCache.onReady(function (e) {
    if (e) {
    
    if (e.target.error) {
      console.log('Error due to: ' + e.target.error.name + ' ' + e.target.error.message);
    }
    throw e;
    
    }
    dbCacheReady = true;
    });
     
    Tip: Looking at the cached data

    You can look at the cached data through the Chrome browser.

    Press F12 – The Chrome developer tools will load.

    Select the “Resources” tab and you’ll see a list of all the available storage mechanisms.

    YDN-DB will use IndexedDB first, if available, before it uses lesser local database options.



    Expand IndexedDB and you’ll see a list of databases you have created for the web site you are currently on.



    Expand this further and you’ll see a list of data sources/tables you have created via the schema or dynamic schema.



    If you select a table, the data is retrieved and displayed in the right hand panel.


    Manipulating Cached Data

    Once data is retrieved and cached, you might still want to manipulate and filter the results further for your specific needs, i.e., you have a list of customers in cache but you need customer number 1,234’s data only.

    Any number of micro-libraries or plain ole JavaScript can be used to work with JSON data stored in the cache. On the ydn-db.html page are several examples of filtering and manipulating returned data using both YDN-DB and Lodash.

    YDN-DB is a simple API used to wrap the complexity of the IndexedDB API. YDN-DB also provides a number of methods to manipulate and filter JSON data including the familiar SQL interface.

    Lodash, a replacement for Underscore, is a powerful API for extending JavaScript and dealing with JSON data.

    Examples on the ynd-db.html page include displaying data on page load, retrieving cached data, using the YDN-DB SQL interface, filter on multiple columns, begins with search, Lodash’s .map, .max, _.min, summaries, totals, and “Like” searches.

    All these examples use the cacheService injected into the module. When the application goes off-line the cacheService continues to provide data as it is still online supporting the Offline-First model.