Skip to content

Commit

Permalink
Merge pull request #4 from sern-handler/feat/ioc
Browse files Browse the repository at this point in the history
feat: sern's internal dependency injection system
  • Loading branch information
jacoobes authored May 2, 2024
2 parents 4cad5af + b9cc0a6 commit 3923a00
Show file tree
Hide file tree
Showing 9 changed files with 815 additions and 1,315 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
.yarn/cache
#.pnp.*
node_modules/**/*
packages/ioc/node_modules/*
packages/poster/dts/discord.d.ts
18 changes: 18 additions & 0 deletions packages/ioc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@sern/ioc",
"version": "1.0.0",
"description": "Dependency Injection system",
"main": "dist/index.js",
"scripts": {
"test": "vitest"
},
"devDependencies": {
"vitest": "^1.0.0"
},
"keywords": [],
"author": "",
"license": "ISC",
"publishConfig": {
"access": "public"
}
}
70 changes: 70 additions & 0 deletions packages/ioc/src/container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

function hasCallableMethod(obj: object, name: PropertyKey) {
//@ts-ignore
return Object.hasOwn(obj, name) && typeof obj[name] == 'function';
}
/**
* A Depedency injection container capable of adding singletons, firing hooks, and managing IOC within an application
*/
export class Container {
private __singletons = new Map<PropertyKey, any>();
private hooks= new Map<string, Function[]>();
private finished_init = false;
constructor(options: { autowire: boolean; path?: string }) {
if(options.autowire) { /* noop */ }
}

addHook(name: string, callback: Function) {
if (!this.hooks.has(name)) {
this.hooks.set(name, []);
}
this.hooks.get(name)!.push(callback);
}
private registerHooks(hookname: string, insert: object) {
if(hasCallableMethod(insert, hookname)) {
console.log(hookname)
//@ts-ignore
this.addHook(hookname, async () => await insert[hookname]())
}
}
addSingleton(key: string, insert: object) {
if(typeof insert !== 'object') {
throw Error("Inserted object must be an object");
}
if(!this.__singletons.has(key)){
this.registerHooks('init', insert)
this.registerHooks('dispose', insert)
this.__singletons.set(key, insert);
return true;
}
return false;
}

addWiredSingleton(key: string, fn: (c: Container) => object) {
const insert = fn(this);
return this.addSingleton(key, insert);
}

async disposeAll() {
await this.executeHooks('dispose');
this.hooks.delete('dispose');
}

isReady() { return this.finished_init; }
hasKey(key: string) { return this.__singletons.has(key); }
get<T>(key: PropertyKey) : T|undefined { return this.__singletons.get(key); }

async ready() {
await this.executeHooks('init');
this.hooks.delete('init');
this.finished_init = true;
}

async executeHooks(name: string) {
const hookFunctions = this.hooks.get(name) || [];
for (const hookFunction of hookFunctions) {
await hookFunction();
}
}
}

82 changes: 82 additions & 0 deletions packages/ioc/src/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<<<<<<< HEAD
=======
import assert from 'assert';
>>>>>>> 82054aa (sss)
import { Container } from './container';

//SIDE EFFECT: GLOBAL DI
let containerSubject: Container;

<<<<<<< HEAD
=======
/**
* Don't use this unless you know what you're doing. Destroys old containerSubject if it exists and disposes everything
* then it will swap
*/
export async function __swap_container(c: Container) {
if(containerSubject) {
await containerSubject.disposeAll()
}
containerSubject = c;
}
>>>>>>> 82054aa (sss)

/**
* Don't use this unless you know what you're doing. Destroys old containerSubject if it exists and disposes everything
* then it will swap
*/
export function __add_container(key: string, v: object) {
containerSubject.addSingleton(key, v);
}

/**
* Initiates the global api.
* Once this is finished, the Service api and the other global api is available
*/
export function __init_container(options: {
autowire: boolean;
path?: string | undefined;
}) {
containerSubject = new Container(options);
}

/**
* Returns the underlying data structure holding all dependencies.
* Exposes methods from iti
* Use the Service API. The container should be readonly
*/
export function useContainerRaw() {
if (!(containerSubject && containerSubject.isReady())) {
throw new Error("Container wasn't ready or init'd. Please ensure container is ready()");
}

return containerSubject;
}

