TypeScript: promesas

Posted by in Javascript y TypeScript

Hace unos cuantos meses, publiqué una entrada que estuvo dedicada a las promesas de Javascript, pero en ningún caso se escribió código. En TypeScript también es posible trabajar con las promesas, pero para ello, nada mejor cómo ver un primer ejemplo y comprobar para qué sirven las promesas.

Ejemplo de Javascript

El primer ejemplo en puro Javascript consiste en lanzar un evento a los 4 segundos de terminar la carga nos notifique un primer evento, y al concluir éste, se produzca un segundo evento 4 segundos después.


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Manejando datos - Promisas</title>
</head>
<body>
<h1>Manejando datos - Promisas</h1>
<div class="completion">Esperando</div>
<script>
function wait(ms) {
return new Promise( function (resolve) {
window.setTimeout(function() { resolve()}, ms)    })        };

var milliseconds = 2000;
wait(milliseconds).then(finish).then(finish2);

function finish() {    var completion = document.querySelector('.completion');    completion.innerHTML = "Completado tras " + milliseconds + "ms.";
};
function finish2() { window.setTimeout(function() {     milliseconds *= 2;    finish();    }, milliseconds)        }
</script>
</body>
</html>

Al ejecutar el código, verá la siguiente secuencia:

1 2 3
Esperando 2 segundos 4 segundos

Al principio pone que hay que esperar, después llega la primera promesa, y luego la segunda.

Promesas y TypeScript 1.x

El asunto de las promesas se complica en TypeScript (version 1.x, que en TypeScript 2.x es distinto) si usamos ES5, que es lo que yo estoy usando, porque el objeto Promise no está disponible.

Si reestructuramos el código de Javascript y preparamos el proyecto para TypeScript, puedes comprobar cómo Promise es un objeto desconocido para TypeScript.

Sin embargo, nada cómo bucear en Stack para encontrar una buena solución. Cómo era de esperar, la respuesta pasa por incorporar un proyecto de Promesas: https://github.com/pragmatrix/Promise. Cómo no me funcionaba tal cual estaba escrito, decidí copiar todo y arreglar lo que me fallaba, para integrarlo con mi plantilla de TypeScript, así creé una interfaz y la correspondiente clase.


/// <reference path="imipromise.ts" />


class PromiseI<Value> implements Promise<Value>
{
constructor(public deferred: DeferredI<Value>)
{ }

get status(): Status { return this.deferred.status; }
get result(): Value { return this.deferred.result; }
get error(): Rejection { return this.deferred.error; }

done(f: (v: Value) => void ): Promise<Value> {
this.deferred.done(f);
return this;
}

fail(f: (err: Rejection) => void ): Promise<Value> {
this.deferred.fail(f);
return this;
}

always(f: (v?: Value, err?: Rejection) => void ): Promise<Value> {
this.deferred.always(f);
return this;
}

then<T2>(f: (v: Value) => any): Promise<T2>
{
return this.deferred.then<any>(f);
}
}

class DeferredI<Value> implements Deferred<Value>{

private _resolved: (v: Value) => void = _ => { };
private _rejected: (err: Rejection) => void = _ => { };

private _status: Status = Status.Unfulfilled;
private _result: Value;
private _error: Rejection = { message: "" };
private _promise: Promise<Value>;

constructor() {
this._promise = new PromiseI<Value>(this);
}

promise(): Promise<Value> {
return this._promise;
}

get status(): Status {
return this._status;
}

get result(): Value {
if (this._status != Status.Resolved)
throw new Error("Promise: result not available");
return this._result;
}

get error(): Rejection {
if (this._status != Status.Rejected)
throw new Error("Promise: rejection reason not available");
return this._error;
}
defer<Value>(): Deferred<Value>
{
return new DeferredI<Value>();
}
then<Result>(f: (v: Value) => any): Promise<Result>
{
var d = this.defer<Result>();

this
.done(v =>
{
var promiseOrValue = f(v);

// todo: need to find another way to check if r is really of interface
// type Promise<any>, otherwise we would not support other
// implementations here.
if (promiseOrValue instanceof PromiseI)
{
var p = <Promise<Result>> promiseOrValue;
p.done(v2 => d.resolve(v2))
.fail(err => d.reject(err));
return p;
}

d.resolve(promiseOrValue);
} )
.fail(err => d.reject(err));

return d.promise();
}

done(f: (v: Value) => void ): Deferred<Value>
{
if (this.status === Status.Resolved) {
f(this._result);
return this;
}

if (this.status !== Status.Unfulfilled)
return this;

var prev = this._resolved;
this._resolved = v => { prev(v); f(v); }

return this;
}

fail(f: (err: Rejection) => void ): Deferred<Value>
{
if (this.status === Status.Rejected) {
f(this._error);
return this;
}

if (this.status !== Status.Unfulfilled)
return this;

var prev = this._rejected;
this._rejected = e => { prev(e); f(e); }

return this;
}

always(f: (v?: Value, err?: Rejection) => void ): Deferred<Value>
{
this
.done(v => f(v))
.fail(err => f(null, err));

return this;
}

resolve(result: Value) {
if (this._status !== Status.Unfulfilled)
throw new Error("tried to resolve a fulfilled promise");

this._result = result;
this._status = Status.Resolved;
this._resolved(result);

this.detach();
return this;
}

reject(err: Rejection) {
if (this._status !== Status.Unfulfilled)
throw new Error("tried to reject a fulfilled promise");

this._error = err;
this._status = Status.Rejected;
this._rejected(err);

this.detach();
return this;
}

private detach()
{
this._resolved = _ => { };
this._rejected = _ => { };
}
}

