Description
At my workplace, I've done a lot with observables, and have used a specific pattern fairly extensively that helps with errors and completion events from observables subscribed to by other observables. I'll start with a problematic example, using the API that our internal observables use (mostly zen-observable like):
// let's have some observables that fetch data from some API,
// in this case some usage limits for "basic" and "advanced"
// (also, ideally these would be ref-counted, but we'll ignore that)
const basicLimit = new Observable<number>((subscriber) => {
// -snip-
})
const advancedLimit = new Observable<number>((subscriber) => {
// -snip-
})
// now let's make an observable that returns data from both
const limits = new Observable<{ basic: number, advanced: number }>((subscriber) => {
let basic: number | null = null
let advanced: number | null = null
function next() {
if (basic !== null && advanced !== null) {
subscriber.next({ basic, advanced })
}
}
const onBasic = basicLimit.subscribe((value) => {
basic = value
next()
})
const onAdvanced = advancedLimit.subscribe((value) => {
advanced = value
next()
})
return () => {
onBasic.unsubscribe()
onAdvanced.unsubscribe()
}
})
You may have already noticed an issue - if basicLimit
or advancedLimit
error or complete early, that isn't at all indicated to the observer of limits
. We're just using the next
callback. You can of course fix this, but it's tedious especially when working with a lot of nested ref-counted observables:
// you have to do this for every single subscription
const subscription = obs.subscribe({
next: (x) => { /* ... */ },
error: (err) => subscriber.error(err),
complete: () => subscriber.complete(),
})
My solution was to add a new way of subscribing, called lift
:
// desugars to the above code block
const subscription = obs.lift(subscriber, (x) => { /* ... */ })
This is just sugar. It subscribes in a way where error
and complete
are automatically passed to subscriber
, but next
is handled by the callback in the second argument. You can also pass an observer object where you can override the behavior of either next
, error
, or complete
, but any you don't override are passed to subscriber
.
I called it lift
because its intended idea is that it "lifts" up inner errors or completions up automatically - there might even be many inner lifts
. If you use this religiously in your library, errors from deeply subscribed observables get passed all the way up to consumers without a lot a boilerplate being needed. I personally found it really helpful.
Since it's just sugar, it's understandable if this is out-of-scope or otherwise not desirable for this proposal. I would like to at least make it known that this was helpful for me and others, and that keeping this bit of friction in mind while working on the proposal would be appreciated.