chore: use injeca
This commit is contained in:
@@ -44,7 +44,6 @@
|
||||
"@proj-airi/font-cjkfonts-allseto": "workspace:^",
|
||||
"@proj-airi/font-xiaolai": "workspace:^",
|
||||
"@proj-airi/i18n": "workspace:^",
|
||||
"@proj-airi/injecta": "workspace:*",
|
||||
"@proj-airi/server-runtime": "workspace:^",
|
||||
"@proj-airi/server-sdk": "workspace:*",
|
||||
"@proj-airi/stage-ui": "workspace:^",
|
||||
@@ -81,6 +80,7 @@
|
||||
"electron-click-drag-plugin": "^2.0.2",
|
||||
"es-toolkit": "^1.41.0",
|
||||
"h3": "2.0.1-rc.5",
|
||||
"injeca": "^0.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"localforage": "^1.10.0",
|
||||
"mediabunny": "^1.24.2",
|
||||
|
||||
@@ -6,9 +6,9 @@ import { platform } from 'node:process'
|
||||
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg'
|
||||
import { createLoggLogger, injecta } from '@proj-airi/injecta'
|
||||
import { app, ipcMain, Menu, nativeImage, Tray } from 'electron'
|
||||
import { noop, once } from 'es-toolkit'
|
||||
import { createLoggLogger, injeca } from 'injeca'
|
||||
import { isLinux, isMacOS } from 'std-env'
|
||||
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
@@ -98,34 +98,34 @@ function setupTray(params: {
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
injecta.setLogger(createLoggLogger(useLogg('injecta').useGlobalConfig()))
|
||||
injeca.setLogger(createLoggLogger(useLogg('injeca').useGlobalConfig()))
|
||||
|
||||
const channelServerModule = injecta.provide('modules:channel-server', async () => setupChannelServer())
|
||||
const chatWindow = injecta.provide('windows:chat', { build: () => setupChatWindowReusableFunc() })
|
||||
const widgetsManager = injecta.provide('windows:widgets', { build: () => setupWidgetsWindowManager() })
|
||||
const channelServerModule = injeca.provide('modules:channel-server', async () => setupChannelServer())
|
||||
const chatWindow = injeca.provide('windows:chat', { build: () => setupChatWindowReusableFunc() })
|
||||
const widgetsManager = injeca.provide('windows:widgets', { build: () => setupWidgetsWindowManager() })
|
||||
|
||||
const settingsWindow = injecta.provide('windows:settings', {
|
||||
const settingsWindow = injeca.provide('windows:settings', {
|
||||
dependsOn: { widgetsManager },
|
||||
build: ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn),
|
||||
})
|
||||
const mainWindow = injecta.provide('windows:main', {
|
||||
const mainWindow = injeca.provide('windows:main', {
|
||||
dependsOn: { settingsWindow, chatWindow, widgetsManager },
|
||||
build: async ({ dependsOn }) => setupMainWindow(dependsOn),
|
||||
})
|
||||
const captionWindow = injecta.provide('windows:caption', {
|
||||
const captionWindow = injeca.provide('windows:caption', {
|
||||
dependsOn: { mainWindow },
|
||||
build: async ({ dependsOn }) => setupCaptionWindowManager(dependsOn),
|
||||
})
|
||||
const tray = injecta.provide('app:tray', {
|
||||
const tray = injeca.provide('app:tray', {
|
||||
dependsOn: { mainWindow, settingsWindow, captionWindow, widgetsWindow: widgetsManager },
|
||||
build: async ({ dependsOn }) => setupTray(dependsOn),
|
||||
})
|
||||
injecta.invoke({
|
||||
injeca.invoke({
|
||||
dependsOn: { mainWindow, tray, channelServerModule },
|
||||
callback: noop,
|
||||
})
|
||||
|
||||
injecta.start().catch(err => console.error(err))
|
||||
injeca.start().catch(err => console.error(err))
|
||||
|
||||
// Lifecycle
|
||||
emitAppReady()
|
||||
@@ -155,5 +155,5 @@ app.on('window-all-closed', () => {
|
||||
// Clean up server and intervals when app quits
|
||||
app.on('before-quit', async () => {
|
||||
emitAppBeforeQuit()
|
||||
injecta.stop()
|
||||
injeca.stop()
|
||||
})
|
||||
|
||||
@@ -123,6 +123,7 @@ words:
|
||||
- hunyuan
|
||||
- hyoban
|
||||
- iconify
|
||||
- injeca
|
||||
- injecta
|
||||
- intlify
|
||||
- jlumbroso
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@proj-airi/injecta",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "Dependency Injection (DI) utilities",
|
||||
"author": {
|
||||
"name": "Moeru AI Project AIRI Team",
|
||||
"email": "airi@moeru.ai",
|
||||
"url": "https://github.com/moeru-ai"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/moeru-ai/airi.git",
|
||||
"directory": "packages/injecta"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"type": "./dist/index.d.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsdown"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@guiiai/logg": "catalog:",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"nanoid": "^5.1.6"
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
export interface LifecycleAppHooks {
|
||||
onStart: (hook: () => void | Promise<void>) => void
|
||||
onStop: (hook: () => void | Promise<void>) => void
|
||||
}
|
||||
|
||||
export interface Lifecycle {
|
||||
appHooks: LifecycleAppHooks
|
||||
}
|
||||
|
||||
export interface LifecycleTriggerable extends Lifecycle {
|
||||
emitOnStart: () => Promise<void>
|
||||
emitOnStop: () => Promise<void>
|
||||
}
|
||||
|
||||
export function buildLifecycle(): Lifecycle {
|
||||
const onStartHooks: (() => void | Promise<void>)[] = []
|
||||
const onStopHooks: (() => void | Promise<void>)[] = []
|
||||
|
||||
const lifecycle: LifecycleTriggerable = {
|
||||
appHooks: {
|
||||
onStart(hook: () => void | Promise<void>) {
|
||||
if (hook) {
|
||||
onStartHooks.push(hook)
|
||||
}
|
||||
},
|
||||
onStop(hook: () => void | Promise<void>) {
|
||||
if (hook) {
|
||||
onStopHooks.push(hook)
|
||||
}
|
||||
},
|
||||
},
|
||||
emitOnStart: async () => {
|
||||
for (const hook of onStartHooks) {
|
||||
await hook()
|
||||
}
|
||||
},
|
||||
emitOnStop: async () => {
|
||||
for (const hook of onStopHooks) {
|
||||
await hook()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return lifecycle
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import type { Lifecycle } from './builtin'
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { invoke, provide, resetContainer, start, stop } from './global'
|
||||
import { lifecycle } from './scoped'
|
||||
|
||||
beforeEach(() => {
|
||||
resetContainer()
|
||||
})
|
||||
|
||||
describe('workflow with named', () => {
|
||||
it('should work with named pattern', async () => {
|
||||
interface Database {
|
||||
connect: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
const databaseConnectSpy = vi.fn()
|
||||
const databaseCloseSpy = vi.fn()
|
||||
|
||||
function createDatabase(params: { lifecycle?: Lifecycle }): Database {
|
||||
const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await database.close())
|
||||
return database
|
||||
}
|
||||
|
||||
interface WebSocketServer {
|
||||
start: () => Promise<void>
|
||||
stop: () => Promise<void>
|
||||
}
|
||||
|
||||
const webSocketServerStartSpy = vi.fn()
|
||||
const webSocketServerStopSpy = vi.fn()
|
||||
|
||||
async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise<WebSocketServer> {
|
||||
await params.database.connect()
|
||||
const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await server.stop())
|
||||
return server
|
||||
}
|
||||
|
||||
provide<{ lifecycle: Lifecycle }>('db', {
|
||||
dependsOn: { lifecycle: 'lifecycle' },
|
||||
build: async ({ dependsOn }) => createDatabase({ lifecycle: dependsOn.lifecycle }),
|
||||
})
|
||||
|
||||
provide<{ database: Database, lifecycle: Lifecycle }>('ws', {
|
||||
dependsOn: { database: 'db', lifecycle: 'lifecycle' },
|
||||
build: async ({ dependsOn }) => createWebSocketServer({ database: dependsOn.database, lifecycle: dependsOn.lifecycle }),
|
||||
})
|
||||
|
||||
invoke<{ webSocketServer: WebSocketServer }>({
|
||||
dependsOn: { webSocketServer: 'ws' },
|
||||
callback: async ({ webSocketServer }) => await webSocketServer.start(),
|
||||
})
|
||||
|
||||
await start()
|
||||
await stop()
|
||||
|
||||
// eslint-disable-next-line no-lone-blocks
|
||||
{
|
||||
expect(databaseConnectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(databaseCloseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('workflow with typed', () => {
|
||||
it('should work with typed named pattern', async () => {
|
||||
interface Database {
|
||||
connect: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
const databaseConnectSpy = vi.fn()
|
||||
const databaseCloseSpy = vi.fn()
|
||||
|
||||
function createDatabase(params: { lifecycle?: Lifecycle }): Database {
|
||||
const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await database.close())
|
||||
return database
|
||||
}
|
||||
|
||||
interface WebSocketServer {
|
||||
start: () => Promise<void>
|
||||
stop: () => Promise<void>
|
||||
}
|
||||
|
||||
const webSocketServerStartSpy = vi.fn()
|
||||
const webSocketServerStopSpy = vi.fn()
|
||||
|
||||
async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise<WebSocketServer> {
|
||||
await params.database.connect()
|
||||
const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await server.stop())
|
||||
return server
|
||||
}
|
||||
|
||||
const database = provide('db', {
|
||||
dependsOn: { lifecycle },
|
||||
build: async ({ dependsOn }) => createDatabase(dependsOn),
|
||||
})
|
||||
|
||||
const webSocketServer = provide('ws', {
|
||||
dependsOn: { database, lifecycle },
|
||||
build: async ({ dependsOn }) => createWebSocketServer(dependsOn),
|
||||
})
|
||||
|
||||
invoke({
|
||||
dependsOn: { webSocketServer },
|
||||
callback: async ({ webSocketServer }) => await webSocketServer.start(),
|
||||
})
|
||||
|
||||
await start()
|
||||
await stop()
|
||||
|
||||
// eslint-disable-next-line no-lone-blocks
|
||||
{
|
||||
expect(databaseConnectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(databaseCloseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('workflow with auto name', () => {
|
||||
it('should work with auto name pattern', async () => {
|
||||
interface Database {
|
||||
connect: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
const databaseConnectSpy = vi.fn()
|
||||
const databaseCloseSpy = vi.fn()
|
||||
|
||||
function createDatabase(params: { lifecycle?: Lifecycle }): Database {
|
||||
const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await database.close())
|
||||
return database
|
||||
}
|
||||
|
||||
interface WebSocketServer {
|
||||
start: () => Promise<void>
|
||||
stop: () => Promise<void>
|
||||
}
|
||||
|
||||
const webSocketServerStartSpy = vi.fn()
|
||||
const webSocketServerStopSpy = vi.fn()
|
||||
|
||||
async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise<WebSocketServer> {
|
||||
await params.database.connect()
|
||||
const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await server.stop())
|
||||
return server
|
||||
}
|
||||
|
||||
const database = provide({
|
||||
dependsOn: { lifecycle },
|
||||
build: async ({ dependsOn }) => createDatabase(dependsOn),
|
||||
})
|
||||
|
||||
const webSocketServer = provide({
|
||||
dependsOn: { database, lifecycle },
|
||||
build: async ({ dependsOn }) => createWebSocketServer(dependsOn),
|
||||
})
|
||||
|
||||
invoke({
|
||||
dependsOn: { webSocketServer },
|
||||
callback: async ({ webSocketServer }) => await webSocketServer.start(),
|
||||
})
|
||||
|
||||
await start()
|
||||
await stop()
|
||||
|
||||
// eslint-disable-next-line no-lone-blocks
|
||||
{
|
||||
expect(databaseConnectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(databaseCloseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { DependencyMap, InvokeOption, InvokeOptionWithKeys, Logger, ProvideOption, ProvideOptionWithKeys, ResolveDependencyDeclaration } from '.'
|
||||
import type { ProvidedKey } from './scoped'
|
||||
|
||||
import { createContainer, provide as indexProvide, start as indexStart, stop as indexStop } from '.'
|
||||
|
||||
let globalContainer = createContainer()
|
||||
|
||||
export function setLogger(logger: Logger) {
|
||||
globalContainer.logger = logger
|
||||
}
|
||||
|
||||
export function resetContainer() {
|
||||
globalContainer = createContainer()
|
||||
}
|
||||
|
||||
export function provide<T, Key extends string, Deps extends Record<string, string | ProvidedKey<any, any, any>>>(name: Key, option: ProvideOptionWithKeys<T, Deps>): ProvidedKey<Key, T, ResolveDependencyDeclaration<Deps>>
|
||||
export function provide<D extends DependencyMap | undefined, T = any, Key extends string = string>(name: Key, option: ProvideOption<T, D>): ProvidedKey<Key, T, D>
|
||||
export function provide<T, Key extends string, Deps extends Record<string, string | ProvidedKey<any, any, any>>>(option: ProvideOptionWithKeys<T, Deps>): ProvidedKey<Key, T, ResolveDependencyDeclaration<Deps>>
|
||||
export function provide<D extends DependencyMap | undefined, T = any, Key extends string = string>(option: ProvideOption<T, D>): ProvidedKey<Key, T, D>
|
||||
export function provide<D extends DependencyMap | undefined, T = any, Key extends string = string>(nameOrOption: Key | ProvideOption<T, D> | ProvideOptionWithKeys<T, any>, option?: ProvideOption<T, D> | ProvideOptionWithKeys<T, any>): ProvidedKey<Key, T, D> {
|
||||
if (option != null && typeof option === 'function') {
|
||||
return indexProvide(globalContainer, nameOrOption as any, { build: option, autoNameStackIndex: 2 } as any)
|
||||
}
|
||||
|
||||
return indexProvide(globalContainer, nameOrOption as any, { ...option, autoNameStackIndex: 2 } as any)
|
||||
}
|
||||
|
||||
export function invoke<Deps extends Record<string, string | ProvidedKey<any, any, any>>>(option: InvokeOptionWithKeys<Deps>): void
|
||||
export function invoke<D extends DependencyMap>(option: InvokeOption<D>): void
|
||||
export function invoke<D extends DependencyMap>(option: InvokeOption<D> | InvokeOptionWithKeys<any>): void {
|
||||
if (typeof option === 'function') {
|
||||
globalContainer.invocations.push({ callback: option } as any)
|
||||
}
|
||||
else {
|
||||
globalContainer.invocations.push(option as any)
|
||||
}
|
||||
}
|
||||
|
||||
export function start(): Promise<void> {
|
||||
return indexStart(globalContainer)
|
||||
}
|
||||
|
||||
export function stop(): Promise<void> {
|
||||
return indexStop(globalContainer)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import {
|
||||
invoke as globalInvoke,
|
||||
provide as globalProvide,
|
||||
setLogger as globalSetLogger,
|
||||
start as globalStart,
|
||||
stop as globalStop,
|
||||
} from './global'
|
||||
|
||||
export type { Lifecycle } from './builtin'
|
||||
|
||||
export type { Logger, LoggerOptions } from './logger'
|
||||
|
||||
export {
|
||||
createDefaultLogger,
|
||||
createLoggLogger,
|
||||
createNoopLogger,
|
||||
} from './logger'
|
||||
|
||||
export {
|
||||
createContainer,
|
||||
invoke,
|
||||
lifecycle,
|
||||
provide,
|
||||
start,
|
||||
stop,
|
||||
} from './scoped'
|
||||
export type {
|
||||
BuildContext,
|
||||
Container,
|
||||
DependencyMap,
|
||||
InvokeOption,
|
||||
InvokeOptionFunc,
|
||||
InvokeOptionObject,
|
||||
InvokeOptionObjectWithKeys,
|
||||
InvokeOptionWithKeys,
|
||||
ProvidedKey,
|
||||
ProvideOption,
|
||||
ProvideOptionFunc,
|
||||
ProvideOptionObject,
|
||||
ProvideOptionObjectWithKeys,
|
||||
ProvideOptionWithKeys,
|
||||
ResolveDependencyDeclaration,
|
||||
} from './scoped'
|
||||
|
||||
export const injecta = {
|
||||
provide: globalProvide,
|
||||
invoke: globalInvoke,
|
||||
start: globalStart,
|
||||
stop: globalStop,
|
||||
setLogger: globalSetLogger,
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import type { Logg as LoggLogger } from '@guiiai/logg'
|
||||
|
||||
import { name } from '../package.json'
|
||||
|
||||
export interface Logger {
|
||||
provide: (name: string, dependencies: string[]) => void
|
||||
invoke: (dependencies: string[]) => void
|
||||
beforeRun: (name: string) => void
|
||||
run: (name: string, duration: number) => void
|
||||
hookOnStart: (name: string) => void
|
||||
hookOnStartComplete: (name: string, duration: number) => void
|
||||
hookOnStop: (name: string) => void
|
||||
hookOnStopComplete: (name: string, duration: number) => void
|
||||
running: () => void
|
||||
}
|
||||
|
||||
export interface LoggerOptions {
|
||||
enabled?: boolean
|
||||
logger?: Logger
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1) {
|
||||
return `${(ms * 1000).toFixed(3)}µs`
|
||||
}
|
||||
if (ms < 1000) {
|
||||
return `${ms.toFixed(3)}ms`
|
||||
}
|
||||
return `${(ms / 1000).toFixed(3)}s`
|
||||
}
|
||||
|
||||
export function createDefaultLogger(): Logger {
|
||||
return {
|
||||
provide: (name: string, dependencies: string[]) => {
|
||||
const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(', ')})` : ''
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}] PROVIDE ${name}${depsStr}`)
|
||||
},
|
||||
|
||||
invoke: (dependencies: string[]) => {
|
||||
const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(', ')})` : ''
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}] INVOKE${depsStr}`)
|
||||
},
|
||||
|
||||
beforeRun: (name: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}] BEFORE RUN provide: ${name}()`)
|
||||
},
|
||||
|
||||
run: (name: string, duration: number) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}] RUN provide: ${name}() in ${formatDuration(duration)}`)
|
||||
},
|
||||
|
||||
hookOnStart: (name: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}] HOOK OnStart ${name}() executing`)
|
||||
},
|
||||
|
||||
hookOnStartComplete: (name: string, duration: number) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}] HOOK OnStart ${name}() ran successfully in ${formatDuration(duration)}`)
|
||||
},
|
||||
|
||||
hookOnStop: (name: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}] HOOK OnStop ${name}() executing`)
|
||||
},
|
||||
|
||||
hookOnStopComplete: (name: string, duration: number) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}] HOOK OnStop ${name}() ran successfully in ${formatDuration(duration)}`)
|
||||
},
|
||||
|
||||
running: () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[${name}] RUNNING`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createLoggLogger(logg: LoggLogger): Logger {
|
||||
return {
|
||||
provide: (name: string, dependencies: string[]) => {
|
||||
const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(', ')})` : ''
|
||||
logg.log(`PROVIDE ${name}${depsStr}`)
|
||||
},
|
||||
|
||||
invoke: (dependencies: string[]) => {
|
||||
const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(', ')})` : ''
|
||||
logg.log(`INVOKE${depsStr}`)
|
||||
},
|
||||
|
||||
beforeRun: (name: string) => {
|
||||
logg.log(`BEFORE RUN provide: ${name}()`)
|
||||
},
|
||||
|
||||
run: (name: string, duration: number) => {
|
||||
logg.log(`RUN provide: ${name}() in ${formatDuration(duration)}`)
|
||||
},
|
||||
|
||||
hookOnStart: (name: string) => {
|
||||
logg.log(`HOOK OnStart ${name}() executing`)
|
||||
},
|
||||
|
||||
hookOnStartComplete: (name: string, duration: number) => {
|
||||
logg.log(`HOOK OnStart ${name}() ran successfully in ${formatDuration(duration)}`)
|
||||
},
|
||||
|
||||
hookOnStop: (name: string) => {
|
||||
logg.log(`HOOK OnStop ${name}() executing`)
|
||||
},
|
||||
|
||||
hookOnStopComplete: (name: string, duration: number) => {
|
||||
logg.log(`HOOK OnStop ${name}() ran successfully in ${formatDuration(duration)}`)
|
||||
},
|
||||
|
||||
running: () => {
|
||||
logg.log(`RUNNING`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createNoopLogger(): Logger {
|
||||
return {
|
||||
provide: () => {},
|
||||
invoke: () => {},
|
||||
beforeRun: () => {},
|
||||
run: () => {},
|
||||
hookOnStart: () => {},
|
||||
hookOnStartComplete: () => {},
|
||||
hookOnStop: () => {},
|
||||
hookOnStopComplete: () => {},
|
||||
running: () => {},
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import type { Lifecycle } from './builtin'
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createContainer, invoke, lifecycle, normalizeName, normalizeProvideOption, provide, start, stop } from './scoped'
|
||||
|
||||
describe('normalizeName', () => {
|
||||
it('should normalize names correctly', () => {
|
||||
expect(normalizeName('simpleName')).toBe('simpleName')
|
||||
expect(normalizeName({ key: 'objectName' })).toBe('objectName')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeProvideOption', () => {
|
||||
it('should normalize provide options correctly', () => {
|
||||
expect(() => normalizeProvideOption('simpleName')).toThrowError('When using provide(...) as named callback, the second argument must be either a valid ProvideOptionObject<T, D> or a ProvideOptionFunc<T, D>.')
|
||||
expect(() => normalizeProvideOption({ key: 'objectName' })).toThrowError('When using provide(...) as typed ProvideOptionWithKeys<T, D, Key> callback, the second argument must be either a valid ProvideOptionObject<T, D> or a ProvideOptionFunc<T, D>.')
|
||||
|
||||
const namedOption = normalizeProvideOption('name', () => {})
|
||||
expect(namedOption).toBeTypeOf('object')
|
||||
expect(namedOption).toHaveProperty('build')
|
||||
expect(() => namedOption.build({} as any)).not.toThrow()
|
||||
|
||||
const typedOption = normalizeProvideOption({ key: 'name' }, () => {})
|
||||
expect(typedOption).toBeTypeOf('object')
|
||||
expect(typedOption).toHaveProperty('build')
|
||||
expect(() => typedOption.build({} as any)).not.toThrow()
|
||||
|
||||
const autoNameOption = normalizeProvideOption(() => {})
|
||||
expect(autoNameOption).toBeTypeOf('object')
|
||||
expect(autoNameOption).toHaveProperty('build')
|
||||
expect(() => autoNameOption.build({} as any)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('workflow with named', () => {
|
||||
it('should work with named pattern', async () => {
|
||||
interface Database {
|
||||
connect: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
const databaseConnectSpy = vi.fn()
|
||||
const databaseCloseSpy = vi.fn()
|
||||
|
||||
function createDatabase(params: { lifecycle?: Lifecycle }): Database {
|
||||
const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await database.close())
|
||||
return database
|
||||
}
|
||||
|
||||
interface WebSocketServer {
|
||||
start: () => Promise<void>
|
||||
stop: () => Promise<void>
|
||||
}
|
||||
|
||||
const webSocketServerStartSpy = vi.fn()
|
||||
const webSocketServerStopSpy = vi.fn()
|
||||
|
||||
async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise<WebSocketServer> {
|
||||
await params.database.connect()
|
||||
const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await server.stop())
|
||||
return server
|
||||
}
|
||||
|
||||
const app = createContainer()
|
||||
|
||||
provide<{ lifecycle: Lifecycle }>(app, 'db', {
|
||||
dependsOn: { lifecycle: 'lifecycle' },
|
||||
build: async ({ dependsOn }) => createDatabase({ lifecycle: dependsOn.lifecycle }),
|
||||
})
|
||||
|
||||
provide<{ database: Database, lifecycle: Lifecycle }>(app, 'ws', {
|
||||
dependsOn: { database: 'db', lifecycle: 'lifecycle' },
|
||||
build: async ({ dependsOn }) => createWebSocketServer({ database: dependsOn.database, lifecycle: dependsOn.lifecycle }),
|
||||
})
|
||||
|
||||
invoke<{ webSocketServer: WebSocketServer }>(app, {
|
||||
dependsOn: { webSocketServer: 'ws' },
|
||||
callback: async ({ webSocketServer }) => await webSocketServer.start(),
|
||||
})
|
||||
|
||||
await start(app)
|
||||
await stop(app)
|
||||
|
||||
// eslint-disable-next-line no-lone-blocks
|
||||
{
|
||||
expect(databaseConnectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(databaseCloseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('workflow with typed', () => {
|
||||
it('should work with typed named pattern', async () => {
|
||||
interface Database {
|
||||
connect: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
const databaseConnectSpy = vi.fn()
|
||||
const databaseCloseSpy = vi.fn()
|
||||
|
||||
function createDatabase(params: { lifecycle?: Lifecycle }): Database {
|
||||
const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await database.close())
|
||||
return database
|
||||
}
|
||||
|
||||
interface WebSocketServer {
|
||||
start: () => Promise<void>
|
||||
stop: () => Promise<void>
|
||||
}
|
||||
|
||||
const webSocketServerStartSpy = vi.fn()
|
||||
const webSocketServerStopSpy = vi.fn()
|
||||
|
||||
async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise<WebSocketServer> {
|
||||
await params.database.connect()
|
||||
const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await server.stop())
|
||||
return server
|
||||
}
|
||||
|
||||
const app = createContainer()
|
||||
|
||||
const database = provide(app, 'db', {
|
||||
dependsOn: { lifecycle },
|
||||
build: async ({ dependsOn }) => createDatabase(dependsOn),
|
||||
})
|
||||
|
||||
const webSocketServer = provide(app, 'ws', {
|
||||
dependsOn: { database, lifecycle },
|
||||
build: async ({ dependsOn }) => createWebSocketServer(dependsOn),
|
||||
})
|
||||
|
||||
invoke(app, {
|
||||
dependsOn: { webSocketServer },
|
||||
callback: async ({ webSocketServer }) => await webSocketServer.start(),
|
||||
})
|
||||
|
||||
await start(app)
|
||||
await stop(app)
|
||||
|
||||
// eslint-disable-next-line no-lone-blocks
|
||||
{
|
||||
expect(databaseConnectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(databaseCloseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('workflow with auto name', () => {
|
||||
it('should work with auto name pattern', async () => {
|
||||
interface Database {
|
||||
connect: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
const databaseConnectSpy = vi.fn()
|
||||
const databaseCloseSpy = vi.fn()
|
||||
|
||||
function createDatabase(params: { lifecycle?: Lifecycle }): Database {
|
||||
const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await database.close())
|
||||
return database
|
||||
}
|
||||
|
||||
interface WebSocketServer {
|
||||
start: () => Promise<void>
|
||||
stop: () => Promise<void>
|
||||
}
|
||||
|
||||
const webSocketServerStartSpy = vi.fn()
|
||||
const webSocketServerStopSpy = vi.fn()
|
||||
|
||||
async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise<WebSocketServer> {
|
||||
await params.database.connect()
|
||||
const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy }
|
||||
params.lifecycle?.appHooks.onStop(async () => await server.stop())
|
||||
return server
|
||||
}
|
||||
|
||||
const app = createContainer()
|
||||
|
||||
const database = provide(app, {
|
||||
dependsOn: { lifecycle },
|
||||
build: async ({ dependsOn }) => createDatabase(dependsOn),
|
||||
})
|
||||
|
||||
const webSocketServer = provide(app, {
|
||||
dependsOn: { database, lifecycle },
|
||||
build: async ({ dependsOn }) => createWebSocketServer(dependsOn),
|
||||
})
|
||||
|
||||
invoke(app, {
|
||||
dependsOn: { webSocketServer },
|
||||
callback: async ({ webSocketServer }) => await webSocketServer.start(),
|
||||
})
|
||||
|
||||
await start(app)
|
||||
await stop(app)
|
||||
|
||||
// eslint-disable-next-line no-lone-blocks
|
||||
{
|
||||
expect(databaseConnectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(databaseCloseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1)
|
||||
expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,296 +0,0 @@
|
||||
import type { Lifecycle, LifecycleTriggerable } from './builtin'
|
||||
import type { Logger, LoggerOptions } from './logger'
|
||||
|
||||
import ErrorStackParser from 'error-stack-parser'
|
||||
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
import { createDefaultLogger, createNoopLogger } from './logger'
|
||||
|
||||
export type DependencyMap = Record<string, any>
|
||||
|
||||
export interface Container {
|
||||
providers: Map<string, ProvideOptionObject<any, any>>
|
||||
instances: Map<string, any>
|
||||
lifecycleHooks: Map<string, LifecycleTriggerable>
|
||||
dependencyGraph: Map<string, string[]>
|
||||
invocations: InvokeOptionObject<any>[]
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export type BuildContext<D extends DependencyMap | undefined = undefined> = {
|
||||
container: Container
|
||||
name: string
|
||||
} & (D extends undefined ? { dependsOn?: unknown } : { dependsOn: D })
|
||||
|
||||
export type ResolveDependencyDeclaration<Deps extends Record<string, string | ProvidedKey<any, any, any>>> = {
|
||||
[K in keyof Deps]: Deps[K] extends ProvidedKey<any, infer T, any> ? T : any
|
||||
}
|
||||
|
||||
export type ProvideOptionObject<T, D extends DependencyMap | undefined = undefined> = { build: (context: BuildContext<D>) => T | Promise<T> } & (D extends undefined ? { dependsOn?: Record<string, never> } : { dependsOn: { [K in keyof D]: string } })
|
||||
export type ProvideOptionFunc<T, D extends DependencyMap | undefined = undefined> = (context: BuildContext<D>) => T | Promise<T>
|
||||
export type ProvideOption<T, D extends DependencyMap | undefined = undefined> = ProvideOptionObject<T, D> | ProvideOptionFunc<T, D>
|
||||
|
||||
export interface ProvideOptionObjectWithKeys<T, Deps extends Record<string, string | ProvidedKey<any, any, any>>> {
|
||||
build: (context: BuildContext<ResolveDependencyDeclaration<Deps>>) => T | Promise<T>
|
||||
dependsOn: Deps
|
||||
}
|
||||
export type ProvideOptionWithKeys<T, Deps extends Record<string, string | ProvidedKey<any, any, any>>> = ProvideOptionObjectWithKeys<T, Deps>
|
||||
|
||||
export type InvokeOptionObject<D extends DependencyMap | undefined = undefined> = { callback: (dependencies: D) => void | Promise<void> } & (D extends undefined ? { dependsOn?: Record<string, never> } : { dependsOn: { [K in keyof D]: string } })
|
||||
export type InvokeOptionFunc<D extends DependencyMap | undefined = undefined> = (dependencies: D) => void | Promise<void>
|
||||
export type InvokeOption<D extends DependencyMap | undefined = undefined> = InvokeOptionObject<D> | InvokeOptionFunc<D>
|
||||
|
||||
export interface InvokeOptionObjectWithKeys<Deps extends Record<string, string | ProvidedKey<any, any, any>>> {
|
||||
callback: (dependencies: ResolveDependencyDeclaration<Deps>) => void | Promise<void>
|
||||
dependsOn: Deps
|
||||
}
|
||||
export type InvokeOptionWithKeys<Deps extends Record<string, string | ProvidedKey<any, any, any>>> = InvokeOptionObjectWithKeys<Deps>
|
||||
|
||||
export const lifecycle: ProvidedKey<'lifecycle', Lifecycle, undefined> = { key: 'lifecycle' }
|
||||
|
||||
export function createContainer(options?: LoggerOptions): Container {
|
||||
const logger = options?.enabled === false
|
||||
? createNoopLogger()
|
||||
: (options?.logger ?? createDefaultLogger())
|
||||
|
||||
return {
|
||||
providers: new Map(),
|
||||
instances: new Map(),
|
||||
lifecycleHooks: new Map(),
|
||||
dependencyGraph: new Map(),
|
||||
invocations: [],
|
||||
logger,
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
export interface ProvidedKey<Key, T, D extends DependencyMap | undefined> {
|
||||
key: Key
|
||||
}
|
||||
|
||||
export function normalizeName(nameOrProvidedKey: string | ProvidedKey<any, any, any>): string {
|
||||
if (typeof nameOrProvidedKey === 'object' && nameOrProvidedKey !== null && 'key' in nameOrProvidedKey) {
|
||||
return (nameOrProvidedKey as ProvidedKey<any, any, any>).key as string
|
||||
}
|
||||
|
||||
return nameOrProvidedKey as string
|
||||
}
|
||||
|
||||
export function normalizeProvideOption<D extends DependencyMap | undefined, T = any, Key extends string = string>(nameOrOption: Key | ProvidedKey<Key, T, D> | ProvideOption<T, D> | ProvideOptionWithKeys<T, any>, option?: (ProvideOption<T, D> | ProvideOptionWithKeys<T, any>) & { autoNameStackIndex?: number }): ProvideOptionObject<T, D> {
|
||||
if (typeof nameOrOption === 'string') {
|
||||
if (option == null || (!('build' in option) && typeof option !== 'function')) {
|
||||
throw new Error('When using provide(...) as named callback, the second argument must be either a valid ProvideOptionObject<T, D> or a ProvideOptionFunc<T, D>.')
|
||||
}
|
||||
if (typeof option === 'function') {
|
||||
return { build: option } as ProvideOptionObject<T, D>
|
||||
}
|
||||
|
||||
return option as ProvideOptionObject<T, D>
|
||||
}
|
||||
if (typeof nameOrOption === 'object' && 'key' in nameOrOption) {
|
||||
if (option == null || (!('build' in option) && typeof option !== 'function')) {
|
||||
throw new Error('When using provide(...) as typed ProvideOptionWithKeys<T, D, Key> callback, the second argument must be either a valid ProvideOptionObject<T, D> or a ProvideOptionFunc<T, D>.')
|
||||
}
|
||||
if (typeof option === 'function') {
|
||||
return { build: option } as ProvideOptionObject<T, D>
|
||||
}
|
||||
|
||||
return option as ProvideOptionObject<T, D>
|
||||
}
|
||||
|
||||
if (typeof nameOrOption === 'function') {
|
||||
return { build: nameOrOption } as ProvideOptionObject<T, D>
|
||||
}
|
||||
|
||||
return nameOrOption as ProvideOptionObject<T, D>
|
||||
}
|
||||
|
||||
export function provide<T, Key extends string, Deps extends Record<string, string | ProvidedKey<any, any, any>>>(container: Container, name: Key, option: ProvideOptionWithKeys<T, Deps>): ProvidedKey<Key, T, ResolveDependencyDeclaration<Deps>>
|
||||
export function provide<T, Key extends string, Deps extends Record<string, string | ProvidedKey<any, any, any>>>(container: Container, option: ProvideOptionWithKeys<T, Deps> & { autoNameStackIndex?: number }): ProvidedKey<Key, T, ResolveDependencyDeclaration<Deps>>
|
||||
export function provide<D extends DependencyMap | undefined, T = any, Key extends string = string>(container: Container, name: Key, option: ProvideOption<T, D>): ProvidedKey<Key, T, D>
|
||||
export function provide<D extends DependencyMap | undefined, T = any, Key extends string = string>(container: Container, option: ProvideOption<T, D> & { autoNameStackIndex?: number }): ProvidedKey<Key, T, D>
|
||||
export function provide<D extends DependencyMap | undefined, T = any, Key extends string = string>(container: Container, nameOrOption: Key | ProvideOption<T, D> | ProvideOptionWithKeys<T, any>, option?: (ProvideOption<T, D> | ProvideOptionWithKeys<T, any>) & { autoNameStackIndex?: number }): ProvidedKey<Key, T, D> {
|
||||
const parentFile = ErrorStackParser.parse(new Error('providing'))[option?.autoNameStackIndex ?? 1]
|
||||
const name = typeof nameOrOption === 'string'
|
||||
? nameOrOption
|
||||
: `${parentFile.fileName ?? `unknown-${nanoid()}`}:${parentFile.lineNumber ?? 0}:${parentFile.columnNumber ?? 0}`
|
||||
|
||||
const providerObject = normalizeProvideOption<D, T, Key>(nameOrOption, option) as unknown as ProvideOptionObject<any, any>
|
||||
if (providerObject.dependsOn) {
|
||||
const resolvedDependsOn: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(providerObject.dependsOn)) {
|
||||
resolvedDependsOn[key] = normalizeName(value)
|
||||
}
|
||||
|
||||
providerObject.dependsOn = resolvedDependsOn
|
||||
}
|
||||
|
||||
container.providers.set(name, providerObject)
|
||||
|
||||
const dependencies = providerObject.dependsOn ? Object.values(providerObject.dependsOn) : []
|
||||
container.logger.provide(name, dependencies)
|
||||
container.dependencyGraph.set(name, dependencies)
|
||||
|
||||
return { key: name } as ProvidedKey<Key, T, D>
|
||||
}
|
||||
|
||||
export function invoke<Deps extends Record<string, string | ProvidedKey<any, any, any>>>(container: Container, option: InvokeOptionWithKeys<Deps>): void
|
||||
export function invoke<D extends DependencyMap>(container: Container, option: InvokeOption<D>): void
|
||||
export function invoke<D extends DependencyMap>(container: Container, option: InvokeOption<D> | InvokeOptionWithKeys<any>): void {
|
||||
const invocationObject = typeof option === 'function'
|
||||
? { callback: option } as InvokeOptionObject<any>
|
||||
: option as InvokeOptionObject<any>
|
||||
|
||||
if (invocationObject.dependsOn) {
|
||||
const resolvedDependsOn: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(invocationObject.dependsOn)) {
|
||||
resolvedDependsOn[key] = normalizeName(value)
|
||||
}
|
||||
|
||||
invocationObject.dependsOn = resolvedDependsOn
|
||||
}
|
||||
|
||||
const dependencies = invocationObject.dependsOn ? Object.values(invocationObject.dependsOn) : []
|
||||
|
||||
container.logger.invoke(dependencies)
|
||||
container.invocations.push(invocationObject)
|
||||
}
|
||||
|
||||
async function resolveInstance<T>(container: Container, name: string): Promise<T> {
|
||||
if (container.instances.has(name)) {
|
||||
return container.instances.get(name) as T
|
||||
}
|
||||
|
||||
const provider = container.providers.get(name)
|
||||
if (!provider) {
|
||||
throw new Error(`No provider found for '${name}'`)
|
||||
}
|
||||
|
||||
const resolvedDependencies: Record<string, any> = {}
|
||||
let serviceLifecycle: any = null
|
||||
|
||||
if (provider.dependsOn) {
|
||||
for (const [key, depName] of Object.entries(provider.dependsOn)) {
|
||||
if (depName === 'lifecycle') {
|
||||
// Create individual lifecycle instance for this service
|
||||
const { buildLifecycle } = await import('./builtin')
|
||||
serviceLifecycle = buildLifecycle()
|
||||
resolvedDependencies[key] = serviceLifecycle
|
||||
|
||||
// Track this service's lifecycle
|
||||
container.lifecycleHooks.set(name, serviceLifecycle)
|
||||
}
|
||||
else {
|
||||
resolvedDependencies[key] = await resolveInstance(container, depName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const context: BuildContext<typeof provider.dependsOn> = {
|
||||
container,
|
||||
dependsOn: resolvedDependencies,
|
||||
name,
|
||||
}
|
||||
|
||||
container.logger.beforeRun(name)
|
||||
|
||||
const startTime = performance.now()
|
||||
const instance = await provider.build(context)
|
||||
const duration = performance.now() - startTime
|
||||
|
||||
container.logger.run(name, duration)
|
||||
|
||||
container.instances.set(name, instance)
|
||||
|
||||
return instance as T
|
||||
}
|
||||
|
||||
function topologicalSort(dependencyGraph: Map<string, string[]>): string[] {
|
||||
const visited = new Set<string>()
|
||||
const visiting = new Set<string>()
|
||||
const result: string[] = []
|
||||
|
||||
function visit(node: string) {
|
||||
if (visiting.has(node)) {
|
||||
throw new Error(`Circular dependency detected involving '${node}'`)
|
||||
}
|
||||
if (visited.has(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
visiting.add(node)
|
||||
const dependencies = dependencyGraph.get(node) || []
|
||||
for (const dep of dependencies) {
|
||||
visit(dep)
|
||||
}
|
||||
visiting.delete(node)
|
||||
visited.add(node)
|
||||
result.push(node)
|
||||
}
|
||||
|
||||
for (const node of dependencyGraph.keys()) {
|
||||
if (!visited.has(node)) {
|
||||
visit(node)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export async function startLifecycleHooks(container: Container): Promise<void> {
|
||||
const sortedServices = topologicalSort(container.dependencyGraph)
|
||||
|
||||
for (const serviceName of sortedServices) {
|
||||
const lifecycle = container.lifecycleHooks.get(serviceName)
|
||||
if (lifecycle?.emitOnStart) {
|
||||
container.logger.hookOnStart(serviceName)
|
||||
|
||||
const startTime = performance.now()
|
||||
await lifecycle.emitOnStart()
|
||||
const duration = performance.now() - startTime
|
||||
|
||||
container.logger.hookOnStartComplete(serviceName, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopLifecycleHooks(container: Container): Promise<void> {
|
||||
const sortedServices = topologicalSort(container.dependencyGraph)
|
||||
|
||||
// Shutdown in reverse order
|
||||
for (const serviceName of sortedServices.reverse()) {
|
||||
const lifecycle = container.lifecycleHooks.get(serviceName)
|
||||
if (lifecycle?.emitOnStop) {
|
||||
container.logger.hookOnStop(serviceName)
|
||||
|
||||
const startTime = performance.now()
|
||||
await lifecycle.emitOnStop()
|
||||
const duration = performance.now() - startTime
|
||||
|
||||
container.logger.hookOnStopComplete(serviceName, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function start(container: Container): Promise<void> {
|
||||
await startLifecycleHooks(container)
|
||||
|
||||
for (const invocation of container.invocations) {
|
||||
const resolvedDependencies: Record<string, any> = {}
|
||||
|
||||
if (invocation.dependsOn) {
|
||||
for (const [key, depName] of Object.entries(invocation.dependsOn)) {
|
||||
resolvedDependencies[key] = await resolveInstance(container, normalizeName(depName))
|
||||
}
|
||||
}
|
||||
|
||||
await invocation.callback(resolvedDependencies)
|
||||
}
|
||||
|
||||
container.logger.running()
|
||||
}
|
||||
|
||||
export async function stop(container: Container): Promise<void> {
|
||||
await stopLifecycleHooks(container)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ESNext"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"vitest",
|
||||
"vite/client"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
'./index': 'src/index.ts',
|
||||
},
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
unused: true,
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
})
|
||||
Generated
+36
-18
@@ -302,9 +302,6 @@ importers:
|
||||
'@proj-airi/i18n':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/i18n
|
||||
'@proj-airi/injecta':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/injecta
|
||||
'@proj-airi/server-runtime':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/server-runtime
|
||||
@@ -413,6 +410,9 @@ importers:
|
||||
h3:
|
||||
specifier: 2.0.1-rc.5
|
||||
version: 2.0.1-rc.5(crossws@0.4.1(srvx@0.9.4))
|
||||
injeca:
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(@guiiai/logg@1.1.0)(error-stack-parser@2.1.4)(nanoid@5.1.6)
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
@@ -1192,18 +1192,6 @@ importers:
|
||||
specifier: ^3.0.7
|
||||
version: 3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.10.0)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@7.1.12(@types/node@24.10.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
|
||||
|
||||
packages/injecta:
|
||||
devDependencies:
|
||||
'@guiiai/logg':
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.0
|
||||
error-stack-parser:
|
||||
specifier: ^2.1.4
|
||||
version: 2.1.4
|
||||
nanoid:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
|
||||
packages/memory-pgvector:
|
||||
dependencies:
|
||||
'@guiiai/logg':
|
||||
@@ -1879,7 +1867,7 @@ importers:
|
||||
version: rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.3(@nuxt/kit@4.0.3(magicast@0.3.5))(rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||
version: 8.0.3(rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||
vue-router:
|
||||
specifier: ^4.6.3
|
||||
version: 4.6.3(vue@3.5.22(typescript@5.9.3))
|
||||
@@ -1913,7 +1901,7 @@ importers:
|
||||
version: rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.3(@nuxt/kit@4.0.3(magicast@0.3.5))(rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||
version: 8.0.3(rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||
vue-router:
|
||||
specifier: ^4.6.3
|
||||
version: 4.6.3(vue@3.5.22(typescript@5.9.3))
|
||||
@@ -11012,6 +11000,16 @@ packages:
|
||||
resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
injeca@0.1.1:
|
||||
resolution: {integrity: sha512-K0vAFK/DD6X9LaNu79hUPeVx4Qe1FgeMVEKNdjbEiLS/XhiahyPk/L0f8iIe6MdW3Fr6w0yay+zcKEbVminxnw==}
|
||||
peerDependencies:
|
||||
'@guiiai/logg': '>= 1'
|
||||
error-stack-parser: ^2.1.4
|
||||
nanoid: '>=5'
|
||||
peerDependenciesMeta:
|
||||
'@guiiai/logg':
|
||||
optional: true
|
||||
|
||||
ioredis@5.7.0:
|
||||
resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
@@ -27240,6 +27238,13 @@ snapshots:
|
||||
|
||||
ini@4.1.3: {}
|
||||
|
||||
injeca@0.1.1(@guiiai/logg@1.1.0)(error-stack-parser@2.1.4)(nanoid@5.1.6):
|
||||
dependencies:
|
||||
error-stack-parser: 2.1.4
|
||||
nanoid: 5.1.6
|
||||
optionalDependencies:
|
||||
'@guiiai/logg': 1.1.0
|
||||
|
||||
ioredis@5.7.0:
|
||||
dependencies:
|
||||
'@ioredis/commands': 1.3.0
|
||||
@@ -33448,7 +33453,6 @@ snapshots:
|
||||
'@nuxt/kit': 3.18.1(magicast@0.3.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
vite-plugin-inspect@11.3.3(@nuxt/kit@3.18.1(magicast@0.3.5))(vite@6.4.1(@types/node@24.10.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)):
|
||||
dependencies:
|
||||
@@ -33527,6 +33531,20 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-devtools@8.0.3(rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.0.3(rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 8.0.3
|
||||
'@vue/devtools-shared': 8.0.3
|
||||
sirv: 3.0.2
|
||||
vite: rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
vite-plugin-inspect: 11.3.3(@nuxt/kit@3.18.1(magicast@0.3.5))(rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
|
||||
vite-plugin-vue-inspector: 5.3.2(rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(rolldown-vite@7.1.20(@types/node@24.10.0)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.3
|
||||
|
||||
Reference in New Issue
Block a user