Skip to content

Commit 9c57b87

Browse files
committed
release: v0.2.0
1 parent fb011c6 commit 9c57b87

File tree

8 files changed

+211
-7
lines changed

8 files changed

+211
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.2.0
2+
3+
- Adds `Future<AsyncCompleted<T>> cancelableAsyncOp<T>(Future<T> Function() work)` wrapper function to `Trent` class, allowing for the cancellation of optional async operations via `void reset({bool cancelAsyncOps = true})`. This helps prevent state leaking across sessions.
4+
15
## 0.1.1
26

37
- Updates `TrentManager`'s `trents` field to be optional.

example/pubspec.lock

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ packages:
4141
url: "https://pub.dev"
4242
source: hosted
4343
version: "1.19.1"
44+
crypto:
45+
dependency: transitive
46+
description:
47+
name: crypto
48+
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
49+
url: "https://pub.dev"
50+
source: hosted
51+
version: "3.0.6"
4452
cupertino_icons:
4553
dependency: "direct main"
4654
description:
@@ -65,6 +73,14 @@ packages:
6573
url: "https://pub.dev"
6674
source: hosted
6775
version: "1.3.2"
76+
fixnum:
77+
dependency: transitive
78+
description:
79+
name: fixnum
80+
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
81+
url: "https://pub.dev"
82+
source: hosted
83+
version: "1.1.1"
6884
flutter:
6985
dependency: "direct main"
7086
description: flutter
@@ -208,6 +224,14 @@ packages:
208224
url: "https://pub.dev"
209225
source: hosted
210226
version: "1.10.1"
227+
sprintf:
228+
dependency: transitive
229+
description:
230+
name: sprintf
231+
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
232+
url: "https://pub.dev"
233+
source: hosted
234+
version: "7.0.0"
211235
stack_trace:
212236
dependency: transitive
213237
description:
@@ -254,7 +278,7 @@ packages:
254278
path: ".."
255279
relative: true
256280
source: path
257-
version: "0.0.7"
281+
version: "0.1.1"
258282
typed_data:
259283
dependency: transitive
260284
description:
@@ -263,6 +287,14 @@ packages:
263287
url: "https://pub.dev"
264288
source: hosted
265289
version: "1.4.0"
290+
uuid:
291+
dependency: transitive
292+
description:
293+
name: uuid
294+
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
295+
url: "https://pub.dev"
296+
source: hosted
297+
version: "4.5.1"
266298
vector_math:
267299
dependency: transitive
268300
description:

lib/src/logic/accessors.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ final GetIt _serviceLocator = GetIt.instance;
1111
/// we'll store typed providers directly:
1212
final List<SingleChildWidget> _allTrentProviders = [];
1313

