diff --git a/apps/stage-tamagotchi/package.json b/apps/stage-tamagotchi/package.json index 5ca262300..13b8b939a 100644 --- a/apps/stage-tamagotchi/package.json +++ b/apps/stage-tamagotchi/package.json @@ -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", diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 665d41674..cb7b81054 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -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() }) diff --git a/cspell.config.yaml b/cspell.config.yaml index 3fcb2e75d..aa0cfc05b 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -123,6 +123,7 @@ words: - hunyuan - hyoban - iconify + - injeca - injecta - intlify - jlumbroso diff --git a/packages/injecta/package.json b/packages/injecta/package.json deleted file mode 100644 index 8dff28727..000000000 --- a/packages/injecta/package.json +++ /dev/null @@ -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" - } -} diff --git a/packages/injecta/src/builtin.ts b/packages/injecta/src/builtin.ts deleted file mode 100644 index 993413d63..000000000 --- a/packages/injecta/src/builtin.ts +++ /dev/null @@ -1,45 +0,0 @@ -export interface LifecycleAppHooks { - onStart: (hook: () => void | Promise) => void - onStop: (hook: () => void | Promise) => void -} - -export interface Lifecycle { - appHooks: LifecycleAppHooks -} - -export interface LifecycleTriggerable extends Lifecycle { - emitOnStart: () => Promise - emitOnStop: () => Promise -} - -export function buildLifecycle(): Lifecycle { - const onStartHooks: (() => void | Promise)[] = [] - const onStopHooks: (() => void | Promise)[] = [] - - const lifecycle: LifecycleTriggerable = { - appHooks: { - onStart(hook: () => void | Promise) { - if (hook) { - onStartHooks.push(hook) - } - }, - onStop(hook: () => void | Promise) { - 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 -} diff --git a/packages/injecta/src/global.test.ts b/packages/injecta/src/global.test.ts deleted file mode 100644 index 9b4ccdd5e..000000000 --- a/packages/injecta/src/global.test.ts +++ /dev/null @@ -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 - close: () => Promise - } - - 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 - stop: () => Promise - } - - const webSocketServerStartSpy = vi.fn() - const webSocketServerStopSpy = vi.fn() - - async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { - 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 - close: () => Promise - } - - 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 - stop: () => Promise - } - - const webSocketServerStartSpy = vi.fn() - const webSocketServerStopSpy = vi.fn() - - async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { - 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 - close: () => Promise - } - - 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 - stop: () => Promise - } - - const webSocketServerStartSpy = vi.fn() - const webSocketServerStopSpy = vi.fn() - - async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { - 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) - } - }) -}) diff --git a/packages/injecta/src/global.ts b/packages/injecta/src/global.ts deleted file mode 100644 index c435b3dfa..000000000 --- a/packages/injecta/src/global.ts +++ /dev/null @@ -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>>(name: Key, option: ProvideOptionWithKeys): ProvidedKey> -export function provide(name: Key, option: ProvideOption): ProvidedKey -export function provide>>(option: ProvideOptionWithKeys): ProvidedKey> -export function provide(option: ProvideOption): ProvidedKey -export function provide(nameOrOption: Key | ProvideOption | ProvideOptionWithKeys, option?: ProvideOption | ProvideOptionWithKeys): ProvidedKey { - 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>>(option: InvokeOptionWithKeys): void -export function invoke(option: InvokeOption): void -export function invoke(option: InvokeOption | InvokeOptionWithKeys): void { - if (typeof option === 'function') { - globalContainer.invocations.push({ callback: option } as any) - } - else { - globalContainer.invocations.push(option as any) - } -} - -export function start(): Promise { - return indexStart(globalContainer) -} - -export function stop(): Promise { - return indexStop(globalContainer) -} diff --git a/packages/injecta/src/index.ts b/packages/injecta/src/index.ts deleted file mode 100644 index b0e0aab75..000000000 --- a/packages/injecta/src/index.ts +++ /dev/null @@ -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, -} diff --git a/packages/injecta/src/logger.ts b/packages/injecta/src/logger.ts deleted file mode 100644 index de961a7d9..000000000 --- a/packages/injecta/src/logger.ts +++ /dev/null @@ -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: () => {}, - } -} diff --git a/packages/injecta/src/scoped.test.ts b/packages/injecta/src/scoped.test.ts deleted file mode 100644 index 3ff408eef..000000000 --- a/packages/injecta/src/scoped.test.ts +++ /dev/null @@ -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 or a ProvideOptionFunc.') - expect(() => normalizeProvideOption({ key: 'objectName' })).toThrowError('When using provide(...) as typed ProvideOptionWithKeys callback, the second argument must be either a valid ProvideOptionObject or a ProvideOptionFunc.') - - 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 - close: () => Promise - } - - 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 - stop: () => Promise - } - - const webSocketServerStartSpy = vi.fn() - const webSocketServerStopSpy = vi.fn() - - async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { - 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 - close: () => Promise - } - - 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 - stop: () => Promise - } - - const webSocketServerStartSpy = vi.fn() - const webSocketServerStopSpy = vi.fn() - - async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { - 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 - close: () => Promise - } - - 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 - stop: () => Promise - } - - const webSocketServerStartSpy = vi.fn() - const webSocketServerStopSpy = vi.fn() - - async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { - 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) - } - }) -}) diff --git a/packages/injecta/src/scoped.ts b/packages/injecta/src/scoped.ts deleted file mode 100644 index 9e4206caa..000000000 --- a/packages/injecta/src/scoped.ts +++ /dev/null @@ -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 - -export interface Container { - providers: Map> - instances: Map - lifecycleHooks: Map - dependencyGraph: Map - invocations: InvokeOptionObject[] - logger: Logger -} - -export type BuildContext = { - container: Container - name: string -} & (D extends undefined ? { dependsOn?: unknown } : { dependsOn: D }) - -export type ResolveDependencyDeclaration>> = { - [K in keyof Deps]: Deps[K] extends ProvidedKey ? T : any -} - -export type ProvideOptionObject = { build: (context: BuildContext) => T | Promise } & (D extends undefined ? { dependsOn?: Record } : { dependsOn: { [K in keyof D]: string } }) -export type ProvideOptionFunc = (context: BuildContext) => T | Promise -export type ProvideOption = ProvideOptionObject | ProvideOptionFunc - -export interface ProvideOptionObjectWithKeys>> { - build: (context: BuildContext>) => T | Promise - dependsOn: Deps -} -export type ProvideOptionWithKeys>> = ProvideOptionObjectWithKeys - -export type InvokeOptionObject = { callback: (dependencies: D) => void | Promise } & (D extends undefined ? { dependsOn?: Record } : { dependsOn: { [K in keyof D]: string } }) -export type InvokeOptionFunc = (dependencies: D) => void | Promise -export type InvokeOption = InvokeOptionObject | InvokeOptionFunc - -export interface InvokeOptionObjectWithKeys>> { - callback: (dependencies: ResolveDependencyDeclaration) => void | Promise - dependsOn: Deps -} -export type InvokeOptionWithKeys>> = InvokeOptionObjectWithKeys - -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: Key -} - -export function normalizeName(nameOrProvidedKey: string | ProvidedKey): string { - if (typeof nameOrProvidedKey === 'object' && nameOrProvidedKey !== null && 'key' in nameOrProvidedKey) { - return (nameOrProvidedKey as ProvidedKey).key as string - } - - return nameOrProvidedKey as string -} - -export function normalizeProvideOption(nameOrOption: Key | ProvidedKey | ProvideOption | ProvideOptionWithKeys, option?: (ProvideOption | ProvideOptionWithKeys) & { autoNameStackIndex?: number }): ProvideOptionObject { - 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 or a ProvideOptionFunc.') - } - if (typeof option === 'function') { - return { build: option } as ProvideOptionObject - } - - return option as ProvideOptionObject - } - 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 callback, the second argument must be either a valid ProvideOptionObject or a ProvideOptionFunc.') - } - if (typeof option === 'function') { - return { build: option } as ProvideOptionObject - } - - return option as ProvideOptionObject - } - - if (typeof nameOrOption === 'function') { - return { build: nameOrOption } as ProvideOptionObject - } - - return nameOrOption as ProvideOptionObject -} - -export function provide>>(container: Container, name: Key, option: ProvideOptionWithKeys): ProvidedKey> -export function provide>>(container: Container, option: ProvideOptionWithKeys & { autoNameStackIndex?: number }): ProvidedKey> -export function provide(container: Container, name: Key, option: ProvideOption): ProvidedKey -export function provide(container: Container, option: ProvideOption & { autoNameStackIndex?: number }): ProvidedKey -export function provide(container: Container, nameOrOption: Key | ProvideOption | ProvideOptionWithKeys, option?: (ProvideOption | ProvideOptionWithKeys) & { autoNameStackIndex?: number }): ProvidedKey { - 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(nameOrOption, option) as unknown as ProvideOptionObject - if (providerObject.dependsOn) { - const resolvedDependsOn: Record = {} - 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 -} - -export function invoke>>(container: Container, option: InvokeOptionWithKeys): void -export function invoke(container: Container, option: InvokeOption): void -export function invoke(container: Container, option: InvokeOption | InvokeOptionWithKeys): void { - const invocationObject = typeof option === 'function' - ? { callback: option } as InvokeOptionObject - : option as InvokeOptionObject - - if (invocationObject.dependsOn) { - const resolvedDependsOn: Record = {} - 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(container: Container, name: string): Promise { - 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 = {} - 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 = { - 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[] { - const visited = new Set() - const visiting = new Set() - 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 { - 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 { - 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 { - await startLifecycleHooks(container) - - for (const invocation of container.invocations) { - const resolvedDependencies: Record = {} - - 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 { - await stopLifecycleHooks(container) -} diff --git a/packages/injecta/tsconfig.json b/packages/injecta/tsconfig.json deleted file mode 100644 index c248248b8..000000000 --- a/packages/injecta/tsconfig.json +++ /dev/null @@ -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" - ] -} diff --git a/packages/injecta/tsdown.config.ts b/packages/injecta/tsdown.config.ts deleted file mode 100644 index 6c938b6a3..000000000 --- a/packages/injecta/tsdown.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'tsdown' - -export default defineConfig({ - entry: { - './index': 'src/index.ts', - }, - dts: true, - sourcemap: true, - unused: true, -}) diff --git a/packages/injecta/vitest.config.ts b/packages/injecta/vitest.config.ts deleted file mode 100644 index 647f39364..000000000 --- a/packages/injecta/vitest.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - include: ['src/**/*.test.ts'], - }, -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 696b0bb8c..7db908a66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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