haxetink / tink_state Goto Github PK
View Code? Open in Web Editor NEWHandle those pesky states.
License: The Unlicense
Handle those pesky states.
License: The Unlicense
Should allow updating multiple states "at the same time" (e.g. at the end of a transition) without firing direct bindings inbetween.
The issue is the same as in the tink_core ticket: haxetink/tink_core#165
Or will adding an exception catching mode to tink_core also fulfill the same purpose for tink_state?
I made a small test program which checks how it works with both signals and observables. The results are more scary than I thought. If any signal handler / observable binding throws an exception, not only this prevents other handlers / bindings from being called with the current signal / state change, but none of the handlers/bindings ever run again on further signals / state changes.
The number of updates increases over time it seems.
Somehow I would like to have a signal to notify when a key is added/removed. For now I can observe the key list and compare the old vs new one but doesn't seems to be very smart.
From the gitter chat:
I wonder if tink_state should/could have a
Futuristic
(or something) type, which is likePromised
but withoutFailed
state, basically an observable forFuture
rather thanPromise
.
in our codebase we use
Future
instead ofPromise
quite a lot, because in a lot of cases the error case is handled globally and leads to a "something went wrong, reload your game" modal dialog, especially when it comes to backend communication. so there's no error handling on a higher level. now if some model have data initialized like this, it can only have two states:Loading
andDone
:)
Currently the follow test fails.
It is not updating the mapped observable after the origin changed.
public function mapAsync() {
final s = new State(0);
final o:Observable<Int> = s;
final m = o.mapAsync(v -> Future.delay(0, v));
final fired = [];
m.bind(v -> fired.push(v)); // observe the mapped value
haxe.Timer.delay(() -> {
s.set(1); // update state
trace(fired);
haxe.Timer.delay(() -> {
asserts.assert(Std.string(fired[0]) == 'Loading');
asserts.assert(Std.string(fired[1]) == 'Done(0)');
asserts.assert(Std.string(fired[2]) == 'Done(1)'); // not fired
asserts.assert(fired.length == 3);
asserts.done();
}, 10);
}, 0);
return asserts;
}
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:
fulfill=true
is not memorized and the comparator on next loop still thinks the last value is false
=> 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?
Seems like they only work with spherical chicken in a vacuum ...
While benchmarking vs. MobX, I've noticed tink_state taxes the GC quite a bit more. Haven't profiled it yet, but two things seem to stand out as relatively obvious:
Invalidator
is the basis of all things and it subscribes every Invalidatable
only once. The usage of a CallbackList
is therefore not necessary, and the allocation of the CallbackLink
can be avoided in favor of a method to unsubscribe an Invalidatable
(by identity) again.SubscribtionTo
can be inlined into AutoObservable
.Some helpers to debug observables would be good, in particular getting the dependency graphs of auto-observables.
Should add an update function to State
that can access the current value without creating dependencies. Particularly useful for autorun
, e.g.:
autorun(() -> {
someDependencies;
someState.value += 1;// will create infinite loop
});
// vs.:
autorun(() -> {
someDependencies;
someState.update(v -> v + 1);
});
At least that's the most plausible explanation for a bug I've observed. Have to find a way to repro though.
Somehow I accidentally used .set()
instead of .push()
to add a new value and discovered this bug. Not sure if worth fixing.
var arr = new ObservableArray<Bool>();
trace(arr.length); // 0
arr.set(0, true);
trace(arr.length); // 0
Disabled for now da8902c
To accompany haxetink/tink_core#161,
bind
and value
are available on all ObservableObject
implementors at runtime (and for StateObject
there should be a setter
for value
).Promised
is usableauto
will be interesting ... perhaps there should simply be an asyncAuto
for async computationsSo essentially make it Loading(prev:Option<T>)
I found this useful in coconut, because I don't want to clear up the visual while the value is refreshing.
There should be a way to opt out of it, but generally speaking if changes to observable A trigger an update of state B, then B should be computed from A.
While previously it would cause the scheduler to enter an eager loop, this was now fixed in be5a620. It is now even possible to have cycles such as this one on async platforms:
var s1 = new State(0),
s2 = new State(0);
s1.observe().bind(function (v) s2.set(v + 1));
s2.observe().bind(function (v) s1.set(v + 1));
It will trash the CPU, but it will not block it. When updates take longer than 10ms, the next batch of updates is scheduled to run later. The above cycle is of course absolute non-sense, but if there's an exit condition, it will be reached eventually, without blowing up everything else in the process.
This opens potential for simplification in coconut.ui.
I don't know if this is still actually a think. But if it is then the Cannot create closure on abstract inline method
errors need to be dealt with.
The compute function is wrapped by an object, which means extra allocations for each call to e.g. auto()
. Is there a reason for that?
Following #30 parts of the documentation are outdated.
Stuff like getRevision
or onInvalidate
ends up in the public API, I guess that's not intended?
Should also return true
for auto observables that have no dependencies. Subscriptions on constant observables would just noop out.
As reference, MobX is able to do the following:
const {observable, autorun} = require("mobx");
const map1 = observable.map();
autorun(() => {
console.log('compute');
console.log(map1.has('foo'))
})
console.log(0);
map1.set('foo', '1'); // computes
console.log(1);
map1.set('bar', '2'); // no compute because 'bar' key is not involved
console.log(2);
map1.set('foo', '3'); // no compute because existence of 'foo' unchanged
console.log(3);
map1.delete('foo'); // computes
console.log(4);
console.log('==================')
const map2 = observable.map();
autorun(() => {
console.log('compute');
console.log(map2.get('foo'));
})
console.log(0);
map2.set('foo', '1'); // computes
console.log(1);
map2.set('bar', '2'); // no compute because 'bar' key is not involved
console.log(2);
map2.set('foo', '3'); // computes
console.log(3);
map2.set('foo', '3'); // no compute because value unchanged
console.log(4);
I think it is done by internally caching the request: https://github.com/mobxjs/mobx/blob/6daafc4f7930f807dd047bae1be55938e95017e5/src/types/observablemap.ts#L115-L131
class Main {
static function main() {
var map = new tink.state.ObservableMap<String, String>(new Map());
map.set('foo', 'bar');
for(key in map.keys()) trace(key);
for(key in map.keys()) trace(key);
}
}
The above code is expected to print foo
twice but actually only once.
Because map.keys()
returns the same iterator which is already depleted.
class Main {
static function main() {
new haxe.Timer(4000).run = function() {
var memory = js.Node.process.memoryUsage();
inline function format(v:Float)
return Std.int(v / 1024 / 1024 * 100) / 100;
trace('${Date.now().toString()}: Heap: ${format(memory.heapUsed)} / ${format(memory.heapTotal)} MB');
};
var map = new tink.state.ObservableMap<String, String>([]);
new haxe.Timer(100).run = function() {
for(id in map.keys()) trace(id);
}
}
}
The above code will create an ObservableIterator
every 100ms, and the memory usage will go up over time. From what I investigated I suspect the changes.nextTime()
given to the measurement caused the leak.
Hi,
got error with haxe preview version 5.
tink_state/0,10,0/src/tink/state/ObservableMap.hx:13: lines 13-84 : Field keyValueIterator needed by haxe.IMap is missing
and warning
tink_state/0,10,0/src/tink/state/ObservableMap.hx:13: characters 38-52 : Warning : This typedef is deprecated in favor of haxe.IMap<IMap.K, IMap.V>
Following thrown when compiled with hxnodejs:
tink/state/State.hx:106: characters 7-14 : You cannot access the js package while in a macro (for js.Node)
Note, haxe version is set to edge "2b6424c". UPD: reproduced with latest haxe as well.
Currently, the computation always finishes, even when there are no observers left.
In the following the binding only fires Loading, but expected to also fire Done(1)
final p = Promise.trigger();
final o = Observable.auto(() -> p.asPromise());
o.bind(v -> trace(Std.string(v)), Scheduler.direct);
trace(Std.string(o.value)); // Loading
p.resolve(1);
trace(Std.string(o.value)); // Done(1)
That would allow for some optimization.
I've found an interesting thing while porting your excellent library to C#. When AutoObservable
checks if any of the subscriptions has actually changed it breaks earlier and skips checking other dependencies, which sounds logical... However hasChanged
in the current revision also updates the revision and value for a Subscription
, and if we only do that for the first changed dependency and skip all other, the subsValid
will return false
and we'll need a second loop iteration.
At first I was thinking that the correct fix would be to simply remove break
here, however after some investigation, I think it's even better to update Subscription.lastRev
on reuse
. What do you think?
var state = new State(0);
state.observe().changed.handle(function(_) trace('state changed: ${state.value}'));
var mapping = state.observe().map(function(_) return state.value + 1);
// trace(mapping.value); // uncomment this line to make it work
mapping.changed.handle(function(_) trace('mapping changed ${mapping.value}'));
state.set(1);
state.set(2);
The above code prints:
state changed: 1
state changed: 2
which means the map is not working.
It is most likely caused by the valid filter here
Currently, auto observables subscribe in a rather inefficient matter:
It should be solvable in a more efficient manner, although self-invaliding computations make things rather challenging.
Hi,
noticed that LinkObject
is being used in src/tink/state/internal/Binding.hx
but there is no import from tink.core.Callback.LinkObject
,
because of that could not compile my app, is it intentional? and I am missing something or its bug ?
thank you
Comes from here: haxetink/tink_multipart@c6ce308
Any idea?
Sometimes weird things happen because of the from/to that Observable and State have with plain values. The latter hasn't yet cause problems for me, but the former can lead to some headaches, in particular the @:from T
for Observable<T>
.
I propose instead to introduce something like:
@:forward
abstract Var<T>(Observable<T>) from Observable<T> to Observable<T> {
@:from static function ofConst<T>(value:T):Var<T> return Observable.const(value);
}
Migration path would be to add this in the next release and generate warnings when implicit conversion from constants to observables is used, and then remove it relatively soon after. Thoughts?
I was using SignalObservable to make a bridge between legacy systems and observables, but looks like there's a bug:
import tink.core.Signal;
import tink.state.Observable;
import tink.state.Scheduler;
function main() {
var value = 1;
var changed = Signal.trigger();
var signalObservable = new Observable(() -> value, changed);
var derivedObservable = Observable.auto(() -> signalObservable.value);
function change(newValue) {
trace("changing");
value = newValue;
changed.trigger(Noise);
}
trace("first bind");
var link = derivedObservable.bind(v -> trace("bind callback: " + v), Scheduler.direct);
change(2);
trace("unbind");
link.cancel();
trace("second bind");
derivedObservable.bind(v -> trace("bind callback: " + v), Scheduler.direct);
change(3); // no bind callback invokation here!
trace(signalObservable.value); // ok: 3
trace(derivedObservable.value); // wrong: 2
}
src/Main.hx:18: first bind
src/Main.hx:19: bind callback: 1
src/Main.hx:13: changing
src/Main.hx:19: bind callback: 2
src/Main.hx:22: unbind
src/Main.hx:25: second bind
src/Main.hx:26: bind callback: 2
src/Main.hx:13: changing
src/Main.hx:29: 3
src/Main.hx:30: 2
Hi,
I'm trying to figure out how to use ObservableArray
for add/remove item notifications. I am using tink_state 1.0.0-beta.2, but also tried 0.11.1. In the older version there were (already deprecated) properties observableLength
and observableValues
. Binding to those properties works correctly for me in 0.11.1. In 1.0.0-beta.2 they no longer work and I cannot figure out what API should I use instead.
The following code returns Loading
on the second value access, unless we bind (and thus wake up and do the triggerAsync thing). Only happens on the v1
branch, master
is fine:
import tink.core.Future;
import tink.state.Observable;
function main() {
var o = Observable.auto(() -> new Future(trigger -> {
return haxe.Timer.delay(() -> trigger(10), 500).stop;
}));
trace(o.value);
haxe.Timer.delay(() -> {
trace(o.value); // still Loading
}, 1000);
}
Consider using something more lightweight for mapAsync
/ combineAsync
and the like.
The current approach thrashes quite a bit of memory.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.