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> = Readonly>> = { 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 extends { readonly globals: infer Globals } ? GlobalServices : never type GlobalServices = Global extends (...args: never) => unknown ? Requirements : Global extends object ? Requirements : never type Requirements = F extends (...args: never) => infer Result ? Effect.Services : never export const make = >>( options: Extension, ): Extension => { 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() 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 = (global: F): F => { asyncGlobals.add(global) return global } export const isAsync = (global: Function) => global instanceof AsyncFunction || asyncGlobals.has(global)