Last active
March 13, 2018 07:54
-
-
Save dead-claudia/2563c9dcf8b19bc2875e5cfb3d7709ad to your computer and use it in GitHub Desktop.
Simpler, better Promise library
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type PollResult<T> = | |
| {type: "pending"} | |
| {type: "resolved", value: T} | |
| {type: "rejected", value: Error} | |
| {type: "cancelled"} | |
interface Receiver<T> { | |
resolve?(value: T): any | |
reject?(value: Error): any | |
} | |
export class Promise<T> { | |
constructor(future: Future<T>) | |
readonly started: boolean | |
poll(): PollResult<T> | |
force(): void | |
// Note: this isn't your normal Fantasy Land `chain` | |
chain<U>(factory: (handle: Future<U>) => Receiver<T>): Promise<U> | |
cancel(): void | |
} | |
export class Future<T> { | |
constructor(func?: (handle: this) => any) | |
// In case the class takes something else. This mirrors the constructor. | |
static readonly lazy: this extends new (...args: infer I) => Future<T> | |
? (this: this, ...args: I) => Promise<T> | |
: never | |
static resolve<T>(value: T): Promise<T> | |
static reject(value: Error): Promise<never> | |
static cancel(): Promise<never> | |
static readonly [Symbol.species]: this | |
readonly promise: Promise<T> | |
// Convenience | |
readonly handle: this | |
readonly started: boolean | |
poll(): PollResult<T> | |
force(): void | |
resolve(value: T): void | |
reject(value: Error): void | |
cancel(): void | |
} | |
export class CancellableFuture<T> extends Future<T> { | |
constructor(func?: (handle: this) => void | (() => any)) | |
onCancel: void | (() => any) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const MaskType = 0x07 | |
const TypeInit = 0x00 | |
const TypePending = 0x01 | |
const TypeResolve = 0x02 | |
const TypeReject = 0x03 | |
const TypeCancel = 0x04 | |
// Anything not at least pending counts as "resolved" (and thus has one of these | |
// bits set). | |
const MaskTypeResolved = 0x06 | |
const FlagHasChild = 0x10 | |
const FlagHasMulti = 0x20 | |
const FlagHasCancel = 0x40 | |
const FlagMayCancel = 0x80 | |
function setCancel(handle, cancel) { | |
if (typeof cancel !== "function") { | |
throw new TypeError("Cancel callbacks must be callable when given") | |
} | |
if (!(handle._mask & MaskTypeResolved)) { | |
handle._mask |= FlagHasCancel | |
handle._cancel = cancel | |
} | |
} | |
function force(handle) { | |
if ((handle._mask & MaskType) === TypeInit) { | |
const func = handle._data | |
handle._mask = (handle._mask & ~MaskType) | TypePending | |
handle._data = undefined | |
try { | |
const cancel = func(handle) | |
if (cancel != null && handle._mask & FlagMayCancel) { | |
setCancel(handle, cancel) | |
} | |
} catch (e) { | |
if ((handle._mask & MaskType) === TypePending) { | |
const parent = handle._parent | |
handle._mask = (handle._mask & ~MaskType) | TypeReject | |
handle._data = e | |
handle._parent = undefined | |
invokeCancel(parent) | |
} | |
} | |
} | |
} | |
function poll(handle) { | |
force(handle) | |
switch (handle._mask & MaskType) { | |
case TypePending: return {type: "pending"} | |
case TypeResolve: return {type: "resolved", value: handle._data} | |
case TypeReject: return {type: "rejected", value: handle._data} | |
case TypeCancel: return {type: "cancelled"} | |
default: throw new TypeError("unreachable") | |
} | |
} | |
function invokeResolve(data, value) { | |
try { | |
const method = data.receiver.resolve | |
if (typeof method === "function") { | |
method.call(data.receiver, value) | |
} else { | |
settlePromise(data.handle, TypeResolve, value, invokeResolve) | |
} | |
} catch (e) { | |
settlePromise(data.handle, TypeReject, e, invokeReject) | |
} | |
} | |
function invokeReject(data, value) { | |
try { | |
const method = data.receiver.reject | |
if (typeof method === "function") { | |
method.call(data.receiver, value) | |
return | |
} | |
} catch (e) { | |
value = e | |
} | |
settlePromise(data.handle, TypeReject, value, invokeReject) | |
} | |
function settlePromise(handle, type, value, invokeListener) { | |
if (handle._mask & MaskTypeResolved) return | |
const receivers = handle._data | |
handle._mask = (handle._mask & ~MaskType) | type | |
handle._data = value | |
handle._parent = undefined | |
if (handle._mask & FlagHasCancel) handle._cancel = undefined | |
if (handle._mask & FlagHasMulti) { | |
for (let i = 0; i < receivers.length; i++) { | |
invokeListener(receivers[i], value) | |
} | |
} else if (handle._mask & FlagHasChild) { | |
invokeListener(receivers, value) | |
} | |
} | |
function invokeCancel(current) { | |
let thrown = false | |
let error | |
while (!(current._mask & MaskTypeResolved)) { | |
const parent = current._parent | |
current._mask = (current._mask & ~MaskType) | TypeCancel | |
current._data = undefined | |
current._parent = undefined | |
// This operates like a rolling `finally`. We just need to do it a | |
// bit differently since we don't recurse. | |
try { | |
if (current._mask & FlagHasCancel) { | |
const func = current._cancel | |
current._cancel = undefined | |
func() | |
} | |
} catch (e) { | |
thrown = true | |
error = e | |
} | |
current = parent | |
} | |
if (thrown) throw error | |
} | |
export class Promise { | |
constructor(future) { | |
if (!(future instanceof Future)) { | |
throw new TypeError("`future` must be a future") | |
} | |
this._ = future | |
} | |
get started() { | |
return (this._._mask & MaskType) !== TypeInit | |
} | |
poll() { | |
return poll(this._) | |
} | |
force() { | |
force(this._) | |
} | |
chain(factory) { | |
const klass = this._.constructor | |
let Species = klass[Symbol.species] | |
if (Species != null) Species = klass | |
const handle = new Species(undefined) | |
handle._parent = this._ | |
const receiver = factory(handle) | |
if (receiver == null || typeof receiver !== "object") { | |
throw new TypeError("receiver must be an object") | |
} | |
// Initialize it if it's lazy. Note that this *could* resolve the | |
// promise during initialization, so you can't just assume it'll end up | |
// pending. | |
force(this._) | |
switch (this._._mask & MaskType) { | |
case TypePending: | |
if (this._._mask & FlagHasMulti) { | |
this._._data.push({receiver, handle}) | |
} else if (this._._mask & FlagHasChild) { | |
this._._mask |= FlagHasMulti | |
this._._data = [this._._data, {receiver, handle}] | |
} else { | |
this._._mask |= FlagHasChild | |
this._._data = {receiver, handle} | |
} | |
break | |
case TypeResolve: | |
process.nextTick(invokeResolve, handle, receiver, this._._data) | |
break | |
case TypeResolve: | |
process.nextTick(invokeReject, handle, receiver, this._._data) | |
break | |
case TypeCancel: | |
invokeCancel(handle) | |
break | |
} | |
return new Promise(handle) | |
} | |
cancel() { | |
invokeCancel(this._) | |
} | |
} | |
export class Future { | |
constructor(func = undefined) { | |
if (func == null) { | |
this._mask = TypePending | |
this._data = undefined | |
} else { | |
if (typeof func !== "function") { | |
throw new TypeError("callback must be a function if passed") | |
} | |
this._mask = TypeInit | |
this._data = func | |
} | |
this._parent = undefined | |
} | |
static get [Symbol.species]() { | |
return this | |
} | |
static lazy(...args) { | |
return new Promise(new this(...args)) | |
} | |
static resolve(value) { | |
const handle = new this(undefined) | |
settlePromise(handle, TypeResolve, value, invokeResolve) | |
return new Promise(handle) | |
} | |
static reject(value) { | |
const handle = new this(undefined) | |
settlePromise(handle, TypeReject, value, invokeReject) | |
return new Promise(handle) | |
} | |
static cancel() { | |
const handle = new this(undefined) | |
invokeCancel(handle) | |
return new Promise(handle) | |
} | |
get promise() { | |
return new Promise(this) | |
} | |
// Convenience | |
get handle() { | |
return this | |
} | |
get started() { | |
return (this._mask & MaskType) !== TypeInit | |
} | |
force() { | |
force(this) | |
} | |
poll() { | |
return poll(this) | |
} | |
resolve(value) { | |
settlePromise(this, TypeResolve, value, invokeResolve) | |
} | |
reject(value) { | |
settlePromise(this, TypeReject, value, invokeReject) | |
} | |
cancel() { | |
invokeCancel(this) | |
} | |
} | |
export class CancellableFuture extends Future { | |
constructor(func = undefined) { | |
super(func) | |
this._cancel = undefined | |
this._mask |= FlagMayCancel | |
} | |
get onCancel() { | |
return this._cancel | |
} | |
set onCancel(value) { | |
if (value != null) { | |
setCancel(handle, cancel) | |
} else { | |
this._mask &= ~FlagHasCancel | |
this._cancel = undefined | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Promise} from "./promise-core.js" | |
// These are made to be used with the pipeline operator | |
export function each(func) { | |
return parent => { | |
parent.then(func) | |
return parent | |
} | |
} | |
export function map(func) { | |
if (typeof func !== "function") { | |
throw new TypeError("func must be a function") | |
} | |
return parent => parent.chain(handle => ({ | |
resolve: value => handle.resolve(func(value)), | |
})) | |
} | |
export function recover(func) { | |
if (typeof func !== "function") { | |
throw new TypeError("func must be a function") | |
} | |
return parent => parent.chain(handle => ({ | |
reject: value => handle.resolve(func(value)), | |
})) | |
} | |
export function mapBoth(func, error) { | |
if (typeof func !== "function") { | |
throw new TypeError("func must be a function") | |
} | |
if (typeof error !== "function") { | |
throw new TypeError("error must be a function") | |
} | |
return parent => parent.chain(handle => ({ | |
resolve: value => handle.resolve(func(value)), | |
reject: value => handle.resolve(error(value)), | |
})) | |
} | |
export function flatten(parent) { | |
return parent.chain(handle => ({ | |
resolve: value => { | |
if (handle.onCancel) handle.onCancel() | |
const p = value.chain(() => handle) | |
handle.onCancel = () => p.cancel() | |
}, | |
})) | |
} | |
const p = Promise.resolve() | |
const schedule = resolve => p.chain({resolve}) | |
export function flattenRec(parent) { | |
return parent.chain(handle => ({ | |
resolve(value) { | |
try { | |
if (typeof value.then === "function") { | |
value.then( | |
v => schedule(() => this.resolve(v)), | |
e => schedule(() => handle.reject(e)) | |
) | |
} else if (typeof value.chain === "function") { | |
value.chain(() => { | |
resolve: v => this.resolve(v), | |
reject: e => handle.reject(e), | |
}) | |
} else { | |
handle.resolve(value) | |
} | |
} catch (e) { | |
handle.reject(e) | |
} | |
}, | |
})) | |
} | |
export function all(iter) { | |
return CancellablePromise.lazy(handle => { | |
const items = [] | |
const deps = [] | |
let remaining = 1 | |
const reject = value => { | |
if (remaining !== 0) { | |
remaining = 0 | |
handle.reject(value) | |
} | |
} | |
const advance = () => { | |
if (remaining !== 0 && --remaining === 0) handle.resolve(items) | |
} | |
for (const item of iter) { | |
const {type, value} = item.poll() | |
const typeString = `${type}` | |
switch (typeString) { | |
case "pending": { | |
const index = items.length | |
items.push(undefined) | |
let resolved = false | |
deps.push(item.chain(() => ({ | |
resolve(value) { | |
if (resolved) return | |
resolved = true | |
if (remaining === 0) return | |
items[index] = value | |
deps[index] = undefined | |
advance() | |
}, | |
reject(value) { | |
if (resolved) return | |
resolved = true | |
reject(value) | |
}, | |
}))) | |
break | |
} | |
case "resolved": | |
items.push(value) | |
advance() | |
break | |
case "rejected": | |
throw value | |
case "cancel": | |
remaining = 0 | |
handle.cancel() | |
default: | |
throw new TypeError(`Unknown type: ${typeString}`) | |
} | |
} | |
advance() | |
return remaining === 0 ? undefined : () => { | |
for (const dep of deps) dep.cancel() | |
} | |
}) | |
} | |
export function race(iter) { | |
return CancellablePromise.lazy(handle => { | |
const deps = [] | |
let resolved = false | |
const resolve = (value, index) => { | |
if (!resolved) { | |
lock(index) | |
handle.resolve(value) | |
} | |
} | |
const reject = (value, index) => { | |
if (!resolved) { | |
lock(index) | |
handle.reject(value) | |
} | |
} | |
const lock = index => { | |
resolved = true | |
for (let i = 0; i < deps.length; i++) { | |
if (i !== index) deps[i].cancel() | |
} | |
} | |
try { | |
for (const item of iter) { | |
const {type, value} = item._.poll() | |
const typeString = `${type}` | |
switch (typeString) { | |
case "pending": | |
const index = deps.length | |
deps.push(item.chain(() => ({ | |
resolve: value => resolve(value, index), | |
reject: value => reject(value, index), | |
}))) | |
break | |
case "resolved": | |
resolve(value) | |
return undefined | |
case "rejected": | |
throw value | |
case "cancel": | |
// skip | |
break | |
default: | |
throw new TypeError(`Unknown type: ${typeString}`) | |
} | |
} | |
// This is set if a receiver synchronously resolves within another's | |
// initialization/`.chain` | |
if (!resolved) handle.promise.cancel() | |
} catch (e) { | |
reject(e) | |
} | |
return resolved ? undefined : () => lock(-1) | |
}) | |
} | |
// This is a simple coroutine helper | |
class Iterator { | |
constructor(handle, iter) { | |
this._handle = handle | |
this._iter = iter | |
} | |
step(func, value) { | |
do { | |
try { | |
const next = func.call(this._iter, value) | |
if (next.done) { | |
value = next.value | |
break | |
} else { | |
next.value.chain(() => this) | |
} | |
} catch (e) { | |
this._handle.reject(e) | |
} | |
return | |
} while (false) | |
this._handle.resolve(value) | |
} | |
resolve(v) { | |
this.step(this._iter.next, v) | |
} | |
reject(e) { | |
this.step(this._iter.throw, e) | |
} | |
} | |
export function co(gen) { | |
return function () { | |
const iter = gen.apply(this, arguments) | |
const {handle, promise} = new CancellablePromise() | |
handle.onCancel = () => iter.return() | |
return new Iterator(handle, iter).resolve() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment