Skip to content

Instantly share code, notes, and snippets.

@dead-claudia
Last active March 13, 2018 07:54
Show Gist options
  • Save dead-claudia/2563c9dcf8b19bc2875e5cfb3d7709ad to your computer and use it in GitHub Desktop.
Save dead-claudia/2563c9dcf8b19bc2875e5cfb3d7709ad to your computer and use it in GitHub Desktop.
Simpler, better Promise library
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)
}
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
}
}
}
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