haxetink / tink_state

Handle those pesky states.
The Unlicense
29 stars 13 forks source link

Change is not remembered somehow #51

Closed kevinresol closed 4 years ago

kevinresol commented 4 years ago

Code is basically the one outlined in #50, with some debug traces added:

public function query(q:Query):Observable<Array<Entity>> {
    final qs = q.toString();
    final entityQueries = {
        final cache = new Map<Int, Pair<Entity, Observable<Bool>>>();
        Observable.auto(() -> {
            for (id => entity in map)
                if (!cache.exists(id)) {
                    var first = true;
                    cache.set(id, new Pair(entity, Observable.auto(() -> {
                        final v = entity.fulfills(q);
                        if (first)
                            first = false
                        else
                            trace('$entity: re-compute fulfill: $v [$qs]');
                        v;
                    }, (now, last) -> {
                            trace('$entity: last:$last, now:$now [$qs]');
                            last == now;
                        }, id -> 'World:${entity.toString()}:fulfills#$id')));
                }

            final deleted = [for (id in cache.keys()) if (!map.exists(id)) id];

            for (id in deleted)
                cache.remove(id);

            cache;
        },
            (_, _) -> false, // we're always returning the same map, so the comparator must always yield false
            id -> 'World:cache#$id');
    }

    return Observable.auto(() -> {
        trace('(re)compute query list [$qs]');
        [for (p in entityQueries.value) if (p.b) p.a];
    }, null, id -> 'World:root#$id');
}

Observation:

=> game loop starts
unset a key in observable map
recompute "fulfill" -> false
comparator -> was:true, now:false
recompute query list
=> game loop ends

=> game loop starts
set a key in observable map
recompute query list
recompute "fulfill" -> true   ** Note this
=> game loop ends

=> game loop starts
unset a key in observable map
recompute "fulfill" -> false
comparator -> was:false, now:false    ** was false????????

This happens randomly and there are a lot of observables in action. So I am still unable to reduce it.

Any hints where I can look at?

kevinresol commented 4 years ago

Ok I think I got it, seems like the result of a computation is not recorded (as a new revision?) if the computation is not triggered by a dependency "from inside" of the computation itself. Here is a reproducible snippet, all traces are expected to print true:

import tink.state.*;

using tink.CoreApi;

class Main {
    public static function main() {
        var baseMap = new ObservableMap<Int, Entity>([]);

        function query(key:String) {
            final entityQueries = {
                final cache = new Map<Int, Pair<Entity, Observable<Bool>>>();
                Observable.auto(() -> {
                    for (id => entity in baseMap)
                        if (!cache.exists(id)) {
                            var first = true;
                            cache.set(id, new Pair(entity, Observable.auto(() -> {
                                final v = entity.fulfills(key);
                                if (first)
                                    first = false
                                else
                                    trace('$entity: re-compute fulfill: $v [$key]');
                                v;
                            }, (now, last) -> {
                                    trace('$entity: last:$last, now:$now [$key]');
                                    last == now;
                                })));
                        }

                    final deleted = [for (id in cache.keys()) if (!baseMap.exists(id)) id];

                    for (id in deleted)
                        cache.remove(id);

                    cache;
                },
                    (_, _) -> false // we're always returning the same map, so the comparator must always yield false
                );
            }

            return Observable.auto(() -> {
                trace('(re)compute query list [$key]');
                [for (p in entityQueries.value) if (p.b) p.a];
            });
        }

        // in the following test, `result` should contain entities whose subMap contains a 'foo' key

        final list = query('foo');
        var result = null;
        list.bind(v -> result = v, Scheduler.direct);

        trace(result.map(e -> e.id).contains(0) == false); // ok (start empty)
        trace(query('foo').value.map(e -> e.id).contains(0) == false); // make sure we have the same result from a fresh query

        final entity0 = new Entity(0);
        final entity1 = new Entity(1);
        Scheduler.atomically(() -> {
            entity0.subMap.set('foo', entity1);
            baseMap.set(0, entity0);
            baseMap.set(1, entity1);
        });

        trace(result.map(e -> e.id).contains(0) == true); // ok (because added 'foo')
        trace(query('foo').value.map(e -> e.id).contains(0) == true); // make sure we have the same result from a fresh query

        final entity2 = new Entity(2);
        Scheduler.atomically(() -> {
            baseMap.set(2, entity2);
            entity0.subMap.remove('foo');
        });

        trace(result.map(e -> e.id).contains(0) == false); // ok (because removed 'foo')
        trace(query('foo').value.map(e -> e.id).contains(0) == false); // make sure we have the same result from a fresh query

        Scheduler.atomically(() -> {
            entity0.subMap.set('foo', entity1);
        });

        trace(result.map(e -> e.id).contains(0) == true); // not ok
        trace(query('foo').value.map(e -> e.id).contains(0) == true); // make sure we have the same result from a fresh query
    }
}

class Entity {
    public final id:Int;
    public final subMap:ObservableMap<String, Entity> = new ObservableMap([]);

    public function new(id) {
        this.id = id;
    }

    public function fulfills(key:String) {
        return subMap.exists(key);
    }

    public function toString() {
        return '$id';
    }
}