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
|
||||||
|
|
||||||
yalc.lock
|
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",
|
"lint": "eslint src/**/*.ts",
|
||||||
"format": "eslint src/**/*.ts --fix",
|
"format": "eslint src/**/*.ts --fix",
|
||||||
"build:dev": "tsup --metafile",
|
"build:dev": "tsup --metafile",
|
||||||
"build:prod": "tsup --minify",
|
"build:prod": "tsup ",
|
||||||
"prepare": "npm run build:prod",
|
"prepare": "npm run build:prod",
|
||||||
"pretty": "prettier --write .",
|
"pretty": "prettier --write .",
|
||||||
"tdd": "vitest",
|
"tdd": "vitest",
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export type { VoidResult } from '../types/core-plugin';
|
|||||||
export { SernError } from './structures/enums';
|
export { SernError } from './structures/enums';
|
||||||
export { ModuleStore } from './structures/module-store';
|
export { ModuleStore } from './structures/module-store';
|
||||||
export * as DefaultServices from './structures/services';
|
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 './module-store';
|
||||||
export * from './init';
|
export * from './init';
|
||||||
export * from './emitter';
|
export * from './emitter';
|
||||||
|
export * from './disposable'
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import { CoreContainer } from './container';
|
|||||||
let containerSubject: CoreContainer<Partial<Dependencies>>;
|
let containerSubject: CoreContainer<Partial<Dependencies>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated
|
||||||
* Returns the underlying data structure holding all dependencies.
|
* Returns the underlying data structure holding all dependencies.
|
||||||
* Exposes methods from iti
|
* Exposes methods from iti
|
||||||
|
* Use the Service API. The container should be readonly
|
||||||
*/
|
*/
|
||||||
export function useContainerRaw() {
|
export function useContainerRaw() {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import { Container } from 'iti';
|
import { Container } from 'iti';
|
||||||
import { SernEmitter } from '../';
|
import { Disposable, SernEmitter } from '../';
|
||||||
import { isAsyncFunction } from 'node:util/types';
|
|
||||||
|
|
||||||
import * as assert from 'node:assert';
|
import * as assert from 'node:assert';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { DefaultServices, ModuleStore } from '../_internal';
|
import { DefaultServices, ModuleStore } from '../_internal';
|
||||||
|
import * as Hooks from './hooks'
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides all the defaults for sern to function properly.
|
* A semi-generic container that provides error handling, emitter, and module store.
|
||||||
* The only user provided dependency needs to be @sern/client
|
* 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, {}> {
|
export class CoreContainer<T extends Partial<Dependencies>> extends Container<T, {}> {
|
||||||
private ready$ = new Subject<never>();
|
private ready$ = new Subject<void>();
|
||||||
private beenCalled = new Set<PropertyKey>();
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
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<{}, {}>)
|
(this as Container<{}, {}>)
|
||||||
.add({
|
.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() {
|
isReady() {
|
||||||
|
|
||||||
return this.ready$.closed;
|
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() {
|
ready() {
|
||||||
|
this.ready$.complete();
|
||||||
this.ready$.unsubscribe();
|
this.ready$.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
|
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
|
||||||
import { SernError, DefaultServices } from '../_internal';
|
import { DefaultServices } from '../_internal';
|
||||||
import { useContainerRaw } from './base';
|
import { useContainerRaw } from './base';
|
||||||
import { CoreContainer } from './container';
|
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';
|
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>) {
|
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,
|
handleError,
|
||||||
SernError,
|
SernError,
|
||||||
VoidResult,
|
VoidResult,
|
||||||
|
useContainerRaw,
|
||||||
} from '../core/_internal';
|
} 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 { contextArgs, createDispatcher, dispatchMessage } from './dispatchers';
|
||||||
import { ObservableInput, pipe } from 'rxjs';
|
import { ObservableInput, pipe } from 'rxjs';
|
||||||
import { SernEmitter } from '../core';
|
import { SernEmitter } from '../core';
|
||||||
|
|||||||
@@ -50,4 +50,7 @@ export {
|
|||||||
CommandExecutable,
|
CommandExecutable,
|
||||||
} from './core/modules';
|
} from './core/modules';
|
||||||
|
|
||||||
|
export {
|
||||||
|
useContainerRaw
|
||||||
|
} from './core/_internal'
|
||||||
export { controller } from './sern';
|
export { controller } from './sern';
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { CoreContainer } from '../../src/core/ioc/container';
|
import { CoreContainer } from '../../src/core/ioc/container';
|
||||||
import { CoreDependencies } from '../../src/core/ioc';
|
|
||||||
import { EventEmitter } from 'events';
|
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', () => {
|
describe('ioc container', () => {
|
||||||
let container: CoreContainer<{}>;
|
let container: CoreContainer<{}> = new CoreContainer();
|
||||||
let initDependency: Logging & Init;
|
let dependency: Logging & Init & Disposable;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initDependency = {
|
dependency = {
|
||||||
init: vi.fn(),
|
init: vi.fn(),
|
||||||
error(): void {},
|
error(): void {},
|
||||||
warning(): void {},
|
warning(): void {},
|
||||||
info(): void {},
|
info(): void {},
|
||||||
debug(): void {},
|
debug(): void {},
|
||||||
|
dispose: vi.fn()
|
||||||
};
|
};
|
||||||
container = new CoreContainer();
|
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()', () => {
|
it('should be ready after calling container.ready()', () => {
|
||||||
container.ready();
|
container.ready();
|
||||||
expect(container.isReady()).toBe(true);
|
expect(container.isReady()).toBe(true);
|
||||||
@@ -39,14 +52,35 @@ describe('ioc container', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should init modules', () => {
|
it('should init modules', () => {
|
||||||
container.upsert({ '@sern/logger': initDependency });
|
container.upsert({ '@sern/logger': dependency });
|
||||||
container.ready();
|
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', () => {
|
it('should not lazy module', () => {
|
||||||
container.upsert({ '@sern/logger': () => initDependency });
|
container.upsert({ '@sern/logger': () => dependency });
|
||||||
container.ready();
|
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`);
|
.map((path, i) => `${path}/${modules[i]}.js`);
|
||||||
|
|
||||||
const metadata: CommandMeta[] = modules.map((cm, i) => ({
|
const metadata: CommandMeta[] = modules.map((cm, i) => ({
|
||||||
id: Id.create(cm.name, cm.type),
|
id: Id.create(cm.name!, cm.type),
|
||||||
isClass: false,
|
isClass: false,
|
||||||
fullPath: `${paths[i]}/${cm.name}.js`,
|
fullPath: `${paths[i]}/${cm.name}.js`,
|
||||||
}));
|
}));
|
||||||
const moduleManager = container.get('@sern/modules');
|
const moduleManager = container.get('@sern/modules');
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const m of modules) {
|
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]);
|
moduleManager.setMetadata(m, metadata[i]);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const shared = {
|
|||||||
external: ['discord.js', 'iti'],
|
external: ['discord.js', 'iti'],
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
clean: true,
|
clean: true,
|
||||||
sourcemap: false,
|
sourcemap: true,
|
||||||
treeshake: {
|
treeshake: {
|
||||||
moduleSideEffects: false,
|
moduleSideEffects: false,
|
||||||
correctVarValueBeforeDeclaration: true, //need this to treeshake esm discord.js empty import
|
correctVarValueBeforeDeclaration: true, //need this to treeshake esm discord.js empty import
|
||||||
@@ -17,33 +17,8 @@ export default defineConfig([
|
|||||||
target: 'node18',
|
target: 'node18',
|
||||||
tsconfig: './tsconfig.json',
|
tsconfig: './tsconfig.json',
|
||||||
outDir: './dist',
|
outDir: './dist',
|
||||||
splitting: true,
|
minify: false,
|
||||||
dts: true,
|
dts: true,
|
||||||
...shared,
|
...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