mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
for now copy paste new ioc system
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
75
src/core/ioc/global.ts
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))));
|
||||
}
|
||||
|
||||
@@ -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?")
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user