Trabajar con datos tipados de un dispositivo desde una página web

Cada vez hay más integración entre la web y la domática, «internet of things» o «web of things» son ejemplos de ello. Uno de los problemas que te puedes encontrar es que a nivel de programación son dos mundos muy dispares. En los dispositivos electrónicos reina C/C++ y similares, mientras que en la web el más usado es JavaScript. Aunque ambos lenguajes no trabajan directamente uno con otro lo hacen a través de API a veces surge el problema tratar los datos y es que mientras que C tiene un tipado de datos muy específico el de JS es menos concreto. ¿Qué podemos hacer cuando necesitamos un tipado de datos muy concreto que nos permita incluso trabajar a nivel de bits?. Simplemente usar datos tipados en JS.

Datos tipados en JavaScript

Pese a su fama JS tiene más tipos de datos que los conocidos String, boolean, number.

En este caso vamos a centrarnos en los siguientes tipos que podemos usar para imitar los tipos habituales de C/C++. Podemos elegir el tamaño en bits, si es con signo o sin signo incluso la forma en que se comporta cuando hay un desbordamiento. La única pega, solo funciona con arrays lo que nos supone una incomodidad a la hora de trabajar con un único elemento (tendremos que usar un array de tamaño 1).

TipoRangoTamaño (bytes)DescripciónTipo en C/C++
Int8Array -128…12718-bit con signoint8_t
Uint8Array 0…25518-bit sin signouint8_t
Uint8ClampedArray 0…25518-bit sin signo (clamped)uint8_t
Int16Array -32768…32767216-bit entero con signoint16_t
Uint16Array 0…65535216-bit entero sin signouint16_t
Int32Array -2147483648… 2147483647432-bit entero con signoint32_t
Uint32Array 0…4294967295432-bit entero sin signouint32_t
Float32Array 1.2E-38…3.4E38432-bit punto flotantefloat
Float64Array 5E-324…1.8E308864-bit punto flotantedouble
BigInt64Array -2^63…2^63 – 1864-bit entero con signoint64_t
BigUint64Array 0…2^64 – 1864-bit entero sin signouint64_t

Lo valores en punto flotante siguen el estándar IEEE.

Veamos algunos ejemplos de como usarlos y alguna propiedades interesantes (BYTES_PER_ELEMENT, byte que ocupa cada elemento; byteLength, longitud total del array en bytes):

var int8 = new Int8Array(3);
int8[0] = 42;
console.log(int8[0]); //42
console.log(int8.length); //3
console.log(int8.BYTES_PER_ELEMENT); //1
console.log(int8.byteLength); //3 = length * BYTES_PER_ELEMENT

var int16 = new Int16Array(3);
int16[0] = 42;
console.log(int16[0]); //42
console.log(int16.length); //3
console.log(int16.BYTES_PER_ELEMENT); //2
console.log(int16.byteLength); //6 = length * BYTES_PER_ELEMENT

Un dato clamped (acotado) es un dato que cuando se produce un desbordamiento de su valor por arriba o por abajo el dato toma su mayor y menor valor. O más simplemente explicado si intentas asignarle un valor mayor de 255 o menor que 0 se le asignará el valor 255 y 0 respectivamente. Lo datos que no son de tipo clamped simplemente ignoran los bits del desbordamiento. Podemos verlo mejor con un ejemplo:

var uInt8Clamped = new Uint8ClampedArray(1);
var uInt8 = new Uint8Array(1);

uInt8Clamped[0] = 256;
uInt8[0] = 256;
console.log(uInt8Clamped[0]); //255 
console.log(uInt8[0]); //0

uInt8Clamped[0] = 265;
uInt8[0] = 265;
console.log(uInt8Clamped[0]); //255 
console.log(uInt8[0]); //9

ArrayBuffer y Dataview

