From 2f52c596b061f311a0e84278eb35c314d6922a22 Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Tue, 15 Apr 2014 17:15:04 +0300 Subject: [PATCH] promises changed to different (generic) library --- net/http_client.android.ts | 12 +- net/http_client.d.ts | 6 +- net/http_client.ios.ts | 12 +- promises/promises.ts | 497 +++++++++++++++++++++++++++++++------ 4 files changed, 433 insertions(+), 94 deletions(-) diff --git a/net/http_client.android.ts b/net/http_client.android.ts index 685346eb4..c330784ae 100644 --- a/net/http_client.android.ts +++ b/net/http_client.android.ts @@ -10,8 +10,8 @@ export class http { /** * Gets string from url. */ - public static getString(url: string): promises.Promise { - var d = new promises.Deferred(); + public static getString(url: string): promises.Promise { + var d = promises.defer(); http.get(url, r => d.resolve(r), e => d.reject(e)); return d.promise(); } @@ -19,8 +19,8 @@ export class http { /** * Gets JSON from url. */ - public static getJSON(url: string): promises.Promise { - var d = new promises.Deferred(); + public static getJSON(url: string): promises.Promise { + var d = promises.defer(); http.get(url, r => d.resolve(JSON.parse(r)), e => d.reject(e)); return d.promise(); } @@ -28,8 +28,8 @@ export class http { /** * Gets image from url. */ - public static getImage(url: string): promises.Promise { - var d = new promises.Deferred(); + public static getImage(url: string): promises.Promise { + var d = promises.defer(); http.get(url, r => { var image = new image_module.Image(); image.loadFromBitmap(r); diff --git a/net/http_client.d.ts b/net/http_client.d.ts index d726dede1..4ff9da2d0 100644 --- a/net/http_client.d.ts +++ b/net/http_client.d.ts @@ -5,7 +5,7 @@ import image_module = require("Image/image"); import promises = require("promises/promises"); export declare class http { - private static getString(url: string): promises.Promise; - private static getJSON(url: string): promises.Promise; - private static getImage(url: string): promises.Promise; + static getString(url: string): promises.Promise; + static getJSON(url: string): promises.Promise; + static getImage(url: string): promises.Promise; } \ No newline at end of file diff --git a/net/http_client.ios.ts b/net/http_client.ios.ts index 5db1880dd..ea700c402 100644 --- a/net/http_client.ios.ts +++ b/net/http_client.ios.ts @@ -9,8 +9,8 @@ export class http { /** * Gets string from url. */ - public static getString(url : string) : promises.Promise { - var d = new promises.Deferred(); + public static getString(url: string): promises.Promise { + var d = promises.defer(); http.get(url, r => d.resolve(Foundation.NSString.initWithDataEncoding(r, 4).toString()), e => d.reject(e)); return d.promise(); } @@ -18,8 +18,8 @@ export class http { /** * Gets JSON from url. */ - public static getJSON(url: string) : promises.Promise { - var d = new promises.Deferred(); + public static getJSON(url: string): promises.Promise { + var d = promises.defer(); http.get(url, r => d.resolve(JSON.parse(Foundation.NSString.initWithDataEncoding(r, 4).toString())), e => d.reject(e)); return d.promise(); } @@ -27,8 +27,8 @@ export class http { /** * Gets image from url. */ - public static getImage(url: string) : promises.Promise { - var d = new promises.Deferred(); + public static getImage(url: string): promises.Promise { + var d = promises.defer(); http.get(url, r => { var image = new image_module.Image(); image.loadFromData(r); diff --git a/promises/promises.ts b/promises/promises.ts index 756f855d5..a970b87e9 100644 --- a/promises/promises.ts +++ b/promises/promises.ts @@ -1,110 +1,449 @@ -export function when(...promises: Promise[]): Promise { - var all_done = new Deferred(); - var results = []; - var remaining = promises.length; +/** + Module P: Generic Promises for TypeScript - promises.map( - (p, i) => { - p.then( - function (...args: any[]) { - results[i] = args; - remaining--; - if (!remaining && all_done.status() !== 'rejected') { - all_done.resolve.apply(all_done, results); - } - }, - function () { all_done.reject() } - ); + Project, documentation, and license: https://github.com/pragmatrix/Promise +*/ + + + +/** + Returns a new "Deferred" value that may be resolved or rejected. +*/ + +export function defer(): Deferred { + return new DeferredI(); +} + +/** + Converts a value to a resolved promise. +*/ + +export function resolve(v: Value): Promise { + return defer().resolve(v).promise(); +} + +/** + Returns a rejected promise. +*/ + +export function reject(err: Rejection): Promise { + return defer().reject(err).promise(); +} + +/** + http://en.wikipedia.org/wiki/Anamorphism + + Given a seed value, unfold calls the unspool function, waits for the returned promise to be resolved, and then + calls it again if a next seed value was returned. + + All the values of all promise results are collected into the resulting promise which is resolved as soon + the last generated element value is resolved. +*/ + +export function unfold( + unspool: (current: Seed) => { promise: Promise; next?: Seed }, + seed: Seed) + : Promise { + var d = defer(); + var elements: Element[] = new Array(); + + unfoldCore(elements, d, unspool, seed) + + return d.promise(); +} + +function unfoldCore( + elements: Element[], + deferred: Deferred, + unspool: (current: Seed) => { promise: Promise; 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; } - ); - - if (!remaining) { - all_done.resolve.apply(all_done, results); } - return all_done.promise(); + result.promise + .done(v => { + elements.push(v); + if (!result.next) + deferred.resolve(elements); + else + unfoldCore(elements, deferred, unspool, result.next); + }) + .fail(e => { + deferred.reject(e); + }); } -export class Promise { +/** + The status of a Promise. Initially a Promise is Unfulfilled and may + change to Rejected or Resolved. + + Once a promise is either Rejected or Resolved, it can not change its + status anymore. +*/ - constructor(private deferred: Deferred) { } +export enum Status { + Unfulfilled, + Rejected, + Resolved +} - then(callback: Function, error?: Function): Promise { - return this.deferred.then(callback, error); +/** + If a promise gets rejected, at least a message that indicates the error or + reason for the rejection must be provided. +*/ + +export interface Rejection { + message: string; +} + +/** + Both Promise and Deferred share these properties. +*/ + +export interface PromiseState { + /// 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; +} + +/** + A Promise supports basic composition and registration of handlers that are called when the + promise is fulfilled. + + When multiple handlers are registered with done(), fail(), or always(), they are called in the + same order. +*/ + +export interface Promise extends PromiseState { + /** + 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(f: (v: Value) => Promise): Promise; + then(f: (v: Value) => T2): Promise; + + /// Add a handler that is called when the promise gets resolved. + done(f: (v: Value) => void): Promise; + /// Add a handler that is called when the promise gets rejected. + fail(f: (err: Rejection) => void): Promise; + /// Add a handler that is called when the promise gets fulfilled (either resolved or rejected). + always(f: (v?: Value, err?: Rejection) => void): Promise; +} + +/** + Deferred supports the explicit resolving and rejecting of the + promise and the registration of fulfillment handlers. + + A Deferred should be only visible to the function that initially sets up + an asynchronous process. Callers of that function should only see the Promise that + is returned by promise(). +*/ + +export interface Deferred extends PromiseState { + /// 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; + + /// Resolve the promise. + resolve(result: Value): Deferred; + /// Reject the promise. + reject(err: Rejection): Deferred; + + done(f: (v: Value) => void): Deferred; + fail(f: (err: Rejection) => void): Deferred; + always(f: (v?: Value, err?: Rejection) => void): Deferred; +} + +/** + Creates a promise that gets resolved when all the promises in the argument list get resolved. + As soon one of the arguments gets rejected, the resulting promise gets rejected. + If no promises were provided, the resulting promise is immediately resolved. +*/ + +export function when(...promises: Promise[]): Promise { + var allDone = defer(); + if (!promises.length) { + allDone.resolve([]); + return allDone.promise(); } - status(): string { return this.deferred.status() } - result(): any[] { return this.deferred.result() } + 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 class Deferred { +/** + Implementation of a promise. - private resolved: Function[] = []; - private rejected: Function[] = []; - private _status: string; - private _result: any[]; - private _promise: Promise; + The Promise instance is a proxy to the Deferred instance. +*/ + +class PromiseI implements Promise +{ + constructor(public deferred: DeferredI) + { } + + 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 { + this.deferred.done(f); + return this; + } + + fail(f: (err: Rejection) => void): Promise { + this.deferred.fail(f); + return this; + } + + always(f: (v?: Value, err?: Rejection) => void): Promise { + this.deferred.always(f); + return this; + } + + then(f: (v: Value) => any): Promise { + return this.deferred.then(f); + } +} + +/** + Implementation of a deferred. +*/ + +class DeferredI implements Deferred{ + + 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; constructor() { - this._promise = new Promise(this); - this._status = 'in progress'; + this._promise = new PromiseI(this); } - promise(): Promise { return this._promise } - status(): string { return this._status } - result(): any[] { return this._result } - - resolve(...result: any[]): Deferred { - this._result = result; - this.notify(this.resolved, result); - this.resolved = []; - this._status = 'resolved'; - return this; + promise(): Promise { + return this._promise; } - reject(...result: any[]): Deferred { - this._result = result; - this.notify(this.rejected, result); - this.rejected = []; - this._status = 'rejected'; - return this; + get status(): Status { + return this._status; } - then(callback: Function, error: Function): Promise { - var d = new Deferred(); + get result(): Value { + if (this._status != Status.Resolved) + throw new Error("Promise: result not available"); + return this._result; + } - this.resolved.push(this.wrap(d, callback, 'resolve')); + get error(): Rejection { + if (this._status != Status.Rejected) + throw new Error("Promise: rejection reason not available"); + return this._error; + } - if (error) { - this.rejected.push(this.wrap(d, error, 'reject')); - } + then(f: (v: Value) => any): Promise { + var d = defer(); - if (this._status === 'resolved') { - this.resolve.apply(this, this.result); - } - else if (this._status === 'rejected' && error) { - this.reject.apply(this, this.result); - } + this + .done(v => { + var promiseOrValue = f(v); + + // todo: need to find another way to check if r is really of interface + // type Promise, otherwise we would not support other + // implementations here. + if (promiseOrValue instanceof PromiseI) { + var p = > 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(); } - private wrap(d: Deferred, f: Function, method: string): Function { - return function (...args: any[]) { - var result = f.apply(f, args); - if (result && result instanceof Promise) { - result.then( - function () { d.resolve() }, - function () { d.reject() } - ); - } - else { - d[method].apply(d, typeof result === 'array' ? result : [result]); - } - } + done(f: (v: Value) => void): Deferred { + if (this.status === Status.Resolved) { + f(this._result); + return this; } - private notify(funcs: Function[], result: any[]): void { - funcs.map((f) => { f.apply(f, result) }); + if (this.status !== Status.Unfulfilled) + return this; + + var prev = this._resolved; + this._resolved = v => { prev(v); f(v); } + + return this; } -} \ No newline at end of file + + fail(f: (err: Rejection) => void): Deferred { + 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 { + 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 = _ => { }; + } +} + +/** + Promise Generators and Iterators. +*/ + +export interface Generator { + (): Iterator; +} + +export interface Iterator { + advance(): Promise; + current: E; +} + +export function generator(g: () => () => Promise): Generator { + return () => iterator(g()); +}; + +export function iterator(f: () => Promise): Iterator { + return new IteratorI(f); +} + +class IteratorI implements Iterator +{ + current: E = undefined; + + constructor(private f: () => Promise) + { } + + advance(): Promise { + var res = this.f(); + return res.then(value => { + if (isUndefined(value)) + return false; + + this.current = value; + return true; + }); + } +} + +/** + Iterator functions. +*/ + +export function each(gen: Generator, f: (e: E) => void): Promise<{}> { + var d = defer(); + eachCore(d, gen(), f); + return d.promise(); +} + +function eachCore(fin: Deferred<{}>, it: Iterator, f: (e: E) => void): void { + it.advance() + .done(hasValue => { + if (!hasValue) { + fin.resolve({}); + return; + } + + f(it.current) + eachCore(fin, it, f); + }) + .fail(err => fin.reject(err)); +} + +/** + std +*/ + +export function isUndefined(v) { + return typeof v === 'undefined'; +}