diff --git a/packages/di/src/lib/context/injector.ts b/packages/di/src/lib/context/injector.ts index c0cccf04..dcf56970 100644 --- a/packages/di/src/lib/context/injector.ts +++ b/packages/di/src/lib/context/injector.ts @@ -1,5 +1,6 @@ -import type { Injector } from "../injector.js"; import { type Context, createContext } from "./protocol.js"; +import type { Injector } from "../injector.js"; + export const INJECTOR_CTX: Context<"injector", Injector> = createContext("injector"); diff --git a/packages/di/src/lib/inject.ts b/packages/di/src/lib/inject.ts index d3e92da9..e52bf975 100644 --- a/packages/di/src/lib/inject.ts +++ b/packages/di/src/lib/inject.ts @@ -1,4 +1,4 @@ -import { injectables } from "./injector.js"; +import { readInjector } from "./metadata.js"; import type { InjectionToken } from "./provider.js"; export type Injected = () => T; @@ -7,9 +7,9 @@ export function inject( token: InjectionToken, ): Injected { return function (this: This) { - const injector = injectables.get(this); + const injector = readInjector(this); - if (injector === undefined) { + if (injector === null) { throw new Error( `${this.constructor.name} is either not injectable or a service is being called in the constructor. \n Either add the @injectable() to your class or use the @injected callback method.`, ); diff --git a/packages/di/src/lib/injectable-el.ts b/packages/di/src/lib/injectable-el.ts index 766ff94a..3ba6f6fb 100644 --- a/packages/di/src/lib/injectable-el.ts +++ b/packages/di/src/lib/injectable-el.ts @@ -2,15 +2,15 @@ import { INJECTOR_CTX } from "./context/injector.js"; import { ContextRequestEvent } from "./context/protocol.js"; -import { injectables } from "./injector.js"; +import { INJECTOR } from "./injector.js"; +import type { Injector } from "./injector.js"; import { callLifecycle } from "./lifecycle.js"; import type { InjectableMetadata } from "./metadata.js"; import type { ConstructableToken } from "./provider.js"; -export function injectableEl>( - Base: T, - ctx: ClassDecoratorContext, -): T { +export function injectableEl< + T extends ConstructableToken, +>(Base: T, ctx: ClassDecoratorContext): T { const metadata: InjectableMetadata = ctx.metadata; const def = { @@ -18,33 +18,29 @@ export function injectableEl>( constructor(..._: any[]) { super(); - const injector = injectables.get(this); + const injector = this[INJECTOR]; - if (injector) { - this.addEventListener("context-request", (e) => { - if (e.target !== this && e.context === INJECTOR_CTX) { - e.stopPropagation(); + this.addEventListener("context-request", (e) => { + if (e.target !== this && e.context === INJECTOR_CTX) { + e.stopPropagation(); - e.callback(injector); - } - }); + e.callback(injector); + } + }); - callLifecycle(this, injector, metadata?.onCreated); - } + callLifecycle(this, injector, metadata?.onCreated); } connectedCallback() { - const injector = injectables.get(this); + const injector = this[INJECTOR]; - if (injector) { - this.dispatchEvent( - new ContextRequestEvent(INJECTOR_CTX, (ctx) => { - injector.parent = ctx; - }), - ); + this.dispatchEvent( + new ContextRequestEvent(INJECTOR_CTX, (ctx) => { + injector.parent = ctx; + }), + ); - callLifecycle(this, injector, metadata?.onInjected); - } + callLifecycle(this, injector, metadata?.onInjected); if (super.connectedCallback) { super.connectedCallback(); @@ -52,11 +48,7 @@ export function injectableEl>( } disconnectedCallback() { - const injector = injectables.get(this); - - if (injector) { - injector.parent = undefined; - } + this[INJECTOR].parent = undefined; if (super.disconnectedCallback) { super.disconnectedCallback(); diff --git a/packages/di/src/lib/injectable.test.ts b/packages/di/src/lib/injectable.test.ts index 318de928..6633bf93 100644 --- a/packages/di/src/lib/injectable.test.ts +++ b/packages/di/src/lib/injectable.test.ts @@ -2,7 +2,8 @@ import { assert } from "chai"; import { inject } from "./inject.js"; import { injectable } from "./injectable.js"; -import { Injector, injectables } from "./injector.js"; +import { Injector } from "./injector.js"; +import { readInjector } from "./metadata.js"; import { StaticToken } from "./provider.js"; it("should locally override a provider", () => { @@ -30,7 +31,7 @@ it("should define an injector for a service instance", () => { const instance = new MyService("b"); - assert.ok(injectables.has(instance)); + assert.ok(readInjector(instance)); assert.ok(instance.arg === "b"); }); @@ -43,7 +44,7 @@ it("should inject the current service injectable instance", () => { const app = new Injector(); const service = app.inject(MyService); - assert.equal(service.injector(), injectables.get(service)); + assert.equal(service.injector(), readInjector(service)); }); it("should not override the name of the original class", () => { diff --git a/packages/di/src/lib/injectable.ts b/packages/di/src/lib/injectable.ts index 97c71cba..3608543f 100644 --- a/packages/di/src/lib/injectable.ts +++ b/packages/di/src/lib/injectable.ts @@ -1,7 +1,7 @@ (Symbol as any).metadata ??= Symbol("Symbol.metadata"); import { injectableEl } from "./injectable-el.js"; -import { Injector, injectables } from "./injector.js"; +import { INJECTOR, Injector } from "./injector.js"; import type { ConstructableToken, InjectionToken, @@ -21,6 +21,8 @@ export function injectable(opts?: InjectableOpts) { ): T { const def = { [Base.name]: class extends Base { + [INJECTOR]: Injector; + constructor(...args: any[]) { super(...args); @@ -38,7 +40,7 @@ export function injectable(opts?: InjectableOpts) { } } - injectables.set(this, injector); + this[INJECTOR] = injector; } }, }; diff --git a/packages/di/src/lib/injector.ts b/packages/di/src/lib/injector.ts index 1fca7e4f..17926eea 100644 --- a/packages/di/src/lib/injector.ts +++ b/packages/di/src/lib/injector.ts @@ -1,5 +1,5 @@ import { callLifecycle } from "./lifecycle.js"; -import { readMetadata } from "./metadata.js"; +import { readInjector, readMetadata } from "./metadata.js"; import { type InjectionToken, type Provider, @@ -8,17 +8,14 @@ import { StaticToken, } from "./provider.js"; -/** - * Keeps track of all Injectable services and their Injector - */ -export const injectables: WeakMap = new WeakMap(); - export interface InjectorOpts { name?: string; providers?: Iterable>; parent?: Injector; } +export const INJECTOR: unique symbol = Symbol("JOIST_INJECTOR"); + /** * Injectors create and store instances of services. * A service is any constructable class. @@ -56,13 +53,10 @@ export class Injector { const instance = this.#instances.get(token); const metadata = readMetadata(token); + const injector = readInjector(instance) ?? this; if (metadata) { - callLifecycle( - instance, - injectables.get(instance) ?? this, - metadata.onInjected, - ); + callLifecycle(instance, injector, metadata.onInjected); } return instance; @@ -114,7 +108,7 @@ export class Injector { * Only values that are objects are able to have associated injectors */ if (typeof instance === "object" && instance !== null) { - const injector = injectables.get(instance); + const injector = readInjector(instance) ?? this; if (injector && injector !== this) { /** @@ -134,8 +128,8 @@ export class Injector { const metadata = readMetadata(token); if (metadata) { - callLifecycle(instance, injector ?? this, metadata.onCreated); - callLifecycle(instance, injector ?? this, metadata.onInjected); + callLifecycle(instance, injector, metadata.onCreated); + callLifecycle(instance, injector, metadata.onInjected); } } diff --git a/packages/di/src/lib/lifecycle.test.ts b/packages/di/src/lib/lifecycle.test.ts index 660b81aa..08f54a9c 100644 --- a/packages/di/src/lib/lifecycle.test.ts +++ b/packages/di/src/lib/lifecycle.test.ts @@ -2,8 +2,9 @@ import { assert } from "chai"; import { inject } from "./inject.js"; import { injectable } from "./injectable.js"; -import { Injector, injectables } from "./injector.js"; +import { Injector } from "./injector.js"; import { created, injected } from "./lifecycle.js"; +import { readInjector } from "./metadata.js"; it("should call onInit and onInject when a service is first created", () => { const i = new Injector(); @@ -55,7 +56,7 @@ it("should pass the injector to all lifecycle callbacks", () => { } const service = i.inject(MyService); - const injector = injectables.get(service); + const injector = readInjector(service); assert.equal(service.res[0], injector); assert.equal(service.res[0].parent, i); diff --git a/packages/di/src/lib/metadata.ts b/packages/di/src/lib/metadata.ts index 71f54f6f..63ed54c7 100644 --- a/packages/di/src/lib/metadata.ts +++ b/packages/di/src/lib/metadata.ts @@ -1,4 +1,4 @@ -import type { Injector } from "./injector.js"; +import { INJECTOR, type Injector } from "./injector.js"; import type { InjectionToken } from "./provider.js"; export type LifecycleCallback = (i: Injector) => void; @@ -15,3 +15,11 @@ export function readMetadata( return metadata; } + +export function readInjector(target: T): Injector | null { + if (INJECTOR in target) { + return target[INJECTOR] as Injector; + } + + return null; +}