Aún hay un truco más que permite JS para trabajar con estos datos. Crear un ArrayBuffer que permite reservar un «espacio de memoria» indicando el tamaño del mismo en bytes. Desde un ArrayBuffer no puedes ni leer ni escribir esta memoria, para ello tienes que asignarlo a un array tipado como los uqe hemos visto antes. Puedes asignar el mismo ArrayBuffer a varios arrays tipados que lo compartirán:

var buffer = new ArrayBuffer(2);
var view8 = new Uint8Array(buffer);
var view16 = new Uint16Array(buffer);
view16[0] = 258;
console.log(view16[0]);// 258 -> 00000001 00000010
console.log(view8[0]); // 2 -> 00000010
console.log(view8[1]); // 1 -> 00000001

En el ejemplo se ve como un ArrayBuffer de 2 bytes se puede leer desde un array de 1 elemento de 16 bits o un array de un array de 2 elementos de 8 bits y como los valores se solapan.

Hay otra forma de hacerlo, con un DataView. Las ventajas de los DataView son dos:

  • Permiten escribir y leer en el ArrayBuffer con cualquier tipo de datos usando el métodoset y get correspondiente getUint8(), setUint8(), getInt8(), setInt8(), getUint16(), setUint16(), …
  • El orden en que se recuperan los bytes es más intuitivo

El ejemplo anterior con DataView

const buffer = new ArrayBuffer(2);
var view = new DataView(buffer, 0);
view.setUint16(0, 258); // (max unsigned 16-bit integer)
console.log(view.getUint16(0));// 258 -> 00000001 00000010
console.log(view.getUint8(0)); // 1 -> 00000001
console.log(view.getUint8(1)); // 2 -> 00000010

Lanzar tareas en una web cuando el navegador está desocupado. RequestIdleCallback

Un problema típico cuando desarrollas webs con mucho javascript es tener que ejecutar ciertas funciones (precarga, envío de estadísticas, actualizar los datos almacenados en local,…) pero que esta ejecución ocurra cuando la carga de la web haya terminado. Y con carga no me refiero únicamente a que haya terminado de cargar el documentos, si no de inicia izar todas las librerías para no afectar al rendimiento de la web. Para ello existe una solución en los navegadores: RequestIdleCallback

RequestIdleCallback lo que hace es encolar las funciones que se le pasan y llamarlas cuando el el navegador tenga poca carga de trabajo, así se mejora el rendimiento de la web y la experiencia de usuario. Su uso es muy sencillo:

requestIdleCallback(nonEssentialFunction);

Donde nonEssentialFunction es la función a la que se llamará cuando el navegador quede inactivo. La función recibe un parámetro un objeto deadline que tiene dos propiedades que podemos usar para planificar la ejecución de nuestro código:

  • timeRemaining(), una función que devuelve cuanto tiempo tenemos para ejecutar nuestro código.
  • didTimeout, que indica si el tiempo fijado como limite para ejecutar nuestra función (más adelante lo veremos) ha expirado. Si su valor es true, timeRemaining() devolverá 0.

Es decir que consultando deadline.timeRemaining() podemos saber si tenemos tiempo suficiente para ejecutar nuestro código. Hay que decir que es un valor orientativo y que si realmente nuestro código tarda más tiempo en terminar no pasara nada.

Veamos el ejemplo más simple, ejecutar una solo función:

function nonEssentialFunction(deadline) {
  task();
}

Si tenemos que realizar varias tareas podemos ir haciéndolas de una en una hasta que se termine el tiempo. na vez terminado el tiempo no se han acabado todas las tareas se puede volver a llamar a requestIdleCallback:

function nonEssentialFunction(deadline) {
    while (deadline.timeRemaining() > 0){ //mientras quede tiempo
        task(n); //ejecutar tarea n
        n--;
    }

    if(n > 0) { //si aun quedan tareas se encola la función otra vez
        requestIdleCallback(nonEssentialFunction);
    }
}

Veamos un ejemplo completo:

<html>
<script>
    function nonEssentialFunction(deadline) {
        while (deadline.timeRemaining() > 0)
            console.log(deadline.timeRemaining());
    }

    //Se prepara la función cuando este libre el procesador
    requestIdleCallback(nonEssentialFunction);
