From 26ccd118ff8cbcde94158a4d09fc0df18da9f254 Mon Sep 17 00:00:00 2001
From: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
Date: Thu, 17 Aug 2023 12:51:24 -0500
Subject: [PATCH] feat: dispose hooks (deprecate useContainerRaw) (#323)
* feat: dispose hooks
* build: unminify, add source map, deprecate useContainerRaw
* fix regression of context and fix tsup build
---
.gitignore | 2 +
dependency-graph.svg | 1484 --------------------------
package.json | 2 +-
src/core/_internal.ts | 1 +
src/core/contracts/disposable.ts | 9 +
src/core/contracts/index.ts | 1 +
src/core/ioc/base.ts | 2 +
src/core/ioc/container.ts | 59 +-
src/core/ioc/dependency-injection.ts | 2 +-
src/core/ioc/hooks.ts | 40 +
src/core/ioc/index.ts | 2 +-
src/core/structures/context.ts | 5 +-
src/handlers/event-utils.ts | 3 +-
src/index.ts | 3 +
test/core/ioc.test.ts | 54 +-
test/core/services.test.ts | 4 +-
tsup.config.js | 31 +-
17 files changed, 142 insertions(+), 1562 deletions(-)
delete mode 100644 dependency-graph.svg
create mode 100644 src/core/contracts/disposable.ts
create mode 100644 src/core/ioc/hooks.ts
diff --git a/.gitignore b/.gitignore
index b6a7e19a..87a9da11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,3 +95,5 @@ dist
.yalc
yalc.lock
+
+*.svg
diff --git a/dependency-graph.svg b/dependency-graph.svg
deleted file mode 100644
index 2ae619b0..00000000
--- a/dependency-graph.svg
+++ /dev/null
@@ -1,1484 +0,0 @@
-
-
-
-
-
diff --git a/package.json b/package.json
index 58598c58..dddc7022 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/core/_internal.ts b/src/core/_internal.ts
index 2899afc7..026d036f 100644
--- a/src/core/_internal.ts
+++ b/src/core/_internal.ts
@@ -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'
diff --git a/src/core/contracts/disposable.ts b/src/core/contracts/disposable.ts
new file mode 100644
index 00000000..42b0142c
--- /dev/null
+++ b/src/core/contracts/disposable.ts
@@ -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;
+}
diff --git a/src/core/contracts/index.ts b/src/core/contracts/index.ts
index f19756e0..7d123b1e 100644
--- a/src/core/contracts/index.ts
+++ b/src/core/contracts/index.ts
@@ -4,3 +4,4 @@ export * from './module-manager';
export * from './module-store';
export * from './init';
export * from './emitter';
+export * from './disposable'
diff --git a/src/core/ioc/base.ts b/src/core/ioc/base.ts
index efb25644..ca82206d 100644
--- a/src/core/ioc/base.ts
+++ b/src/core/ioc/base.ts
@@ -7,8 +7,10 @@ import { CoreContainer } from './container';
let containerSubject: CoreContainer>;
/**
+ * @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(
diff --git a/src/core/ioc/container.ts b/src/core/ioc/container.ts
index 98bbd617..0302afc1 100644
--- a/src/core/ioc/container.ts
+++ b/src/core/ioc/container.ts
@@ -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> extends Container {
- private ready$ = new Subject();
- private beenCalled = new Set();
+ private ready$ = new Subject();
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> extends Container 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();
}
}
diff --git a/src/core/ioc/dependency-injection.ts b/src/core/ioc/dependency-injection.ts
index 7a67de5c..c8046267 100644
--- a/src/core/ioc/dependency-injection.ts
+++ b/src/core/ioc/dependency-injection.ts
@@ -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';
diff --git a/src/core/ioc/hooks.ts b/src/core/ioc/hooks.ts
new file mode 100644
index 00000000..a1db322e
--- /dev/null
+++ b/src/core/ioc/hooks.ts
@@ -0,0 +1,40 @@
+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 436b54e7..e89f8b6f 100644
--- a/src/core/ioc/index.ts
+++ b/src/core/ioc/index.ts
@@ -1,2 +1,2 @@
-export { useContainerRaw, makeDependencies } from './base';
+export { makeDependencies } from './base';
export { Service, Services, single, transient } from './dependency-injection';
diff --git a/src/core/structures/context.ts b/src/core/structures/context.ts
index a009f901..d933fdd1 100644
--- a/src/core/structures/context.ts
+++ b/src/core/structures/context.ts
@@ -120,5 +120,8 @@ export class Context extends CoreContext {
}
function safeUnwrap(res: Result) {
- return res.unwrap()
+ if(res.isOk()) {
+ return res.expect("Tried unwrapping message field: " + res)
+ }
+ return res.expectErr("Tried unwrapping interaction field" + res)
}
diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts
index 8e5c8cf3..e61cc2ed 100644
--- a/src/handlers/event-utils.ts
+++ b/src/handlers/event-utils.ts
@@ -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';
diff --git a/src/index.ts b/src/index.ts
index c9ffad7c..62bcbad8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -50,4 +50,7 @@ export {
CommandExecutable,
} from './core/modules';
+export {
+ useContainerRaw
+} from './core/_internal'
export { controller } from './sern';
diff --git a/test/core/ioc.test.ts b/test/core/ioc.test.ts
index 94134442..48f4d8ad 100644
--- a/test/core/ioc.test.ts
+++ b/test/core/ioc.test.ts
@@ -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(dependency.init).to.toHaveBeenCalledOnce();
+ });
+ it('should dispose modules', async () => {
+
+ container.upsert({ '@sern/logger': dependency })
+
container.ready();
- expect(initDependency.init).to.toHaveBeenCalledOnce();
+ // 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);
});
});
diff --git a/test/core/services.test.ts b/test/core/services.test.ts
index c8205ab8..fea60460 100644
--- a/test/core/services.test.ts
+++ b/test/core/services.test.ts
@@ -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++;
}
diff --git a/tsup.config.js b/tsup.config.js
index c9c1a1af..812c2a60 100644
--- a/tsup.config.js
+++ b/tsup.config.js
@@ -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',
- // },
-]);
+ ]);