projects/start-sdk/lib/dependencies.ts
Rama master · réplica de lectura del repositorio original.
import { VersionRange } from '@start9labs/start-core/exver'
import { checkDependencies } from '@start9labs/start-core/dependencies/dependencies'
import type {
Effects,
DependencyRequirement,
Manifest,
LocaleString,
} from '@start9labs/start-core/types'
import {
runReactiveInit,
setupOnInit,
} from '@start9labs/start-core/inits/setupInit'
import type {
InitKind,
InitScript,
InitScriptOrFn,
} from '@start9labs/start-core/inits/setupInit'
import { deepEqual } from '@start9labs/start-core/util/deepEqual'
import { FullProgressTracker } from '@start9labs/start-core/util/FullProgressTracker'
type Base = {
description: LocaleString | null
metadata: { title: LocaleString; icon: string }
versionRange: string
} & (
| { kind: 'running'; healthChecks: string[] }
| { kind: 'exists'; healthChecks?: never }
)
type Narrowing = {
versionRange?: string
kind?: 'running'
healthChecks?: string[]
}
/** A published base requirement with optional reactive runtime restrictions. */
export class Dependency<Id extends string = string> {
private narrowing?: (options: {
effects: Effects
}) => Promise<Narrowing | null>
private readonly inits: InitScript[] = []
private enabledFn?: (options: { effects: Effects }) => Promise<boolean>
private constructor(
readonly id: Id,
readonly optional: boolean,
readonly base: Base,
) {
VersionRange.parse(base.versionRange)
}
/** Publishes a requirement that is always active. */
static required<const Id extends string>(id: Id, base: Base) {
return new Dependency(id, false, base)
}
/** Publishes a requirement activated by the service's configuration. */
static optional<const Id extends string>(
id: Id,
base: Base & {
enabled: (options: { effects: Effects }) => Promise<boolean>
},
) {
const { enabled, ...fields } = base
const dependency = new Dependency(id, true, fields)
dependency.enabledFn = enabled
return dependency
}
/** Restricts the published requirement for the active configuration. */
withDynamicNarrowing(
fn: (options: { effects: Effects }) => Promise<Narrowing | null>,
) {
this.narrowing = fn
return this
}
/** Appends an independent reactive init, run while the dependency is enabled. */
withInit(fn: InitScriptOrFn) {
this.inits.push(setupOnInit(fn))
return this
}
async enabled(effects: Effects): Promise<boolean> {
return !this.enabledFn || (await this.enabledFn({ effects }))
}
initHandlers(): readonly InitScript[] {
return this.inits
}
manifestInfo(): Manifest['dependencies'][string] {
return {
description: this.base.description,
optional: this.optional,
versionRange: this.base.versionRange,
kind: this.base.kind,
...(this.base.kind === 'running'
? { healthChecks: this.base.healthChecks }
: {}),
metadata: this.base.metadata,
}
}
/** The runtime requirement while enabled. */
async requirement(effects: Effects): Promise<DependencyRequirement> {
const narrowed = await this.narrowing?.({ effects })
const baseRange = VersionRange.parse(this.base.versionRange)
const runtimeRange = narrowed?.versionRange
? VersionRange.parse(narrowed.versionRange)
: baseRange
if (!baseRange.intersects(runtimeRange)) {
throw new Error(`Dependency ${this.id} has an incompatible version range`)
}
if (
narrowed?.healthChecks?.length &&
this.base.kind === 'exists' &&
narrowed.kind !== 'running'
) {
throw new Error(
`Dependency ${this.id} has health checks without a running requirement`,
)
}
const kind =
this.base.kind === 'running' || narrowed?.kind === 'running'
? 'running'
: 'exists'
const versionRange = narrowed?.versionRange
? baseRange.and(runtimeRange).toString()
: this.base.versionRange
const requirement: DependencyRequirement =
kind === 'running'
? {
id: this.id,
kind,
versionRange,
healthChecks: [
...(this.base.kind === 'running' ? this.base.healthChecks : []),
...(narrowed?.healthChecks || []),
],
}
: { id: this.id, kind, versionRange }
return requirement
}
}
/** A single dependency definition for the manifest and runtime. */
export class Dependencies<Ids extends string = never> implements InitScript {
private constructor(private readonly entries: Dependency[]) {}
static of() {
return new Dependencies([])
}
addDependency<const Id extends string>(
dependency: Dependency<Id>,
): Dependencies<Ids | Id> {
if (this.entries.some(entry => entry.id === dependency.id)) {
throw new Error(`Duplicate dependency ${dependency.id}`)
}
return new Dependencies<Ids | Id>([...this.entries, dependency])
}
/** The dependency metadata embedded in the package manifest. */
manifestDependencies(): Manifest['dependencies'] {
return Object.fromEntries(
this.entries.map(entry => [entry.id, entry.manifestInfo()]),
)
}
async init(
effects: Effects,
kind: InitKind = null,
progress?: FullProgressTracker,
): Promise<void> {
const active = new Map<string, DependencyRequirement>()
let ready = false
let published: DependencyRequirement[] | null = null
let lastPublish = Promise.resolve()
const publish = () => {
const next = lastPublish.then(async () => {
const dependencies = this.entries.flatMap(entry => {
const requirement = active.get(entry.id)
return requirement ? [requirement] : []
})
if (deepEqual(published, dependencies)) return
await effects.setDependencies({ dependencies })
published = dependencies
})
lastPublish = next.then(
() => {},
() => {},
)
return next
}
const setRequirement = async (
id: string,
requirement: DependencyRequirement | null,
) => {
if (requirement) active.set(id, requirement)
else active.delete(id)
if (ready) await publish()
}
const starters: (() => Promise<void> | false)[] = []
for (const entry of this.entries) {
let enabled = false
let generation = 0
let initsStarted = -1
const startInits = async (
gen: number,
initKind: InitKind,
initProgress?: FullProgressTracker,
) => {
if (gen !== generation || initsStarted === gen) return
initsStarted = gen
for (const [index, handler] of entry.initHandlers().entries()) {
await runReactiveInit(
effects,
`dependency_${entry.id}_init_${index}`,
handler,
initKind,
initProgress,
() => gen === generation,
)
}
}
await runReactiveInit(
effects,
`dependency_${entry.id}_enabled`,
async child => {
const next = await entry.enabled(child)
if (next === enabled) return
enabled = next
const gen = ++generation
if (!next) return setRequirement(entry.id, null)
await runReactiveInit(
effects,
`dependency_${entry.id}`,
async child => {
const requirement = await entry.requirement(child)
if (gen === generation)
await setRequirement(entry.id, requirement)
},
null,
undefined,
() => gen === generation,
)
if (ready) await startInits(gen, null, new FullProgressTracker())
},
null,
)
starters.push(() => enabled && startInits(generation, kind, progress))
}
ready = true
await publish()
for (const start of starters) await start()
}
/** Checks the active runtime requirements. */
check(effects: Effects, packageIds?: Ids[]) {
return checkDependencies<Ids>(effects, packageIds)
}
}