</script>

<body>
</body>

<script>
    //esto se ejecuta antes que nonEssentialFunction
    for(let i = 0; i < 100; i++){
        console.log(i);
    }    
</script>
</html>

Poner un limite de tiempo

Puede ser que no queramos que la función que queremos llamar se retrase demasiado, para ellos podemos pasar otro parámetro que establece el tiempo de expiación, pasado ese tiempo se lanza la ejecución de la función:

requestIdleCallback(nonEssentialFunction, { timeout: 2000 });

El tiempo viene expresado en milisegundos.

Cuando una función es ejecuta porque expira el tiempo fijado en timeout se puede ver en que deadline.didTimeout es true.

function nonEssentialFunction(deadline) {
    while (deadline.timeRemaining() > 0 || deadline.didTimeout )
        console.log(deadline.timeRemaining());
}


requestIdleCallback(nonEssentialFunction, {timeout, 3000});

Como saber si un elemento HTML se ve en pantalla en una web con scroll. IntersectionObserver

Todos los que llevamos un tiempo programando webs nos hemos tenido que enfrentar al scroll. ¿Qué vel el usuario en su pantalla? ¿Por que punto del div (párrafo, imagen,…) está?. Muchas veces la solución es un enrevesado conjunto de cálculos. Por suerte los navegadores han implementado un solución en el javascripr que permite hacerlo fácilmente: IntersectionObserver

Explicado de forma muy resumida, IntersectionObserver invoca a una función cuando el elemento html que le indiquemos se muestre en la pantalla en distintos porcentajes.

Primero veamos como se crea un IntersectionObserver:

let observer = new IntersectionObserver(handleIntersect, options);
observer.observe(element);

handleIntersect es la función a la que llamara el observer. Se le pasa un objeto entries que es un array de objetos entry correspondiente a vrios eventos de intersección asociados (por lo general es solo uno). Cada uno de los objetos entry tiene las siguientes propiedades:

entry.boundingClientRect
entry.intersectionRatio
entry.intersectionRect
entry.isIntersecting
entry.rootBounds
entry.target
entry.time

option es un objeto con tres posibles campos:

  • root: Indica que elemento que es usado como viewport para comprobar la visibilidad de nuestro elemento. Debe ser ancestro del nuestro. Por defecto es el viewport del navegador.
  • rootMargin: Delimita los margenes del elemento root que se ha de tener en cuanta al calcular la intersecciones. Por defecto son cero.
  • threshold: Es un número o un array que indican a que porcentaje de visibilidad se llama a la función asociada al observer. Su valor debe estar definido entre 0 (0%) y 1 (100%)

element es el elemento que el observer «estará vigilando»

Veamos un ejemplo completo:

<html>
<style>
    p {
        font-size: 120px;
        border: 2px solid black;    
    }
</style>

<body>
    <p id="p0">0</p>
    <p id="p1">1</p>
    <p id="p2">2</p>
    <p id="p3">3</p>
    <p id="p4">4</p>
    <p id="p5">5</p>
    <p id="p6">6</p>
    <p id="p7">7</p>
    <p id="p8">8</p>
    <p id="p9">9</p>    
</body>

<script>
    let element = document.getElementById("p5");

    let options = {
        root: null, //document.getElementById()
        rootMargin: '0px',
        threshold: [0, 0.5, 1.0]
    }

    let observer = new IntersectionObserver(handleIntersect, options);
    observer.observe(element);

    function handleIntersect(entries){
        entries.forEach(entry => {
            console.log("Elemento con id "+entry.target.id+"se muestra en pantalla en un porcentaje de: "+(entry.intersectionRatio*100));
        });
    }
</script>
</html>

Generador de código a partir de plantillas

