Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retain core API and leave operators to user-land libraries #210

Closed
alshdavid opened this issue Dec 17, 2020 · 15 comments
Closed

Retain core API and leave operators to user-land libraries #210

alshdavid opened this issue Dec 17, 2020 · 15 comments

Comments

@alshdavid
Copy link

alshdavid commented Dec 17, 2020

Motivation

The motivation behind this proposal is the need for JavaScript to have a formal interface describing streamable types. You can see this in the way Observable-like implementations are re-implemented time and time again.

  • Redux
  • Mobx
  • Ace Editor
  • Socket.io
  • Express.js

While the browser has the EventTarget interface, allowing objects to implement the addEventListener method - this interface is lacking on Node and further, requires consumers to specify the event name and submit messages through initializing an Event. Great for HTMLElement instances that have several event types (such as click, hover), but less logical for basic streamable data.

The Observable specification, much like Promise, provides a standardised interface that everyone can target.

It's my belief that operators should be dropped from the specification as they can be implemented by user-land libraries and seem to serve to distract from the core value of the proposal.

Lastly, with so many competing implementations, it's much harder to pipe data from one stream into another without hand written adapters bridging the separate stream implementations.

The Whole Thing

Base Type

Implementation via Github gist
Example implementation in replit

type Callback<T extends Array<any> = [], U = void> = (...args: T) => U

class Observable<T> {
  constructor(
    setup: Callback<[
      Callback<[T]>,   // Value
      Callback<[any]>, // Error
      Callback,        // Complete
    ], void | Callback<[], any>>
  )

  subscribe(
    value: Callback<[T], any>,
    error?: Callback<[unknown], any>,
    complete?: Callback<[], any>
  ): Subscription
}

class Subscription {
  unsubscribe(): void
}

Usage

const values = new Observable((next, _error, complete) => {
  next('foo')
  setTimeout(complete, 1000)
})

const subscription = values.subscribe(
  console.log, // "foo"
  console.error,
  console.warn, // Will run with empty message
)

Async

Observable will execute synchronously unless an asynchronous action happens in the execution chain somewhere. Such as a fetch in the Observable constructor or a setTimeout in the Subscriber

Errors

If something throws inside an Observable setup callback, an error is pushed to subscribers. If the error method is called, an error is pushed to subscribers.

const values = new Observable(() => {
  throw new Error('Foobar')
})

const subscription = values.subscribe(
  console.log, 
  console.error, // typeof Error message "Foobar"
)

Interfaces

type Callback<T extends Array<any> = [], U = void> = (...args: T) => U

interface Subscriber<T> {
  subscribe(
    value: Callback<[T], any>,
    error?: Callback<[unknown], any>,
    complete?: Callback<[], any>
  ): Unsubscriber
}

interface Unsubscriber {
  unsubscribe(): void
}

What about Subject, ReplaySubject, etc?

These are fantastic types that help with the control flow of streamable values, but can all be made from the base Observable. class. and should therefore be left to user-land libraries to implement

class Subject extends Observable {
  // ...
}

Operators

The use of operators on the Observable type reminds me a bit of how libraries like Bluebird had operators on Promise. Operators being a part of rxjs and rxjs being so popular makes them feel like they are required for Observable to be useful but it's important to recognise that Observable is simple a standard interface to enable the consumption of streamed values. The reactive extensions are there to enhance that behaviour.

While operators may be ergonomic, they are not necessary for the handling of streamable data and can be implemented by user libraries

An example of how operators could be used as implemented by a user-land library is as follows:

const values = new Observable(next => {setInterval(next, 1000, 1)})

const modifiedValues = pipe(values)(
  filter(value => value > 0),
  map(value => value.toString()),
)

modifiedValues.subscribe(console.log)

Conversion to Promise

Much like the challenges described by the rxjs team on what it even means to convert a stream to a Promise, this falls into the domain of user-land libraries.

const values = new Observable(next => {setInterval(next, 1000, 1)})

firstValueFrom(values).then(console.log)
const values = new Observable((_next, _error, complete) => {setTimeout(complete, 1000, 1)})

lastValueFrom(values).then(console.log)

Conclusion

Keeping the API limited in scope allows solving the major issue with streams in JavaScript, a consistent target API.

@benjamingr
Copy link

While the browser has the EventTarget interface, allowing objects to implement the addEventListener method - this interface is lacking on Node

This isn't actually true anymore, Node already ships EventTarget and it's even already stable.

