Last active
June 23, 2024 13:50
-
-
Save temoncher/1bc55745a51cb3099bcd26b23697a62e to your computer and use it in GitHub Desktop.
Factorius, typesafe dependency injection library frontend, with agnostic backend
This file contains hidden or 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
import { | |
factorius, | |
requires, | |
instance, | |
transient, | |
optional, | |
externalId, | |
type, | |
tokensOf, | |
} from './factorius'; | |
import { factoriusProxy } from './factorius-proxy'; | |
type Wat = { str: string }; | |
interface IKidsApi { | |
getKidsInfos(): { name: string }[]; | |
} | |
const kidsApiModule = factorius().define({ | |
kidsApi: externalId( | |
(): IKidsApi => ({ | |
getKidsInfos: () => [{ name: 'Kenny' }], | |
}), | |
'KIDS_API' | |
), | |
kidsApi_additionalServiceTest: () => 'kidsApi_additionalServiceTest', | |
}); | |
const getKidsModule = factorius() | |
.requires(kidsApiModule, { kidsApi: true }) | |
.define({ | |
getKids: (di) => () => di.kidsApi.getKidsInfos(), | |
}); | |
const kekStrModule = factorius().layer({ | |
kek: () => '', | |
}); | |
const kekNumModule = factorius().layer({ | |
kek: () => 42, | |
}); | |
const config = factorius() | |
.define({ | |
some: optional(type<string>(), 'SOME_TOKEN'), | |
wat: requires<Wat>(), | |
fortyTwo: optional<number>(), | |
someStr: requires(type<string>(), 'SOME_STR_TOKEN'), | |
}) | |
.import(kekStrModule) | |
.import(kekNumModule) // FIXME: forbid modules owerwriting with different types | |
.import(kidsApiModule) | |
.import(getKidsModule) | |
.provide({ | |
fortyTwo: () => 42, | |
}) | |
.provide({ | |
wat: () => ({ | |
str: 'fff', | |
num: 44, | |
}), | |
}) | |
.define({ | |
some41: externalId(() => 'some41', 'SOME_41_TOKEN'), | |
some42: transient((di) => { | |
const fortyTwo1 = di.fortyTwo; | |
const fortyTwo2 = di.fortyTwo; | |
console.log('should be the same', fortyTwo1, fortyTwo2); | |
return (di.some ?? '') + (di.fortyTwo ?? '') + di.wat.str; | |
}, 'SOME_42_TOKEN'), | |
}) | |
.provide({ | |
// TODO?: keep externalId in case we don't provide new one | |
// someStr: instance('someStr'), | |
}) | |
.override({ | |
fortyTwo: transient(() => Math.floor(Math.random() * 33)), | |
some41: () => 'reee', | |
}) | |
.check({ ignore: { someStr: true } }); | |
console.log('config', config); | |
console.log('tokensOf(config)', tokensOf(config)); | |
console.log('config.tokens.some42', config.tokens.some42); | |
const di = factoriusProxy(config); | |
console.log('di.some42 1', di.some42); | |
console.log('di.some42 2', di.some42); | |
console.log('di.some42 3', di.some42); | |
console.log(di); |
This file contains hidden or 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
import { mapValues } from 'remeda'; | |
import { FactoriusCore, FactoriusServices, InferDi, Scope } from './factorius'; | |
export const transientsTag = Symbol('transients'); | |
function transientsProxy<S extends FactoriusServices>( | |
core: FactoriusCore<S>, | |
defaultScope: Scope, | |
originalProxy: object | |
) { | |
return Object.defineProperties( | |
{ [transientsTag]: new Map<string, unknown>() }, | |
mapValues(core.factories, (provider, key) => { | |
const scope = | |
'scope' in provider ? provider.scope ?? defaultScope : defaultScope; | |
const get = | |
typeof provider !== 'function' | |
? () => undefined | |
: scope === Scope.TRANSIENT | |
? function ( | |
this: InferDi<S> & { [transientsTag]: Map<string, unknown> } | |
) { | |
const instances = this[transientsTag]; | |
if (instances.has(key)) return instances.get(key); | |
const newInstance = provider(this); | |
instances.set(key, newInstance); | |
return newInstance; | |
} | |
: () => (originalProxy as any)[key]; | |
return { | |
enumerable: true, | |
configurable: false, | |
get, | |
}; | |
}) as any | |
); | |
} | |
export const singletonsTag = Symbol('singletons'); | |
export function factoriusProxy<S extends FactoriusServices>( | |
core: FactoriusCore<S>, | |
defaultScope: Scope = Scope.SINGLETON | |
): InferDi<S> & { [singletonsTag]: Map<string, unknown> } { | |
return Object.defineProperties( | |
{ [singletonsTag]: new Map<string, unknown>() }, | |
mapValues(core.factories, (provider, key) => { | |
const scope = | |
'scope' in provider ? provider.scope ?? defaultScope : defaultScope; | |
const get = | |
typeof provider !== 'function' | |
? () => undefined | |
: scope === Scope.TRANSIENT | |
? function ( | |
this: InferDi<S> & { [singletonsTag]: Map<string, unknown> } | |
) { | |
// TODO: check provider.length and skip proxy creation? | |
// to cache consequent calls to the same transient | |
const proxy = transientsProxy(core, defaultScope, this); | |
return provider(proxy); | |
} | |
: function ( | |
this: InferDi<S> & { [singletonsTag]: Map<string, unknown> } | |
) { | |
const instances = this[singletonsTag]; | |
if (instances.has(key)) return instances.get(key); | |
const newInstance = provider(this); | |
instances.set(key, newInstance); | |
return newInstance; | |
}; | |
return { | |
enumerable: true, | |
configurable: false, | |
get, | |
}; | |
}) as any | |
) as any; | |
} |
This file contains hidden or 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
import { mapValues } from 'remeda'; | |
import type { O, U } from 'ts-toolbelt'; | |
export interface ExternalId<T> { | |
externalId: T; | |
} | |
function cloneFn<F extends Function>(fn: F): F { | |
const newFn = fn.bind(null); | |
// preserve custom keys on a function | |
for (const key in fn) { | |
console.log('key', key); | |
(newFn as any)[key] = (fn as any)[key]; | |
} | |
return newFn; | |
} | |
/** @typeonly */ | |
const __type = Symbol('type'); | |
export type InferDi<S extends object> = { | |
readonly [K in keyof S]: Provider.Service<S[K]>; | |
} extends infer Res extends object | |
? O.Merge<Res, {}> // to prettify results | |
: never; | |
type ValueOf<T> = T[keyof T]; | |
export const Scope = { | |
SINGLETON: 'SINGLETON', | |
TRANSIENT: 'TRANSIENT', | |
} as const; | |
export type Scope = ValueOf<typeof Scope>; | |
export namespace Scope { | |
export type SINGLETON = typeof Scope.SINGLETON; | |
// TODO?: TRANSIENT should work as proxy scope? resolves one time for single factory, | |
// consequent requests for the same prop return cached version | |
export type TRANSIENT = typeof Scope.TRANSIENT; | |
} | |
export interface Scoped<S extends Scope> { | |
scope: S; | |
} | |
export function singleton<F>(fn: F): F & Scoped<Scope.SINGLETON>; | |
export function singleton<F, const EID>( | |
fn: F, | |
externalId: EID | |
): F & Scoped<Scope.SINGLETON> & ExternalId<EID>; | |
export function singleton<F, const EID>( | |
fn: F, | |
externalId?: EID | |
): F & Scoped<Scope.SINGLETON> & ExternalId<EID> { | |
const newFn = cloneFn(fn as any); | |
newFn.scope = Scope.SINGLETON; | |
newFn.externalId = externalId; | |
return newFn; | |
} | |
export function transient<T>(fn: T): T & Scoped<Scope.TRANSIENT>; | |
export function transient<T, const EID>( | |
fn: T, | |
externalId: EID | |
): T & Scoped<Scope.TRANSIENT> & ExternalId<EID>; | |
export function transient<T, const EID>( | |
fn: T, | |
externalId?: EID | |
): T & Scoped<Scope.TRANSIENT> & ExternalId<EID> { | |
const newFn = cloneFn(fn as any); | |
newFn.scope = Scope.TRANSIENT; | |
newFn.externalId = externalId; | |
return newFn; | |
} | |
export function externalId<T, const EID>( | |
target: T, | |
id: EID | |
): T & ExternalId<EID> { | |
const newTarget: any = | |
typeof target === 'function' ? cloneFn(target) : { ...target }; | |
newTarget.externalId = id; | |
return newTarget; | |
} | |
export type Factory< | |
S, | |
DI extends object = object, | |
TScope extends Scope = Scope | |
> = ((di: DI) => S) & Partial<Scoped<TScope>>; | |
export type FactoriusServices = Record<string, Provider<any, any, any, any>>; | |
export interface FactoriusCore<S extends FactoriusServices = {}> { | |
readonly factories: S; | |
} | |
export type InferEid<T> = T extends { externalId: infer R } ? R : undefined; | |
export type Provider< | |
S, | |
DI extends object = object, | |
TScope extends Scope = Scope, | |
EID = undefined | |
> = Factory<S, DI, TScope> | Requirement<S, EID>; | |
export namespace Provider { | |
export type Service<I> = I extends Optional<infer S, any> | |
? S | undefined | |
: I extends Provider<infer S, any, any, any> | |
? S | |
: never; | |
} | |
type RequiredServices<S> = { | |
[K in keyof S as S[K] extends Required<any, any> ? K : never]: S[K]; | |
}; | |
export const stillRequiresErrorKey = 'stillRequires'; | |
export type TokensOf<S> = { | |
[K in keyof S]: Token<Provider.Service<S[K]>, K & string, InferEid<S[K]>>; | |
}; | |
export function tokensOf<S extends FactoriusServices>( | |
core: FactoriusCore<S> | |
): TokensOf<S> { | |
return mapValues(core.factories, (fac, id) => ({ | |
[__type]: undefined as any, | |
id, | |
externalId: (fac as any).externalId, | |
})) as any; | |
} | |
declare const typeErrorTag: unique symbol; | |
type TypeError<Msg extends string> = { [typeErrorTag]: Msg }; | |
type LayerRestriction<T, S extends object> = Omit<T, keyof S> & { | |
[K in keyof S & keyof T]: Provider<Provider.Service<S[K]>, InferDi<S>>; | |
}; | |
type DefineRestriction<T, S extends object> = Omit<T, keyof S> & { | |
[K in keyof S & keyof T]: TypeError<`Service '${K & | |
string}' already defined`>; | |
}; | |
type ProvideRestriction<S extends object> = Partial<{ | |
[K in keyof S]: S[K] extends Requirement<any, any> | |
? Factory<Provider.Service<S[K]>, InferDi<S>, any> | |
: TypeError<`Service '${K & | |
string}' already provided, use '.override' in case you want to override its implementation`>; | |
}>; | |
type OverrideRestriction<S extends object> = Partial<{ | |
[K in keyof S]: S[K] extends Factory<any, any, any> | |
? Factory<Provider.Service<S[K]>, InferDi<S>, any> | |
: TypeError<`Service '${K & | |
string}' is not provided yet, use '.provide' in case you want to provide its implementation`>; | |
}>; | |
export interface Factorius<S extends FactoriusServices = {}> | |
extends FactoriusCore<S> { | |
readonly tokens: TokensOf<S>; | |
// TODO?: multitokens? | |
layer<const NewS extends LayerRestriction<NewS, S>>( | |
newServices: NewS & Record<string, Provider<any, InferDi<S>, any, any>> | |
): O.Merge<NewS, S> extends infer R extends FactoriusServices | |
? Factorius<R> | |
: never; | |
/** works as layer, but only allows new keys */ | |
define<const NewS extends DefineRestriction<NewS, S>>( | |
newServices: NewS & Record<string, Provider<any, InferDi<S>, any, any>> | |
): O.Merge<NewS, S> extends infer R extends FactoriusServices | |
? Factorius<R> | |
: never; | |
/** works as layer, but only allows keys of required services to be entered */ | |
provide<const NewS extends ProvideRestriction<S>>( | |
newServices: NewS | |
): O.Merge<NewS, S> extends infer R extends FactoriusServices | |
? Factorius<R> | |
: never; | |
/** works as layer, but only allows keys of already provided services to be entered */ | |
override<const NewS extends OverrideRestriction<S>>( | |
newServices: NewS | |
): O.Merge<NewS, S> extends infer R extends FactoriusServices | |
? Factorius<R> | |
: never; | |
check< | |
const I extends Partial<Record<keyof RequiredServices<S>, true>> = {} | |
>(params?: { | |
ignore: I & { | |
[K in keyof I]: K extends keyof RequiredServices<S> | |
? true | |
: K extends keyof S | |
? TypeError<`Key '${K & string}' is already provided inside module`> | |
: TypeError<`Key '${K & string}' does not exist in the module`>; | |
}; | |
}): Exclude<keyof RequiredServices<S>, keyof I> extends infer Required | |
? [Required] extends [never] | |
? Factorius<S> | |
: { [stillRequiresErrorKey]: U.ListOf<Required> } | |
: never; | |
requires<ModuleS extends FactoriusServices>( | |
factorius: FactoriusCore<ModuleS> | |
): O.Merge< | |
{ | |
[K in keyof ModuleS]: Required< | |
Provider.Service<ModuleS[K]>, | |
InferEid<ModuleS[K]> | |
>; | |
}, | |
S | |
> extends infer R extends FactoriusServices | |
? Factorius<R> | |
: never; | |
requires< | |
ModuleS extends FactoriusServices, | |
const KeysObject extends Partial<Record<keyof ModuleS, true>> | |
>( | |
factorius: FactoriusCore<ModuleS>, | |
keys: KeysObject & { | |
[K in keyof KeysObject]: K extends keyof ModuleS | |
? true | |
: TypeError<`Key '${K & string}' does not exist in the module`>; | |
} | |
): O.Merge< | |
{ | |
[K in keyof KeysObject & keyof ModuleS]: Required< | |
Provider.Service<ModuleS[K]>, | |
InferEid<ModuleS[K]> | |
>; | |
}, | |
S | |
> extends infer R extends FactoriusServices | |
? Factorius<R> | |
: never; | |
import<NewS extends FactoriusServices>( | |
factorius: FactoriusCore<NewS> | |
): O.Merge< | |
{ | |
[K in keyof NewS as NewS[K] extends Requirement<any, any> | |
? K extends keyof S | |
? S[K] extends Requirement<any, any> | |
? K | |
: never | |
: K | |
: K]: NewS[K]; | |
}, | |
S | |
> extends infer R extends FactoriusServices | |
? Factorius<R> | |
: never; | |
} | |
export namespace Factorius { | |
export type Services<F> = F extends Factorius<infer S> ? S : never; | |
} | |
export interface Type<T> { | |
[__type]?: T; | |
} | |
// TODO: Requires and Factory should extend token? | |
export interface Token<T, ID extends string = string, EID = undefined> | |
extends Type<T>, | |
ExternalId<EID> { | |
id: ID; | |
} | |
export function isToken(obj: unknown): obj is Token<unknown, string, unknown> { | |
return ( | |
typeof obj === 'object' && | |
!!obj && | |
__type in obj && | |
'id' in obj && | |
typeof obj.id === 'string' | |
); | |
} | |
export function type<T>(): Type<T> { | |
return { [__type]: undefined as T }; | |
} | |
const requirementTag = Symbol('requirement'); | |
export type Requirement<T, EID = undefined> = | |
| Required<T, EID> | |
| Optional<T, EID>; | |
export type Required<T, EID = undefined> = Omit<Token<T, string, EID>, 'id'> & { | |
[requirementTag]: true; | |
optional?: false; | |
}; | |
export interface Optional<T, EID = undefined> extends Type<T>, ExternalId<EID> { | |
[requirementTag]: true; | |
optional: true; | |
} | |
export function isRequirement( | |
obj: unknown | |
): obj is Requirement<unknown, unknown> { | |
return ( | |
typeof obj === 'object' && | |
!!obj && | |
__type in obj && | |
'optional' in obj && | |
typeof obj.optional === 'boolean' | |
); | |
} | |
export function requires<T>(): Required<T>; | |
export function requires<T, const EID>( | |
type: Type<T>, | |
externalId: EID | |
): Required<T, EID>; | |
export function requires( | |
type?: Type<unknown> | Token<unknown>, | |
externalId?: unknown | |
): Required<unknown, unknown> { | |
return { | |
[requirementTag]: true, | |
[__type]: undefined, | |
optional: false, | |
externalId, | |
}; | |
} | |
export function optional<T>(): Optional<T>; | |
export function optional<T, const EID>( | |
type: Type<T>, | |
externalId: EID | |
): Optional<T, EID>; | |
export function optional( | |
type?: Type<unknown> | Token<unknown>, | |
externalId?: unknown | |
): Optional<unknown, unknown> { | |
return { | |
[requirementTag]: true, | |
[__type]: undefined, | |
optional: true, | |
externalId, | |
}; | |
} | |
export function instance<const T>(val: T) { | |
return () => val; | |
} | |
export function factorius<S extends FactoriusServices = {}>( | |
core?: FactoriusCore<S> | |
): Factorius<S> { | |
const factories = (core?.factories ?? {}) as S; | |
function layer(newServices: any) { | |
const newDipper = factorius({ | |
factories: { | |
...factories, | |
...newServices, | |
}, | |
}); | |
return newDipper as any; | |
} | |
return { | |
factories, | |
layer, | |
define: layer, | |
provide: layer, | |
override: layer, | |
get tokens() { | |
return tokensOf({ factories }); | |
}, | |
check(params) { | |
const ignore = params?.ignore ?? {}; | |
const stillRequires: string[] = []; | |
for (const [key, factory] of Object.entries(factories)) { | |
if (key in ignore || !isRequirement(factory) || factory.optional) { | |
continue; | |
} | |
stillRequires.push(key); | |
} | |
if (stillRequires.length !== 0) { | |
return { [stillRequiresErrorKey]: stillRequires }; | |
} | |
return factorius(core) as any; | |
}, | |
requires<S extends FactoriusServices>( | |
m: FactoriusCore<S>, | |
requiredKeys?: Record<string, true> | |
) { | |
const keys = Object.keys(requiredKeys ? requiredKeys : m.factories); | |
const res = {} as any; | |
for (const key of keys) { | |
res[key] = requires(type<any>(), (m.factories[key] as any).externalId); | |
} | |
return factorius({ | |
factories: { | |
...factories, | |
...res, | |
}, | |
}) as any; | |
}, | |
import(m) { | |
const res = {} as any; | |
for (const key in m.factories) { | |
const factory = m.factories[key]; | |
if ( | |
isRequirement(factory) && | |
key in factories && | |
!isRequirement(factories[key]) | |
) { | |
continue; | |
} | |
res[key] = factory; | |
} | |
return factorius({ | |
factories: { | |
...factories, | |
...res, | |
}, | |
}) as any; | |
}, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment