Skip to content
This repository has been archived by the owner on Jul 21, 2020. It is now read-only.

Initial Bluefill implementation #1

Merged
merged 2 commits into from
Apr 24, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#### joe made this: http://goel.io/joe
#### node ####
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

*.js
3 changes: 3 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*
!/index.js
!/index.d.ts
45 changes: 45 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
interface Promise<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be overridden when index.ts is being build?
Can we please stick to having a src and a dist folder? 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, I told tsc not to emite declarations.

/**
* Pass a handler that will be called regardless of this promise's fate.
* Returns a new promise chained from this promise. There are special
* semantics for .finally in that the final value cannot be modified
* from the handler.
*
* @see http://bluebirdjs.com/docs/api/finally.html
*/
finally(handler: (result?: T, error?: Error) => any): Promise<T>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said before, this doesn't conform the bluebird implementation, that's generally fine, just add a note.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yep, wanted to fix that, forgot to


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rm line


/**
* This is an extension to .catch to work more like catch-clauses in
* languages like Java or C#. Instead of manually checking instanceof or
* .name === "SomeError", you may specify a number of error constructors
* which are eligible for this catch handler. The catch handler that is
* first met that has eligible constructors specified, is the one that
* will be called.
*
* @see http://bluebirdjs.com/docs/api/catch.html
*/
catch<E extends Error, U>(errorCls: { new(...args: any[]): E }, onReject: (error: E) => U | PromiseLike<U>): Promise<U | T>;
catch<E extends Error>(errorCls: { new(...args: any[]): E }, onReject: (error: E) => T | PromiseLike<T> | void | PromiseLike<void>): Promise<T>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the error predicate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not supported here, though I considered it. It would require a lot more code / dependencies, the polyfill right now is quite streamlined.


/**
* tap is called with the result of the previously-resolved promise. It
* can inspect the result and run logic, possibly returning a promise,
* but the result will not be modified.
*/
tap(handler: (result: T) => any): Promise<T>;

/**
* Takes an array of items from the previous promise, running the iterator
* function over all of them with maximal concurrency.
*/
map<T, R>(iterator: (item: T, index: number) => R | PromiseLike<R>): Promise<R[]>;
}

interface PromiseConstructor {
/**
* 'static' implementation of `Promise.map`
*/
map<T, R>(items: T[], iterator: (item: T, index: number) => R | PromiseLike<R>): Promise<R[]>;
}
52 changes: 52 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const originalCatch = Promise.prototype.catch;

Object.assign(Promise.prototype, {
finally<T>(this: Promise<T>, handler: (result?: T, error?: Error) => any): Promise<T> {
return this
.then(result => Promise.resolve(handler(result)).then(() => result))
.catch(err => Promise.resolve(handler(undefined, err)).then(() => { throw err; }));
},

catch<T>(this: Promise<T>, errorCls: Function, onReject: (error: any) => T | Promise<T>): Promise<T> {
if (errorCls !== Error && !(errorCls.prototype instanceof Error)) {
return originalCatch.apply(this, arguments);
}

return originalCatch.call(this, (err: Error) => {
if (!(err instanceof errorCls)) {
throw err;
}

return onReject(err);
});
},

tap<T>(this: Promise<T>, handler: (result: T) => any): Promise<T> {
return this.then(result => {
handler(result);
return result;
});
},

map<T, R>(this: Promise<T[]>, iterator: (item: T, index: number) => R | PromiseLike<R>): Promise<R[]> {
return this.then(items => {
if (!Array.isArray(items)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps allow any Iterable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave this a shot but it looks like TypeScript has issues (e.g. microsoft/TypeScript#6842) using iterators when targeting ES5 code. Thanks for the suggestion though!

throw new Error('Expected array of items to Promise.map (via ' +
`bluefill). Got: ${JSON.stringify(items)}`);
}

const promises: (R | PromiseLike<R>)[] = new Array(items.length);
for (let i = 0; i < promises.length; i++) {
promises[i] = iterator(items[i], i);
}

return Promise.all(promises);
});
},
});

Object.assign(Promise, {
map<T, R>(items: T[], iterator: (item: T, index: number) => R | PromiseLike<R>): Promise<R[]> {
return Promise.resolve(items).map(iterator);
},
});
39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "bluefill",
"version": "0.1.0",
"description": "Bluebird-like promise polyfills with TypeScript definition support",
"main": "index.js",
"typings": "index.d.ts",
"scripts": {
"test": "npm run -s clean && mocha --compilers ts:ts-node/register test.ts",
"clean": "rm -f *.js",
"build": "tsc",
"prepublish": "tsc"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WatchBeam/bluefill.git"
},
"keywords": [
"bluebird",
"promise",
"polyfill",
"typescript"
],
"author": "Connor Peet <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/WatchBeam/bluefill/issues"
},
"homepage": "https://github.com/WatchBeam/bluefill#readme",
"devDependencies": {
"@types/chai": "^3.5.1",
"@types/chai-as-promised": "0.0.30",
"@types/mocha": "^2.2.41",
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"mocha": "^3.3.0",
"ts-node": "^3.0.2",
"typescript": "^2.2.2"
}
}
11 changes: 11 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# bluefill

Bluefill is a polyfill designed for browsers and TypeScript to add promise utility methods such as those found in [Bluebird](http://bluebirdjs.com). We do not aim for total compatibility with Bluebird or take pains to ensure extremely high performance, rather we optimize for a small browser package based solely on the Promise implementation found in the browser.

The total package size of bluefill is under 400 bytes. We supply the following methods:

- [`.catch`](http://bluebirdjs.com/docs/api/catch.html) with the ability to filter by Error classes as predicates.
- [`.finally`](http://bluebirdjs.com/docs/api/finally.html)
- [`.map`](http://bluebirdjs.com/docs/api/promise.map.html) (both as a static call and a chainable promise method)
- [`.tap`](http://bluebirdjs.com/docs/api/tap.html)

130 changes: 130 additions & 0 deletions test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { expect, use } from 'chai';
import * as chap from 'chai-as-promised';

use(chap);

import './';

class FooError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, FooError.prototype);
}
}

class BarError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, BarError.prototype);
}
}

const ok = Symbol('promise result passed');

describe('Promise.catch', () => {
it('does not interfere with normal catches', () => {
const err = new FooError();
return expect(
Promise.reject(err).catch(e => {
expect(e).to.equal(err);
return ok;
})
).to.eventually.equal(ok);
});

it('catches by class constructor', () => {
const err = new FooError();
return expect(
Promise.reject(err).catch(FooError, e => {
expect(e).to.equal(err);
return ok;
})
).to.eventually.equal(ok);
});

it('does not catch if the constructor is mismatched', () => {
const err = new FooError();
return expect(Promise.reject(err).catch(BarError, () => ok))
.to.eventually.be.rejectedWith(err);
});

it('catches if the constructor is a parent class', () => {
const err = new FooError();
return expect(Promise.reject(err).catch(Error, () => ok))
.to.eventually.equal(ok);
});
});

describe('Promise.finally', () => {
it('runs when promise is resolved', () => {
let finallyCalled = false;
return expect(
Promise.resolve(ok).finally(() => finallyCalled = true)
)
.to.eventually.equal(ok)
.then(() => expect(finallyCalled).to.be.true);
});

it('runs when promise is rejected', () => {
let finallyCalled = false;
const err = new FooError();
return expect(
Promise.reject(err).finally(() => finallyCalled = true)
)
.to.eventually.rejectedWith(err)
.then(() => expect(finallyCalled).to.be.true);
});
});

describe('Promise.tap', () => {
it('intercepts and does modify result', () => {
let tappedResult: number;
return expect(
Promise.resolve(2).tap(r => {
tappedResult = r;
return r * 2;
})
).to.eventually.equal(2);
});

it('is skipped during a rejection', () => {
const err = new FooError();
return expect(
Promise.reject(err).tap(() => {
throw new Error('should not have been called');
})
).to.eventually.be.rejectedWith(err);
});
});

describe('Promise.map', () => {
it('maps over items where values are returned', () => {
return expect(
Promise.resolve([1, 2, 3]).map((item: number) => item * 2)
).to.eventually.deep.equal([2, 4, 6]);
});

it('maps over items where promises are returned', () => {
return expect(
Promise.resolve([1, 2, 3]).map((item: number) => Promise.resolve(item * 2))
).to.eventually.deep.equal([2, 4, 6]);
});

it('(static) maps over items where values are returned', () => {
return expect(
Promise.map([1, 2, 3], item => item * 2)
).to.eventually.deep.equal([2, 4, 6]);
});

it('(static) maps over items where promises are returned', () => {
return expect(
Promise.map([1, 2, 3], item => Promise.resolve(item * 2))
).to.eventually.deep.equal([2, 4, 6]);
});

it('throws if a non-array is provided', () => {
return expect(
Promise.resolve('wut').map((item: number) => item * 2)
).to.eventually.rejectedWith(/Expected array of items/);
});
});
25 changes: 25 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strictNullChecks": true,
"module": "commonjs",
"target": "es5",
"lib": [
"es5",
"es6"
],
"typeRoots": [
"node_modules/@types"
],
"types": [
"mocha"
]
},
"exclude": [
"node_modules"
]
}