Skip to content

Commit

Permalink
feat: add side effect mutations to rebuilds (#64)
Browse files Browse the repository at this point in the history
Fixes #61
  • Loading branch information
GregoryConrad authored Jan 4, 2024
1 parent 04dd82f commit e10925f
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 53 deletions.
12 changes: 11 additions & 1 deletion packages/flutter_rearch/lib/src/widgets/consumer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,17 @@ class _WidgetSideEffectApiProxyImpl implements WidgetSideEffectApi {
final _RearchElement manager;

@override
void rebuild() => manager.markNeedsBuild();
void rebuild([
void Function(void Function() cancelRebuild)? sideEffectMutation,
]) {
if (sideEffectMutation != null) {
var isCanceled = false;
sideEffectMutation(() => isCanceled = true);
if (isCanceled) return;
}

manager.markNeedsBuild();
}

@override
BuildContext get context => manager;
Expand Down
38 changes: 21 additions & 17 deletions packages/rearch/lib/rearch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ typedef SideEffectApiCallback = void Function();
@experimental
abstract interface class SideEffectApi {
/// Triggers a rebuild in the supplied capsule.
void rebuild();
///
/// The supplied [sideEffectMutation] will be called with a `void Function()`
/// argument that can be invoked from within the [sideEffectMutation] to
/// cancel the rebuild (say, if the side effect state doesn't need to change).
void rebuild([
void Function(void Function() cancelRebuild)? sideEffectMutation,
]);

/// Registers the given [SideEffectApiCallback]
/// to be called on capsule disposal.
Expand All @@ -100,33 +106,31 @@ class CapsuleContainer implements Disposable {
final _capsules = <_UntypedCapsule, _CapsuleManager>{};

/// Non-null indicates we are currently in a transaction,
/// with the changed nodes in the set.
/// When null, we are not in a transaction,
/// and we can rebuild capsules on the spot normally.
Set<_CapsuleManager>? _managersToRebuildFromTxn;

void _markNeedsBuild(_CapsuleManager manager) {
runTransaction(() => _managersToRebuildFromTxn!.add(manager));
}
/// with the side effect mutations to call in the [List].
/// When null, we are not in a transaction and we must make one for rebuilds.
/// Side effect mutations return their _CapsuleManager when it should
/// be rebuilt, and null when the side effect state wasn't updated.
List<_CapsuleManager? Function()>? _sideEffectMutationsToCallInTxn;

/// Runs the supplied [sideEffectTransaction] that combines all side effect
/// state updates into a single container rebuild.
/// state updates into a single container rebuild sweep.
/// These state updates can originate from the same or different capsules,
/// enabling you to make transactional side effect changes across capsules.
void runTransaction(void Function() sideEffectTransaction) {
// We can have nested transactions, so check whether we are the "root" txn.
// If we are, then we need to handle the actual capsule builds and cleanup.
final isRootTxn = _managersToRebuildFromTxn == null;

if (isRootTxn) {
_managersToRebuildFromTxn = {};
}
final isRootTxn = _sideEffectMutationsToCallInTxn == null;
if (isRootTxn) _sideEffectMutationsToCallInTxn = [];

sideEffectTransaction();

if (isRootTxn) {
DataflowGraphNode.buildNodesAndDependents(_managersToRebuildFromTxn!);
_managersToRebuildFromTxn = null;
final managersToRebuild = _sideEffectMutationsToCallInTxn!
.map((mutation) => mutation())
.whereType<_CapsuleManager>()
.toSet();
DataflowGraphNode.buildNodesAndDependents(managersToRebuild);
_sideEffectMutationsToCallInTxn = null;
}
}

Expand Down
12 changes: 11 additions & 1 deletion packages/rearch/lib/src/impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,17 @@ class _CapsuleManager extends DataflowGraphNode
}

@override
void rebuild() => container._markNeedsBuild(this);
void rebuild([
void Function(void Function() cancelRebuild)? sideEffectMutation,
]) {
container.runTransaction(() {
container._sideEffectMutationsToCallInTxn!.add(() {
var isCanceled = false;
sideEffectMutation?.call(() => isCanceled = true);
return isCanceled ? null : this;
});
});
}

@override
void registerDispose(SideEffectApiCallback callback) =>
Expand Down
71 changes: 37 additions & 34 deletions packages/rearch/lib/src/side_effects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ extension BuiltinSideEffects on SideEffectRegistrar {
SideEffectApi api() => use.register((api) => api);

/// Convenience side effect that gives a copy of [SideEffectApi.rebuild].
void Function() rebuilder() => use.api().rebuild;
void Function([
void Function(void Function() cancelRebuild)? sideEffectMutation,
]) rebuilder() => use.api().rebuild;

/// Convenience side effect that gives a copy of
/// [SideEffectApi.runTransaction].
Expand All @@ -40,9 +42,14 @@ extension BuiltinSideEffects on SideEffectRegistrar {

T getter() => state;
void setter(T newState) {
if (newState == state) return;
state = newState;
api.rebuild();
api.rebuild((cancelRebuild) {
if (newState == state) {
cancelRebuild();
return;
}

state = newState;
});
}

return (getter, setter);
Expand Down Expand Up @@ -231,14 +238,12 @@ extension BuiltinSideEffects on SideEffectRegistrar {
setValue(AsyncLoading(getValue().data));
setSubscription(
stream?.listen(
(data) {
setValue(AsyncData(data));
rebuild();
},
onError: (Object error, StackTrace trace) {
setValue(AsyncError(error, trace, getValue().data));
rebuild();
},
(data) => rebuild(
(_) => setValue(AsyncData(data)),
),
onError: (Object error, StackTrace trace) => rebuild(
(_) => setValue(AsyncError(error, trace, getValue().data)),
),
cancelOnError: false,
),
);
Expand Down Expand Up @@ -304,16 +309,14 @@ extension BuiltinSideEffects on SideEffectRegistrar {
);

final subscription = asStream?.listen(
(data) {
setValue(AsyncData(data));
rebuild();
},
onError: (Object error, StackTrace trace) {
setValue(
(data) => rebuild(
(_) => setValue(AsyncData(data)),
),
onError: (Object error, StackTrace trace) => rebuild(
(_) => setValue(
AsyncError(error, trace, getValue()?.data ?? None<T>()),
);
rebuild();
},
),
),
);

return () => subscription?.cancel();
Expand Down Expand Up @@ -373,25 +376,25 @@ extension BuiltinSideEffects on SideEffectRegistrar {
if (getFutureCancel() == null) {
setAsyncState(AsyncLoading<T>(getAsyncState().data));
final subscription = futureFactory().asStream().listen(
(data) {
setAsyncState(AsyncData(data));
rebuild();
},
onError: (Object error, StackTrace trace) {
setAsyncState(AsyncError(error, trace, getAsyncState().data));
rebuild();
},
);
(data) => rebuild(
(_) => setAsyncState(AsyncData(data)),
),
onError: (Object error, StackTrace trace) => rebuild(
(_) => setAsyncState(
AsyncError(error, trace, getAsyncState().data),
),
),
);
setFutureCancel(subscription.cancel);
}

return getAsyncState();
},
() {
getFutureCancel()?.call();
setFutureCancel(null);

rebuild();
rebuild((_) {
getFutureCancel()?.call();
setFutureCancel(null);
});
},
);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/rearch/test/side_effects_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -293,5 +293,23 @@ void main() {
expect(state3, equals(321));
}
});

test('side effect mutations are batched at end of txn', () {
var builds = 0;
(int Function(), void Function(int)) lazyStateCapsule(CapsuleHandle use) {
builds++;
return use.stateGetterSetter(0);
}

final container = useContainer();
container.runTransaction(() {
container.read(lazyStateCapsule).$2(1);
container.read(lazyStateCapsule).$2(2);
expect(container.read(lazyStateCapsule).$1(), equals(0));
expect(builds, equals(1));
});
expect(container.read(lazyStateCapsule).$1(), equals(2));
expect(builds, equals(2));
});
});
}

0 comments on commit e10925f

Please sign in to comment.