/**
* The Service api, retrieve from the globally init'ed container
* Note: this method only works AFTER your container has been initiated
* @since 3.0.0
* @example
* ```ts
* const client = Service('@sern/client');
* ```
* @param key a key that corresponds to a dependency registered.
*
*/
export function Service<const T>(key: PropertyKey) {
const dep = useContainerRaw().get<T>(key)!;
if(!dep) {
throw Error("Requested key " + String(key) + " returned undefined");
}
return dep;
}
/**
* @since 3.0.0
* The plural version of {@link Service}
* @returns array of dependencies, in the same order of keys provided
*/
export function Services<const T extends string[], V>(...keys: [...T]) {
const container = useContainerRaw();
return keys.map(k => container.get(k)!) as V;
}
2 changes: 2 additions & 0 deletions packages/ioc/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Service, Services, __init_container, __add_container } from './global';
export { Container } from './container'
120 changes: 120 additions & 0 deletions packages/ioc/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@

import { Container } from '../src/container';
import { describe, it, expect, beforeEach, vi } from 'vitest';

describe('CoreContainer Tests', () => {
let coreContainer: Container;

beforeEach(() => {
coreContainer = new Container({ autowire: false });
});

it('Adding and getting singletons', () => {
coreContainer.addSingleton('singletonKey', { value: 'singletonValue' });
const singleton = coreContainer.get('singletonKey');
expect(singleton).toEqual({ value: 'singletonValue' });
});


it('Checking if container is ready', () => {
expect(coreContainer.isReady()).toBe(false);
coreContainer.ready().then(() => {
expect(coreContainer.isReady()).toBe(true);
});
});

it('Adding and getting singletons - async', async () => {
await coreContainer.ready();
coreContainer.addSingleton('asyncSingletonKey', { value: 'asyncSingletonValue' });
const singleton = coreContainer.get('asyncSingletonKey');
expect(singleton).toEqual({ value: 'asyncSingletonValue' });
})
it('Registering and executing hooks - init should be called once after ready', async () => {
let initCount = 0;

const singletonWithInit = {
value: 'singletonValueWithInit',
init: async () => {
initCount++;
}
};

coreContainer.addSingleton('singletonKeyWithInit', singletonWithInit);

// Call ready twice to ensure hooks are executed only once
await coreContainer.ready();
await coreContainer.ready();

expect(initCount).toBe(1);
});

it('Registering and executing hooks - ', async () => {
let initCount = 0;

const singletonWithInit = {
value: 'singletonValueWithInit',
init: async () => {
initCount++;
}
};

coreContainer.addSingleton('singletonKeyWithInit', singletonWithInit);

// Call ready twice to ensure hooks are executed only once
await coreContainer.ready();
await coreContainer.ready();

expect(initCount).toBe(1);
});


it('wired singleton', async () => {
let fn = vi.fn()
const wiredSingletonFn = (container: Container) => {
return { value: 'wiredSingletonValue', init: fn };
};
const added = coreContainer.addWiredSingleton('wiredSingletonKey', wiredSingletonFn);
expect(added).toBe(true);
const wiredSingleton = coreContainer.get('wiredSingletonKey');
expect(wiredSingleton).toEqual({ value: 'wiredSingletonValue', init: fn });
await coreContainer.ready()
await coreContainer.ready()
//@ts-ignore
expect(wiredSingleton.init).toHaveBeenCalledOnce();
})

it('dispose', async () => {
let dfn = vi.fn()
const wiredSingletonFn = { value: 'wiredSingletonValue', dispose: dfn };
coreContainer.addSingleton('sk', wiredSingletonFn);

await coreContainer.disposeAll();

expect(dfn).toHaveBeenCalledOnce()
})

it('Checking if container is ready', async () => {
expect(coreContainer.isReady()).toBe(false);
await coreContainer.ready();
expect(coreContainer.isReady()).toBe(true);
});
it('Registering and executing hooks - init should be called once after ready', async () => {
let initCount = 0;

const singletonWithInit = {
value: 'singletonValueWithInit',
init: async () => {
initCount++;
}
};

coreContainer.addSingleton('singletonKeyWithInit', singletonWithInit);

// Call ready twice to ensure hooks are executed only once
await coreContainer.ready();
await coreContainer.ready();

expect(initCount).toBe(1);
});

})
Loading

0 comments on commit 3923a00

Please sign in to comment.