class IteratorI<E> implements Iterator<E>
{
current: E = undefined;

constructor(private f: () => Promise<E>)
{ }

advance() : Promise<boolean>
{
var res = this.f();
return res.then(value =>
{
if (isUndefined(value))
return false;

this.current = value;
return true;
} );
}
}

function isUndefined(v)    {    return typeof v === 'undefined';    }

class P  {
contructor() {}

iterator<E>(f: () => Promise<E>): Iterator<E>
{
return new IteratorI<E>(f);
}

generator<E>(g: () => () => Promise<E>): Generator<E>
{
return () => this.iterator<E>(g());
};
defer<Value>(): Deferred<Value>
{
return new DeferredI<Value>();
}

resolve<Value>(v: Value): Promise<Value>
{
return this.defer<Value>().resolve(v).promise();
}
reject<Value>(err: Rejection): Promise<Value>
{
return this.defer<Value>().reject(err).promise();
}
each<E>(gen: Generator<E>, f: (e: E) => void ): Promise<{}>
{
var d = this.defer();
this.eachCore(d, gen(), f);
return d.promise();
}
eachCore<E>(fin: Deferred<{}>, it: Iterator<E>, f: (e: E) => void ) : void
{
it.advance()
.done(hasValue =>
{
if (!hasValue)
{
fin.resolve({});
return;
}

f(it.current)
this.eachCore<E>(fin, it, f);
} )
.fail(err => fin.reject(err));
}

unfold<Seed, Element>(
unspool: (current: Seed) => { promise: Promise<Element>; next?: Seed },
seed: Seed)
: Promise<Element[]>
{
var d = this.defer<Element[]>();
var elements: Element[] = new Array<Element>();

this.unfoldCore<Seed, Element>(elements, d, unspool, seed)

return d.promise();
}
unfoldCore<Seed, Element>(
elements: Element[],
deferred: Deferred<Element[]>,
unspool: (current: Seed) => { promise: Promise<Element>; next?: Seed },
seed: Seed): void
{
var result = unspool(seed);
if (!result) {
deferred.resolve(elements);
return;
}

// fastpath: don't waste stack space if promise resolves immediately.

while (result.next && result.promise.status == Status.Resolved)
{
elements.push(result.promise.result);
result = unspool(result.next);
if (!result) {
deferred.resolve(elements);
return;
}
}

result.promise
.done(v =>
{
elements.push(v);
if (!result.next)
deferred.resolve(elements);
else
this.unfoldCore<Seed, Element>(elements, deferred, unspool, result.next);
} )
.fail(e =>
{
deferred.reject(e);
} );
}


when(...promises: Promise<any>[]): Promise<any[]>
{
var allDone = this.defer<any[]>();
if (!promises.length) {
allDone.resolve([]);
return allDone.promise();
}

var resolved = 0;
var results = [];

promises.forEach((p, i) => {
p
.done(v => {
results[i] = v;
++resolved;
if (resolved === promises.length && allDone.status !== Status.Rejected)
allDone.resolve(results);
} )
.fail(e => {
if (allDone.status !== Status.Rejected)
allDone.reject(new Error("when: one or more promises were rejected"));
} );
} );

return allDone.promise();
}
}

export = P;

Al principio del código, se hace una referencia a las interfaces utilizadas, que son:


enum Status {
Unfulfilled,
Rejected,
Resolved
}

