Skip to content

Commit

Permalink
Merge pull request #9 from sern-handler/ioc/swap
Browse files Browse the repository at this point in the history
impl swap for sern/ioc
  • Loading branch information
jacoobes authored Jun 13, 2024
2 parents f950854 + dfe587e commit 7811d2b
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 36 deletions.
1 change: 1 addition & 0 deletions packages/ioc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "dist/index.js",
"scripts": {
"test": "vitest --run",
"tdd": "vitest",
"build": "tsc"
},
"devDependencies": {
Expand Down
59 changes: 46 additions & 13 deletions packages/ioc/src/container.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,42 @@

/**
* A semi-generic container that provides error handling, emitter, and module store.
* For the handler to operate correctly, The only user provided dependency needs to be @sern/client
*/
function hasCallableMethod(obj: object, name: PropertyKey) {
// object will always be defined
//@ts-ignore
return 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[]>();
//hooks are Maps of string -> object, where object is a reference to an object in __singletons
private hooks= new Map<string, object[]>();
private finished_init = false;
constructor(options: { autowire: boolean; path?: string }) {
if(options.autowire) { /* noop */ }
}

addHook(name: string, callback: Function) {
addHook(name: string, callback: object) {
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]())
this.addHook(hookname, insert)
}
}

addSingleton(key: string, insert: object) {
if(typeof insert !== 'object') {
throw Error("Inserted object must be an object");
}
if(!this.__singletons.has(key)){
if(!this.__singletons.has(key)) {
this.registerHooks('init', insert)
this.registerHooks('dispose', insert)
this.__singletons.set(key, insert);
Expand All @@ -40,8 +45,8 @@ export class Container {
return false;
}

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

Expand All @@ -64,11 +69,39 @@ export class Container {
return Object.fromEntries(this.__singletons) as T
}

async executeHooks(name: string) {
private async executeHooks(name: string) {
const hookFunctions = this.hooks.get(name) || [];
for (const hookFunction of hookFunctions) {
await hookFunction();
for (const hookObject of hookFunctions) {
//@ts-ignore .registerHooks verifies the hookObject hasCallableMethod
await hookObject[name]();
}
}
}

swap(key: string, swp: object) {
if (typeof swp !== 'object') {
throw Error("Inserted object must be an object");
}

const existing = this.__singletons.get(key);
if (!existing) {
return false;
}
// check if there's dispose hook, and call it
if (hasCallableMethod(existing, 'dispose')) {
//this should technically be awaited to ensure synchronicity of swap
// but i dont want to ruin the function signature of swap.
existing.dispose();
// get the index of the existing singleton, now delete the dispose hook at that index
// .indexOf is safe because we only store singletons, and it should be a reference to
// the original object in this.__singletons
const hookIndex = this.hooks.get('dispose')!.indexOf(existing);
if (hookIndex > -1) {
this.hooks.get('dispose')!.splice(hookIndex, 1);
}
}

this.__singletons.set(key, swp);
this.registerHooks('init', swp);
return true;
}
}
115 changes: 93 additions & 22 deletions packages/ioc/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@

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

import { describe, it, expect, beforeEach, vi, Mock, afterEach } from 'vitest';

class SingletonCheese {
dispose() {
return this.value
}
constructor(public value: string){}
}
describe('CoreContainer Tests', () => {
let coreContainer: Container;

let singletonWInit: { init: Mock<any, any>; value: string }
let singletonWDispose: SingletonCheese
beforeEach(() => {
coreContainer = new Container({ autowire: false });
singletonWInit = {
value: 'singletonWithInit',
init: vi.fn()
};
singletonWDispose = new SingletonCheese('singletonWithDispose')
});

afterEach(() => {
vi.clearAllMocks()
})

it('Adding and getting singletons', () => {
coreContainer.addSingleton('singletonKey', { value: 'singletonValue' });
const singleton = coreContainer.get('singletonKey');
Expand All @@ -29,6 +45,7 @@ describe('CoreContainer Tests', () => {
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;

Expand Down Expand Up @@ -70,51 +87,105 @@ describe('CoreContainer Tests', () => {

it('wired singleton', async () => {
let fn = vi.fn()
const wiredSingletonFn = (container: Container) => {
const wiredSingletonFn = (container: unknown) => {
return { value: 'wiredSingletonValue', init: fn };
};
const added = coreContainer.addWiredSingleton('wiredSingletonKey', wiredSingletonFn);
expect(added).toBe(true);
const wiredSingleton = coreContainer.get('wiredSingletonKey');

const wiredSingleton = coreContainer.get<Record<string, unknown>>('wiredSingletonKey')!;
expect(wiredSingleton).toEqual({ value: 'wiredSingletonValue', init: fn });
await coreContainer.ready()
await coreContainer.ready()
//@ts-ignore

await coreContainer.ready();
await coreContainer.ready();

expect(wiredSingleton.init).toHaveBeenCalledOnce();
})

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

const added = coreContainer.addSingleton('sk', wiredSingletonFn);
expect(added).toBe(true);

await coreContainer.disposeAll();
await coreContainer.disposeAll();

expect(dfn).toHaveBeenCalledOnce()
})

it('Checking if container is ready', async () => {
it('Checking if container is ready - async', 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++;
}
};
it('Registering and executing hooks - init should be called once after ready', async () => {

coreContainer.addSingleton('singletonKeyWithInit', singletonWithInit);
coreContainer.addSingleton('singletonKeyWithInit', singletonWInit);

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

expect(initCount).toBe(1);
expect(singletonWInit.init).toHaveBeenCalledOnce();
});

it('should return false because not swapping anything', () => {
const swap = coreContainer.swap('singletonKeyWithInit', singletonWInit);
expect(swap).toBe(false);
})
it('should return true because not swapping anything', () => {
coreContainer.addSingleton('singletonKeyWithInit', singletonWInit);
const singletonWithInit2 = {
value: 'singletonValueWithInit2',
init: vi.fn()
};
const swap = coreContainer.swap('singletonKeyWithInit', singletonWithInit2);
expect(swap).toBe(true);
})
it('should swap object with another', () => {
coreContainer.addSingleton('singleton', singletonWInit)
const singletonWithInit2 = {
value: 'singletonValueWithInit2',
init: vi.fn()
};
coreContainer.swap('singleton', singletonWithInit2)
expect(coreContainer.get<Record<string, unknown>>('singleton')).toBe(singletonWithInit2)
})

it('should swap object, calling dispose hook', () => {
coreContainer.addSingleton('singleton', singletonWDispose);
const singletonWithDispose2 = {
value: 'singletonValueWithDispose2',
dispose: vi.fn()
};

const singletonWithDispose3 = {
value: 'singletonValueWithDispose3',
dispose: vi.fn()
};

coreContainer.addSingleton('singletonWithDispose3', singletonWithDispose3);
vi.spyOn(singletonWDispose, 'dispose')
coreContainer.swap('singleton', singletonWithDispose2);

expect(singletonWDispose.dispose).toHaveBeenCalledOnce();
expect(coreContainer.get<Record<string, unknown>>('singleton')).toBe(singletonWithDispose2);
expect(singletonWithDispose2.dispose).not.toHaveBeenCalledOnce();
expect(singletonWithDispose3.dispose).not.toHaveBeenCalledOnce();
})
it('should swap object, maintaining reference to `this`', () => {
coreContainer.addSingleton('singleton', singletonWDispose);
const singletonWithDispose2 = {
value: 'singletonValueWithDispose2',
dispose: vi.fn()
};
const spiedDispose = vi.spyOn(singletonWDispose, 'dispose')
const swapped = coreContainer.swap('singleton', singletonWithDispose2);
expect(spiedDispose.mock.results[0].value).toEqual('singletonWithDispose');
expect(coreContainer.get<Record<string, unknown>>('singleton')).toBe(singletonWithDispose2);
expect(singletonWithDispose2.dispose).not.toHaveBeenCalledOnce();
})
})
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2278,7 +2278,7 @@ __metadata:

"typescript@patch:typescript@^5.0.0#~builtin<compat/typescript>":
version: 5.4.5
resolution: "typescript@patch:typescript@npm%3A5.4.5#~builtin<compat/typescript>::version=5.4.5&hash=14eedb"
resolution: "typescript@patch:typescript@npm%3A5.4.5#~builtin<compat/typescript>::version=5.4.5&hash=f3b441"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
Expand Down

0 comments on commit 7811d2b

Please sign in to comment.