and further, requires consumers to specify the event name and submit messages through initializing an Event. Great for HTMLElement instances that have several event types (such as click, hover), but less logical for basic streamable data.

Oh there are a million problems with that interface that we (as in "people who care a lot about the web platform") should address (I have a list somewhere I think?) - I don't think it "competes" with observables although observables in the web platform could complement it.

It's my belief that operators should be dropped from the specification as they can be implemented by user-land libraries and seem to serve to distract from the core value of the proposal.

That's such a good idea and people agree on it that it's already the case :] Look at the spec.

@runarberg
Copy link

I remember sitting down one weekend earlier this year having fun writing operators for the new proposed API, and it was an absolute joy. And if a pipe operator gets approved that allows point free style, I bet such libraries will flourish.

@kirly-af
Copy link

Hi @benjamingr,

I'd be interested understanding your view on why Observable wouldn't "compete" with previous interfaces (EventTarget, EventEmitter). My understanding was that in most cases it would replace those APIs, how do you see it? I guess older APIs would still make sense for low level implementations (like ports of Web APIs in Node.js).

Either way, you seem to know a lot on this proposal. Do you have an idea of if it planned to have it presented for stage 2 any time soon? Nice to see the spec got significantly smaller anyway.

Thanks!

@benjamingr
Copy link

benjamingr commented Feb 10, 2021

Either way, you seem to know a lot on this proposal. Do you have an idea of if it planned to have it presented for stage 2 any time soon? Nice to see the spec got significantly smaller anyway.

The proposal is mostly blocked on someone putting in the (admittedly large) amount of work in to prepare it and present it. The committee isn't actually opposed to it. To quote a TC39 member I talked to about this last week "TC39 is not opposed to observables, it is more that no one is working on it"


I'd be interested understanding your view on why Observable wouldn't "compete" with previous interfaces (EventTarget, EventEmitter).

EventTarget and EventEmitter are not a part of JavaScript. EventEmitter is a part of Node.js and EventTarget is a part of WHATWG/dom (and also Node.js).

EventTarget/EventEmitter don't actually compose (you can't really easily "map" them) and they're a much higher level abstraction (observables are fundamentally much simpler).

@dy
Copy link

dy commented Jan 22, 2022

TC39 is not opposed to observables, it is more that no one is working on it

What work would it require?

This proposed version seems to be the minimal viable standard needed for interop in subscribable-things, observable-value, hyperf and others.

@benjamingr
Copy link

@dy I think the first step would be to find someone from TC39 who is willing to sit down with you and enumerate the things needed to move observable forward (show a good DOM interop story, deal with existing concerns, show viability in language APIs, write an explainer on where push is required etc).

Maybe @ljharb would know what would be an appropriate way to reach out to champions?

@ljharb
Copy link
Member

ljharb commented Jan 22, 2022

Posting on https://es.discourse.group is your best bet.

@benlesh
Copy link

benlesh commented Jan 22, 2022

And if a pipe operator gets approved that allows point free style, I bet such libraries will flourish.

This isn't going to happen. I'd love it if it did. But it's thoroughly blocked by influential people.

@benlesh
Copy link

benlesh commented Jan 22, 2022

FYI: There are more Observable-like implementations in the wild:

  • Svelte
  • Relay
  • ApolloGraphQL (uses a subclassed zen-observable, but seems to be switching to RxJS soon).

I've seen other very, very similar types in dozens of lesser known libraries, as well. A WAMP client I've used comes to mind.

@benlesh
Copy link

benlesh commented Jan 22, 2022

Oh, and @alshdavid, what you have above is missing an important aspect of Observable, which is how to finalize what you've set up during subscription. You need to either pass in some sort of token/signal to register finalization on, or you need to allow the user to return a function that will be called during finalization, or something to that effect.

@benlesh
Copy link

benlesh commented Jan 22, 2022

This is probably a some what duplicate of this issue from 2019

@dy
Copy link

dy commented Jan 23, 2022

Posted to es.discourse.

@dy
Copy link

dy commented Jan 25, 2022

One thing is missing here. API must include [Symbol.observable] for interop:

class Observable<T> {
    ...
    // Returns itself
    [Symbol.observable]() : Observable;
    ...
}

@Pyrolistical
Copy link

@alshdavid
Copy link
Author

Closing this as the discussion has been moved here https://es.discourse.group/t/observables/1175/4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants