Skip to content

Instantly share code, notes, and snippets.

@infinite-system
Last active August 25, 2023 15:23
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save infinite-system/12269d96f06b7abe9ad050ec85838bbe to your computer and use it in GitHub Desktop.
Save infinite-system/12269d96f06b7abe9ad050ec85838bbe to your computer and use it in GitHub Desktop.
Basic IOC container similar to Inversify
class Mapping {
constructor (public from: ObjectConstructor) {
this.to = from
}
to: ObjectConstructor
scope = 'singleton'
onActivation: (resolved, ...args) => {}
}
export class Kernel {
mappings = new Map()
instances = new Map()
current: Mapping
bind (from) {
const mapping = new Mapping(from)
this.current = mapping
this.mappings.set(from, mapping)
return this
}
to (to) {
this.current.to = to
return this
}
transient () {
this.current.scope = 'transient'
return this
}
singleton () {
this.current.scope = 'singleton'
return this
}
onActivation (callback) {
this.current.onActivation = callback
return this
}
}
export const kernel = new Kernel()
export function get<T extends abstract new (...args: any) => any> (
className: T,
...args: T extends { new (...args: infer P): any } ? P : never[]
): any {
const mapping = kernel.mappings.get(className)
if (!mapping) {
const constructorName = className?.constructor?.name || String(className)
throw new Error('Kernel mapping does not exist for ' + constructorName)
}
// Handle singleton
if (mapping.scope === 'singleton') {
// Instance exists? Return it.
if (kernel.instances.has(className))
return kernel.instances.get(className)
// Instance does not exist? Create it.
const newInstance = mapping.onActivation
? mapping.onActivation(mapping.to, ...args)
: new mapping.to(...args)
// Save instance.
kernel.instances.set(className, newInstance)
return newInstance
} else {
// Handle transient
return mapping.onActivation
? mapping.onActivation(mapping.to, ...args)
: new mapping.to(...args)
}
}
@infinite-system
Copy link
Author

infinite-system commented Aug 10, 2023

Usage:

import { kernel, get } from '../Kernel.ts'

import { RouterRepository } from '../src/Classes/Routing/RouterRepository';
import { Router } from '../src/Classes/Routing/Router';
import { MessagesRepository } from '../src/Classes/Core/Messages/MessagesRepository';
import { NavigationRepository } from '../src/Classes/Navigation/NavigationRepository';
import { UserModel } from '../src/Classes/Authentication/UserModel';
import { AuthRepository } from '../src/Classes/Authentication/AuthRepository';
import { XVueTestClass } from '../src/Classes/Authentication/XVueTestClass';
import { LoginRegisterPresenter } from '../src/Classes/Authentication/LoginRegisterPresenter';
import { Config } from '../src/Classes/Core/Config';
import { TestClass } from '../src/Classes/Test/TestClass';
import { TestClass2 } from '../src/Classes/Test/TestClass2';
import { HttpGateway } from '../src/Classes/Core/HttpGateway';
import { RouterGateway } from '../src/Classes/Routing/RouterGateway';
import { AppPresenter } from '../src/Classes/AppPresenter';

export function initBaseKernel () {
  kernel
    .bind(RouterRepository)
    .bind(Router)

    .bind(MessagesRepository)
    .bind(NavigationRepository)
    .bind(UserModel)
    .bind(AuthRepository)

    .bind(XVueTestClass)
    .bind(LoginRegisterPresenter)
    .bind(Config)
    .bind(TestClass).transient() // transient definition
    .bind(TestClass2).transient()  // transient definition

  return kernel
}

export function initKernel () {
  initBaseKernel()
    .bind(HttpGateway).to(HttpGateway) // use to() to bind to something else, by default it binds to itself
    .bind(RouterGateway).to(RouterGateway) // use to() to bind to something else, by default it binds to itself
    .bind(AppPresenter)
}

initKernel()

// Get transient instances:
const testClass = get(TestClass2)
// You can even pass arguments
const testClass2 = get(TestClass2, arg1, arg2, arg3) // etc.

// Get singletons:
// The first singleton will use the args
// But the later calls to get will reuse the created instance
const app = get(AppPresenter, arg1, arg2, arg3) // First instantiation

// Somewhere else in some component
const app = get(AppPresenter) // arguments will be dismissed as you are getting the same instance

@infinite-system
Copy link
Author

infinite-system commented Aug 10, 2023

You can use .onActivation to transform the final object that gets returned from the container like this

kernel.bind(RouterRepository).onActivation((resolvedClass, ...args) => {
  return iVueBuilder(false, resolvedClass, ...args)
})

I use it to return reactive instances of objects in Vue integration for Infinite UI Architecture.

@fatso83
Copy link

fatso83 commented Aug 25, 2023

How does it compare (in your own perspective) to other solutions mentioned in this space, like https://github.com/nicojs/typed-inject?

@infinite-system
Copy link
Author

infinite-system commented Aug 25, 2023

@fatso83

Hey Carl-Erik!

This was just an initial draft of the Kernel, there is a new version as part of my vue integration package, you can take a look here:
https://github.com/infinite-system/infinite-vue/blob/main/src/kernel.ts

It's a bit different from the version here, the singletons and transients in the example gist share the same namespace, in the newer version in the link, the singletons and transients have their own scope, so you can define:

bind(ClassName).to(SomeOtherClass).singleton()
bind(ClassName).to(SomeAnotherClass).transient()

// To use these you do:
const className = get(ClassName, ...args) // will return a singleton SomeOtherClass instance
const classNameTransient = make(ClassName, ...args) // will return a new SomeAnotherClass instance each time

So I changed the design a bit, that I have 2 functions to use the container(get, make) rather than 1 to make it explicit whether you are using a singleton or a transient.

The main difference is my api is very succinct, most frameworks api for this is very verbose and I don't like it, too much typing involved, as well as doesn't fully suit my needs.

These are just examples, so you can build your own, it's not that difficult if you follow the right concepts.
I am not sure how it fully compares to others, but I get full typescript support in my approach, especially the updated version in the link.

Also my method supports auto-binding.

const someClass = get(SomeClass) // if there is no binding for SomeClass, it will auto-bind to itself and create new singleton instance
const someClassTransient = make(SomeClassTransient) // if there is no binding for SomeClass, it will auto-bind to itself and create a new transient instance

This is useful, so that you do not have to bind anything in startup, unless you want to change the behavior of those classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment