index.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. /**
  2. * Configurable registry for package-owned runtime invariant contributions.
  3. * Every workspace package registers checks from a `./invariant` companion;
  4. * ordinary package entrypoints stay independent of diagnostics.
  5. *
  6. * @module @deepseek-ai/dsh-invariants
  7. */
  8. import { Context, Service } from '@deepseek-ai/cordis'
  9. import type { Inject } from '@deepseek-ai/cordis'
  10. import z from '@deepseek-ai/schemastery'
  11. import type Schema from '@deepseek-ai/schemastery'
  12. /** Runtime invariant selection configured on the service plugin. */
  13. export interface Config {
  14. /** Global switch; defaults to `true`. */
  15. readonly enabled?: boolean
  16. /** Case-sensitive JavaScript regex sources that admit package names; empty admits all. */
  17. readonly package_allowlist?: string[]
  18. /** Case-sensitive JavaScript regex sources that exclude package names after allowlist matching. */
  19. readonly package_blocklist?: string[]
  20. }
  21. /**
  22. * Throw a package-attributed invariant failure.
  23. * @param message - violated package contract without the standard prefix.
  24. * @returns never because reporting a violation throws.
  25. */
  26. export type InvariantFailure = (message: string) => never
  27. /** Install one package's checks into the registration's child context. */
  28. export interface InvariantInstaller {
  29. /**
  30. * Install the package contribution.
  31. * @param ctx - child context owned by this invariant registration.
  32. * @param fail - reporter bound to the registering package name.
  33. * @returns nothing, or a promise settling after asynchronous checks finish.
  34. */
  35. (ctx: Context, fail: InvariantFailure): void | Promise<void>
  36. /** Services the child installer fiber may access. */
  37. readonly inject?: Inject
  38. }
  39. /** Internal effect shape used to join child startup before a companion loads. */
  40. interface PendingInvariantRegistration extends PromiseLike<() => void> {
  41. (): void | Promise<void>
  42. }
  43. /** Thrown when a package-owned runtime invariant is violated. */
  44. export class InvariantError extends Error {
  45. /** Stable machine-readable invariant failure code. */
  46. readonly code = 'INVARIANT' as const
  47. /** Full npm package name that owns the violated invariant. */
  48. readonly packageName: string
  49. /**
  50. * Construct a package-attributed invariant failure.
  51. * @param packageName - full npm package name that registered the check.
  52. * @param message - violated contract, without the standard error prefix.
  53. */
  54. constructor(packageName: string, message: string) {
  55. super(`invariant violated by "${packageName}": ${message}`)
  56. this.name = 'InvariantError'
  57. this.packageName = packageName
  58. }
  59. }
  60. declare module '@deepseek-ai/cordis' {
  61. interface Context {
  62. invariants: InvariantRegistry
  63. }
  64. }
  65. /** Compile and validate one package-filter list. */
  66. function compilePatterns(field: 'package_allowlist' | 'package_blocklist', values: readonly string[]): RegExp[] {
  67. const seen = new Set<string>()
  68. return values.map((value) => {
  69. if (value.length === 0 || value.trim() !== value) {
  70. throw new Error(`invariants: ${field} entries must be non-blank and have no surrounding whitespace`)
  71. }
  72. if (seen.has(value)) {
  73. throw new Error(`invariants: ${field} contains duplicate regex ${JSON.stringify(value)}`)
  74. }
  75. seen.add(value)
  76. try {
  77. return new RegExp(value)
  78. } catch (cause) {
  79. throw new Error(`invariants: ${field} contains invalid regex ${JSON.stringify(value)}`, { cause })
  80. }
  81. })
  82. }
  83. /** Package-owned invariant registry with global and regex-based selection. */
  84. export class InvariantRegistry extends Service {
  85. static Config: Schema<Config> = z.object({
  86. enabled: z.boolean().default(true),
  87. package_allowlist: z.array(z.string()).default([]),
  88. package_blocklist: z.array(z.string()).default([]),
  89. })
  90. private readonly enabled: boolean
  91. private readonly ownerCtx: Context
  92. private readonly packageAllowlist: readonly RegExp[]
  93. private readonly packageBlocklist: readonly RegExp[]
  94. private readonly registrations = new Set<string>()
  95. /**
  96. * Create and install the invariant registry.
  97. * @param ctx - Cordis context that owns the service.
  98. * @param config - global enablement and package-name regex filters.
  99. */
  100. constructor(ctx: Context, config: Config = {}) {
  101. super(ctx, 'invariants')
  102. this.ownerCtx = ctx
  103. this.enabled = config.enabled ?? true
  104. this.packageAllowlist = compilePatterns('package_allowlist', config.package_allowlist ?? [])
  105. this.packageBlocklist = compilePatterns('package_blocklist', config.package_blocklist ?? [])
  106. }
  107. /** Return whether one full package name passes the configured filters. */
  108. private selected(packageName: string): boolean {
  109. if (!this.enabled) return false
  110. if (this.packageAllowlist.length > 0
  111. && !this.packageAllowlist.some(pattern => pattern.test(packageName))) return false
  112. return !this.packageBlocklist.some(pattern => pattern.test(packageName))
  113. }
  114. /**
  115. * Register one package's invariant installer. The package name is reserved
  116. * even when filtering disables its checks. Enabled installers run in a child
  117. * fiber; failure disposes that fiber and releases the reservation.
  118. * @param packageName - full npm package name that owns the contribution.
  119. * @param installer - listener or startup-check installer for the child context.
  120. * @returns an effect-scoped disposer for the registration.
  121. */
  122. register(packageName: string, installer: InvariantInstaller): () => void {
  123. if (packageName.length === 0 || packageName.trim() !== packageName || /\s/.test(packageName)) {
  124. throw new Error('invariants: packageName must be non-blank and contain no whitespace')
  125. }
  126. if (this.registrations.has(packageName)) {
  127. throw new Error(`invariants: package "${packageName}" is already registered`)
  128. }
  129. // Service method tracing binds `this.ctx` to the caller. This explicit
  130. // origin keeps registrations and their child fibers owned by the service;
  131. // companion disposal is covered independently by the returned disposer.
  132. const ctx = this.ownerCtx
  133. const registrations = this.registrations
  134. registrations.add(packageName)
  135. let registration: PendingInvariantRegistration
  136. try {
  137. registration = ctx.effect(async () => {
  138. if (!this.selected(packageName)) {
  139. return () => {
  140. registrations.delete(packageName)
  141. }
  142. }
  143. const installInvariant = (childCtx: Context) => (
  144. installer(childCtx, (message): never => {
  145. throw new InvariantError(packageName, message)
  146. })
  147. )
  148. try {
  149. const child = ctx.plugin(installer.inject === undefined
  150. ? installInvariant
  151. : Object.assign(installInvariant, { inject: installer.inject }))
  152. try {
  153. await child
  154. } catch (error) {
  155. await child.dispose()
  156. throw error
  157. }
  158. return async () => {
  159. try {
  160. await child.dispose()
  161. } finally {
  162. registrations.delete(packageName)
  163. }
  164. }
  165. } catch (error) {
  166. registrations.delete(packageName)
  167. throw error
  168. }
  169. }, `invariants.register(${JSON.stringify(packageName)})`)
  170. } catch (error) {
  171. registrations.delete(packageName)
  172. throw error
  173. }
  174. // Cordis attaches setup thenability and async teardown to this callable;
  175. // the service contract intentionally exposes only the conventional disposer.
  176. // oxlint-disable-next-line typescript/no-misused-promises -- the extra runtime shape stays private.
  177. return registration
  178. }
  179. }
  180. export default InvariantRegistry