feat(injecta): supported typed use and auto name use

This commit is contained in:
Neko Ayaka
2025-10-18 17:20:22 +08:00
parent b0af3cd12b
commit b438495d97
11 changed files with 517 additions and 52 deletions
+18 -5
View File
@@ -108,11 +108,24 @@ async function setupProjectAIRIServerRuntime() {
app.whenReady().then(async () => {
await setupProjectAIRIServerRuntime()
injecta.setLogger(createLoggLogger())
injecta.provide('windows:settings', () => setupSettingsWindowReusableFunc())
injecta.provide<{ settingsWindow: () => Promise<BrowserWindow> }>('windows:main', { dependsOn: { settingsWindow: 'windows:settings' }, build: async ({ dependsOn }) => setupMainWindow(dependsOn) })
injecta.provide<{ mainWindow: BrowserWindow, settingsWindow: () => Promise<BrowserWindow> }>('tray', { dependsOn: { mainWindow: 'windows:main', settingsWindow: 'windows:settings' }, build: async ({ dependsOn }) => setupTray(dependsOn) })
injecta.invoke({ dependsOn: { mainWindow: 'windows:main', tray: 'tray' }, callback: noop })
injecta.setLogger(createLoggLogger(useLogg('injecta').useGlobalConfig()))
const settingsWindow = injecta.provide('windows:settings', {
build: () => setupSettingsWindowReusableFunc(),
})
const mainWindow = injecta.provide('windows:main', {
dependsOn: { settingsWindow },
build: async ({ dependsOn }) => setupMainWindow(dependsOn),
})
const tray = injecta.provide('app:tray', {
dependsOn: { mainWindow, settingsWindow },
build: async ({ dependsOn }) => setupTray(dependsOn),
})
injecta.invoke({
dependsOn: { mainWindow, tray },
callback: noop,
})
injecta.start()
// Lifecycle
+1
View File
@@ -99,6 +99,7 @@
"@parcel/watcher",
"bufferutil",
"electron",
"electron-click-drag-plugin",
"es5-ext",
"esbuild",
"ffmpeg-static",
+4 -2
View File
@@ -24,7 +24,9 @@
"typecheck": "tsc --noEmit",
"build": "tsdown"
},
"dependencies": {
"@guiiai/logg": "catalog:"
"devDependencies": {
"@guiiai/logg": "catalog:",
"error-stack-parser": "^2.1.4",
"nanoid": "^5.1.6"
}
}
+187
View File
@@ -0,0 +1,187 @@
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)
}
})
})
+20 -8
View File
@@ -1,21 +1,33 @@
import type { DependencyMap, InvokeOption, Logger, ProvideOption } from '.'
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 '.'
const globalContainer = createContainer()
let globalContainer = createContainer()
export function setLogger(logger: Logger) {
globalContainer.logger = logger
}
export function provide<D extends DependencyMap | undefined, T = any>(
name: string,
option: ProvideOption<T, D>,
): void {
indexProvide(globalContainer, name, option)
export function resetContainer() {
globalContainer = createContainer()
}
export function invoke<D extends DependencyMap>(option: InvokeOption<D>): void {
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)
}
+7
View File
@@ -19,6 +19,7 @@ export {
export {
createContainer,
invoke,
lifecycle,
provide,
start,
stop,
@@ -30,9 +31,15 @@ export type {
InvokeOption,
InvokeOptionFunc,
InvokeOptionObject,
InvokeOptionObjectWithKeys,
InvokeOptionWithKeys,
ProvidedKey,
ProvideOption,
ProvideOptionFunc,
ProvideOptionObject,
ProvideOptionObjectWithKeys,
ProvideOptionWithKeys,
ResolveDependencyDeclaration,
} from './scoped'
export const injecta = {
+2 -4
View File
@@ -1,4 +1,4 @@
import { useLogg } from '@guiiai/logg'
import type { Logg as LoggLogger } from '@guiiai/logg'
import { name } from '../package.json'
@@ -80,9 +80,7 @@ export function createDefaultLogger(): Logger {
}
}
export function createLoggLogger(): Logger {
const logg = useLogg(name).useGlobalConfig()
export function createLoggLogger(logg: LoggLogger): Logger {
return {
provide: (name: string, dependencies: string[]) => {
const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(', ')})` : ''
+161 -8
View File
@@ -2,10 +2,39 @@ import type { Lifecycle } from './builtin'
import { describe, expect, it, vi } from 'vitest'
import { invoke, provide, start, stop } from './global'
import { createContainer, invoke, lifecycle, normalizeName, normalizeProvideOption, provide, start, stop } from './scoped'
describe('di', () => {
it('should work with individual lifecycle injection', async () => {
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>
@@ -35,23 +64,147 @@ describe('di', () => {
return server
}
provide<{ lifecycle: Lifecycle }>('db', {
const app = createContainer()
provide<{ lifecycle: Lifecycle }>(app, 'db', {
dependsOn: { lifecycle: 'lifecycle' },
build: async ({ dependsOn }) => createDatabase({ lifecycle: dependsOn.lifecycle }),
})
provide<{ database: Database, lifecycle: Lifecycle }>('ws', {
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 }>({
invoke<{ webSocketServer: WebSocketServer }>(app, {
dependsOn: { webSocketServer: 'ws' },
callback: async ({ webSocketServer }) => await webSocketServer.start(),
})
await start()
await stop()
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
{
+100 -13
View File
@@ -1,6 +1,10 @@
import type { LifecycleTriggerable } from './builtin'
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>
@@ -19,14 +23,32 @@ export type BuildContext<D extends DependencyMap | undefined = undefined> = {
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()
@@ -42,29 +64,93 @@ export function createContainer(options?: LoggerOptions): Container {
}
}
export function provide<D extends DependencyMap | undefined, T = any>(
container: Container,
name: string,
option: ProvideOption<T, D>,
): void {
const providerObject = typeof option === 'function'
? { build: option } as ProvideOptionObject<any, any>
: option as ProvideOptionObject<any, any>
// 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)
// Track dependencies for lifecycle ordering
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<D extends DependencyMap>(container: Container, option: InvokeOption<D>): void {
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)
@@ -91,6 +177,7 @@ async function resolveInstance<T>(container: Container, name: string): Promise<T
const { buildLifecycle } = await import('./builtin')
serviceLifecycle = buildLifecycle()
resolvedDependencies[key] = serviceLifecycle
// Track this service's lifecycle
container.lifecycleHooks.set(name, serviceLifecycle)
}
@@ -194,7 +281,7 @@ export async function start(container: Container): Promise<void> {
if (invocation.dependsOn) {
for (const [key, depName] of Object.entries(invocation.dependsOn)) {
resolvedDependencies[key] = await resolveInstance(container, depName)
resolvedDependencies[key] = await resolveInstance(container, normalizeName(depName))
}
}
+8 -12
View File
@@ -1148,10 +1148,16 @@ importers:
version: 3.0.6(@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.7.2)(db0@0.3.2(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(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.4)(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.43)(rollup@4.52.4)(vite@7.1.9(@types/node@24.7.2)(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:
dependencies:
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:
@@ -4619,9 +4625,6 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@napi-rs/wasm-runtime@1.0.6':
resolution: {integrity: sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g==}
'@napi-rs/wasm-runtime@1.0.7':
resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==}
@@ -18743,13 +18746,6 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@napi-rs/wasm-runtime@1.0.6':
dependencies:
'@emnapi/core': 1.5.0
'@emnapi/runtime': 1.5.0
'@tybys/wasm-util': 0.10.1
optional: true
'@napi-rs/wasm-runtime@1.0.7':
dependencies:
'@emnapi/core': 1.5.0
@@ -20782,7 +20778,7 @@ snapshots:
'@rolldown/binding-wasm32-wasi@1.0.0-beta.42':
dependencies:
'@napi-rs/wasm-runtime': 1.0.6
'@napi-rs/wasm-runtime': 1.0.7
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-beta.43':
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
'packages/injecta',
],
},
})