-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from sern-handler/feat/ioc
feat: sern's internal dependency injection system
- Loading branch information
Showing
9 changed files
with
815 additions
and
1,315 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,4 +9,5 @@ | |
.yarn/cache | ||
#.pnp.* | ||
node_modules/**/* | ||
packages/ioc/node_modules/* | ||
packages/poster/dts/discord.d.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
|
||
}) |
Oops, something went wrong.