Skip to content

Instantly share code, notes, and snippets.

@temoncher
Last active June 23, 2024 13:50
Show Gist options
  • Save temoncher/1bc55745a51cb3099bcd26b23697a62e to your computer and use it in GitHub Desktop.
Save temoncher/1bc55745a51cb3099bcd26b23697a62e to your computer and use it in GitHub Desktop.
Factorius, typesafe dependency injection library frontend, with agnostic backend
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);
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;
}
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