14-
ChangeNotifierProvider register<T extends ChangeNotifier>(
15-
T trent,
16-
) {
14+
ChangeNotifierProvider register<T extends ChangeNotifier>(T trent,
15+
{bool debug = false}) {
1716
if (!_serviceLocator.isRegistered<T>()) {
1817
_serviceLocator.registerSingleton<T>(trent);
1918
}
@@ -23,6 +22,13 @@ ChangeNotifierProvider register<T extends ChangeNotifier>(
2322
ChangeNotifierProvider<T>.value(value: trent),
2423
);
2524

25+
// if debug is true, we want to start listening to the trent in my web server?
26+
if (debug) {
27+
trent.addListener(() {
28+
debugPrint('Trent updated: $trent');
29+
});
30+
}
31+
2632
return ChangeNotifierProvider<T>.value(
2733
value: _serviceLocator.get<T>(),
2834
);

lib/src/logic/trent.dart

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import 'package:equatable/equatable.dart';
33
import 'package:flutter/widgets.dart';
44
import 'package:rxdart/rxdart.dart';
55
import 'package:trent/src/logic/mappers.dart';
6+
import 'package:trent/src/types/async_completed.dart';
67
import 'package:trent/src/types/option.dart';
8+
import 'package:async/async.dart';
9+
import 'package:uuid/uuid.dart';
710

811
/// A generic, abstract Trent that manages state transitions.
912
abstract class Trents<Base> extends ChangeNotifier {
@@ -14,6 +17,13 @@ abstract class Trents<Base> extends ChangeNotifier {
1417
_updateLastState(_state);
1518
}
1619

20+
String _sessionToken = const Uuid().v4();
21+
22+
/// Under-the-hood session token getter incase it's needed for reference.
23+
String get sessionToken => _sessionToken;
24+
25+
final List<CancelableOperation<void>> _ops = [];
26+
1727
/// The current state.
1828
Base _state;
1929

@@ -48,12 +58,45 @@ abstract class Trents<Base> extends ChangeNotifier {
4858
/// Reset the Trent to its initial state.
4959
///
5060
/// All last states are cleared.
51-
void reset() {
61+
void reset({bool cancelAsyncOps = true}) {
62+
if (cancelAsyncOps) {
63+
for (var op in _ops) {
64+
op.cancel();
65+
}
66+
_ops.clear();
67+
_sessionToken = const Uuid().v4();
68+
}
5269
clearAllExes();
5370
emit(_initialState);
5471
_updateLastState(_initialState);
5572
}
5673

74+
/// Wrap any Trent function in this method to ensure it can be
75+
/// optionally cancelled if the Trent is reset.
76+
///
77+
/// This ensures no "leakage" of async operations that are not cancelled
78+
/// across Trent resets, and the result is wrapped in an [AsyncCompleted]
79+
/// to safely distinguish between completed and cancelled/stale executions.
80+
Future<AsyncCompleted<T>> cancelableAsyncOp<T>(
81+
Future<T> Function() work) async {
82+
final captured = _sessionToken;
83+
final op = CancelableOperation<T>.fromFuture(work());
84+
_ops.add(op);
85+
86+
try {
87+
final result = await op.valueOrCancellation(null);
88+
final isStale = captured != _sessionToken;
89+
90+
if (result == null || isStale) {
91+
return AsyncCompleted.withCancelled();
92+
} else {
93+
return AsyncCompleted.withCompleted(result);
94+
}
95+
} finally {
96+
_ops.remove(op);
97+
}
98+
}
99+
57100
/// Emit a new state to the stream. This WILL update the UI.
58101
///
59102
/// Does not emit if and only if the new state is the same as the current state (ie: old: A(val: 55), new A(val: 55)).

lib/src/types/async_completed.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
class AsyncCompleted<T> {
2+
final T? value;
3+
final bool wasCancelled;
4+
5+
AsyncCompleted._({this.value, required this.wasCancelled});
6+
7+
static AsyncCompleted<T> withCompleted<T>(T value) =>
8+
AsyncCompleted._(value: value, wasCancelled: false);
9+
10+
static AsyncCompleted<T> withCancelled<T>() =>
11+
AsyncCompleted._(wasCancelled: true);
12+
13+
/// Match on the result.
14+
/// - `onCancelled` is called if the operation was cancelled or the value is null.
15+
/// - `onCompleted` is called with the value if it was completed successfully.
16+
R match<R>(R Function() onCancelled, R Function(T val) onCompleted) {
17+
return wasCancelled || value == null
18+
? onCancelled()
19+
: onCompleted(value as T);
20+
}
21+
22+
/// Optional helpers
23+
bool isNothing() => wasCancelled || value == null;
24+
25+
T unwrap() {
26+
if (isNothing()) {
27+
throw StateError('Attempted to access cancelled or stale value');
28+
}
29+
return value as T;
30+
}
31+
}

lib/trent.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Logic
22
export 'src/logic/trent.dart';
33
export 'src/logic/accessors.dart';
4-
export 'src/logic/accessors.dart';
54

65
// Types
76
export 'src/types/option.dart';

pubspec.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: trent
22
description: A Flutter package for simple, scalable, and reactive state management with built-in dependency injection and efficient stream-based state handling.
3-
version: 0.1.1
3+
version: 0.2.0
44

55
homepage: https://matthewtrent.me
66
repository: https://github.com/mattrltrent/trent
@@ -18,6 +18,8 @@ dependencies:
1818
rxdart: ^0.28.0
1919
provider: ^6.1.2
2020
get_it: ^8.0.3
21+
async: ^2.12.0
22+
uuid: ^4.5.1
2123

2224
dev_dependencies:
2325
flutter_test:

test/trent_test.dart

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,4 +548,91 @@ void main() {
548548

549549
expect(handlerCalled, false); // Handler for B should not have been called
550550
});
551+
552+
test("dashboard", () {
553+
register(SimpleTrent(), debug: true);
554+
final trent = get<SimpleTrent>();
555+
trent.emit(A(10));
556+
expect(trent.state, isA<A>());
557+
});
558+
559+
test('cancelableAsyncOp returns correct AsyncCompleted', () async {
560+
final trent = SimpleTrent();
561+
562+
// Complete normally
563+
final result1 = await trent.cancelableAsyncOp(() async {
564+
await Future.delayed(Duration(milliseconds: 10));
565+
return 'done';
566+
});
567+
568+
expect(result1.isNothing(), false);
569+
expect(result1.unwrap(), 'done');
570+
571+
// Trigger session reset during async op
572+
final result2Future = trent.cancelableAsyncOp(() async {
573+
await Future.delayed(Duration(milliseconds: 50));
574+
return 'should be stale';
575+
});
576+
577+
trent.reset(); // cancels and changes sessionToken
578+
579+
// should have returned and updated state now
580+
await Future.delayed(Duration(milliseconds: 75));
581+
582+
final result2 = await result2Future;
583+
expect(result2.isNothing(), true);
584+
585+
// Match-style handling
586+
final result3 = await trent.cancelableAsyncOp(() async {
587+
await Future.delayed(Duration(milliseconds: 10));
588+
return 123;
589+
});
590+
591+
final matchOutput = result3.match(
592+
() => 'cancelled',
593+
(val) => 'value: $val',
594+
);
595+
expect(matchOutput, 'value: 123');
596+
});
597+
598+
test(
599+
'reset(cancelAsyncOps: true) cancels inflight ops and invalidates session',
600+
() async {
601+
final trent = SimpleTrent();
602+
603+
final future = trent.cancelableAsyncOp(() async {
604+
await Future.delayed(Duration(milliseconds: 50));
605+
return 'should not complete';
606+
});
607+
608+
// Trigger reset before the future completes
609+
await Future.delayed(Duration(milliseconds: 10));
610+
trent.reset(cancelAsyncOps: true);
611+
612+
final result = await future;
613+
expect(result.isNothing(), true); // Result discarded due to session flip
614+
});
615+
616+
test(
617+
'reset(cancelAsyncOps: false) preserves session and allows ops to complete',
618+
() async {
619+
final trent = SimpleTrent();
620+
621+
final capturedToken = trent.sessionToken; // assume you expose this for test
622+
final future = trent.cancelableAsyncOp(() async {
623+
await Future.delayed(Duration(milliseconds: 30));
624+
return 'survives reset';
625+
});
626+
627+
// Reset WITHOUT canceling ops
628+
await Future.delayed(Duration(milliseconds: 10));
629+
trent.reset(cancelAsyncOps: false);
630+
631+
final result = await future;
632+
expect(result.isNothing(), false);
633+
expect(result.unwrap(), 'survives reset');
634+
635+
// Ensure session token didn't change
636+
expect(trent.sessionToken, capturedToken);
637+
});
551638
}

0 commit comments

Comments
 (0)