Vamos a ver cómo funciona chicote un generador de código basado en plantillas. Hace tiempo tuve que hacer un trabajo muy repetitivo de migración de código de una tecnología a otra, al final el 90% del trabajo era repetitivo. No tan simple como copiar y pegar, pero nada que requiriera demasiado cerebro. Para esos casos cree chicote. Chicote es el nombre de un famoso chef español. Por eso el uso de términos relativos a la cocina.

Vamos a ver de que consta este sistema.

Motor de plantillas

Lo primero que necesita un generador basado en plantillas es un motor de plantillas que genere texto a partir de una plantilla.

Para este caso opte por mustache.js que es simple, ligero y muy fácil de aprender. Además permite personalizar varios aspectos que necesitaba.

Una de sus principales limitaciones es que no se le puede pasar «parámetros» a las etiquetas, para resolverlo tuve que modificar el código añadiendo esa posibilidad.

Operadores de texto

Otra cosa necesaria para generar código amigable para los seres humanos es poder operar sobre el texto. Es habitual que el mismo nombre se escriba en capital, mayúsculas, minúsculas, separado por guiones,… Los programadores somos unos maniáticos de esto y definimos documentos de estilo solo para especificar este tipo de cosas.

Para esto elegí dos librerías voca.js y pluralize.js y transforme sus operaciones en etiquetas que se puedan usar desde la plantilla.

Además son útiles funciones que actúen a nivel de línea para ordenarlas, limpiar espacios, eliminar líneas duplicadas o tabularlas.

Generador de datos falsos

Los datos falsos con cierta estructura son útiles para generar test, plantillas o demos. En este caso recurrí a la librería faker.js

Algunas ayudas más

Finalmente añadí alguna ayuda más con función habituales en programación como la fecha, la hora, un timestamp, números aleatorios o un contador

Directorios y ficheros

Otra necesidad de los generadores de código programador es poder generar directorios y nombres de archivos con algún tipo de plantilla. Es muy habitual usar la estructura de directorios para organizar el código.

El cocinero (Chicote)

Chicote es un generador agnóstico de código basado en plantillas. Es agnóstico puesto que sirve para cualquier lenguaje que use ficheros de texto. Necesita NodeJS para funcionar. No necesitas instalar nada, sólo descargar el código y ejecutar cooking.js

node cooking.js

La salida se genera en el directorio output

La receta (recipes.json)

Necesitas crear un archivo recipes.json, que contiene una o mas recetas, cada receta contiene todos los ingredientes y pasos para generar código. El único apartado obligatorio en cada receta es steps ,un array con donde se indican que pasos (steps) tiene que ejecutar para realizar la receta. Un step es el nombre de un archivo JSON en el directorio steps.

Ejemplo de fichero recipes.json con solo una receta:

[
{	
    "name": "Test",
	"author": "Bob",
	"fields": [
		{"name": "id", "type": "int"},
		{"name": "firstname", "type": "String"},
		{"name": "lastname", "type": "String"},
		{"name": "birthday", "type": "date"}
	],
	"names": ["HelloWorld", "helloWorld", "hello-world"],
	"steps": ["example"]
}
]

Pasos de la receta (step)

Un step es un archivo JSON en el directorio «steps» donde se preparan los ingredientes. Un paso tiene tres partes: variables (vars), directorios (directories), plantillas (templates)

  • vars son variables creadas a partir de los ingredientes. Actúan de forma similar a los ingredientes.
  • directories indica los directorios de salida.
  • plantillas archivos de texto en el directorio de plantillas. Se compone de un array de dos cadenas de texto, la primera indica la ruta de la plantilla en el directorio templates, la segunda la ruta del fichero generado en el directorio output

Se pueden usar los tags que más adelante veremos para calcular lo valores de estos apartados.

Ejemplo de step:

