for now copy paste new ioc system

This commit is contained in:
Jacob Nguyen
2024-05-03 18:13:34 -05:00
parent b0e9d15fa7
commit cd92b54839
12 changed files with 222 additions and 205 deletions

View File

@@ -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: {

View File

@@ -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<Partial<Dependencies>>;
/**
* @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<Partial<Dependencies>>) {
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<Dependencies[K]>
}
@@ -121,8 +86,8 @@ type ValidDependencyConfig =
* Finally, update the containerSubject with the new container state
* @param conf
*/
function composeRoot(
container: CoreContainer<Partial<Dependencies>>,
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<Omit<CoreDependencies, '@sern/client'>>);
conf.build(container as Container);
if (!hasLogger) {
container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
container
.get<Logging>('@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);
}
}

View File

@@ -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<T extends Partial<Dependencies>> extends Container<T, {}> {
private ready$ = new Subject<void>();
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<PropertyKey, any>();
private hooks= new Map<string, Function[]>();
private finished_init = false;
constructor(options: { autowire: boolean; path?: string }) {
if(options.autowire) { /* noop */ }
}
hasKey(key: string): boolean {
return Boolean((this as Container<any,any>)._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<T>(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();
}
}
}

View File

@@ -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<T>(cb: () => T) { return cb; }
* @param cb
*/
export function transient<T>(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<const T extends keyof Dependencies>(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<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
const container = useContainerRaw();
return keys.map(k => container.get(k)!) as IntoDependencies<T>;
}

75
src/core/ioc/global.ts Normal file
View File

@@ -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<const T>(key: PropertyKey) {
const dep = useContainerRaw().get<T>(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<const T extends string[], V>(...keys: [...T]) {
const container = useContainerRaw();
return keys.map(k => container.get(k)!) as V;
}

View File

@@ -1,41 +0,0 @@
import type { CoreContainer } from "./container"
interface HookEvent {
key : PropertyKey
newContainer: any
}
type HookName = 'init';
export const createInitListener = (coreContainer : CoreContainer<any>) => {
const initCalled = new Set<PropertyKey>();
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 = <T extends HookEvent>(called: Set<PropertyKey>) => {
return (hookName: HookName, event: T) => {
const hasMethod = Reflect.has(event.newContainer!, hookName);
const beenCalledOnce = !called.has(event.key)
return hasMethod && beenCalledOnce
}
}

View File

@@ -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<const T extends keyof Dependencies>(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<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
return __Services<T, IntoDependencies<T>>(...keys)
}

View File

@@ -34,17 +34,17 @@ export const shouldHandle = (pth: string, filenam: string) => {
* esm javascript, typescript, and commonjs typescript
* export default commandModule({})
*/
export async function importModule<T>(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<T>(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;

View File

@@ -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<unknown>
@@ -22,24 +20,24 @@ const parseConfig = async (conf: Promise<PresenceResult>) => {
return of(s).pipe(take(1));
})
};
export const presenceHandler = (path: string, setPresence: SetPresence) => {
interface PresenceModule {
module: PresenceConfig<(keyof Dependencies)[]>
}
const presence = Files
.importModule<PresenceModule>(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<PresenceModule>(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))));
}

View File

@@ -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?")
}

View File

@@ -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<Omit<CoreDependencies, '@sern/client'>, {}>,
) => Container<Dependencies, {}>;
build: (root: Container) => Container;
}

View File

@@ -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> = T extends (...args: any) => infer U ? U : T
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;