From d920180845f51d87383b54d3442752c34d529e27 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Thu, 26 Dec 2024 22:45:12 -0700 Subject: [PATCH] refactor: adding rebound callbacks --- .../container/lib/src/container.dart | 68 +++++++++++ .../container/test/rebound_test.dart | 111 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 incubation/container/container/test/rebound_test.dart diff --git a/incubation/container/container/lib/src/container.dart b/incubation/container/container/lib/src/container.dart index 84dcc8a..2409ae2 100644 --- a/incubation/container/container/lib/src/container.dart +++ b/incubation/container/container/lib/src/container.dart @@ -30,6 +30,13 @@ class Container { /// The container's service extenders final Map> _extenders = {}; + /// The container's rebound callbacks + final Map> _reboundCallbacks = + {}; + + /// The container's refreshing instances + final Set _refreshing = {}; + /// The container's contextual bindings final Map> _contextual = {}; @@ -883,6 +890,67 @@ class Container { return instance; } + /// Register a callback to be run when a type is rebound. + /// + /// The callback will be invoked whenever the type's binding is replaced + /// or when refresh() is called on the type. + /// + /// ```dart + /// container.rebinding((logger, container) { + /// print('Logger was rebound'); + /// }); + /// ``` + void rebinding( + void Function(dynamic instance, Container container) callback) { + _reboundCallbacks.putIfAbsent(T, () => []).add(callback); + } + + /// Refresh an instance in the container. + /// + /// This will create a new instance and trigger any rebound callbacks. + /// If the instance is a singleton, it will be replaced in the container. + /// + /// ```dart + /// container.refresh(); + /// ``` + T refresh() { + if (_refreshing.contains(T)) { + throw CircularDependencyException( + 'Circular dependency detected while refreshing $T'); + } + + _refreshing.add(T); + try { + // Create new instance + var instance = make(); + + // If it's a singleton, replace it + if (_singletons.containsKey(T)) { + _singletons[T] = instance; + } + + // Fire rebound callbacks + _fireReboundCallbacks(T, instance); + + return instance; + } finally { + _refreshing.remove(T); + } + } + + /// Fire the rebound callbacks for a type. + void _fireReboundCallbacks(Type type, dynamic instance) { + Container? search = this; + while (search != null) { + if (search._reboundCallbacks.containsKey(type)) { + for (var callback in search._reboundCallbacks[type]!) { + callback(instance, this); + } + } + search = search._parent; + } + } + /// Check if we're in danger of a circular dependency. void _checkCircularDependency(Type type) { if (_buildStack.contains(type)) { diff --git a/incubation/container/container/test/rebound_test.dart b/incubation/container/container/test/rebound_test.dart new file mode 100644 index 0000000..c7ff637 --- /dev/null +++ b/incubation/container/container/test/rebound_test.dart @@ -0,0 +1,111 @@ +import 'package:platformed_container/container.dart'; +import 'package:test/test.dart'; + +class MockReflector extends Reflector { + @override + String? getName(Symbol symbol) => null; + + @override + ReflectedClass? reflectClass(Type clazz) => null; + + @override + ReflectedType? reflectType(Type type) => null; + + @override + ReflectedInstance? reflectInstance(Object? instance) => null; + + @override + ReflectedFunction? reflectFunction(Function function) => null; + + @override + ReflectedType reflectFutureOf(Type type) => throw UnimplementedError(); +} + +class Service { + String value = 'initial'; +} + +void main() { + late Container container; + + setUp(() { + container = Container(MockReflector()); + }); + + group('Rebound Tests', () { + test('rebinding callback is called when singleton is refreshed', () { + var callCount = 0; + var lastInstance; + + container.registerSingleton(Service()); + container.rebinding((instance, container) { + callCount++; + lastInstance = instance; + }); + + var refreshed = container.refresh(); + expect(callCount, equals(1)); + expect(lastInstance, equals(refreshed)); + expect(container.make(), equals(refreshed)); + }); + + test('multiple rebound callbacks are called in order', () { + var order = []; + container.registerSingleton(Service()); + + container.rebinding((instance, container) { + order.add(1); + }); + + container.rebinding((instance, container) { + order.add(2); + }); + + container.refresh(); + expect(order, equals([1, 2])); + }); + + test('child container inherits parent rebound callbacks', () { + var parentCallCount = 0; + var childCallCount = 0; + + container.registerSingleton(Service()); + container.rebinding((instance, container) { + parentCallCount++; + }); + + var child = container.createChild(); + child.rebinding((instance, container) { + childCallCount++; + }); + + child.refresh(); + expect(parentCallCount, equals(1)); + expect(childCallCount, equals(1)); + }); + + test('refresh throws on circular dependency', () { + container.registerSingleton(Service()); + container.rebinding((instance, container) { + container.refresh(); + }); + + expect(() => container.refresh(), + throwsA(isA())); + }); + + test('refresh creates new instance for factory binding', () { + var count = 0; + container.registerFactory((c) { + count++; + return Service(); + }); + + var first = container.make(); + var second = container.refresh(); + + expect(count, equals(2)); + expect(first, isNot(equals(second))); + }); + }); +}