{
    "vars":[
        ["filename1", "exampleA-{{timestamp}}.code"],
        ["filename2", "exampleB-{{timestamp}}.code"],
        ["className", "{{#capital}}{{name}}{{/capital}}"],
        ["varName", "{{#decapital}}{{name}}{{/decapital}}"],
        ["moduleName", "{{#dash}}{{name}}Module{{/dash}}"]
    ],	
    "directories":[
        "example/example1",
        "example/example2",
        "examples/example1/example2"
    ],
    "templates":[
        ["example/example1.text", "text.txt"],
        ["example/example1.text", "example/example1/{{filename1}}"],
        ["example/example2.text", "example/example2/{{filename2}}"],
        ["example/example1.text", "examples/example1/{{filename1}}"],
        ["example/example2.text", "examples/example1/example2/{{filename2}}"]
    ]
}

El cocinado (templates)

Las plantillas son archivos de texto en el directorio «templates» que utilizan la sintaxis de mustache pero con algunas «etiquetas especiales».

{{timestamp}}  
{{date}} - fecha en formato yyyy-mm-dd
{{time}} - hora en formato hh:mm:ss
{{year}} - año
{{month}} - mes
{{day}} - dia
{{hour}} - hora
{{minute}} - minuto
{{second}} - segundo
{{GUID}}   

