@xylabs/threads
Version:
Web workers & worker threads as simple as a function call
185 lines (161 loc) • 5.96 kB
text/typescript
/* eslint-disable unicorn/no-thenable */
/* eslint-disable @typescript-eslint/member-ordering */
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable unicorn/no-this-assignment */
import type { ObservableLike, SubscriptionObserver } from 'observable-fns'
import { Observable } from 'observable-fns'
type OnFulfilled<T, Result = void> = (value: T) => Result
type OnRejected<Result = void> = (error: Error) => Result
type Initializer<T> = (observer: SubscriptionObserver<T>) => UnsubscribeFn | void
type Thenable<T> = { then: (onFulfilled?: (value: T) => any, onRejected?: (error: any) => any) => any }
type UnsubscribeFn = () => void
const doNothing = () => {}
const returnInput = <T>(input: T): T => input
const runDeferred = (fn: () => void) => Promise.resolve().then(fn)
function fail(error: Error): never {
throw error
}
function isThenable(thing: any): thing is Thenable<any> {
return thing && typeof thing.then === 'function'
}
/**
* Creates a hybrid, combining the APIs of an Observable and a Promise.
*
* It is used to proxy async process states when we are initially not sure
* if that async process will yield values once (-> Promise) or multiple
* times (-> Observable).
*
* Note that the observable promise inherits some of the observable's characteristics:
* The `init` function will be called *once for every time anyone subscribes to it*.
*
* If this is undesired, derive a hot observable from it using `makeHot()` and
* subscribe to that.
*/
export class ObservablePromise<T> extends Observable<T> implements Promise<T> {
readonly [Symbol.toStringTag] = '[object ObservablePromise]'
private initHasRun = false
private fulfillmentCallbacks: Array<OnFulfilled<T>> = []
private rejectionCallbacks: OnRejected[] = []
private firstValue: T | undefined
private firstValueSet = false
private rejection: Error | undefined
private state: 'fulfilled' | 'pending' | 'rejected' = 'pending'
constructor(init: Initializer<T>) {
super((originalObserver: SubscriptionObserver<T>) => {
// tslint:disable-next-line no-this-assignment
const self = this
const observer: SubscriptionObserver<T> = {
...originalObserver,
complete() {
originalObserver.complete()
self.onCompletion()
},
error(error: Error) {
originalObserver.error(error)
self.onError(error)
},
next(value: T) {
originalObserver.next(value)
self.onNext(value)
},
}
try {
this.initHasRun = true
return init(observer)
} catch (error) {
observer.error(error)
}
})
}
private onNext(value: T) {
if (!this.firstValueSet) {
this.firstValue = value
this.firstValueSet = true
}
}
private onError(error: Error) {
this.state = 'rejected'
this.rejection = error
for (const onRejected of this.rejectionCallbacks) {
// Promisifying the call to turn errors into unhandled promise rejections
// instead of them failing sync and cancelling the iteration
runDeferred(() => onRejected(error))
}
}
private onCompletion() {
this.state = 'fulfilled'
for (const onFulfilled of this.fulfillmentCallbacks) {
// Promisifying the call to turn errors into unhandled promise rejections
// instead of them failing sync and cancelling the iteration
runDeferred(() => onFulfilled(this.firstValue as T))
}
}
then<TResult1 = T, TResult2 = never>(
onFulfilledRaw?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onRejectedRaw?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): Promise<TResult1 | TResult2> {
const onFulfilled: OnFulfilled<T, TResult1> = onFulfilledRaw || (returnInput as any)
const onRejected = onRejectedRaw || fail
let onRejectedCalled = false
return new Promise<TResult1 | TResult2>((resolve, reject) => {
const rejectionCallback = (error: Error) => {
if (onRejectedCalled) return
onRejectedCalled = true
try {
resolve(onRejected(error))
} catch (anotherError) {
reject(anotherError)
}
}
const fulfillmentCallback = (value: T) => {
try {
resolve(onFulfilled(value))
} catch (ex) {
const error = ex as Error
rejectionCallback(error)
}
}
if (!this.initHasRun) {
this.subscribe({ error: rejectionCallback })
}
if (this.state === 'fulfilled') {
return resolve(onFulfilled(this.firstValue as T))
}
if (this.state === 'rejected') {
onRejectedCalled = true
return resolve(onRejected(this.rejection as Error))
}
this.fulfillmentCallbacks.push(fulfillmentCallback)
this.rejectionCallbacks.push(rejectionCallback)
})
}
catch<Result = never>(onRejected: ((error: Error) => Promise<Result> | Result) | null | undefined) {
return this.then(undefined, onRejected) as Promise<Result>
}
finally(onCompleted?: (() => void) | null | undefined) {
const handler = onCompleted || doNothing
return this.then(
(value: T) => {
handler()
return value
},
() => handler(),
) as Promise<T>
}
static override from<T>(thing: Observable<T> | ObservableLike<T> | ArrayLike<T> | Thenable<T>): ObservablePromise<T> {
return isThenable(thing)
? new ObservablePromise((observer) => {
const onFulfilled = (value: T) => {
observer.next(value)
observer.complete()
}
const onRejected = (error: any) => {
observer.error(error)
}
thing.then(onFulfilled, onRejected)
})
: (super.from(thing) as ObservablePromise<T>)
}
}