##// END OF EJS Templates
intoruced initial work on ILifetimeSlot
cin -
r10:99a53d63fd6f default
parent child
Show More
@@ -1,217 +1,243
1 import { IActivationContext, ILifetime, ILifetimeManager } from "./interfaces";
1 import { IActivationContext, ILifetime, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "./interfaces";
2 2 import { ActivationContext } from "./ActivationContext";
3 3 import { argumentNotNull, isDestroyable } from "./traits";
4 4
5 5 const safeCall = (item: () => void) => {
6 6 try {
7 7 item();
8 8 } catch {
9 9 // silence!
10 10 }
11 11 };
12 12
13 const _emptyLifetime = Object.freeze({
14 has() {
15 return false;
16 },
13 const _emptySlot = Object.freeze({
14 has: () => false,
17 15
18 initialize() {
19 },
16 initialize: () => void (0),
20 17
21 get() {
18 get: () => {
22 19 throw new Error("The specified item isn't registered with this lifetime manager");
23 20 },
24 21
25 store() {
26 // does nothing
27 },
28
29 toString() {
30 return `[object EmptyLifetime]`;
31 }
32
22 store: () => void (0)
33 23 });
34 24
35 const _unknownLifetime = Object.freeze({
36 has() {
37 return false;
38 },
39 initialize() {
25 const _unknonwSlot = Object.freeze({
26 has: () => false,
27
28 initialize: () => {
40 29 throw new Error("Can't call initialize on the unknown lifetime object");
41 30 },
42 get() {
31 get: () => {
43 32 throw new Error("The lifetime object isn't initialized");
44 33 },
45 store() {
34 store: () => {
46 35 throw new Error("Can't store a value in the unknown lifetime object");
47 },
48
49 toString() {
50 return `[object UnknownLifetime]`;
51 36 }
52 37 });
53 38
54 39 let nextId = 0;
55 40
56 41 const singletons: { [K: string]: unknown } = {};
57 42
58 export class LifetimeManager implements ILifetimeManager {
59 private _cleanup: (() => void)[] = [];
60 private readonly _cache: { [K: string]: unknown } = {};
61 private _destroyed = false;
62 43
63 private readonly _pending: { [K: string]: unknown } = {};
64
65 create<T>(): ILifetime<T> & { remove(): void; } {
66 const id = ++nextId;
67 return {
68 has: () => id in this._cache,
44 const pendingSlot = <T>(store: (item: T) => void) => ({
45 has: () => false,
69 46
70 47 get: () => {
71 const t = this._cache[id] as T;
72 if (t === undefined || t === null)
73 throw new Error(`The item with with the key ${id} isn't found`);
74 return t;
48 throw new Error("The value in this slot doesn't exist");
75 49 },
76 50
77 51 initialize: () => {
78 if (this._pending[id])
79 throw Error(`Cyclic reference detected: the item with the key ${id} is already activating.`);
80 this._pending[id] = true;
52 throw new Error("Cyclic reference detected");
53 },
54
55 store
56 });
57
58 const noop = () => void(0);
59
60 const valueSlot = <T>(value: T, cleanup: (item: T) => void) => ({
61 has: () => true,
62
63 get: () => value,
64
65 initialize: () => {
66 throw new Error("The slot already has a value");
81 67 },
82 68
83 store: (item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void) => {
84 if (id in this._cache)
85 throw new Error(`The item with with the key ${id} already registered with this lifetime manager`);
86 if (this._destroyed)
87 throw new Error("Lifetime manager is destroyed");
69 store: () => {
70 throw new Error("The slot already has a value");
71 }
72 });
88 73
89 delete this._pending[id];
74 export class LifetimeManager implements ILifetimeManager {
75 private _destroyed = false;
90 76
91 this._cache[id] = item;
77 private readonly _slots: Record<string, ILifetimeSlot<unknown>> = {};
78
79 slot<T>(cookie: string): ILifetimeSlot<T> {
80 if (cookie in this._slots)
81 return this._slots[cookie] as ILifetimeSlot<T>;
92 82
93 if (cleanup) {
94 this._cleanup.push(() => cleanup(item));
95 } else if (isDestroyable(item)) {
96 this._cleanup.push(() => item.destroy());
97 }
83 const store = (item: T, cleanup?: (item: T) => void) => {
84 this._assertNotDestroyed();
85 this._slots[cookie] = valueSlot(
86 item,
87 cleanup ?? isDestroyable(item) ? () => item.destroy() : noop
88 );
89 };
90
91 return {
92 has: () => false,
93 get: () => {
94 throw new Error("The value isn't stored in this slot");
98 95 },
99
100 remove: () => {
101 if (this._pending[id])
102 throw new Error(`The item '${id}' can't be removed before it has been stored`);
103 delete this._cache[id];
96 store,
97 initialize: () => {
98 this._assertNotDestroyed();
99 this._slots[cookie] = pendingSlot(store);
104 100 }
105 101 };
106 102 }
107 103
104 private _assertNotDestroyed() {
105 if (this._destroyed)
106 throw new Error("The lifetime manager is destroyed");
107
108 }
109
108 110 destroy() {
109 111 if (!this._destroyed) {
110 112 this._destroyed = true;
111 113 this._cleanup.forEach(safeCall);
112 114 this._cleanup.length = 0;
113 115 }
114 116 }
115 117
116 118 }
117 119
118 120 export const emptyLifetime = <T>(): ILifetime<T> => {
119 121 return _emptyLifetime;
120 122 };
121 123
122 124 export const hierarchyLifetime = <T>(): ILifetime<T> => {
123 125 let _lifetime: ILifetime<T> = _unknownLifetime;
124 126 return {
125 initialize(context: IActivationContext<object>) {
127 initialize(context: ILifetimeContext) {
126 128 if (_lifetime !== _unknownLifetime)
127 129 throw new Error("Cyclic reference activation detected");
128 130
129 131 _lifetime = context.createContainerLifetime<T>();
130 132 },
131 133 get() {
132 134 return _lifetime.get();
133 135 },
134 136 has() {
135 137 return _lifetime.has();
136 138 },
137 139 store(item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void) {
138 140 return _lifetime.store(item, cleanup);
139 141 },
140 142 toString() {
141 143 return `[object HierarchyLifetime, has=${String(this.has())}]`;
142 144 }
143 145 };
144 146 };
145 147
148 /**
149 * Creates a lifetime instance bound to the current activation context. This
150 * lifetime will store the service instance per activation context. Every
151 * top level service resolution will create a new activation context. This
152 * context is propagated to subsequent service resolution thus all services
153 * with context lifetime will be shared among their consumers.
154 *
155 * @returns The instance of the lifetime.
156 */
146 157 export const contextLifetime = <T>(): ILifetime<T> => {
147 158 let _lifetime: ILifetime<T> = _unknownLifetime;
148 159 return {
149 initialize(context: ActivationContext<object>) {
160 initialize(context: ILifetimeContext) {
150 161 if (_lifetime !== _unknownLifetime)
151 162 throw new Error("Cyclic reference detected");
152 163 _lifetime = context.createLifetime();
153 164 },
154 165 get() {
155 166 return _lifetime.get();
156 167 },
157 168 has() {
158 169 return _lifetime.has();
159 170 },
160 171 store(item: NonNullable<T>) {
161 172 _lifetime.store(item);
162 173 },
163 174 toString() {
164 175 return `[object ContextLifetime, has=${String(this.has())}]`;
165 176 }
166 177 };
167 178 };
168 179
180 /**
181 * Creates the lifetime for the service which will allow existence only one
182 * instance with the specified {@linkcode typeId}. If there will be created
183 * several lifetime instances with same `typeId` in the runtime, they will
184 * share the same service instance.
185 *
186 * @param typeId The identified for the global instance, usually this is a
187 * fully qualified class name
188 * @returns The lifetime instance
189 */
169 190 export const singletonLifetime = <T>(typeId: string): ILifetime<T> => {
170 191 argumentNotNull(typeId, "typeId");
171 192 let pending = false;
172 193 return {
173 194 has() {
174 195 return typeId in singletons;
175 196 },
176 197 get() {
177 198 if (!this.has())
178 199 throw new Error(`The instance ${typeId} doesn't exists`);
179 200 return singletons[typeId] as NonNullable<T>;
180 201 },
181 202 initialize() {
182 203 if (pending)
183 204 throw new Error("Cyclic reference detected");
184 205 pending = true;
185 206 },
186 207 store(item: NonNullable<T>) {
187 208 singletons[typeId] = item;
188 209 pending = false;
189 210 },
190 211 toString() {
191 212 return `[object SingletonLifetime, has=${String(this.has())}, typeId=${typeId}]`;
192 213 }
193 214 };
194 215 };
195 216
217 /** Creates a lifetime bound to the specified container. Using this lifetime
218 * will create a single service instance per the specified container.
219 *
220 * @param container The container which will manage the lifetime for the service
221 */
196 222 export const containerLifetime = <T>(container: { createLifetime<X>(): ILifetime<X> }) => {
197 223 let _lifetime: ILifetime<T> = _unknownLifetime;
198 224 return {
199 225 initialize() {
200 226 if (_lifetime !== _unknownLifetime)
201 227 throw new Error("Cyclic reference detected");
202 228 _lifetime = container.createLifetime();
203 229 },
204 230 get() {
205 231 return _lifetime.get();
206 232 },
207 233 has() {
208 234 return _lifetime.has();
209 235 },
210 store(item: NonNullable<T>) {
211 _lifetime.store(item);
236 store(item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void) {
237 _lifetime.store(item, cleanup);
212 238 },
213 239 toString() {
214 240 return `[object ContainerLifetime, has=${String(_lifetime.has())}]`;
215 241 }
216 242 };
217 243 };
@@ -1,258 +1,273
1 1 import { key } from "./traits";
2 2
3 3 export interface IDestroyable {
4 4 destroy(): void;
5 5 }
6 6
7 7 /**
8 8 * @template S Карта доступных зависимостей
9 9 */
10 10 export interface Resolver<S> {
11 11 /**
12 12 * Функция для разрешения зависимостей, поддерживает создание фабричных методов,
13 13 * отложенную активацию и значение по-умолчанию для сервисов
14 14 * @template K Ключ сервиса из {@linkcode S}
15 15 * @template O Тип параметра {@linkcode opts} используется для выведения типа
16 16 * возвращаемого значения.
17 17 * @param name Ключ сервиса, который будет разрешен.
18 18 * @param {boolean=} opts.lazy Признак того, что требуется отложенная активация,
19 19 * будет возвращен фабричный метод для получения зависимости. Если не указан,
20 20 * то считается `false`.
21 21 * @param {any=} opts.default Значение по умолчанию, если в контейнере указанный
22 22 * сервис не зарегистрирован
23 23 * @returns Либо фабричный метод для получения зависимости, либо значение зависимости
24 24 * @throws Error Если зависимость не найдена и не предоставлено значение по-умолчанию
25 25 */
26 26 <K extends keyof S, O extends { lazy: true; }>(name: K, opts?: O): () => NonNullable<S[K]> | InferDefault<O>;
27 27 <K extends keyof S, O extends { lazy?: false; }>(name: K, opts?: O): NonNullable<S[K]> | InferDefault<O>;
28 28 }
29 29
30 30 export type DepsMap<S> = {
31 31 [k in key]: Refs<S> | keyof S;
32 32 };
33 33
34 34 export type Refs<S> = {
35 35 [k in keyof S]: Ref<k, S[k]>;
36 36 }[keyof S];
37 37
38 38 export type Ref<K extends key, D> = {
39 39 /** The name of the service */
40 40 name: K;
41 41
42 42 /** Make a lazy reference, the resolved dependency will be a function */
43 43 lazy?: boolean;
44 44
45 45 /** The default value for the case where the service isn't defined.
46 46 * When specified the dependency becomes optional, the default value can be
47 47 * `null` or `undefined`
48 48 */
49 49 default?: D | null
50 50 };
51 51
52 52 export type Lazy<T, L extends boolean> = L extends true ? () => T : T;
53 53
54 54 /** Возвращает тип свойства `default` в типе {@link T} */
55 55 export type InferDefault<T> = T extends { default: infer D } ? D : never;
56 56
57 57 export type InferLazy<R> = R extends { lazy: infer L } ?
58 58 L extends true ? true : false :
59 59 false;
60 60 export type Resolve<S, R> =
61 61 R extends keyof S ? NonNullable<S[R]> :
62 62 R extends Ref<infer K, unknown> ?
63 63 K extends keyof S ?
64 64 Lazy<NonNullable<S[K]> | InferDefault<R>, InferLazy<R>> :
65 65 never :
66 66 never;
67 67
68 68 /**
69 69 * Интерфейс для конфигурирования сервиса в контейнере. Конфигурирование сервиса
70 70 * состоит из настройки различных параметров вызовами методов {@linkcode wants},
71 71 * {@linkcode lifetime}, {@linkcode override}, {@linkcode cleanup}. Завершение настройки
72 72 * сервиса осуществляется вызовом одного из методов {@linkcode factory} либо
73 73 * {@linkcode value}.
74 74 *
75 75 * @template S Карта сервисов контейнера, доступных при описании дескриптора
76 76 * @template T Тип сервиса
77 77 * @template R Карта зависимостей, которая передается параметром фабрике
78 78 * @template U Имена пользовательских сервисов, доступных для переопределения
79 79 */
80 80 export interface IDescriptorBuilder<S, T, R, U extends keyof S> {
81 81
82 82 /** Указывает фабрика для создания экземпляра сервиса, фабрика передается
83 83 * в виде параметра. При вызове фабрике будет передан объект с зависимостями,
84 84 * которые были предварительно указаны вызовами метода `wants(...)`
85 85 *
86 86 * Вызов данного метода завершает конфигурирование сервиса.
87 87 *
88 88 * @param f Фабрика для создания экземпляра сервиса.
89 89 */
90 90 factory(f: (refs: R) => NonNullable<T>): void;
91 91
92 92 /**
93 93 * Используется для указания зависимостей, которые потребуются фабричному
94 94 * методу при создании нового экземпляра сервиса. Данный метод может быть
95 95 * вызван несколько раз подряд, при этом вызовы этого метода имеют
96 96 * кумулятивный эффект.
97 97 *
98 98 * @template X Тип объекта с зависимостями, которые требуется получить при
99 99 * создании экземпляра фабрики при помощи фабричного метода.
100 100 * @param refs Объект с описанием зависимостей
101 101 * @returns Возвращает дескриптор сервиса, в котором указаны необходимые
102 102 * зависимости
103 103 */
104 104 wants<X extends DepsMap<S> & Record<keyof R & keyof X, never>>(refs: X):
105 105 IDescriptorBuilder<S, T, R & {
106 106 [k in keyof X]: Resolve<S, X[k]>;
107 107 }, U>
108 108
109 109 override<K extends U>(name: K, builder: BuildDescriptorFn<S, S[K], U>): this;
110 110 override<X extends ConfigurationMapConstraint<S, U, keyof X>>(services: X): this;
111 111
112 112 lifetime(lifetime: "singleton", typeId: string | number | object): this;
113 113 lifetime(lifetime: ILifetime<T> | Exclude<ActivationType, "singleton">): this;
114 114
115 115 /** Указывает функцию для освобождения экземпляра сервиса для случаев, когда
116 116 * время жизни привязано к контейнеру.
117 117 */
118 118 cleanup(cb: (item: T) => void): this;
119 119
120 120 /**
121 121 * Регистрирует в контейнере постоянное значение в качестве реализации сервиса.
122 122 *
123 123 * @param v Экземпляр реализации сервиса.
124 124 */
125 125 value(v: NonNullable<T>): void;
126 126 }
127 127
128 128 export type BuildDescriptorFn<S, T, U extends keyof S> = (d: IDescriptorBuilder<S, T, Record<never, never>, U>) => void;
129 129
130 130 /**
131 131 * Конфигурация контейнера, состоит из набора функций, которые выполняют конфигурацию.
132 132 *
133 133 * Все параметры конфигурации являются обязательными, если требуется ввести
134 134 * необязательные параметры, то нужно ограничить параметр типа {@linkcode K}
135 135 *
136 136 * @template S Сервисы доступные в контейнере
137 137 * @template K Сервисы участвующие в конфигурации
138 138 */
139 139 export type ConfigurationMap<S, K extends keyof S, U extends keyof S> = {
140 140 [k in K]-?: BuildDescriptorFn<S, S[k], U>
141 141 };
142 142
143 143 export type ConfigurationMapConstraint<S, U extends keyof S, X extends string | number | symbol> = {
144 144 [k in X]-?: k extends U ? BuildDescriptorFn<S, S[k], U> : never;
145 145 };
146 146
147 147 /**
148 148 * The type constraint useful to restrict type parameters to prevent defining
149 149 * the services with the {@link ContainerKeys} names.
150 150 *
151 151 * The constraint doesn't exclude using this keys but declares them as `never`
152 152 * which effectively will lead using this keys to the error.
153 153 */
154 154 export type ContainerServicesConstraint<S> = {
155 155 [k in keyof S]: k extends ContainerKeys ? never : S[k];
156 156 };
157 157
158 158 export interface Descriptor<S, T> {
159 159
160 160 /** This flags indicates that this registration can be replaced or overridden. */
161 161 readonly configurable?: boolean;
162 162
163 163 /** If specified signals the activation context that a new service scope
164 164 * should be created to isolate service overrides.
165 165 */
166 166 readonly hasOverrides?: boolean;
167 167
168 168 activate(context: IActivationContext<S>): NonNullable<T>;
169 169 }
170 170
171 export interface IActivationContext<S> extends ServiceLocator<S> {
171 /** The context used to initialize lifetime instance {@linkcode ILifetime} */
172 export interface ILifetimeContext {
172 173 createLifetime<T>(): ILifetime<T>;
173 174
174 175 createContainerLifetime<T>(): ILifetime<T>;
175 176
177 }
178
179 export interface IActivationContext<S> extends ILifetimeContext, ServiceLocator<S> {
180
176 181 register<K extends keyof S>(name: K, service: DescriptorMap<S>[K]): void;
177 182 }
178 183
179 184 /**
180 185 * Descriptors map for the specified services {@linkcode S}. All entries are
181 186 * optional regardless the required or optional services in the original map.
182 187 *
183 188 * @template S Сервисы контекста активации
184 189 * @template U Карта сервисов которые создаются дескрипторами
185 190 */
186 191 export type DescriptorMap<S> = {
187 192 [k in keyof S]?: Descriptor<S, S[k]>;
188 193 };
189 194
190 195 type ContainerKeys = keyof ContainerProvided<object>;
191 196
192 197 export type ContainerProvided<S extends ContainerServicesConstraint<S>> = {
193 198 container: ServiceLocator<ContainerServices<S>>;
194 199
195 200 childContainer: IContainerBuilder<ContainerServices<S>, Exclude<keyof S, ContainerKeys>>;
196 201 };
197 202
198 203
199 204 /**
200 205 * Таблица сервисов, которые предоставляет контейнер.
201 206 *
202 207 * Сервисы, предоставляемые контейнером не могут быть null или undefined.
203 208 */
204 209 export type ContainerServices<S extends ContainerServicesConstraint<S>> = {
205 210 [k in keyof S | ContainerKeys]:
206 211 k extends ContainerKeys ? ContainerProvided<S>[k] :
207 212 k extends keyof S ? S[k] : never
208 213 };
209 214
210 215
211 216 /**
212 217 * Returns the service declared in the type map {@link S}.
213 218 *
214 219 *
215 220 */
216 221 export interface ServiceLocator<S> {
217 222 resolve<K extends keyof S>(name: K): NonNullable<S[K]>;
218 223 resolve<K extends keyof S, T>(name: K, def: T): NonNullable<S[K]> | T;
219 224 }
220 225
221 226 export interface LifetimeContainer {
222 227 createLifetime<T>(): ILifetime<T>;
223 228 }
224 229
225 230 export interface IContainerBuilder<S, U extends keyof S> {
226 231 createServiceBuilder<K extends U>(name: K):
227 232 IDescriptorBuilder<S, S[K], Record<never, never>, U>;
228 233
229 234 build(): ServiceLocator<S>;
230 235 }
231 236
232 237
233 238 export type ActivationType = "singleton" | "container" | "hierarchy" | "context" | "call";
234 239
235 240 /**
236 241 * Интерфейс для управления жизнью экземпляра объекта. Каждая регистрация имеет
237 242 * свой собственный объект `ILifetime`, который создается при первой активации
238 243 */
239 244 export interface ILifetime<T> {
240 245 /** Проверяет, что уже создан экземпляр объекта */
241 246 has(): boolean;
242 247
243 248 get(): NonNullable<T>;
244 249
245 initialize(context: IActivationContext<unknown>): void;
250 initialize(context: ILifetimeContext): void;
246 251
247 252 store(item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void): void;
248 253
249 254 toString(): string;
250 255 }
251 256
257 export interface ILifetimeSlot<T> {
258 has: () => boolean;
259
260 get: () => T;
261
262 initialize: () => void;
263
264 store(item: T, cleanup?: (item: T) => void): void;
265 }
266
252 267 export interface ILifetimeManager extends IDestroyable {
253 268 create<T>(): ILifetime<T>;
254 269 }
255 270
256 271 export type ExtractRequired<T, K extends keyof T = keyof T> = { [p in K as (undefined extends T[p] ? never : p)]-?: T[p] };
257 272
258 273 export type ExtractRequiredKeys<T, K extends keyof T = keyof T> = { [p in K]-?: undefined extends T[p] ? never : p }[K]; No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now