Skip to content

Commit

Permalink
Attach injector to instance (#1162)
Browse files Browse the repository at this point in the history
* initial commit

* cleanup

* more cleanup

* even more cleanup

* EVEN MORE CLEANUP
  • Loading branch information
deebloo authored Jan 29, 2025
1 parent e3b0d47 commit e3b1fa4
Show file tree
Hide file tree
Showing 8 changed files with 54 additions and 55 deletions.
3 changes: 2 additions & 1 deletion packages/di/src/lib/context/injector.ts
Original file line number Diff line number Diff line change
@@ -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");
6 changes: 3 additions & 3 deletions packages/di/src/lib/inject.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { injectables } from "./injector.js";
import { readInjector } from "./metadata.js";
import type { InjectionToken } from "./provider.js";

export type Injected<T> = () => T;
Expand All @@ -7,9 +7,9 @@ export function inject<This extends object, T>(
token: InjectionToken<T>,
): Injected<T> {
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.`,
);
Expand Down
50 changes: 21 additions & 29 deletions packages/di/src/lib/injectable-el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,53 @@

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<T extends ConstructableToken<HTMLElement>>(
Base: T,
ctx: ClassDecoratorContext,
): T {
export function injectableEl<
T extends ConstructableToken<HTMLElement & { [INJECTOR]: Injector }>,
>(Base: T, ctx: ClassDecoratorContext): T {
const metadata: InjectableMetadata = ctx.metadata;

const def = {
[Base.name]: class extends Base {
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();
}
}

disconnectedCallback() {
const injector = injectables.get(this);

if (injector) {
injector.parent = undefined;
}
this[INJECTOR].parent = undefined;

if (super.disconnectedCallback) {
super.disconnectedCallback();
Expand Down
7 changes: 4 additions & 3 deletions packages/di/src/lib/injectable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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");
});

Expand All @@ -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", () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/di/src/lib/injectable.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -21,6 +21,8 @@ export function injectable(opts?: InjectableOpts) {
): T {
const def = {
[Base.name]: class extends Base {
[INJECTOR]: Injector;

constructor(...args: any[]) {
super(...args);

Expand All @@ -38,7 +40,7 @@ export function injectable(opts?: InjectableOpts) {
}
}

injectables.set(this, injector);
this[INJECTOR] = injector;
}
},
};
Expand Down
22 changes: 8 additions & 14 deletions packages/di/src/lib/injector.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -8,17 +8,14 @@ import {
StaticToken,
} from "./provider.js";

/**
* Keeps track of all Injectable services and their Injector
*/
export const injectables: WeakMap<object, Injector> = new WeakMap();

export interface InjectorOpts {
name?: string;
providers?: Iterable<Provider<any>>;
parent?: Injector;
}

export const INJECTOR: unique symbol = Symbol("JOIST_INJECTOR");

/**
* Injectors create and store instances of services.
* A service is any constructable class.
Expand Down Expand Up @@ -56,13 +53,10 @@ export class Injector {
const instance = this.#instances.get(token);

const metadata = readMetadata<T>(token);
const injector = readInjector(instance) ?? this;

if (metadata) {
callLifecycle(
instance,
injectables.get(instance) ?? this,
metadata.onInjected,
);
callLifecycle(instance, injector, metadata.onInjected);
}

return instance;
Expand Down Expand Up @@ -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) {
/**
Expand All @@ -134,8 +128,8 @@ export class Injector {
const metadata = readMetadata<T>(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);
}
}

Expand Down
5 changes: 3 additions & 2 deletions packages/di/src/lib/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 9 additions & 1 deletion packages/di/src/lib/metadata.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,3 +15,11 @@ export function readMetadata<T>(

return metadata;
}

export function readInjector<T extends object>(target: T): Injector | null {
if (INJECTOR in target) {
return target[INJECTOR] as Injector;
}

return null;
}

0 comments on commit e3b1fa4

Please sign in to comment.