diff --git a/packages/ioc/package.json b/packages/ioc/package.json index d3ee3bb..c0f40f4 100644 --- a/packages/ioc/package.json +++ b/packages/ioc/package.json @@ -5,6 +5,7 @@ "main": "dist/index.js", "scripts": { "test": "vitest --run", + "tdd": "vitest", "build": "tsc" }, "devDependencies": { diff --git a/packages/ioc/src/container.ts b/packages/ioc/src/container.ts index 8c86bd1..08f5a39 100644 --- a/packages/ioc/src/container.ts +++ b/packages/ioc/src/container.ts @@ -1,6 +1,9 @@ - +/** + * 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'; } /** @@ -8,30 +11,32 @@ function hasCallableMethod(obj: object, name: PropertyKey) { */ export class Container { private __singletons = new Map(); - private hooks= new Map(); + //hooks are Maps of string -> object, where object is a reference to an object in __singletons + private hooks= new Map(); 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); @@ -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) => object) { + const insert = fn(this.deps()); return this.addSingleton(key, insert); } @@ -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; + } +} diff --git a/packages/ioc/test/index.test.ts b/packages/ioc/test/index.test.ts index 207f242..4cc8f1b 100644 --- a/packages/ioc/test/index.test.ts +++ b/packages/ioc/test/index.test.ts @@ -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; 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'); @@ -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; @@ -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>('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>('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>('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>('singleton')).toBe(singletonWithDispose2); + expect(singletonWithDispose2.dispose).not.toHaveBeenCalledOnce(); + }) }) diff --git a/yarn.lock b/yarn.lock index 7d2288b..cd6a09a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2278,7 +2278,7 @@ __metadata: "typescript@patch:typescript@^5.0.0#~builtin": version: 5.4.5 - resolution: "typescript@patch:typescript@npm%3A5.4.5#~builtin::version=5.4.5&hash=14eedb" + resolution: "typescript@patch:typescript@npm%3A5.4.5#~builtin::version=5.4.5&hash=f3b441" bin: tsc: bin/tsc tsserver: bin/tsserver