Truebase-com / TruthStack

Monorepo for the Truth technology stack.
10 stars 1 forks source link

Support List Operations in Reflex #2

Closed paul-go closed 5 years ago

paul-go commented 5 years ago

We need to implement synchronizing arrays created with the reflex([…]) function as described at the bottom of this page: https://www.truebase.com/open-source/reflex/

Some of the initial plumbing for this feature has been put into place, and exists at: Reflex/ReflexCore/source/Meta/ArrayStreamMeta.ts

Basic explanation of the implementation

The typical way to achieve array synchronization with a UI is to use something like a virtual DOM, which uses some algorithm that compares the before state and the after state of a given array, which computes some delta, and then a patch, which is then applied to the DOM to bring it up to date.

However, we’re going a different direction with reflex. Instead, we’re going to observe arrays passed through the reflex() function directly. When these arrays are modified, the changes to them are automatically reflected in the DOM.

To the best of my knowledge, libraries like Angular and React have more or less opted away from this method for the reason that in order to do it properly, it more or less requires the use of ES6 proxies, which are a semi-modern feature (not supported in IE11 and older) in order to get a completely transparent developer experience. This is because developers are able to do things to arrays like edit indexes directly (array[0] = value), which cannot be trapped without the use of a Proxy. Because Reflex doesn’t officially support IE11, we don’t have the same limitation as the other frameworks.

The implementation described at the URL above assumes that reflex([…]) returns an ArrayReflex object, which can then be fed in as a valid selector to on(..):

ml.div(
    on(
        array
            .filter(num => positive.value ? num >= 0 : num < 0, positive)
            .sort((a, b) => asc.value ? a - b : b - a, asc),
        item => ml(` ${item} `)
);

The higher-order functions (filter, sort, map, etc) in ArrayReflex are chainable ... you should be able to chain many of these calls together, and still end up with something that can be used as a selector to the on() at the end of it.

Each of these higher-order functions need a rest parameter which is a series of reflexes. These reflexes are basically the dependencies of the function–meaning that when one of the reflexes change, it causes a recalculation of the entire chain.

(There's probably some other stuff I'm missing here that should be specified. I'll keep revising this issue)

kaaninel commented 5 years ago

From where call chain should start ?

ml.div(
    on(
        array
            .filter(num => positive.value ? num >= 0 : num < 0, positive)
            .sort((a, b) => asc.value ? a - b : b - a, asc),
        item => ml(` ${item} `)
);

when we change asc should it only trigger sort on cached result of filter or should it trigger whole pipeline and calculate filter again and then sort result ?

paul-go commented 5 years ago

@kaaninel I think this is something we need to test and figure out. There are several ways of doing this, I think there's still questions that need to be answered here in terms of finding an actual implementation. For example, what do we do in the case when someone creates one of these higher-order progressions, but doesn't actually attach it to any on() anywhere? Do we still keep it in memory? Is it possible to detect when these things get garbage collected somehow?

In terms of storing independent representations of an array for each higher-order function application, I haven't done enough deep thinking about any particular implementation to determine whether or not this is premature optimization.