|
|
import { IActivationContext, ILifetime, ILifetimeContext, ILifetimeManager, ILifetimeSlot } from "./interfaces";
|
|
|
import { ActivationContext } from "./ActivationContext";
|
|
|
import { argumentNotNull, isDestroyable } from "./traits";
|
|
|
|
|
|
const safeCall = (item: () => void) => {
|
|
|
try {
|
|
|
item();
|
|
|
} catch {
|
|
|
// silence!
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const noop = () => {};
|
|
|
|
|
|
const fail = (message: string) => {
|
|
|
throw new Error(message);
|
|
|
};
|
|
|
|
|
|
const _emptySlot = Object.freeze({
|
|
|
has: () => false,
|
|
|
|
|
|
initialize: noop,
|
|
|
|
|
|
get: fail("The specified item isn't registered with a lifetime manager"),
|
|
|
|
|
|
store: noop,
|
|
|
|
|
|
remove: noop
|
|
|
});
|
|
|
|
|
|
const _unknonwSlot = Object.freeze({
|
|
|
has: () => false,
|
|
|
|
|
|
initialize: () => {
|
|
|
throw new Error("Can't call initialize on the unknown lifetime object");
|
|
|
},
|
|
|
get: () => {
|
|
|
throw new Error("The lifetime object isn't initialized");
|
|
|
},
|
|
|
store: () => {
|
|
|
throw new Error("Can't store a value in the unknown lifetime object");
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
const pendingSlot = <T>(store: (item: T) => void) => ({
|
|
|
has: () => false,
|
|
|
|
|
|
get: () => {
|
|
|
throw new Error("The value in this slot doesn't exist");
|
|
|
},
|
|
|
|
|
|
initialize: () => {
|
|
|
throw new Error("Cyclic reference detected");
|
|
|
},
|
|
|
|
|
|
store
|
|
|
});
|
|
|
|
|
|
const valueSlot = <T>(value: T, cleanup: (item: T) => void) => ({
|
|
|
has: () => true,
|
|
|
|
|
|
get: () => value,
|
|
|
|
|
|
initialize: () => {
|
|
|
throw new Error("The slot already has a value");
|
|
|
},
|
|
|
|
|
|
store: () => {
|
|
|
throw new Error("The slot already has a value");
|
|
|
}
|
|
|
});
|
|
|
|
|
|
const singletons: { [K: string]: unknown } = {};
|
|
|
|
|
|
export class LifetimeManager implements ILifetimeManager {
|
|
|
private _destroyed = false;
|
|
|
|
|
|
private readonly _slots: Record<string, ILifetimeSlot<unknown>> = {};
|
|
|
|
|
|
slot<T>(cookie: string): ILifetimeSlot<T> {
|
|
|
if (cookie in this._slots)
|
|
|
return this._slots[cookie] as ILifetimeSlot<T>;
|
|
|
|
|
|
const store = (item: T, cleanup?: (item: T) => void) => {
|
|
|
this._assertNotDestroyed();
|
|
|
this._slots[cookie] = valueSlot(
|
|
|
item,
|
|
|
cleanup ?? isDestroyable(item) ? () => item.destroy() : noop
|
|
|
);
|
|
|
};
|
|
|
|
|
|
return {
|
|
|
has: () => false,
|
|
|
get: () => {
|
|
|
throw new Error("The value isn't stored in this slot");
|
|
|
},
|
|
|
store,
|
|
|
initialize: () => {
|
|
|
this._assertNotDestroyed();
|
|
|
this._slots[cookie] = pendingSlot(store);
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
remove(cookie: string) {
|
|
|
delete this._slots[cookie];
|
|
|
}
|
|
|
|
|
|
private _assertNotDestroyed() {
|
|
|
if (this._destroyed)
|
|
|
throw new Error("The lifetime manager is destroyed");
|
|
|
|
|
|
}
|
|
|
|
|
|
destroy() {
|
|
|
if (!this._destroyed) {
|
|
|
this._destroyed = true;
|
|
|
Object.values(this._slots).forEach(({clean}) => )
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
export const emptyLifetime = <T>(): ILifetime<T> => {
|
|
|
return _emptyLifetime;
|
|
|
};
|
|
|
|
|
|
export const hierarchyLifetime = <T>(): ILifetime<T> => {
|
|
|
// TODO: вот здесь ошибка, при первой активации сервиса будет получен и
|
|
|
// привязан lifetime из дочернего контейнера, при активации через второй
|
|
|
// дочерний контейнера это приведет к ошибке, точнее будет взят экземпляр
|
|
|
// из первого контейнера.
|
|
|
let _lifetime: ILifetime<T> = _unknownLifetime;
|
|
|
return {
|
|
|
initialize(context: ILifetimeContext) {
|
|
|
if (_lifetime !== _unknownLifetime)
|
|
|
throw new Error("Cyclic reference activation detected");
|
|
|
|
|
|
_lifetime = context.createContainerLifetime<T>();
|
|
|
},
|
|
|
get() {
|
|
|
return _lifetime.get();
|
|
|
},
|
|
|
has() {
|
|
|
return _lifetime.has();
|
|
|
},
|
|
|
store(item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void) {
|
|
|
return _lifetime.store(item, cleanup);
|
|
|
},
|
|
|
toString() {
|
|
|
return `[object HierarchyLifetime, has=${String(this.has())}]`;
|
|
|
}
|
|
|
};
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Creates a lifetime instance bound to the current activation context. This
|
|
|
* lifetime will store the service instance per activation context. Every
|
|
|
* top level service resolution will create a new activation context. This
|
|
|
* context is propagated to subsequent service resolution thus all services
|
|
|
* with context lifetime will be shared among their consumers.
|
|
|
*
|
|
|
* @returns The instance of the lifetime.
|
|
|
*/
|
|
|
export const contextLifetime = <T>(): ILifetime<T> => {
|
|
|
let _lifetime: ILifetime<T> = _unknownLifetime;
|
|
|
return {
|
|
|
initialize(context: ILifetimeContext) {
|
|
|
if (_lifetime !== _unknownLifetime)
|
|
|
throw new Error("Cyclic reference detected");
|
|
|
_lifetime = context.createLifetime();
|
|
|
},
|
|
|
get() {
|
|
|
return _lifetime.get();
|
|
|
},
|
|
|
has() {
|
|
|
return _lifetime.has();
|
|
|
},
|
|
|
store(item: NonNullable<T>) {
|
|
|
_lifetime.store(item);
|
|
|
},
|
|
|
toString() {
|
|
|
return `[object ContextLifetime, has=${String(this.has())}]`;
|
|
|
}
|
|
|
};
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Creates the lifetime for the service which will allow existence only one
|
|
|
* instance with the specified {@linkcode typeId}. If there will be created
|
|
|
* several lifetime instances with same `typeId` in the runtime, they will
|
|
|
* share the same service instance.
|
|
|
*
|
|
|
* @param typeId The identified for the global instance, usually this is a
|
|
|
* fully qualified class name
|
|
|
* @returns The lifetime instance
|
|
|
*/
|
|
|
export const singletonLifetime = <T>(typeId: string): ILifetime<T> => {
|
|
|
argumentNotNull(typeId, "typeId");
|
|
|
let pending = false;
|
|
|
return {
|
|
|
has() {
|
|
|
return typeId in singletons;
|
|
|
},
|
|
|
get() {
|
|
|
if (!this.has())
|
|
|
throw new Error(`The instance ${typeId} doesn't exists`);
|
|
|
return singletons[typeId] as NonNullable<T>;
|
|
|
},
|
|
|
initialize() {
|
|
|
if (pending)
|
|
|
throw new Error("Cyclic reference detected");
|
|
|
pending = true;
|
|
|
},
|
|
|
store(item: NonNullable<T>) {
|
|
|
singletons[typeId] = item;
|
|
|
pending = false;
|
|
|
},
|
|
|
toString() {
|
|
|
return `[object SingletonLifetime, has=${String(this.has())}, typeId=${typeId}]`;
|
|
|
}
|
|
|
};
|
|
|
};
|
|
|
|
|
|
/** Creates a lifetime bound to the specified container. Using this lifetime
|
|
|
* will create a single service instance per the specified container.
|
|
|
*
|
|
|
* @param container The container which will manage the lifetime for the service
|
|
|
*/
|
|
|
export const containerLifetime = <T>(container: { createLifetime<X>(): ILifetime<X> }) => {
|
|
|
let _lifetime: ILifetime<T> = _unknownLifetime;
|
|
|
return {
|
|
|
initialize() {
|
|
|
if (_lifetime !== _unknownLifetime)
|
|
|
throw new Error("Cyclic reference detected");
|
|
|
_lifetime = container.createLifetime();
|
|
|
},
|
|
|
get() {
|
|
|
return _lifetime.get();
|
|
|
},
|
|
|
has() {
|
|
|
return _lifetime.has();
|
|
|
},
|
|
|
store(item: NonNullable<T>, cleanup?: (item: NonNullable<T>) => void) {
|
|
|
_lifetime.store(item, cleanup);
|
|
|
},
|
|
|
toString() {
|
|
|
return `[object ContainerLifetime, has=${String(_lifetime.has())}]`;
|
|
|
}
|
|
|
};
|
|
|
};
|
|
|
|