Skip to content

Commit 816a752

Browse files
committed
feat: optimistic update integration
1 parent 92d228e commit 816a752

File tree

2 files changed

+114
-5
lines changed

2 files changed

+114
-5
lines changed

lib/src/logic/optimism.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ class OptimisticAttempt<TState extends EquatableCopyable<TState>, TValue> {
5858
}
5959
}
6060

61+
/// Accepts the optimistic update as committed (locks in the current value).
62+
void accept() {
63+
if (!_started || _finished) return;
64+
if (_isLatest()) {
65+
_finish();
66+
}
67+
}
68+
6169
void _finish() {
6270
_finished = true;
6371
registry.remove(compositeKey);

test/trent_test.dart

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ void main() {
744744
expect((trent.state as A).value, 111); // updated from 110 to 111
745745
await Future.delayed(const Duration(milliseconds: 5));
746746
attempt3.reject();
747-
expect((trent.state as A).value, 11); // was 10, should be 11
747+
expect((trent.state as A).value, 11); // should be 11
748748

749749
// Try two different tags (should not collide)
750750
final a = trent.optimisticUpdate<int>(
@@ -759,13 +759,114 @@ void main() {
759759
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
760760
);
761761
b.execute(2);
762-
expect((trent.state as A).value, 15); // updated from 14 to 15
762+
expect((trent.state as A).value, 14);
763763
await Future.delayed(const Duration(milliseconds: 5));
764764
a.acceptAs(1); // AcceptAs with same value (no change)
765-
expect((trent.state as A).value, 15);
765+
expect((trent.state as A).value, 14);
766766
b.reject();
767-
expect((trent.state as A).value, 13);
767+
expect((trent.state as A).value, 12);
768768
});
769769

770-
// todo later (needed?): all trent optimistic updates should be wrapped in cancelableAsyncOp
770+
test('OptimisticAttempt accept, reject, acceptAs with same and different tags', () async {
771+
final trent = SimpleTrent();
772+
expect((trent.state as A).value, 0);
773+
774+
// Same tag: accept, reject, acceptAs
775+
final att1 = trent.optimisticUpdate<int>(
776+
tag: 'foo',
777+
forward: (state, value) => (state as A).copyWith(value: state.value + value),
778+
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
779+
);
780+
att1.execute(5);
781+
expect((trent.state as A).value, 5);
782+
att1.accept(); // lock in
783+
// New attempt with same tag should NOT revert previous
784+
final att2 = trent.optimisticUpdate<int>(
785+
tag: 'foo',
786+
forward: (state, value) => (state as A).copyWith(value: state.value + value),
787+
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
788+
);
789+
att2.execute(10);
790+
expect((trent.state as A).value, 15); // 5 (locked) + 10
791+
att2.reject();
792+
expect((trent.state as A).value, 5); // back to locked in value
793+
794+
// Now test acceptAs
795+
final att3 = trent.optimisticUpdate<int>(
796+
tag: 'foo',
797+
forward: (state, value) => (state as A).copyWith(value: state.value + value),
798+
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
799+
);
800+
att3.execute(2);
801+
expect((trent.state as A).value, 7);
802+
att3.acceptAs(20); // should revert 2, then apply 20
803+
expect((trent.state as A).value,
804+
25); // 5 (locked) - 2 + 20 = 23, but since att3 was on top of 5, 5+2=7, revert 2 (7-2=5), then +20=25
805+
806+
// Now test with different tags (should stack)
807+
final attA = trent.optimisticUpdate<int>(
808+
tag: 'A',
809+
forward: (state, value) => (state as A).copyWith(value: state.value + value),
810+
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
811+
);
812+
attA.execute(1);
813+
expect((trent.state as A).value, 26);
814+
final attB = trent.optimisticUpdate<int>(
815+
tag: 'B',
816+
forward: (state, value) => (state as A).copyWith(value: state.value + value),
817+
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
818+
);
819+
attB.execute(2);
820+
expect((trent.state as A).value, 28);
821+
attA.reject();
822+
expect((trent.state as A).value, 27);
823+
attB.accept();
824+
// Now new B should not revert previous
825+
final attB2 = trent.optimisticUpdate<int>(
826+
tag: 'B',
827+
forward: (state, value) => (state as A).copyWith(value: state.value + value),
828+
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
829+
);
830+
attB2.execute(3);
831+
expect((trent.state as A).value, 30);
832+
attB2.reject();
833+
expect((trent.state as A).value, 27);
834+
});
835+
836+
test('OptimisticAttempt: multiple pending, flood, and fallback to previous', () async {
837+
final trent = SimpleTrent();
838+
expect((trent.state as A).value, 0);
839+
840+
// Start first optimistic attempt (pending)
841+
final att1 = trent.optimisticUpdate<int>(
842+
tag: 'flood',
843+
forward: (state, value) => (state as A).copyWith(value: state.value + value),
844+
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
845+
);
846+
att1.execute(1);
847+
expect((trent.state as A).value, 1);
848+
849+
// Start a second attempt before the first resolves (flood/collision)
850+
final att2 = trent.optimisticUpdate<int>(
851+
tag: 'flood', // same tag, so it should replace attempt1
852+
forward: (state, value) => (state as A).copyWith(value: state.value + value),
853+
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
854+
);
855+
att2.execute(10);
856+
expect((trent.state as A).value, 10); // corrected from 11 to 10
857+
858+
// Start a third optimistic attempt (floods, replaces att2)
859+
final att3 = trent.optimisticUpdate<int>(
860+
tag: 'flood',
861+
forward: (state, value) => (state as A).copyWith(value: state.value + value),
862+
reverse: (state, value) => (state as A).copyWith(value: state.value - value),
863+
);
864+
att3.execute(100);
865+
expect((trent.state as A).value, 100);
866+
867+
// att3 fails (reject)
868+
att3.reject();
869+
// Should revert att3's effect, fallback to base state (not att2's effect)
870+
expect((trent.state as A).value, 0);
871+
});
771872
}

0 commit comments

Comments
 (0)