Skip to content

Commit 23db94e

Browse files
feat: add map transaction operations
Co-authored-by: MasterMarcoHD <MasterMarcoHD@users.noreply.github.com>
1 parent ca9d5a7 commit 23db94e

21 files changed

+1917
-53
lines changed

lib/src/transactions/domain/models/list_add_element_tx_operation.dart

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class ListAddElementTxOperation<Element, TResult>
1313
required this.element,
1414
required this.createElement,
1515
required this.apply,
16-
this.shouldRollbackOnError = TxOperation.alwaysRollback,
16+
super.shouldRollbackOnError = TxOperation.alwaysRollback,
1717
}) : super(touchedKeys: const <String>{});
1818

1919
@override
@@ -28,10 +28,6 @@ class ListAddElementTxOperation<Element, TResult>
2828
/// Applies commit result to the optimistic element and returns the confirmed element.
2929
final Element Function(Element optimisticElement, TResult createResult) apply;
3030

31-
/// {@macro shouldRollbackOnError}
32-
final bool Function(Object error, StackTrace? stackTrace)
33-
shouldRollbackOnError;
34-
3531
@override
3632
List<Element>? applyConfirmed(List<Element> confirmed, TResult result) {
3733
final appliedElement = apply(element, result);
@@ -47,6 +43,5 @@ class ListAddElementTxOperation<Element, TResult>
4743
}
4844

4945
@override
50-
bool shouldRollback(Object error, StackTrace? stackTrace) =>
51-
shouldRollbackOnError(error, stackTrace);
46+
String get logTag => 'ListAddElementTxOperation';
5247
}

lib/src/transactions/domain/models/list_remove_element_tx_operation.dart

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class ListRemoveElementTxOperation<Element, TResult>
1515
required this.element,
1616
required this.removeElement,
1717
required this.apply,
18-
this.shouldRollbackOnError = TxOperation.alwaysRollback,
18+
super.shouldRollbackOnError = TxOperation.alwaysRollback,
1919
}) : super(touchedKeys: const <String>{});
2020

2121
@override
@@ -30,10 +30,6 @@ class ListRemoveElementTxOperation<Element, TResult>
3030
/// Applies commit result to the optimistic element and returns the confirmed element.
3131
final Element Function(Element optimisticElement, TResult createResult) apply;
3232

33-
/// {@macro shouldRollbackOnError}
34-
final bool Function(Object error, StackTrace? stackTrace)
35-
shouldRollbackOnError;
36-
3733
@override
3834
List<Element>? applyConfirmed(List<Element> confirmed, TResult result) {
3935
final appliedElement = apply(element, result);
@@ -55,6 +51,5 @@ class ListRemoveElementTxOperation<Element, TResult>
5551
}
5652

5753
@override
58-
bool shouldRollback(Object error, StackTrace? stackTrace) =>
59-
shouldRollbackOnError(error, stackTrace);
54+
String get logTag => 'ListRemoveElementTxOperation';
6055
}

lib/src/transactions/domain/models/list_update_element_tx_operation.dart

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class ListUpdateElementTxOperation<Element, TResult>
1717
required this.apply,
1818
required this.optimisticUpdate,
1919
required this.updateElement,
20-
this.shouldRollbackOnError = TxOperation.alwaysRollback,
20+
super.shouldRollbackOnError = TxOperation.alwaysRollback,
2121
}) : super(touchedKeys: touchedElementKeys);
2222

2323
@override
@@ -36,10 +36,6 @@ class ListUpdateElementTxOperation<Element, TResult>
3636
/// Updates the element on the remote and returns the commit result.
3737
final Future<TResult> Function(Element element) updateElement;
3838

39-
/// {@macro shouldRollbackOnError}
40-
final bool Function(Object error, StackTrace? stackTrace)
41-
shouldRollbackOnError;
42-
4339
/// Applies commit result to the optimistic element and returns the confirmed element.
4440
final Element Function(Element optimisticElement, TResult result) apply;
4541

