Ctrl K
ComunidadGuías…

packages/codemode/src/extension.ts

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
export * as Extension from "./extension.js"

import type { Effect } from "effect"

/**
 * Host values a program reaches directly as globals: functions, or objects of functions such as `fs`. Values crossing
 * in either direction are converted, never shared: arguments come in as copies, results go out as copies, and a
 * function inside a result is callable the same way. Extension calls are not tool calls. A global may return a value,
 * a Promise, or an Effect, and an Effect's requirements join the runtime's environment.
 *
 * A global is synchronous unless it is an `async` function or is marked with `async`. The `extension.before` hook
 * runs before the call, so a denied call never reaches the host; it fails the way the global would, by rejecting the
 * promise of an asynchronous global and by throwing from a synchronous one. Functions inside a global object are
 * called the same way, named by their path (`fs.readFile`).
 *
 * Programs can also `import` or `require` a global by its Node module name; see `interpreter/modules.ts`.
 */
export type Extension<Globals extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>> = {
  readonly name: string
  readonly globals: Globals
  /** How a suspended run saves the values this extension's functions returned (`engine: "vm"` only); see `Snapshot`. */
  readonly snapshot?: Snapshot | undefined
}

/**
 * Lets a suspended run keep the functions inside values this extension's functions returned (a response's `text`,
 * say). `encode` gets such a value, as the host function returned it, and gives plain data: JSON values and
 * `Uint8Array`s. On resume, `decode` gets that data back, once per value, and gives a value of the same shape, whose
 * functions the program's copies call from then on. Without it, those functions are lost on resume: calling one
 * throws. A suspend fails, and the run goes on, when `encode` throws or gives anything else.
 */
export type Snapshot = {
  readonly encode: (value: unknown) => unknown
  readonly decode: (data: unknown) => unknown
}

/** The Effect environment required by an extension's Effect-returning functions, at the top level or one level in. */
export type Services<E> = E extends { readonly globals: infer Globals } ? GlobalServices<Globals[keyof Globals]> : never

type GlobalServices<Global> = Global extends (...args: never) => unknown
  ? Requirements<Global>
  : Global extends object
    ? Requirements<Global[keyof Global]>
    : never

type Requirements<F> = F extends (...args: never) => infer Result ? Effect.Services<Result> : never

export const make = <const Globals extends Readonly<Record<string, unknown>>>(
  options: Extension<Globals>,
): Extension<Globals> => {
  for (const [name, value] of Object.entries(options.globals)) {
    if (typeof value !== "function" && (typeof value !== "object" || value === null)) {
      throw new TypeError(`Extension "${options.name}" global "${name}" must be a function or an object.`)
    }
  }
  return {
    name: options.name,
    globals: { ...options.globals },
    ...(options.snapshot === undefined ? {} : { snapshot: options.snapshot }),
  }
}

const asyncGlobals = new WeakSet<Function>()
const AsyncFunction = (async () => {}).constructor

/** Marks a global that returns a Promise or an Effect, so a denied call rejects its promise instead of throwing. */
export const async = <F extends Function>(global: F): F => {
  asyncGlobals.add(global)
  return global
}

export const isAsync = (global: Function) => global instanceof AsyncFunction || asyncGlobals.has(global)