interface Rejection
{
message: string;
}
interface PromiseState<Value>
{
/// The current status of the promise.
status: Status;

/// If the promise got resolved, the result of the promise.
result?: Value;

/// If the promise got rejected, the rejection message.
error?: Rejection;
}
interface Promise<Value> extends PromiseState<Value>
{
/**
Returns a promise that represents a promise chain that consists of this
promise and the promise that is returned by the function provided.
The function receives the value of this promise as soon it is resolved.

If this promise fails, the function is never called and the returned promise
will also fail.
*/
then<T2>(f: (v: Value) => Promise<T2>): Promise<T2>;
then<T2>(f: (v: Value) => T2): Promise<T2>;

/// Add a handler that is called when the promise gets resolved.
done(f: (v: Value) => void ): Promise<Value>;
/// Add a handler that is called when the promise gets rejected.
fail(f: (err: Rejection) => void ): Promise<Value>;
/// Add a handler that is called when the promise gets fulfilled (either resolved or rejected).
always(f: (v?: Value, err?: Rejection) => void ): Promise<Value>;
}
interface Deferred<Value> extends PromiseState<Value>
{
/// Returns the encapsulated promise of this deferred instance.
/// The returned promise supports composition but removes the ability to resolve or reject
/// the promise.
promise(): Promise<Value>;

/// Resolve the promise.
resolve(result: Value): Deferred<Value>;
/// Reject the promise.
reject(err: Rejection): Deferred<Value>;

done(f: (v: Value) => void ): Deferred<Value>;
fail(f: (err: Rejection) => void ): Deferred<Value>;
always(f: (v?: Value, err?: Rejection) => void ): Deferred<Value>;
}

interface Generator<E>
{        (): Iterator<E>;    }

interface Iterator<E>
{
advance(): Promise<boolean>;
current: E;
}

Naturalmente, hay que modificar la configuración de RequireJS para notificarle que queremos usar iPromise:


/// <reference path="./typings/require.d.ts" />

/**
* Application configuration declaration.
*/
require.config({

baseUrl: 'ts/',

paths: {
//main libraries
"jquery": '../js/jquery',
"bootstrap": '../js/bootstrap.min',
"imipromise": 'imipromise', // Promise Status Enum
//shortcut paths
templates: '../templates',

//require plugins
text: '../typings/require/text',
tpl: '../typings/require/tpl',
json: '../typings/require/json',
hbs: '../typings/require-handlebars-plugin/hbs'
},

shim: {
"jquery": {
exports: '$'
},
"bootstrap": {
deps: ['jquery']
},
"Handlebars": {
exports: 'Handlebars'
}
}
});

require(["app"]);

Y preparar app.ts para usarlo:

/// <reference path="../typings/jquery.d.ts" />
/// <reference path="../typings/require.d.ts" />
/// <reference path="../typings/bootstrap.d.ts" />

/// <reference path="menu.ts" />
/// <amd-dependency path="menu.ts" />

require([
    'Menu',
    'jquery',
    'bootstrap',
    "imipromise" // Promise Status Enum
], function (MenuApp, $) {
    'use strict';
    $(function () {
        var menu = new MenuApp();
    });
});

Y ya lo tenemos todo listo. Ahora toca preparar menu.ts para completar el ejemplo:

/// <reference path="../typings/jquery.d.ts" />
/// <reference path="../typings/bootstrap.d.ts" />
/// <reference path="mipromise.ts" />
/// <reference path="imipromise.ts" />

import $ = require("jquery");
import Promise = require("mipromise");

var P = new Promise();
// https://github.com/pragmatrix/Promise
var defer = P.defer;
var when = P.when;


var opciones_app = {
    base: 'div'
}

class MenuApp {
    private debug: boolean = false;
    
    private div_base: JQuery;
    
    constructor() {
        this.div_base = $(opciones_app.base);
        console.log('Plantilla cargada con exito!');
        this.inicializar()
    }
    inicializar() { // Aquí vienen REQUISITOS iniciales
        var that = this;
        var milliseconds = 4000;
        console.log('Iniciando promesa');
        this.div_base.html('Sin promesa!');
        this.wait(milliseconds)
            .then(() => that.promise_done());

    }
    promise_done() {
        console.log('Promesa cumplida!');
        this.div_base.html('Promesa cumplida!');
    }
    wait(ms) { 
        var d = defer<string>();
        this.div_base.html('¿se cumplirá la promesa?');
        window.setTimeout(() => { d.resolve("ok")} , ms);
        return d.promise();
    }
}

export = MenuApp;

Vemos el código en acción:

Promesas en TypeScript

Promesas en TypeScript

El código lo teneis disponible en GitHub. IMPORTANTE: Esta entrada es válida para los que trabajais con TypeScript 1.x, puesto que en la versión 2, más avanzada, el uso de Promesas es una realidad y no hay que andar con «plugins» para disponer de buenas herramientas!

Buen día y happy coding!