Q&A
Why is the method map called map?#
It cannot be then since JavaScript has some built-in behaviors around that particular name, and as to map vs. flatMap, here we’re taking advantage of the fact that there can be no higher-order lazy promises. If map gets a LazyPromise from its callback, it cannot return a LazyPromise<LazyPromise<...>> and has no choice but to flatten the result, so we don’t need to disambiguate between map and flatMap. Similarly, we can just say box since we don’t have to disambiguate between box (wrap in a LazyPromise) and normalize (coerce to a LazyPromise).
Why dot notation and not pipes-only like RxJS?#
Because unlike RxJS, there exists a small and well-defined set of operators that are comparable to language features and are more equal than others.
Do sink.resolve and sink.reject fire synchronously?#
Not when called synchronously from the LazyPromise constructor callback: we first let that callback return any teardown logic, call that logic, and only then fire. Otherwise, yes. It’s only an academic question though. Consider the synchronous case:
const lazyPromise = new LazyPromise<number>((sink) => {
sink.resolve(42);
// `foo` has not run.
});
lazyPromise.subscribe({ resolve: foo });
// `foo` has run.Once the constructor callback has settled the promise, its job is done, and in practice you wouldn’t do anything in the no man’s land after sink.resolve(42) and before the constructor callback returns. As to wrapping sink.resolve(42) in an AsyncContext/AsyncLocalStorage run call, this wouldn’t have any effect whether we fire synchronously or asynchronously, since downstream code always runs in the context of the .subscribe call.
Why not have the subscribe method return a function instead of a subscription object?#
To avoid the overhead of creating and garbage-collecting a function. You can create a utility toDisposeFn that takes a LazyPromise<NotAnErrorBox, undefined>, subscribes to it, and returns a teardown function:
// () => void
const dispose = lazyPromise.pipe(toDisposeFn);Why not have an equivalent of Promise.allSettled?#
LazyPromise is designed in such a way that all boxed errors must be either explicitly caught with catchBoxed or whitelisted when you call subscribe. allSettled implicitly catches errors by turning them into values that you can potentially discard without the type system flagging it.
Why not have an equivalent of Promise.withResolvers?#
A native promise has a single resolve, reject pair, whereas a LazyPromise has a new sink for each subscription.
Why not have an affordance for sharing/caching the result?#
While this is achievable with userland operators like those in RxJS, it’s not something you want to bake into the primitive, because how you do it depends on what you use for state. For example if it’s Signals, you would extend computed/createMemo so it knows what to do with lazy promises.
Why not a separate channel for typed errors like in Effect?#
Error boxes are part of what makes LazyPromise simple. LazyPromise<"value" | ErrorBox<"error">> is more verbose than LazyPromise<"value", "error">, but you can produce typed errors without wrapping them in lazy promises. An error box is a self-sufficient construct: a synchronous function can return one, and so can a native async function. Speaking of type signatures, LazyPromise does not attempt to collapse types like ErrorBox<"a"> | ErrorBox<"b"> into ErrorBox<"a" | "b"> or to enforce a specific order of elements in the union. Large TS types have always been hard to read, and there is little LazyPromise can do about that: instead it focuses on correctness and simplicity.
Why does LazyPromise have deferral utilities, but no equivalent of fromEvent in RxJS?#
Sometimes you want to resolve the lazy promise not on the first event, but on the first event matching a condition, or to error out the promise based on some event. To cover all such use cases, it’s better to have a lower-level utility that’s not even specific to LazyPromise, and that simply returns a teardown function or a Disposable after adding an event listener. You can then use it in the LazyPromise constructor as follows:
const opened = new LazyPromise<void>((sink) =>
addDisposableEventListener(socket, "open", () => {
sink.resolve();
}),
);