Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/catalyst_builder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 6.3.0

### Features

- Implemented `ServiceContainer.scope()`: child scope with an isolated singleton cache and parent resolution fallback (also for `has` and `resolveByTag`).

## 6.2.1

### Fixes
Expand Down
48 changes: 40 additions & 8 deletions packages/catalyst_builder/lib/src/service_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class ServiceContainer implements AbstractServiceContainer, ServiceRegistry {

var _booted = false;

ServiceContainer? _parent;

@override
T? tryResolve<T>([Type? t]) {
return _tryResolveInternal<T>(t ?? T);
Expand All @@ -34,7 +36,7 @@ class ServiceContainer implements AbstractServiceContainer, ServiceRegistry {
}
var descriptor = _knownServices[exposedType];
if (descriptor == null) {
return null;
return _parent?._tryResolveInternal<T>(t);
}
var instance = descriptor.produce();
if (descriptor.service.lifetime == ServiceLifetime.singleton) {
Expand All @@ -56,13 +58,15 @@ class ServiceContainer implements AbstractServiceContainer, ServiceRegistry {
@override
List<dynamic> resolveByTag(Symbol tag) {
var services = <dynamic>[];
if (!_servicesByTag.containsKey(tag)) {
return services;
}
for (var svc in _servicesByTag[tag]!) {
services.add((_tryResolveInternal(svc) as dynamic));
if (_servicesByTag.containsKey(tag)) {
for (var svc in _servicesByTag[tag]!) {
services.add((_tryResolveInternal(svc) as dynamic));
}
}
return services;
return [
...services,
...?_parent?.resolveByTag(tag),
];
}

@override
Expand Down Expand Up @@ -114,7 +118,8 @@ class ServiceContainer implements AbstractServiceContainer, ServiceRegistry {
@override
bool has<T>([Type? type]) {
var lookupType = type ?? T;
return _knownServices.containsKey(_exposeMap[lookupType] ?? lookupType);
return _knownServices.containsKey(_exposeMap[lookupType] ?? lookupType) ||
(_parent?.has<T>(type) ?? false);
}

@override
Expand Down Expand Up @@ -171,6 +176,33 @@ class ServiceContainer implements AbstractServiceContainer, ServiceRegistry {
return enhanced;
}

@override
AbstractServiceContainer scope({
List<LazyServiceDescriptor> services = const <LazyServiceDescriptor>[],
Map<String, dynamic> parameters = const <String, dynamic>{},
}) {
_ensureBoot();
var child = ServiceContainer();
child._parent = this;
// child._serviceInstances stays its OWN fresh empty map (the key difference
// from enhance, which shares the parent's map by reference). Scope-local
// services registered below bind their factory to the child, so they
// resolve their own dependencies through the child first, then the parent.
for (var service in services) {
child._registerInternal(
service.returnType,
service.factory,
service.service,
);
}
// Parameters are snapshot-merged at creation; service resolution still uses
// live parent fallback through _tryResolveInternal.
child.parameters.addAll(this.parameters);
child.parameters.addAll(parameters);
child._booted = true;
return child;
}

@override
void applyPlugin(ServiceContainerPlugin plugin) {
if (_booted) {
Expand Down
4 changes: 2 additions & 2 deletions packages/catalyst_builder/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: catalyst_builder
description: A lightweight and easy to use dependency injection container builder for dart.
version: 6.2.1
version: 6.3.0
homepage: 'https://github.com/mintware-de/catalyst_builder'
repository: 'https://github.com/mintware-de/catalyst_builder/tree/main/packages/catalyst_builder'
documentation: 'https://github.com/mintware-de/catalyst_builder/wiki'
Expand Down Expand Up @@ -32,7 +32,7 @@ dependencies:
build: ^4.0.0
glob: ^2.1.0
analyzer: ">=8.1.0 <14.0.0"
catalyst_builder_contracts: ^2.1.0
catalyst_builder_contracts: ^2.2.0

dev_dependencies:
test: any
Expand Down
6 changes: 6 additions & 0 deletions packages/catalyst_builder/pubspec_overrides.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Local development override: resolve the sibling contracts package from source
# so the builder's tests run against the in-repo (yet-unpublished) version.
# Ignored by the parent workspace resolution (overrides only apply at the root).
dependency_overrides:
catalyst_builder_contracts:
path: ../catalyst_builder_contracts
83 changes: 83 additions & 0 deletions packages/catalyst_builder/test/unit/scope_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:catalyst_builder/catalyst_builder.dart';
import 'package:catalyst_builder_contracts/catalyst_builder_contracts.dart';
import 'package:test/test.dart';

class A {}

class B {}

class C {
C(this.a);

final A a;
}

void main() {
group('ServiceContainer.scope', () {
test('parent singleton resolves through the scope (cached at parent)', () {
var parent = ServiceContainer()
..register<A>((_) => A())
..boot();

var child = parent.scope();

expect(child.resolve<A>(), same(parent.resolve<A>()));
});

test('scope-local service does not leak to the parent', () {
var parent = ServiceContainer()..boot();

var child = parent.scope(
services: [
LazyServiceDescriptor<B>((_) => B()),
],
);

expect(child.resolve<B>(), isA<B>());
expect(parent.has<B>(), isFalse);
expect(parent.tryResolve<B>(), isNull);
});

test('sibling scopes are isolated but share parent singletons', () {
var parent = ServiceContainer()
..register<A>((_) => A())
..boot();

var s1 = parent.scope(
services: [LazyServiceDescriptor<B>((_) => B())],
);
var s2 = parent.scope(
services: [LazyServiceDescriptor<B>((_) => B())],
);

expect(s1.resolve<B>(), isNot(same(s2.resolve<B>())));
expect(s1.resolve<A>(), same(s2.resolve<A>()));
});

test('scope-local service can depend on a parent service', () {
var parent = ServiceContainer()
..register<A>((_) => A())
..boot();

var child = parent.scope(
services: [
LazyServiceDescriptor<C>((c) => C(c.resolve<A>())),
],
);

expect(child.resolve<C>().a, same(parent.resolve<A>()));
});

test('has and resolveByTag fall back to the parent', () {
const tag = #thing;
var parent = ServiceContainer()
..register<A>((_) => A(), const Service(tags: [tag]))
..boot();

var child = parent.scope();

expect(child.has<A>(), isTrue);
expect(child.resolveByTag(tag), contains(parent.resolve<A>()));
});
});
}
6 changes: 6 additions & 0 deletions packages/catalyst_builder_contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2.2.0

### Features

- Added `scope()` to `AbstractServiceContainer` for hierarchical child scopes (own instance cache + parent fallback).

## 2.1.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ abstract interface class AbstractServiceContainer {
Map<String, dynamic> parameters = const {},
});

/// Creates a child scope: a container with its OWN singleton-instance cache
/// and this container as parent. Unknown services resolve from the parent
/// (and cache there); services passed in [services] are scope-local and
/// cached only in the child, so they never leak into the parent or siblings.
///
/// [parameters] are snapshot-merged at creation (parent parameters first,
/// then the provided [parameters]); service resolution still falls back to
/// the live parent on a local miss.
AbstractServiceContainer scope({
List<LazyServiceDescriptor> services = const [],
Map<String, dynamic> parameters = const {},
});

/// Resolves a service or gets a matching parameter.
/// If neither a service nor a parameter is found, an exception is thrown.
T resolveOrGetParameter<T>(
Expand Down
2 changes: 1 addition & 1 deletion packages/catalyst_builder_contracts/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: catalyst_builder_contracts
description: This is the contracts package for the catalyst_builder package. It includes annotations and marker interfaces.
version: 2.1.0
version: 2.2.0
homepage: 'https://github.com/mintware-de/catalyst_builder'
repository: 'https://github.com/mintware-de/catalyst_builder/tree/main/packages/catalyst_builder_contracts'
documentation: https://github.com/mintware-de/catalyst_builder/wiki
Expand Down