tc39 / proposal-observable

Observables for ECMAScript
https://tc39.github.io/proposal-observable/
3.08k stars 90 forks source link

Syntax Support #207

Open loreanvictor opened 4 years ago

loreanvictor commented 4 years ago

The aim of this issue is not expanding the scope of the proposal, but rather contemplation on potential syntax that could follow IF this proposal is accepted.

As it stands, the only integration with JavaScript syntax for Observables would be through implementation of Symbol.asyncIterator (see this and this). This interop would make basic consumption of Observables rather intuitive:

for await (let o of o$) console.log(o);

However, the for await syntax is designed for pulling values from a generator rather than responding to a source emitting values. This for example can cause confusion because we are waiting for the Observable to complete (which might not happen), or leads to a necessity for buffering the values of the Observable so that they can be served when they are pulled by the consumer (which has an overhead and can lead to memory issues).

Generally, while it is enticing to use await to wait for values from sources that push stuff (Promises and Observables), there is a clear distinction between the two: while a Promise only ever pushes one value (so it makes sense to await that one value), an Observable can push many values, which raises the question of consolidating that behavior with a pulling mechanism (e.g. buffering).

An alternative might be an additional syntax support for consumption of Observables, for example through a new keyword:

on (o$) console.log('Got something!');

Which would be translated to:

o$.subscribe(() => console.log('Got something'));

Or with a scoped variable set to emitted value:

on (let o from o$) console.log(o);

Which would be roughly equivalent to:

o$.subscribe(o => console.log(o));

The on keyword could actually happen in a non async context (since it doesn't block the flow).


Inspiring Examples

on (fromEvent(button, 'click'))) {
  const response = await someRequest();
  updateUI();
}
on (let {clientX, clientY} from fromEvent(document, 'mousemove')) {
  console.log(clientX);
}
on (const connection from socket) {
  on (const msg from connection) {
    // handle msg from socket
  } finally {
    // cleanup for connection closing
  }
}


Aborting

Similar to for loops, it can support continue and break keywords:

on (let o from o$) {
  if (o == 3) continue;
  if (o > 10) break;
  console.log(o);
}

Which would roughly translate to:

const _ctrl = new AbortControl();
o$.subscribe(o => {
  if (o == 3) return;
  if (o > 10) {
    _ctrl.abort();
    return;
  }

  console.log(o);
}, undefined, undefined, _ctrl);


Error and Completion Handling

It could also be extended with catch and finally keywords for error/completion handling:

on (let o from o$) {
  /* do stuff */
} catch(err) {
  /* handle the error */
} finally {
  /* handle completion / cleanup */
}

Which would be roughly equivalent to

o$.subscribe(
  o => {
    /* do stuff */
  },
  err => {
    try { /* handle the error */ } 
    finally { /* handle completion / cleanup */ }
  },
  () => {
    /* handle completion / cleanup */
  }
);

It is notable that in this situation the default behavior of .subscribe() would differ from what you would get from the syntax, as finally will also be executed after an error has occurred (to be in sync with behavior of try/catch), which might be a source of confusion.


EDIT: as discussed here, it might be a good idea to make the subscription process asynchronous to make the behavior a bit more predictable.

benjamingr commented 4 years ago

Are you aware of the (ancient) for..on proposal?

benjamingr commented 4 years ago

https://github.com/jhusain/compositional-functions

Edit: sorry, meant https://github.com/jhusain/asyncgenerator

loreanvictor commented 4 years ago

Edit: sorry, meant https://github.com/jhusain/asyncgenerator

Yeah I was reading through the original link and wondering how it related (though definitely interesting on its own).

loreanvictor commented 4 years ago

Thanks for sharing. Isn't that proposal actually withdrawn in favor of this proposal?

Edit: still good to know of existence of that proposal, I am going through the discussion on it specifically regarding the for ... on syntax and its caveats now. Specifically this issue seems to apply to what I've suggested here as well, and while my preference would be for the syntax to instantly subscribe since the idea is that you should not care when values are emitted, based on personal experience it seems it is safer to make the subscription asynchronous, as I have for example used a work-around like this a lot:

// do stuff
setImmediate(() => observable.subscribe(...));