From 5c7779429fb787c279b2c184476f129757fbdb1d Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 4 Jul 2021 17:53:13 +0200 Subject: [PATCH 01/45] Support haxe 4.1 --- src/tink/state/internal/TransformObservable.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tink/state/internal/TransformObservable.hx b/src/tink/state/internal/TransformObservable.hx index 5d87377..e38686a 100644 --- a/src/tink/state/internal/TransformObservable.hx +++ b/src/tink/state/internal/TransformObservable.hx @@ -34,7 +34,7 @@ class TransformObservable implements ObservableObject { return source.getObservers(); public function getDependencies() - return [source].iterator(); + return [cast source].iterator(); public function toString():String return _toString(); From e82515f1120ac2bf3a737a8ac355be77348608bb Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Wed, 7 Jul 2021 17:46:02 +0200 Subject: [PATCH 02/45] Minor tweak. --- src/tink/state/internal/AutoObservable.hx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 71ea0d2..2c854e3 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -78,19 +78,17 @@ private class SubscriptionTo { public var used = true; - public function new(source, cur, owner:AutoObservable) { + public function new(source, cur, owner) { 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 { + public function hasChanged():Bool { var nextRev = source.getRevision(); if (nextRev == lastRev) return false; lastRev = nextRev; @@ -290,6 +288,7 @@ class AutoObservable extends Invalidator logger.subscribed(source, this); #end var sub:Subscription = cast new SubscriptionTo(source, cur, this); + if (hot) sub.connect(); dependencies.set(source, sub); subscriptions.push(sub); case v: From 8692116659c095ac463f40d8b83e09d33e9f8da9 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 10:08:41 +0200 Subject: [PATCH 03/45] Add API to check if tracking is needed. --- src/tink/state/internal/AutoObservable.hx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 2c854e3..209c560 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -208,6 +208,13 @@ class AutoObservable extends Invalidator static public inline function untracked(fn:()->T) return computeFor(null, fn); + + static public inline function needsTracking(o:ObservableObject):Bool + return switch cur { + case null: false; + case v: !v.isSubscribedTo(o); + } + static public inline function track(o:ObservableObject):V { var ret = o.getValue(); if (cur != null && o.canFire()) @@ -298,6 +305,12 @@ class AutoObservable extends Invalidator } } + 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; @@ -311,5 +324,6 @@ class AutoObservable extends Invalidator } private interface Derived { + function isSubscribedTo(source:ObservableObject):Bool; function subscribeTo(source:ObservableObject, cur:R):Void; } \ No newline at end of file From cfefb498f094f7cd8bc904c382d2929fadb7f36d Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 10:43:17 +0200 Subject: [PATCH 04/45] Add retain/release --- src/tink/state/Observable.hx | 3 +++ src/tink/state/ObservableArray.hx | 3 +++ src/tink/state/ObservableDate.hx | 3 +++ src/tink/state/ObservableMap.hx | 3 +++ src/tink/state/State.hx | 5 ++++- src/tink/state/internal/AutoObservable.hx | 2 ++ src/tink/state/internal/Invalidatable.hx | 2 ++ src/tink/state/internal/ObservableObject.hx | 3 +++ src/tink/state/internal/SignalObservable.hx | 3 +++ src/tink/state/internal/TransformObservable.hx | 5 ++++- 10 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index 2d193c9..cd9d1b3 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -276,6 +276,9 @@ private class ConstObservable implements ObservableObject { #end } + function retain() {} + function release() {} + public function getValue() return value; diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index 2b591bd..335ed98 100644 --- a/src/tink/state/ObservableArray.hx +++ b/src/tink/state/ObservableArray.hx @@ -297,4 +297,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 073d48c..90f5af7 100644 --- a/src/tink/state/ObservableDate.hx +++ b/src/tink/state/ObservableDate.hx @@ -74,4 +74,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..a52f175 100644 --- a/src/tink/state/ObservableMap.hx +++ b/src/tink/state/ObservableMap.hx @@ -101,6 +101,9 @@ private class Derived implements MapView { public function getComparator() return neverEqual; + function retain() {} + function release() {} + #if tink_state.debug public function getObservers() return self().getObservers(); diff --git a/src/tink/state/State.hx b/src/tink/state/State.hx index 385b42b..801a6fb 100644 --- a/src/tink/state/State.hx +++ b/src/tink/state/State.hx @@ -72,7 +72,7 @@ private class CompoundState implements StateObject { #if tink_state.debug public function getObservers() - return data.getObservers();//TODO: this is not very exact + return data.getObservers();//TODO: this is incorrect public function getDependencies() return [(cast data:Observable)].iterator(); @@ -89,6 +89,9 @@ private class CompoundState implements StateObject { public function getComparator() return this.comparator; + + function retain() {} + function release() {} } private class GuardedState extends SimpleState { diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 209c560..52108f9 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -269,6 +269,7 @@ class AutoObservable extends Invalidator if (!s.used) { if (hot) s.disconnect(); dependencies.remove(s.source); + s.source.release(); #if tink_state.debug logger.unsubscribed(s.source, this); #end @@ -295,6 +296,7 @@ class AutoObservable extends Invalidator logger.subscribed(source, this); #end var sub:Subscription = cast new SubscriptionTo(source, cur, this); + source.retain(); if (hot) sub.connect(); dependencies.set(source, sub); subscriptions.push(sub); diff --git a/src/tink/state/internal/Invalidatable.hx b/src/tink/state/internal/Invalidatable.hx index f298c5c..8f333e9 100644 --- a/src/tink/state/internal/Invalidatable.hx +++ b/src/tink/state/internal/Invalidatable.hx @@ -37,6 +37,8 @@ class Invalidator implements OwnedDisposable { public function ondispose(d:()->Void) list.ondispose(d); + function retain() {} + function release() {} public inline function dispose() { list.dispose(); diff --git a/src/tink/state/internal/ObservableObject.hx b/src/tink/state/internal/ObservableObject.hx index c388ecd..cabca85 100644 --- a/src/tink/state/internal/ObservableObject.hx +++ b/src/tink/state/internal/ObservableObject.hx @@ -1,6 +1,9 @@ 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; diff --git a/src/tink/state/internal/SignalObservable.hx b/src/tink/state/internal/SignalObservable.hx index fc8a5bd..fcdba42 100644 --- a/src/tink/state/internal/SignalObservable.hx +++ b/src/tink/state/internal/SignalObservable.hx @@ -53,6 +53,9 @@ class SignalObservable implements ObservableObject { public function getComparator():Comparator return null; + function retain() {} + function release() {} + public function onInvalidate(i:Invalidatable):CallbackLink // TODO: this largely duplicates Invalidatable.onInvalidate return diff --git a/src/tink/state/internal/TransformObservable.hx b/src/tink/state/internal/TransformObservable.hx index e38686a..ddefa70 100644 --- a/src/tink/state/internal/TransformObservable.hx +++ b/src/tink/state/internal/TransformObservable.hx @@ -31,7 +31,7 @@ class TransformObservable implements ObservableObject { #if tink_state.debug public function getObservers() - return source.getObservers(); + return source.getObservers();// TODO: this is incorrect public function getDependencies() return [cast source].iterator(); @@ -54,4 +54,7 @@ class TransformObservable implements ObservableObject { public function canFire():Bool return source.canFire(); + + function retain() {} + function release() {} } \ No newline at end of file From aa8dd6689030c24cc89d53ae7dc0172a936569a0 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 11:30:34 +0200 Subject: [PATCH 05/45] Improve dependency graph. Add dispose callback for TransformObservable. --- src/tink/state/State.hx | 33 ++++++++------ src/tink/state/internal/Invalidatable.hx | 8 ++-- src/tink/state/internal/ObjectMap.hx | 2 +- src/tink/state/internal/ObjectMap.js.hx | 15 +++++-- .../state/internal/TransformObservable.hx | 43 +++++++++++++------ 5 files changed, 69 insertions(+), 32 deletions(-) diff --git a/src/tink/state/State.hx b/src/tink/state/State.hx index ea2cb48..b5a1527 100644 --- a/src/tink/state/State.hx +++ b/src/tink/state/State.hx @@ -67,19 +67,28 @@ 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 incorrect - - public function getDependencies() - return [(cast data:Observable)].iterator(); - - @:keep public function toString() - return 'CompoundState[${data.toString()}]';//TODO: perhaps this should be providable from outside - + final observers = new ObjectMap(); + + public function onInvalidate(i) + return switch observers[i] { + case null: + observers[i] = i; + data.onInvalidate(i) & () -> observers.remove(i); + default: null; + } + + 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 onInvalidate(i) + return data.onInvalidate(i); #end public function set(value) { diff --git a/src/tink/state/internal/Invalidatable.hx b/src/tink/state/internal/Invalidatable.hx index 8f333e9..b1f8c24 100644 --- a/src/tink/state/internal/Invalidatable.hx +++ b/src/tink/state/internal/Invalidatable.hx @@ -11,7 +11,7 @@ interface Invalidatable { class Invalidator implements OwnedDisposable { var revision = new Revision(); - final observers = new ObjectMap(); + 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; @@ -53,9 +53,9 @@ class Invalidator implements OwnedDisposable { public function onInvalidate(i:Invalidatable):CallbackLink return - if (observers.get(i) || list.disposed) null; + if (observers.exists(i) || list.disposed) null; else { - observers.set(i, true); + observers[i] = i; list.add( #if tink_state.debug _ -> { @@ -71,7 +71,7 @@ class Invalidator implements OwnedDisposable { #if tink_state.debug public function getObservers() - return observers.keys(); + return observers.iterator(); #end function fire() { 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..b9891f4 100644 --- a/src/tink/state/internal/ObjectMap.js.hx +++ b/src/tink/state/internal/ObjectMap.js.hx @@ -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/TransformObservable.hx b/src/tink/state/internal/TransformObservable.hx index ddefa70..5d7e74a 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,28 @@ 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();// TODO: this is incorrect + final observers = new ObjectMap(); + + public function onInvalidate(i) + return switch observers[i] { + case null: + observers[i] = i; + source.onInvalidate(i) & () -> observers.remove(i); + default: null; + } - public function getDependencies() - return [cast source].iterator(); + public function getObservers() + return observers.iterator(); - public function toString():String - return _toString(); + public function getDependencies() + return [cast source].iterator(); + + public function toString():String + return _toString(); + #else + public function onInvalidate(i) + return source.onInvalidate(i); #end public function getValue() { @@ -55,6 +70,10 @@ class TransformObservable implements ObservableObject { public function canFire():Bool return source.canFire(); - function retain() {} - function release() {} + var retainCount = 0; + function retain() retainCount++; + function release() + if (--retainCount == 0) dispose(); + + static function noop() {} } \ No newline at end of file From 311009f730438eae2988764d6e4c9e78712d7e4a Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 11:38:04 +0200 Subject: [PATCH 06/45] Fix formatting in vscode. --- tests/TestArrays.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestArrays.hx b/tests/TestArrays.hx index 533b7cb..668ec3e 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); From 318c2e43883f4c36d0727e8f21638b536a216bbe Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 12:01:44 +0200 Subject: [PATCH 07/45] Begin working on granular observability. --- src/tink/state/ObservableArray.hx | 22 +++++++++++++++--- tests/TestArrays.hx | 37 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index 335ed98..fd5aa6d 100644 --- a/src/tink/state/ObservableArray.hx +++ b/src/tink/state/ObservableArray.hx @@ -135,16 +135,17 @@ private class ArrayImpl extends Invalidator implements ArrayView { var valid = false; var entries:Array; + final observableEntries = new Map>(); final observableLength:Observable; 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); 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, _ -> this.entries.length, null, null #if tink_state.debug , () -> 'length of ${toString()}' #end); } public function replace(values:Array) @@ -193,7 +194,22 @@ private class ArrayImpl extends Invalidator implements ArrayView { return update(() -> entries.shift()); public function get(index:Int) - return calc(() -> entries[index]); + return + if (AutoObservable.needsTracking(this)) { + var wrapper = switch observableEntries[index] { + case null: + observableEntries[index] = new TransformObservable( + this, + _ -> entries[index], + null, + () -> observableEntries.remove(index) + #if tink_state.debug , () -> 'Entry $index of ${this.toString()}' #end + ); + case v: v; + } + wrapper.value; + } + else entries[index]; public function set(index:Int, value:T) return update(() -> entries[index] = value); diff --git a/tests/TestArrays.hx b/tests/TestArrays.hx index 668ec3e..af7c352 100644 --- a/tests/TestArrays.hx +++ b/tests/TestArrays.hx @@ -64,6 +64,43 @@ class TestArrays { return asserts.done(); } + @:include public function issue49() { + tink.state.debug.Logger.printTo(Sys.println); + final arr = new ObservableArray([for (i in 0...10) i]); + + var o = Observable.auto(() -> arr[2]); + asserts.assert(2 == o.value); + arr[2] = 5; + asserts.assert(5 == o.value); + // 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(); From d749da2995b8da03496881015a1aed0aa4a2810d Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 12:20:58 +0200 Subject: [PATCH 08/45] Make #49 work for arrays. --- src/tink/state/ObservableArray.hx | 14 +++++----- tests/TestArrays.hx | 43 ++++++++++++++----------------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index fd5aa6d..fbbbc2d 100644 --- a/src/tink/state/ObservableArray.hx +++ b/src/tink/state/ObservableArray.hx @@ -145,7 +145,11 @@ private class ArrayImpl extends Invalidator implements ArrayView { public function new(entries) { super(#if tink_state.debug id -> 'ObservableArray#$id${this.entries.toString()}' #end); this.entries = entries; - this.observableLength = new TransformObservable(this, _ -> this.entries.length, null, null #if tink_state.debug , () -> 'length of ${toString()}' #end); + this.observableLength = transform(() -> this.entries.length #if tink_state.debug , 'length' #end); + } + + function transform(f:()->X, ?dispose #if tink_state.debug , name:String #end) { + return new TransformObservable(this, _ -> { this.valid = true; f(); }, null, dispose #if tink_state.debug , () -> '$name of ${toString()}' #end); } public function replace(values:Array) @@ -198,12 +202,10 @@ private class ArrayImpl extends Invalidator implements ArrayView { if (AutoObservable.needsTracking(this)) { var wrapper = switch observableEntries[index] { case null: - observableEntries[index] = new TransformObservable( - this, - _ -> entries[index], - null, + observableEntries[index] = transform( + () -> entries[index], () -> observableEntries.remove(index) - #if tink_state.debug , () -> 'Entry $index of ${this.toString()}' #end + #if tink_state.debug , 'Entry $index of ${this.toString()}' #end ); case v: v; } diff --git a/tests/TestArrays.hx b/tests/TestArrays.hx index af7c352..b69a3ce 100644 --- a/tests/TestArrays.hx +++ b/tests/TestArrays.hx @@ -64,38 +64,33 @@ class TestArrays { return asserts.done(); } - @:include public function issue49() { - tink.state.debug.Logger.printTo(Sys.println); + public function issue49() { final arr = new ObservableArray([for (i in 0...10) i]); - var o = Observable.auto(() -> arr[2]); - asserts.assert(2 == o.value); - arr[2] = 5; - asserts.assert(5 == o.value); - // var computations = 0; + var computations = 0; - // final sum = Observable.auto(() -> { - // computations++; - // arr[2] + arr[5]; - // }); + final sum = Observable.auto(() -> { + computations++; + arr[2] + arr[5]; + }); - // function checkSum(?pos:haxe.PosInfos) - // asserts.assert(sum.value == arr[2] + arr[5], null, pos); + 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); - // checkSum(); - // asserts.assert(computations == 1); + checkSum(); + asserts.assert(computations == 1); - // arr[1] = 0; - // checkSum(); - // asserts.assert(computations == 1); - // asserts.assert(arr[1] == 0); + arr[1] = 0; + checkSum(); + asserts.assert(computations == 1); + asserts.assert(arr[1] == 0); - // arr[2] = 123; - // checkSum(); - // asserts.assert(computations == 2); + arr[2] = 123; + checkSum(); + asserts.assert(computations == 2); return asserts.done(); From d947b53b36e10b05f1554e8b62c8658cb80ddf77 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 12:23:55 +0200 Subject: [PATCH 09/45] Minor. --- src/tink/state/ObservableArray.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index fbbbc2d..55b12f2 100644 --- a/src/tink/state/ObservableArray.hx +++ b/src/tink/state/ObservableArray.hx @@ -143,7 +143,7 @@ private class ArrayImpl extends Invalidator implements ArrayView { 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 = transform(() -> this.entries.length #if tink_state.debug , 'length' #end); } From a23a3e50c8c351f4e5f3663604fe3f87bd428d18 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 12:34:48 +0200 Subject: [PATCH 10/45] Delete unused stuff. --- src/tink/state/ObservableMap.hx | 62 --------------------------------- 1 file changed, 62 deletions(-) diff --git a/src/tink/state/ObservableMap.hx b/src/tink/state/ObservableMap.hx index a52f175..2a027bb 100644 --- a/src/tink/state/ObservableMap.hx +++ b/src/tink/state/ObservableMap.hx @@ -54,68 +54,6 @@ 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; - - function retain() {} - function release() {} - - #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 { var valid = false; From 1350c45e43167fbc403e0ede22988bbb8c22730b Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 14:49:18 +0200 Subject: [PATCH 11/45] Make maps multitype and granularly observable. --- src/tink/state/ObservableMap.hx | 60 +++++++++++++++++++++++++++------ tests/TestMaps.hx | 4 +-- tests/issues/Issue51.hx | 5 ++- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/tink/state/ObservableMap.hx b/src/tink/state/ObservableMap.hx index 2a027bb..6d80b02 100644 --- a/src/tink/state/ObservableMap.hx +++ b/src/tink/state/ObservableMap.hx @@ -4,13 +4,13 @@ import haxe.Constraints.IMap; import haxe.iterators.*; @:forward -abstract ObservableMap(MapImpl) from MapImpl to IMap { +@:multiType(@:followWithAbstracts K) +abstract ObservableMap(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); @@ -23,11 +23,24 @@ abstract ObservableMap(MapImpl) from MapImpl to IMap { public function toMap():Map return view.toMap(); - public function copy():ObservableMap - return view.copy(); + // public function copy():ObservableMap + // return view.copy(); public function entry(key:K) return Observable.auto(this.get.bind(key)); + + @:to static function toIntMap(dict:MapImpl):MapImpl + return new MapImpl(new Map(), new Map(), new Map()); + + @:to static function toStringMap(dict:MapImpl):MapImpl + return new MapImpl(new Map(), new Map(), new Map()); + + @:to static function toObjectMap(dict:MapImpl):MapImpl<{}, V> + return new MapImpl<{}, V>(new Map(), new Map(), new Map()); + + extern static public inline function of(m:Map):ObservableMap + return cast new MapImpl(m.copy(), new Map(), new Map()); + } @:forward @@ -38,8 +51,8 @@ abstract ObservableMapView(MapView) from MapView { public function toMap():Map return cast this.copy(); - public function copy():ObservableMap - return new MapImpl(cast this.copy()); + // public function copy():ObservableMap + // return new MapImpl(cast this.copy(), null); public function entry(key:K) return Observable.auto(this.get.bind(key)); @@ -57,11 +70,15 @@ private interface MapView extends ObservableObject> { private class MapImpl extends Invalidator implements MapView implements IMap { var valid = false; - var entries:Map; + final entries:Map; + final observableEntries:Map>; + final observableExistences:Map>; - public function new(entries:Map) { + public function new(entries, observableEntries, observableExistences) { super(); this.entries = entries; + this.observableEntries = observableEntries; + this.observableExistences = observableExistences; } public function observe():Observable> @@ -73,14 +90,35 @@ private class MapImpl extends Invalidator implements MapView impleme public function getValue():MapView return this; + function transformed(cache:Map>, key:K, f:()->X #if tink_state.debug , name:String #end) + return + if (AutoObservable.needsTracking(this)) { + var wrapper = switch cache[key] { + case null: + cache[key] = new TransformObservable( + this, + _ -> { + valid = true; + f(); + }, + null, + () -> cache.remove(key) + #if tink_state.debug , () -> '$name ${this.toString()}' #end + ); + case v: v; + } + wrapper.value; + } + else f(); + public function get(k:K):Null - return calc(() -> entries.get(k)); + return transformed(observableEntries, k, () -> entries.get(k) #if tink_state.debug , 'Entry for $k in' #end); 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)); + return transformed(observableExistences, k, () -> entries.exists(k) #if tink_state.debug , 'Existance of $k in' #end); public function remove(k:K):Bool return update(() -> entries.remove(k)); diff --git a/tests/TestMaps.hx b/tests/TestMaps.hx index 7e3e085..71f6365 100644 --- a/tests/TestMaps.hx +++ b/tests/TestMaps.hx @@ -11,7 +11,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 +74,7 @@ class TestMaps { } public function testIterators() { - final map = new ObservableMap(new Map()); + final map = new ObservableMap(); map.set('key', 'value'); var count = 0; 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 From 9fb34f680fec6c4871e973378f3ba447004a1417 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 15:40:12 +0200 Subject: [PATCH 12/45] Implement granular array tracking without leaking. --- src/tink/state/ObservableArray.hx | 51 ++++++++++++++++++----- src/tink/state/internal/AutoObservable.hx | 13 ++++++ 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index 55b12f2..3e42d88 100644 --- a/src/tink/state/ObservableArray.hx +++ b/src/tink/state/ObservableArray.hx @@ -135,7 +135,6 @@ private class ArrayImpl extends Invalidator implements ArrayView { var valid = false; var entries:Array; - final observableEntries = new Map>(); final observableLength:Observable; public var length(get, never):Int; @@ -200,16 +199,13 @@ private class ArrayImpl extends Invalidator implements ArrayView { public function get(index:Int) return if (AutoObservable.needsTracking(this)) { - var wrapper = switch observableEntries[index] { - case null: - observableEntries[index] = transform( - () -> entries[index], - () -> observableEntries.remove(index) - #if tink_state.debug , 'Entry $index of ${this.toString()}' #end - ); - case v: v; - } - wrapper.value; + var wrappers = AutoObservable.currentAnnex().get(Wrappers).forSource(this); + + wrappers.get(index, () -> transform( + () -> entries[index], + () -> wrappers.remove(index) + #if tink_state.debug , 'Entry $index of ${this.toString()}' #end + )).value; } else entries[index]; @@ -256,6 +252,39 @@ private class ArrayImpl extends Invalidator implements ArrayView { } } +private class Wrappers { + final bySource = new Map<{}, SourceWrappers>(); + + public function new(target:{}) {} + + public function forSource(source:ArrayImpl):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 { public var length(get, never):Int; diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 52108f9..03965fd 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -3,6 +3,7 @@ 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) @@ -135,6 +136,7 @@ class AutoObservable extends Invalidator } #end public var hot(default, null) = false; + final annex:Annex<{}>; var status = Dirty; var last:T = null; var subscriptions:Array; @@ -155,6 +157,9 @@ class AutoObservable extends Invalidator return revision; } + public function getAnnex() + return annex; + function subsValid() { if (subscriptions == null) return false; @@ -178,6 +183,7 @@ class AutoObservable extends Invalidator this.comparator = comparator; this.list.onfill = () -> inline heatup(); this.list.ondrain = () -> inline cooldown(); + this.annex = new Annex<{}>(this); } function heatup() { @@ -215,6 +221,12 @@ class AutoObservable extends Invalidator 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 { var ret = o.getValue(); if (cur != null && o.canFire()) @@ -326,6 +338,7 @@ class AutoObservable extends Invalidator } private interface Derived { + function getAnnex():Annex<{}>; function isSubscribedTo(source:ObservableObject):Bool; function subscribeTo(source:ObservableObject, cur:R):Void; } \ No newline at end of file From 8745ca2e42156985bb7f332bcc600baaa0efc3a2 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 16:25:43 +0200 Subject: [PATCH 13/45] Move granularity to ObservableArrayView. --- src/tink/state/ObservableArray.hx | 70 ++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index 3e42d88..f7831ff 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(); @@ -144,11 +157,16 @@ private class ArrayImpl extends Invalidator implements ArrayView { public function new(entries) { super(#if tink_state.debug id -> 'ObservableArray#$id[${this.entries.toString()}]' #end); this.entries = entries; - this.observableLength = transform(() -> this.entries.length #if tink_state.debug , 'length' #end); - } - - function transform(f:()->X, ?dispose #if tink_state.debug , name:String #end) { - return new TransformObservable(this, _ -> { this.valid = true; f(); }, null, dispose #if tink_state.debug , () -> '$name 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) @@ -196,18 +214,10 @@ private class ArrayImpl extends Invalidator implements ArrayView { public function shift() return update(() -> entries.shift()); - public function get(index:Int) - return - if (AutoObservable.needsTracking(this)) { - var wrappers = AutoObservable.currentAnnex().get(Wrappers).forSource(this); - - wrappers.get(index, () -> transform( - () -> entries[index], - () -> wrappers.remove(index) - #if tink_state.debug , 'Entry $index of ${this.toString()}' #end - )).value; - } - else entries[index]; + public function get(index:Int) { + valid = true; + return entries[index]; + } public function set(index:Int, value:T) return update(() -> entries[index] = value); @@ -257,7 +267,7 @@ private class Wrappers { public function new(target:{}) {} - public function forSource(source:ArrayImpl):SourceWrappers + public function forSource(source:ArrayView):SourceWrappers return cast switch bySource[source] { case null: bySource[source] = new SourceWrappers(() -> bySource.remove(source)); case v: v; @@ -287,9 +297,11 @@ private class SourceWrappers { 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>; @@ -299,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>); From f1d7b8be20da947420c160f5a2ecbf0f40eb3c16 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 20:02:46 +0200 Subject: [PATCH 14/45] Non-leaky granular observability for maps. Add tests too. --- .haxerc | 2 +- src/tink/state/ObservableMap.hx | 188 ++++++++++++++++++++++++-------- tests/TestMaps.hx | 56 ++++++++++ 3 files changed, 201 insertions(+), 45 deletions(-) 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/src/tink/state/ObservableMap.hx b/src/tink/state/ObservableMap.hx index 6d80b02..6f0b060 100644 --- a/src/tink/state/ObservableMap.hx +++ b/src/tink/state/ObservableMap.hx @@ -5,42 +5,56 @@ import haxe.iterators.*; @:forward @:multiType(@:followWithAbstracts K) -abstract ObservableMap(MapImpl) { +abstract ObservableMap(MapImpl) from MapImpl { public var view(get, never):ObservableMapView; inline function get_view() return this; 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(); - // public function copy():ObservableMap - // return view.copy(); + public function copy():ObservableMap + 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(), new Map(), new Map()); + 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(), new Map(), new Map()); + return new MapImpl(new Map(), StringMaps.INST); @:to static function toObjectMap(dict:MapImpl):MapImpl<{}, V> - return new MapImpl<{}, V>(new Map(), new Map(), new Map()); - - extern static public inline function of(m:Map):ObservableMap - return cast new MapImpl(m.copy(), new Map(), new Map()); + 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 @@ -51,8 +65,8 @@ abstract ObservableMapView(MapView) from MapView { public function toMap():Map return cast this.copy(); - // public function copy():ObservableMap - // return new MapImpl(cast this.copy(), null); + public function copy():ObservableMap + return new MapImpl(cast this.copy(), this.getFactory()); public function entry(key:K) return Observable.auto(this.get.bind(key)); @@ -60,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; @@ -71,16 +86,17 @@ private class MapImpl extends Invalidator implements MapView impleme var valid = false; final entries:Map; - final observableEntries:Map>; - final observableExistences:Map>; + final factory:MapFactory; - public function new(entries, observableEntries, observableExistences) { + public function new(entries, factory) { super(); this.entries = entries; - this.observableEntries = observableEntries; - this.observableExistences = observableExistences; + this.factory = factory; } + public function getFactory() + return factory; + public function observe():Observable> return this; @@ -90,35 +106,18 @@ private class MapImpl extends Invalidator implements MapView impleme public function getValue():MapView return this; - function transformed(cache:Map>, key:K, f:()->X #if tink_state.debug , name:String #end) - return - if (AutoObservable.needsTracking(this)) { - var wrapper = switch cache[key] { - case null: - cache[key] = new TransformObservable( - this, - _ -> { - valid = true; - f(); - }, - null, - () -> cache.remove(key) - #if tink_state.debug , () -> '$name ${this.toString()}' #end - ); - case v: v; - } - wrapper.value; - } - else f(); - - public function get(k:K):Null - return transformed(observableEntries, k, () -> entries.get(k) #if tink_state.debug , 'Entry for $k in' #end); + 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 transformed(observableExistences, k, () -> entries.exists(k) #if tink_state.debug , 'Existance of $k in' #end); + public function exists(k:K):Bool { + valid = true; + return entries.exists(k); + } public function remove(k:K):Bool return update(() -> entries.remove(k)); @@ -169,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/tests/TestMaps.hx b/tests/TestMaps.hx index 71f6365..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.*; @@ -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 From bf329d94b4e073fd95542c7da4333057f9589ed5 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 18 Jul 2021 20:29:09 +0200 Subject: [PATCH 15/45] Switch to next version of streams/testrunner. Should fix tests. --- haxe_libraries/tink_streams.hxml | 8 +++----- haxe_libraries/tink_testrunner.hxml | 7 +++---- 2 files changed, 6 insertions(+), 9 deletions(-) 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 From d437d37e27636c8a21cc6a77a2be1cd5d7a2ff91 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 20 Jul 2021 10:07:54 +0200 Subject: [PATCH 16/45] Get rid of callback links in invalidation (for #69). --- src/tink/state/Observable.hx | 6 +- src/tink/state/ObservableArray.hx | 7 +- src/tink/state/ObservableDate.hx | 7 +- src/tink/state/State.hx | 31 ++++---- src/tink/state/internal/AutoObservable.hx | 13 ++-- src/tink/state/internal/Binding.hx | 5 +- src/tink/state/internal/Invalidatable.hx | 72 ++++++++--------- src/tink/state/internal/ObjectMap.js.hx | 2 +- src/tink/state/internal/ObservableObject.hx | 3 +- src/tink/state/internal/OrderedObjectMap.hx | 77 +++++++++++++++++++ .../state/internal/OrderedObjectMap.js.hx | 5 ++ src/tink/state/internal/SignalObservable.hx | 31 ++++---- .../state/internal/TransformObservable.hx | 24 +++--- 13 files changed, 186 insertions(+), 97 deletions(-) create mode 100644 src/tink/state/internal/OrderedObjectMap.hx create mode 100644 src/tink/state/internal/OrderedObjectMap.js.hx diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index 00ca905..35070a2 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -300,8 +300,8 @@ private class ConstObservable implements ObservableObject { return EmptyIterator.DEPENDENCIES; #end - public function onInvalidate(i:Invalidatable):CallbackLink - return null; + public function subscribe(i:Invalidatable) {} + public function unsubscribe(i:Invalidatable) {} } private class SimpleObservable extends Invalidator implements ObservableObject { @@ -311,7 +311,7 @@ private class SimpleObservable extends Invalidator implements ObservableObjec 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; } diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index f7831ff..debaed1 100644 --- a/src/tink/state/ObservableArray.hx +++ b/src/tink/state/ObservableArray.hx @@ -346,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(); diff --git a/src/tink/state/ObservableDate.hx b/src/tink/state/ObservableDate.hx index 67efef0..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) { diff --git a/src/tink/state/State.hx b/src/tink/state/State.hx index b5a1527..dafbb35 100644 --- a/src/tink/state/State.hx +++ b/src/tink/state/State.hx @@ -70,13 +70,15 @@ private class CompoundState implements StateObject { #if tink_state.debug final observers = new ObjectMap(); - public function onInvalidate(i) - return switch observers[i] { - case null: - observers[i] = i; - data.onInvalidate(i) & () -> observers.remove(i); - default: null; - } + public function subscribe(i) { + observers[i] = i; + data.subscribe(i); + } + + public function unsubscribe(i) { + if (observers.remove(i)) + data.unsubscribe(i); + } public function getObservers() return observers.iterator(); @@ -87,8 +89,11 @@ private class CompoundState implements StateObject { @:keep public function toString() return 'CompoundState[${data.toString()}]';//TODO: perhaps this should be providable from outside #else - public function onInvalidate(i) - return data.onInvalidate(i); + public function subscribe(i) + data.subscribe(i); + public function unsubscribe(i) + data.unsubscribe(i); + #end public function set(value) { @@ -137,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() diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 03965fd..898479e 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -74,7 +74,6 @@ private class SubscriptionTo { public final source:ObservableObject; var last:T; var lastRev:Revision; - var link:CallbackLink; final owner:Invalidatable; public var used = true; @@ -107,14 +106,14 @@ private class SubscriptionTo { #if tink_state.debug logger.disconnected(source, cast owner); #end - link.cancel(); + source.unsubscribe(owner); } public inline function connect():Void { #if tink_state.debug logger.connected(source, cast owner); #end - this.link = source.onInvalidate(owner); + source.subscribe(owner); } } @@ -178,15 +177,13 @@ class AutoObservable extends Invalidator return comparator; public function new(compute, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end) { - super(#if tink_state.debug toString, pos #end); + super(active -> if (active) wakeup() else sleep() #if tink_state.debug , toString, pos #end); this.compute = compute; this.comparator = comparator; - this.list.onfill = () -> inline heatup(); - this.list.ondrain = () -> inline cooldown(); this.annex = new Annex<{}>(this); } - function heatup() { + function wakeup() { getValue(); getRevision(); if (subscriptions != null) @@ -194,7 +191,7 @@ class AutoObservable extends Invalidator hot = true; } - function cooldown() { + function sleep() { hot = false; if (subscriptions != null) for (s in subscriptions) s.disconnect(); diff --git a/src/tink/state/internal/Binding.hx b/src/tink/state/internal/Binding.hx index b3cd617..78e18a6 100644 --- a/src/tink/state/internal/Binding.hx +++ b/src/tink/state/internal/Binding.hx @@ -7,7 +7,6 @@ class Binding implements Invalidatable implements Scheduler.Schedulable imple 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()); @@ -27,7 +26,7 @@ class Binding implements Invalidatable implements Scheduler.Schedulable imple case v: v; } this.comparator = data.getComparator().or(comparator); - link = data.onInvalidate(this); + data.subscribe(this); cb.invoke(this.last = value); } @@ -39,7 +38,7 @@ class Binding implements Invalidatable implements Scheduler.Schedulable imple #end public function cancel() { - link.cancel(); + data.unsubscribe(this); status = Canceled; } diff --git a/src/tink/state/internal/Invalidatable.hx b/src/tink/state/internal/Invalidatable.hx index b1f8c24..ff92f81 100644 --- a/src/tink/state/internal/Invalidatable.hx +++ b/src/tink/state/internal/Invalidatable.hx @@ -1,6 +1,6 @@ package tink.state.internal; -import tink.core.Disposable.OwnedDisposable; +import tink.core.Disposable; interface Invalidatable { function invalidate():Void; @@ -9,10 +9,11 @@ interface Invalidatable { #end } -class Invalidator implements OwnedDisposable { +class Invalidator extends SimpleDisposable { 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 + final observers = new OrderedObjectMap(); + final onStatusChange:(watched:Bool)->Void; + static function noop(_) {} #if tink_state.debug static var counter = 0; final id = counter++; @@ -21,7 +22,12 @@ class Invalidator implements OwnedDisposable { return Observable.untracked(_toString); #end var used = 0; - function new(#if tink_state.debug ?toString:(id:Int)->String, ?pos:haxe.PosInfos #end) { + 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})'; @@ -30,44 +36,26 @@ class Invalidator implements OwnedDisposable { #end } - public var disposed(get, never):Bool; - inline function get_disposed() - return list.disposed; - - public function ondispose(d:()->Void) - list.ondispose(d); - function retain() {} function release() {} - 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.exists(i) || list.disposed) null; - else { - observers[i] = i; - 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); - } + public function subscribe(i:Invalidatable) { + if (observers.exists(i) || disposed) null; + var wasEmpty = observers.size == 0; + observers[i] = i; + if (wasEmpty) onStatusChange(true); + } + + public function unsubscribe(i:Invalidatable) { + observers.remove(i); + if (observers.size == 0) onStatusChange(false); + } #if tink_state.debug public function getObservers() @@ -75,7 +63,21 @@ class Invalidator implements OwnedDisposable { #end function fire() { + #if tink_state.debug + var report = + if (Std.is(this, ObservableObject)) { + var o = cast this; + v -> tink.state.debug.Logger.inst.triggered(o, v); + } + else _ -> {}; + #end + revision = new Revision(); - list.invoke(Noise); + for (v in observers) { + #if tink_state.debug + report(v); + #end + v.invalidate(); + } } } \ No newline at end of file diff --git a/src/tink/state/internal/ObjectMap.js.hx b/src/tink/state/internal/ObjectMap.js.hx index b9891f4..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(); diff --git a/src/tink/state/internal/ObservableObject.hx b/src/tink/state/internal/ObservableObject.hx index cabca85..55a13b5 100644 --- a/src/tink/state/internal/ObservableObject.hx +++ b/src/tink/state/internal/ObservableObject.hx @@ -8,7 +8,8 @@ interface ObservableObject { function getRevision():Revision; function isValid():Bool; function getComparator():Comparator; - function onInvalidate(i:Invalidatable):CallbackLink; + function subscribe(i:Invalidatable):Void; + function unsubscribe(i:Invalidatable):Void; function canFire():Bool; #if tink_state.debug function getObservers():Iterator; diff --git a/src/tink/state/internal/OrderedObjectMap.hx b/src/tink/state/internal/OrderedObjectMap.hx new file mode 100644 index 0000000..6bf6388 --- /dev/null +++ b/src/tink/state/internal/OrderedObjectMap.hx @@ -0,0 +1,77 @@ +package tink.state.internal; + +@:forward(iterator, exists, clear) +abstract OrderedObjectMap(Impl) { + public var size(get, never):Int; + inline function get_size() + return this.count; + + 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.keyOrder.push(key); + this.set(key, value); + return value; + } + + public inline function remove(key) + return this.remove(key) && this.substrac(key); + + public inline function forEach(f) + for (k in this.compact()) f(get(k), k, (cast this:ObjectMap)); + + public inline function count() + return this.count; + +} + +private class Impl extends haxe.ds.ObjectMap { + public final keyOrder:Array = []; + public var count:Int = 0; + public inline function add(key:K) + count = keyOrder.push(key); + + public function compact() { + if (count > keyOrder.length) { + var pos = 0; + for (k in keyOrder) + if (k != null) + keyOrder[pos++] = k; + keyOrder.resize(count); + } + return keyOrder; + } + + public function subtract(key:K) { + keyOrder[keyOrder.indexOf(key)] = null; + count--; + 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 7c9ae75..56f9509 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; }); @@ -56,24 +60,15 @@ class SignalObservable implements ObservableObject { function retain() {} function release() {} - 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); - } + public function subscribe(i:Invalidatable) + if (!observers.exists(i)) observers[i] = changed.handle(i.invalidate); + + public function unsubscribe(i:Invalidatable) { + 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 5d7e74a..a3af5e8 100644 --- a/src/tink/state/internal/TransformObservable.hx +++ b/src/tink/state/internal/TransformObservable.hx @@ -34,13 +34,16 @@ class TransformObservable implements ObservableObject { #if tink_state.debug final observers = new ObjectMap(); - public function onInvalidate(i) - return switch observers[i] { - case null: - observers[i] = i; - source.onInvalidate(i) & () -> observers.remove(i); - default: null; - } + public function subscribe(i) { + observers[i] = i; + source.subscribe(i); + } + + public function unsubscribe(i) { + if (observers.remove(i)) + source.unsubscribe(i); + } + public function getObservers() return observers.iterator(); @@ -51,8 +54,11 @@ class TransformObservable implements ObservableObject { public function toString():String return _toString(); #else - public function onInvalidate(i) - return source.onInvalidate(i); + public function subscribe(i) + source.subscribe(i); + + public function unsubscribe(i) + source.unsubscribe(i); #end public function getValue() { From 876654ad0fe3a46f7404af5c6d4300a3062f4612 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 20 Jul 2021 10:49:05 +0200 Subject: [PATCH 17/45] Get rid of subscriptions (for #69). --- src/tink/state/internal/AutoObservable.hx | 157 +++++++++------------- 1 file changed, 67 insertions(+), 90 deletions(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 898479e..c90a9f0 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -67,61 +67,12 @@ private abstract Computation((a:AutoObservable,?Noise)->T) { } } -private typedef Subscription = SubscriptionTo; - -private class SubscriptionTo { - - public final source:ObservableObject; - var last:T; - var lastRev:Revision; - final owner:Invalidatable; - - public var used = true; - - public function new(source, cur, owner) { - this.source = source; - this.last = cur; - this.lastRev = source.getRevision(); - this.owner = owner; - } - - 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:T) { - 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); - } -} - private enum abstract AutoObservableStatus(Int) { var Dirty; var Computed; } +private typedef Source = ObservableObject; class AutoObservable extends Invalidator implements Invalidatable implements Derived implements ObservableObject { @@ -138,19 +89,20 @@ class AutoObservable extends Invalidator final annex:Annex<{}>; var status = Dirty; var last:T = null; - var subscriptions:Array; - var dependencies = new ObjectMap, Subscription>(); + var sources:Array; + var lastValues = new ObjectMap(); + var lastRevisions = new ObjectMap(); var comparator:Comparator; override function getRevision() { if (hot) return revision; - if (subscriptions == null) + if (sources == null) getValue(); - for (s in subscriptions) - if (s.source.getRevision() > revision) + for (s in sources) + if (s.getRevision() > lastRevisions[s]) return revision = new Revision(); return revision; @@ -160,11 +112,11 @@ class AutoObservable extends Invalidator return annex; function subsValid() { - if (subscriptions == null) + if (sources == null) return false; - for (s in subscriptions) - if (!s.isValid()) + for (s in sources) + if (s.getRevision() != lastRevisions[s]) return false; return true; @@ -183,18 +135,33 @@ class AutoObservable extends Invalidator this.annex = new Annex<{}>(this); } + inline function connect(s:Source) { + #if tink_state.debug + logger.connected(s, this); + #end + s.subscribe(this); + } + + inline function disconnect(s:Source):Void { + #if tink_state.debug + logger.disconnected(s, this); + #end + s.unsubscribe(this); + } + function wakeup() { getValue(); getRevision(); - if (subscriptions != null) - for (s in subscriptions) s.connect(); + if (sources != null) + for (s in sources) connect(s); hot = true; } + function sleep() { hot = false; - if (subscriptions != null) - for (s in subscriptions) s.disconnect(); + if (sources != null) + for (s in sources) disconnect(s); } static public inline function computeFor(o:Derived, fn:()->T) { @@ -235,19 +202,19 @@ class AutoObservable extends Invalidator function doCompute() { status = Computed; - if (subscriptions != null) - for (s in subscriptions) s.used = false; - subscriptions = []; + if (sources != null) + lastValues.clear();// TODO: this might actually cause some churn ... who knows + sources = []; sync = true; last = computeFor(this, () -> compute(this)); sync = false; #if tink_state.debug logger.revalidated(this, false); #end - if (subscriptions.length == 0) dispose(); + if (sources.length == 0) dispose(); } - var prevSubs = subscriptions, + var prevSources = sources, count = 0; while (!isValid()) { @@ -256,11 +223,11 @@ class AutoObservable extends Invalidator #end if (++count == Observable.MAX_ITERATIONS) throw 'no result after ${Observable.MAX_ITERATIONS} attempts'; - else if (subscriptions != null) { + else if (sources != null) { var valid = true; - for (s in subscriptions) - if (s.hasChanged()) { + for (s in sources) + if (hasChanged(s)) { valid = false; break; } @@ -273,14 +240,14 @@ class AutoObservable extends Invalidator } else { doCompute(); - if (prevSubs != null) { - for (s in prevSubs) - if (!s.used) { - if (hot) s.disconnect(); - dependencies.remove(s.source); - s.source.release(); + if (prevSources != null) { + for (s in prevSources) + if (!isUsed(s)) { + if (hot) disconnect(s); + lastRevisions.remove(s); + s.release(); #if tink_state.debug - logger.unsubscribed(s.source, this); + logger.unsubscribed(s, this); #end } } @@ -291,6 +258,16 @@ class AutoObservable extends Invalidator return last; } + public function hasChanged(s:ObservableObject):Bool { + var nextRev = s.getRevision(); + if (nextRev == lastRevisions[s]) return false; + lastRevisions[s] = nextRev; + var before:R = lastValues[s]; + var last = s.getValue(); + lastValues[s] = last; + return !s.getComparator().eq(last, before); + } + var sync = true; function update(value) if (!sync) { @@ -298,29 +275,29 @@ class AutoObservable extends Invalidator fire(); } + inline function isUsed(s:Source) + return lastValues.exists(s); + public function subscribeTo(source:ObservableObject, cur:R):Void - switch dependencies.get(source) { + switch lastRevisions[source] { case null: #if tink_state.debug logger.subscribed(source, this); #end - var sub:Subscription = cast new SubscriptionTo(source, cur, this); + lastRevisions[source] = source.getRevision(); + lastValues[source] = cur; source.retain(); - if (hot) sub.connect(); - dependencies.set(source, sub); - subscriptions.push(sub); + if (hot) connect(source); + sources.push(source); case v: - if (!v.used) { - v.reuse(cur); - subscriptions.push(v); + if (!isUsed(source)) { + lastValues[source] = cur; + sources.push(source); } } public function isSubscribedTo(source:ObservableObject) - return switch dependencies.get(source) { - case null: false; - case s: s.used; - } + return isUsed(source); public function invalidate() if (status == Computed) { @@ -330,7 +307,7 @@ class AutoObservable extends Invalidator #if tink_state.debug public function getDependencies() - return cast dependencies.keys(); + return sources.iterator(); #end } From 60cd663f12e74563d13ce130329d9e7bc3f9cd80 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 20 Jul 2021 10:52:50 +0200 Subject: [PATCH 18/45] Randomly move things around for a change. --- src/tink/state/Observable.hx | 2 +- src/tink/state/ObservableArray.hx | 2 +- src/tink/state/ObservableMap.hx | 2 +- src/tink/state/State.hx | 2 +- src/tink/state/internal/AutoObservable.hx | 2 +- src/tink/state/internal/Dispatcher.hx | 76 +++++++++++++++++++++++ src/tink/state/internal/Invalidatable.hx | 75 ---------------------- 7 files changed, 81 insertions(+), 80 deletions(-) create mode 100644 src/tink/state/internal/Dispatcher.hx diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index 35070a2..84f7508 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -304,7 +304,7 @@ private class ConstObservable implements ObservableObject { public function unsubscribe(i:Invalidatable) {} } -private class SimpleObservable extends Invalidator implements ObservableObject { +private class SimpleObservable extends Dispatcher implements ObservableObject { var _poll:Void->Measurement; var _cache:Measurement = null; diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index debaed1..8aeba1a 100644 --- a/src/tink/state/ObservableArray.hx +++ b/src/tink/state/ObservableArray.hx @@ -144,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; diff --git a/src/tink/state/ObservableMap.hx b/src/tink/state/ObservableMap.hx index 6f0b060..3cbdd60 100644 --- a/src/tink/state/ObservableMap.hx +++ b/src/tink/state/ObservableMap.hx @@ -82,7 +82,7 @@ private interface MapView extends ObservableObject> { function keyValueIterator():KeyValueIterator; } -private class MapImpl extends Invalidator implements MapView implements IMap { +private class MapImpl extends Dispatcher implements MapView implements IMap { var valid = false; final entries:Map; diff --git a/src/tink/state/State.hx b/src/tink/state/State.hx index dafbb35..1562a4c 100644 --- a/src/tink/state/State.hx +++ b/src/tink/state/State.hx @@ -134,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; diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index c90a9f0..fd601ea 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -73,7 +73,7 @@ private enum abstract AutoObservableStatus(Int) { } private typedef Source = ObservableObject; -class AutoObservable extends Invalidator +class AutoObservable extends Dispatcher implements Invalidatable implements Derived implements ObservableObject { static var cur:Derived; diff --git a/src/tink/state/internal/Dispatcher.hx b/src/tink/state/internal/Dispatcher.hx new file mode 100644 index 0000000..3982aa8 --- /dev/null +++ b/src/tink/state/internal/Dispatcher.hx @@ -0,0 +1,76 @@ +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:Invalidatable) { + if (observers.exists(i) || disposed) null; + var wasEmpty = observers.size == 0; + observers[i] = i; + if (wasEmpty) onStatusChange(true); + } + + public function unsubscribe(i:Invalidatable) { + observers.remove(i); + if (observers.size == 0) onStatusChange(false); + } + + #if tink_state.debug + public function getObservers() + return observers.iterator(); + #end + + function fire() { + #if tink_state.debug + var report = + if (Std.is(this, ObservableObject)) { + var o = cast this; + v -> tink.state.debug.Logger.inst.triggered(o, v); + } + else _ -> {}; + #end + + revision = new Revision(); + for (v in observers) { + #if tink_state.debug + report(v); + #end + v.invalidate(); + } + } +} \ No newline at end of file diff --git a/src/tink/state/internal/Invalidatable.hx b/src/tink/state/internal/Invalidatable.hx index ff92f81..fa1d36d 100644 --- a/src/tink/state/internal/Invalidatable.hx +++ b/src/tink/state/internal/Invalidatable.hx @@ -1,83 +1,8 @@ package tink.state.internal; -import tink.core.Disposable; - interface Invalidatable { function invalidate():Void; #if tink_state.debug @:keep function toString():String; #end -} - -class Invalidator 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:Invalidatable) { - if (observers.exists(i) || disposed) null; - var wasEmpty = observers.size == 0; - observers[i] = i; - if (wasEmpty) onStatusChange(true); - } - - public function unsubscribe(i:Invalidatable) { - observers.remove(i); - if (observers.size == 0) onStatusChange(false); - } - - #if tink_state.debug - public function getObservers() - return observers.iterator(); - #end - - function fire() { - #if tink_state.debug - var report = - if (Std.is(this, ObservableObject)) { - var o = cast this; - v -> tink.state.debug.Logger.inst.triggered(o, v); - } - else _ -> {}; - #end - - revision = new Revision(); - for (v in observers) { - #if tink_state.debug - report(v); - #end - v.invalidate(); - } - } } \ No newline at end of file From 4eb7e44dbba4eee7b03bdb9c1838d202d14b5850 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 20 Jul 2021 11:00:14 +0200 Subject: [PATCH 19/45] And yet more random refactoring. --- src/tink/state/Observable.hx | 6 +++--- src/tink/state/ObservableArray.hx | 2 +- src/tink/state/ObservableMap.hx | 2 +- src/tink/state/State.hx | 4 ++-- src/tink/state/debug/Logger.hx | 16 +++++++------- src/tink/state/import.hx | 2 +- src/tink/state/internal/AutoObservable.hx | 8 +++---- src/tink/state/internal/Binding.hx | 4 ++-- src/tink/state/internal/Dispatcher.hx | 21 ++++++------------- src/tink/state/internal/EmptyIterator.hx | 2 +- src/tink/state/internal/ObservableObject.hx | 6 +++--- .../{Invalidatable.hx => Observer.hx} | 4 ++-- src/tink/state/internal/SignalObservable.hx | 6 +++--- .../state/internal/TransformObservable.hx | 2 +- 14 files changed, 38 insertions(+), 47 deletions(-) rename src/tink/state/internal/{Invalidatable.hx => Observer.hx} (56%) diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index 84f7508..2f90b18 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -300,8 +300,8 @@ private class ConstObservable implements ObservableObject { return EmptyIterator.DEPENDENCIES; #end - public function subscribe(i:Invalidatable) {} - public function unsubscribe(i:Invalidatable) {} + public function subscribe(i:Observer) {} + public function unsubscribe(i:Observer) {} } private class SimpleObservable extends Dispatcher implements ObservableObject { @@ -324,7 +324,7 @@ private class SimpleObservable extends Dispatcher implements ObservableObject function reset(_) { _cache = null; - fire(); + fire(this); } function poll() { diff --git a/src/tink/state/ObservableArray.hx b/src/tink/state/ObservableArray.hx index 8aeba1a..0abeeb8 100644 --- a/src/tink/state/ObservableArray.hx +++ b/src/tink/state/ObservableArray.hx @@ -250,7 +250,7 @@ private class ArrayImpl extends Dispatcher implements ArrayView { var ret = fn(); if (valid) { valid = false; - fire(); + fire(this); } return ret; } diff --git a/src/tink/state/ObservableMap.hx b/src/tink/state/ObservableMap.hx index 3cbdd60..9358c27 100644 --- a/src/tink/state/ObservableMap.hx +++ b/src/tink/state/ObservableMap.hx @@ -153,7 +153,7 @@ private class MapImpl extends Dispatcher implements MapView implemen var ret = fn(); if (valid) { valid = false; - fire(); + fire(this); } return ret; } diff --git a/src/tink/state/State.hx b/src/tink/state/State.hx index 1562a4c..5ce5aea 100644 --- a/src/tink/state/State.hx +++ b/src/tink/state/State.hx @@ -68,7 +68,7 @@ private class CompoundState implements StateObject { return data.getValue(); #if tink_state.debug - final observers = new ObjectMap(); + final observers = new ObjectMap(); public function subscribe(i) { observers[i] = i; @@ -173,7 +173,7 @@ private class SimpleState extends Dispatcher 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 fd601ea..d75bd16 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -74,7 +74,7 @@ private enum abstract AutoObservableStatus(Int) { private typedef Source = ObservableObject; class AutoObservable extends Dispatcher - implements Invalidatable implements Derived implements ObservableObject { + implements Observer implements Derived implements ObservableObject { static var cur:Derived; @@ -272,7 +272,7 @@ class AutoObservable extends Dispatcher function update(value) if (!sync) { last = value; - fire(); + fire(this); } inline function isUsed(s:Source) @@ -299,10 +299,10 @@ class AutoObservable extends Dispatcher public function isSubscribedTo(source:ObservableObject) return isUsed(source); - public function invalidate() + public function notify(from) if (status == Computed) { status = Dirty; - fire(); + fire(this); } #if tink_state.debug diff --git a/src/tink/state/internal/Binding.hx b/src/tink/state/internal/Binding.hx index 78e18a6..b5082fc 100644 --- a/src/tink/state/internal/Binding.hx +++ b/src/tink/state/internal/Binding.hx @@ -1,6 +1,6 @@ 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; @@ -42,7 +42,7 @@ class Binding implements Invalidatable implements Scheduler.Schedulable imple status = Canceled; } - public function invalidate() + public function notify(_) if (status == Valid) { status = Invalid; scheduler.schedule(this); diff --git a/src/tink/state/internal/Dispatcher.hx b/src/tink/state/internal/Dispatcher.hx index 3982aa8..5561ac1 100644 --- a/src/tink/state/internal/Dispatcher.hx +++ b/src/tink/state/internal/Dispatcher.hx @@ -4,7 +4,7 @@ import tink.core.Disposable; class Dispatcher extends SimpleDisposable { var revision = new Revision(); - final observers = new OrderedObjectMap(); + final observers = new OrderedObjectMap(); final onStatusChange:(watched:Bool)->Void; static function noop(_) {} #if tink_state.debug @@ -38,14 +38,14 @@ class Dispatcher extends SimpleDisposable { public function getRevision() return revision; - public function subscribe(i:Invalidatable) { + 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:Invalidatable) { + public function unsubscribe(i:Observer) { observers.remove(i); if (observers.size == 0) onStatusChange(false); } @@ -55,22 +55,13 @@ class Dispatcher extends SimpleDisposable { return observers.iterator(); #end - function fire() { - #if tink_state.debug - var report = - if (Std.is(this, ObservableObject)) { - var o = cast this; - v -> tink.state.debug.Logger.inst.triggered(o, v); - } - else _ -> {}; - #end - + function fire(from:ObservableObject) { revision = new Revision(); for (v in observers) { #if tink_state.debug - report(v); + tink.state.debug.Logger.inst.triggered(from, v); #end - v.invalidate(); + 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/ObservableObject.hx b/src/tink/state/internal/ObservableObject.hx index 55a13b5..0b18311 100644 --- a/src/tink/state/internal/ObservableObject.hx +++ b/src/tink/state/internal/ObservableObject.hx @@ -8,11 +8,11 @@ interface ObservableObject { function getRevision():Revision; function isValid():Bool; function getComparator():Comparator; - function subscribe(i:Invalidatable):Void; - function unsubscribe(i:Invalidatable):Void; + 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/Invalidatable.hx b/src/tink/state/internal/Observer.hx similarity index 56% rename from src/tink/state/internal/Invalidatable.hx rename to src/tink/state/internal/Observer.hx index fa1d36d..3249eb5 100644 --- a/src/tink/state/internal/Invalidatable.hx +++ b/src/tink/state/internal/Observer.hx @@ -1,7 +1,7 @@ package tink.state.internal; -interface Invalidatable { - function invalidate():Void; +interface Observer { + function notify(from:ObservableObject):Void; #if tink_state.debug @:keep function toString():String; #end diff --git a/src/tink/state/internal/SignalObservable.hx b/src/tink/state/internal/SignalObservable.hx index 56f9509..bc07c89 100644 --- a/src/tink/state/internal/SignalObservable.hx +++ b/src/tink/state/internal/SignalObservable.hx @@ -60,10 +60,10 @@ class SignalObservable implements ObservableObject { function retain() {} function release() {} - public function subscribe(i:Invalidatable) - if (!observers.exists(i)) observers[i] = changed.handle(i.invalidate); + public function subscribe(i:Observer) + if (!observers.exists(i)) observers[i] = changed.handle(() -> i.notify(this)); - public function unsubscribe(i:Invalidatable) { + public function unsubscribe(i:Observer) { switch observers[i] { case null: case v: diff --git a/src/tink/state/internal/TransformObservable.hx b/src/tink/state/internal/TransformObservable.hx index a3af5e8..d63365f 100644 --- a/src/tink/state/internal/TransformObservable.hx +++ b/src/tink/state/internal/TransformObservable.hx @@ -32,7 +32,7 @@ class TransformObservable implements ObservableObject { return lastSeenRevision == source.getRevision(); #if tink_state.debug - final observers = new ObjectMap(); + final observers = new ObjectMap(); public function subscribe(i) { observers[i] = i; From 5cd1e6b0d6a29d4aacaefd4c7063f46bc786913b Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 20 Jul 2021 11:23:49 +0200 Subject: [PATCH 20/45] Convolution beats correctness. Always. --- src/tink/state/internal/OrderedObjectMap.hx | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/tink/state/internal/OrderedObjectMap.hx b/src/tink/state/internal/OrderedObjectMap.hx index 6bf6388..ba9801f 100644 --- a/src/tink/state/internal/OrderedObjectMap.hx +++ b/src/tink/state/internal/OrderedObjectMap.hx @@ -4,7 +4,7 @@ package tink.state.internal; abstract OrderedObjectMap(Impl) { public var size(get, never):Int; inline function get_size() - return this.count; + return this.keyCount; public inline function new() this = new Impl(); @@ -20,42 +20,44 @@ abstract OrderedObjectMap(Impl) { @:op([]) public function set(key, value) { if (!this.exists(key)) - this.keyOrder.push(key); + this.add(key); this.set(key, value); return value; } public inline function remove(key) - return this.remove(key) && this.substrac(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.count; + return this.keyCount; } private class Impl extends haxe.ds.ObjectMap { public final keyOrder:Array = []; - public var count:Int = 0; - public inline function add(key:K) - count = keyOrder.push(key); + public var keyCount:Int = 0; + public inline function add(key:K) { + keyOrder.push(key); + keyCount++; + } public function compact() { - if (count > keyOrder.length) { + if (keyCount < keyOrder.length) { var pos = 0; for (k in keyOrder) if (k != null) keyOrder[pos++] = k; - keyOrder.resize(count); + keyOrder.resize(keyCount); } return keyOrder; } public function subtract(key:K) { keyOrder[keyOrder.indexOf(key)] = null; - count--; + keyCount--; return true; } } From 383c32ec8bb366d64bdb92aac41487126d44bebe Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 20 Jul 2021 12:24:48 +0200 Subject: [PATCH 21/45] Avoid NPE. --- src/tink/state/internal/Binding.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tink/state/internal/Binding.hx b/src/tink/state/internal/Binding.hx index b5082fc..b55c53f 100644 --- a/src/tink/state/internal/Binding.hx +++ b/src/tink/state/internal/Binding.hx @@ -37,7 +37,7 @@ class Binding implements Observer implements Scheduler.Schedulable implements return 'Binding#$id[${data.toString()}]';//TODO: position might be helpful too #end - public function cancel() { + public function cancel() if (status != Canceled) { data.unsubscribe(this); status = Canceled; } From 213ab73363c2ba0eae009027a47e77b96ceba8eb Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 20 Jul 2021 13:17:45 +0200 Subject: [PATCH 22/45] Revert "Get rid of subscriptions (for #69)." This reverts commit 876654ad0fe3a46f7404af5c6d4300a3062f4612. --- src/tink/state/internal/AutoObservable.hx | 157 +++++++++++++--------- 1 file changed, 90 insertions(+), 67 deletions(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index d75bd16..a3f9422 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -67,12 +67,61 @@ private abstract Computation((a:AutoObservable,?Noise)->T) { } } +private typedef Subscription = SubscriptionTo; + +private class SubscriptionTo { + + public final source:ObservableObject; + var last:T; + var lastRev:Revision; + final owner:Observer; + + public var used = true; + + public function new(source, cur, owner) { + this.source = source; + this.last = cur; + this.lastRev = source.getRevision(); + this.owner = owner; + } + + 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:T) { + 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); + } +} + private enum abstract AutoObservableStatus(Int) { var Dirty; var Computed; } -private typedef Source = ObservableObject; class AutoObservable extends Dispatcher implements Observer implements Derived implements ObservableObject { @@ -89,20 +138,19 @@ class AutoObservable extends Dispatcher final annex:Annex<{}>; var status = Dirty; var last:T = null; - var sources:Array; - var lastValues = new ObjectMap(); - var lastRevisions = new ObjectMap(); + var subscriptions:Array; + var dependencies = new ObjectMap, Subscription>(); var comparator:Comparator; override function getRevision() { if (hot) return revision; - if (sources == null) + if (subscriptions == null) getValue(); - for (s in sources) - if (s.getRevision() > lastRevisions[s]) + for (s in subscriptions) + if (s.source.getRevision() > revision) return revision = new Revision(); return revision; @@ -112,11 +160,11 @@ class AutoObservable extends Dispatcher return annex; function subsValid() { - if (sources == null) + if (subscriptions == null) return false; - for (s in sources) - if (s.getRevision() != lastRevisions[s]) + for (s in subscriptions) + if (!s.isValid()) return false; return true; @@ -135,33 +183,18 @@ class AutoObservable extends Dispatcher this.annex = new Annex<{}>(this); } - inline function connect(s:Source) { - #if tink_state.debug - logger.connected(s, this); - #end - s.subscribe(this); - } - - inline function disconnect(s:Source):Void { - #if tink_state.debug - logger.disconnected(s, this); - #end - s.unsubscribe(this); - } - function wakeup() { getValue(); getRevision(); - if (sources != null) - for (s in sources) connect(s); + if (subscriptions != null) + for (s in subscriptions) s.connect(); hot = true; } - function sleep() { hot = false; - if (sources != null) - for (s in sources) disconnect(s); + if (subscriptions != null) + for (s in subscriptions) s.disconnect(); } static public inline function computeFor(o:Derived, fn:()->T) { @@ -202,19 +235,19 @@ class AutoObservable extends Dispatcher function doCompute() { status = Computed; - if (sources != null) - lastValues.clear();// TODO: this might actually cause some churn ... who knows - sources = []; + if (subscriptions != null) + for (s in subscriptions) s.used = false; + subscriptions = []; sync = true; last = computeFor(this, () -> compute(this)); sync = false; #if tink_state.debug logger.revalidated(this, false); #end - if (sources.length == 0) dispose(); + if (subscriptions.length == 0) dispose(); } - var prevSources = sources, + var prevSubs = subscriptions, count = 0; while (!isValid()) { @@ -223,11 +256,11 @@ class AutoObservable extends Dispatcher #end if (++count == Observable.MAX_ITERATIONS) throw 'no result after ${Observable.MAX_ITERATIONS} attempts'; - else if (sources != null) { + else if (subscriptions != null) { var valid = true; - for (s in sources) - if (hasChanged(s)) { + for (s in subscriptions) + if (s.hasChanged()) { valid = false; break; } @@ -240,14 +273,14 @@ class AutoObservable extends Dispatcher } else { doCompute(); - if (prevSources != null) { - for (s in prevSources) - if (!isUsed(s)) { - if (hot) disconnect(s); - lastRevisions.remove(s); - s.release(); + if (prevSubs != null) { + for (s in prevSubs) + if (!s.used) { + if (hot) s.disconnect(); + dependencies.remove(s.source); + s.source.release(); #if tink_state.debug - logger.unsubscribed(s, this); + logger.unsubscribed(s.source, this); #end } } @@ -258,16 +291,6 @@ class AutoObservable extends Dispatcher return last; } - public function hasChanged(s:ObservableObject):Bool { - var nextRev = s.getRevision(); - if (nextRev == lastRevisions[s]) return false; - lastRevisions[s] = nextRev; - var before:R = lastValues[s]; - var last = s.getValue(); - lastValues[s] = last; - return !s.getComparator().eq(last, before); - } - var sync = true; function update(value) if (!sync) { @@ -275,29 +298,29 @@ class AutoObservable extends Dispatcher fire(this); } - inline function isUsed(s:Source) - return lastValues.exists(s); - public function subscribeTo(source:ObservableObject, cur:R):Void - switch lastRevisions[source] { + switch dependencies.get(source) { case null: #if tink_state.debug logger.subscribed(source, this); #end - lastRevisions[source] = source.getRevision(); - lastValues[source] = cur; + var sub:Subscription = cast new SubscriptionTo(source, cur, this); source.retain(); - if (hot) connect(source); - sources.push(source); + if (hot) sub.connect(); + dependencies.set(source, sub); + subscriptions.push(sub); case v: - if (!isUsed(source)) { - lastValues[source] = cur; - sources.push(source); + if (!v.used) { + v.reuse(cur); + subscriptions.push(v); } } public function isSubscribedTo(source:ObservableObject) - return isUsed(source); + return switch dependencies.get(source) { + case null: false; + case s: s.used; + } public function notify(from) if (status == Computed) { @@ -307,7 +330,7 @@ class AutoObservable extends Dispatcher #if tink_state.debug public function getDependencies() - return sources.iterator(); + return cast dependencies.keys(); #end } From 07c67f7047ecc351b7dcdabcfd6db787adb3ac32 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 20 Jul 2021 13:22:07 +0200 Subject: [PATCH 23/45] Ditch the subscription type param. --- src/tink/state/internal/AutoObservable.hx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index a3f9422..48b84bc 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -67,12 +67,10 @@ private abstract Computation((a:AutoObservable,?Noise)->T) { } } -private typedef Subscription = SubscriptionTo; +private class Subscription { -private class SubscriptionTo { - - public final source:ObservableObject; - var last:T; + public final source:ObservableObject; + var last:Any; var lastRev:Revision; final owner:Observer; @@ -97,7 +95,7 @@ private class SubscriptionTo { return !source.getComparator().eq(last, before); } - public inline function reuse(value:T) { + public inline function reuse(value:Any) { used = true; last = value; } @@ -115,6 +113,10 @@ private class SubscriptionTo { #end source.subscribe(owner); } + + public inline function release() { + source.release(); + } } private enum abstract AutoObservableStatus(Int) { @@ -276,12 +278,12 @@ class AutoObservable extends Dispatcher if (prevSubs != null) { for (s in prevSubs) if (!s.used) { - if (hot) s.disconnect(); - dependencies.remove(s.source); - s.source.release(); #if tink_state.debug logger.unsubscribed(s.source, this); #end + dependencies.remove(s.source); + if (hot) s.disconnect(); + s.release(); } } } @@ -304,7 +306,7 @@ class AutoObservable extends Dispatcher #if tink_state.debug logger.subscribed(source, this); #end - var sub:Subscription = cast new SubscriptionTo(source, cur, this); + var sub:Subscription = new Subscription(source, cur, this); source.retain(); if (hot) sub.connect(); dependencies.set(source, sub); From 1e5696853320776590aaa881caa164f362329c60 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 20 Jul 2021 13:55:04 +0200 Subject: [PATCH 24/45] Focus benchmark on updates (rather than setup/teardown). --- bench/Bench.hx | 27 +++++++++++++++------------ bench/mobx-bench.js | 27 +++++++++++++++------------ 2 files changed, 30 insertions(+), 24 deletions(-) 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 From db3f026151cbe88f3f148f79da080fcb4ef0c4a3 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 25 Jul 2021 20:58:39 +0200 Subject: [PATCH 25/45] One more level of indirection \o/ --- src/tink/state/Observable.hx | 2 +- src/tink/state/internal/AutoObservable.hx | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index 2f90b18..0466888 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -196,7 +196,7 @@ abstract Observable(ObservableObject) from ObservableObject to Observab 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); + return AutoObservable.create(compute, comparator #if tink_state.debug , toString, pos #end); /** Create a constant Observable object from a value. Const observables are lightweight objects diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 48b84bc..038042c 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -334,6 +334,9 @@ class AutoObservable extends Dispatcher public function getDependencies() return cast dependencies.keys(); #end + + static public inline function create(compute, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end) + return new AutoObservable(compute, comparator #if tink_state.debug , toString, pos #end); } private interface Derived { From cb06a998ca7e68e35275ccf342c89c7ef9d8e8fe Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 25 Jul 2021 22:18:34 +0200 Subject: [PATCH 26/45] Make tests pass again. --- haxe_libraries/tink_core.hxml | 4 +- src/tink/state/Observable.hx | 2 +- src/tink/state/internal/AutoObservable.hx | 300 +++++++++++----------- src/tink/state/internal/Computation.hx | 37 +++ 4 files changed, 197 insertions(+), 146 deletions(-) create mode 100644 src/tink/state/internal/Computation.hx diff --git a/haxe_libraries/tink_core.hxml b/haxe_libraries/tink_core.hxml index 0a415b3..f97bc45 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 +# @install: lix --silent download "gh://github.com/haxetink/tink_core#33a5b72257d421c0b278973d58805c9ecefea259" into tink_core/2.0.2/github/33a5b72257d421c0b278973d58805c9ecefea259 +-cp ${HAXE_LIBCACHE}/tink_core/2.0.2/github/33a5b72257d421c0b278973d58805c9ecefea259/src -D tink_core=2.0.2 \ No newline at end of file diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index 0466888..6e22ae3 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -195,7 +195,7 @@ 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 + @:noUsing static public inline function auto(compute:Computation, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end):Observable return AutoObservable.create(compute, comparator #if tink_state.debug , toString, pos #end); /** diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 038042c..9a438bc 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -5,131 +5,12 @@ 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 class Subscription { - - public final source:ObservableObject; - var last:Any; - var lastRev:Revision; - final owner:Observer; - - public var used = true; - - public function new(source, cur, owner) { - this.source = source; - this.last = cur; - this.lastRev = source.getRevision(); - this.owner = owner; - } - - 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; -} - -class AutoObservable extends Dispatcher - implements Observer implements Derived implements ObservableObject { +class AutoObservable extends Dispatcher + implements Observer implements Derived implements ObservableObject { static var cur:Derived; - var compute:Computation; + final update:AutoObservable->Void; #if hotswap static var rev = new State(0); static function onHotswapLoad() { @@ -139,11 +20,11 @@ class AutoObservable extends Dispatcher public var hot(default, null) = false; final annex:Annex<{}>; var status = Dirty; - var last:T = null; + var last:Result = null; var subscriptions:Array; var dependencies = new ObjectMap, Subscription>(); - var comparator:Comparator; + var comparator:Comparator; override function getRevision() { if (hot) @@ -178,9 +59,9 @@ class AutoObservable extends Dispatcher public function getComparator() return comparator; - public function new(compute, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end) { + public function new(update, ?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.compute = compute; + this.update = update; this.comparator = comparator; this.annex = new Annex<{}>(this); } @@ -199,7 +80,7 @@ class AutoObservable extends Dispatcher 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 @@ -211,8 +92,7 @@ class AutoObservable extends Dispatcher } static public inline function untracked(fn:()->T) - return computeFor(null, fn); - + return inline computeFor(null, fn); static public inline function needsTracking(o:ObservableObject):Bool return switch cur { @@ -233,16 +113,14 @@ class AutoObservable extends Dispatcher return ret; } - public function getValue():T { + public function getValue():Result { function doCompute() { status = Computed; if (subscriptions != null) for (s in subscriptions) s.used = false; subscriptions = []; - sync = true; - last = computeFor(this, () -> compute(this)); - sync = false; + computeFor(this, () -> update(this)); #if tink_state.debug logger.revalidated(this, false); #end @@ -293,13 +171,6 @@ class AutoObservable extends Dispatcher return last; } - var sync = true; - - function update(value) if (!sync) { - last = value; - fire(this); - } - public function subscribeTo(source:ObservableObject, cur:R):Void switch dependencies.get(source) { case null: @@ -324,7 +195,7 @@ class AutoObservable extends Dispatcher case s: s.used; } - public function notify(from) + public function notify(from:ObservableObject) if (status == Computed) { status = Dirty; fire(this); @@ -335,12 +206,155 @@ class AutoObservable extends Dispatcher return cast dependencies.keys(); #end - static public inline function create(compute, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end) - return new AutoObservable(compute, comparator #if tink_state.debug , toString, pos #end); + static public function create(compute:Computation, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end):Observable + return new AutoObservable( + switch compute.kind() { + case Sync(f): + + a -> a.last = cast f(); + + case SyncWithLast(f): + + var last = None; + a -> last = Some(cast a.last = cast f(last)); + + case Async(f): + + var ref = new CallbackLinkRef(); + a -> { + var p = f(); + ref.link = p.handle( + o -> { + a.last = switch o { + case Success(v): Done(v); + case Failure(e): Failed(e); + } + a.notify(a); + } + ); + if (!p.status.match(Ready(_))) + a.last = Loading; + } + + case AsyncWithLast(f): + + var ref = new CallbackLinkRef(), + last = None; + a -> { + var p = f(last); + ref.link = p.handle( + o -> { + a.last = cast switch o { + case Success(v): + last = Some(v); + Done(v); + case Failure(e): + Failed(e); + } + a.notify(a); + } + ); + if (!p.status.match(Ready(_))) + a.last = Loading; + } + + case SafeAsync(f): + + var ref = new CallbackLinkRef(); + a -> { + var p = f(); + ref.link = p.handle( + v -> { + a.last = cast Done(v); + a.notify(a); + } + ); + if (!p.status.match(Ready(_))) + a.last = Loading; + } + + case SafeAsyncWithLast(f): + + var ref = new CallbackLinkRef(), + last = None; + a -> { + var p = f(last); + ref.link = p.handle( + v -> { + last = Some(v); + a.last = cast Done(v); + a.notify(a); + } + ); + if (!p.status.match(Ready(_))) + a.last = Loading; + } + }, + comparator + #if tink_state.debug , toString, pos #end + ); } private interface Derived { function getAnnex():Annex<{}>; function isSubscribedTo(source:ObservableObject):Bool; function subscribeTo(source:ObservableObject, cur:R):Void; +} + + +private class Subscription { + + public final source:ObservableObject; + var last:Any; + var lastRev:Revision; + final owner:Observer; + + public var used = true; + + public function new(source, cur, owner) { + this.source = source; + this.last = cur; + this.lastRev = source.getRevision(); + this.owner = owner; + } + + 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; } \ No newline at end of file diff --git a/src/tink/state/internal/Computation.hx b/src/tink/state/internal/Computation.hx new file mode 100644 index 0000000..f259e2d --- /dev/null +++ b/src/tink/state/internal/Computation.hx @@ -0,0 +1,37 @@ +package tink.state.internal; + +abstract Computation(ComputationKind) { + + inline function new(v) + this = v; + + public inline function kind() + return this; + + @:from static function ofAsyncWithLast(f:(last:Option)->Promise):Computation> + return new Computation(AsyncWithLast(f)); + + @:from static function ofAsync(f:()->Promise):Computation> + return new Computation(Async(f)); + + @:from static function ofSafeAsyncWithLast(f:(last:Option)->Future):Computation> + return new Computation(SafeAsyncWithLast(f)); + + @:from static function ofSafeAsync(f:()->Future):Computation> + return new Computation(SafeAsync(f)); + + @:from static function ofSync(f:()->Data):Computation + return new Computation(Sync(f)); + + @:from static function ofSyncWithLast(f:(last:Option)->Data):Computation + return new Computation(SyncWithLast(f)); +} + +enum ComputationKind { + Sync(f:()->Data):ComputationKind; + SyncWithLast(f:(last:Option)->Data):ComputationKind; + Async(f:()->Promise):ComputationKind>; + AsyncWithLast(f:(last:Option)->Promise):ComputationKind>; + SafeAsync(f:()->Future):ComputationKind>; + SafeAsyncWithLast(f:(last:Option)->Future):ComputationKind>; +} \ No newline at end of file From d641c492a6fe4d06a9e18c1a6cae15082031bb34 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 3 Aug 2021 17:15:45 +0200 Subject: [PATCH 27/45] Tests seem to pass again after implementing #60. --- src/tink/state/Observable.hx | 4 +- src/tink/state/internal/AutoObservable.hx | 121 ++--------- src/tink/state/internal/Computation.hx | 240 ++++++++++++++++++++-- tests/TestAuto.hx | 2 + 4 files changed, 244 insertions(+), 123 deletions(-) diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index 6e22ae3..be8f6a8 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -195,8 +195,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:Computation, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end):Observable - return AutoObservable.create(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 diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 9a438bc..7d3920c 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -5,12 +5,12 @@ import tink.state.debug.Logger.inst as logger; #end import tink.core.Annex; -class AutoObservable extends Dispatcher - implements Observer implements Derived implements ObservableObject { +@:allow(tink.state.internal) +class AutoObservable extends Dispatcher + implements Observer implements Derived implements ObservableObject { static var cur:Derived; - final update:AutoObservable->Void; #if hotswap static var rev = new State(0); static function onHotswapLoad() { @@ -20,11 +20,12 @@ class AutoObservable extends Dispatcher public var hot(default, null) = false; final annex:Annex<{}>; var status = Dirty; - var last:Result = null; + 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) @@ -59,22 +60,24 @@ class AutoObservable extends Dispatcher public function getComparator() return comparator; - public function new(update, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end) { + 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.update = update; + this.computation = computation.init(this); this.comparator = comparator; this.annex = new Annex<{}>(this); } function wakeup() { - getValue(); - getRevision(); + computation.wakeup(); + hot = true; if (subscriptions != null) for (s in subscriptions) s.connect(); - hot = true; + getValue(); + getRevision(); } function sleep() { + computation.sleep(); hot = false; if (subscriptions != null) for (s in subscriptions) s.disconnect(); @@ -113,14 +116,20 @@ class AutoObservable extends Dispatcher return ret; } - public function getValue():Result { + function triggerAsync(v:T) { + last = v; + fire(this); + } + + public function getValue():T { function doCompute() { status = Computed; if (subscriptions != null) for (s in subscriptions) s.used = false; subscriptions = []; - computeFor(this, () -> update(this)); + last = computeFor(this, () -> computation.getNext()); + #if tink_state.debug logger.revalidated(this, false); #end @@ -205,94 +214,6 @@ class AutoObservable extends Dispatcher public function getDependencies() return cast dependencies.keys(); #end - - static public function create(compute:Computation, ?comparator #if tink_state.debug , ?toString, ?pos:haxe.PosInfos #end):Observable - return new AutoObservable( - switch compute.kind() { - case Sync(f): - - a -> a.last = cast f(); - - case SyncWithLast(f): - - var last = None; - a -> last = Some(cast a.last = cast f(last)); - - case Async(f): - - var ref = new CallbackLinkRef(); - a -> { - var p = f(); - ref.link = p.handle( - o -> { - a.last = switch o { - case Success(v): Done(v); - case Failure(e): Failed(e); - } - a.notify(a); - } - ); - if (!p.status.match(Ready(_))) - a.last = Loading; - } - - case AsyncWithLast(f): - - var ref = new CallbackLinkRef(), - last = None; - a -> { - var p = f(last); - ref.link = p.handle( - o -> { - a.last = cast switch o { - case Success(v): - last = Some(v); - Done(v); - case Failure(e): - Failed(e); - } - a.notify(a); - } - ); - if (!p.status.match(Ready(_))) - a.last = Loading; - } - - case SafeAsync(f): - - var ref = new CallbackLinkRef(); - a -> { - var p = f(); - ref.link = p.handle( - v -> { - a.last = cast Done(v); - a.notify(a); - } - ); - if (!p.status.match(Ready(_))) - a.last = Loading; - } - - case SafeAsyncWithLast(f): - - var ref = new CallbackLinkRef(), - last = None; - a -> { - var p = f(last); - ref.link = p.handle( - v -> { - last = Some(v); - a.last = cast Done(v); - a.notify(a); - } - ); - if (!p.status.match(Ready(_))) - a.last = Loading; - } - }, - comparator - #if tink_state.debug , toString, pos #end - ); } private interface Derived { diff --git a/src/tink/state/internal/Computation.hx b/src/tink/state/internal/Computation.hx index f259e2d..dad90c8 100644 --- a/src/tink/state/internal/Computation.hx +++ b/src/tink/state/internal/Computation.hx @@ -1,37 +1,235 @@ package tink.state.internal; -abstract Computation(ComputationKind) { +@:forward +abstract Computation(ComputationObject) from ComputationObject { inline function new(v) this = v; - public inline function kind() + @: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 wakeup():Void; + function sleep():Void; +} + +private abstract 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); + } + + abstract function cloneFor(owner:AutoObservable):ComputationObject; + abstract public function getNext():Result; + + public function wakeup():Void {} + public function sleep():Void {} + +} + +private class Async extends AsyncBase, Promise> { + final get:()->Promise; + + public function new(get, ?owner) { + super(owner); + this.get = get; + } + + function cloneFor(owner:AutoObservable>):ComputationObject> + return new Async(get, owner); + + function pull():Promise + return get(); + + function wrap(raw:Outcome):Promised + return switch raw { + case Success(v): + Done(v); + case Failure(e): + Failed(e); + } +} + +private class AsyncWithLast extends AsyncBase, Promise> { + final get:(o:Option)->Promise; + var last = None; + + public function new(get, ?owner) { + super(owner); + this.get = get; + } + + function cloneFor(owner:AutoObservable>):ComputationObject> + return new AsyncWithLast(get, owner); + + function pull():Promise + return get(last); + + function wrap(raw:Outcome):Promised + return switch raw { + case Success(v): + last = Some(v); + Done(v); + case Failure(e): + Failed(e); + } +} + +abstract private class AsyncBase> extends StatefulBase> { + + var result:Result; + var link:CallbackLink; + var sync = false; + + abstract function pull():Result; + abstract function wrap(raw:Raw):PromisedWith; + + 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:Result) { + sync = true; + link = r.handle(o -> if (!sync) owner.triggerAsync(wrap(o))); + sync = false; + } + + 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; - @:from static function ofAsyncWithLast(f:(last:Option)->Promise):Computation> - return new Computation(AsyncWithLast(f)); + public function getNext():T + return get(); - @:from static function ofAsync(f:()->Promise):Computation> - return new Computation(Async(f)); + public function sleep() {} + public function wakeup() {} +} - @:from static function ofSafeAsyncWithLast(f:(last:Option)->Future):Computation> - return new Computation(SafeAsyncWithLast(f)); +private class SyncWithLast extends StatefulBase { + final get:Option->T; + var last = None; - @:from static function ofSafeAsync(f:()->Future):Computation> - return new Computation(SafeAsync(f)); + public function new(get, ?owner) { + super(owner); + this.get = get; + } - @:from static function ofSync(f:()->Data):Computation - return new Computation(Sync(f)); + function cloneFor(owner:AutoObservable):ComputationObject + return new SyncWithLast(get, owner); - @:from static function ofSyncWithLast(f:(last:Option)->Data):Computation - return new Computation(SyncWithLast(f)); + public function getNext():T { + var ret = get(last); + last = Some(ret); + return ret; + } } -enum ComputationKind { - Sync(f:()->Data):ComputationKind; - SyncWithLast(f:(last:Option)->Data):ComputationKind; - Async(f:()->Promise):ComputationKind>; - AsyncWithLast(f:(last:Option)->Promise):ComputationKind>; - SafeAsync(f:()->Future):ComputationKind>; - SafeAsyncWithLast(f:(last:Option)->Future):ComputationKind>; + +private class SafeAsync extends AsyncBase> { + final get:()->Future; + + public function new(get, ?owner) { + super(owner); + this.get = get; + } + + function cloneFor(owner:AutoObservable>):ComputationObject> + return new SafeAsync(get, owner); + + function pull():Future + return get(); + + 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; + } + + function cloneFor(owner:AutoObservable>):ComputationObject> + return new SafeAsyncWithLast(get, owner); + + function pull():Future + return get(last); + + function wrap(raw:T):Predicted { + last = Some(raw); + return Done(raw); + } } \ No newline at end of file diff --git a/tests/TestAuto.hx b/tests/TestAuto.hx index 6a4209b..a31e25a 100644 --- a/tests/TestAuto.hx +++ b/tests/TestAuto.hx @@ -101,6 +101,8 @@ class TestAuto { ); }); + o.bind(function () {}, Scheduler.direct); + asserts.assert(o.value.match(Loading)); asserts.assert(last.match(None)); yield(12); From 1683e3710e80ddfdd883582ce92dd762587fda1b Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 3 Aug 2021 17:27:31 +0200 Subject: [PATCH 28/45] Haxe 4.1 compat. --- src/tink/state/internal/Computation.hx | 47 ++++++++++++++------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/tink/state/internal/Computation.hx b/src/tink/state/internal/Computation.hx index dad90c8..961e00f 100644 --- a/src/tink/state/internal/Computation.hx +++ b/src/tink/state/internal/Computation.hx @@ -23,7 +23,6 @@ abstract Computation(ComputationObject) from ComputationObject(f:(last:Option)->Data):Computation return new SyncWithLast(f); - } /** @@ -46,7 +45,7 @@ private interface ComputationObject { function sleep():Void; } -private abstract class StatefulBase implements ComputationObject { +private class StatefulBase implements ComputationObject { var owner:AutoObservable = null; function new(?owner) this.owner = owner; @@ -61,8 +60,10 @@ private abstract class StatefulBase implements ComputationObject default: cloneFor(owner); } - abstract function cloneFor(owner:AutoObservable):ComputationObject; - abstract public function getNext():Result; + function cloneFor(owner:AutoObservable):ComputationObject + return throw 'abstract'; + public function getNext():Result + return throw 'abstract'; public function wakeup():Void {} public function sleep():Void {} @@ -77,13 +78,13 @@ private class Async extends AsyncBase, Promise this.get = get; } - function cloneFor(owner:AutoObservable>):ComputationObject> + override function cloneFor(owner:AutoObservable>):ComputationObject> return new Async(get, owner); - function pull():Promise + override function pull():Promise return get(); - function wrap(raw:Outcome):Promised + override function wrap(raw:Outcome):Promised return switch raw { case Success(v): Done(v); @@ -101,13 +102,13 @@ private class AsyncWithLast extends AsyncBase, Pr this.get = get; } - function cloneFor(owner:AutoObservable>):ComputationObject> + override function cloneFor(owner:AutoObservable>):ComputationObject> return new AsyncWithLast(get, owner); - function pull():Promise + override function pull():Promise return get(last); - function wrap(raw:Outcome):Promised + override function wrap(raw:Outcome):Promised return switch raw { case Success(v): last = Some(v); @@ -117,16 +118,18 @@ private class AsyncWithLast extends AsyncBase, Pr } } -abstract private class AsyncBase> extends StatefulBase> { +private class AsyncBase> extends StatefulBase> { var result:Result; var link:CallbackLink; var sync = false; - abstract function pull():Result; - abstract function wrap(raw:Raw):PromisedWith; + function pull():Result + return throw 'abstract'; + function wrap(raw:Raw):PromisedWith + return throw 'abstract'; - public function getNext():PromisedWith { + override public function getNext():PromisedWith { var prev = result; result = pull(); @@ -184,10 +187,10 @@ private class SyncWithLast extends StatefulBase { this.get = get; } - function cloneFor(owner:AutoObservable):ComputationObject + override function cloneFor(owner:AutoObservable):ComputationObject return new SyncWithLast(get, owner); - public function getNext():T { + override public function getNext():T { var ret = get(last); last = Some(ret); return ret; @@ -203,13 +206,13 @@ private class SafeAsync extends AsyncBase> { this.get = get; } - function cloneFor(owner:AutoObservable>):ComputationObject> + override function cloneFor(owner:AutoObservable>):ComputationObject> return new SafeAsync(get, owner); - function pull():Future + override function pull():Future return get(); - function wrap(raw:T):Predicted + override function wrap(raw:T):Predicted return Done(raw); } @@ -222,13 +225,13 @@ private class SafeAsyncWithLast extends AsyncBase> { this.get = get; } - function cloneFor(owner:AutoObservable>):ComputationObject> + override function cloneFor(owner:AutoObservable>):ComputationObject> return new SafeAsyncWithLast(get, owner); - function pull():Future + override function pull():Future return get(last); - function wrap(raw:T):Predicted { + override function wrap(raw:T):Predicted { last = Some(raw); return Done(raw); } From f5e6bf80c04a5e7ba10a6df6c4f7ce1b28b3adcb Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 3 Aug 2021 17:28:04 +0200 Subject: [PATCH 29/45] Use final only where for constant locals. --- tests/TestArrays.hx | 4 ++-- tests/TestAuto.hx | 2 +- tests/TestBasic.hx | 2 +- tests/TestDate.hx | 2 +- tests/TestScheduler.hx | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/TestArrays.hx b/tests/TestArrays.hx index b69a3ce..fca3025 100644 --- a/tests/TestArrays.hx +++ b/tests/TestArrays.hx @@ -129,7 +129,7 @@ class TestArrays { .bind(() -> keysChanges++, direct); Observable.auto(() -> { - final first = 0; + var first = 0; for (v in a) { first += v; break; @@ -163,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 a31e25a..3713535 100644 --- a/tests/TestAuto.hx +++ b/tests/TestAuto.hx @@ -221,7 +221,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/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)'); }); From deec843f4b2a52034e6e9abcd932c9e61dd37259 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 3 Aug 2021 17:32:22 +0200 Subject: [PATCH 30/45] Make tests slightly trickier. --- tests/TestAuto.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/TestAuto.hx b/tests/TestAuto.hx index 3713535..7db57ae 100644 --- a/tests/TestAuto.hx +++ b/tests/TestAuto.hx @@ -101,6 +101,8 @@ class TestAuto { ); }); + final o = Observable.auto(() -> o.value); + o.bind(function () {}, Scheduler.direct); asserts.assert(o.value.match(Loading)); From 810e5afaa3a1facdff223f5369e21bb20f590092 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 3 Aug 2021 17:39:50 +0200 Subject: [PATCH 31/45] Don't use direct scheduler in tests when not necessary. --- tests/TestAuto.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestAuto.hx b/tests/TestAuto.hx index 7db57ae..e6f3a59 100644 --- a/tests/TestAuto.hx +++ b/tests/TestAuto.hx @@ -103,7 +103,7 @@ class TestAuto { final o = Observable.auto(() -> o.value); - o.bind(function () {}, Scheduler.direct); + o.bind(function () {}); asserts.assert(o.value.match(Loading)); asserts.assert(last.match(None)); From 36584275698f3969f581c7a69bd6b5cadae7f5ce Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 3 Aug 2021 17:50:18 +0200 Subject: [PATCH 32/45] Add a rudimentary test for #60. --- tests/RunTests.hx | 1 + tests/issues/Issue60.hx | 81 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 tests/issues/Issue60.hx diff --git a/tests/RunTests.hx b/tests/RunTests.hx index 16d7034..1428785 100644 --- a/tests/RunTests.hx +++ b/tests/RunTests.hx @@ -16,6 +16,7 @@ class RunTests { new TestScheduler(), new TestProgress(), new issues.Issue51(), + new issues.Issue60(), new issues.Issue61(), new issues.Issue63(), ])).handle(Runner.exit); 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 From c733f9858a83fd79822ac358ef025cd421f9778d Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 3 Aug 2021 19:08:35 +0200 Subject: [PATCH 33/45] Move pruning to better place. --- src/tink/state/internal/AutoObservable.hx | 37 ++++++++++++----------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 7d3920c..94406ef 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -125,24 +125,38 @@ class AutoObservable extends Dispatcher function doCompute() { status = Computed; - if (subscriptions != null) - for (s in subscriptions) s.used = false; + var prevSubs = subscriptions; + if (prevSubs != null) + for (s in prevSubs) s.used = false; subscriptions = []; last = computeFor(this, () -> computation.getNext()); #if tink_state.debug logger.revalidated(this, false); #end + + 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) dispose(); } - var prevSubs = subscriptions, - count = 0; + var count = 0; while (!isValid()) { #if tink_state.debug logger.revalidating(this); #end + var prevSubs = subscriptions; + if (++count == Observable.MAX_ITERATIONS) throw 'no result after ${Observable.MAX_ITERATIONS} attempts'; else if (subscriptions != null) { @@ -160,20 +174,7 @@ class AutoObservable extends Dispatcher logger.revalidated(this, true); #end } - else { - doCompute(); - 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(); - } - } - } + else doCompute(); } else doCompute(); } From 7048c7869a0d2d9f2e26e0d186269ebd7006ca77 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 3 Aug 2021 19:35:55 +0200 Subject: [PATCH 34/45] Resolves #73. --- src/tink/state/internal/AutoObservable.hx | 15 +++++--- tests/RunTests.hx | 1 + tests/issues/Issue73.hx | 43 +++++++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 tests/issues/Issue73.hx diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 94406ef..e76927a 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -19,7 +19,7 @@ class AutoObservable extends Dispatcher #end public var hot(default, null) = false; final annex:Annex<{}>; - var status = Dirty; + var status = Fresh; var last:T = null; var subscriptions:Array; var dependencies = new ObjectMap, Subscription>(); @@ -54,8 +54,14 @@ class AutoObservable extends Dispatcher 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; @@ -155,11 +161,9 @@ class AutoObservable extends Dispatcher #if tink_state.debug logger.revalidating(this); #end - var prevSubs = subscriptions; - 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) @@ -279,4 +283,5 @@ private class Subscription { private enum abstract AutoObservableStatus(Int) { var Dirty; var Computed; + var Fresh; } \ No newline at end of file diff --git a/tests/RunTests.hx b/tests/RunTests.hx index 1428785..d5b87ae 100644 --- a/tests/RunTests.hx +++ b/tests/RunTests.hx @@ -19,6 +19,7 @@ class RunTests { 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/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 From 85515e36666e91db1281d574c3c31ecc9ae2e450 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sat, 7 Aug 2021 09:28:16 +0200 Subject: [PATCH 35/45] Add tracking getter to AutoObservable. --- src/tink/state/internal/AutoObservable.hx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index e76927a..a72aa84 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -18,6 +18,10 @@ class AutoObservable extends Dispatcher } #end public var hot(default, null) = false; + public var value(get, never):T; + inline function get_value() + return track(this); + final annex:Annex<{}>; var status = Fresh; var last:T = null; From 15396fec1d56001325b8f68209cf3bedd01e3847 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Fri, 13 Aug 2021 11:47:22 +0200 Subject: [PATCH 36/45] Generate a little less code. --- src/tink/state/internal/AutoObservable.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index a72aa84..334bbc2 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -19,7 +19,7 @@ class AutoObservable extends Dispatcher #end public var hot(default, null) = false; public var value(get, never):T; - inline function get_value() + function get_value() return track(this); final annex:Annex<{}>; From 61dc15be3936b1273e5e727ae6b508c9cd8352e2 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 15 Aug 2021 15:47:17 +0200 Subject: [PATCH 37/45] Subscribe before polling. --- src/tink/state/internal/AutoObservable.hx | 57 ++++++++++++----------- src/tink/state/internal/Binding.hx | 9 ++-- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 334bbc2..22acdff 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -119,12 +119,12 @@ class AutoObservable extends Dispatcher case v: v.getAnnex(); } - 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 track(o:ObservableObject):V + return + if (cur != null && o.canFire()) + cur.subscribeTo(o); + else + o.getValue(); function triggerAsync(v:T) { last = v; @@ -189,23 +189,25 @@ class AutoObservable extends Dispatcher return last; } - 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 = new Subscription(source, cur, this); - source.retain(); - if (hot) sub.connect(); - dependencies.set(source, sub); - subscriptions.push(sub); - case v: - if (!v.used) { - v.reuse(cur); - subscriptions.push(v); - } - } + 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(cur); + subscriptions.push(v); + } + v.last; + } public function isSubscribedTo(source:ObservableObject) return switch dependencies.get(source) { @@ -228,24 +230,25 @@ class AutoObservable extends Dispatcher private interface Derived { function getAnnex():Annex<{}>; function isSubscribedTo(source:ObservableObject):Bool; - function subscribeTo(source:ObservableObject, cur:R):Void; + function subscribeTo(source:ObservableObject):R; } private class Subscription { public final source:ObservableObject; - var last:Any; + public var last(default, null):Any; var lastRev:Revision; final owner:Observer; public var used = true; - public function new(source, cur, owner) { + public function new(source, hot, owner) { this.source = source; - this.last = cur; this.lastRev = source.getRevision(); this.owner = owner; + if (hot) connect(); + this.last = source.getValue(); } public inline function isValid() diff --git a/src/tink/state/internal/Binding.hx b/src/tink/state/internal/Binding.hx index b55c53f..5e8c057 100644 --- a/src/tink/state/internal/Binding.hx +++ b/src/tink/state/internal/Binding.hx @@ -9,16 +9,15 @@ class Binding implements Observer implements Scheduler.Schedulable implements var last:Null = null; 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,7 +26,7 @@ class Binding implements Observer implements Scheduler.Schedulable implements } this.comparator = data.getComparator().or(comparator); data.subscribe(this); - cb.invoke(this.last = value); + cb.invoke(this.last = Observable.untracked(() -> data.getValue())); } #if tink_state.debug From e32baecca77edcc061af890c698b411e8340dd4a Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 15 Aug 2021 17:21:05 +0200 Subject: [PATCH 38/45] Wahoops. --- src/tink/state/internal/AutoObservable.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index 22acdff..a60ee59 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -203,7 +203,7 @@ class AutoObservable extends Dispatcher sub.last; case v: if (!v.used) { - v.reuse(cur); + v.reuse(source.getValue()); subscriptions.push(v); } v.last; From 244fde59346d3338163bc3bd6895bd2e0fb37547 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 15 Aug 2021 21:36:03 +0200 Subject: [PATCH 39/45] Don't trigger bindings while computing. --- src/tink/state/internal/AutoObservable.hx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index a60ee59..f6eb68e 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -134,13 +134,15 @@ class AutoObservable extends Dispatcher public function getValue():T { function doCompute() { - status = Computed; + status = Computing; var prevSubs = subscriptions; if (prevSubs != null) for (s in prevSubs) s.used = false; subscriptions = []; last = computeFor(this, () -> computation.getNext()); + if (status == Computing) + status = Computed; #if tink_state.debug logger.revalidated(this, false); #end @@ -205,8 +207,9 @@ class AutoObservable extends Dispatcher if (!v.used) { v.reuse(source.getValue()); subscriptions.push(v); + v.last; } - v.last; + else source.getValue(); } public function isSubscribedTo(source:ObservableObject) @@ -216,9 +219,13 @@ class AutoObservable extends Dispatcher } public function notify(from:ObservableObject) - if (status == Computed) { - status = Dirty; - fire(this); + switch status { + case Computed: + status = Dirty; + fire(this); + case Computing: + status = Dirty; + default: } #if tink_state.debug @@ -290,5 +297,6 @@ private class Subscription { private enum abstract AutoObservableStatus(Int) { var Dirty; var Computed; + var Computing; var Fresh; } \ No newline at end of file From 9fda3b07ea27e299e7f53720d6e349787477268e Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Thu, 4 Nov 2021 15:19:16 +0100 Subject: [PATCH 40/45] Add test for #76 and make it pass. --- src/tink/state/internal/AutoObservable.hx | 3 ++- src/tink/state/internal/Computation.hx | 26 ++++++++++++++++------- tests/TestAuto.hx | 13 ++++++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/tink/state/internal/AutoObservable.hx b/src/tink/state/internal/AutoObservable.hx index f6eb68e..196d4b4 100644 --- a/src/tink/state/internal/AutoObservable.hx +++ b/src/tink/state/internal/AutoObservable.hx @@ -129,6 +129,7 @@ class AutoObservable extends Dispatcher function triggerAsync(v:T) { last = v; fire(this); + if (subscriptions.length == 0) dispose(); } public function getValue():T { @@ -158,7 +159,7 @@ class AutoObservable extends Dispatcher s.release(); } - if (subscriptions.length == 0) dispose(); + if (subscriptions.length == 0 && !computation.isPending()) dispose(); } var count = 0; diff --git a/src/tink/state/internal/Computation.hx b/src/tink/state/internal/Computation.hx index 961e00f..5493532 100644 --- a/src/tink/state/internal/Computation.hx +++ b/src/tink/state/internal/Computation.hx @@ -41,6 +41,7 @@ abstract Computation(ComputationObject) from ComputationObject { function init(owner:AutoObservable):ComputationObject; function getNext():Result; + function isPending():Bool; function wakeup():Void; function sleep():Void; } @@ -65,12 +66,15 @@ private class StatefulBase implements ComputationObject { 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, Promise> { +private class Async extends AsyncBase> { final get:()->Promise; public function new(get, ?owner) { @@ -93,7 +97,7 @@ private class Async extends AsyncBase, Promise } } -private class AsyncWithLast extends AsyncBase, Promise> { +private class AsyncWithLast extends AsyncBase> { final get:(o:Option)->Promise; var last = None; @@ -118,14 +122,15 @@ private class AsyncWithLast extends AsyncBase, Pr } } -private class AsyncBase> extends StatefulBase> { +private class AsyncBase extends StatefulBase> { - var result:Result; + var result:Future; var link:CallbackLink; var sync = false; - function pull():Result + function pull():Future return throw 'abstract'; + function wrap(raw:Raw):PromisedWith return throw 'abstract'; @@ -148,12 +153,15 @@ private class AsyncBase> extends StatefulBase) { 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: @@ -176,6 +184,8 @@ private class Sync implements ComputationObject { public function sleep() {} public function wakeup() {} + public function isPending():Bool + return false; } private class SyncWithLast extends StatefulBase { @@ -198,7 +208,7 @@ private class SyncWithLast extends StatefulBase { } -private class SafeAsync extends AsyncBase> { +private class SafeAsync extends AsyncBase { final get:()->Future; public function new(get, ?owner) { @@ -216,7 +226,7 @@ private class SafeAsync extends AsyncBase> { return Done(raw); } -private class SafeAsyncWithLast extends AsyncBase> { +private class SafeAsyncWithLast extends AsyncBase { final get:(o:Option)->Future; var last = None; diff --git a/tests/TestAuto.hx b/tests/TestAuto.hx index e6f3a59..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>>(); From 33fd87fb0bdba971c39af460697890fd9067ba75 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sun, 10 Apr 2022 18:17:29 +0200 Subject: [PATCH 41/45] Allow cancellation from within autorun. --- src/tink/state/Observable.hx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index be8f6a8..e240aec 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -172,12 +172,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 From 9acb3a6e2ef0115c94b5834a4180f922a14701b8 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Sat, 14 May 2022 09:37:09 +0200 Subject: [PATCH 42/45] Use new Promise.never() --- src/tink/state/Promised.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From f8f57d786c5960eaac407fe3258e3dccb4f2167e Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 6 Jun 2023 09:37:00 +0200 Subject: [PATCH 43/45] Bump tink_core. --- haxe_libraries/tink_core.hxml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/haxe_libraries/tink_core.hxml b/haxe_libraries/tink_core.hxml index f97bc45..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#33a5b72257d421c0b278973d58805c9ecefea259" into tink_core/2.0.2/github/33a5b72257d421c0b278973d58805c9ecefea259 --cp ${HAXE_LIBCACHE}/tink_core/2.0.2/github/33a5b72257d421c0b278973d58805c9ecefea259/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 From d8a0ba64f320c6a667e354dd79b4fd7ab051540e Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 6 Jun 2023 09:43:29 +0200 Subject: [PATCH 44/45] Const observables can't fire, obviously. --- src/tink/state/Observable.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index e240aec..42a1281 100644 --- a/src/tink/state/Observable.hx +++ b/src/tink/state/Observable.hx @@ -279,7 +279,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; From c693cf38fb4c5e19163ceb3770755fa85e22cec9 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Tue, 6 Jun 2023 09:48:47 +0200 Subject: [PATCH 45/45] Remove some deprecated stuff. --- src/tink/state/Observable.hx | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/src/tink/state/Observable.hx b/src/tink/state/Observable.hx index 42a1281..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;