Socket
Socket
Sign inDemoInstall

@furystack/inject

Package Overview
Dependencies
Maintainers
1
Versions
148
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@furystack/inject - npm Package Compare versions

Comparing version 10.0.0 to 11.0.0

esm/with-injector-reference.d.ts

17

esm/injectable.d.ts

@@ -12,3 +12,19 @@ import type { Constructable } from './models/constructable.js';

export declare const defaultInjectableOptions: InjectableOptions;
export declare const InjectableOptionsSymbol: unique symbol;
type WithInjectableOptions<T> = T & {
[InjectableOptionsSymbol]: InjectableOptions;
};
/**
* Checks if the constructor is decorated with Injectable() with verifying if it has Injectable options
* @param ctor The constructor to check
* @returns if the constructor has the InjectableOptionsSymbol
*/
export declare const hasInjectableOptions: <T extends Constructable<any>>(ctor: T) => ctor is WithInjectableOptions<T>;
/**
* @throws Error if the class is not an injectable
* @param ctor The constructor to get the options from
* @returns The InjectableOptions object
*/
export declare const getInjectableOptions: <T extends Constructable<any>>(ctor: T) => InjectableOptions;
/**
* Decorator method for tagging a class as injectable

@@ -19,2 +35,3 @@ * @param options The options object

export declare const Injectable: (options?: Partial<InjectableOptions>) => <T extends Constructable<any>>(ctor: T) => void;
export {};
//# sourceMappingURL=injectable.d.ts.map

30

esm/injectable.js

@@ -1,2 +0,1 @@

import { Injector } from './injector.js';
/**

@@ -8,3 +7,24 @@ * The default options for the injectable classes

};
export const InjectableOptionsSymbol = Symbol('InjectableOptions');
/**
* Checks if the constructor is decorated with Injectable() with verifying if it has Injectable options
* @param ctor The constructor to check
* @returns if the constructor has the InjectableOptionsSymbol
*/
export const hasInjectableOptions = (ctor) => {
return (typeof ctor[InjectableOptionsSymbol] === 'object' &&
typeof ctor[InjectableOptionsSymbol].lifetime === 'string');
};
/**
* @throws Error if the class is not an injectable
* @param ctor The constructor to get the options from
* @returns The InjectableOptions object
*/
export const getInjectableOptions = (ctor) => {
if (!hasInjectableOptions(ctor)) {
throw Error(`The class '${ctor.name}' is not an injectable`);
}
return ctor[InjectableOptionsSymbol];
};
/**
* Decorator method for tagging a class as injectable

@@ -16,5 +36,7 @@ * @param options The options object

return (ctor) => {
Injector.options.set(ctor, {
...defaultInjectableOptions,
...options,
Object.assign(ctor, {
[InjectableOptionsSymbol]: {
...defaultInjectableOptions,
...options,
},
});

@@ -21,0 +43,0 @@ };

@@ -7,7 +7,35 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

};
import { Injectable } from './injectable.js';
import { Injector } from './injector.js';
import { Injectable, InjectableOptionsSymbol, getInjectableOptions, hasInjectableOptions } from './injectable.js';
import { describe, expect, it } from 'vitest';
describe('hasInjectableOptions', () => {
it('Should return true if the object has the InjectableOptionsSymbol', () => {
class Alma {
}
Object.assign(Alma, { [InjectableOptionsSymbol]: { lifetime: 'singleton' } });
expect(hasInjectableOptions(Alma)).toBe(true);
});
it('Should return false if the object does not have the InjectableOptionsSymbol', () => {
class Alma {
}
expect(hasInjectableOptions(Alma)).toBe(false);
});
it('Should return false if the object is not an object', () => {
expect(hasInjectableOptions('')).toBe(false);
});
});
describe('getInjectableOptions', () => {
it('Should throw an error if the object does not have the InjectableOptionsSymbol', () => {
class Alma {
}
expect(() => getInjectableOptions(Alma)).toThrowError("The class 'Alma' is not an injectable");
});
it('Should return the options if the object has the InjectableOptionsSymbol', () => {
class Alma {
}
Object.assign(Alma, { [InjectableOptionsSymbol]: { lifetime: 'singleton' } });
expect(getInjectableOptions(Alma)).toEqual({ lifetime: 'singleton' });
});
});
describe('@Injectable()', () => {
it('Should fill meta store with default options', () => {
it('Should attach the default options by default', () => {
let TestClass1 = class TestClass1 {

@@ -18,7 +46,7 @@ };

], TestClass1);
const meta = Injector.options.get(TestClass1);
const meta = getInjectableOptions(TestClass1);
expect(meta).toBeDefined();
expect(meta?.lifetime).toBe('transient');
});
it('Should fill meta store', () => {
it('Should attach the explicitly set options', () => {
let TestClass2 = class TestClass2 {

@@ -29,3 +57,3 @@ };

], TestClass2);
const meta = Injector.options.get(TestClass2);
const meta = getInjectableOptions(TestClass2);
expect(meta).toBeDefined();

@@ -32,0 +60,0 @@ expect(meta?.lifetime).toBe('scoped');

5

esm/injected.d.ts
import type { Constructable } from './models/constructable.js';
export declare const Injected: <T extends Constructable<unknown>>(ctor: T) => PropertyDecorator;
import type { Injector } from './injector.js';
export declare const InjectableDependencyList: unique symbol;
export declare const getDependencyList: <T extends Constructable<unknown>>(ctor: T) => Set<Constructable<any>>;
export declare const Injected: <T>(injectable: Constructable<unknown> | ((injector: Injector) => T)) => PropertyDecorator;
//# sourceMappingURL=injected.d.ts.map

@@ -1,13 +0,43 @@

import { Injector } from './injector.js';
export const Injected = (ctor) => (target, propertyKey) => {
const targetCtor = target.constructor;
if (!Injector.injectableFields.has(targetCtor)) {
Injector.injectableFields.set(targetCtor, {});
import { getInjectorReference } from './with-injector-reference.js';
import { hasInjectableOptions } from './injectable.js';
export const InjectableDependencyList = Symbol('InjectableDependencyList');
export const getDependencyList = (ctor) => {
const existing = ctor[InjectableDependencyList];
if (existing && existing instanceof Set) {
return existing;
}
const meta = Injector.injectableFields.get(targetCtor);
Injector.injectableFields.set(targetCtor, {
...meta,
[propertyKey]: ctor,
});
const newSet = new Set();
Object.assign(ctor, { [InjectableDependencyList]: newSet });
return newSet;
};
const addDependency = (ctor, dependency) => {
const list = getDependencyList(ctor);
list.add(dependency);
};
export const Injected = (injectable) => (target, propertyKey) => {
const hasMeta = hasInjectableOptions(injectable);
// The provided injectable is a constructor
if (hasMeta) {
addDependency(target.constructor, injectable);
Object.defineProperty(target.constructor.prototype, propertyKey, {
set() {
throw new Error(`Injected property '${target.constructor.name}.${propertyKey.toString()}' is read-only`);
},
get() {
return getInjectorReference(this).getInstance(injectable);
},
});
}
else {
// The provided injectable is a getter function
Object.defineProperty(target.constructor.prototype, propertyKey, {
set() {
throw new Error(`Injected property '${target.constructor.name}.${propertyKey.toString()}' is read-only`);
},
get() {
return injectable.call(this, getInjectorReference(this));
},
});
}
};
//# sourceMappingURL=injected.js.map

@@ -10,14 +10,15 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

};
import { Injected } from './injected.js';
import { Injectable } from './injectable.js';
import { Injected, getDependencyList } from './injected.js';
import { describe, expect, it } from 'vitest';
import { Injector } from './injector.js';
import { describe, expect, it } from 'vitest';
describe('@Injected()', () => {
it('Should register into the injectable fields', () => {
class Property {
let Property = class Property {
foo = 3;
}
};
Property = __decorate([
Injectable()
], Property);
class TestClass {
property;
property2;
property3;
}

@@ -32,9 +33,89 @@ __decorate([

], TestClass.prototype, "property3", void 0);
expect(Injector.injectableFields.has(TestClass)).toBe(true);
expect(Injector.injectableFields.get(TestClass)).toEqual({
property: Property,
property3: Property,
});
const dependencyList = getDependencyList(TestClass);
expect(dependencyList.has(Property)).toBeTruthy();
});
it('Should inject a property from the decorator', () => {
let Property = class Property {
foo = 3;
};
Property = __decorate([
Injectable()
], Property);
let TestClass = class TestClass {
};
__decorate([
Injected(Property),
__metadata("design:type", Property)
], TestClass.prototype, "property", void 0);
TestClass = __decorate([
Injectable()
], TestClass);
const instance = new Injector().getInstance(TestClass);
expect(instance.property).toBeInstanceOf(Property);
expect(instance.property.foo).toBe(3);
});
it('Should throw an error when trying to modify the injected property', () => {
let Property = class Property {
foo = 3;
};
Property = __decorate([
Injectable()
], Property);
let TestClass = class TestClass {
};
__decorate([
Injected(Property),
__metadata("design:type", Property)
], TestClass.prototype, "property", void 0);
TestClass = __decorate([
Injectable()
], TestClass);
const instance = new Injector().getInstance(TestClass);
expect(() => (instance.property = new Property())).toThrowErrorMatchingInlineSnapshot(`[Error: Injected property 'TestClass.property' is read-only]`);
});
it('Should inject a property with a callback syntax', () => {
let Property = class Property {
foo = 3;
};
Property = __decorate([
Injectable()
], Property);
let TestClass = class TestClass {
initial = 2;
};
__decorate([
Injected(function (injector) {
return injector.getInstance(Property).foo + this.initial;
}),
__metadata("design:type", Number)
], TestClass.prototype, "property", void 0);
TestClass = __decorate([
Injectable()
], TestClass);
const instance = new Injector().getInstance(TestClass);
expect(instance.property).toBe(5);
});
it('Should throw an error when trying to modify the injected property with a callback syntax', () => {
let Property = class Property {
foo = 3;
};
Property = __decorate([
Injectable()
], Property);
let TestClass = class TestClass {
initial = 2;
};
__decorate([
Injected(function (injector) {
return injector.getInstance(Property).foo + this.initial;
}),
__metadata("design:type", Number)
], TestClass.prototype, "property", void 0);
TestClass = __decorate([
Injectable()
], TestClass);
const instance = new Injector().getInstance(TestClass);
expect(() => (instance.property = 3)).toThrowErrorMatchingInlineSnapshot(`[Error: Injected property 'TestClass.property' is read-only]`);
});
});
//# sourceMappingURL=injected.spec.js.map
import type { Disposable } from '@furystack/utils';
import type { InjectableOptions } from './injectable.js';
import type { Constructable } from './models/constructable.js';
export declare class InjectorAlreadyDisposedError extends Error {
constructor();
}
export declare class Injector implements Disposable {
private isDisposed;
/**

@@ -16,18 +19,11 @@ * Disposes the Injector object and all its disposable injectables

};
/**
* Static class metadata map, filled by the @Injectable() decorator
*/
static options: Map<Constructable<any>, InjectableOptions>;
static injectableFields: Map<Constructable<any>, {
[K: string]: Constructable<any>;
}>;
readonly cachedSingletons: Map<Constructable<any>, any>;
remove: <T>(ctor: Constructable<T>) => boolean;
remove: <T>(ctor: Constructable<T>) => void;
getInstance<T>(ctor: Constructable<T>): T;
/**
*
* @param ctor The constructor object (e.g. MyClass)
* @param dependencies Resolved dependencies (usually provided by the framework)
* @returns The instance of the requested service
*/
getInstance<T>(ctor: Constructable<T>, dependencies?: Array<Constructable<T>>): T;
private getInstanceInternal;
/**

@@ -34,0 +30,0 @@ * Sets explicitliy an instance for a key in the store

@@ -0,5 +1,21 @@

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Injector_1;
import { Injectable, getInjectableOptions } from './injectable.js';
import { withInjectorReference } from './with-injector-reference.js';
import { getDependencyList } from './injected.js';
const hasInitMethod = (obj) => {
return typeof obj.init === 'function';
};
export class Injector {
export class InjectorAlreadyDisposedError extends Error {
constructor() {
super('Injector already disposed');
}
}
let Injector = Injector_1 = class Injector {
isDisposed = false;
/**

@@ -9,2 +25,6 @@ * Disposes the Injector object and all its disposable injectables

async dispose() {
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError();
}
this.isDisposed = true;
const singletons = Array.from(this.cachedSingletons.entries()).map((e) => e[1]);

@@ -29,16 +49,22 @@ const disposeRequests = singletons

options = {};
/**
* Static class metadata map, filled by the @Injectable() decorator
*/
static options = new Map();
static injectableFields = new Map();
// public static injectableFields: Map<Constructable<any>, { [K: string]: Constructable<any> }> = new Map()
cachedSingletons = new Map();
remove = (ctor) => this.cachedSingletons.delete(ctor);
remove = (ctor) => {
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError();
}
this.cachedSingletons.delete(ctor);
};
getInstance(ctor) {
return withInjectorReference(this.getInstanceInternal(ctor), this);
}
/**
*
* @param ctor The constructor object (e.g. MyClass)
* @param dependencies Resolved dependencies (usually provided by the framework)
* @returns The instance of the requested service
*/
getInstance(ctor, dependencies = []) {
getInstanceInternal(ctor) {
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError();
}
if (ctor === this.constructor) {

@@ -50,16 +76,11 @@ return this;

}
const meta = Injector.options.get(ctor);
if (!meta) {
throw Error(`No metadata found for '${ctor.name}'. Dependencies: ${dependencies
.map((d) => d.name)
.join(',')}. Be sure that it's decorated with '@Injectable()' or added explicitly with SetInstance()`);
}
const dependencies = [...getDependencyList(ctor)];
if (dependencies.includes(ctor)) {
throw Error(`Circular dependencies found.`);
}
const { lifetime = 'singleton' } = meta;
const injectedFields = Object.entries(Injector.injectableFields.get(ctor) || {});
const meta = getInjectableOptions(ctor);
const { lifetime } = meta;
if (lifetime === 'singleton') {
const invalidDeps = [...injectedFields]
.map(([, dep]) => ({ meta: Injector.options.get(dep), dep }))
const invalidDeps = dependencies
.map((dep) => ({ meta: getInjectableOptions(dep), dep }))
.filter((m) => m.meta && (m.meta.lifetime === 'scoped' || m.meta.lifetime === 'transient'))

@@ -72,4 +93,4 @@ .map((i) => i.meta && `${i.dep.name}:${i.meta.lifetime}`);

else if (lifetime === 'scoped') {
const invalidDeps = [...injectedFields.values()]
.map(([, dep]) => ({ meta: Injector.options.get(dep), dep }))
const invalidDeps = dependencies
.map((dep) => ({ meta: getInjectableOptions(dep), dep }))
.filter((m) => m.meta && m.meta.lifetime === 'transient')

@@ -81,11 +102,7 @@ .map((i) => i.meta && `${i.dep.name}:${i.meta.lifetime}`);

}
const fromParent = lifetime === 'singleton' && this.options.parent && this.options.parent.getInstance(ctor);
const fromParent = lifetime === 'singleton' && this.options.parent && this.options.parent.getInstanceInternal(ctor);
if (fromParent) {
return fromParent;
}
const deps = injectedFields.map(([key, dep]) => [key, this.getInstance(dep, [...dependencies, ctor])]);
const newInstance = new ctor();
deps.forEach(([key, value]) => {
newInstance[key] = value;
});
if (lifetime !== 'transient') {

@@ -95,3 +112,3 @@ this.setExplicitInstance(newInstance);

if (hasInitMethod(newInstance)) {
newInstance.init(this);
withInjectorReference(newInstance, this).init(this);
}

@@ -106,2 +123,5 @@ return newInstance;

setExplicitInstance(instance, key) {
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError();
}
const ctor = key || instance.constructor;

@@ -119,3 +139,6 @@ if (instance.constructor === this.constructor) {

createChild(options) {
const i = new Injector();
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError();
}
const i = new Injector_1();
i.options = i.options || options;

@@ -125,3 +148,7 @@ i.options.parent = this;

}
}
};
Injector = Injector_1 = __decorate([
Injectable({ lifetime: 'system' })
], Injector);
export { Injector };
//# sourceMappingURL=injector.js.map

@@ -24,16 +24,2 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

});
it('Should throw an error on circular dependencies', () => {
const i = new Injector();
let InstanceClass = class InstanceClass {
ohgodno;
};
__decorate([
Injected(InstanceClass),
__metadata("design:type", InstanceClass)
], InstanceClass.prototype, "ohgodno", void 0);
InstanceClass = __decorate([
Injectable()
], InstanceClass);
expect(() => i.getInstance(InstanceClass)).toThrowError('Circular dependencies found.');
});
it('Should set and return instance from cache', () => {

@@ -92,4 +78,2 @@ const i = new Injector();

let InstanceClass = class InstanceClass {
injected1;
injected2;
};

@@ -120,3 +104,2 @@ __decorate([

let Injected2 = class Injected2 {
injected1;
};

@@ -131,3 +114,2 @@ __decorate([

let InstanceClass = class InstanceClass {
injected2;
};

@@ -199,3 +181,3 @@ __decorate([

using(new Injector(), (i) => {
expect(() => i.getInstance(UndecoratedTestClass, [Injector])).toThrowError(`No metadata found for 'UndecoratedTestClass'. Dependencies: Injector. Be sure that it's decorated with '@Injectable()' or added explicitly with SetInstance()`);
expect(() => i.getInstance(UndecoratedTestClass)).toThrowError(`The class 'UndecoratedTestClass' is not an injectable`);
});

@@ -210,3 +192,2 @@ });

let St1 = class St1 {
lt;
};

@@ -231,3 +212,2 @@ __decorate([

let St2 = class St2 {
sc;
};

@@ -252,3 +232,2 @@ __decorate([

let Sc2 = class Sc2 {
sc;
};

@@ -281,3 +260,30 @@ __decorate([

});
describe('Disposed injector', () => {
it('Should throw an error on getInstance', async () => {
const i = new Injector();
await i.dispose();
expect(() => i.getInstance(Injector)).toThrowError('Injector already disposed');
});
it('Should throw an error on setExplicitInstance', async () => {
const i = new Injector();
await i.dispose();
expect(() => i.setExplicitInstance({})).toThrowError('Injector already disposed');
});
it('Should throw an error on remove', async () => {
const i = new Injector();
await i.dispose();
expect(() => i.remove(Object)).toThrowError('Injector already disposed');
});
it('Should throw an error on createChild', async () => {
const i = new Injector();
await i.dispose();
expect(() => i.createChild()).toThrowError('Injector already disposed');
});
it('Should throw an error on dispose', async () => {
const i = new Injector();
await i.dispose();
await expect(async () => await i.dispose()).rejects.toThrowError('Injector already disposed');
});
});
});
//# sourceMappingURL=injector.spec.js.map
{
"name": "@furystack/inject",
"version": "10.0.0",
"version": "11.0.0",
"description": "Core FuryStack package",

@@ -40,6 +40,6 @@ "type": "module",

"dependencies": {
"@furystack/utils": "^6.0.0"
"@furystack/utils": "^6.0.1"
},
"devDependencies": {
"typescript": "^5.4.2",
"typescript": "^5.4.3",
"vitest": "^1.4.0"

@@ -46,0 +46,0 @@ },

@@ -1,10 +0,41 @@

import { Injectable } from './injectable.js'
import { Injector } from './injector.js'
import { Injectable, InjectableOptionsSymbol, getInjectableOptions, hasInjectableOptions } from './injectable.js'
import { describe, expect, it } from 'vitest'
describe('hasInjectableOptions', () => {
it('Should return true if the object has the InjectableOptionsSymbol', () => {
class Alma {}
Object.assign(Alma, { [InjectableOptionsSymbol]: { lifetime: 'singleton' } })
expect(hasInjectableOptions(Alma)).toBe(true)
})
it('Should return false if the object does not have the InjectableOptionsSymbol', () => {
class Alma {}
expect(hasInjectableOptions(Alma)).toBe(false)
})
it('Should return false if the object is not an object', () => {
expect(hasInjectableOptions('' as any)).toBe(false)
})
})
describe('getInjectableOptions', () => {
it('Should throw an error if the object does not have the InjectableOptionsSymbol', () => {
class Alma {}
expect(() => getInjectableOptions(Alma)).toThrowError("The class 'Alma' is not an injectable")
})
it('Should return the options if the object has the InjectableOptionsSymbol', () => {
class Alma {}
Object.assign(Alma, { [InjectableOptionsSymbol]: { lifetime: 'singleton' } })
expect(getInjectableOptions(Alma)).toEqual({ lifetime: 'singleton' })
})
})
describe('@Injectable()', () => {
it('Should fill meta store with default options', () => {
it('Should attach the default options by default', () => {
@Injectable()
class TestClass1 {}
const meta = Injector.options.get(TestClass1)
const meta = getInjectableOptions(TestClass1)
expect(meta).toBeDefined()

@@ -14,6 +45,6 @@ expect(meta?.lifetime).toBe('transient')

it('Should fill meta store', () => {
it('Should attach the explicitly set options', () => {
@Injectable({ lifetime: 'scoped' })
class TestClass2 {}
const meta = Injector.options.get(TestClass2)
const meta = getInjectableOptions(TestClass2)
expect(meta).toBeDefined()

@@ -20,0 +51,0 @@ expect(meta?.lifetime).toBe('scoped')

@@ -1,2 +0,1 @@

import { Injector } from './injector.js'
import type { Constructable } from './models/constructable.js'

@@ -18,3 +17,31 @@

export const InjectableOptionsSymbol = Symbol('InjectableOptions')
type WithInjectableOptions<T> = T & { [InjectableOptionsSymbol]: InjectableOptions }
/**
* Checks if the constructor is decorated with Injectable() with verifying if it has Injectable options
* @param ctor The constructor to check
* @returns if the constructor has the InjectableOptionsSymbol
*/
export const hasInjectableOptions = <T extends Constructable<any>>(ctor: T): ctor is WithInjectableOptions<T> => {
return (
typeof (ctor as any)[InjectableOptionsSymbol] === 'object' &&
typeof ((ctor as any)[InjectableOptionsSymbol] as InjectableOptions).lifetime === 'string'
)
}
/**
* @throws Error if the class is not an injectable
* @param ctor The constructor to get the options from
* @returns The InjectableOptions object
*/
export const getInjectableOptions = <T extends Constructable<any>>(ctor: T): InjectableOptions => {
if (!hasInjectableOptions(ctor)) {
throw Error(`The class '${ctor.name}' is not an injectable`)
}
return ctor[InjectableOptionsSymbol]
}
/**
* Decorator method for tagging a class as injectable

@@ -26,7 +53,9 @@ * @param options The options object

return <T extends Constructable<any>>(ctor: T) => {
Injector.options.set(ctor, {
...defaultInjectableOptions,
...options,
Object.assign(ctor, {
[InjectableOptionsSymbol]: {
...defaultInjectableOptions,
...options,
},
})
}
}

@@ -1,6 +0,8 @@

import { Injected } from './injected.js'
import { Injectable } from './injectable.js'
import { Injected, getDependencyList } from './injected.js'
import { describe, expect, it } from 'vitest'
import { Injector } from './injector.js'
import { describe, expect, it } from 'vitest'
describe('@Injected()', () => {
it('Should register into the injectable fields', () => {
@Injectable()
class Property {

@@ -11,16 +13,86 @@ foo = 3

@Injected(Property)
public property!: Property
declare property: Property
property2?: Property
declare property2?: Property
@Injected(Property)
property3!: Property
declare property3: Property
}
expect(Injector.injectableFields.has(TestClass)).toBe(true)
expect(Injector.injectableFields.get(TestClass)).toEqual({
property: Property,
property3: Property,
})
const dependencyList = getDependencyList(TestClass)
expect(dependencyList.has(Property)).toBeTruthy()
})
it('Should inject a property from the decorator', () => {
@Injectable()
class Property {
foo = 3
}
@Injectable()
class TestClass {
@Injected(Property)
declare property: Property
}
const instance = new Injector().getInstance(TestClass)
expect(instance.property).toBeInstanceOf(Property)
expect(instance.property.foo).toBe(3)
})
it('Should throw an error when trying to modify the injected property', () => {
@Injectable()
class Property {
foo = 3
}
@Injectable()
class TestClass {
@Injected(Property)
declare property: Property
}
const instance = new Injector().getInstance(TestClass)
expect(() => (instance.property = new Property())).toThrowErrorMatchingInlineSnapshot(
`[Error: Injected property 'TestClass.property' is read-only]`,
)
})
it('Should inject a property with a callback syntax', () => {
@Injectable()
class Property {
foo = 3
}
@Injectable()
class TestClass {
private initial = 2
@Injected(function (this: TestClass, injector) {
return injector.getInstance(Property).foo + this.initial
})
declare property: number
}
const instance = new Injector().getInstance(TestClass)
expect(instance.property).toBe(5)
})
it('Should throw an error when trying to modify the injected property with a callback syntax', () => {
@Injectable()
class Property {
foo = 3
}
@Injectable()
class TestClass {
private initial = 2
@Injected(function (this: TestClass, injector) {
return injector.getInstance(Property).foo + this.initial
})
declare property: number
}
const instance = new Injector().getInstance(TestClass)
expect(() => (instance.property = 3)).toThrowErrorMatchingInlineSnapshot(
`[Error: Injected property 'TestClass.property' is read-only]`,
)
})
})

@@ -1,15 +0,48 @@

import { Injector } from './injector.js'
import { getInjectorReference } from './with-injector-reference.js'
import type { Constructable } from './models/constructable.js'
import type { Injector } from './injector.js'
import { hasInjectableOptions } from './injectable.js'
export const Injected: <T extends Constructable<unknown>>(ctor: T) => PropertyDecorator =
(ctor) => (target, propertyKey) => {
const targetCtor = target.constructor as Constructable<any>
if (!Injector.injectableFields.has(targetCtor)) {
Injector.injectableFields.set(targetCtor, {})
export const InjectableDependencyList = Symbol('InjectableDependencyList')
export const getDependencyList = <T extends Constructable<unknown>>(ctor: T): Set<Constructable<any>> => {
const existing = (ctor as any)[InjectableDependencyList] as Set<Constructable<any>>
if (existing && existing instanceof Set) {
return existing
}
const newSet = new Set<Constructable<any>>()
Object.assign(ctor, { [InjectableDependencyList]: newSet })
return newSet
}
const addDependency = <T extends Constructable<unknown>>(ctor: T, dependency: Constructable<any>) => {
const list = getDependencyList(ctor)
list.add(dependency)
}
export const Injected: <T>(injectable: Constructable<unknown> | ((injector: Injector) => T)) => PropertyDecorator =
(injectable) => (target, propertyKey) => {
const hasMeta = hasInjectableOptions(injectable as Constructable<unknown>)
// The provided injectable is a constructor
if (hasMeta) {
addDependency(target.constructor as Constructable<unknown>, injectable as Constructable<unknown>)
Object.defineProperty(target.constructor.prototype, propertyKey, {
set() {
throw new Error(`Injected property '${target.constructor.name}.${propertyKey.toString()}' is read-only`)
},
get() {
return getInjectorReference(this).getInstance(injectable as Constructable<unknown>)
},
})
} else {
// The provided injectable is a getter function
Object.defineProperty(target.constructor.prototype, propertyKey, {
set() {
throw new Error(`Injected property '${target.constructor.name}.${propertyKey.toString()}' is read-only`)
},
get() {
return (injectable as (injector: Injector) => unknown).call(this, getInjectorReference(this))
},
})
}
const meta = Injector.injectableFields.get(targetCtor)
Injector.injectableFields.set(targetCtor, {
...meta,
[propertyKey]: ctor,
})
}

@@ -19,12 +19,2 @@ import type { Disposable } from '@furystack/utils'

it('Should throw an error on circular dependencies', () => {
const i = new Injector()
@Injectable()
class InstanceClass {
@Injected(InstanceClass)
public ohgodno!: InstanceClass
}
expect(() => i.getInstance(InstanceClass)).toThrowError('Circular dependencies found.')
})
it('Should set and return instance from cache', () => {

@@ -76,6 +66,6 @@ const i = new Injector()

@Injected(Injected1)
public injected1!: Injected1
declare injected1: Injected1
@Injected(Injected2)
public injected2!: Injected2
declare injected2: Injected2
}

@@ -98,3 +88,3 @@

@Injected(Injected1)
public injected1!: Injected1
declare injected1: Injected1
}

@@ -105,3 +95,3 @@

@Injected(Injected2)
public injected2!: Injected2
declare injected2: Injected2
}

@@ -173,4 +163,4 @@ expect(i.getInstance(InstanceClass)).toBeInstanceOf(InstanceClass)

using(new Injector(), (i) => {
expect(() => i.getInstance(UndecoratedTestClass, [Injector])).toThrowError(
`No metadata found for 'UndecoratedTestClass'. Dependencies: Injector. Be sure that it's decorated with '@Injectable()' or added explicitly with SetInstance()`,
expect(() => i.getInstance(UndecoratedTestClass)).toThrowError(
`The class 'UndecoratedTestClass' is not an injectable`,
)

@@ -187,3 +177,3 @@ })

@Injected(Trs1)
lt!: Trs1
declare lt: Trs1
}

@@ -205,3 +195,3 @@

@Injected(Sc1)
public sc!: Sc1
declare sc: Sc1
}

@@ -223,3 +213,3 @@

@Injected(Tr2)
public sc!: Tr2
declare sc: Tr2
}

@@ -248,2 +238,34 @@

})
describe('Disposed injector', () => {
it('Should throw an error on getInstance', async () => {
const i = new Injector()
await i.dispose()
expect(() => i.getInstance(Injector)).toThrowError('Injector already disposed')
})
it('Should throw an error on setExplicitInstance', async () => {
const i = new Injector()
await i.dispose()
expect(() => i.setExplicitInstance({})).toThrowError('Injector already disposed')
})
it('Should throw an error on remove', async () => {
const i = new Injector()
await i.dispose()
expect(() => i.remove(Object)).toThrowError('Injector already disposed')
})
it('Should throw an error on createChild', async () => {
const i = new Injector()
await i.dispose()
expect(() => i.createChild()).toThrowError('Injector already disposed')
})
it('Should throw an error on dispose', async () => {
const i = new Injector()
await i.dispose()
await expect(async () => await i.dispose()).rejects.toThrowError('Injector already disposed')
})
})
})
import type { Disposable } from '@furystack/utils'
import type { InjectableOptions } from './injectable.js'
import { Injectable, getInjectableOptions } from './injectable.js'
import type { Constructable } from './models/constructable.js'
import { withInjectorReference } from './with-injector-reference.js'
import { getDependencyList } from './injected.js'

@@ -9,3 +11,12 @@ const hasInitMethod = (obj: Object): obj is { init: (injector: Injector) => void } => {

export class InjectorAlreadyDisposedError extends Error {
constructor() {
super('Injector already disposed')
}
}
@Injectable({ lifetime: 'system' as any })
export class Injector implements Disposable {
private isDisposed = false
/**

@@ -15,2 +26,8 @@ * Disposes the Injector object and all its disposable injectables

public async dispose() {
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError()
}
this.isDisposed = true
const singletons = Array.from(this.cachedSingletons.entries()).map((e) => e[1])

@@ -42,20 +59,28 @@ const disposeRequests = singletons

/**
* Static class metadata map, filled by the @Injectable() decorator
*/
public static options: Map<Constructable<any>, InjectableOptions> = new Map()
// public static injectableFields: Map<Constructable<any>, { [K: string]: Constructable<any> }> = new Map()
public static injectableFields: Map<Constructable<any>, { [K: string]: Constructable<any> }> = new Map()
public readonly cachedSingletons: Map<Constructable<any>, any> = new Map()
public remove = <T>(ctor: Constructable<T>) => this.cachedSingletons.delete(ctor)
public remove = <T>(ctor: Constructable<T>) => {
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError()
}
this.cachedSingletons.delete(ctor)
}
public getInstance<T>(ctor: Constructable<T>): T {
return withInjectorReference(this.getInstanceInternal(ctor), this)
}
/**
*
* @param ctor The constructor object (e.g. MyClass)
* @param dependencies Resolved dependencies (usually provided by the framework)
* @returns The instance of the requested service
*/
public getInstance<T>(ctor: Constructable<T>, dependencies: Array<Constructable<T>> = []): T {
private getInstanceInternal<T>(ctor: Constructable<T>): T {
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError()
}
if (ctor === this.constructor) {

@@ -69,10 +94,4 @@ return this as any as T

const meta = Injector.options.get(ctor)
if (!meta) {
throw Error(
`No metadata found for '${ctor.name}'. Dependencies: ${dependencies
.map((d) => d.name)
.join(',')}. Be sure that it's decorated with '@Injectable()' or added explicitly with SetInstance()`,
)
}
const dependencies = [...getDependencyList(ctor)]
if (dependencies.includes(ctor)) {

@@ -82,9 +101,9 @@ throw Error(`Circular dependencies found.`)

const { lifetime = 'singleton' } = meta
const meta = getInjectableOptions(ctor)
const injectedFields = Object.entries(Injector.injectableFields.get(ctor) || {})
const { lifetime } = meta
if (lifetime === 'singleton') {
const invalidDeps = [...injectedFields]
.map(([, dep]) => ({ meta: Injector.options.get(dep), dep }))
const invalidDeps = dependencies
.map((dep) => ({ meta: getInjectableOptions(dep), dep }))
.filter((m) => m.meta && (m.meta.lifetime === 'scoped' || m.meta.lifetime === 'transient'))

@@ -100,4 +119,4 @@ .map((i) => i.meta && `${i.dep.name}:${i.meta.lifetime}`)

} else if (lifetime === 'scoped') {
const invalidDeps = [...injectedFields.values()]
.map(([, dep]) => ({ meta: Injector.options.get(dep), dep }))
const invalidDeps = dependencies
.map((dep) => ({ meta: getInjectableOptions(dep), dep }))
.filter((m) => m.meta && m.meta.lifetime === 'transient')

@@ -112,11 +131,7 @@ .map((i) => i.meta && `${i.dep.name}:${i.meta.lifetime}`)

const fromParent = lifetime === 'singleton' && this.options.parent && this.options.parent.getInstance(ctor)
const fromParent = lifetime === 'singleton' && this.options.parent && this.options.parent.getInstanceInternal(ctor)
if (fromParent) {
return fromParent
}
const deps = injectedFields.map(([key, dep]) => [key, this.getInstance(dep, [...dependencies, ctor])])
const newInstance = new ctor()
deps.forEach(([key, value]) => {
newInstance[key as keyof T] = value
})
if (lifetime !== 'transient') {

@@ -127,3 +142,3 @@ this.setExplicitInstance(newInstance)

if (hasInitMethod(newInstance)) {
newInstance.init(this)
withInjectorReference(newInstance, this).init(this)
}

@@ -139,2 +154,6 @@ return newInstance

public setExplicitInstance<T extends object>(instance: T, key?: Constructable<any>) {
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError()
}
const ctor = key || (instance.constructor as Constructable<T>)

@@ -154,2 +173,6 @@

public createChild(options?: Partial<Injector['options']>) {
if (this.isDisposed) {
throw new InjectorAlreadyDisposedError()
}
const i = new Injector()

@@ -156,0 +179,0 @@ i.options = i.options || options

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc