Thursday, June 30, 2016

Updating Default Parameters in .NET Assemblies

One of the things that I love about C# is how so many of it's features are just very conveniently designed compiler tricks. This means that, just like any other magic trick, once you know how the trick is performed you immediately realize that there really is nothing magical about it.

So, let's talk about default parameters. They are actually just constant values that get compiled into your code when you go to use a method that has them. Let's look at an example...

The Code

public enum Country
{
    US,
    CA
}
 
public static class SharedUtility
{
    public static bool CanDrink(
        int age, 
        Country country = Country.CA)
    {
        switch (country)
        {
            case Country.US:
                return age >= 21;
 
            case Country.CA:
                return age >= 18;
 
            default:
                throw new ArgumentException(
                    "Invalid Country",
                    nameof(country));
        }
    }
}
 
public class SharedUtilityTests
{
    [Fact]
    public void CanDrink()
    {
        var result = SharedUtility.CanDrink(20);
 
        // The line above will compile into the following:
        // var result = SharedUtility.CanDrink(20, Country.CA);
        // Thus, the Assert below will succeed!
 
        Assert.True(result, "The default was not US!");
    }
}

So, now that you know how the trick is performed, how could this cause a problem for you? Specifically, what happens if you update a library that exposes methods with default parameters?

Nothing will change, until you recompile against the library!

If another library changes their default parameters, but you do not recompile your code against it, then your code will be using the old default values! Let's look back at the previous example and see why this could cause confusion...

Sunday, June 26, 2016

MP3 Playlist for Chromecast

Come to find out, I am a very retro guy when it comes to music. I have these old things that I like to use to play music, you may not have heard of them, they are called MP3s. They are kind of like 8-track tapes, but digital, not in the cloud, and not old enough to be cool yet.

How do you play MP3s via Chromecast?

The official answer is to use Google Play, but that implies that you want to both pay for that service and upload your files to the cloud. The unofficial answer is to drag and drop your MP3s into a Chrome tab and cast that tab, however this does not allow you to create a playlist.

Introducing Playlist for Chromecast

I have created a simple single page HTML 5 application that will act as a playlist for MP3s on your computer. Just download the project and open up release/playlist.html in Chrome, then drag and drop MP3s on to the page.

Development

I had a lot of fun making this, and I'm not done. I intend to use this project as a case study to talk about VSCode, TypeScript, SASS, HTML5 Audio, NPM, and unit testing. For now, I just wanted to start by getting this initial post up, but expect more to follow.

What's next?

  • Update project documentation.
  • Create unit tests.
  • Add theme support.
  • Write blog posts about development.
  • Maybe host it on a domain.
  • Maybe submit it as a Chrome application.

Enjoy,
Tom

Saturday, June 18, 2016

Client Side Caching for jQuery

Updates: 6/26/16

  • Fixed bug where triple equals null check would miss.
  • Added support for data driven cache key.
  • Removed let and const statements (some minifiers were having a hard time with them)

Original:

There is great question on Stack Overflow about caching a jquery ajax response in javascript/browser. Unfortunately, even thought it was a good solution, it did not do quite what I needed it to.

The application I was trying to optimize sometimes made redundant parallel requests, and I needed my caching solution to include a queuing system to prevent duplicate fetches.

Below is a simple solution that uses jQuery.ajaxPrefilter to check a local cache prior to making GET requests. Additionally, it will queue the callback if the request is already in flight. The cache stores the queue in both memory and local storage, ensuring that the cache will persist across page loads.

Implementation

(function ($) {
  "use strict";
 
  var timeout = 60000;
  var cache = {};
 
  $.ajaxPrefilter(onPrefilter);
 
  function onPrefilter(options, originalOptions) {
    if (options.cache !== true) {
      return;
    }
 
    var callback = originalOptions.complete || $.noop;
    var cacheKey = getCacheKey(originalOptions);
 
    options.cache = false;
    options.beforeSend = onBeforeSend;
    options.complete = onComplete;
 
    function onBeforeSend() {
      var cachedItem = tryGet(cacheKey);
 
      if (!!cachedItem) {
        if (cachedItem.data === null) {
          cachedItem.queue.push(callback);
        } else {
          setTimeout(onCacheHit, 0);
        }
 
        return false;
      }
 
      cachedItem = createCachedItem();
      cachedItem.queue.push(callback);
      setCache(cacheKey, cachedItem);
      return true;
 
      function onCacheHit() {
        invoke(callback, cachedItem);
      }
    }
 
    function onComplete(data, textStatus) {
      var cachedItem = tryGet(cacheKey);
 
      if (!!cachedItem) {
        cachedItem.data = data;
        cachedItem.status = textStatus;
        setCache(cacheKey, cachedItem);
 
        var queuedCallback;
        while (!!(queuedCallback = cachedItem.queue.pop())) {
          invoke(queuedCallback, cachedItem);
        }
 
        return;
      }
 
      cachedItem = createCachedItem(data, textStatus);
      setCache(cacheKey, cachedItem);
      invoke(callback, cachedItem);
    }
  }
 
  function tryGet(cacheKey) {
    var cachedItem = cache[cacheKey];
 
    if (!!cachedItem) {
      var diff = new Date().getTime() - cachedItem.created;
 
      if (diff < timeout) {
        return cachedItem;
      }
    }
 
    var item = localStorage.getItem(cacheKey);
 
    if (!!item) {
      cachedItem = JSON.parse(item);
 
      var diff = new Date().getTime() - cachedItem.created;
 
      if (diff < timeout) {
        return cachedItem;
      }
 
      localStorage.removeItem(cacheKey);
    }
 
    return null;
  }
 
  function setCache(cacheKey, cachedItem) {
    cache[cacheKey] = cachedItem;
 
    var clone = createCachedItem(cachedItem.data, cachedItem.status, cachedItem.created);
    var json = JSON.stringify(clone);
    localStorage.setItem(cacheKey, json);
  }
 
  function createCachedItem(data, status, created) {
    return {
      data: data || null,
      status: status,
      created: created || new Date().getTime(),
      queue: []
    };
  }
 
  function invoke(callback, cachedItem) {
    if ($.isFunction(callback)) {
      callback(cachedItem.data, cachedItem.status);
    }
  }
  
  function getCacheKey(originalOptions) {
    if (!!originalOptions.data) {
      return originalOptions.url + "?" + JSON.stringify(originalOptions.data);
    }
 
    return originalOptions.url;
  }
 
})(jQuery);

Enjoy,
Tom

Real Time Web Analytics