mirror of
https://github.com/sern-handler/handler
synced 2026-06-05 17:06:53 +00:00
feat: dispose hooks (deprecate useContainerRaw) (#323)
* feat: dispose hooks * build: unminify, add source map, deprecate useContainerRaw * fix regression of context and fix tsup build
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -95,3 +95,5 @@ dist
|
||||
.yalc
|
||||
|
||||
yalc.lock
|
||||
|
||||
*.svg
|
||||
|
||||
1484
dependency-graph.svg
1484
dependency-graph.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 119 KiB |
@@ -19,7 +19,7 @@
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"format": "eslint src/**/*.ts --fix",
|
||||
"build:dev": "tsup --metafile",
|
||||
"build:prod": "tsup --minify",
|
||||
"build:prod": "tsup ",
|
||||
"prepare": "npm run build:prod",
|
||||
"pretty": "prettier --write .",
|
||||
"tdd": "vitest",
|
||||
|
||||
@@ -7,3 +7,4 @@ export type { VoidResult } from '../types/core-plugin';
|
||||
export { SernError } from './structures/enums';
|
||||
export { ModuleStore } from './structures/module-store';
|
||||
export * as DefaultServices from './structures/services';
|
||||
export { useContainerRaw } from './ioc/base'
|
||||
|
||||
9
src/core/contracts/disposable.ts
Normal file
9
src/core/contracts/disposable.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Awaitable } from '../../types/utility';
|
||||
|
||||
/**
|
||||
* Represents a Disposable contract.
|
||||
* Let dependencies implement this to dispose and cleanup.
|
||||
*/
|
||||
export interface Disposable {
|
||||
dispose(): Awaitable<unknown>;
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './module-manager';
|
||||
export * from './module-store';
|
||||
export * from './init';
|
||||
export * from './emitter';
|
||||
export * from './disposable'
|
||||
|
||||
@@ -7,8 +7,10 @@ import { CoreContainer } from './container';
|
||||
let containerSubject: CoreContainer<Partial<Dependencies>>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Returns the underlying data structure holding all dependencies.
|
||||
* Exposes methods from iti
|
||||
* Use the Service API. The container should be readonly
|
||||
*/
|
||||
export function useContainerRaw() {
|
||||
assert.ok(
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { Container } from 'iti';
|
||||
import { SernEmitter } from '../';
|
||||
import { isAsyncFunction } from 'node:util/types';
|
||||
|
||||
import { Disposable, SernEmitter } from '../';
|
||||
import * as assert from 'node:assert';
|
||||
import { Subject } from 'rxjs';
|
||||
import { DefaultServices, ModuleStore } from '../_internal';
|
||||
import * as Hooks from './hooks'
|
||||
|
||||
|
||||
/**
|
||||
* Provides all the defaults for sern to function properly.
|
||||
* The only user provided dependency needs to be @sern/client
|
||||
* 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<never>();
|
||||
private beenCalled = new Set<PropertyKey>();
|
||||
private ready$ = new Subject<void>();
|
||||
constructor() {
|
||||
super();
|
||||
assert.ok(!this.isReady(), 'Listening for dispose & init should occur prior to sern being ready.');
|
||||
|
||||
this.listenForInsertions();
|
||||
const { unsubscribe } = Hooks.createInitListener(this);
|
||||
this.ready$
|
||||
.subscribe({ complete: unsubscribe });
|
||||
|
||||
(this as Container<{}, {}>)
|
||||
.add({
|
||||
@@ -32,36 +34,27 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
|
||||
});
|
||||
}
|
||||
|
||||
private listenForInsertions() {
|
||||
assert.ok(
|
||||
!this.isReady(),
|
||||
'listening for init functions should only occur prior to sern being ready.',
|
||||
);
|
||||
const unsubscriber = this.on('containerUpserted', e => this.callInitHooks(e));
|
||||
|
||||
this.ready$.subscribe({
|
||||
complete: unsubscriber,
|
||||
});
|
||||
}
|
||||
|
||||
private async callInitHooks(e: { key: keyof T; newContainer: T[keyof T] | null }) {
|
||||
const dep = e.newContainer;
|
||||
|
||||
assert.ok(dep);
|
||||
//Ignore any dependencies that are not objects or array
|
||||
if (typeof dep !== 'object' || Array.isArray(dep)) {
|
||||
return;
|
||||
}
|
||||
if ('init' in dep && typeof dep.init === 'function' && !this.beenCalled.has(e.key)) {
|
||||
isAsyncFunction(dep.init) ? await dep.init() : dep.init();
|
||||
this.beenCalled.add(e.key);
|
||||
}
|
||||
}
|
||||
|
||||
isReady() {
|
||||
|
||||
return this.ready$.closed;
|
||||
}
|
||||
override async disposeAll() {
|
||||
|
||||
const otherDisposables = Object
|
||||
.entries(this._context)
|
||||
.flatMap(([key, value]) =>
|
||||
'dispose' in value
|
||||
? [key]
|
||||
: []);
|
||||
|
||||
for(const key of otherDisposables) {
|
||||
this.addDisposer({ [key]: (dep: Disposable) => dep.dispose() } as never);
|
||||
}
|
||||
await super.disposeAll()
|
||||
}
|
||||
ready() {
|
||||
this.ready$.complete();
|
||||
this.ready$.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
|
||||
import { SernError, DefaultServices } from '../_internal';
|
||||
import { DefaultServices } from '../_internal';
|
||||
import { useContainerRaw } from './base';
|
||||
import { CoreContainer } from './container';
|
||||
|
||||
|
||||
40
src/core/ioc/hooks.ts
Normal file
40
src/core/ioc/hooks.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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,2 @@
|
||||
export { useContainerRaw, makeDependencies } from './base';
|
||||
export { makeDependencies } from './base';
|
||||
export { Service, Services, single, transient } from './dependency-injection';
|
||||
|
||||
@@ -120,5 +120,8 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
||||
}
|
||||
|
||||
function safeUnwrap<T>(res: Result<T, T>) {
|
||||
return res.unwrap()
|
||||
if(res.isOk()) {
|
||||
return res.expect("Tried unwrapping message field: " + res)
|
||||
}
|
||||
return res.expectErr("Tried unwrapping interaction field" + res)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ import {
|
||||
handleError,
|
||||
SernError,
|
||||
VoidResult,
|
||||
useContainerRaw,
|
||||
} from '../core/_internal';
|
||||
import { Emitter, ErrorHandling, Logging, ModuleManager, useContainerRaw } from '../core';
|
||||
import { Emitter, ErrorHandling, Logging, ModuleManager } from '../core';
|
||||
import { contextArgs, createDispatcher, dispatchMessage } from './dispatchers';
|
||||
import { ObservableInput, pipe } from 'rxjs';
|
||||
import { SernEmitter } from '../core';
|
||||
|
||||
@@ -50,4 +50,7 @@ export {
|
||||
CommandExecutable,
|
||||
} from './core/modules';
|
||||
|
||||
export {
|
||||
useContainerRaw
|
||||
} from './core/_internal'
|
||||
export { controller } from './sern';
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CoreContainer } from '../../src/core/ioc/container';
|
||||
import { CoreDependencies } from '../../src/core/ioc';
|
||||
import { EventEmitter } from 'events';
|
||||
import { DefaultLogging, Init, Logging } from '../../src/core';
|
||||
import { DefaultLogging, Disposable, Init, Logging } from '../../src/core';
|
||||
import { CoreDependencies } from '../../src/types/ioc';
|
||||
|
||||
describe('ioc container', () => {
|
||||
let container: CoreContainer<{}>;
|
||||
let initDependency: Logging & Init;
|
||||
let container: CoreContainer<{}> = new CoreContainer();
|
||||
let dependency: Logging & Init & Disposable;
|
||||
beforeEach(() => {
|
||||
initDependency = {
|
||||
dependency = {
|
||||
init: vi.fn(),
|
||||
error(): void {},
|
||||
warning(): void {},
|
||||
info(): void {},
|
||||
debug(): void {},
|
||||
dispose: vi.fn()
|
||||
};
|
||||
container = new CoreContainer();
|
||||
});
|
||||
|
||||
const wait = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds));
|
||||
class DB implements Init, Disposable {
|
||||
public connected = false
|
||||
constructor() {}
|
||||
async init() {
|
||||
this.connected = true
|
||||
await wait(10)
|
||||
}
|
||||
async dispose() {
|
||||
await wait(20)
|
||||
this.connected = false
|
||||
}
|
||||
}
|
||||
it('should be ready after calling container.ready()', () => {
|
||||
container.ready();
|
||||
expect(container.isReady()).toBe(true);
|
||||
@@ -39,14 +52,35 @@ describe('ioc container', () => {
|
||||
}
|
||||
});
|
||||
it('should init modules', () => {
|
||||
container.upsert({ '@sern/logger': initDependency });
|
||||
container.upsert({ '@sern/logger': dependency });
|
||||
container.ready();
|
||||
expect(initDependency.init).to.toHaveBeenCalledOnce();
|
||||
expect(dependency.init).to.toHaveBeenCalledOnce();
|
||||
});
|
||||
it('should dispose modules', async () => {
|
||||
|
||||
container.upsert({ '@sern/logger': dependency })
|
||||
|
||||
container.ready();
|
||||
// We need to access the dependency at least once to be able to dispose of it.
|
||||
container.get('@sern/logger' as never);
|
||||
await container.disposeAll();
|
||||
expect(dependency.dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should init and dispose', async () => {
|
||||
container.add({ db: new DB() })
|
||||
container.ready()
|
||||
const db = container.get('db' as never) as DB
|
||||
expect(db.connected).toBeTruthy()
|
||||
|
||||
await container.disposeAll();
|
||||
|
||||
expect(db.connected).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should not lazy module', () => {
|
||||
container.upsert({ '@sern/logger': () => initDependency });
|
||||
container.upsert({ '@sern/logger': () => dependency });
|
||||
container.ready();
|
||||
expect(initDependency.init).toHaveBeenCalledTimes(0);
|
||||
expect(dependency.init).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,14 +39,14 @@ describe('services', () => {
|
||||
.map((path, i) => `${path}/${modules[i]}.js`);
|
||||
|
||||
const metadata: CommandMeta[] = modules.map((cm, i) => ({
|
||||
id: Id.create(cm.name, cm.type),
|
||||
id: Id.create(cm.name!, cm.type),
|
||||
isClass: false,
|
||||
fullPath: `${paths[i]}/${cm.name}.js`,
|
||||
}));
|
||||
const moduleManager = container.get('@sern/modules');
|
||||
let i = 0;
|
||||
for (const m of modules) {
|
||||
moduleManager.set(Id.create(m.name, m.type), paths[i]);
|
||||
moduleManager.set(Id.create(m.name!, m.type), paths[i]);
|
||||
moduleManager.setMetadata(m, metadata[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const shared = {
|
||||
external: ['discord.js', 'iti'],
|
||||
platform: 'node',
|
||||
clean: true,
|
||||
sourcemap: false,
|
||||
sourcemap: true,
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
correctVarValueBeforeDeclaration: true, //need this to treeshake esm discord.js empty import
|
||||
@@ -17,33 +17,8 @@ export default defineConfig([
|
||||
target: 'node18',
|
||||
tsconfig: './tsconfig.json',
|
||||
outDir: './dist',
|
||||
splitting: true,
|
||||
minify: false,
|
||||
dts: true,
|
||||
...shared,
|
||||
},
|
||||
// {
|
||||
// format: 'cjs',
|
||||
// esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'cjs' }, verbose: true })],
|
||||
// splitting: false,
|
||||
// target: 'node18',
|
||||
// tsconfig: './tsconfig-cjs.json',
|
||||
// outDir: './dist/cjs',
|
||||
// outExtension() {
|
||||
// return {
|
||||
// js: '.cjs',
|
||||
// };
|
||||
// },
|
||||
// async onSuccess() {
|
||||
// console.log('writing json commonjs');
|
||||
// await writeFile('./dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }));
|
||||
// },
|
||||
// ...shared,
|
||||
// },
|
||||
// {
|
||||
// dts: {
|
||||
// only: true,
|
||||
// },
|
||||
// entry: ['src/index.ts'],
|
||||
// outDir: 'dist',
|
||||
// },
|
||||
]);
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user