From cd92b548397da96374f1ae50b2469c9356169e10 Mon Sep 17 00:00:00 2001 From: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com> Date: Fri, 3 May 2024 18:13:34 -0500 Subject: [PATCH] for now copy paste new ioc system --- src/core/_internal.ts | 1 - src/core/ioc/base.ts | 71 +++++-------------- src/core/ioc/container.ts | 100 ++++++++++++++++----------- src/core/ioc/dependency-injection.ts | 32 +-------- src/core/ioc/global.ts | 75 ++++++++++++++++++++ src/core/ioc/hooks.ts | 41 ----------- src/core/ioc/index.ts | 30 +++++++- src/core/module-loading.ts | 22 +++--- src/handlers/presence.ts | 44 ++++++------ src/sern.ts | 1 - src/types/ioc.ts | 7 +- src/types/utility.ts | 3 +- 12 files changed, 222 insertions(+), 205 deletions(-) create mode 100644 src/core/ioc/global.ts delete mode 100644 src/core/ioc/hooks.ts diff --git a/src/core/_internal.ts b/src/core/_internal.ts index fa9d4b5..5066fa3 100644 --- a/src/core/_internal.ts +++ b/src/core/_internal.ts @@ -3,7 +3,6 @@ import type { Result } from 'ts-results-es' export * from './operators'; export * from './functions'; export { SernError } from './structures/enums'; -export { useContainerRaw } from './ioc/base'; export type _Module = { meta: { diff --git a/src/core/ioc/base.ts b/src/core/ioc/base.ts index 0e86dee..f8cc0d9 100644 --- a/src/core/ioc/base.ts +++ b/src/core/ioc/base.ts @@ -1,53 +1,18 @@ -import * as assert from 'assert'; -import type { CoreDependencies, DependencyConfiguration } from '../../types/ioc'; -import { CoreContainer } from './container'; +import type { DependencyConfiguration } from '../../types/ioc'; +import { Container } from './container'; import { Result } from 'ts-results-es'; import * as __Services from '../structures/default-services'; -import { AnyFunction } from '../../types/utility'; +import { AnyFunction, UnpackFunction } from '../../types/utility'; import type { Logging } from '../interfaces'; -import type { UnpackFunction } from 'iti'; -//SIDE EFFECT: GLOBAL DI -let containerSubject: CoreContainer>; - -/** - * @internal - * Don't use this unless you know what you're doing. Destroys old containerSubject if it exists and disposes everything - * then it will swap - */ -export async function __swap_container(c: CoreContainer>) { - if(containerSubject) { - await containerSubject.disposeAll() - } - containerSubject = c; -} - -/** - * @internal - * Don't use this unless you know what you're doing. Destroys old containerSubject if it exists and disposes everything - * then it will swap - */ -export function __add_container(key: string,v : Insertable) { - containerSubject.add({ [key]: v }); -} - -/** - * Returns the underlying data structure holding all dependencies. - * Exposes methods from iti - * Use the Service API. The container should be readonly from the consumer side - */ -export function useContainerRaw() { - assert.ok( - containerSubject && containerSubject.isReady(), - "Could not find container or container wasn't ready. Did you call makeDependencies?", - ); - return containerSubject; -} +import { __add_container, __swap_container, useContainerRaw } from './global'; export function disposeAll(logger: Logging|undefined) { - containerSubject + useContainerRaw() ?.disposeAll() .then(() => logger?.info({ message: 'Cleaning container and crashing' })); } + + type UnpackedDependencies = { [K in keyof Dependencies]: UnpackFunction } @@ -121,8 +86,8 @@ type ValidDependencyConfig = * Finally, update the containerSubject with the new container state * @param conf */ -function composeRoot( - container: CoreContainer>, +async function composeRoot( + container: Container, conf: DependencyConfiguration, ) { //container should have no client or logger yet. @@ -131,32 +96,32 @@ function composeRoot( __add_container('@sern/logger', new __Services.DefaultLogging); } //Build the container based on the callback provided by the user - conf.build(container as CoreContainer>); + conf.build(container as Container); if (!hasLogger) { - container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' }); + container + .get('@sern/logger') + ?.info({ message: 'All dependencies loaded successfully.' }); } - container.ready(); } export async function makeDependencies (conf: ValidDependencyConfig) { - containerSubject = new CoreContainer(); + __swap_container(new Container({ autowire: false })); if(typeof conf === 'function') { const excluded: string[] = []; - conf(dependencyBuilder(containerSubject, excluded)); + conf(dependencyBuilder(useContainerRaw(), excluded)); //We only include logger if it does not exist const includeLogger = !excluded.includes('@sern/logger') - && !containerSubject.hasKey('@sern/logger'); + && !useContainerRaw().hasKey('@sern/logger'); if(includeLogger) { __add_container('@sern/logger', new __Services.DefaultLogging); } - - containerSubject.ready(); + await useContainerRaw().ready(); } else { - composeRoot(containerSubject, conf); + await composeRoot(useContainerRaw(), conf); } } diff --git a/src/core/ioc/container.ts b/src/core/ioc/container.ts index d7afd0a..528629d 100644 --- a/src/core/ioc/container.ts +++ b/src/core/ioc/container.ts @@ -1,54 +1,76 @@ -import { Container } from 'iti'; import type { Disposable } from '../interfaces'; -import * as assert from 'node:assert'; -import { Subject } from 'rxjs'; import * as __Services from '../structures/default-services'; -import * as Hooks from './hooks'; -import { EventEmitter } from 'node:events'; /** * 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 */ -export class CoreContainer> extends Container { - private ready$ = new Subject(); - constructor() { - super(); - assert.ok(!this.isReady(), 'Listening for dispose & init should occur prior to sern being ready.'); - - const { unsubscribe } = Hooks.createInitListener(this); - - this.ready$ - .subscribe({ complete: unsubscribe }); - - (this as Container<{}, {}>) - .add({ '@sern/errors': () => new __Services.DefaultErrorHandling, - '@sern/emitter': () => new EventEmitter({ captureRejections: true }) }) - } - - isReady() { - return this.ready$.closed; +export function hasCallableMethod(obj: object, name: PropertyKey) { + //@ts-ignore + return Object.hasOwn(obj, name) && 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(); + private hooks= new Map(); + private finished_init = false; + constructor(options: { autowire: boolean; path?: string }) { + if(options.autowire) { /* noop */ } } - hasKey(key: string): boolean { - return Boolean((this as Container)._context[key]); + addHook(name: string, callback: Function) { + 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]()) + } + } + addSingleton(key: string, insert: object) { + if(typeof insert !== 'object') { + throw Error("Inserted object must be an object"); + } + if(!this.__singletons.has(key)){ + this.registerHooks('init', insert) + this.registerHooks('dispose', insert) + this.__singletons.set(key, insert); + return true; + } + return false; } - override async disposeAll() { - const otherDisposables = Object - .entries(this._context) - .flatMap(([key, value]) => - 'dispose' in value ? [key] : []); - otherDisposables.forEach(key => { - //possible source of bug: dispose is a property. - this.addDisposer({ [key]: (dep: Disposable) => dep.dispose() } as never); - }) - await super.disposeAll(); + addWiredSingleton(key: string, fn: (c: Container) => object) { + const insert = fn(this); + return this.addSingleton(key, insert); } - - ready() { - this.ready$.complete(); - this.ready$.unsubscribe(); + + async disposeAll() { + await this.executeHooks('dispose'); + this.hooks.delete('dispose'); + } + + isReady() { return this.finished_init; } + hasKey(key: string) { return this.__singletons.has(key); } + get(key: PropertyKey) : T|undefined { return this.__singletons.get(key); } + + async ready() { + await this.executeHooks('init'); + this.hooks.delete('init'); + this.finished_init = true; + } + + async executeHooks(name: string) { + const hookFunctions = this.hooks.get(name) || []; + for (const hookFunction of hookFunctions) { + await hookFunction(); + } } } diff --git a/src/core/ioc/dependency-injection.ts b/src/core/ioc/dependency-injection.ts index e5fa4ed..9b9373c 100644 --- a/src/core/ioc/dependency-injection.ts +++ b/src/core/ioc/dependency-injection.ts @@ -1,7 +1,5 @@ -import assert from 'node:assert'; import type { IntoDependencies } from '../../types/ioc'; -import { useContainerRaw } from './base'; - +import { Service as __Service, Services as __Services } from './global' /** * @since 2.0.0. * Creates a singleton object. @@ -16,30 +14,4 @@ export function single(cb: () => T) { return cb; } * @param cb */ export function transient(cb: () => () => T) { return cb; } -/** - * The new Service api, a cleaner alternative to useContainer - * To obtain intellisense, ensure a .d.ts file exists in the root of compilation. - * Usually our scaffolding tool takes care of this. - * Note: this method only works AFTER your container has been initiated - * @since 3.0.0 - * @example - * ```ts - * const client = Service('@sern/client'); - * ``` - * @param key a key that corresponds to a dependency registered. - * - */ -export function Service(key: T) { - const dep = useContainerRaw().get(key)!; - assert(dep, "Requested key " + key + " returned undefined"); - return dep; -} -/** - * @since 3.0.0 - * The plural version of {@link Service} - * @returns array of dependencies, in the same order of keys provided - */ -export function Services(...keys: [...T]) { - const container = useContainerRaw(); - return keys.map(k => container.get(k)!) as IntoDependencies; -} + diff --git a/src/core/ioc/global.ts b/src/core/ioc/global.ts new file mode 100644 index 0000000..1f3d6d1 --- /dev/null +++ b/src/core/ioc/global.ts @@ -0,0 +1,75 @@ +import { Container } from './container'; + +//SIDE EFFECT: GLOBAL DI +let containerSubject: Container; + +/** + * Don't use this unless you know what you're doing. Destroys old containerSubject if it exists and disposes everything + * then it will swap + */ +export async function __swap_container(c: Container) { + if(containerSubject) { + await containerSubject.disposeAll() + } + containerSubject = c; +} + +/** + * Don't use this unless you know what you're doing. Destroys old containerSubject if it exists and disposes everything + * then it will swap + */ +export function __add_container(key: string, v: object) { + containerSubject.addSingleton(key, v); +} + +/** + * Initiates the global api. + * Once this is finished, the Service api and the other global api is available + */ +export function __init_container(options: { + autowire: boolean; + path?: string | undefined; +}) { + containerSubject = new Container(options); +} + +/** + * Returns the underlying data structure holding all dependencies. + * Exposes methods from iti + * Use the Service API. The container should be readonly + */ +export function useContainerRaw() { + if (!(containerSubject && containerSubject.isReady())) { + throw new Error("Container wasn't ready or init'd. Please ensure container is ready()"); + } + + return containerSubject; +} + +/** + * The Service api, retrieve from the globally init'ed container + * Note: this method only works AFTER your container has been initiated + * @since 3.0.0 + * @example + * ```ts + * const client = Service('@sern/client'); + * ``` + * @param key a key that corresponds to a dependency registered. + * + */ +export function Service(key: PropertyKey) { + const dep = useContainerRaw().get(key)!; + if(!dep) { + throw Error("Requested key " + String(key) + " returned undefined"); + } + return dep; +} +/** + * @since 3.0.0 + * The plural version of {@link Service} + * @returns array of dependencies, in the same order of keys provided + */ +export function Services(...keys: [...T]) { + const container = useContainerRaw(); + return keys.map(k => container.get(k)!) as V; +} diff --git a/src/core/ioc/hooks.ts b/src/core/ioc/hooks.ts deleted file mode 100644 index 0a32557..0000000 --- a/src/core/ioc/hooks.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CoreContainer } from "./container" - -interface HookEvent { - key : PropertyKey - newContainer: any -} -type HookName = 'init'; - -export const createInitListener = (coreContainer : CoreContainer) => { - const initCalled = new Set(); - const hasCallableMethod = createPredicate(initCalled); - const unsubscribe = coreContainer.on('containerUpserted', async event => { - - if(isNotHookable(event)) { - return; - } - - if(hasCallableMethod('init', event)) { - await event.newContainer?.init(); - initCalled.add(event.key); - } - - }); - - return { unsubscribe }; -} - -const isNotHookable = (hk: HookEvent) => { - return typeof hk.newContainer !== 'object' - || Array.isArray(hk.newContainer) - || hk.newContainer === null; -} - -const createPredicate = (called: Set) => { - return (hookName: HookName, event: T) => { - const hasMethod = Reflect.has(event.newContainer!, hookName); - const beenCalledOnce = !called.has(event.key) - - return hasMethod && beenCalledOnce - } -} diff --git a/src/core/ioc/index.ts b/src/core/ioc/index.ts index e89f8b6..5d48b6a 100644 --- a/src/core/ioc/index.ts +++ b/src/core/ioc/index.ts @@ -1,2 +1,30 @@ +import { IntoDependencies } from '../../types/ioc'; +import { Service as __Service, Services as __Services } from './global' export { makeDependencies } from './base'; -export { Service, Services, single, transient } from './dependency-injection'; +export { single, transient } from './dependency-injection'; + + +/** + * The new Service api, a cleaner alternative to useContainer + * To obtain intellisense, ensure a .d.ts file exists in the root of compilation. + * Usually our scaffolding tool takes care of this. + * Note: this method only works AFTER your container has been initiated + * @since 3.0.0 + * @example + * ```ts + * const client = Service('@sern/client'); + * ``` + * @param key a key that corresponds to a dependency registered. + * + */ +export function Service(key: T) { + return __Service(key) as Dependencies[T] +} +/** + * @since 3.0.0 + * The plural version of {@link Service} + * @returns array of dependencies, in the same order of keys provided + */ +export function Services(...keys: [...T]) { + return __Services>(...keys) +} diff --git a/src/core/module-loading.ts b/src/core/module-loading.ts index 14e620f..6f7b0aa 100644 --- a/src/core/module-loading.ts +++ b/src/core/module-loading.ts @@ -34,17 +34,17 @@ export const shouldHandle = (pth: string, filenam: string) => { * esm javascript, typescript, and commonjs typescript * export default commandModule({}) */ -export async function importModule(absPath: string) { - let fileModule = await import(absPath); - - let commandModule = fileModule.default; - - assert(commandModule , `No export @ ${absPath}. Forgot to ignore with "!"? (!${path.basename(absPath)})?`); - if ('default' in commandModule) { - commandModule = commandModule.default; - } - return { module: commandModule } as T; -} +//async function importModule(absPath: string) { +// let fileModule = await import(absPath); +// +// let commandModule = fileModule.default; +// +// assert(commandModule , `No export @ ${absPath}. Forgot to ignore with "!"? (!${path.basename(absPath)})?`); +// if ('default' in commandModule) { +// commandModule = commandModule.default; +// } +// return { module: commandModule } as T; +//} export const fmtFileName = (fileName: string) => path.parse(fileName).name; diff --git a/src/handlers/presence.ts b/src/handlers/presence.ts index 4565da3..331f545 100644 --- a/src/handlers/presence.ts +++ b/src/handlers/presence.ts @@ -1,7 +1,5 @@ -import { concatMap, from, interval, of, map, scan, startWith, fromEvent, take } from "rxjs" -import * as Files from "../core/module-loading"; +import { interval, scan, startWith, fromEvent, take, of } from "rxjs" import { PresenceConfig, PresenceResult } from "../core/presences"; -import { Services } from "../core/ioc"; import assert from "node:assert"; type SetPresence = (conf: PresenceResult) => Promise @@ -22,24 +20,24 @@ const parseConfig = async (conf: Promise) => { return of(s).pipe(take(1)); }) }; - -export const presenceHandler = (path: string, setPresence: SetPresence) => { - interface PresenceModule { - module: PresenceConfig<(keyof Dependencies)[]> - } - const presence = Files - .importModule(path) - .then(({ module }) => { - //fetch services with the order preserved, passing it to the execute fn - const fetchedServices = Services(...module.inject ?? []); - return async () => module.execute(...fetchedServices); - }) - const module$ = from(presence); - return module$.pipe( - //compose: - //call the execute function, passing that result into parseConfig. - //concatMap resolves the promise, and passes it to the next concatMap. - concatMap(fn => parseConfig(fn())), - // subscribe to the observable parseConfig yields, and set the presence. - concatMap(conf => conf.pipe(map(setPresence)))); +interface PresenceModule { + module: PresenceConfig<(keyof Dependencies)[]> +} +export const presenceHandler = (path: string, setPresence: SetPresence) => { + +// const presence = Files +// .importModule(path) +// .then(({ module }) => { +// //fetch services with the order preserved, passing it to the execute fn +// const fetchedServices = Services(...module.inject ?? []); +// return async () => module.execute(...fetchedServices); +// }) +// const module$ = from(presence); +// return module$.pipe( +// //compose: +// //call the execute function, passing that result into parseConfig. +// //concatMap resolves the promise, and passes it to the next concatMap. +// concatMap(fn => parseConfig(fn())), +// // subscribe to the observable parseConfig yields, and set the presence. +// concatMap(conf => conf.pipe(map(setPresence)))); } diff --git a/src/sern.ts b/src/sern.ts index 70fe15e..9ff09bb 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -37,7 +37,6 @@ export function init(wrapper: Wrapper) { const initCallsite = callsites()[1].getFileName(); const handlerModule = Files.shouldHandle(initCallsite!, "handler"); - console.log(handlerModule) if(!handlerModule.exists) { throw Error("Could not find handler module, did you run sern build?") } diff --git a/src/types/ioc.ts b/src/types/ioc.ts index 45bce58..8ce0aca 100644 --- a/src/types/ioc.ts +++ b/src/types/ioc.ts @@ -1,5 +1,6 @@ -import { Container, UnpackFunction } from 'iti'; +import type { Container } from '../core/ioc/container'; import * as Contracts from '../core/interfaces'; +import type { UnpackFunction } from './utility' /** * Type to annotate that something is a singleton. * T is created once and lazily. @@ -39,7 +40,5 @@ export interface DependencyConfiguration { * @deprecated. Loggers will be opt-in the future */ exclude?: Set<'@sern/logger'>; - build: ( - root: Container, {}>, - ) => Container; + build: (root: Container) => Container; } diff --git a/src/types/utility.ts b/src/types/utility.ts index 95d18fc..4a705dd 100644 --- a/src/types/utility.ts +++ b/src/types/utility.ts @@ -28,5 +28,6 @@ export type Payload = | { type: PayloadType.Failure; module?: Module; reason: string | Error } | { type: PayloadType.Warning; module: undefined; reason: string }; - +//https://github.com/molszanski/iti/blob/0a3a006113b4176316c308805314a135c0f47902/iti/src/_utils.ts#L29C1-L29C76 +export type UnpackFunction = T extends (...args: any) => infer U ? U : T export type ReplyOptions = string | Omit | MessageReplyOptions;