Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attach injector to instance #1162

Merged
merged 5 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}