@@ -73,6 +69,5 @@ class ListUpdateElementTxOperation<Element, TResult>
7369
}
7470

7571
@override
76-
bool shouldRollback(Object error, StackTrace? stackTrace) =>
77-
shouldRollbackOnError(error, stackTrace);
72+
String get logTag => 'ListUpdateElementTxOperation';
7873
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import 'package:grumpy/grumpy.dart';
2+
3+
/// {@template map_change_key_tx_operation}
4+
/// A [TxOperation] for changing the key of an entry in a map.
5+
///
6+
/// To update the value of an entry without changing the key, use [MapUpdateEntryTxOperation] instead.
7+
/// {@endtemplate}
8+
class MapChangeKeyTxOperation<Key, Value, TResult>
9+
extends TxOperation<Map<Key, Value>, TResult> {
10+
/// {@macro map_change_key_tx_operation}
11+
MapChangeKeyTxOperation({
12+
required super.name,
13+
required super.id,
14+
required super.baseVersion,
15+
required this.oldKey,
16+
required this.newKey,
17+
required this.changeKey,
18+
required this.apply,
19+
required this.conflictResolution,
20+
super.shouldRollbackOnError = TxOperation.alwaysRollback,
21+
}) : super(touchedKeys: {oldKey.toString(), newKey.toString()});
22+
23+
/// The conflict resolution strategy to use when the [newKey] already exists in the confirmed state.
24+
final MapChangeKeyConflictResolution conflictResolution;
25+
26+
/// The old key of the entry to be updated.
27+
final Key oldKey;
28+
29+
/// The new key of the entry to be updated.
30+
final Key newKey;
31+
32+
/// Changes the key of the entry on the remote and returns the commit result.
33+
final Future<TResult> Function(Key oldKey, Key newKey, Value value) changeKey;
34+
35+
/// Applies commit result to the optimistic entry and returns the confirmed entry.
36+
final Key Function(
37+
Key oldKey,
38+
Key newKey,
39+
Value value,
40+
TResult confirmedEntry,
41+
)
42+
apply;
43+
44+
@override
45+
Map<Key, Value>? applyConfirmed(Map<Key, Value> confirmed, TResult result) {
46+
if (!confirmed.containsKey(oldKey)) {
47+
// Old key not found, cannot apply key change
48+
return null;
49+
}
50+
51+
if (confirmed.containsKey(newKey)) {
52+
// New key already exists, resolve conflict based on the specified strategy
53+
switch (conflictResolution) {
54+
case MapChangeKeyConflictResolution.rollback:
55+
// Rollback the operation by returning null
56+
return null;
57+
case MapChangeKeyConflictResolution.overwrite:
58+
// Overwrite the existing entry with the new key
59+
break;
60+
}
61+
}
62+
63+
final value = confirmed[oldKey] as Value;
64+
final appliedNewKey = apply(oldKey, newKey, value, result);
65+
final copy = Map<Key, Value>.from(confirmed);
66+
copy.remove(oldKey);
67+
copy[appliedNewKey] = value;
68+
return copy;
69+
}
70+
71+
@override
72+
Future<TResult> commit(Map<Key, Value> current) {
73+
if (!current.containsKey(oldKey)) {
74+
// Old key not found, cannot commit key change
75+
throw StateError('Old key not found: $oldKey');
76+
}
77+
final value = current[oldKey] as Value;
78+
return changeKey(oldKey, newKey, value);
79+
}
80+
81+
@override
82+
Map<Key, Value> optimisticApply(Map<Key, Value> current) {
83+
if (!current.containsKey(oldKey)) {
84+
// Old key not found, cannot apply key change
85+
return current;
86+
}
87+
final value = current[oldKey] as Value;
88+
final copy = Map<Key, Value>.from(current);
89+
copy.remove(oldKey);
90+
copy[newKey] = value;
91+
return copy;
92+
}
93+
94+
@override
95+
String get logTag => 'MapChangeKeyTxOperation';
96+
}
97+
98+
/// Conflict resolution strategies for [MapChangeKeyTxOperation].
99+
enum MapChangeKeyConflictResolution {
100+
/// The operation will be rolled back if the new key already exists in the confirmed state.
101+
rollback,
102+
103+
/// The operation will overwrite the existing entry with the new key if it already exists in the confirmed state.
104+
overwrite,
105+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import 'package:grumpy/grumpy.dart';
2+
3+
/// {@template map_put_entry_tx_operation}
4+
/// A [TxOperation] for putting an entry in a map.
5+
/// {@endtemplate}
6+
class MapPutEntryTxOperation<Key, Value, TResult>
7+
extends TxOperation<Map<Key, Value>, TResult> {
8+
/// {@macro map_put_entry_tx_operation}
9+
MapPutEntryTxOperation({
10+
required super.name,
11+
required super.id,
12+
required super.baseVersion,
13+
required this.key,
14+
required this.optimisticValue,
15+
super.shouldRollbackOnError = TxOperation.alwaysRollback,
16+
required this.putEntry,
17+
required this.apply,
18+
}) : super(touchedKeys: {key.toString()});
19+
20+
/// The key of the entry to be put in the map.
21+
final Key key;
22+
23+
/// Creates the entry on the remote and returns the commit result.
24+
final Future<TResult> Function() putEntry;
25+
26+
/// The optimistic value to be put in the map.
27+
final Value optimisticValue;
28+
29+
/// Applies commit result to the optimistic entry and returns the confirmed map.
30+
final MapEntry<Key, Value> Function(
31+
Key key,
32+
Value optimisticValue,
33+
TResult putResult,
34+
)
35+
apply;
36+
37+
@override
38+
Map<Key, Value>? applyConfirmed(Map<Key, Value> confirmed, TResult result) {
39+
final copy = Map<Key, Value>.from(confirmed);
40+
final appliedEntry = apply(key, optimisticValue, result);
41+
copy[appliedEntry.key] = appliedEntry.value;
42+
return copy;
43+
}
44+
45+
@override
46+
Map<Key, Value> optimisticApply(Map<Key, Value> current) {
47+
final copy = Map<Key, Value>.from(current);
48+
copy[key] = optimisticValue;
49+
return copy;
50+
}
51+
52+
@override
53+
Future<TResult> commit(_) => putEntry();
54+
55+
@override
56+
String get logTag => 'MapPutEntryTxOperation';
57+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import 'package:grumpy/grumpy.dart';
2+
3+
/// {@template map_remove_entry_tx_operation}
4+
/// A [TxOperation] that removes an entry from a [Map].
5+
/// {@endtemplate}
6+
class MapRemoveEntryTxOperation<Key, Value, TResult>
7+
extends TxOperation<Map<Key, Value>, TResult> {
8+
/// {@macro map_remove_entry_tx_operation}
9+
MapRemoveEntryTxOperation({
10+
required super.name,
11+
required super.id,
12+
required super.baseVersion,
13+
required this.key,
14+
required this.removeEntry,
15+
required this.apply,
16+
super.shouldRollbackOnError = TxOperation.alwaysRollback,
17+
}) : super(touchedKeys: {key.toString()});
18+
19+
/// The key of the entry to remove.
20+
final Key key;
21+
22+
/// Removes the entry and returns the commit result.
23+
final Future<TResult> Function() removeEntry;
24+
25+
/// Applies commit result to the optimistic entry and returns the confirmed map.
26+
final MapEntry<Key, Value> Function(Key key, TResult confirmedEntry) apply;
27+
28+
@override
29+
Map<Key, Value>? applyConfirmed(Map<Key, Value> confirmed, TResult result) {
30+
final copy = Map<Key, Value>.from(confirmed);
31+
final optimisticEntry = apply(key, result);
32+
copy.remove(optimisticEntry.key);
33+
return copy;
34+
}
35+
36+
@override
37+
Future<TResult> commit(_) => removeEntry();
38+
39+
@override
40+
Map<Key, Value> optimisticApply(Map<Key, Value> current) {
41+
final copy = Map<Key, Value>.from(current);
42+
copy.removeWhere((k, v) => k == key);
43+
return copy;
44+
}
45+
46+
@override
47+
String get logTag => 'MapRemoveEntryTxOperation';
48+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import 'package:grumpy/grumpy.dart';
2+
3+
/// {@template map_update_entry_tx_operation}
4+
/// A [TxOperation] for updating an entry in a map.
5+
///
6+
/// Note: To change the key of an entry, use [MapChangeKeyTxOperation] instead.
7+
/// {@endtemplate}
8+
class MapUpdateEntryTxOperation<Key, Value, TResult>
9+
extends TxOperation<Map<Key, Value>, TResult> {
10+
/// {@macro map_update_entry_tx_operation}
11+
const MapUpdateEntryTxOperation({
12+
required super.name,
13+
required super.id,
14+
required super.baseVersion,
15+
required this.key,
16+
required this.touchedValueKeys,
17+
required this.apply,
18+
required this.updateEntry,
19+
required this.optimisticUpdate,
20+
}) : super(touchedKeys: const <String>{});
21+
22+
@override
23+
Set<String> get touchedKeys => touchedValueKeys.map((k) => '$key.$k').toSet();
24+
25+
/// The key of the entry to be updated.
26+
final Key key;
27+
28+
/// The keys of the value that are touched by this operation. This is used to determine if this operation conflicts with other operations that touch the same keys.
29+
final Set<String> touchedValueKeys;
30+
31+
/// Applies commit result to the optimistic entry and returns the confirmed map entry.
32+
final MapEntry<Key, Value> Function(
33+
MapEntry<Key, Value> optimisticEntry,
34+
TResult confirmedEntry,
35+
)
36+
apply;
37+
38+
/// Updates the entry on the remote and returns the commit result.
39+
final Future<TResult> Function(MapEntry<Key, Value> entry) updateEntry;
40+
41+
/// Returns the optimistically updated value based on the current entry.
42+
final Value Function(MapEntry<Key, Value> current) optimisticUpdate;
43+
44+
@override
45+
Map<Key, Value>? applyConfirmed(Map<Key, Value> confirmed, TResult result) {
46+
if (!confirmed.containsKey(key)) {
47+
// Key not found, cannot apply update
48+
return null;
49+
}
50+
final optimisticEntry = MapEntry(key, confirmed[key] as Value);
51+
final appliedEntry = apply(optimisticEntry, result);
52+
final copy = Map<Key, Value>.from(confirmed);
53+
copy[appliedEntry.key] = appliedEntry.value;
54+
return copy;
55+
}
56+
57+
@override
58+
Future<TResult> commit(Map<Key, Value> current) {
59+
final currentEntry = MapEntry(key, current[key] as Value);
60+
return updateEntry(currentEntry);
61+
}
62+
63+
@override
64+
Map<Key, Value> optimisticApply(Map<Key, Value> current) {
65+
final currentEntry = MapEntry(key, current[key] as Value);
66+
final optimisticValue = optimisticUpdate(currentEntry);
67+
final copy = Map<Key, Value>.from(current);
68+
copy[key] = optimisticValue;
69+
return copy;
70+
}
71+
72+
@override
73+
String get logTag => 'MapUpdateEntryTxOperation';
74+
}

lib/src/transactions/domain/models/models.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ export 'simple_tx_operation.dart';
77
export 'list_add_element_tx_operation.dart';
88
export 'list_remove_element_tx_operation.dart';
99
export 'list_update_element_tx_operation.dart';
10+
export 'map_put_entry_tx_operation.dart';
11+
export 'map_remove_entry_tx_operation.dart';
12+
export 'map_update_entry_tx_operation.dart';
13+
export 'map_change_key_tx_operation.dart';

0 commit comments

Comments
 (0)