diff --git a/.haxerc b/.haxerc index f8137ff..cf76fd6 100644 --- a/.haxerc +++ b/.haxerc @@ -1,4 +1,4 @@ { - "version": "4.2.0", + "version": "4.2.3", "resolveLibs": "scoped" } \ No newline at end of file diff --git a/bench/Bench.hx b/bench/Bench.hx index 09cbfd3..523bcba 100644 --- a/bench/Bench.hx +++ b/bench/Bench.hx @@ -11,20 +11,21 @@ class Bench { }); return todos; } - measure('create 10000 todos', () -> makeTodos(1000), 100); + var count = 1000; + measure('creating ${count} todos', () -> makeTodos(count), 100); - var todos = makeTodos(1000); - for (mode in ['direct', 'batched', 'atomic']) - measure('toggle 1000 todos [$mode]', () -> { + var todos = makeTodos(count); + for (mode in ['direct', 'batched', 'atomic']) { + var unfinishedTodoCount = Observable.auto(() -> { + var sum = 0; + for (t in todos) + if (!t.done.value) sum++; + sum; + }); - var unfinishedTodoCount = Observable.auto(() -> { - var sum = 0; - for (t in todos) - if (!t.done.value) sum++; - sum; - }); + var watch = unfinishedTodoCount.bind(_ -> {}, if (mode == 'batched') null else Scheduler.direct); - var watch = unfinishedTodoCount.bind(_ -> {}, if (mode == 'batched') null else Scheduler.direct); + measure('toggling ${todos.length} todos [$mode]', () -> { function update() for (t in todos) @@ -38,13 +39,15 @@ class Bench { if (mode == 'batched') Observable.updateAll(); - watch.cancel(); }, switch mode { case 'atomic': 1000; case 'batched': 1000; default: 10; }); + + watch.cancel(); + } } static function measure(name, f:()->Void, ?repeat = 1) { diff --git a/bench/mobx-bench.js b/bench/mobx-bench.js index 7bffea9..99659bd 100644 --- a/bench/mobx-bench.js +++ b/bench/mobx-bench.js @@ -18,7 +18,8 @@ function measure(name, task, repeat = 1) { console.log(`${name} took ${(Date.now() - start) / repeat}ms (avg. of ${repeat} runs)`); } -measure('create 10000 todos', () => createTodos(1000), 100); +let count = 1000; +measure(`creating ${count} todos`, () => createTodos(count), 100); { let todos = createTodos(1000); @@ -40,24 +41,26 @@ measure('create 10000 todos', () => createTodos(1000), 100); } ['direct', 'batched', 'atomic'].forEach(mode => { - measure(`create 1000 todos, finish all [${mode}]`, () => { - let unfinishedTodoCount = computed(() => { - return todos.reduce((count, { done }) => done ? count : count + 1, 0); - }); + let unfinishedTodoCount = computed(() => { + return todos.reduce((count, { done }) => done ? count : count + 1, 0); + }); - let dispose = - (mode == 'batched') - ? autorun(() => unfinishedTodoCount.get(), { - scheduler: scheduler() - }) - : unfinishedTodoCount.observe(x => {}); + let dispose = + (mode == 'batched') + ? autorun(() => unfinishedTodoCount.get(), { + scheduler: scheduler() + }) + : unfinishedTodoCount.observe(x => {}); + + measure(`toggling ${todos.length} todos [${mode}]`, () => { let update = (mode == 'atomic') ? transaction : f => f(); update(() => { for (let item of todos) item.done = !item.done; }); - dispose(); }, { atomic: 1000, batched: 1000, direct: 10 }[mode]); + + dispose(); }); } \ No newline at end of file diff --git a/haxe_libraries/tink_core.hxml b/haxe_libraries/tink_core.hxml index 0a415b3..4fa6607 100644 --- a/haxe_libraries/tink_core.hxml +++ b/haxe_libraries/tink_core.hxml @@ -1,3 +1,3 @@ -# @install: lix --silent download "gh://github.com/haxetink/tink_core#e0ed6c33f6f396fb83397a590bee4c3d48ab2e17" into tink_core/2.0.2/github/e0ed6c33f6f396fb83397a590bee4c3d48ab2e17 --cp ${HAXE_LIBCACHE}/tink_core/2.0.2/github/e0ed6c33f6f396fb83397a590bee4c3d48ab2e17/src --D tink_core=2.0.2 \ No newline at end of file +# @install: lix --silent download "haxelib:/tink_core#2.1.1" into tink_core/2.1.1/haxelib +-cp ${HAXE_LIBCACHE}/tink_core/2.1.1/haxelib/src +-D tink_core=2.1.1 \ No newline at end of file diff --git a/haxe_libraries/tink_streams.hxml b/haxe_libraries/tink_streams.hxml index 3d33804..0db9043 100644 --- a/haxe_libraries/tink_streams.hxml +++ b/haxe_libraries/tink_streams.hxml @@ -1,6 +1,4 @@ -# @install: lix --silent download "gh://github.com/haxetink/tink_streams#5066a96c4a8b483479b6a8df8893eaf8922d3bea" into tink_streams/0.4.0/github/5066a96c4a8b483479b6a8df8893eaf8922d3bea +# @install: lix --silent download "gh://github.com/haxetink/tink_streams#f4478825ef0a30df1187f02a354ec61176b47b8b" into tink_streams/0.3.3/github/f4478825ef0a30df1187f02a354ec61176b47b8b -lib tink_core --cp ${HAXE_LIBCACHE}/tink_streams/0.4.0/github/5066a96c4a8b483479b6a8df8893eaf8922d3bea/src --D tink_streams=0.4.0 -# temp for development, delete this file when pure branch merged --D pure \ No newline at end of file +-cp ${HAXE_LIBCACHE}/tink_streams/0.3.3/github/f4478825ef0a30df1187f02a354ec61176b47b8b/src +-D tink_streams=0.3.3 \ No newline at end of file diff --git a/haxe_libraries/tink_testrunner.hxml b/haxe_libraries/tink_testrunner.hxml index d09dc08..d5a2d6f 100644 --- a/haxe_libraries/tink_testrunner.hxml +++ b/haxe_libraries/tink_testrunner.hxml @@ -1,7 +1,6 @@ -# @install: lix --silent download "gh://github.com/haxetink/tink_testrunner#45f704215ae28c3d864755036dc2ee63f7c44e8a" into tink_testrunner/0.9.0/github/45f704215ae28c3d864755036dc2ee63f7c44e8a +# @install: lix --silent download "gh://github.com/haxetink/tink_testrunner#866de8b991be89b969825b0c0f5565d51f96a6f7" into tink_testrunner/0.8.0/github/866de8b991be89b969825b0c0f5565d51f96a6f7 -lib ansi -lib tink_macro -lib tink_streams --cp ${HAXE_LIBCACHE}/tink_testrunner/0.9.0/github/45f704215ae28c3d864755036dc2ee63f7c44e8a/src --D tink_testrunner=0.9.0 ---macro addGlobalMetadata('ANSI.Attribute', "@:native('ANSIAttribute')", false) \ No newline at end of file +-cp ${HAXE_LIBCACHE}/tink_testrunner/0.8.0/github/866de8b991be89b969825b0c0f5565d51f96a6f7/src +-D tink_testrunner=0.8.0 \ No newline at end of file diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index eecd940..11f10ec 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -1,17 +1,5 @@ package tink.state; -private typedef BindingOptions = Deprecated<{ - ?direct:Bool, - ?comparator:T->T->Bool, -}>; - -@:forward -abstract Deprecated(T) { - @:deprecated - @:from static function of(v:X):Deprecated - return cast v; -} - /** Common representation of a piece of observable state. It can be read using the `value` property and bound to listen for changes using the `bind` method. @@ -56,16 +44,7 @@ abstract Observable(ObservableObject) from ObservableObject to Observab You can customize this behaviour by passing a different `scheduler` and `comparator` instances to this function. **/ - public function bind( - #if tink_state.legacy_binding_options ?options:BindingOptions, #end - callback:Callback, ?comparator:Comparator, ?scheduler:Scheduler - ):CallbackLink { - #if tink_state.legacy_binding_options - if (options != null) { - comparator = options.comparator; - if (options.direct) scheduler = Scheduler.direct; - } - #end + public function bind(callback:Callback, ?comparator:Comparator, ?scheduler:Scheduler):CallbackLink { if (scheduler == null) scheduler = Observable.scheduler; return Binding.create(this, callback, scheduler, comparator); @@ -121,18 +100,6 @@ abstract Observable(ObservableObject) from ObservableObject to Observab public function mapAsync(f:Transform>):Observable> return Observable.auto(() -> f.apply(this.getValue())); - @:deprecated('use auto instead') - public function switchSync(cases:Array<{ when: T->Bool, then: Lazy> } > , dfault:Lazy>):Observable - return Observable.auto(() -> { - var v = value; - for (c in cases) - if (c.when(v)) { - dfault = c.then; - break; - } - return dfault.get().value; - }); - static var scheduler:Scheduler = #if macro Scheduler.direct; @@ -172,12 +139,28 @@ abstract Observable(ObservableObject) from ObservableObject to Observab Returned `CallbackLink` object can be used to cancel the binding. **/ - static public function autorun(callback:()->Void, ?scheduler):CallbackLink { + static public function autorun(callback:Callback, ?scheduler):CallbackLink { var i = 0; - return auto(() -> { - callback(); + var link:CallbackLink = null, + cancelled = false; + + var cancel:CallbackLink = () -> { + cancelled = true; + link.cancel(); + } + + link = auto(() -> { + if (cancelled) return 0; + callback.invoke(cancel); i++; }).bind(ignore, null, scheduler); + + return + if (cancelled) { + link.cancel(); + null; + } + else link; } @:deprecated @@ -195,8 +178,8 @@ abstract Observable(ObservableObject) from ObservableObject to Observab will take place and the type of the observable value will become `tink.state.Promised` or `tink.State.Promised.Predicted` respectively. The future/promise will be automatically handled to update the value of this Observable. **/ - @:noUsing static public inline function auto(compute, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end):Observable - return new AutoObservable(compute, comparator #if tink_state.debug , toString, pos #end); + @:noUsing static public inline function auto(compute, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end):Observable + return new AutoObservable(compute, comparator #if tink_state.debug , toString, pos #end); /** Create a constant Observable object from a value. Const observables are lightweight objects @@ -263,7 +246,7 @@ private class ConstObservable implements ObservableObject { return revision; public function canFire() - return true; + return false; public function new(value, ?toString:()->String #if tink_state.debug , ?pos:haxe.PosInfos #end) { this.value = value; @@ -276,6 +259,9 @@ private class ConstObservable implements ObservableObject { #end } + function retain() {} + function release() {} + public function getValue() return value; @@ -297,18 +283,18 @@ private class ConstObservable implements ObservableObject { return EmptyIterator.DEPENDENCIES; #end - public function onInvalidate(i:Invalidatable):CallbackLink - return null; + public function subscribe(i:Observer) {} + public function unsubscribe(i:Observer) {} } -private class SimpleObservable extends Invalidator implements ObservableObject { +private class SimpleObservable extends Dispatcher implements ObservableObject { var _poll:Void->Measurement; var _cache:Measurement = null; var comparator:Comparator; public function new(poll, ?comparator #if tink_state.debug , ?toString, ?pos #end) { - super(#if tink_state.debug toString, pos #end); + super(null #if tink_state.debug , toString, pos #end); this._poll = poll; this.comparator = comparator; } @@ -321,7 +307,7 @@ private class SimpleObservable extends Invalidator implements ObservableObjec function reset(_) { _cache = null; - fire(); + fire(this); } function poll() { diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index 2b591bd..0abeeb8 100644 --- a/src/tink/state/ObservableArray.hx +++ b/src/tink/state/ObservableArray.hx @@ -23,7 +23,7 @@ abstract ObservableArray(ArrayImpl) from ArrayImpl to Observable this.get(index)); + return Observable.auto(() -> get(index)); @:deprecated('use iterator instead') public function values() @@ -33,7 +33,7 @@ abstract ObservableArray(ArrayImpl) from ArrayImpl to Observable(ArrayView) from ArrayView { public function keys() return 0...this.length; - @:op([]) public inline function get(index) - return this.get(index); + @:op([]) public function get(index) { + return + if (AutoObservable.needsTracking(this)) { + var wrappers = AutoObservable.currentAnnex().get(Wrappers).forSource(this); + + wrappers.get(index, () -> new TransformObservable( + this, + _ -> this.get(index), + null, + () -> wrappers.remove(index) + #if tink_state.debug , () -> 'Entry $index of ${this.toString()}' #end + )).value; + } + else this.get(index); + } public function toArray():Array return this.copy(); @@ -131,7 +144,7 @@ private interface ArrayView extends ObservableObject> { function keyValueIterator():ArrayKeyValueIterator; } -private class ArrayImpl extends Invalidator implements ArrayView { +private class ArrayImpl extends Dispatcher implements ArrayView { var valid = false; var entries:Array; @@ -139,12 +152,21 @@ private class ArrayImpl extends Invalidator implements ArrayView { public var length(get, never):Int; function get_length() - return calc(() -> entries.length); + return observableLength.value; public function new(entries) { - super(#if tink_state.debug id -> 'ObservableArray#$id${this.entries.toString()}' #end); + super(#if tink_state.debug id -> 'ObservableArray#$id[${this.entries.toString()}]' #end); this.entries = entries; - this.observableLength = new TransformObservable(this, _ -> this.entries.length, null #if tink_state.debug , () -> 'length of ${toString()}' #end); + this.observableLength = new TransformObservable( + this, + _ -> { + valid = true; + this.entries.length; + }, + null, + null + #if tink_state.debug , () -> 'length of ${this.toString()}' #end + ); } public function replace(values:Array) @@ -192,8 +214,10 @@ private class ArrayImpl extends Invalidator implements ArrayView { public function shift() return update(() -> entries.shift()); - public function get(index:Int) - return calc(() -> entries[index]); + public function get(index:Int) { + valid = true; + return entries[index]; + } public function set(index:Int, value:T) return update(() -> entries[index] = value); @@ -226,7 +250,7 @@ private class ArrayImpl extends Invalidator implements ArrayView { var ret = fn(); if (valid) { valid = false; - fire(); + fire(this); } return ret; } @@ -238,11 +262,46 @@ private class ArrayImpl extends Invalidator implements ArrayView { } } +private class Wrappers { + final bySource = new Map<{}, SourceWrappers>(); + + public function new(target:{}) {} + + public function forSource(source:ArrayView):SourceWrappers + return cast switch bySource[source] { + case null: bySource[source] = new SourceWrappers(() -> bySource.remove(source)); + case v: v; + } +} + +private class SourceWrappers { + final dispose:()->Void; + var count = 0; + final observables = new Map>(); + + public function new(dispose) + this.dispose = dispose; + + public function get(index, create:() -> Observable):Observable + return switch observables[index] { + case null: + count++; + observables[index] = create(); + case v: v; + } + + public function remove(index:Int) { + if (observables.remove(index) && (--count == 0)) dispose(); + } +} + private class DerivedView implements ArrayView { + final observableLength:Observable; + public var length(get, never):Int; function get_length() - return o.value.length; + return observableLength.value; final o:Observable>; @@ -252,11 +311,19 @@ private class DerivedView implements ArrayView { public function canFire() return self().canFire(); - public function new(o) + public function new(o) { this.o = o; + this.observableLength = new TransformObservable( + o, + a -> a.length, + null, + null + #if tink_state.debug , () -> 'length of ${toString()}' #end + ); + } public function get(index:Int) - return o.value[index]; + return self().getValue()[index]; inline function self() return (o:ObservableObject>); @@ -279,8 +346,11 @@ private class DerivedView implements ArrayView { public function isValid() return self().isValid(); - public function onInvalidate(i) - return self().onInvalidate(i); + public function subscribe(i) + self().subscribe(i); + + public function unsubscribe(i) + self().unsubscribe(i); public function copy() return o.value.copy(); @@ -297,4 +367,7 @@ private class DerivedView implements ArrayView { public function keyValueIterator() return o.value.keyValueIterator(); + function retain() {} + function release() {} + } \ No newline at end of file diff --git a/src/tink/state/ObservableDate.hx b/src/tink/state/ObservableDate.hx index 5b5c6d4..0cb10f0 100644 --- a/src/tink/state/ObservableDate.hx +++ b/src/tink/state/ObservableDate.hx @@ -25,8 +25,11 @@ class ObservableDate implements ObservableObject { public function getValue() return _observable.getValue(); - public function onInvalidate(i) - return _observable.onInvalidate(i); + public function subscribe(i) + _observable.subscribe(i); + + public function unsubscribe(i) + _observable.unsubscribe(i); public function new(?date:Date) { @@ -74,4 +77,7 @@ class ObservableDate implements ObservableObject { public function getComparator() return null; + + function retain() {} + function release() {} } diff --git a/src/tink/state/ObservableMap.hx b/src/tink/state/ObservableMap.hx index cffcd61..9358c27 100644 --- a/src/tink/state/ObservableMap.hx +++ b/src/tink/state/ObservableMap.hx @@ -4,22 +4,33 @@ import haxe.Constraints.IMap; import haxe.iterators.*; @:forward -abstract ObservableMap(MapImpl) from MapImpl to IMap { +@:multiType(@:followWithAbstracts K) +abstract ObservableMap(MapImpl) from MapImpl { public var view(get, never):ObservableMapView; inline function get_view() return this; - public function new(init:Map) - this = new MapImpl(init.copy()); + public function new(); - @:op([]) public inline function get(index) - return this.get(index); + @:op([]) public function get(key) + return + if (AutoObservable.needsTracking(this)) + AutoObservable.currentAnnex().get(Wrappers).forSource(this).get(key).value; + else + this.get(key); @:op([]) public inline function set(index, value) { this.set(index, value); return value; } + public function exists(key) + return + if (AutoObservable.needsTracking(this)) + AutoObservable.currentAnnex().get(Wrappers).forSource(this).exists(key).value; + else + this.exists(key); + public function toMap():Map return view.toMap(); @@ -27,7 +38,23 @@ abstract ObservableMap(MapImpl) from MapImpl to IMap { return view.copy(); public function entry(key:K) - return Observable.auto(this.get.bind(key)); + return Observable.auto(() -> get(key)); + + @:to static function toIntMap(dict:MapImpl):MapImpl + return new MapImpl(new Map(), IntMaps.INST); + + @:to static function toEnumValueMap(dict:MapImpl):MapImpl + return new MapImpl(new Map(), EnumValueMaps.INST); + + @:to static function toStringMap(dict:MapImpl):MapImpl + return new MapImpl(new Map(), StringMaps.INST); + + @:to static function toObjectMap(dict:MapImpl):MapImpl<{}, V> + return new MapImpl<{}, V>(new Map(), ObjectMaps.INST); + + static public inline function of(m:Map):ObservableMap + // This runtime lookup here is messy, but I don't see what else we could do ... + return cast new MapImpl(m.copy(), DynamicFactory.of(m)); } @:forward @@ -39,7 +66,7 @@ abstract ObservableMapView(MapView) from MapView { return cast this.copy(); public function copy():ObservableMap - return new MapImpl(cast this.copy()); + return new MapImpl(cast this.copy(), this.getFactory()); public function entry(key:K) return Observable.auto(this.get.bind(key)); @@ -47,6 +74,7 @@ abstract ObservableMapView(MapView) from MapView { private interface MapView extends ObservableObject> { function copy():IMap; + function getFactory():MapFactory; function exists(key:K):Bool; function get(key:K):Null; function iterator():Iterator; @@ -54,75 +82,21 @@ private interface MapView extends ObservableObject> { function keyValueIterator():KeyValueIterator; } -private class Derived implements MapView { - final o:Observable>; - public function new(o) - this.o = o; - - public function canFire() - return self().canFire(); - - public function getRevision() - return self().getRevision(); - - public function exists(key:K):Bool - return o.value.exists(key); - - public function get(key:K):Null - return o.value.get(key); - - public function iterator():Iterator - return o.value.iterator(); - - public function keys():Iterator - return o.value.keys(); - - public function keyValueIterator():KeyValueIterator - return o.value.keyValueIterator(); - - public function copy():IMap - return cast o.value.copy(); - - inline function self() - return (o:ObservableObject>); - - public function getValue() - return this; - - public function isValid() - return self().isValid(); - - public function onInvalidate(i) - return self().onInvalidate(i); - - function neverEqual(a, b) - return false; - - public function getComparator() - return neverEqual; - - #if tink_state.debug - public function getObservers() - return self().getObservers(); - - public function getDependencies() - return self().getDependencies(); - - @:keep public function toString() - return 'ObservableMapView#${o.value.toString()}'; - #end -} - -private class MapImpl extends Invalidator implements MapView implements IMap { +private class MapImpl extends Dispatcher implements MapView implements IMap { var valid = false; - var entries:Map; + final entries:Map; + final factory:MapFactory; - public function new(entries:Map) { + public function new(entries, factory) { super(); this.entries = entries; + this.factory = factory; } + public function getFactory() + return factory; + public function observe():Observable> return this; @@ -132,14 +106,18 @@ private class MapImpl extends Invalidator implements MapView impleme public function getValue():MapView return this; - public function get(k:K):Null - return calc(() -> entries.get(k)); + public function get(k:K):Null { + valid = true; + return entries.get(k); + } public function set(k:K, v:V):Void update(() -> { entries.set(k, v); null; }); - public function exists(k:K):Bool - return calc(() -> entries.exists(k)); + public function exists(k:K):Bool { + valid = true; + return entries.exists(k); + } public function remove(k:K):Bool return update(() -> entries.remove(k)); @@ -175,7 +153,7 @@ private class MapImpl extends Invalidator implements MapView impleme var ret = fn(); if (valid) { valid = false; - fire(); + fire(this); } return ret; } @@ -190,4 +168,105 @@ private class MapImpl extends Invalidator implements MapView impleme public function getDependencies() return EmptyIterator.DEPENDENCIES; #end +} + +private interface MapFactory { + function createMap():Map; +} + +private class IntMaps implements MapFactory { + static public final INST = new IntMaps(); + function new() {} + public function createMap():Map + return new Map(); +} + +private class StringMaps implements MapFactory { + static public final INST = new StringMaps(); + function new() {} + public function createMap():Map + return new Map(); +} + +private class ObjectMaps implements MapFactory<{}> { + static public final INST = new ObjectMaps(); + function new() {} + public function createMap():Map<{}, X> + return new Map(); +} + +private class EnumValueMaps implements MapFactory { + static public final INST = new EnumValueMaps(); + function new() {} + public function createMap():Map + return new Map(); +} + +private class DynamicFactory { + static public function of(m:Map):MapFactory { + var cl:Class = Type.getClass(m); + return + if (cl == haxe.ds.IntMap) cast IntMaps.INST; + else if (cl == haxe.ds.StringMap) cast StringMaps.INST; + else if (cl == haxe.ds.EnumValueMap) cast EnumValueMaps.INST; + else cast ObjectMaps.INST; + } +} + +private class Wrappers { + final bySource = new Map<{}, SourceWrappers>(); + + public function new(target:{}) {} + + public function forSource(source:MapView):SourceWrappers + return cast switch bySource[source] { + case null: bySource[source] = new SourceWrappers(source, () -> bySource.remove(source)); + case v: v; + } +} + +private class SourceWrappers {// TODO: it's probably better to split this in two + final dispose:()->Void; + final source:MapView; + final entries:Map>; + final existences:Map>; + + var count = 0; + + public function new(source, dispose) { + this.source = source; + this.dispose = dispose; + var factory = source.getFactory(); + this.entries = factory.createMap(); + this.existences = factory.createMap(); + } + + public function get(key) + return switch entries[key] { + case null: + count++; + entries[key] = new TransformObservable( + source, + o -> o.get(key), + null, + () -> if (entries.remove(key) && (--count == 0)) dispose() + #if tink_state.debug , () -> 'Entry for $key in ${source.toString()}' #end + ); + case v: v; + } + + public function exists(key) + return switch existences[key] { + case null: + count++; + existences[key] = new TransformObservable( + source, + o -> o.exists(key), + null, + () -> if (existences.remove(key) && (--count == 0)) dispose() + #if tink_state.debug , () -> 'Existence of $key in ${source.toString()}' #end + + ); + case v: v; + } } \ No newline at end of file diff --git a/src/tink/state/Promised.hx b/src/tink/state/Promised.hx index 8f79291..5da06da 100644 --- a/src/tink/state/Promised.hx +++ b/src/tink/state/Promised.hx @@ -13,7 +13,7 @@ enum PromisedWith { class PromisedTools { static public function next(a:Promised, f:Next):Promise return switch a { - case Loading: Promise.NEVER #if (tink_core < "2" && haxe_ver >= "4.2") .next(_ -> (null:B)) #end; + case Loading: Promise.never(); case Failed(e): e; case Done(a): f(a); } diff --git a/src/tink/state/State.hx b/src/tink/state/State.hx index 91fdc69..5ce5aea 100644 --- a/src/tink/state/State.hx +++ b/src/tink/state/State.hx @@ -67,18 +67,32 @@ private class CompoundState implements StateObject { public function getValue() return data.getValue(); - public function onInvalidate(i) - return data.onInvalidate(i); - #if tink_state.debug - public function getObservers() - return data.getObservers();//TODO: this is not very exact + final observers = new ObjectMap(); - public function getDependencies() - return [(cast data:Observable)].iterator(); + public function subscribe(i) { + observers[i] = i; + data.subscribe(i); + } + + public function unsubscribe(i) { + if (observers.remove(i)) + data.unsubscribe(i); + } - @:keep public function toString() - return 'CompoundState[${data.toString()}]';//TODO: perhaps this should be providable from outside + public function getObservers() + return observers.iterator(); + + public function getDependencies() + return [(cast data:Observable)].iterator(); + + @:keep public function toString() + return 'CompoundState[${data.toString()}]';//TODO: perhaps this should be providable from outside + #else + public function subscribe(i) + data.subscribe(i); + public function unsubscribe(i) + data.unsubscribe(i); #end @@ -89,6 +103,9 @@ private class CompoundState implements StateObject { public function getComparator() return this.comparator; + + function retain() {} + function release() {} } private class GuardedState extends SimpleState { @@ -117,7 +134,7 @@ private class GuardedState extends SimpleState { } } -private class SimpleState extends Invalidator implements StateObject { +private class SimpleState extends Dispatcher implements StateObject { final comparator:Comparator; var value:T; @@ -125,14 +142,10 @@ private class SimpleState extends Invalidator implements StateObject { public function isValid() return true; - public function new(value, ?comparator, ?onStatusChange:Bool->Void #if tink_state.debug , ?toString, ?pos #end) { - super(#if tink_state.debug toString, pos #end); + public function new(value, ?comparator, ?onStatusChange #if tink_state.debug , ?toString, ?pos #end) { + super(onStatusChange #if tink_state.debug , toString, pos #end); this.value = value; this.comparator = comparator; - if (onStatusChange != null) { - list.ondrain = onStatusChange.bind(false); - list.onfill = onStatusChange.bind(true); - } } public function getValue() @@ -160,7 +173,7 @@ private class SimpleState extends Invalidator implements StateObject { if (!comparator.eq(value, this.value)) { this.value = value; - fire(); + fire(this); } return value; } diff --git a/src/tink/state/debug/Logger.hx b/src/tink/state/debug/Logger.hx index ce280f5..69a7b40 100644 --- a/src/tink/state/debug/Logger.hx +++ b/src/tink/state/debug/Logger.hx @@ -7,7 +7,7 @@ class Logger { public function unsubscribed(source:Observable, derived:Observable) {} public function connected(source:Observable, derived:Observable) {} public function disconnected(source:Observable, derived:Observable) {} - public function triggered(source:Observable, watcher:Invalidatable) {} + public function triggered(source:Observable, watcher:Observer) {} public function revalidating(source:Observable) {} public function revalidated(source:Observable, reused:Bool) {} public function filter(match) @@ -50,13 +50,13 @@ class StringLogger extends Logger { override function disconnected(source:Observable, derived:Observable) output('${derived.toString()} disconnected from ${source.toString()}'); - override function triggered(source:Observable, watcher:Invalidatable) + override function triggered(source:Observable, watcher:Observer) output('${watcher.toString()} triggered by ${source.toString()}'); - - override function revalidating(source:Observable) + + override function revalidating(source:Observable) output('${source.toString()} revalidating'); - - override function revalidated(source:Observable, reused:Bool) + + override function revalidated(source:Observable, reused:Bool) output('${source.toString()} revalidated (reused=$reused)'); } @@ -78,7 +78,7 @@ class LoggerGroup extends Logger { override public function disconnected(source:Observable, derived:Observable) for (l in loggers) l.disconnected(source, derived); - override public function triggered(source:Observable, watcher:Invalidatable) + override public function triggered(source:Observable, watcher:Observer) for (l in loggers) l.triggered(source, watcher); override public function revalidating(source:Observable) @@ -105,7 +105,7 @@ class Filter extends Logger { if (match(source)) logger.connected(source, derived); override public function disconnected(source:Observable, derived:Observable) if (match(source)) logger.disconnected(source, derived); - override public function triggered(source:Observable, watcher:Invalidatable) + override public function triggered(source:Observable, watcher:Observer) if (match(source)) logger.triggered(source, watcher); override public function revalidating(source:Observable) if (match(source)) logger.revalidating(source); diff --git a/src/tink/state/import.hx b/src/tink/state/import.hx index 533ddcf..6237422 100644 --- a/src/tink/state/import.hx +++ b/src/tink/state/import.hx @@ -9,4 +9,4 @@ import tink.core.Signal; import tink.core.Lazy; import tink.state.Promised; import tink.state.internal.*; -import tink.state.internal.Invalidatable; +import tink.state.internal.Observer; diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 71ea0d2..196d4b4 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -3,133 +3,14 @@ package tink.state.internal; #if tink_state.debug import tink.state.debug.Logger.inst as logger; #end +import tink.core.Annex; -@:callable -@:access(tink.state.internal.AutoObservable) -private abstract Computation((a:AutoObservable,?Noise)->T) { - inline function new(f) this = f; - - @:from static function asyncWithLast(f:Option->Promise):Computation> { - var link:CallbackLink = null, - last = None, - ret = Loading; - return new Computation((a, ?_) -> { - ret = Loading; - var prev = link; - link = f(last).handle(o -> a.update(ret = switch o { - case Success(v): last = Some(v); Done(v); - case Failure(e): Failed(e); - })); - prev.cancel(); - return ret; - }); - } - - @:from static function async(f:()->Promise):Computation> { - var link:CallbackLink = null, - ret = Loading; - return new Computation((a, ?_) -> { - ret = Loading; - var prev = link; - link = f().handle(o -> a.update(ret = switch o { - case Success(v): Done(v); - case Failure(e): Failed(e); - })); - prev.cancel(); - return ret; - }); - } - - @:from static function safeAsync(f:()->Future):Computation> { - var link:CallbackLink = null, - ret = Loading; - return new Computation((a, ?_) -> { - ret = Loading; - var prev = link; - link = f().handle(v -> a.update(ret = Done(v))); - prev.cancel(); - return ret; - }); - } - - @:from static inline function withLast(f:Option->T):Computation { - var last = None; - return new Computation((_, ?_) -> { - var ret = f(last); - last = Some(ret); - return ret; - }); - } - - @:from static function sync(f:()->T) { - return new Computation((_, ?_) -> f()); - } -} - -private typedef Subscription = SubscriptionTo; - -private class SubscriptionTo { - - public final source:ObservableObject; - var last:T; - var lastRev:Revision; - var link:CallbackLink; - final owner:Invalidatable; - - public var used = true; - - public function new(source, cur, owner:AutoObservable) { - this.source = source; - this.last = cur; - this.lastRev = source.getRevision(); - this.owner = owner; - - if (owner.hot) connect(); - } - - public inline function isValid() - return source.getRevision() == lastRev; - - public inline function hasChanged():Bool { - var nextRev = source.getRevision(); - if (nextRev == lastRev) return false; - lastRev = nextRev; - var before = last; - last = source.getValue(); - return !source.getComparator().eq(last, before); - } - - public inline function reuse(value:T) { - used = true; - last = value; - } - - public inline function disconnect():Void { - #if tink_state.debug - logger.disconnected(source, cast owner); - #end - link.cancel(); - } - - public inline function connect():Void { - #if tink_state.debug - logger.connected(source, cast owner); - #end - this.link = source.onInvalidate(owner); - } -} - -private enum abstract AutoObservableStatus(Int) { - var Dirty; - var Computed; -} - -class AutoObservable extends Invalidator - implements Invalidatable implements Derived implements ObservableObject { +@:allow(tink.state.internal) +class AutoObservable extends Dispatcher + implements Observer implements Derived implements ObservableObject { static var cur:Derived; - var compute:Computation; #if hotswap static var rev = new State(0); static function onHotswapLoad() { @@ -137,12 +18,18 @@ class AutoObservable extends Invalidator } #end public var hot(default, null) = false; - var status = Dirty; + public var value(get, never):T; + function get_value() + return track(this); + + final annex:Annex<{}>; + var status = Fresh; var last:T = null; var subscriptions:Array; var dependencies = new ObjectMap, Subscription>(); - var comparator:Comparator; + final comparator:Comparator; + var computation:Computation; override function getRevision() { if (hot) @@ -157,6 +44,9 @@ class AutoObservable extends Invalidator return revision; } + public function getAnnex() + return annex; + function subsValid() { if (subscriptions == null) return false; @@ -168,35 +58,42 @@ class AutoObservable extends Invalidator return true; } + public function swapComputation(c:Computation) { + this.computation = c; + this.status = Fresh; + fire(this); + } + public function isValid() - return status != Dirty && (hot || subsValid()); + return status == Computed && (hot || subsValid()); public function getComparator() return comparator; - public function new(compute, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end) { - super(#if tink_state.debug toString, pos #end); - this.compute = compute; + public function new(computation:Computation, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end) { + super(active -> if (active) wakeup() else sleep() #if tink_state.debug , toString, pos #end); + this.computation = computation.init(this); this.comparator = comparator; - this.list.onfill = () -> inline heatup(); - this.list.ondrain = () -> inline cooldown(); + this.annex = new Annex<{}>(this); } - function heatup() { - getValue(); - getRevision(); + function wakeup() { + computation.wakeup(); + hot = true; if (subscriptions != null) for (s in subscriptions) s.connect(); - hot = true; + getValue(); + getRevision(); } - function cooldown() { + function sleep() { + computation.sleep(); hot = false; if (subscriptions != null) for (s in subscriptions) s.disconnect(); } - static public inline function computeFor(o:Derived, fn:()->T) { + static public function computeFor(o:Derived, fn:()->T) { var before = cur; cur = o; #if hotswap @@ -208,33 +105,64 @@ class AutoObservable extends Invalidator } static public inline function untracked(fn:()->T) - return computeFor(null, fn); + return inline computeFor(null, fn); - static public inline function track(o:ObservableObject):V { - var ret = o.getValue(); - if (cur != null && o.canFire()) - cur.subscribeTo(o, ret); - return ret; + static public inline function needsTracking(o:ObservableObject):Bool + return switch cur { + case null: false; + case v: !v.isSubscribedTo(o); + } + + static public function currentAnnex() + return switch cur { + case null: null; + case v: v.getAnnex(); + } + + static public inline function track(o:ObservableObject):V + return + if (cur != null && o.canFire()) + cur.subscribeTo(o); + else + o.getValue(); + + function triggerAsync(v:T) { + last = v; + fire(this); + if (subscriptions.length == 0) dispose(); } public function getValue():T { function doCompute() { - status = Computed; - if (subscriptions != null) - for (s in subscriptions) s.used = false; + status = Computing; + var prevSubs = subscriptions; + if (prevSubs != null) + for (s in prevSubs) s.used = false; subscriptions = []; - sync = true; - last = computeFor(this, () -> compute(this)); - sync = false; + last = computeFor(this, () -> computation.getNext()); + + if (status == Computing) + status = Computed; #if tink_state.debug logger.revalidated(this, false); #end - if (subscriptions.length == 0) dispose(); + + if (prevSubs != null) + for (s in prevSubs) + if (!s.used) { + #if tink_state.debug + logger.unsubscribed(s.source, this); + #end + dependencies.remove(s.source); + if (hot) s.disconnect(); + s.release(); + } + + if (subscriptions.length == 0 && !computation.isPending()) dispose(); } - var prevSubs = subscriptions, - count = 0; + var count = 0; while (!isValid()) { #if tink_state.debug @@ -242,7 +170,7 @@ class AutoObservable extends Invalidator #end if (++count == Observable.MAX_ITERATIONS) throw 'no result after ${Observable.MAX_ITERATIONS} attempts'; - else if (subscriptions != null) { + else if (status != Fresh) { var valid = true; for (s in subscriptions) @@ -257,52 +185,48 @@ class AutoObservable extends Invalidator logger.revalidated(this, true); #end } - else { - doCompute(); - if (prevSubs != null) { - for (s in prevSubs) - if (!s.used) { - if (hot) s.disconnect(); - dependencies.remove(s.source); - #if tink_state.debug - logger.unsubscribed(s.source, this); - #end - } - } - } + else doCompute(); } else doCompute(); } return last; } - var sync = true; - - function update(value) if (!sync) { - last = value; - fire(); - } + public function subscribeTo(source:ObservableObject):R + return + switch dependencies.get(source) { + case null: + #if tink_state.debug + logger.subscribed(source, this); + #end + var sub:Subscription = new Subscription(source, hot, this); + source.retain(); + dependencies.set(source, sub); + subscriptions.push(sub); + sub.last; + case v: + if (!v.used) { + v.reuse(source.getValue()); + subscriptions.push(v); + v.last; + } + else source.getValue(); + } - public function subscribeTo(source:ObservableObject, cur:R):Void - switch dependencies.get(source) { - case null: - #if tink_state.debug - logger.subscribed(source, this); - #end - var sub:Subscription = cast new SubscriptionTo(source, cur, this); - dependencies.set(source, sub); - subscriptions.push(sub); - case v: - if (!v.used) { - v.reuse(cur); - subscriptions.push(v); - } + public function isSubscribedTo(source:ObservableObject) + return switch dependencies.get(source) { + case null: false; + case s: s.used; } - public function invalidate() - if (status == Computed) { - status = Dirty; - fire(); + public function notify(from:ObservableObject) + switch status { + case Computed: + status = Dirty; + fire(this); + case Computing: + status = Dirty; + default: } #if tink_state.debug @@ -312,5 +236,68 @@ class AutoObservable extends Invalidator } private interface Derived { - function subscribeTo(source:ObservableObject, cur:R):Void; + function getAnnex():Annex<{}>; + function isSubscribedTo(source:ObservableObject):Bool; + function subscribeTo(source:ObservableObject):R; +} + + +private class Subscription { + + public final source:ObservableObject; + public var last(default, null):Any; + var lastRev:Revision; + final owner:Observer; + + public var used = true; + + public function new(source, hot, owner) { + this.source = source; + this.lastRev = source.getRevision(); + this.owner = owner; + if (hot) connect(); + this.last = source.getValue(); + } + + public inline function isValid() + return source.getRevision() == lastRev; + + public function hasChanged():Bool { + var nextRev = source.getRevision(); + if (nextRev == lastRev) return false; + lastRev = nextRev; + var before = last; + last = source.getValue(); + return !source.getComparator().eq(last, before); + } + + public inline function reuse(value:Any) { + used = true; + last = value; + } + + public inline function disconnect():Void { + #if tink_state.debug + logger.disconnected(source, cast owner); + #end + source.unsubscribe(owner); + } + + public inline function connect():Void { + #if tink_state.debug + logger.connected(source, cast owner); + #end + source.subscribe(owner); + } + + public inline function release() { + source.release(); + } +} + +private enum abstract AutoObservableStatus(Int) { + var Dirty; + var Computed; + var Computing; + var Fresh; } \ No newline at end of file diff --git a/src/tink/state/internal/Binding.hx b/src/tink/state/internal/Binding.hx index b3cd617..5e8c057 100644 --- a/src/tink/state/internal/Binding.hx +++ b/src/tink/state/internal/Binding.hx @@ -1,25 +1,23 @@ package tink.state.internal; -class Binding implements Invalidatable implements Scheduler.Schedulable implements LinkObject { +class Binding implements Observer implements Scheduler.Schedulable implements LinkObject { var data:ObservableObject; var cb:Callback; var scheduler:Scheduler; var comparator:Comparator; var status = Valid; var last:Null = null; - final link:CallbackLink; static public function create(o:ObservableObject, cb, ?scheduler, comparator):CallbackLink { - var value = Observable.untracked(() -> o.getValue()); return - if (o.canFire()) new Binding(o, value, cb, scheduler, comparator); + if (o.canFire()) new Binding(o, cb, scheduler, comparator); else { - cb.invoke(value); + cb.invoke(Observable.untracked(() -> o.getValue())); null; } } - function new(data, value, cb, ?scheduler, ?comparator) { + function new(data, cb, ?scheduler, ?comparator) { this.data = data; this.cb = cb; this.scheduler = switch scheduler { @@ -27,8 +25,8 @@ class Binding implements Invalidatable implements Scheduler.Schedulable imple case v: v; } this.comparator = data.getComparator().or(comparator); - link = data.onInvalidate(this); - cb.invoke(this.last = value); + data.subscribe(this); + cb.invoke(this.last = Observable.untracked(() -> data.getValue())); } #if tink_state.debug @@ -38,12 +36,12 @@ class Binding implements Invalidatable implements Scheduler.Schedulable imple return 'Binding#$id[${data.toString()}]';//TODO: position might be helpful too #end - public function cancel() { - link.cancel(); + public function cancel() if (status != Canceled) { + data.unsubscribe(this); status = Canceled; } - public function invalidate() + public function notify(_) if (status == Valid) { status = Invalid; scheduler.schedule(this); diff --git a/src/tink/state/internal/Computation.hx b/src/tink/state/internal/Computation.hx new file mode 100644 index 0000000..5493532 --- /dev/null +++ b/src/tink/state/internal/Computation.hx @@ -0,0 +1,248 @@ +package tink.state.internal; + +@:forward +abstract Computation(ComputationObject) from ComputationObject { + + inline function new(v) + this = v; + + @:from static function ofAsyncWithLast(f:(last:Option)->Promise):Computation> + return new AsyncWithLast(f); + + @:from static function ofAsync(f:()->Promise):Computation> + return new Async(f); + + @:from static function ofSafeAsyncWithLast(f:(last:Option)->Future):Computation> + return new SafeAsyncWithLast(f); + + @:from static function ofSafeAsync(f:()->Future):Computation> + return new SafeAsync(f); + + @:from static function ofSync(f:()->Data):Computation + return new Sync(f); + + @:from static function ofSyncWithLast(f:(last:Option)->Data):Computation + return new SyncWithLast(f); +} + +/** + * This whole part is pretty dirty, but it's well suited to avoid having too much state in AutoObservable. + * To avoid doing so, the computation itself is allowed to be stateful: + * + * - Stateless computations will return themselves when initialized with any owner. + * - Stateful computations start out ownerless, but when initialized a second time, + * will return a copy of themselves with a different owner. + * + * There is some pretty horrible coupling going on. In particular, AutoObservable will only call sleep/wakeup + * from its own sleep/wakup. When calling getNext, it expects the computation to arrange its internal state + * based on whether the owner is hot or not. This is really only necessary for async computations and it's luckily + * unified in the heavy handed generalization that is AsyncBase. + */ +private interface ComputationObject { + function init(owner:AutoObservable):ComputationObject; + function getNext():Result; + function isPending():Bool; + function wakeup():Void; + function sleep():Void; +} + +private class StatefulBase implements ComputationObject { + var owner:AutoObservable = null; + function new(?owner) + this.owner = owner; + + public function init(owner:AutoObservable):ComputationObject + return switch owner { + case null: + this.owner = owner; + this; + case _ == this.owner => true: // unlikely, but who knows ... + this; + default: cloneFor(owner); + } + + function cloneFor(owner:AutoObservable):ComputationObject + return throw 'abstract'; + public function getNext():Result + return throw 'abstract'; + + public function isPending():Bool + return false; + + public function wakeup():Void {} + public function sleep():Void {} + +} + +private class Async extends AsyncBase> { + final get:()->Promise; + + public function new(get, ?owner) { + super(owner); + this.get = get; + } + + override function cloneFor(owner:AutoObservable>):ComputationObject> + return new Async(get, owner); + + override function pull():Promise + return get(); + + override function wrap(raw:Outcome):Promised + return switch raw { + case Success(v): + Done(v); + case Failure(e): + Failed(e); + } +} + +private class AsyncWithLast extends AsyncBase> { + final get:(o:Option)->Promise; + var last = None; + + public function new(get, ?owner) { + super(owner); + this.get = get; + } + + override function cloneFor(owner:AutoObservable>):ComputationObject> + return new AsyncWithLast(get, owner); + + override function pull():Promise + return get(last); + + override function wrap(raw:Outcome):Promised + return switch raw { + case Success(v): + last = Some(v); + Done(v); + case Failure(e): + Failed(e); + } +} + +private class AsyncBase extends StatefulBase> { + + var result:Future; + var link:CallbackLink; + var sync = false; + + function pull():Future + return throw 'abstract'; + + function wrap(raw:Raw):PromisedWith + return throw 'abstract'; + + override public function getNext():PromisedWith { + var prev = result; + result = pull(); + + if (result != prev && owner.hot) { + link.cancel(); + listen(result); + } + + return switch result.status { + case Ready(v) if (v.computed): + wrap(v.get()); + default: Loading; + } + } + + override function sleep() + link.cancel(); + + inline function listen(r:Future) { + sync = true; + link = r.handle(o -> if (!sync) owner.triggerAsync(wrap(o))); + sync = false; + } + + override function isPending():Bool + return !result.status.match(Ready(_)); + + override function wakeup() + switch result { + case null: + case p: listen(p); + } +} + +private class Sync implements ComputationObject { + + final get:()->T; + + public function new(get) + this.get = get; + + public function init(_) + return this; + + public function getNext():T + return get(); + + public function sleep() {} + public function wakeup() {} + public function isPending():Bool + return false; +} + +private class SyncWithLast extends StatefulBase { + final get:Option->T; + var last = None; + + public function new(get, ?owner) { + super(owner); + this.get = get; + } + + override function cloneFor(owner:AutoObservable):ComputationObject + return new SyncWithLast(get, owner); + + override public function getNext():T { + var ret = get(last); + last = Some(ret); + return ret; + } +} + + +private class SafeAsync extends AsyncBase { + final get:()->Future; + + public function new(get, ?owner) { + super(owner); + this.get = get; + } + + override function cloneFor(owner:AutoObservable>):ComputationObject> + return new SafeAsync(get, owner); + + override function pull():Future + return get(); + + override function wrap(raw:T):Predicted + return Done(raw); +} + +private class SafeAsyncWithLast extends AsyncBase { + final get:(o:Option)->Future; + var last = None; + + public function new(get, ?owner) { + super(owner); + this.get = get; + } + + override function cloneFor(owner:AutoObservable>):ComputationObject> + return new SafeAsyncWithLast(get, owner); + + override function pull():Future + return get(last); + + override function wrap(raw:T):Predicted { + last = Some(raw); + return Done(raw); + } +} \ No newline at end of file diff --git a/src/tink/state/internal/Dispatcher.hx b/src/tink/state/internal/Dispatcher.hx new file mode 100644 index 0000000..5561ac1 --- /dev/null +++ b/src/tink/state/internal/Dispatcher.hx @@ -0,0 +1,67 @@ +package tink.state.internal; + +import tink.core.Disposable; + +class Dispatcher extends SimpleDisposable { + var revision = new Revision(); + final observers = new OrderedObjectMap(); + final onStatusChange:(watched:Bool)->Void; + static function noop(_) {} + #if tink_state.debug + static var counter = 0; + final id = counter++; + final _toString:()->String; + @:keep public function toString() + return Observable.untracked(_toString); + #end + var used = 0; + function new(?onStatusChange #if tink_state.debug , ?toString:(id:Int)->String, ?pos:haxe.PosInfos #end) { + super(() -> observers.clear()); + this.onStatusChange = switch onStatusChange { + case null: noop; + case v: v; + } + #if tink_state.debug + this._toString = switch toString { + case null: () -> Type.getClassName(Type.getClass(this)) + '#$id(${pos.fileName}:${pos.lineNumber})'; + case v: v.bind(id); + } + #end + } + + function retain() {} + function release() {} + + public function canFire() + return !disposed; + + public function getRevision() + return revision; + + public function subscribe(i:Observer) { + if (observers.exists(i) || disposed) null; + var wasEmpty = observers.size == 0; + observers[i] = i; + if (wasEmpty) onStatusChange(true); + } + + public function unsubscribe(i:Observer) { + observers.remove(i); + if (observers.size == 0) onStatusChange(false); + } + + #if tink_state.debug + public function getObservers() + return observers.iterator(); + #end + + function fire(from:ObservableObject) { + revision = new Revision(); + for (v in observers) { + #if tink_state.debug + tink.state.debug.Logger.inst.triggered(from, v); + #end + v.notify(from); + } + } +} \ No newline at end of file diff --git a/src/tink/state/internal/EmptyIterator.hx b/src/tink/state/internal/EmptyIterator.hx index ce3334b..b86d372 100644 --- a/src/tink/state/internal/EmptyIterator.hx +++ b/src/tink/state/internal/EmptyIterator.hx @@ -2,7 +2,7 @@ package tink.state.internal; class EmptyIterator { #if tink_state.debug - static public final OBSERVERS = new EmptyIterator(); + static public final OBSERVERS = new EmptyIterator(); static public final DEPENDENCIES = new EmptyIterator>(); #end public function new() {} diff --git a/src/tink/state/internal/Invalidatable.hx b/src/tink/state/internal/Invalidatable.hx deleted file mode 100644 index f298c5c..0000000 --- a/src/tink/state/internal/Invalidatable.hx +++ /dev/null @@ -1,79 +0,0 @@ -package tink.state.internal; - -import tink.core.Disposable.OwnedDisposable; - -interface Invalidatable { - function invalidate():Void; - #if tink_state.debug - @:keep function toString():String; - #end -} - -class Invalidator implements OwnedDisposable { - var revision = new Revision(); - final observers = new ObjectMap(); - final list = new CallbackList();//TODO: get rid of the list ... currently primarily here to guarantee stable callback order - #if tink_state.debug - static var counter = 0; - final id = counter++; - final _toString:()->String; - @:keep public function toString() - return Observable.untracked(_toString); - #end - var used = 0; - function new(#if tink_state.debug ?toString:(id:Int)->String, ?pos:haxe.PosInfos #end) { - #if tink_state.debug - this._toString = switch toString { - case null: () -> Type.getClassName(Type.getClass(this)) + '#$id(${pos.fileName}:${pos.lineNumber})'; - case v: v.bind(id); - } - #end - } - - public var disposed(get, never):Bool; - inline function get_disposed() - return list.disposed; - - public function ondispose(d:()->Void) - list.ondispose(d); - - - public inline function dispose() { - list.dispose(); - observers.clear(); - } - - public function canFire() - return !disposed; - - public function getRevision() - return revision; - - public function onInvalidate(i:Invalidatable):CallbackLink - return - if (observers.get(i) || list.disposed) null; - else { - observers.set(i, true); - list.add( - #if tink_state.debug - _ -> { - if (Std.is(this, ObservableObject)) - tink.state.debug.Logger.inst.triggered(cast this, i); - i.invalidate(); - } - #else - _ -> i.invalidate() - #end - ) & () -> observers.remove(i); - } - - #if tink_state.debug - public function getObservers() - return observers.keys(); - #end - - function fire() { - revision = new Revision(); - list.invoke(Noise); - } -} \ No newline at end of file diff --git a/src/tink/state/internal/ObjectMap.hx b/src/tink/state/internal/ObjectMap.hx index eb8ec2b..ebd064b 100644 --- a/src/tink/state/internal/ObjectMap.hx +++ b/src/tink/state/internal/ObjectMap.hx @@ -1,6 +1,6 @@ package tink.state.internal; -@:forward(keys, exists, clear) +@:forward(keys, iterator, exists, clear) abstract ObjectMap(haxe.ds.ObjectMap) { public inline function new() this = new haxe.ds.ObjectMap(); diff --git a/src/tink/state/internal/ObjectMap.js.hx b/src/tink/state/internal/ObjectMap.js.hx index b79979c..a8f7330 100644 --- a/src/tink/state/internal/ObjectMap.js.hx +++ b/src/tink/state/internal/ObjectMap.js.hx @@ -2,7 +2,7 @@ package tink.state.internal; import js.lib.*; -@:forward(clear) +@:forward(clear, size) abstract ObjectMap(Map) { public inline function new() this = new Map(); @@ -22,9 +22,18 @@ abstract ObjectMap(Map) { return try new HaxeIterator(this.keys()) catch (e:Dynamic) {// because IE11 - var keys = []; - forEach((_, k, _) -> keys.push(k)); - keys.iterator(); + var ret = []; + forEach((_, k, _) -> ret.push(k)); + ret.iterator(); + } + + public function iterator():Iterator + return + try new HaxeIterator(this.values()) + catch (e:Dynamic) {// because IE11 + var ret = []; + forEach((v, _, _) -> ret.push(v)); + ret.iterator(); } public inline function remove(key) diff --git a/src/tink/state/internal/ObservableObject.hx b/src/tink/state/internal/ObservableObject.hx index c388ecd..0b18311 100644 --- a/src/tink/state/internal/ObservableObject.hx +++ b/src/tink/state/internal/ObservableObject.hx @@ -1,14 +1,18 @@ package tink.state.internal; +@:allow(tink.state.internal) interface ObservableObject { + private function retain():Void; + private function release():Void; function getValue():T; function getRevision():Revision; function isValid():Bool; function getComparator():Comparator; - function onInvalidate(i:Invalidatable):CallbackLink; + function subscribe(i:Observer):Void; + function unsubscribe(i:Observer):Void; function canFire():Bool; #if tink_state.debug - function getObservers():Iterator; + function getObservers():Iterator; function getDependencies():Iterator>; @:keep function toString():String; #end diff --git a/src/tink/state/internal/Observer.hx b/src/tink/state/internal/Observer.hx new file mode 100644 index 0000000..3249eb5 --- /dev/null +++ b/src/tink/state/internal/Observer.hx @@ -0,0 +1,8 @@ +package tink.state.internal; + +interface Observer { + function notify(from:ObservableObject):Void; + #if tink_state.debug + @:keep function toString():String; + #end +} \ No newline at end of file diff --git a/src/tink/state/internal/OrderedObjectMap.hx b/src/tink/state/internal/OrderedObjectMap.hx new file mode 100644 index 0000000..ba9801f --- /dev/null +++ b/src/tink/state/internal/OrderedObjectMap.hx @@ -0,0 +1,79 @@ +package tink.state.internal; + +@:forward(iterator, exists, clear) +abstract OrderedObjectMap(Impl) { + public var size(get, never):Int; + inline function get_size() + return this.keyCount; + + public inline function new() + this = new Impl(); + + @:op([]) public inline function get(key:K) + return this.get(key); + + public inline function keys() + return this.compact().iterator(); + + public function iterator() + return new ImplIterator(this); + + @:op([]) public function set(key, value) { + if (!this.exists(key)) + this.add(key); + this.set(key, value); + return value; + } + + public inline function remove(key) + return this.remove(key) && this.subtract(key); + + public inline function forEach(f) + for (k in this.compact()) f(get(k), k, (cast this:ObjectMap)); + + public inline function count() + return this.keyCount; + +} + +private class Impl extends haxe.ds.ObjectMap { + public final keyOrder:Array = []; + public var keyCount:Int = 0; + public inline function add(key:K) { + keyOrder.push(key); + keyCount++; + } + + public function compact() { + if (keyCount < keyOrder.length) { + var pos = 0; + for (k in keyOrder) + if (k != null) + keyOrder[pos++] = k; + keyOrder.resize(keyCount); + } + return keyOrder; + } + + public function subtract(key:K) { + keyOrder[keyOrder.indexOf(key)] = null; + keyCount--; + return true; + } +} + +class ImplIterator { + final keys:Array; + final target:haxe.ds.ObjectMap; + var pos = 0; + public inline function new(i:Impl) { + this.keys = i.compact(); + this.target = i; + } + + public inline function hasNext() + return pos < keys.length; + + public inline function next() + return target.get(keys[pos++]); +} \ No newline at end of file diff --git a/src/tink/state/internal/OrderedObjectMap.js.hx b/src/tink/state/internal/OrderedObjectMap.js.hx new file mode 100644 index 0000000..0928ea9 --- /dev/null +++ b/src/tink/state/internal/OrderedObjectMap.js.hx @@ -0,0 +1,5 @@ +package tink.state.internal; + +import js.lib.*; + +typedef OrderedObjectMap = ObjectMap; \ No newline at end of file diff --git a/src/tink/state/internal/SignalObservable.hx b/src/tink/state/internal/SignalObservable.hx index 62a4afd..bc07c89 100644 --- a/src/tink/state/internal/SignalObservable.hx +++ b/src/tink/state/internal/SignalObservable.hx @@ -20,6 +20,10 @@ class SignalObservable implements ObservableObject { this.get = get; this.changed = changed; this.changed.handle(_ -> if (valid) { + #if tink_state.debug + for (i in observers.keys()) + tink.state.debug.Logger.inst.triggered(this, i); + #end revision = new Revision(); valid = false; }); @@ -53,24 +57,18 @@ class SignalObservable implements ObservableObject { public function getComparator():Comparator return null; - public function onInvalidate(i:Invalidatable):CallbackLink - // TODO: this largely duplicates Invalidatable.onInvalidate - return - if (observers.get(i)) null; - else { - observers.set(i, true); - changed.handle( - #if tink_state.debug - _ -> { - if (Std.is(this, ObservableObject)) - tink.state.debug.Logger.inst.triggered(cast this, i); - i.invalidate(); - } - #else - _ -> i.invalidate() - #end - ) & () -> observers.remove(i); - } + function retain() {} + function release() {} + + public function subscribe(i:Observer) + if (!observers.exists(i)) observers[i] = changed.handle(() -> i.notify(this)); + + public function unsubscribe(i:Observer) { + switch observers[i] { + case null: + case v: + } + } #if tink_state.debug public function getObservers() diff --git a/src/tink/state/internal/TransformObservable.hx b/src/tink/state/internal/TransformObservable.hx index 5d87377..d63365f 100644 --- a/src/tink/state/internal/TransformObservable.hx +++ b/src/tink/state/internal/TransformObservable.hx @@ -7,14 +7,19 @@ class TransformObservable implements ObservableObject { final transform:Transform; final source:ObservableObject; final comparator:Comparator; + var dispose:()->Void; #if tink_state.debug final _toString:()->String; #end - public function new(source, transform, ?comparator #if tink_state.debug , toString #end) { + public function new(source, transform, ?comparator, ?dispose #if tink_state.debug , toString #end) { this.source = source; this.transform = transform; this.comparator = comparator; + this.dispose = switch dispose { + case null: noop; + case v: v; + } #if tink_state.debug this._toString = toString; #end @@ -26,18 +31,34 @@ class TransformObservable implements ObservableObject { public function isValid() return lastSeenRevision == source.getRevision(); - public function onInvalidate(i) - return source.onInvalidate(i); - #if tink_state.debug - public function getObservers() - return source.getObservers(); + final observers = new ObjectMap(); + + public function subscribe(i) { + observers[i] = i; + source.subscribe(i); + } + + public function unsubscribe(i) { + if (observers.remove(i)) + source.unsubscribe(i); + } - public function getDependencies() - return [source].iterator(); - public function toString():String - return _toString(); + public function getObservers() + return observers.iterator(); + + public function getDependencies() + return [cast source].iterator(); + + public function toString():String + return _toString(); + #else + public function subscribe(i) + source.subscribe(i); + + public function unsubscribe(i) + source.unsubscribe(i); #end public function getValue() { @@ -54,4 +75,11 @@ class TransformObservable implements ObservableObject { public function canFire():Bool return source.canFire(); + + var retainCount = 0; + function retain() retainCount++; + function release() + if (--retainCount == 0) dispose(); + + static function noop() {} } \ No newline at end of file diff --git a/tests/RunTests.hx b/tests/RunTests.hx index 16d7034..d5b87ae 100644 --- a/tests/RunTests.hx +++ b/tests/RunTests.hx @@ -16,8 +16,10 @@ class RunTests { new TestScheduler(), new TestProgress(), new issues.Issue51(), + new issues.Issue60(), new issues.Issue61(), new issues.Issue63(), + new issues.Issue73(), ])).handle(Runner.exit); } } \ No newline at end of file diff --git a/tests/TestArrays.hx b/tests/TestArrays.hx index 533b7cb..fca3025 100644 --- a/tests/TestArrays.hx +++ b/tests/TestArrays.hx @@ -22,7 +22,7 @@ class TestArrays { function getLog() return log.join(',').replace('undefined', '-').replace('null', '-'); - function report(name:String) return (v:Null) -> log.push('$name:$v'); + function report(name:String) { return (v:Null) -> log.push('$name:$v'); } Observable.auto(() -> a.length).bind(report('l'), direct); @@ -64,6 +64,38 @@ class TestArrays { return asserts.done(); } + public function issue49() { + final arr = new ObservableArray([for (i in 0...10) i]); + + var computations = 0; + + final sum = Observable.auto(() -> { + computations++; + arr[2] + arr[5]; + }); + + function checkSum(?pos:haxe.PosInfos) + asserts.assert(sum.value == arr[2] + arr[5], null, pos); + + checkSum(); + asserts.assert(computations == 1); + + checkSum(); + asserts.assert(computations == 1); + + arr[1] = 0; + checkSum(); + asserts.assert(computations == 1); + asserts.assert(arr[1] == 0); + + arr[2] = 123; + checkSum(); + asserts.assert(computations == 2); + + + return asserts.done(); + } + public function iteration() { var counter = 0, a = new ObservableArray(); @@ -97,7 +129,7 @@ class TestArrays { .bind(() -> keysChanges++, direct); Observable.auto(() -> { - final first = 0; + var first = 0; for (v in a) { first += v; break; @@ -131,7 +163,7 @@ class TestArrays { public function clear() { final o = new ObservableArray>([1,2,3]); - final log = ''; + var log = ''; Observable.auto(() -> o.length).bind(v -> log += 'len:$v', direct); for(i in 0...o.length) o.entry(i).bind(v -> log += ',$i:$v', direct); diff --git a/tests/TestAuto.hx b/tests/TestAuto.hx index 6a4209b..5a9433e 100644 --- a/tests/TestAuto.hx +++ b/tests/TestAuto.hx @@ -70,6 +70,19 @@ class TestAuto { return asserts.done(); } + public function issue76() { + + final p = Promise.trigger(); + final o = Observable.auto(() -> p.asPromise()); + + var log = []; + o.bind(log.push, Scheduler.direct); + p.resolve(1); + + asserts.assert(Std.string(log) == '[Loading,Done(1)]'); + return asserts.done(); + } + public function testAsync() { final triggers = new Array>>(); @@ -101,6 +114,10 @@ class TestAuto { ); }); + final o = Observable.auto(() -> o.value); + + o.bind(function () {}); + asserts.assert(o.value.match(Loading)); asserts.assert(last.match(None)); yield(12); @@ -219,7 +236,7 @@ class TestAuto { final select = new State([for (i in 0...states.length) i % 3 == 0]); function add() { - final ret = 0; + var ret = 0; for (i => s in select.value) if (s) ret += states[i].value; return ret; diff --git a/tests/TestBasic.hx b/tests/TestBasic.hx index 34aa72d..52fb6c5 100644 --- a/tests/TestBasic.hx +++ b/tests/TestBasic.hx @@ -13,7 +13,7 @@ class TestBasic { public function donotFireEqual() { final s = new State(0), sLog = []; - final watch = s.observe().bind(sLog.push, (_, _) -> true, direct); + var watch = s.observe().bind(sLog.push, (_, _) -> true, direct); final o1Log = [], o1 = Observable.auto(() -> { diff --git a/tests/TestDate.hx b/tests/TestDate.hx index 89ebc43..889a58c 100644 --- a/tests/TestDate.hx +++ b/tests/TestDate.hx @@ -15,7 +15,7 @@ class TestDate { final d = new ObservableDate(), log = []; - final watch = d.becomesOlderThan(1.seconds()).bind(log.push); + var watch = d.becomesOlderThan(1.seconds()).bind(log.push); watch &= d.becomesOlderThan(10.seconds()).bind(log.push); Observable.updateAll(); diff --git a/tests/TestMaps.hx b/tests/TestMaps.hx index 7e3e085..d6191d7 100644 --- a/tests/TestMaps.hx +++ b/tests/TestMaps.hx @@ -1,5 +1,6 @@ package ; +import tink.state.internal.ObjectMap; import tink.state.Scheduler.direct; import tink.state.*; @@ -11,7 +12,7 @@ class TestMaps { public function new() {} public function testEntries() { - final o = new ObservableMap([5 => 0, 6 => 0]); + final o = ObservableMap.of([5 => 0, 6 => 0]); var a = []; @@ -74,7 +75,7 @@ class TestMaps { } public function testIterators() { - final map = new ObservableMap(new Map()); + final map = new ObservableMap(); map.set('key', 'value'); var count = 0; @@ -111,4 +112,59 @@ class TestMaps { return asserts.done(); } + + public function of() { + ObservableMap.of(new haxe.ds.IntMap()).set(1, 'foo'); + ObservableMap.of(new haxe.ds.StringMap()).set('1', 'foo'); + ObservableMap.of([{ foo: 213 } => '123']).set({ foo: 123 }, 'foo'); + + return asserts.done(); + } + + public function issue49() { + var o = ObservableMap.of([1 => 2]), + computations = 0; + + + final sum = Observable.auto(() -> { + computations++; + var ret = 0; + if (o.exists(2)) ret += o[2]; + if (o.exists(3)) ret += o[3]; + return ret; + }); + + asserts.assert(sum.value == 0); + asserts.assert(computations == 1); + + asserts.assert(sum.value == 0); + asserts.assert(computations == 1); + + o[5] = 5; + + asserts.assert(sum.value == 0); + asserts.assert(computations == 1); + + o[2] = 2; + + asserts.assert(sum.value == 2); + asserts.assert(computations == 2); + + o[3] = 3; + + asserts.assert(sum.value == 5); + asserts.assert(computations == 3); + + o[4] = 4; + o.remove(5); + + asserts.assert(sum.value == 5); + asserts.assert(computations == 3); + + o.remove(2); + asserts.assert(sum.value == 3); + asserts.assert(computations == 4); + + return asserts.done(); + } } \ No newline at end of file diff --git a/tests/TestScheduler.hx b/tests/TestScheduler.hx index 0135312..bd7e014 100644 --- a/tests/TestScheduler.hx +++ b/tests/TestScheduler.hx @@ -36,7 +36,7 @@ class TestScheduler { final log = []; - final watch = s1.observe().bind(v -> { + var watch = s1.observe().bind(v -> { s2.set('foo($v)'); s3.set('bar($v)'); }); diff --git a/tests/issues/Issue51.hx b/tests/issues/Issue51.hx index b924726..5102e26 100644 --- a/tests/issues/Issue51.hx +++ b/tests/issues/Issue51.hx @@ -9,7 +9,7 @@ class Issue51 { public function new() {} public function testNested() { - final baseMap = new ObservableMap([]); + final baseMap = new ObservableMap(); function query(key:String) { final entityQueries = { @@ -75,7 +75,7 @@ class Issue51 { private class Entity { public final id:Int; - public final subMap:ObservableMap = new ObservableMap([]); + public final subMap:ObservableMap = new ObservableMap(); public function new(id) { this.id = id; @@ -85,4 +85,3 @@ private class Entity { return '$id'; } } - \ No newline at end of file diff --git a/tests/issues/Issue60.hx b/tests/issues/Issue60.hx new file mode 100644 index 0000000..5be6107 --- /dev/null +++ b/tests/issues/Issue60.hx @@ -0,0 +1,81 @@ +package issues; + +import tink.state.State; +import tink.state.Observable; +using tink.CoreApi; + +@:asserts +class Issue60 { + public function new() {} + public function test() { + var counter = new State(0), + triggers = [], + futures = []; + + function load() { + var value = counter.value; + var trigger = new FutureTrigger(); + var future = new Future(fire -> trigger.handle(() -> fire(value))); + + // there's probably no need for arrays here, but whatever + triggers.push(trigger); + futures.push(future); + + return future; + } + + function progress() + asserts.assert(triggers[triggers.length - 1].trigger(Noise)); + + function status() + return futures[futures.length - 1].status; + + final o = Observable.auto(load); + + function eager() { + return o.bind(function () {}); + } + + var binding = eager(); + + asserts.assert(o.value.match(Loading)); + asserts.assert(status().match(Awaited)); + + progress(); + + asserts.assert(o.value.match(Done(0))); + asserts.assert(status().match(Ready(_))); + + counter.value++; + + asserts.assert(o.value.match(Loading)); + asserts.assert(status().match(Awaited)); + + progress(); + + asserts.assert(o.value.match(Done(1))); + asserts.assert(status().match(Ready(_))); + + counter.value++; + + asserts.assert(o.value.match(Loading)); + asserts.assert(status().match(Awaited)); + + binding.cancel(); + + asserts.assert(o.value.match(Loading)); + asserts.assert(status().match(Suspended)); + + binding = eager(); + + asserts.assert(o.value.match(Loading)); + asserts.assert(status().match(Awaited)); + + progress(); + + asserts.assert(o.value.match(Done(2))); + asserts.assert(status().match(Ready(_))); + + return asserts.done(); + } +} \ No newline at end of file diff --git a/tests/issues/Issue73.hx b/tests/issues/Issue73.hx new file mode 100644 index 0000000..1d00f37 --- /dev/null +++ b/tests/issues/Issue73.hx @@ -0,0 +1,43 @@ +package issues; + +import tink.state.internal.AutoObservable; +import tink.state.*; + +@:asserts +class Issue73 { + public function new() {} + public function test() { + + var log = ''; + + final s1 = new State(2, (active) -> log += (if (active) '+' else '-') + '1'), + s2 = new State(3, (active) -> log += (if (active) '+' else '-') + '2'); + + final sum = Observable.auto(() -> s1.value + s2.value), + product = Observable.auto(() -> s1.value * s2.value); + + final a = new AutoObservable(() -> product.value - sum.value); + + (a:Observable).bind(() -> {}); + + asserts.assert(log == '+1+2'); + asserts.assert(a.getValue() == 1); + + a.swapComputation(() -> product.value + sum.value); + + asserts.assert(log == '+1+2'); + asserts.assert(a.getValue() == 11); + + a.swapComputation(() -> product.value); + + asserts.assert(a.getValue() == 6); + asserts.assert(log == '+1+2'); + + a.swapComputation(() -> s1.value); + + asserts.assert(a.getValue() == 2); + asserts.assert(log == '+1+2-2'); + + return asserts.done(); + } +} \ No newline at end of file