Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:ioc_container/async_lock.dart';
4 :
5 : ///❌ An exception that occurs when the service is not found
6 : class ServiceNotFoundException<T> implements Exception {
7 : ///❌ Creates a new instance of [ServiceNotFoundException]
8 1 : const ServiceNotFoundException(this.message);
9 :
10 : ///The exception message
11 : final String message;
12 1 : @override
13 2 : String toString() => 'ServiceNotFoundException: $message';
14 : }
15 :
16 : ///📙 Defines a factory for the service and whether or not it is a singleton.
17 : class ServiceDefinition<T> {
18 : ///📙 Defines a factory for the service and whether or not it is a singleton.
19 2 : const ServiceDefinition(
20 : this.factory, {
21 : this.isSingleton = false,
22 : this.dispose,
23 : this.disposeAsync,
24 : }) : assert(
25 2 : dispose == null || disposeAsync == null,
26 : "Service definitions can't have both dispose and disposeAsync",
27 : );
28 :
29 : ///1️⃣ If true, only one instance of the service will be created and shared
30 : ///for the lifespan of the container.
31 : final bool isSingleton;
32 :
33 : ///🏭 The factory that creates the instance of the service and can access
34 : ///other services in this container
35 : final T Function(
36 : IocContainer container,
37 : ) factory;
38 :
39 : ///🗑️ The dispose method that is called when you dispose the scope
40 : final void Function(T service)? dispose;
41 :
42 : ///🗑️ The async dispose method that is called when you dispose the scope
43 : final Future<void> Function(T service)? disposeAsync;
44 :
45 3 : void _dispose(T instance) => dispose?.call(instance);
46 :
47 3 : Future<void> _disposeAsync(T instance) async => disposeAsync?.call(instance);
48 : }
49 :
50 : ///📦 A built Ioc Container. To create a new [IocContainer], use
51 : ///[IocContainerBuilder]. To get a service from the container, call
52 : ///[get], or [getAsync]
53 : ///Call [scoped] to get a scoped container
54 : class IocContainer {
55 : ///📦 Creates an IocContainer. You can build your own container by injecting
56 : ///service definitions and singletons here, but you should probably use
57 : ///[IocContainerBuilder] instead.
58 2 : const IocContainer(
59 : this.serviceDefinitionsByType,
60 : this.singletons,
61 : this.locks, {
62 : this.isScoped = false,
63 : });
64 :
65 : ///📙 The service definitions by type
66 : final Map<Type, ServiceDefinition<dynamic>> serviceDefinitionsByType;
67 :
68 : ///1️⃣ Map of singletons or scoped services by type. This map is mutable
69 : ///so the container can store scope or singletons
70 : final Map<Type, Object> singletons;
71 :
72 : ///🔒 Map of locks by type. This ensures that no async singletons execute
73 : ///more than once, unless there is an error
74 : // ignore: strict_raw_type
75 : final Map<Type, AsyncLock> locks;
76 :
77 : ///⌖ If true, this container is a scoped container. Scoped containers never
78 : ///create more than one instance of a service
79 : final bool isScoped;
80 :
81 : ///👐 Get an instance of the service by type
82 2 : T get<T extends Object>() {
83 4 : final serviceDefinition = serviceDefinitionsByType[T];
84 :
85 : if (serviceDefinition == null) {
86 1 : throw ServiceNotFoundException<T>(
87 1 : 'Service $T not found',
88 : );
89 : }
90 :
91 4 : if (serviceDefinition.isSingleton || isScoped) {
92 4 : final singletonValue = singletons[T];
93 :
94 : if (singletonValue != null) {
95 : return singletonValue as T;
96 : }
97 : }
98 :
99 4 : final service = serviceDefinition.factory(this) as T;
100 :
101 4 : if (serviceDefinition.isSingleton || isScoped) {
102 4 : singletons[T] = service;
103 : }
104 :
105 : return service;
106 : }
107 :
108 : ///👐 This is a shortcut for [get]
109 2 : T call<T extends Object>() => get<T>();
110 : }
111 :
112 : ///👷 A builder for creating an [IocContainer].
113 : class IocContainerBuilder {
114 : ///👷 Creates a container builder
115 2 : IocContainerBuilder({this.allowOverrides = false});
116 : final Map<Type, ServiceDefinition<dynamic>> _serviceDefinitionsByType = {};
117 :
118 : /// 🔃 Throw an error if a service is added more than once. Set this to true
119 : /// when you want to add mocks to set of services for a test.
120 : final bool allowOverrides;
121 :
122 : ///📙 Add a factory to the container.
123 2 : void addServiceDefinition<T>(
124 : ///Add a factory and whether or not this service is a singleton
125 : ServiceDefinition<T> serviceDefinition,
126 : ) {
127 4 : if (_serviceDefinitionsByType.containsKey(T)) {
128 1 : if (allowOverrides) {
129 2 : _serviceDefinitionsByType.remove(T);
130 : } else {
131 1 : throw Exception('Service already exists');
132 : }
133 : }
134 :
135 6 : _serviceDefinitionsByType.putIfAbsent(T, () => serviceDefinition);
136 : }
137 :
138 : ///📦 Create an [IocContainer] from the [IocContainerBuilder].
139 4 : IocContainer toContainer() => IocContainer(
140 2 : Map<Type, ServiceDefinition<dynamic>>.unmodifiable(
141 2 : _serviceDefinitionsByType,
142 : ),
143 2 : <Type, Object>{},
144 : // ignore: strict_raw_type
145 2 : <Type, AsyncLock>{},
146 : );
147 :
148 : ///1️⃣ Add a singleton factory to the container. The container
149 : ///will only call this once throughout the lifespan of the container
150 2 : void addSingleton<T>(
151 : T Function(
152 : IocContainer container,
153 : ) factory, {
154 : void Function(T service)? dispose,
155 : }) =>
156 2 : addServiceDefinition<T>(
157 2 : ServiceDefinition<T>(
158 4 : (container) => factory(container),
159 : isSingleton: true,
160 : dispose: dispose,
161 : ),
162 : );
163 :
164 : ///🏭 Add a factory to the container.
165 2 : void add<T>(
166 : T Function(
167 : IocContainer container,
168 : ) factory, {
169 : void Function(T service)? dispose,
170 : }) =>
171 2 : addServiceDefinition<T>(
172 2 : ServiceDefinition<T>(
173 4 : (container) => factory(container),
174 : dispose: dispose,
175 : ),
176 : );
177 :
178 : ///⌛ Adds an async [ServiceDefinition]
179 1 : void addAsync<T>(
180 : Future<T> Function(
181 : IocContainer container,
182 : ) factory, {
183 : Future<void> Function(T service)? disposeAsync,
184 : }) =>
185 1 : addServiceDefinition<Future<T>>(
186 1 : ServiceDefinition<Future<T>>(
187 2 : (container) async => factory(container),
188 2 : disposeAsync: (service) async => disposeAsync?.call(await service),
189 : ),
190 : );
191 :
192 : ///1️⃣ ⌛ Add an async singleton factory to the container. The container
193 : ///will only call the factory once throughout the lifespan of the container
194 1 : void addSingletonAsync<T>(
195 : Future<T> Function(
196 : IocContainer container,
197 : ) factory, {
198 : Future<void> Function(T service)? disposeAsync,
199 : }) =>
200 1 : addServiceDefinition<Future<T>>(
201 1 : ServiceDefinition<Future<T>>(
202 : isSingleton: true,
203 2 : (container) async => factory(container),
204 0 : disposeAsync: (service) async => disposeAsync?.call(await service),
205 : ),
206 : );
207 : }
208 :
209 : ///Extensions for IocContainer
210 : extension IocContainerExtensions on IocContainer {
211 : ///🗑️ Dispose all singletons or scope. Warning: don't use this on your root
212 : ///container. You should only use this on scoped containers.
213 1 : Future<void> dispose() async {
214 1 : assert(isScoped, 'Only dispose scoped containers');
215 3 : for (final type in singletons.keys) {
216 : //Note: we don't need to check if the service is a singleton because
217 : //singleton service definitions never have dispose
218 2 : final serviceDefinition = serviceDefinitionsByType[type]!;
219 :
220 : //We can't do a null check here because if a Dart issue
221 4 : serviceDefinition._dispose.call(singletons[type]);
222 :
223 3 : await serviceDefinition._disposeAsync(singletons[type]);
224 : }
225 2 : singletons.clear();
226 : }
227 :
228 : ///🏁 Initalizes and stores each singleton in case you want a zealous
229 : ///container instead of a lazy one
230 1 : void initializeSingletons() {
231 3 : serviceDefinitionsByType.forEach((type, serviceDefinition) {
232 1 : if (serviceDefinition.isSingleton) {
233 2 : singletons.putIfAbsent(
234 : type,
235 3 : () => serviceDefinition.factory(
236 : this,
237 : ) as Object,
238 : );
239 : }
240 : });
241 : }
242 :
243 : ///⌖ Gets a service, but each service in the object mesh will have only one
244 : ///instance. If you want to get multiple scoped objects, call [scoped] to
245 : ///get a reusable [IocContainer] and then call [get] or [getAsync] on that.
246 3 : T getScoped<T extends Object>() => scoped().get<T>();
247 :
248 : ///⌖ Creates a new Ioc Container for a particular scope. Does not use existing
249 : ///singletons/scope by default. Warning: if you use the existing singletons,
250 : ///calling [dispose] will dispose those singletons
251 2 : IocContainer scoped({
252 : bool useExistingSingletons = false,
253 : }) =>
254 2 : IocContainer(
255 2 : serviceDefinitionsByType,
256 4 : useExistingSingletons ? Map<Type, Object>.from(singletons) : {},
257 2 : {},
258 : isScoped: true,
259 : );
260 :
261 : ///⌛ Gets a service that requires async initialization. Add these services
262 : ///with [IocContainerBuilder.addAsync] or
263 : ///[IocContainerBuilder.addSingletonAsync]. You can only use this on factories
264 : ///that return a Future<>.
265 1 : Future<T> getAsync<T>([
266 : Duration? timeout = const Duration(minutes: 5),
267 : ]) async {
268 2 : final serviceDefinition = serviceDefinitionsByType[Future<T>];
269 :
270 : if (serviceDefinition == null) {
271 1 : throw ServiceNotFoundException<T>(
272 1 : 'Service $T not found',
273 : );
274 : }
275 :
276 2 : if (serviceDefinition.isSingleton || isScoped) {
277 2 : final singletonValue = singletons[Future<T>];
278 :
279 : if (singletonValue != null) {
280 : //Return completed successful future
281 : return singletonValue as Future<T>;
282 : }
283 :
284 2 : if (!locks.containsKey(T)) {
285 : //Add a lock
286 2 : locks[T] =
287 4 : AsyncLock<T>(() => serviceDefinition.factory(this) as Future<T>);
288 : }
289 :
290 2 : final lock = locks[T]! as AsyncLock<T>;
291 :
292 : try {
293 : //Await the locked call
294 1 : final future = timeout != null ? lock.execute(timeout) : lock.execute();
295 : await future;
296 :
297 : //Store successful future
298 2 : singletons[Future<T>] = future;
299 :
300 : return future;
301 : } finally {
302 : //Remove the lock
303 2 : locks.remove(T);
304 : }
305 : }
306 :
307 2 : return serviceDefinition.factory(this) as Future<T>;
308 : }
309 :
310 : ///⛙ Merge the singletons or scope from a container into this container. This
311 : ///only moves singleton definitions by default, but you can override this
312 : ///with [mergeTest]
313 1 : void merge(
314 : IocContainer container, {
315 : bool overwrite = false,
316 : bool Function(
317 : Type type,
318 : ServiceDefinition<dynamic>? serviceDefinition,
319 : Object? singleton,
320 : )? mergeTest,
321 : }) {
322 3 : for (final key in container.singletons.keys.where(
323 : mergeTest != null
324 2 : ? (type) => mergeTest(
325 : type,
326 2 : serviceDefinitionsByType[type],
327 2 : container.singletons[type],
328 : )
329 4 : : (type) => serviceDefinitionsByType[type]?.isSingleton ?? false,
330 1 : )) {
331 : if (overwrite) {
332 4 : singletons[key] = container.singletons[key]!;
333 : } else {
334 5 : singletons.putIfAbsent(key, () => container.singletons[key]!);
335 : }
336 : }
337 : }
338 : }
|