Skip to content

Commit

Permalink
feat(cordis): support inject object with resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jun 13, 2024
1 parent d373e08 commit acffbd3
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 92 deletions.
4 changes: 2 additions & 2 deletions packages/cordis/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,25 @@ export class Context extends core.Context {
this.baseDir = globalThis.process?.cwd() || ''

this.provide('logger', undefined, true)
this.provide('schema', undefined, true)
this.provide('timer', undefined, true)

this.plugin(LoggerService)
this.plugin(SchemaService)
this.plugin(TimerService)
}
}

export abstract class Service<T = unknown, C extends Context = Context> extends core.Service<T, C> {
/** @deprecated use `this.ctx.logger` instead */
public logger: Logger
public schema: SchemaService

constructor(...args: core.Spread<T>)
constructor(ctx: C, ...args: core.Spread<T>)
constructor(ctx: C, name: string, immediate?: boolean)
constructor(...args: any) {
super(...args)
this.logger = this.ctx.logger(this.name)
this.schema = new SchemaService(this.ctx)
}

[core.Service.setup]() {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default class Lifecycle {
// non-reusable plugin forks are not responsive to isolated service changes
defineProperty(this.on('internal/before-service', function (this: Context, name) {
for (const runtime of this.registry.values()) {
if (!runtime.using.includes(name)) continue
if (!runtime.inject[name]?.required) continue
const scopes = runtime.isReusable ? runtime.children : [runtime]
for (const scope of scopes) {
if (!this[symbols.filter](scope.ctx)) continue
Expand All @@ -94,7 +94,7 @@ export default class Lifecycle {

defineProperty(this.on('internal/service', function (this: Context, name) {
for (const runtime of this.registry.values()) {
if (!runtime.using.includes(name)) continue
if (!runtime.inject[name]?.required) continue
const scopes = runtime.isReusable ? runtime.children : [runtime]
for (const scope of scopes) {
if (!this[symbols.filter](scope.ctx)) continue
Expand All @@ -106,7 +106,7 @@ export default class Lifecycle {
// inject in ancestor contexts
const checkInject = (scope: EffectScope, name: string) => {
if (!scope.runtime.plugin) return false
for (const key of scope.runtime.inject) {
for (const key in scope.runtime.inject) {
if (name === ReflectService.resolveInject(scope.ctx, key)[0]) return true
}
return checkInject(scope.parent.scope, name)
Expand Down
36 changes: 27 additions & 9 deletions packages/core/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,31 @@ import { Context } from './context.ts'
import { ForkScope, MainScope } from './scope.ts'
import { resolveConfig, symbols } from './utils.ts'

export function isApplicable(object: Plugin) {
function isApplicable(object: Plugin) {
return object && typeof object === 'object' && typeof object.apply === 'function'
}

export interface Inject {
readonly required?: string[]
readonly optional?: string[]
export type Inject = string[] | Dict<Inject.Meta>

export namespace Inject {
export interface Meta {
required: boolean
}

export function resolve(inject: Inject | null | undefined) {
if (!inject) return {}
if (Array.isArray(inject)) {
return Object.fromEntries(inject.map(name => [name, { required: true }]))
}
const { required, optional, ...rest } = inject
if (Array.isArray(required)) {
Object.assign(rest, Object.fromEntries(required.map(name => [name, { required: true }])))
}
if (Array.isArray(optional)) {
Object.assign(rest, Object.fromEntries(optional.map(name => [name, { required: false }])))
}
return rest
}
}

export type Plugin<C extends Context = Context, T = any> =
Expand All @@ -23,7 +41,7 @@ export namespace Plugin {
reactive?: boolean
reusable?: boolean
Config?: (config: any) => T
inject?: string[] | Inject
inject?: Inject
intercept?: Dict<boolean>
}

Expand All @@ -50,8 +68,8 @@ export type Spread<T> = undefined extends T ? [config?: T] : [config: T]
declare module './context.ts' {
export interface Context {
/** @deprecated use `ctx.inject()` instead */
using(deps: string[] | Inject, callback: Plugin.Function<this, void>): ForkScope<this>
inject(deps: string[] | Inject, callback: Plugin.Function<this, void>): ForkScope<this>
using(deps: Inject, callback: Plugin.Function<this, void>): ForkScope<this>
inject(deps: Inject, callback: Plugin.Function<this, void>): ForkScope<this>
plugin<T = undefined, S = T>(plugin: Plugin.Function<this, T> & Plugin.Transform<S, T>, ...args: Spread<S>): ForkScope<this>
plugin<T = undefined, S = T>(plugin: Plugin.Constructor<this, T> & Plugin.Transform<S, T>, ...args: Spread<S>): ForkScope<this>
plugin<T = undefined, S = T>(plugin: Plugin.Object<this, T> & Plugin.Transform<S, T>, ...args: Spread<S>): ForkScope<this>
Expand Down Expand Up @@ -135,11 +153,11 @@ export default class Registry<C extends Context = Context> {
return this._internal.forEach(callback)
}

using(inject: string[] | Inject, callback: Plugin.Function<C, void>) {
using(inject: Inject, callback: Plugin.Function<C, void>) {
return this.inject(inject, callback)
}

inject(inject: string[] | Inject, callback: Plugin.Function<C, void>) {
inject(inject: Inject, callback: Plugin.Function<C, void>) {
return this.plugin({ inject, apply: callback, name: callback.name })
}

Expand Down
28 changes: 6 additions & 22 deletions packages/core/src/scope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deepEqual, defineProperty, isNullable, remove } from 'cosmokit'
import { deepEqual, defineProperty, Dict, isNullable, remove } from 'cosmokit'
import { Context } from './context.ts'
import { Inject, Plugin } from './registry.ts'
import { isConstructor, resolveConfig } from './utils.ts'
Expand Down Expand Up @@ -167,7 +167,9 @@ export abstract class EffectScope<C extends Context = Context> {
}

get ready() {
return this.runtime.using.every(name => !isNullable(this.ctx.get(name)))
return Object.entries(this.runtime.inject).every(([name, inject]) => {
return !inject.required || !isNullable(this.ctx.get(name))
})
}

reset() {
Expand Down Expand Up @@ -303,8 +305,7 @@ export class MainScope<C extends Context = Context> extends EffectScope<C> {
runtime = this
schema: any
name?: string
using: string[] = []
inject = new Set<string>()
inject: Dict<Inject.Meta> = Object.create(null)
forkables: Function[] = []
children: ForkScope<C>[] = []
isReusable?: boolean = false
Expand Down Expand Up @@ -336,28 +337,11 @@ export class MainScope<C extends Context = Context> extends EffectScope<C> {
return true
}

private setInject(inject?: string[] | Inject): void {
if (Array.isArray(inject)) {
for (const name of inject) {
this.using.push(name)
this.inject.add(name)
}
} else if (inject) {
for (const name of inject.required || []) {
this.using.push(name)
this.inject.add(name)
}
for (const name of inject.optional || []) {
this.inject.add(name)
}
}
}

private setup() {
const { name } = this.plugin
if (name && name !== 'apply') this.name = name
this.schema = this.plugin['Config'] || this.plugin['schema']
this.setInject(this.plugin['using'] || this.plugin['inject'])
this.inject = Inject.resolve(this.plugin['using'] || this.plugin['inject'])
this.isReusable = this.plugin['reusable']
this.isReactive = this.plugin['reactive']
this.context.emit('internal/runtime', this)
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const symbols = {
// internal symbols
shadow: Symbol.for('cordis.shadow'),
receiver: Symbol.for('cordis.receiver'),
original: Symbol.for('cordis.original'),

// context symbols
source: Symbol.for('cordis.source') as typeof Context.source,
Expand Down Expand Up @@ -133,6 +134,7 @@ function createTraceable(ctx: Context, value: any, tracker: Tracker, noTrap?: bo
}
const proxy = new Proxy(value, {
get: (target, prop, receiver) => {
if (prop === symbols.original) return target
if (prop === tracker.property) return ctx
if (typeof prop === 'symbol') {
return Reflect.get(target, prop, receiver)
Expand All @@ -152,6 +154,7 @@ function createTraceable(ctx: Context, value: any, tracker: Tracker, noTrap?: bo
}
},
set: (target, prop, value, receiver) => {
if (prop === symbols.original) return false
if (prop === tracker.property) return false
if (typeof prop === 'symbol') {
return Reflect.set(target, prop, value, receiver)
Expand Down
26 changes: 8 additions & 18 deletions packages/loader/src/inject.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
import { Context, EffectScope, Inject } from '@cordisjs/core'
import { filterKeys } from 'cosmokit'
import { Entry } from './entry.ts'

declare module './entry.ts' {
interface EntryOptions {
inject?: string[] | Inject | null
inject?: Inject | null
}
}

export const name = 'inject'

export function apply(ctx: Context) {
function getRequired(entry?: Entry) {
return Array.isArray(entry?.options.inject)
? entry.options.inject
: entry?.options.inject?.required ?? []
}

function getInject(entry?: Entry) {
return Array.isArray(entry?.options.inject)
? entry?.options.inject
: [
...entry?.options.inject?.required ?? [],
...entry?.options.inject?.optional ?? [],
]
function getRequired(entry: Entry) {
return filterKeys(Inject.resolve(entry.options.inject), (_, meta) => meta.required)
}

const checkInject = (scope: EffectScope, name: string) => {
if (!scope.runtime.plugin) return false
if (scope.runtime === scope) {
return scope.runtime.children.every(fork => checkInject(fork, name))
}
if (getInject(scope.entry).includes(name)) return true
if (name in Inject.resolve(scope.entry?.options.inject)) return true
return checkInject(scope.parent.scope, name)
}

Expand All @@ -39,21 +29,21 @@ export function apply(ctx: Context) {
})

ctx.on('loader/entry-check', (entry) => {
for (const name of getRequired(entry)) {
for (const name in getRequired(entry)) {
if (!entry.ctx.get(name)) return true
}
})

ctx.on('internal/before-service', (name) => {
for (const entry of ctx.loader.entries()) {
if (!getRequired(entry).includes(name)) continue
if (!(name in getRequired(entry))) continue
entry.refresh()
}
}, { global: true })

ctx.on('internal/service', (name) => {
for (const entry of ctx.loader.entries()) {
if (!getRequired(entry).includes(name)) continue
if (!(name in getRequired(entry))) continue
entry.refresh()
}
}, { global: true })
Expand Down
6 changes: 2 additions & 4 deletions packages/loader/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,9 @@ export namespace Loader {
export abstract class Loader<C extends Context = Context> extends ImportTree<C> {
// TODO auto inject optional when provided?
static inject = {
optional: ['loader'],
loader: { required: false },
}

private [Context.current]!: C

// process
public envData = process.env.CORDIS_SHARED
? JSON.parse(process.env.CORDIS_SHARED)
Expand Down Expand Up @@ -127,7 +125,7 @@ export abstract class Loader<C extends Context = Context> extends ImportTree<C>
await super.start()
}

locate(ctx = this[Context.current]) {
locate(ctx = this.ctx) {
return this._locate(ctx.scope).map(entry => entry.id)
}

Expand Down
56 changes: 22 additions & 34 deletions packages/schema/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Dict, remove } from 'cosmokit'
import { defineProperty, remove } from 'cosmokit'
import { Context, Service } from '@cordisjs/core'
import Schema from 'schemastery'

Expand All @@ -7,51 +7,39 @@ export { default as Schema, default as z } from 'schemastery'
const kSchemaOrder = Symbol('cordis.schema.order')

declare module '@cordisjs/core' {
interface Context {
schema: SchemaService
}

interface Events {
'internal/schema'(name: string): void
'internal/service-schema'(): void
}
}

export class SchemaService extends Service {
_data: Dict<Schema> = Object.create(null)
export class SchemaService {
_data = Schema.intersect([]) as Schema & { list: Schema[] }

constructor(public ctx: Context) {
super(ctx, 'schema', true)
defineProperty(this, Service.tracker, {
property: 'ctx',
})
}

extend(name: string, schema: Schema, order = 0) {
const caller = this[Context.current]
const target = this.get(name)
const index = target.list.findIndex(a => a[kSchemaOrder] < order)
extend(schema: Schema, order = 0) {
const index = this._data.list.findIndex(a => a[kSchemaOrder] < order)
schema[kSchemaOrder] = order
if (index >= 0) {
target.list.splice(index, 0, schema)
} else {
target.list.push(schema)
}
this.ctx.emit('internal/schema', name)
caller.on('dispose', () => {
remove(target.list, schema)
this.ctx.emit('internal/schema', name)
return this.ctx.effect(() => {
if (index >= 0) {
this._data.list.splice(index, 0, schema)
} else {
this._data.list.push(schema)
}
this.ctx.emit('internal/service-schema')
return () => {
remove(this._data.list, schema)
this.ctx.emit('internal/service-schema')
}
})
}

get(name: string) {
return (this._data[name] ||= Schema.intersect([])) as Schema & { list: Schema[] }
}

set(name: string, schema: Schema) {
const caller = this[Context.current]
this._data[name] = schema
this.ctx.emit('internal/schema', name)
caller?.on('dispose', () => {
delete this._data[name]
this.ctx.emit('internal/schema', name)
})
toJSON() {
return this._data.toJSON()
}
}

Expand Down

0 comments on commit acffbd3

Please sign in to comment.