knockout/tko

TC39 Proposal - Observable

krnlde opened this issue ยท 14 comments

The TC39 is going to standardize the way Observables should be used. Currently it is in Stage 1 (ready to advance to the next stage) https://github.com/tc39/proposal-observable

Maybe we should take action early on to match tko with the behavior of Observables in the proposal. Would be a huge advantage for tko when the proposal becomes a standard in 1 or 2 years.

Smart, thanks for linking

Could be done via:

import {observable as Observable} from 'tko';

const x = new Observable(/* TODO: API should be concise with the spec */);

There are some problems. For example, the TC Observable is not a function; the Knockout observable is essentially both the entity being observed and the observable class.

Which doesn't make it not doable, just interesting. :)

If we're going to be making an ES6 class of the TKO Observable (and arguably we should), here's a discussion maybe worth noting: http://stackoverflow.com/questions/36871299/how-to-extend-function-with-es6-classes

Maybe the ko.observable of the future is going to be obsolete and knockout will be the 2way databinding layer together with some utility functionality like computeds, validation, mapping, extend, susbscribable etc.

Yeah, this requires some thought, I believe. These are the priorities that come to mind:

  1. Maintain backwards compat
  2. Add new functionality for compatibility with TC
  3. Polyfill TC as needed (passing the proposal's tests)
  4. Add new functionality to supplement TC in ways that make sense for (t)KO (e.g. if we can reduce code duplication)

We've also got some code now that has Observable; we'll need to be cautious not to add more, owing to the name collision.

So the ko.Observable will become

  1. an TC-39 Observable, and
  2. also be the generator reader function i.e. when called with one argument it generates/triggers the subscriptions, when called with no arguments it returns the latest value.
  3. Lots of other things. :)

The ko subscriptions will need:

  1. an unsubscribe method. (=== dispose)
  2. a closed property

Great thoughts! I'll need some time to think about that too :)

@far-blue I'm copying your issue from tko.observable (and closing tko.observable issues as we've since/just moved to a monorepo here), as:

There's a standards proposal for ES8 (https://github.com/tc39/proposal-observable) which takes the bare bones of the observer/subscriber pattern from most.js, rxjs, xstream, zen-observable etc. to provide some compatibility in the same way Promises became standardised.

I don't think KO's observer/subscriber pattern is very far away from the suggested standard and it would be cool if KO observables could interop.

As an example of how these libraries work, I found this tutorial helpful :) https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

As you can see, where the tutorial talks about streams of DOM events or subscribers updating the DOM, KO could slot in very neatly :)

In the terminology used by libraries such as most.js, ko's observables are closest to 'Properties' rather than streams because they hold on to their last value. I'm not convinced KO needs to adopt Observables entirely, simply provide interop - at least to start with.

If you think about how KO would interop with Observables (aka 'streams'), I see three situations.

Firstly, a ko.observable could subscribe to a stream. the ko.observable would present as an Observer and so would need start(), next(), error() and complete() methods although error() and complete() methods could throw and be ignored respectively. start() and next() methods would simply update the value like the setter and trigger a hasMutated. This would work the same for writable computeds.

Secondly, a ko.observable would need to be observable through subscription. I'm not sure the constructor, the statics or the callebacks-based subscribe in the proposal are needed in this context and ko already has the concept of subscribe so extending it to support subscribing with an observer should be simple enough. The same applies to computeds.

Considering observableArrays, I'm not sure it would be a good idea to try to make them into streams. Instead, I suggest if an observableArray subscribed to a stream then it would simply update to match the latest value and expect the data values on the stream to already be arrays. In the same way, subscribing to an observableArray would mean the new state of the array was the pushed to the subscriber when the observableArray was changed. In the terminology of most.js and rxjs which treat arrays (Iterables) and promises as types of streams, an observableArray would generate a metastream but it's then up to the subscriber to deal with it.

In theory it also sounds nice to maybe have a stream of changes for an observableArray but then you'd need to define the structure for each event (add, remove, update). If you did, then you could also subscribe the observableArray to such a stream and have it apply the changes.

In all cases, I don't believe the standards proposal requires the Observer or Observable to be objects. As long as the methods are supported everything should work. As such, it should work if the required methods were in the function prototype for the functions returned by ko.observable() etc.

To try and give some code examples.

First, a ko.observable tracking an Observable:

var foo = ko.observable();
var stream = Observable.from([1, 2, 3, 4]);
var subscriptionHandler = stream.subscribe(foo);

foo will be set to 1 then 2, then 3, then 4 in sequence.

An Observable from a ko.observable using from():

var foo = ko.observable();
var stream = Observable.from(foo);

I've put together an example extender for ko.observable that works with most.js:
https://gist.github.com/far-blue/a652029beaaf9b7a97455c0d9cdd8678

Just to comment on the gist. Clearly things would be neater if done natively rather than as an extender but the gist proves the interop suggestion. In the end it turns out (at least for most.js) observe() accepts a function that is called on each update of the thing it is observing so for the demo I just rely on a ko.observable being a function and passing it straight to observe(). For proper spec. interop it would prob. be sensible to have the Observer interface of next() and error() methods (next.js docs suggest complete() is deprecated) so the subscribe() method in the TC can be used rather than observe().

I've investigated the ability to subscribe() and compatibility seems to come down to each library's interpretation of 'object'. RxJS and ZenObservable consider a function to be an object while most.js currently doesn't. ZenObservable does the check as Object(subscriber) === subscriber while most.js does typeof subscriber === 'object'. I think ZenObservable's approach to be better and will submit an issue for most.js.

I've updated the gist to demonstrate how subscribe() could work if most.js followed ZenObservable's approach but that means the gist now only works if you update line 1075 of most.js (non-minified!) from if (subscriber == null || typeof subscriber !== 'object') { to if (subscriber == null || Object(subscriber) !== subscriber) {

The gist also renamed what was the "Observable" extender to "Observer" and created a second extender for the actual "Observable".

FYI, most.js v1.4.0 includes my patch to allow functions as subscribers :)