{"action":"created","issue":{"url":"https://api.github.com/repos/tc39/proposal-observable/issues/112","repository_url":"https://api.github.com/repos/tc39/proposal-observable","labels_url":"https://api.github.com/repos/tc39/proposal-observable/issues/112/labels{/name}","comments_url":"https://api.github.com/repos/tc39/proposal-observable/issues/112/comments","events_url":"https://api.github.com/repos/tc39/proposal-observable/issues/112/events","html_url":"https://github.com/tc39/proposal-observable/issues/112","id":177654881,"number":112,"title":"Replace Subscriptions with Cancellation Tokens","user":{"login":"jhusain","id":466863,"avatar_url":"https://avatars.githubusercontent.com/u/466863?v=3","gravatar_id":"","url":"https://api.github.com/users/jhusain","html_url":"https://github.com/jhusain","followers_url":"https://api.github.com/users/jhusain/followers","following_url":"https://api.github.com/users/jhusain/following{/other_user}","gists_url":"https://api.github.com/users/jhusain/gists{/gist_id}","starred_url":"https://api.github.com/users/jhusain/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jhusain/subscriptions","organizations_url":"https://api.github.com/users/jhusain/orgs","repos_url":"https://api.github.com/users/jhusain/repos","events_url":"https://api.github.com/users/jhusain/events{/privacy}","received_events_url":"https://api.github.com/users/jhusain/received_events","type":"User","site_admin":false},"labels":[{"id":206344308,"url":"https://api.github.com/repos/tc39/proposal-observable/labels/enhancement","name":"enhancement","color":"84b6eb","default":true}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":63,"created_at":"2016-09-18T14:27:55Z","updated_at":"2016-12-20T00:40:56Z","closed_at":null,"body":"In Munich the committee pushed back when Observable was proposed for advancement to stage 2. Instead the committee suggested we examine whether it was possible to integrate Observable with the Cancellation Tokens presented by @domenic at the same meeting. Reasonable questions were raised about whether JavaScript really needs both Cancellation tokens and Subscriptions given both have similar responsibilities and APIs.\n\n@domenic and I looked at this and came up with the following proposal:\n\n``` js\n\nclass Observable<T> {\n    constructor(subscribeDefn: Function)\n\n    subscribe(observer: Observer, token: CancelToken): void\n\n   forEach(nextFn: Function, token: CancelToken): Promise\n\n    [Symbol.observable](): Observable\n\n    static of(...items): Observable\n\n    static from(ObservableLike: Object): Observable\n}\n\ninterface Observer {\n\n    next(value),\n\n   // try/else equivalent\n    else(error),\n\n    complete(value)\n\n    // receives object sent during unsubscription/cancellation\n   // try/catch equivalent\n    catch(cancel)\n}\n\n```\n\nIn this model, Cancellation tokens replace Subscriptions. Instead of an Observable creating a Subscription object when subscribe is called, the consumer creates a CT and passes it to the subscribe method alongside an Observer. Internally each Observable creates its own CT, registers all of its cleanup behavior to run on its CT’s cancellation, then links the internal CT with the one passed to the subscribe method. The Observable runs its cleanup behavior by canceling its own cancellation token whenever the subscription process finishes.\n\nGiven that the cancellation promise resolves asynchronously, this introduces a race condition in which an Observer could be notified after the token was canceled. To eliminate this race condition, the Observable always confirms the \"CancelToken.requested\" property is undefined before notifying.\n\nNote that the Observer’s “error” function has been renamed \"else\" to match both the proposed language keyword and Promise method. similar to the proposed \"else\" method for Promise, the Observer’s  \"else\" method will only receive Errors, not Cancellations. A new Observer method “catch” will receive any error that would be caught by a \"catch\" block, including cancellations passed to the cancellation token source’s cancel function. Either the \"catch\" or \"else\" methods can be present on the Observer, but not both. This mirrors the syntactic restriction against having a try/catch/else.\n\nThis is a significant change to the proposal and we need to do more work to confirm that all of the edge cases are met. I have tried to implement several combinators and haven't run into any issues yet. However I am also eager to gather feedback from library implementers to help identify any gaps in this proposal.\n\nHere is my perspective on this proposal. Initially I was troubled by the ergonomics of replacing subscriptions with cancellation tokens, because using cancellation tokens requires an additional line of code when subscribing. However I became less concerned about this once I considered the change from the perspective of three personas:\n1. library author\n2. advanced user\n3. novice user\n\nLibrary authors can wrap a method which returns a subscription and adapt it to the fn accepted by the Observable constructor. The wrapper method would simply attach the unsubscribe logic to the cancellation token promise. This is straightforward. However a larger question is around the composability of CT's. Often times it is useful to compose Subscriptions when writing more complex functions which compose together many Observables (ex. flat map). It's my intuition that Cancellation Token Sources can be composed in much the same way as Subscriptions (ex. CompositeCancellationTokenSource). It would be useful if a library author could falsify this.\n\nIn my experience, advanced users of Observable use methods like map, filter, and flatMap far more than subscribe. As a result I do not believe that requiring an extra line of code when subscribing is onerous. It's also important to note that advanced users unsubscribe even less frequently than they subscribe. If you look through the Netflix code base you will find that often subscriptions are not even captured, because takeUntil or switchLatest are used to declaratively trigger unsubscription instead. Given that the cancellation token is an optional argument, the new subscription API is effectively the same as the old one in these cases. \n\nWhile advanced users will use combinators to coordinate concurrency, I believe novice users will use asynchronous functions. Given the early adoption and excitement around asynchronous functions, I expect that they will quickly become pervasive, and by extension so will cancellation tokens. Over time it will become increasingly likely that when Observable subscriptions occur inside of an asynchronous function, a cancellation token will already be in scope. As a result, the forEach API will only require the same boilerplate as any other asynchronous function: you have to pass the cancellation token as the last argument.\n\nI want to call attention to the fact that using cancellation tokens improves ergonomics in one minor way: there is no longer any need for a \"start\" method. Both pulling/returning and using the \"start\" method to push the Subscription object was always an awkward API. It's cleaner to do it only one or the other, and cancellation tokens elegantly solve this problem. It is arguable that cancellation tokens are more appropriate than Subscriptions given the decision to allow Observable to synchronously notify.\n\nEager to hear your thoughts.\n"},"comment":{"url":"https://api.github.com/repos/tc39/proposal-observable/issues/comments/268120823","html_url":"https://github.com/tc39/proposal-observable/issues/112#issuecomment-268120823","issue_url":"https://api.github.com/repos/tc39/proposal-observable/issues/112","id":268120823,"user":{"login":"benjamingr","id":1315533,"avatar_url":"https://avatars.githubusercontent.com/u/1315533?v=3","gravatar_id":"","url":"https://api.github.com/users/benjamingr","html_url":"https://github.com/benjamingr","followers_url":"https://api.github.com/users/benjamingr/followers","following_url":"https://api.github.com/users/benjamingr/following{/other_user}","gists_url":"https://api.github.com/users/benjamingr/gists{/gist_id}","starred_url":"https://api.github.com/users/benjamingr/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/benjamingr/subscriptions","organizations_url":"https://api.github.com/users/benjamingr/orgs","repos_url":"https://api.github.com/users/benjamingr/repos","events_url":"https://api.github.com/users/benjamingr/events{/privacy}","received_events_url":"https://api.github.com/users/benjamingr/received_events","type":"User","site_admin":false},"created_at":"2016-12-20T00:40:56Z","updated_at":"2016-12-20T00:40:56Z","body":">  Weren't these three things true when @jhusain opened this issue? \r\n\r\nThe issue was a discussion and an experiment. He opened it knowing it might end like this even without tokens being withdrawn.\r\n\r\n> ? What happened with the Cancellable Promise proposal and this one? \r\n\r\nThe `CancelToken` proposal which is the most recent cancellable-promise proposal has been withdrawn and currently no champion (I'm aware of) is working on an alternative.\r\n\r\n>  I mean, was there a requirement that the proposals proceed harmoniously, or not? \r\n\r\nNo, given cancel tokens are withdrawn we should probably do what we all wanted all along when the constraint wasn't there and progress with subscriptions. It won't inhibit future co-op anyway if that's ever required.\r\n\r\n > And is the failure to work together with the (now-withdrawn) proposal a victory, failure, or neither? \r\n\r\nIt's a fact of life.\r\n\r\n> Does that mean that this proposal will still need to collaborate and move together with a new proposal for promise cancellation?\r\n\r\nThis proposal needs to and will collaborate with anything the TC would like and probably with other proposals - and vice-versa. Subscriptions work and are the best bet we have to finally being able to standardize this."}}
{"actor":{"display_login":"benjamingr"}}