{{#plural}}text{{/plural}}  
{{#camel}}text{{/camel}}  
{{#capital}}text{{/capital}}   
{{#decapital}}text{{/decapital}}  
{{#dash}}text{{/dash}}  
{{#snake}}text{{/snake}}  
{{#swap}}text{{/swap}}  
{{#title}}text{{/title}}  
{{#lower}}text{{/lower}}  
{{#upper}}text{{/upper}}  
{{#slug}}text{{/slug}}  
{{#reverse}}text{{/reverse}}  
{{#stripTags}}text{{/stripTags}}  
{{#escHtml}}text{{/escHtml}}  
{{#unHtml}}text{{/unHtml}}  
{{escRegExp}}text{{/escRegExp}}  
{{#trim}}text{{/trim}}  
{{#latin}}text{{/latin}}  

{{#count}}text{{/count}} - reemplaza texto por número de caracteres    
{{#countWords}}text{{/countWords}} - reemplaza el texto por el número de palabras  

{{#delSpaces}}text{{/delSpaces}} - suprimir todos los espacios  
{{#delDuplicateSpaces}}text{{/delDuplicateSpaces}}  - eliminar los espacios duplicados   
{{#delLast|C}}text{{/delLast|C}} - suprimir la última coincidencia de caracteres C  
{{#delFirst|C}}text{{/delFirst|C}} - suprimir la primera coincidencia de caracteres C  
{{#delEnd|N}}text{{/delEnd|N}} - suprimir N caracteres del final  
{{#delStart|N}}text{{/delStart|N}} - del N caracteres desde el inicio  
  
{{#repeat|N}}text{{/repeat2|N}} - repetir el texto N veces  
  
{{#sortAscL}}text{{/sortAscL}} - ordenar las líneas de forma ascendente   
{{#sortDescL}}text{{/sortDescL}} - líneas de ordenación descendente  
{{#naturalSortAscL}}text{{/naturalSortAscL}} - líneas de ordenación natural ascendente  
{{#naturalSortDescL}}text{{/naturalSortAscL}} - líneas de ordenación natural descendente  
{{#shuffleL}}text{{/shuffleL}} - barajar líneas  
{{#trimL}}text{{/trimL}} - recortar líneas  
{{#joinL}}text{{/joinL}} - unir líneas  
{{#removeDuplicateL}}text{{/removeDuplicateL}} - eliminar líneas duplicadas  
{{#spaceL|N}}text{{/spaceL|N}} - Añadir N espacios al principio de cada línea   
{{#tabL|N}}text{{/tabL|N}} - Añadir N tabulaciones al principio de cada línea  
{{#addStartL|C}}text{{/addStartL|C}} - Añadir el carácter C al principio de cada línea   
{{#addEndL|C}}text{{/addEndL|C}} - Añadir el carácter C al final de cada línea  

{{#log}}text{{/log}} - escribir texto en la consola   
{{#eval}}text{{/eval}} - evalúa el texto como código JS  
{{#R}}texto{{/R}} - renderizar texto  
  
{{#C=|N}}{{/C=|N}} - poner el contador en N  
{{#C+|N}}{{/C+|N}} - aumentar el contador en N  
{{#C-|N}}{{/C-|N}} - reducir el contador en N  
{{#C}}{/C}} - imprimir el contador  

{{#K}}texto{{/K}}  - cargar datos de la base de conocimiento (cookbook.json)  
  
{{!text}} - Comentario  

{{#faker}}data{{\faker}} - genera un dato aleatorio usando faker.js

{{=AA BB=}} - Cambia los caracteres para indicar que es un tag de {{ }} to AA BB

Ejemplo de uso de alguno de los tags:

helloWorld
plural: helloWorlds
camel: helloWorld
capital: HelloWorld
decapital: helloWorld
dash: hello-world
snake: hello_world
swap: HELLOwORLD
title: HelloWorld
lower: helloworld
upper: HELLOWORLD
escHtml: helloWorld
slug: hello-world
count: 10
countWords: 2

hello-world
plural: hello-worlds
camel: helloWorld
capital: Hello-world
decapital: hello-world
dash: hello-world
snake: hello_world
swap: HELLO-WORLD
title: Hello-World
lower: hello-world
upper: HELLO-WORLD
escHtml: hello-world
slug: hello-world
count: 11
countWords: 2

El código de Chicote incluye un ejemplo de plantillas.

El libro de cocina (cookbook.json)

Actúa como base de conocimiento para Chicote. Funciona como un sistema de clave valor.

Para leer los datos de la base de conocimiento se usan los tags {{#K}} y {{/K}}. Si tenemos {{#K}}texto{{/K}} texto será reemplazado por el valor que se encuentre en la base de conocimiento con la clave texto

Un ejemplo de base de conocimiento

{	
	"intDefaultValue": " = 0",
	"dateDefaultValue": " = new Date()",
	"stringDefaultValue": " = ''"
}

Ahora veamos un ejemplo de su uso en una plantilla:

var v{{#K}}{{#lower}}{{type}}{{/lower}}DefaultValue{{/K}};

En el ejemplo si type es date dará como resultado:

var v{{#K}}dateDefaultValue{{/K}};

Buscando esa clave en la base de conocimientos:

var v = new Date();

La foto del plato

Como pequeño resumen se puede ver este diagrama que trata de simplificar el funcionamiento de Chicote:

chicote

Bon appetit

Eventos personalizados en javascript

Los eventos fundamentalmente sirven para notificar a otras funciones que están «escuchando» esos eventos  que algo ha ocurrido. En las aplicaciones web estamos acostumbrados a usar eventos asociados a acciones del usuario como pulsar un botón, mover el ratón, hacer click, … Sin embargo podemos crear nuestros eventos asociados a cualquier acción que deseemos.

Para crear un evento solo necesitamos instanciar un objeto CustomEvent con el nombre del evento:

event = new CustomEvent("event");

Si queremos pasar parámetros  tenemos que usar el campo detail:

event = new CustomEvent("event", {
   detail: {
     value: 1
   }
});

El evento debe de ir asociado a un elemento del DOM o al document:

var button = document.getElementById("boton");
button.dispatchEvent(event);

Para «escuchar» un evento usamos addEventListen que asocia el evento a una función que se llama cuando este se dispara:

button.addEventListener("odd", function(e){console.log(e.detail.value);});

La función recibe como parámetro un objeto event con todos los datos del evento. Para acceder a los datos que hemos incluido se puede acceder a valor detail del parámetro.

Veamos un ejemplo completo que convierte un click del ratón en dos eventos uno que se lanza las veces pares y otro las impares:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>
  <button id="boton"> Pulsame </button>
</body>
  
<script>
  
var button = document.getElementById("boton");
var clicks = 0;
button.addEventListener("click", 
function(e) { 
  console.log("click");
  clicks++;
  var event;
  if(clicks % 2){
    event = new CustomEvent("odd", {
    detail: {
      value: clicks
    }
    });   
  } else {
    event = new CustomEvent("even");
  }
  
  button.dispatchEvent(event);
  
});
button.addEventListener("odd", function(e){console.log(e.detail.value);});
button.addEventListener("even", function(e){console.log("even!");});
</script>
</html>

Los eventos permiten una fácil comunicación entre distintas partes de la aplicación. La parte que emite el evento se desvincula de quién está escuchándolo. A su vez los consumidores del evento escuchan o dejan de hacerlo cuando les interesa sin interferir entre ellos.

Memoization con tiempo de vida

La memoization ya vimos lo que era y como usarla. Ahora vamos a añadir una característica más que puede resultar útil en algunos casos: «tiempo de vida». Cada respuesta memorizada solo va a ser válida durante un periodo de tiempo. Pasado ese tiempo el resultado deja de ser válido y si se vuelve a llamar

Todo lo que veamos en este texto esta publicado en la librería memo.js

El primer cambio que vamos a realizar sobre lo que vimos en el anterior post de memoization es que tendremos dos variables para almacenar los datos, una con la cache de los parámetros y sus valores y otra con el momento en que se almacenó esa cache.

La forma de funcionar es la misma que la memoization normal solo que cuando se va recuperar un valor de la cache se comprueba cuanto tiempo hace que se almaceno y si ha transcurrido un tiempo mayor que el tiempo de vida fijado se llama a la función y se actualiza la cache con el valor que devuelva.

Veamos algo de código partiendo de la librería de memoization que creamos.

Lo primero es extender la clase original:

class MemoTime extends Memo {

}

Modificamos el constructor para que cree una variable (lifetime) donde guardar cuando fue la última vez que se actualizo esa clave. También se le tiene que pasar al constructor el tiempo de vida (timeOfLife)

 constructor (func, timeOfLife, keyFunc) {
    super(func, keyFunc);
    this.timeOfLife = timeOfLife || 1000;
    this.lifetime = {};        
 }

También hay que modificar la función que añade una clave – valor para que almacene el momento (new Date()) en que se guarda esa clave.

add(key, value){
    this.lifetime[key] = new Date();
    super.add(key, value);
}

Por último la función que verifica si esta en la cache para que responda ‘false’ en el caso de que este pero haya caducado:

 isInCache(key){
   if(key in this.lifetime) {
      if((new Date() - this.lifetime[key]) < this.timeOfLife){ //¿ha caducado?
         return true;
      }
   } else {
      return false;
   }
}

El resultado final es el siguiente:

class MemoTime extends Memo {
    constructor (func, timeOfLife, keyFunc) {
        super(func, keyFunc);
        this.timeOfLife = timeOfLife || 1000;
        this.lifetime = {};        
    }

    isInCache(key){
        if(key in this.lifetime) {
            if((new Date() - this.lifetime[key]) < this.timeOfLife){
                return true;
            }
        } else {
            return false;
        }
    }

    add(key, value){
        this.lifetime[key] = new Date();
        super.add(key, value);
    }
}

Puede parecer contradictorio tener un mecanismo para almacenar los resultados de una función para no tener que volver a llamarla y que caduquen. Esto es útil cuando el resultado de la función cambia con el tiempo o queremos evitar que a una función se le llame muchas veces seguidas.

Por ejemplo si tenemos un servidor que nos devuelve un dato y no queremos saturarlo de peticiones podemos limitar el número de las mismas usando está técnica. Así cada vez que se llame a la función se tendrá una respuesta pero como mucho se le llamara una vez por cada periodo indicado en el tiempo de vida.

Por último vamos a ver un ejemplo de la clase que hemos creado:

function sum(a,b){  
    console.log("calculating "+a+" + "+b);  
    return a+b;  
}  

let memoTime = new MemoTime(sum, 2000);

console.log(memoTime.call(4,5)); //call sum

console.log(memoTime.call(4,5)); //no call sum
setTimeout(memoTime.call(4,5),1000); //no call sum
setTimeout(memoTime.call(4,5),3000); //call sum