Vendor Cordis framework packages as source
cordis 4.0.0-rc.6, plugin-loader, -include, -group, -timer, -hmr, -logger-console, cosmokit 1.8.1, schemastery 3.18.0 — copied from the cordis-workspace checkout, flattened under vendor/, original npm names, private: true. vendor/README.md is the manifest: upstream repos + commit SHAs, local-modification log, sync procedure. Local modification: hmr's locale YAML imports and .i18n() call removed (avoids a runtime YAML import hook we don't vendor).
This commit is contained in:
97
vendor/cordis/src/context.ts
vendored
Normal file
97
vendor/cordis/src/context.ts
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Dict } from 'cosmokit'
|
||||
import { EventsService } from './events'
|
||||
import { LoggerService } from './logger'
|
||||
import { ReflectService } from './reflect'
|
||||
import { InjectKey, RegistryService } from './registry'
|
||||
import { getTraceable, symbols } from './utils'
|
||||
import { Fiber } from './fiber'
|
||||
|
||||
/**
|
||||
* Public shape of a Cordis context.
|
||||
*
|
||||
* The concrete `Context` class is proxied at runtime, so this interface is
|
||||
* augmented by core services and plugins to describe the properties that may
|
||||
* be read from `ctx`.
|
||||
*/
|
||||
export interface Context {
|
||||
[symbols.isolate]: Dict<symbol>
|
||||
[symbols.intercept]: Dict
|
||||
/** @experimental */
|
||||
root: this
|
||||
baseUrl?: string
|
||||
events: EventsService
|
||||
logger: LoggerService
|
||||
reflect: ReflectService
|
||||
registry: RegistryService
|
||||
}
|
||||
|
||||
/**
|
||||
* Root and child dependency containers for Cordis plugins.
|
||||
*
|
||||
* A context is a proxy: normal property reads go through the service resolver,
|
||||
* while `extend()`, `isolate()`, and `intercept()` create scoped child
|
||||
* contexts without mutating their parent.
|
||||
*/
|
||||
export class Context {
|
||||
static readonly effect: unique symbol = symbols.effect
|
||||
static readonly filter: unique symbol = symbols.filter
|
||||
static readonly isolate: unique symbol = symbols.isolate
|
||||
static readonly intercept: unique symbol = symbols.intercept
|
||||
|
||||
/** Returns true for Cordis context proxies and context prototypes. */
|
||||
static is(value: any): value is Context {
|
||||
return !!value?.[Context.is as any]
|
||||
}
|
||||
|
||||
static {
|
||||
Context.is[Symbol.toPrimitive] = () => Symbol.for('cordis.is')
|
||||
Context.prototype[Context.is as any] = true
|
||||
}
|
||||
|
||||
/** Create the root context and install the built-in services. */
|
||||
constructor() {
|
||||
this[symbols.isolate] = Object.create(null)
|
||||
this[symbols.intercept] = Object.create(null)
|
||||
const self = new Proxy<this>(this, ReflectService.handler)
|
||||
this.root = self
|
||||
this.baseUrl = undefined
|
||||
this.fiber = new Fiber(self, {}, Object.create(null), null, () => [])
|
||||
this.reflect = new ReflectService(self)
|
||||
this.registry = new RegistryService(self)
|
||||
this.events = new EventsService(self)
|
||||
this.logger = new LoggerService(self)
|
||||
this.fiber._disposables.clear()
|
||||
return self
|
||||
}
|
||||
|
||||
[Symbol.for('nodejs.util.inspect.custom')]() {
|
||||
return `Context <${this.fiber.name}>`
|
||||
}
|
||||
|
||||
/** Create a child context with extra metadata on top of the current scope. */
|
||||
extend(meta = {}): this {
|
||||
const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value
|
||||
const self = Object.create(getTraceable(this, this))
|
||||
for (const prop of Reflect.ownKeys(meta)) {
|
||||
Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop)!)
|
||||
}
|
||||
if (!shadow) return self
|
||||
return Object.assign(Object.create(self), { [symbols.shadow]: shadow })
|
||||
}
|
||||
|
||||
/** Create a child context with an independent service scope for `name`. */
|
||||
isolate(name: string, label?: symbol) {
|
||||
const shadow = Object.create(this[symbols.isolate])
|
||||
shadow[name] = label ?? Symbol(name)
|
||||
return this.extend({ [symbols.isolate]: shadow })
|
||||
}
|
||||
|
||||
/** Add service-specific intercept config for plugins started below this context. */
|
||||
intercept<K extends InjectKey>(name: K, config: Context[K] extends { [symbols.config]: infer T } ? T : never): this
|
||||
intercept(name: string, config: any): this
|
||||
intercept(name: string, config: any) {
|
||||
const intercept = Object.create(this[symbols.intercept])
|
||||
intercept[name] = config
|
||||
return this.extend({ [symbols.intercept]: intercept })
|
||||
}
|
||||
}
|
||||
205
vendor/cordis/src/events.ts
vendored
Normal file
205
vendor/cordis/src/events.ts
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
import { defineProperty, Promisify } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { Fiber, FiberState } from './fiber'
|
||||
import { DisposableList, symbols } from './utils'
|
||||
|
||||
/** Return whether an event result should stop a bail-style dispatch. */
|
||||
export function isBailed(value: any) {
|
||||
return value !== null && value !== false && value !== undefined
|
||||
}
|
||||
|
||||
/** Extract the parameter tuple from a function type. */
|
||||
export type Parameters<F> = F extends (...args: infer P) => any ? P : never
|
||||
/** Extract the return type from a function type. */
|
||||
export type ReturnType<F> = F extends (...args: any) => infer R ? R : never
|
||||
/** Extract the explicit `this` type from a function type. */
|
||||
export type ThisType<F> = F extends (this: infer T, ...args: any) => any ? T : never
|
||||
|
||||
/**
|
||||
* Event dispatch strategy used by the event service.
|
||||
*
|
||||
* `emit` runs synchronous listeners without awaiting them, `parallel` awaits
|
||||
* all listeners together, `serial` awaits them in order until one bails,
|
||||
* `bail` stops on the first synchronous bail value, and `waterfall` composes
|
||||
* listeners around a final `next` callback.
|
||||
*/
|
||||
export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
|
||||
|
||||
declare module './context' {
|
||||
export interface Context {
|
||||
/* eslint-disable max-len */
|
||||
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
|
||||
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>
|
||||
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
|
||||
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void
|
||||
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
||||
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
||||
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
||||
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
||||
once<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
}
|
||||
|
||||
/** Options accepted by `ctx.on()` and `ctx.once()`. */
|
||||
export interface EventOptions {
|
||||
/** Add the listener before existing listeners for the same event. */
|
||||
prepend?: boolean
|
||||
/** Receive the event regardless of context filter checks. */
|
||||
global?: boolean
|
||||
}
|
||||
|
||||
/** Registered listener record stored by the event service. */
|
||||
export interface Hook extends EventOptions {
|
||||
ctx: Context
|
||||
callback: (...args: any[]) => any
|
||||
}
|
||||
|
||||
/**
|
||||
* Event bus installed as `ctx.events` and mixed into every context.
|
||||
*
|
||||
* The service supports concurrent, synchronous, serial, bail, and waterfall
|
||||
* dispatch and automatically disposes listeners with their owning fiber.
|
||||
*/
|
||||
export class EventsService {
|
||||
_hooks: Record<keyof any, Hook[]> = {}
|
||||
|
||||
constructor(private ctx: Context) {
|
||||
defineProperty(this, symbols.tracker, {
|
||||
property: 'ctx',
|
||||
noShadow: true,
|
||||
})
|
||||
|
||||
this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) {
|
||||
if (name === 'internal/update' && !options.global) {
|
||||
const hooks = this.fiber._hooks['internal/update'] ??= new DisposableList()
|
||||
const method = options.prepend ? 'unshift' : 'push'
|
||||
return hooks[method](listener)
|
||||
}
|
||||
})
|
||||
|
||||
this.on('internal/update', function (config, noSave, next) {
|
||||
const cbs = [...this._hooks['internal/update'] || []]
|
||||
const _next = () => {
|
||||
const cb = cbs.shift() ?? next
|
||||
return cb.call(this, config, noSave, _next)
|
||||
}
|
||||
return _next()
|
||||
}, { global: true, prepend: true })
|
||||
}
|
||||
|
||||
/** Resolve listeners for one dispatch and apply context filtering. */
|
||||
dispatch(type: string, args: any[]) {
|
||||
const thisArg = typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null
|
||||
const name: string = args.shift()
|
||||
if (!name.startsWith('internal/')) {
|
||||
this.emit('internal/dispatch', type, name, args, thisArg)
|
||||
}
|
||||
const filter = thisArg?.[Context.filter]
|
||||
return (this._hooks[name] || [])
|
||||
.filter(hook => hook.global || !filter || filter.call(thisArg, hook.ctx))
|
||||
.map(hook => hook.callback.bind(thisArg))
|
||||
}
|
||||
|
||||
/** Run listeners concurrently and wait for all of them. */
|
||||
async parallel(...args: any[]) {
|
||||
await Promise.all(this.dispatch('emit', args).map(cb => cb(...args)))
|
||||
}
|
||||
|
||||
/** Run listeners synchronously without waiting for returned promises. */
|
||||
emit(...args: any[]) {
|
||||
this.dispatch('emit', args).map(cb => cb(...args))
|
||||
}
|
||||
|
||||
/** Run listeners in order until one returns a bail value. */
|
||||
async serial(...args: any[]) {
|
||||
for (const cb of this.dispatch('serial', args)) {
|
||||
const result = await cb(...args)
|
||||
if (isBailed(result)) return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Run listeners synchronously until one returns a bail value. */
|
||||
bail(...args: any[]) {
|
||||
for (const cb of this.dispatch('bail', args)) {
|
||||
const result = cb(...args)
|
||||
if (isBailed(result)) return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Compose listeners around the final `next` callback. */
|
||||
waterfall(...args: any[]) {
|
||||
const cbs = this.dispatch('waterfall', args)
|
||||
const inner = args.pop()
|
||||
const next = () => {
|
||||
const cb = cbs.shift() ?? inner
|
||||
return cb(...args)
|
||||
}
|
||||
args.push(next)
|
||||
return next()
|
||||
}
|
||||
|
||||
register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
|
||||
const method = options.prepend ? 'unshift' : 'push'
|
||||
return this.ctx.fiber.effect(() => {
|
||||
hooks[method]({ ctx: this.ctx, callback, ...options })
|
||||
return () => this.unregister(hooks, callback)
|
||||
}, label)
|
||||
}
|
||||
|
||||
unregister(hooks: Hook[], callback: any) {
|
||||
const index = hooks.findIndex(hook => hook.callback === callback)
|
||||
if (index >= 0) {
|
||||
hooks.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/** Register an event listener owned by the current fiber. */
|
||||
on(name: string | symbol, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
||||
if (typeof options !== 'object') {
|
||||
options = { prepend: options }
|
||||
}
|
||||
|
||||
// handle special events
|
||||
this.ctx.fiber.assertActive()
|
||||
listener = this.ctx.reflect.bind(listener)
|
||||
const result = this.bail(this.ctx, 'internal/listener', name, listener, options)
|
||||
if (result) return result
|
||||
|
||||
const hooks = this._hooks[name] ||= []
|
||||
const label = `ctx.on(${typeof name === 'string' ? JSON.stringify(name) : name.toString()})`
|
||||
return this.register(label, hooks, listener, options)
|
||||
}
|
||||
|
||||
/** Register an event listener that disposes itself after the first call. */
|
||||
once(name: string, listener: (...args: any) => any, options?: boolean | EventOptions) {
|
||||
const dispose = this.on(name, function (...args: any[]) {
|
||||
dispose()
|
||||
return listener.apply(this, args)
|
||||
}, options)
|
||||
return dispose
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in framework events used by core services and extension points.
|
||||
*
|
||||
* Plugin and status events track fiber lifecycle, service events observe
|
||||
* dependency registration, update/get/set/listener events allow core services
|
||||
* to intercept runtime operations, and `internal/dispatch` exposes event-bus
|
||||
* diagnostics before public events are delivered.
|
||||
*/
|
||||
export interface Events {
|
||||
'internal/plugin'(fiber: Fiber): void
|
||||
'internal/status'(fiber: Fiber, oldValue: FiberState): void
|
||||
'internal/service'(this: Context, name: string, value: any): void
|
||||
'internal/update'(this: Fiber, config: any, noSave: boolean, next: () => void): void
|
||||
'internal/get'(ctx: Context, name: string, error: Error, next: () => any): any
|
||||
'internal/set'(ctx: Context, name: string, value: any, error: Error, next: () => boolean): boolean
|
||||
'internal/listener'(this: Context, name: string, listener: any, prepend: boolean): void
|
||||
'internal/dispatch'(mode: DispatchMode, name: string, args: any[], thisArg: any): void
|
||||
}
|
||||
504
vendor/cordis/src/fiber.ts
vendored
Normal file
504
vendor/cordis/src/fiber.ts
vendored
Normal file
@@ -0,0 +1,504 @@
|
||||
import { Awaitable, defineProperty, Dict, isNullable } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { Plugin } from './registry'
|
||||
import { buildOuterStack, composeError, DisposableList, getTraceable, isConstructor, isObject, symbols } from './utils'
|
||||
import { Impl } from './reflect'
|
||||
import { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
|
||||
declare module './context' {
|
||||
export interface Context extends Pick<Fiber, 'effect'> {
|
||||
fiber: Fiber
|
||||
}
|
||||
}
|
||||
|
||||
const kValidationError = Symbol.for('ValidationError')
|
||||
|
||||
/** Error raised when plugin configuration fails standard-schema validation. */
|
||||
export class ValidationError extends TypeError {
|
||||
name = 'ValidationError'
|
||||
|
||||
constructor(issues: readonly StandardSchemaV1.Issue[]) {
|
||||
super(`invalid config:\n` + issues.map(issue => {
|
||||
if (issue.path) {
|
||||
return ` - ${issue.message} (at ${issue.path.join('.')})`
|
||||
} else {
|
||||
return ` - ${issue.message}`
|
||||
}
|
||||
}).join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(ValidationError.prototype, kValidationError, {
|
||||
value: true,
|
||||
})
|
||||
|
||||
/** Validate and normalize config for a plugin runtime before it starts. */
|
||||
export function resolveConfig(runtime: Plugin.Runtime, config: any) {
|
||||
if (!runtime.Config) return config
|
||||
// TODO: async validation
|
||||
const result = runtime.Config['~standard'].validate(config)
|
||||
if ('then' in result) {
|
||||
throw new TypeError('Async config validation is not supported')
|
||||
}
|
||||
if (result.issues) {
|
||||
throw new ValidationError(result.issues)
|
||||
} else {
|
||||
return result.value
|
||||
}
|
||||
}
|
||||
|
||||
interface AsyncDisposable<T extends Awaitable<void> = Awaitable<void>> extends PromiseLike<() => T> {
|
||||
(): T
|
||||
}
|
||||
|
||||
/** Function returned by an effect to release resources during disposal. */
|
||||
export type Disposable<T = any> = () => T
|
||||
|
||||
/** Effect body result accepted by `ctx.effect()` and plugin startup. */
|
||||
export type Effect<T = any> =
|
||||
| SyncEffect<T>
|
||||
| AsyncEffect<T>
|
||||
|
||||
type SyncEffect<T = any> =
|
||||
| Disposable<T>
|
||||
| Iterable<Disposable<T>, void, void>
|
||||
|
||||
type AsyncEffect<T = any> =
|
||||
| Promise<Disposable<T>>
|
||||
| AsyncIterable<Disposable<T>, void, void>
|
||||
|
||||
/** Tree node used to expose nested effect labels for diagnostics. */
|
||||
export interface EffectMeta {
|
||||
label: string
|
||||
children: EffectMeta[]
|
||||
}
|
||||
|
||||
interface EffectRunner<T> {
|
||||
epoch: T
|
||||
execute: () => any
|
||||
collect: (dispose: Disposable) => void
|
||||
getOuterStack: () => string[]
|
||||
}
|
||||
|
||||
/** Lifecycle state for one plugin fiber. */
|
||||
export const enum FiberState {
|
||||
PENDING,
|
||||
LOADING,
|
||||
ACTIVE,
|
||||
FAILED,
|
||||
DISPOSED,
|
||||
UNLOADING,
|
||||
}
|
||||
|
||||
/** Framework error with a stable machine-readable code. */
|
||||
export class CordisError extends Error {
|
||||
constructor(public code: CordisError.Code, message?: string) {
|
||||
super(message ?? CordisError.Code[code])
|
||||
}
|
||||
}
|
||||
|
||||
/** Cordis error code definitions. */
|
||||
export namespace CordisError {
|
||||
export type Code = keyof typeof Code
|
||||
|
||||
export const Code = {
|
||||
INACTIVE_EFFECT: 'cannot create effect on inactive context',
|
||||
} as const
|
||||
}
|
||||
|
||||
const INACTIVE = '__INACTIVE__'
|
||||
|
||||
/**
|
||||
* Runtime instance of one plugin application.
|
||||
*
|
||||
* A fiber tracks dependency state, validated config, lifecycle effects, and
|
||||
* cleanup for the plugin context returned by `ctx.plugin()`.
|
||||
*/
|
||||
export class Fiber {
|
||||
public uid: number | null
|
||||
public readonly ctx: Context
|
||||
public config: any
|
||||
public state = FiberState.PENDING
|
||||
public readonly dispose: () => Promise<void>
|
||||
public store: Dict<Impl> | undefined
|
||||
public inertia: Promise<void> | undefined
|
||||
|
||||
public readonly _hooks: Dict<DisposableList<Function>> = Object.create(null)
|
||||
public readonly _disposables = new DisposableList<Disposable>()
|
||||
|
||||
// Same as `this.ctx`, but with a more specific type.
|
||||
protected context: Context
|
||||
|
||||
private _error: any
|
||||
private _runner: EffectRunner<string>
|
||||
private _store: Dict<Impl> = Object.create(null)
|
||||
|
||||
constructor(
|
||||
public parent: Context,
|
||||
config: any,
|
||||
public inject: Dict<any>,
|
||||
public runtime: Plugin.Runtime | null,
|
||||
getOuterStack: () => string[],
|
||||
) {
|
||||
const collect = (dispose: Disposable) => {
|
||||
this._disposables.push(dispose)
|
||||
}
|
||||
|
||||
if (runtime) {
|
||||
this.uid = parent.registry.counter
|
||||
this.ctx = this.context = parent.extend({ fiber: this })
|
||||
|
||||
const injectEntries = Object.entries(this.inject)
|
||||
if (injectEntries.length) {
|
||||
this.ctx[Context.intercept] = Object.create(parent[Context.intercept])
|
||||
for (const [name, config] of injectEntries) {
|
||||
if (isNullable(config)) continue
|
||||
this.ctx[Context.intercept][name] = config
|
||||
}
|
||||
}
|
||||
|
||||
this._runner = {
|
||||
epoch: INACTIVE,
|
||||
getOuterStack,
|
||||
execute: () => {
|
||||
if (isConstructor(runtime.callback)) {
|
||||
// eslint-disable-next-line new-cap
|
||||
const instance = new runtime.callback(this.ctx, this.config)
|
||||
for (const hook of instance?.[symbols.initHooks] ?? []) {
|
||||
hook()
|
||||
}
|
||||
return instance?.[symbols.init]?.()
|
||||
} else {
|
||||
return runtime.callback(this.ctx, this.config)
|
||||
}
|
||||
},
|
||||
collect,
|
||||
}
|
||||
|
||||
this.context.emit('internal/plugin', this)
|
||||
|
||||
for (const name of Object.keys(this.inject)) {
|
||||
this._checkImpl(name)
|
||||
}
|
||||
|
||||
this.dispose = parent.fiber.effect(() => {
|
||||
const remove = runtime.fibers.push(this)
|
||||
try {
|
||||
this.config = resolveConfig(runtime, config)
|
||||
this._refresh()
|
||||
} catch (error) {
|
||||
this.ctx.logger.error(error)
|
||||
this._error = error
|
||||
}
|
||||
return async () => {
|
||||
this.uid = null
|
||||
this.context.emit('internal/plugin', this)
|
||||
if (this.ctx.registry.has(runtime.callback)) {
|
||||
remove()
|
||||
if (!runtime.fibers.length) {
|
||||
this.ctx.registry.delete(runtime.callback)
|
||||
}
|
||||
}
|
||||
this._setEpoch(INACTIVE)
|
||||
// `this.inertia` itself should never reject — both `_reload` and
|
||||
// `_unload` swallow their own work errors via `ctx.logger.error`.
|
||||
// If it *does* reject, the only remaining cause is the logger
|
||||
// itself failing, which we can't recover from in this exact spot
|
||||
// (calling the logger again is what just failed). Let the
|
||||
// rejection propagate; process-level crash is the honest outcome.
|
||||
while (this.inertia) {
|
||||
await this.inertia
|
||||
}
|
||||
}
|
||||
}, 'ctx.plugin()')
|
||||
} else {
|
||||
this.uid = 0
|
||||
this.ctx = this.context = parent
|
||||
this.state = FiberState.ACTIVE
|
||||
this.store = Object.create(null)
|
||||
this._runner = {
|
||||
epoch: '',
|
||||
getOuterStack,
|
||||
execute: () => {},
|
||||
collect,
|
||||
}
|
||||
this.dispose = () => this.restart()
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
let fiber: Fiber = this
|
||||
do {
|
||||
if (fiber.runtime?.name) return fiber.runtime.name
|
||||
fiber = fiber.parent.fiber
|
||||
} while (fiber !== fiber.parent.fiber)
|
||||
return 'root'
|
||||
}
|
||||
|
||||
/** Throw if the fiber has already been disposed. */
|
||||
assertActive() {
|
||||
if (this.uid !== null) return
|
||||
throw new CordisError('INACTIVE_EFFECT')
|
||||
}
|
||||
|
||||
private _execute<T>(runner: EffectRunner<T>) {
|
||||
const oldEpoch = runner.epoch
|
||||
return composeError((info) => {
|
||||
const safeCollect = (dispose: void | Disposable) => {
|
||||
if (typeof dispose === 'function') {
|
||||
runner.collect(dispose)
|
||||
} else if (!isNullable(dispose)) {
|
||||
throw new TypeError('Invalid effect')
|
||||
}
|
||||
}
|
||||
const effect: Effect = runner.execute()
|
||||
if (typeof effect === 'function') {
|
||||
return runner.collect(effect)
|
||||
} else if (isNullable(effect)) {
|
||||
// return
|
||||
} else if (!isObject(effect)) {
|
||||
throw new TypeError('Invalid effect')
|
||||
} else if ('then' in effect) {
|
||||
return effect.then(safeCollect)
|
||||
} else if (Symbol.iterator in effect) {
|
||||
info.error = new Error()
|
||||
const iter = effect[Symbol.iterator]()
|
||||
while (true) {
|
||||
const result = iter.next()
|
||||
safeCollect(result.value)
|
||||
if (result.done) return
|
||||
}
|
||||
} else if (Symbol.asyncIterator in effect) {
|
||||
const iter = effect[Symbol.asyncIterator]()
|
||||
return (async () => {
|
||||
// force async stack trace
|
||||
await Promise.resolve()
|
||||
info.error = new Error()
|
||||
while (true) {
|
||||
if (runner.epoch !== oldEpoch) return
|
||||
const result = await iter.next()
|
||||
safeCollect(result.value)
|
||||
if (result.done) return
|
||||
}
|
||||
})()
|
||||
} else {
|
||||
throw new TypeError('Invalid effect')
|
||||
}
|
||||
}, runner.getOuterStack)
|
||||
}
|
||||
|
||||
/** Register a cleanup-aware effect on this fiber. */
|
||||
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
|
||||
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
|
||||
effect(execute: () => Effect, label = 'anonymous'): any {
|
||||
this.assertActive()
|
||||
|
||||
const disposables: Disposable[] = []
|
||||
const dispose = () => {
|
||||
let task!: void | Promise<void>
|
||||
for (const dispose of disposables.splice(0).reverse()) {
|
||||
if (task) {
|
||||
task = task.then(dispose)
|
||||
} else {
|
||||
const result = dispose()
|
||||
if (isObject(result) && 'then' in result) {
|
||||
task = result as any
|
||||
}
|
||||
}
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
const meta: EffectMeta = { label, children: [] }
|
||||
const runner: EffectRunner<boolean> = {
|
||||
execute,
|
||||
epoch: true,
|
||||
collect: (dispose) => {
|
||||
disposables.push(dispose)
|
||||
this._disposables.delete(dispose)
|
||||
if (dispose[symbols.effect]) {
|
||||
meta.children.push(dispose[symbols.effect])
|
||||
}
|
||||
},
|
||||
getOuterStack: buildOuterStack(),
|
||||
}
|
||||
|
||||
let task: void | Promise<void>
|
||||
try {
|
||||
task = this._execute(runner)
|
||||
} catch (reason) {
|
||||
dispose()
|
||||
throw reason
|
||||
}
|
||||
|
||||
// prevent unhandled rejection — both from `task` itself and from the
|
||||
// disposer chain if it fails to settle cleanly.
|
||||
task?.catch(dispose).catch((error) => this.ctx.logger.error(error))
|
||||
|
||||
const wrapper = defineProperty(() => {
|
||||
if (!runner.epoch) return
|
||||
runner.epoch = false
|
||||
return task ? task.then(dispose) : dispose()
|
||||
}, symbols.effect, meta) as AsyncDisposable
|
||||
|
||||
const disposeAsync = () => {
|
||||
if (!runner.epoch) return
|
||||
runner.epoch = false
|
||||
return dispose()
|
||||
}
|
||||
wrapper.then = async (onFulfilled, onRejected) => {
|
||||
return Promise.resolve(task)
|
||||
.then(() => disposeAsync)
|
||||
.then(onFulfilled, onRejected)
|
||||
}
|
||||
disposables.push(this._disposables.push(wrapper))
|
||||
return wrapper
|
||||
}
|
||||
|
||||
/** Return metadata for currently registered effects. */
|
||||
getEffects() {
|
||||
return [...this._disposables]
|
||||
.map<EffectMeta>(dispose => dispose[symbols.effect])
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
private _getState() {
|
||||
if (this.uid === null) return FiberState.DISPOSED
|
||||
if (this._error) return FiberState.FAILED
|
||||
if (this._runner.epoch !== INACTIVE) return FiberState.ACTIVE
|
||||
return FiberState.PENDING
|
||||
}
|
||||
|
||||
private _updateState(callback: () => void | FiberState) {
|
||||
const oldState = this.state
|
||||
this.state = callback() ?? this._getState()
|
||||
if (oldState === this.state) return
|
||||
// FIXME internal/fiber-info
|
||||
this.context.emit('internal/status', this, oldState)
|
||||
|
||||
// only notify changes between ACTIVE and NON-ACTIVE states
|
||||
if (oldState !== FiberState.ACTIVE && this.state !== FiberState.ACTIVE) return
|
||||
for (const key of Reflect.ownKeys(this.ctx.reflect.store)) {
|
||||
const impl = this.ctx.reflect.store[key as symbol]
|
||||
if (impl.fiber !== this) continue
|
||||
this.ctx.reflect.notify([impl.name])
|
||||
}
|
||||
}
|
||||
|
||||
_checkImpl(name: string) {
|
||||
const impl = this.ctx.reflect._getImpl(name, true)
|
||||
if (!impl) return delete this._store[name]
|
||||
try {
|
||||
if (impl.check && !impl.check.call(getTraceable(this.ctx, impl.value))) {
|
||||
return delete this._store[name]
|
||||
}
|
||||
} catch (error) {
|
||||
impl.fiber.ctx.logger.error(error)
|
||||
return delete this._store[name]
|
||||
}
|
||||
this._store[name] = impl
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
let epoch: string | boolean = false
|
||||
epoch = ''
|
||||
for (const name of Object.keys(this.inject)) {
|
||||
const impl = this._store[name]
|
||||
if (!impl) {
|
||||
epoch = INACTIVE
|
||||
break
|
||||
}
|
||||
epoch += ':' + impl.fiber.uid
|
||||
}
|
||||
this._setEpoch(epoch)
|
||||
}
|
||||
|
||||
private _setEpoch(epoch: string) {
|
||||
const oldEpoch = this._runner.epoch
|
||||
if (epoch === oldEpoch) return
|
||||
this._runner.epoch = epoch
|
||||
if (this.inertia) return
|
||||
this._updateState(() => {
|
||||
if (epoch !== INACTIVE && oldEpoch === INACTIVE) {
|
||||
this.inertia = this._reload()
|
||||
return FiberState.LOADING
|
||||
} else {
|
||||
this.inertia = this._unload()
|
||||
return FiberState.UNLOADING
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async _reload() {
|
||||
this.store = { ...this._store }
|
||||
const oldEpoch = this._runner.epoch
|
||||
try {
|
||||
await Promise.resolve()
|
||||
await this._execute(this._runner)
|
||||
} catch (reason) {
|
||||
// impl guarantees that the error is non-null (?)
|
||||
this.ctx.logger.error(reason)
|
||||
this._error = reason
|
||||
this._runner.epoch = INACTIVE
|
||||
}
|
||||
this._updateState(() => {
|
||||
if (this._runner.epoch === oldEpoch) {
|
||||
this.inertia = undefined
|
||||
} else {
|
||||
this.inertia = this._unload()
|
||||
return FiberState.UNLOADING
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async _unload() {
|
||||
await Promise.all(this._disposables.clear().map(async (dispose) => {
|
||||
try {
|
||||
await composeError(async (info) => {
|
||||
await Promise.resolve()
|
||||
info.error = new Error()
|
||||
await dispose()
|
||||
}, this._runner.getOuterStack)
|
||||
} catch (reason) {
|
||||
this.ctx.logger.error(reason)
|
||||
}
|
||||
}))
|
||||
this.store = undefined
|
||||
this._updateState(() => {
|
||||
if (this._runner.epoch === INACTIVE) {
|
||||
this.inertia = undefined
|
||||
} else {
|
||||
this.inertia = this._reload()
|
||||
return FiberState.LOADING
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Wait for current lifecycle work and rethrow startup errors. */
|
||||
async await() {
|
||||
while (this.inertia) {
|
||||
await this.inertia
|
||||
}
|
||||
if (this._error) throw this._error
|
||||
return this
|
||||
}
|
||||
|
||||
/** Dispose and immediately reload this plugin with its current config. */
|
||||
async restart() {
|
||||
this.assertActive()
|
||||
this._setEpoch(INACTIVE)
|
||||
this._refresh()
|
||||
await this.await()
|
||||
}
|
||||
|
||||
/** Validate and apply new config, then restart the plugin. */
|
||||
update(config: any, noSave = false) {
|
||||
this.assertActive()
|
||||
config = resolveConfig(this.runtime!, config)
|
||||
this.context.waterfall(this, 'internal/update', config, noSave, () => {
|
||||
this.config = config
|
||||
this._error = undefined
|
||||
return this.restart()
|
||||
})
|
||||
}
|
||||
}
|
||||
14
vendor/cordis/src/index.ts
vendored
Normal file
14
vendor/cordis/src/index.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/** Core context type and root context implementation. */
|
||||
export * from './context'
|
||||
/** Event bus, dispatch modes, and event augmentation types. */
|
||||
export * from './events'
|
||||
/** Plugin fiber lifecycle, effects, and config validation helpers. */
|
||||
export * from './fiber'
|
||||
/** Logger facade, logger service, message, exporter, and formatting types. */
|
||||
export * from './logger'
|
||||
/** Plugin registry, dependency injection, and plugin entrypoint types. */
|
||||
export * from './registry'
|
||||
/** Base service class and service lifecycle symbols. */
|
||||
export * from './service'
|
||||
/** Shared internal helpers used by context, services, and plugin fibers. */
|
||||
export * from './utils'
|
||||
262
vendor/cordis/src/logger.ts
vendored
Normal file
262
vendor/cordis/src/logger.ts
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
import { defineProperty, hyphenate } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { Fiber } from './fiber'
|
||||
import { createCallable, joinPrototype, symbols, Tracker } from './utils'
|
||||
|
||||
declare module './context' {
|
||||
interface Intercept {
|
||||
logger: LoggerService.Intercept
|
||||
}
|
||||
}
|
||||
|
||||
/** Logger method name and severity category. */
|
||||
export type LoggerType = 'error' | 'info' | 'warn' | 'debug'
|
||||
|
||||
/** Callable shape for one logger severity method. */
|
||||
export type LoggerMethod = (format: any, ...param: any[]) => void
|
||||
|
||||
/** Formatter used to resolve a printf-style placeholder. */
|
||||
export type Formatter = (value: any, exporter: Exporter, message: Message) => any
|
||||
|
||||
/** Numeric severity used when exporters decide whether to emit a message. */
|
||||
export const enum LoggerLevel {
|
||||
ERROR = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
DEBUG = 3,
|
||||
}
|
||||
|
||||
/** Structured log record delivered to exporters. */
|
||||
export interface Message {
|
||||
sn: number
|
||||
ts: number
|
||||
name: string
|
||||
type: LoggerType
|
||||
level: number
|
||||
args: any[]
|
||||
fiber?: WeakRef<Fiber>
|
||||
}
|
||||
|
||||
/** Sink that receives structured log messages. */
|
||||
export interface Exporter {
|
||||
colors?: number | false
|
||||
maxLength?: number
|
||||
levels?: Record<string, number>
|
||||
formatters?: Record<string, Formatter>
|
||||
export(message: Message): void
|
||||
}
|
||||
|
||||
/** Built-in placeholder formatters used by `Logger.format()`. */
|
||||
export const defaultFormatters: Record<string, Formatter> = {
|
||||
s: (value) => String(value),
|
||||
d: (value) => Math.trunc(Number(value)),
|
||||
i: (value) => Math.trunc(Number(value)),
|
||||
f: (value) => Number(value),
|
||||
o: (value) => JSON.stringify(value),
|
||||
O: (value) => JSON.stringify(value),
|
||||
c: () => '',
|
||||
C: (value, exporter, message) => {
|
||||
return Logger.color(exporter, Logger.code(message.name, exporter.colors), value)
|
||||
},
|
||||
}
|
||||
|
||||
/** Options used when creating a named logger facade. */
|
||||
export interface LoggerOptions {
|
||||
name: string
|
||||
meta?: Partial<Message>
|
||||
level?: number
|
||||
}
|
||||
|
||||
/** Logger facade identity, inherited message metadata, and optional minimum level. */
|
||||
export interface Logger extends LoggerOptions {}
|
||||
/** Logger facade severity methods. */
|
||||
export interface Logger extends Record<LoggerType, LoggerMethod> {}
|
||||
|
||||
function isAggregateError(error: any): error is Error & { errors: Error[] } {
|
||||
return error instanceof Error && Array.isArray(error['errors'])
|
||||
}
|
||||
|
||||
/** Logger facade for one named subsystem. */
|
||||
export class Logger {
|
||||
static color(exporter: Exporter, code: number, value: any, decoration = '') {
|
||||
if (!exporter.colors) return '' + value
|
||||
return `\u001b[3${code < 8 ? code : '8;5;' + code}${exporter.colors >= 2 ? decoration : ''}m${value}\u001b[0m`
|
||||
}
|
||||
|
||||
static code(name: string, level?: false | number) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = ((hash << 3) - hash) + name.charCodeAt(i) + 13
|
||||
hash |= 0
|
||||
}
|
||||
const colors = !level ? [] : level >= 2 ? c256 : c16
|
||||
return colors[Math.abs(hash) % colors.length]
|
||||
}
|
||||
|
||||
static format(exporter: Exporter, message: Message): string {
|
||||
const args = message.args.slice()
|
||||
if (args[0] instanceof Error) {
|
||||
args[0] = args[0].stack || args[0].message
|
||||
args.unshift('%s')
|
||||
} else if (typeof args[0] !== 'string') {
|
||||
args.unshift('%o')
|
||||
}
|
||||
|
||||
let format: string = args.shift()
|
||||
format = format.replace(/%([a-zA-Z%])/g, (match, char) => {
|
||||
if (match === '%%') return '%'
|
||||
const formatter = exporter.formatters?.[char] ?? defaultFormatters[char]
|
||||
if (typeof formatter === 'function') {
|
||||
const value = args.shift()
|
||||
return formatter(value, exporter, message)
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
const oFormatter = exporter.formatters?.o ?? defaultFormatters.o
|
||||
for (let arg of args) {
|
||||
if (typeof arg === 'object' && arg) {
|
||||
arg = oFormatter(arg, exporter, message)
|
||||
}
|
||||
format += ' ' + arg
|
||||
}
|
||||
|
||||
const { maxLength = 10240 } = exporter
|
||||
return format.split(/\r?\n/g).map(line => {
|
||||
return line.slice(0, maxLength) + (line.length > maxLength ? '...' : '')
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
constructor(options: LoggerOptions, private service: LoggerService) {
|
||||
Object.assign(this, options)
|
||||
this.error = this._method('error', LoggerLevel.ERROR)
|
||||
this.info = this._method('info', LoggerLevel.INFO)
|
||||
this.warn = this._method('warn', LoggerLevel.WARN)
|
||||
this.debug = this._method('debug', LoggerLevel.DEBUG)
|
||||
}
|
||||
|
||||
private _method(type: LoggerType, level: number): LoggerMethod {
|
||||
return (...args: any[]) => {
|
||||
if (args.length === 1 && args[0] instanceof Error) {
|
||||
if (args[0].cause) {
|
||||
this[type](args[0].cause)
|
||||
} else if (isAggregateError(args[0])) {
|
||||
args[0].errors.forEach(error => this[type](error))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const sn = ++this.service._snMessage
|
||||
const ts = Date.now()
|
||||
for (const exporter of this.service.exporters.values()) {
|
||||
const targetLevel = exporter.levels?.[this.name] ?? exporter.levels?.default ?? this.level ?? LoggerLevel.INFO
|
||||
if (targetLevel < level) continue
|
||||
const message: Message = { sn, ts, type, level, name: this.name, ...this.meta, args }
|
||||
exporter.export(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** ANSI 16-color palette indexes used for logger name coloring. */
|
||||
export const c16 = [6, 2, 3, 4, 5, 1]
|
||||
/** ANSI 256-color palette indexes used for logger name coloring. */
|
||||
export const c256 = [
|
||||
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62,
|
||||
63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113,
|
||||
129, 134, 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168,
|
||||
169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200,
|
||||
201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221,
|
||||
]
|
||||
|
||||
/** Logger service configuration merged from context intercepts. */
|
||||
export namespace LoggerService {
|
||||
export interface Intercept {
|
||||
name?: string
|
||||
level?: number
|
||||
}
|
||||
}
|
||||
|
||||
/** Callable `ctx.logger` service shape. */
|
||||
export interface LoggerService extends Record<LoggerType, LoggerMethod> {
|
||||
(name?: string): Logger
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in logging service.
|
||||
*
|
||||
* Call `ctx.logger()` to create a named logger, or call `ctx.logger.info()`
|
||||
* directly to log with the current fiber-derived name.
|
||||
*/
|
||||
export class LoggerService {
|
||||
bufferSize = 1000
|
||||
buffer: Message[] = []
|
||||
ctx!: Context
|
||||
|
||||
_snMessage = 0
|
||||
_snExporter = 0
|
||||
exporters = new Map<number, Exporter>()
|
||||
|
||||
constructor(ctx: Context) {
|
||||
const tracker: Tracker = {
|
||||
property: 'ctx',
|
||||
noShadow: true,
|
||||
}
|
||||
const self = createCallable('logger', joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker) as unknown as LoggerService
|
||||
Object.assign(self, this)
|
||||
self.ctx = ctx
|
||||
defineProperty(self, symbols.tracker, tracker)
|
||||
|
||||
self.exporter({
|
||||
colors: 3,
|
||||
export: (message) => {
|
||||
self.buffer.push(message)
|
||||
if (self.buffer.length > self.bufferSize) {
|
||||
self.buffer = self.buffer.slice(-self.bufferSize)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/** Register an exporter and dispose it with the current fiber. */
|
||||
exporter(exporter: Exporter) {
|
||||
return this.ctx.effect(() => {
|
||||
this.exporters.set(++this._snExporter, exporter)
|
||||
return () => this.exporters.delete(this._snExporter)
|
||||
}, 'ctx.logger.exporter()')
|
||||
}
|
||||
|
||||
private _resolveConfig(): LoggerService.Intercept {
|
||||
let intercept = this.ctx[symbols.intercept]
|
||||
const configs: LoggerService.Intercept[] = []
|
||||
while ('logger' in intercept) {
|
||||
if (Object.hasOwn(intercept, 'logger')) {
|
||||
configs.unshift(intercept['logger'])
|
||||
}
|
||||
intercept = Object.getPrototypeOf(intercept)
|
||||
}
|
||||
return Object.assign({}, ...configs)
|
||||
}
|
||||
|
||||
[symbols.invoke](name?: string): Logger {
|
||||
const config = this._resolveConfig()
|
||||
const fiber = ((this.ctx as any)[symbols.shadow] ?? this.ctx).fiber
|
||||
name ??= config.name
|
||||
name ??= hyphenate(fiber.name)
|
||||
return new Logger({
|
||||
name,
|
||||
level: config.level,
|
||||
meta: { fiber: new WeakRef(fiber) },
|
||||
}, this)
|
||||
}
|
||||
|
||||
static {
|
||||
for (const type of ['error', 'info', 'warn', 'debug'] as const) {
|
||||
;(LoggerService.prototype as any)[type] = function (this: LoggerService, ...args: any[]) {
|
||||
return (this as any)()[type](...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
287
vendor/cordis/src/reflect.ts
vendored
Normal file
287
vendor/cordis/src/reflect.ts
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
import { defineProperty, Dict, isNullable } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { getTraceable, symbols, withProps } from './utils'
|
||||
import { Fiber, FiberState } from './fiber'
|
||||
|
||||
declare module './context' {
|
||||
interface Context {
|
||||
get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K]
|
||||
get(name: string, strict?: boolean): any
|
||||
set<K extends string & keyof this>(name: K, value: undefined | this[K]): void
|
||||
set(name: string, value: any): void
|
||||
provide<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void
|
||||
provide(name: string, value?: any): () => void
|
||||
accessor(name: string, options: Omit<Property.Accessor, 'type'>): void
|
||||
mixin<K extends string & keyof this>(name: K, mixins: (keyof this & keyof this[K])[] | Dict<string>): void
|
||||
mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>): void
|
||||
}
|
||||
}
|
||||
|
||||
function enhanceError(error: Error) {
|
||||
const lines = error.stack!.split('\n')
|
||||
lines.splice(0, 2, `Error: ${error.message}`)
|
||||
error.stack = lines.join('\n')
|
||||
return error
|
||||
}
|
||||
|
||||
const RESERVED_WORDS = ['prototype', 'then']
|
||||
|
||||
// - is a symbol
|
||||
// - is a reserved word (prototype, then)
|
||||
// - is a number string (0, 1, 2, ...)
|
||||
// - starts with `_`
|
||||
function isSpecialProperty(prop: string | symbol): prop is symbol {
|
||||
return typeof prop === 'symbol'
|
||||
|| RESERVED_WORDS.includes(prop)
|
||||
|| parseInt(prop).toString() === prop
|
||||
|| prop.startsWith('_')
|
||||
}
|
||||
|
||||
/** Context property definition known by the reflection service. */
|
||||
export type Property = Property.Service | Property.Accessor
|
||||
|
||||
/** Property definition variants understood by `ReflectService`. */
|
||||
export namespace Property {
|
||||
/** Service property backed by a provided implementation. */
|
||||
export interface Service {
|
||||
type: 'service'
|
||||
}
|
||||
|
||||
/** Computed context property backed by custom get/set hooks. */
|
||||
export interface Accessor {
|
||||
type: 'accessor'
|
||||
get: (this: Context, receiver: any, error: Error) => any
|
||||
set?: (this: Context, value: any, receiver: any, error: Error) => boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** Concrete service implementation record stored in the root reflect service. */
|
||||
export interface Impl {
|
||||
name: string
|
||||
fiber: Fiber
|
||||
value?: any
|
||||
check?: () => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflection and service-resolution layer installed as `ctx.reflect`.
|
||||
*
|
||||
* This service powers the context proxy, service registration, accessors, and
|
||||
* the mixins that expose core service methods directly on `ctx`.
|
||||
*/
|
||||
export class ReflectService {
|
||||
static handler: ProxyHandler<Context> = {
|
||||
get: (target, prop, ctx: Context) => {
|
||||
if (isSpecialProperty(prop)) {
|
||||
return Reflect.get(target, prop, ctx)
|
||||
}
|
||||
if (Reflect.has(target, prop)) {
|
||||
return getTraceable(ctx, Reflect.get(target, prop, ctx))
|
||||
}
|
||||
|
||||
const error = new Error(`cannot get property "${prop}" without inject`)
|
||||
|
||||
try {
|
||||
const def = target.reflect.props[prop]
|
||||
if (def?.type === 'accessor') {
|
||||
return def.get.call(ctx, ctx[symbols.receiver], error)
|
||||
}
|
||||
|
||||
if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false)
|
||||
return ctx.events.waterfall('internal/get', ctx, prop, error, () => {
|
||||
const key = target[symbols.isolate][prop]
|
||||
let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber
|
||||
while (true) {
|
||||
const impl = fiber.store?.[prop]
|
||||
if (impl) return getTraceable(ctx, impl.value)
|
||||
if (prop in fiber.inject) {
|
||||
error.message = `cannot get required service "${prop}" in inactive context`
|
||||
throw error
|
||||
}
|
||||
if (!fiber.runtime) throw error
|
||||
if (fiber.parent[symbols.isolate][prop] !== key) throw error
|
||||
fiber = fiber.parent.fiber
|
||||
}
|
||||
})
|
||||
} catch (e: any) {
|
||||
throw e === error ? enhanceError(e) : e
|
||||
}
|
||||
},
|
||||
|
||||
set: (target, prop, value, ctx: Context) => {
|
||||
if (isSpecialProperty(prop)) {
|
||||
return Reflect.set(target, prop, value, ctx)
|
||||
}
|
||||
|
||||
const error = new Error(`cannot set property "${prop}" without provide`)
|
||||
const def = target.reflect.props[prop]
|
||||
if (!def) {
|
||||
if (!ctx.fiber.runtime) return Reflect.set(target, prop, value, ctx)
|
||||
throw enhanceError(error)
|
||||
}
|
||||
|
||||
try {
|
||||
if (def.type === 'accessor') {
|
||||
if (!def.set) return false
|
||||
return def.set.call(ctx, value, ctx[symbols.receiver], error)
|
||||
}
|
||||
|
||||
return ctx.events.waterfall('internal/set', ctx, prop, value, error, () => {
|
||||
return ctx.reflect.set(prop, value, error)
|
||||
})
|
||||
} catch (e: any) {
|
||||
throw e === error ? enhanceError(e) : e
|
||||
}
|
||||
},
|
||||
|
||||
has: (target, prop) => {
|
||||
if (isSpecialProperty(prop)) {
|
||||
return Reflect.has(target, prop)
|
||||
}
|
||||
if (Reflect.has(target, prop)) return true
|
||||
return !!target.reflect.props[prop]
|
||||
},
|
||||
}
|
||||
|
||||
public store: Dict<Impl, symbol> = Object.create(null)
|
||||
public props: Dict<Property> = Object.create(null)
|
||||
|
||||
constructor(public ctx: Context) {
|
||||
defineProperty(this, symbols.tracker, {
|
||||
property: 'ctx',
|
||||
noShadow: true,
|
||||
})
|
||||
|
||||
this.mixin('reflect', ['get', 'set', 'provide', 'accessor', 'mixin'])
|
||||
this.mixin('fiber', ['runtime', 'effect'])
|
||||
this.mixin('registry', ['inject', 'plugin'])
|
||||
this.mixin('events', ['on', 'once', 'parallel', 'emit', 'serial', 'bail', 'waterfall'])
|
||||
}
|
||||
|
||||
get(name: string, strict = true) {
|
||||
return getTraceable(this.ctx, this._getImpl(name, strict)?.value)
|
||||
}
|
||||
|
||||
_getImpl(name: string, strict = true) {
|
||||
const key = this.ctx[symbols.isolate][name]
|
||||
const impl = key && this.store[key]
|
||||
if (!impl) return
|
||||
if (strict && impl.fiber.state !== FiberState.ACTIVE) return
|
||||
return impl
|
||||
}
|
||||
|
||||
set(name: string, value: any, error?: Error) {
|
||||
const key = this.ctx[symbols.isolate][name]
|
||||
const impl = this.store[key]
|
||||
if (!impl) {
|
||||
throw new Error(`cannot set property "${name}" without provide`)
|
||||
}
|
||||
if (impl.fiber !== this.ctx.fiber) {
|
||||
throw new Error(`cannot set property "${name}" in multiple fibers`)
|
||||
}
|
||||
impl.value = value
|
||||
return true
|
||||
}
|
||||
|
||||
provide(name: string, value?: any, check?: () => boolean) {
|
||||
return this.ctx.fiber.effect(() => {
|
||||
if (!this.props[name]) {
|
||||
this.props[name] ??= { type: 'service' }
|
||||
} else if (this.props[name].type !== 'service') {
|
||||
throw new Error(`property "${name}" is already declared as ${this.props[name].type}`)
|
||||
}
|
||||
this.props[name] = { type: 'service' }
|
||||
|
||||
this.ctx.root[symbols.isolate][name] ??= Symbol(name)
|
||||
const key = this.ctx[symbols.isolate][name]
|
||||
const impl: Impl = { name, value, fiber: this.ctx.fiber, check }
|
||||
if (this.store[key]) {
|
||||
throw new Error(`service "${name}" has been registered at <${this.store[key].fiber.name}>`)
|
||||
}
|
||||
this.store[key] = impl
|
||||
this.ctx.fiber.store![name] = impl
|
||||
if (this.ctx.fiber.state === FiberState.ACTIVE) {
|
||||
this.notify([name])
|
||||
}
|
||||
return async () => {
|
||||
delete this.store[key]
|
||||
const fibers = this.notify([name])
|
||||
await Promise.allSettled(fibers.map(fiber => fiber.await()))
|
||||
// ensure self access before dependencies cleanup
|
||||
delete this.ctx.fiber.store![name]
|
||||
}
|
||||
}, `ctx.provide(${JSON.stringify(name)})`)
|
||||
}
|
||||
|
||||
notify(names: string[], filter = (ctx: Context, name: string) => ctx[symbols.isolate][name] === this.ctx[symbols.isolate][name]) {
|
||||
const fibers: Fiber[] = []
|
||||
for (const runtime of this.ctx.registry.values()) {
|
||||
for (const fiber of runtime.fibers) {
|
||||
let hasUpdate = false
|
||||
for (const name of names) {
|
||||
if (!(name in fiber.inject)) continue
|
||||
if (!filter(fiber.ctx, name)) continue
|
||||
hasUpdate = true
|
||||
fiber._checkImpl(name)
|
||||
}
|
||||
if (!hasUpdate) continue
|
||||
fiber._refresh()
|
||||
fibers.push(fiber)
|
||||
}
|
||||
}
|
||||
return fibers
|
||||
}
|
||||
|
||||
accessor(name: string, options: Omit<Property.Accessor, 'type'>) {
|
||||
return this.ctx.fiber.effect(() => {
|
||||
if (name in this.props) {
|
||||
throw new Error(`property "${name}" is already declared as ${this.props[name].type}`)
|
||||
}
|
||||
this.props[name] = { type: 'accessor', ...options }
|
||||
return () => delete this.props[name]
|
||||
}, `ctx.accessor(${JSON.stringify(name)})`)
|
||||
}
|
||||
|
||||
mixin(source: any, mixins: string[] | Dict<string>) {
|
||||
const self = this
|
||||
return this.ctx.fiber.effect(function* () {
|
||||
const entries = Array.isArray(mixins) ? mixins.map(key => [key, key]) : Object.entries(mixins)
|
||||
const getTarget = (ctx: Context, error: Error) => {
|
||||
// TODO enhance error message
|
||||
return ctx[source]
|
||||
}
|
||||
for (const [key, value] of entries) {
|
||||
yield self.accessor(value, {
|
||||
get(receiver, error) {
|
||||
const service = getTarget(this, error)
|
||||
if (isNullable(service)) return service
|
||||
const mixin = receiver ? withProps(receiver, service) : service
|
||||
const value = Reflect.get(service, key, mixin)
|
||||
if (typeof value !== 'function') return value
|
||||
return value.bind(mixin ?? service)
|
||||
},
|
||||
set(value, receiver, error) {
|
||||
const service = getTarget(this, error)
|
||||
const mixin = receiver ? withProps(receiver, service) : service
|
||||
return Reflect.set(service, key, value, mixin)
|
||||
},
|
||||
})
|
||||
}
|
||||
}, `ctx.mixin(${JSON.stringify(source)})`)
|
||||
}
|
||||
|
||||
trace<T>(value: T) {
|
||||
return getTraceable(this.ctx, value)
|
||||
}
|
||||
|
||||
bind<T extends Function>(callback: T) {
|
||||
return new Proxy(callback, {
|
||||
apply: (target, thisArg, args) => {
|
||||
return Reflect.apply(target, this.trace(thisArg), args.map(arg => this.trace(arg)))
|
||||
},
|
||||
construct: (target, args, newTarget) => {
|
||||
return Reflect.construct(target, args.map(arg => this.trace(arg)), newTarget)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
249
vendor/cordis/src/registry.ts
vendored
Normal file
249
vendor/cordis/src/registry.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
import { defineProperty, Dict } from 'cosmokit'
|
||||
import { StandardSchemaV1 } from '@standard-schema/spec'
|
||||
import { Context } from './context'
|
||||
import { Fiber } from './fiber'
|
||||
import { buildOuterStack, DisposableList, symbols, withProps } from './utils'
|
||||
|
||||
function isApplicable(object: Plugin) {
|
||||
return object && typeof object === 'object' && typeof object.apply === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* Service dependency declaration accepted by plugins and the `@Inject`
|
||||
* decorator.
|
||||
*
|
||||
* Array form requests services without intercept config. Object form maps each
|
||||
* service name to optional intercept config for the plugin context.
|
||||
*/
|
||||
export type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }
|
||||
|
||||
/** Context keys that correspond to services with typed intercept config. */
|
||||
export type InjectKey = keyof {
|
||||
[K in keyof Context & string as Context[K] extends { [symbols.config]: any } ? K : never]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for declaring service dependencies on classes or class methods.
|
||||
*
|
||||
* On classes it contributes to the plugin's static `inject` map. On methods it
|
||||
* delays the method call until the declared services are available.
|
||||
*/
|
||||
export function Inject<K extends InjectKey>(name: K, config?: Context[K] extends { [symbols.config]: infer T } ? T : never) {
|
||||
return function (value: any, decorator: ClassDecoratorContext<any> | ClassMethodDecoratorContext<any>) {
|
||||
if (decorator.kind === 'class') {
|
||||
if (!Object.hasOwn(value, 'inject')) {
|
||||
defineProperty(value, 'inject', Object.create(Object.getPrototypeOf(value).inject ?? null))
|
||||
defineProperty(value.inject, symbols.checkProto, true)
|
||||
}
|
||||
value.inject[name] = config
|
||||
} else if (decorator.kind === 'method') {
|
||||
const inject = (value[symbols.metadata] ??= {}).inject ??= Object.create(null)
|
||||
inject[name] = config
|
||||
decorator.addInitializer(function () {
|
||||
const property = this[symbols.tracker]?.property
|
||||
;(this[symbols.initHooks] ??= []).push(() => {
|
||||
(this.ctx as Context).inject(inject, (ctx) => {
|
||||
return value.call(property ? withProps(this, { [property]: ctx }) : this)
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
throw new Error('@Inject() can only be used on class or class methods')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Utilities for normalizing plugin dependency declarations. */
|
||||
export namespace Inject {
|
||||
/** Convert array/object/class-inherited inject metadata into a plain map. */
|
||||
export function resolve(inject: Inject | null | undefined, result: Dict = Object.create(null)) {
|
||||
if (!inject) return result
|
||||
if (Array.isArray(inject)) {
|
||||
for (const name of inject) {
|
||||
result[name] = null
|
||||
}
|
||||
} else if (Reflect.has(inject, symbols.checkProto)) {
|
||||
Object.assign(result, resolve(Object.getPrototypeOf(inject)))
|
||||
for (const name of Object.keys(inject)) {
|
||||
result[name] = inject[name] ?? null
|
||||
}
|
||||
} else {
|
||||
for (const name of Object.keys(inject)) {
|
||||
result[name] = inject[name] ?? null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Supported plugin entrypoint shapes. */
|
||||
export type Plugin<T = any> =
|
||||
| Plugin.Function<T>
|
||||
| Plugin.Constructor<T>
|
||||
| Plugin.Object<T>
|
||||
|
||||
/** Types associated with plugin entrypoints and runtime records. */
|
||||
export namespace Plugin {
|
||||
/** Shared metadata understood by the plugin registry and related tooling. */
|
||||
export interface Base<T = any> {
|
||||
name?: string
|
||||
Config?: StandardSchemaV1<any, T>
|
||||
inject?: Inject
|
||||
provide?: string | string[]
|
||||
intercept?: Dict<boolean>
|
||||
}
|
||||
|
||||
export interface Transform<S, T> {
|
||||
/** Marks the transform object as a schema/config transform. */
|
||||
schema?: true
|
||||
/** Convert user-facing config to runtime config. */
|
||||
Config: (config: S) => T
|
||||
}
|
||||
|
||||
/** Function plugin called with `(ctx, config)`. */
|
||||
export interface Function<T = any> extends Base<T> {
|
||||
(ctx: Context, config: T): any
|
||||
}
|
||||
|
||||
/** Class plugin constructed with `(ctx, config)`. */
|
||||
export interface Constructor<T = any> extends Base<T> {
|
||||
new (ctx: Context, config: T): any
|
||||
}
|
||||
|
||||
/** Object plugin with an `apply(ctx, config)` method. */
|
||||
export interface Object<T = any> extends Base<T> {
|
||||
apply(ctx: Context, config: T): any
|
||||
}
|
||||
|
||||
/** Mutable registry record shared by all fibers of one plugin callback. */
|
||||
export interface Runtime {
|
||||
name?: string
|
||||
fibers: DisposableList<Fiber>
|
||||
callback: globalThis.Function
|
||||
Config?: StandardSchemaV1
|
||||
}
|
||||
}
|
||||
|
||||
type Spread<T> = undefined extends T ? [config?: T] : [config: T]
|
||||
|
||||
type GetPluginParameters<P> =
|
||||
| P extends (ctx: Context, ...args: infer R) => any
|
||||
? R
|
||||
: P extends new (ctx: Context, ...args: infer R) => any
|
||||
? R
|
||||
: P extends { apply(ctx: Context, ...args: infer R): any }
|
||||
? R
|
||||
: never
|
||||
|
||||
type GetPluginConfig<P> =
|
||||
| P extends Plugin.Transform<infer S, any>
|
||||
? S
|
||||
: GetPluginParameters<P>[0]
|
||||
|
||||
declare module './context' {
|
||||
export interface Context {
|
||||
inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
|
||||
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin registry installed as `ctx.registry` and mixed into every context.
|
||||
*
|
||||
* It normalizes plugin shapes, tracks plugin runtimes, starts fibers, and
|
||||
* exposes map-like inspection over active plugin callbacks.
|
||||
*/
|
||||
export class RegistryService {
|
||||
private _counter = 0
|
||||
private _internal = new Map<Function, Plugin.Runtime>()
|
||||
|
||||
constructor(public ctx: Context) {
|
||||
defineProperty(this, symbols.tracker, {
|
||||
property: 'ctx',
|
||||
noShadow: true,
|
||||
})
|
||||
}
|
||||
|
||||
get counter() {
|
||||
return ++this._counter
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._internal.size
|
||||
}
|
||||
|
||||
/** Resolve a supported plugin shape to its executable callback. */
|
||||
resolve(plugin: Plugin): Function | undefined {
|
||||
// plugin.apply may throw
|
||||
try {
|
||||
if (typeof plugin === 'function') return plugin
|
||||
if (isApplicable(plugin)) return plugin.apply
|
||||
} catch {}
|
||||
}
|
||||
|
||||
get(plugin: Plugin) {
|
||||
const key = this.resolve(plugin)
|
||||
return key && this._internal.get(key)
|
||||
}
|
||||
|
||||
has(plugin: Plugin) {
|
||||
const key = this.resolve(plugin)
|
||||
return !!key && this._internal.has(key)
|
||||
}
|
||||
|
||||
/** Dispose every running fiber for a plugin and remove its runtime record. */
|
||||
delete(plugin: Plugin) {
|
||||
const key = this.resolve(plugin)
|
||||
const runtime = key && this._internal.get(key)
|
||||
if (!runtime) return
|
||||
this._internal.delete(key)
|
||||
for (const fiber of runtime.fibers) {
|
||||
fiber.dispose()
|
||||
}
|
||||
return runtime
|
||||
}
|
||||
|
||||
keys() {
|
||||
return this._internal.keys()
|
||||
}
|
||||
|
||||
values() {
|
||||
return this._internal.values()
|
||||
}
|
||||
|
||||
entries() {
|
||||
return this._internal.entries()
|
||||
}
|
||||
|
||||
forEach(callback: (value: Plugin.Runtime, key: Function) => void) {
|
||||
return this._internal.forEach(callback)
|
||||
}
|
||||
|
||||
/** Start a callback once the requested dependencies are available. */
|
||||
inject(inject: Inject, callback: Plugin.Function<void>) {
|
||||
return this.plugin({ inject, apply: callback, name: callback.name })
|
||||
}
|
||||
|
||||
/** Start a plugin in the current context and return its fiber. */
|
||||
plugin(plugin: Plugin, config?: any, getOuterStack = buildOuterStack()) {
|
||||
// check if it's a valid plugin
|
||||
const callback = this.resolve(plugin)
|
||||
if (!callback) throw new Error('invalid plugin, expect function or object with an "apply" method, received ' + typeof plugin)
|
||||
this.ctx.fiber.assertActive()
|
||||
|
||||
let runtime = this._internal.get(callback)
|
||||
if (!runtime) {
|
||||
let name = plugin.name
|
||||
if (name === 'apply') name = undefined
|
||||
runtime = { name, callback, fibers: new DisposableList(), Config: plugin.Config }
|
||||
this._internal.set(callback, runtime)
|
||||
}
|
||||
|
||||
const fiber = new Fiber(this.ctx, config, Inject.resolve(plugin.inject), runtime, getOuterStack)
|
||||
const wrapped = Object.create(fiber) as Fiber & PromiseLike<Fiber>
|
||||
wrapped.then = (onFulfilled, onRejected) => {
|
||||
return fiber.await().then(onFulfilled, onRejected)
|
||||
}
|
||||
return wrapped
|
||||
}
|
||||
}
|
||||
88
vendor/cordis/src/service.ts
vendored
Normal file
88
vendor/cordis/src/service.ts
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
import { defineProperty } from 'cosmokit'
|
||||
import { Context } from './context'
|
||||
import { createCallable, joinPrototype, symbols, Tracker } from './utils'
|
||||
|
||||
/**
|
||||
* Base class for services that expose a named API on `ctx`.
|
||||
*
|
||||
* Subclasses call `super(ctx, name)` from their constructor. The service is
|
||||
* registered immediately and is automatically removed with the owning fiber.
|
||||
*/
|
||||
export abstract class Service<out T = never> {
|
||||
static readonly init: unique symbol = symbols.init
|
||||
static readonly check: unique symbol = symbols.check
|
||||
static readonly config: unique symbol = symbols.config
|
||||
static readonly invoke: unique symbol = symbols.invoke
|
||||
static readonly extend: unique symbol = symbols.extend
|
||||
static readonly tracker: unique symbol = symbols.tracker
|
||||
static readonly resolveConfig: unique symbol = symbols.resolveConfig
|
||||
|
||||
declare [symbols.config]: T
|
||||
|
||||
public name!: string
|
||||
|
||||
/** Register this instance as `name` in the current context. */
|
||||
constructor(protected ctx: Context, name: string) {
|
||||
name ??= this.constructor['provide'] as string
|
||||
|
||||
let self = this
|
||||
const tracker: Tracker = {
|
||||
associate: name,
|
||||
property: 'ctx',
|
||||
}
|
||||
if (self[symbols.invoke]) {
|
||||
self = createCallable(name, joinPrototype(Object.getPrototypeOf(this), Function.prototype), tracker)
|
||||
}
|
||||
self.ctx = ctx
|
||||
self.name = name
|
||||
defineProperty(self, symbols.tracker, tracker)
|
||||
|
||||
self.ctx.reflect.provide(name, self, this[symbols.check])
|
||||
return self
|
||||
}
|
||||
|
||||
protected [symbols.filter](ctx: Context) {
|
||||
return ctx[symbols.isolate][this.name] === this.ctx[symbols.isolate][this.name]
|
||||
}
|
||||
|
||||
protected [symbols.extend](props?: any) {
|
||||
let self: any
|
||||
if (this[Service.invoke]) {
|
||||
self = createCallable(this.name, this, this[symbols.tracker])
|
||||
} else {
|
||||
self = Object.create(this)
|
||||
}
|
||||
return Object.assign(self, props)
|
||||
}
|
||||
|
||||
/** Merge intercept config from ancestors with optional base and head values. */
|
||||
[symbols.resolveConfig](base?: T, head?: T): T {
|
||||
let intercept = this.ctx[Context.intercept]
|
||||
const configs: any[] = []
|
||||
while (this.name in intercept) {
|
||||
if (Object.hasOwn(intercept, this.name)) {
|
||||
configs.unshift(intercept[this.name])
|
||||
}
|
||||
intercept = Object.getPrototypeOf(intercept)
|
||||
}
|
||||
if (base) configs.unshift(base)
|
||||
if (head) configs.push(head)
|
||||
if (this['Config']?.merge) {
|
||||
return this['Config'].merge(...configs)
|
||||
} else {
|
||||
return Object.assign({}, ...configs)
|
||||
}
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance](instance: any) {
|
||||
if (!instance) return false
|
||||
let constructor = instance.constructor
|
||||
while (constructor) {
|
||||
// constructor may be a proxy
|
||||
constructor = constructor.prototype?.constructor
|
||||
if (constructor === this) return true
|
||||
constructor &&= Object.getPrototypeOf(constructor)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
287
vendor/cordis/src/utils.ts
vendored
Normal file
287
vendor/cordis/src/utils.ts
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
import { defineProperty } from 'cosmokit'
|
||||
import type { Context, Service } from '.'
|
||||
|
||||
/** Ordered collection of disposable values with O(1) deletion by value. */
|
||||
export class DisposableList<T extends WeakKey> {
|
||||
private sn = 0
|
||||
private map = new Map<number, T>()
|
||||
private weak = new WeakMap<T, number>()
|
||||
|
||||
get length() {
|
||||
return this.map.size
|
||||
}
|
||||
|
||||
push(value: T) {
|
||||
const sn = ++this.sn
|
||||
this.map.set(sn, value)
|
||||
this.weak.set(value, sn)
|
||||
return () => this.map.delete(sn)
|
||||
}
|
||||
|
||||
delete(value: T) {
|
||||
const sn = this.weak.get(value)
|
||||
if (!sn) return false
|
||||
return this.map.delete(sn)
|
||||
}
|
||||
|
||||
clear() {
|
||||
const values = [...this.map.values()]
|
||||
this.map.clear()
|
||||
return values.reverse()
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.map.values()
|
||||
}
|
||||
|
||||
[Symbol.for('nodejs.util.inspect.custom')]() {
|
||||
return [...this]
|
||||
}
|
||||
}
|
||||
|
||||
/** Metadata used by traceable proxies to rebind `ctx` and associated services. */
|
||||
export interface Tracker {
|
||||
associate?: string
|
||||
property?: string
|
||||
noShadow?: boolean
|
||||
}
|
||||
|
||||
/** Shared symbols used to avoid public property-name collisions. */
|
||||
export const symbols = {
|
||||
// internal symbols
|
||||
shadow: Symbol.for('cordis.shadow'),
|
||||
receiver: Symbol.for('cordis.receiver'),
|
||||
original: Symbol.for('cordis.original'),
|
||||
metadata: Symbol.for('cordis.metadata'),
|
||||
initHooks: Symbol.for('cordis.initHooks'),
|
||||
checkProto: Symbol.for('cordis.checkProto'),
|
||||
|
||||
// context symbols
|
||||
effect: Symbol.for('cordis.effect') as typeof Context.effect,
|
||||
filter: Symbol.for('cordis.filter') as typeof Context.filter,
|
||||
isolate: Symbol.for('cordis.isolate') as typeof Context.isolate,
|
||||
intercept: Symbol.for('cordis.intercept') as typeof Context.intercept,
|
||||
|
||||
// service symbols
|
||||
init: Symbol.for('cordis.init') as typeof Service.init,
|
||||
check: Symbol.for('cordis.check') as typeof Service.check,
|
||||
config: Symbol.for('cordis.config') as typeof Service.config,
|
||||
invoke: Symbol.for('cordis.invoke') as typeof Service.invoke,
|
||||
extend: Symbol.for('cordis.extend') as typeof Service.extend,
|
||||
tracker: Symbol.for('cordis.tracker') as typeof Service.tracker,
|
||||
resolveConfig: Symbol.for('cordis.resolveConfig') as typeof Service.resolveConfig,
|
||||
}
|
||||
|
||||
const GeneratorFunction = function* () {}.constructor
|
||||
const AsyncGeneratorFunction = async function* () {}.constructor
|
||||
|
||||
/** Return true when a plugin callback should be constructed with `new`. */
|
||||
export function isConstructor(func: any): func is new (...args: any) => any {
|
||||
// async function or arrow function
|
||||
if (!func.prototype) return false
|
||||
// generator function or malformed definition
|
||||
// we cannot use below check because `mock.fn()` is proxied
|
||||
// if (func.prototype.constructor !== func) return false
|
||||
if (func instanceof GeneratorFunction) return false
|
||||
// polyfilled AsyncGeneratorFunction === Function
|
||||
if (AsyncGeneratorFunction !== Function && func instanceof AsyncGeneratorFunction) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/** Merge two prototype chains while preserving descriptors from `proto1`. */
|
||||
export function joinPrototype(proto1: {}, proto2: {}) {
|
||||
if (proto1 === Object.prototype) return proto2
|
||||
const result = Object.create(joinPrototype(Object.getPrototypeOf(proto1), proto2))
|
||||
for (const key of Reflect.ownKeys(proto1)) {
|
||||
Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(proto1, key)!)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Return true for non-null objects and functions. */
|
||||
export function isObject(value: any): value is {} {
|
||||
return value && (typeof value === 'object' || typeof value === 'function')
|
||||
}
|
||||
|
||||
/** Find a property descriptor by walking an object's prototype chain. */
|
||||
export function getPropertyDescriptor(target: any, prop: string | symbol) {
|
||||
let proto = target
|
||||
while (proto) {
|
||||
const desc = Reflect.getOwnPropertyDescriptor(proto, prop)
|
||||
if (desc) return desc
|
||||
proto = Object.getPrototypeOf(proto)
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrap services/functions so method calls see the caller's active context. */
|
||||
export function getTraceable<T>(ctx: Context, value: T): T {
|
||||
if (!isObject(value)) return value
|
||||
if (Object.hasOwn(value, symbols.shadow)) {
|
||||
return Object.getPrototypeOf(value)
|
||||
}
|
||||
const tracker = value[symbols.tracker]
|
||||
if (!tracker) return value
|
||||
return createTraceable(ctx, value, tracker)
|
||||
}
|
||||
|
||||
/** Return a proxy that overlays readonly or writable properties onto a target. */
|
||||
export function withProps(target: any, props?: {}) {
|
||||
if (!props) return target
|
||||
return new Proxy(target, {
|
||||
get: (target, prop, receiver) => {
|
||||
if (prop in props && prop !== 'constructor') return Reflect.get(props, prop, receiver)
|
||||
return Reflect.get(target, prop, receiver)
|
||||
},
|
||||
set: (target, prop, value, receiver) => {
|
||||
if (prop in props && prop !== 'constructor') return Reflect.set(props, prop, value, receiver)
|
||||
return Reflect.set(target, prop, value, receiver)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function withProp(target: any, prop: string | symbol, value: any) {
|
||||
return withProps(target, Object.defineProperty(Object.create(null), prop, {
|
||||
value,
|
||||
writable: false,
|
||||
}))
|
||||
}
|
||||
|
||||
function createShadow(ctx: Context, target: any, property: string | undefined, receiver: any) {
|
||||
if (!property) return receiver
|
||||
const origin = Reflect.getOwnPropertyDescriptor(target, property)?.value
|
||||
if (!origin) return receiver
|
||||
return withProp(receiver, property, ctx.extend({ [symbols.shadow]: origin }))
|
||||
}
|
||||
|
||||
function createShadowMethod(ctx: Context, value: any, outer: any, shadow: {}) {
|
||||
return new Proxy(value, {
|
||||
apply: (target, thisArg, args) => {
|
||||
if (thisArg === outer) thisArg = shadow
|
||||
return getTraceable(ctx, Reflect.apply(target, thisArg, args))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function createTraceable(ctx: Context, value: any, tracker: Tracker) {
|
||||
// noShadow services are identity-aware (e.g. logger uses the origin fiber to
|
||||
// derive its name): keep the shadow ctx so they can read [symbols.shadow]
|
||||
// and resolve the origin. Non-noShadow services strip — their side effects
|
||||
// bind to caller, not origin.
|
||||
if (ctx[symbols.shadow] && !tracker.noShadow) {
|
||||
ctx = Object.getPrototypeOf(ctx)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) {
|
||||
return Reflect.get(ctx, `${tracker.associate}.${prop}`, withProp(ctx, symbols.receiver, receiver))
|
||||
}
|
||||
let shadow: any, innerValue: any
|
||||
const desc = getPropertyDescriptor(target, prop)
|
||||
if (desc && 'value' in desc) {
|
||||
innerValue = desc.value
|
||||
} else {
|
||||
shadow = createShadow(ctx, target, tracker.property, receiver)
|
||||
innerValue = Reflect.get(target, prop, shadow)
|
||||
}
|
||||
const innerTracker = innerValue?.[symbols.tracker]
|
||||
if (innerTracker) {
|
||||
return createTraceable(ctx, innerValue, innerTracker)
|
||||
} else if (!tracker.noShadow && typeof innerValue === 'function') {
|
||||
shadow ??= createShadow(ctx, target, tracker.property, receiver)
|
||||
return createShadowMethod(ctx, innerValue, receiver, shadow)
|
||||
} else {
|
||||
return innerValue
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) {
|
||||
return Reflect.set(ctx, `${tracker.associate}.${prop}`, value, withProp(ctx, symbols.receiver, receiver))
|
||||
}
|
||||
const shadow = createShadow(ctx, target, tracker.property, receiver)
|
||||
return Reflect.set(target, prop, value, shadow)
|
||||
},
|
||||
apply: (target, thisArg, args) => {
|
||||
return applyTraceable(proxy, target, thisArg, args)
|
||||
},
|
||||
})
|
||||
return proxy
|
||||
}
|
||||
|
||||
function applyTraceable(proxy: any, value: any, thisArg: any, args: any[]) {
|
||||
if (!value[symbols.invoke]) return Reflect.apply(value, thisArg, args)
|
||||
return value[symbols.invoke].apply(proxy, args)
|
||||
}
|
||||
|
||||
/** Create a callable service object that dispatches through `symbols.invoke`. */
|
||||
export function createCallable(name: string, proto: {}, tracker: Tracker) {
|
||||
const self = function (...args: any[]) {
|
||||
const proxy = createTraceable(self['ctx'], self, tracker)
|
||||
return applyTraceable(proxy, self, this, args)
|
||||
}
|
||||
defineProperty(self, 'name', name)
|
||||
return Object.setPrototypeOf(self, proto)
|
||||
}
|
||||
|
||||
interface StackInfo {
|
||||
offset: number
|
||||
error: Error
|
||||
}
|
||||
|
||||
function handleError(info: StackInfo, reason: any, getOuterStack: () => string[]): never {
|
||||
const innerLines = info.error.stack!.split('\n')
|
||||
|
||||
// malformed error
|
||||
if (typeof reason?.stack !== 'string') {
|
||||
const outerError = new Error(reason)
|
||||
const lines = outerError.stack!.split('\n')
|
||||
lines.splice(1, Infinity, ...getOuterStack())
|
||||
outerError.stack = lines.join('\n')
|
||||
throw outerError
|
||||
}
|
||||
|
||||
// long stack trace
|
||||
const lines: string[] = reason.stack.split('\n')
|
||||
let index = lines.indexOf(innerLines[2])
|
||||
if (index === -1) throw reason
|
||||
|
||||
index -= info.offset
|
||||
while (index > 0) {
|
||||
if (!lines[index - 1].endsWith(' (<anonymous>)')) break
|
||||
index -= 1
|
||||
}
|
||||
lines.splice(index, Infinity, ...getOuterStack())
|
||||
reason.stack = lines.join('\n')
|
||||
throw reason
|
||||
}
|
||||
|
||||
/** Run a callback and splice outer call-site frames into thrown async errors. */
|
||||
export function composeError<T>(callback: (info: StackInfo) => T, getOuterStack = buildOuterStack()): T {
|
||||
const info: StackInfo = { offset: 1, error: new Error() }
|
||||
|
||||
try {
|
||||
const result: any = callback(info)
|
||||
if (isObject(result) && 'then' in result) {
|
||||
return (result as any).then(undefined, (reason) => handleError(info, reason, getOuterStack)) as T
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
} catch (reason: any) {
|
||||
handleError(info, reason, getOuterStack)
|
||||
}
|
||||
}
|
||||
|
||||
/** Capture a lazy stack-frame supplier for later error composition. */
|
||||
export function buildOuterStack(offset = 0) {
|
||||
const outerError = new Error()
|
||||
return () => outerError.stack!.split('\n').slice(3 + offset)
|
||||
}
|
||||
Reference in New Issue
Block a user