mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-08 18:51:24 +03:00
120 lines
5.2 KiB
TypeScript
120 lines
5.2 KiB
TypeScript
export * as Data from "./data.js"
|
|
|
|
import type { Schema } from "effect"
|
|
import type { Interpreter } from "./interpreter/interpreter.js"
|
|
import { MAX_VALUE_DEPTH } from "./interpreter/limits.js"
|
|
import { invalidData, rangeError, typeError } from "./interpreter/model.js"
|
|
import {
|
|
Arr,
|
|
Bytes,
|
|
Callable,
|
|
define,
|
|
ErrorObj,
|
|
get,
|
|
keys,
|
|
Obj,
|
|
PromiseObj,
|
|
record,
|
|
type Value,
|
|
SetObj,
|
|
URLSearchParamsObj,
|
|
HeadersObj,
|
|
} from "./interpreter/objects.js"
|
|
import { typeofValue } from "./interpreter/references.js"
|
|
import { type Eval, Slice } from "./interpreter/runner.js"
|
|
|
|
export type Json = Schema.Json
|
|
|
|
type Replacer = (args: Array<Value>, holder: Obj) => Eval<Value>
|
|
|
|
/**
|
|
* What `JSON.stringify` would serialize for a program value, as host JSON: `toJSON` is honored, functions and
|
|
* `undefined` vanish, non-finite numbers become null, and everything else is copied. Two departures from JS
|
|
* so a mistake is not a silent `{}`: an Error serializes as `{ name, message, ...own }`, and a promise throws.
|
|
*/
|
|
export const toJson = <R>(ctx: Interpreter<R>, value: Value, replacer?: Replacer) => walk(ctx, value, replacer, false)
|
|
|
|
/**
|
|
* The host boundary: `toJson`, plus what a program most likely meant when a value cannot be JSON. A promise is
|
|
* awaited, a Set crosses as an array, a URLSearchParams as its query string, a Uint8Array asks to be encoded as
|
|
* text first, and a `__proto__` key is dropped so host code can never receive one.
|
|
*/
|
|
export const toBoundary = <R>(ctx: Interpreter<R>, value: Value) => walk(ctx, value, undefined, true)
|
|
|
|
const walk = <R>(
|
|
ctx: Interpreter<R>,
|
|
value: Value,
|
|
replacer: Replacer | undefined,
|
|
boundary: boolean,
|
|
): Eval<Json | undefined> => {
|
|
const stack = new Set<object>()
|
|
const visit = function* (holder: Obj, key: string, depth: number): Eval<Json | undefined> {
|
|
// Shared references expand into a tree, so the work is not bounded by the value's size or depth.
|
|
if (ctx.runner.tick()) yield Slice
|
|
if (depth > MAX_VALUE_DEPTH) throw rangeError(`Value exceeds the maximum depth of ${MAX_VALUE_DEPTH}.`)
|
|
const raw = get(holder, key)
|
|
if (raw instanceof PromiseObj && !boundary) {
|
|
throw invalidData(
|
|
"JSON.stringify received an un-awaited Promise; await it first - e.g. `const result = await tools.ns.tool(...)`.",
|
|
)
|
|
}
|
|
const settled = raw instanceof PromiseObj ? yield* ctx.awaitValue(raw) : raw
|
|
const toJSON = settled instanceof Obj ? get(settled, "toJSON") : undefined
|
|
const own = toJSON instanceof Callable ? yield* ctx.call(toJSON, settled, [key]) : settled
|
|
const value = replacer === undefined ? own : yield* replacer([key, own], holder)
|
|
if (value === undefined || typeofValue(value) === "function") return undefined
|
|
if (typeof value === "number") return Number.isFinite(value) ? value : null
|
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value
|
|
if (!(value instanceof Obj)) return {}
|
|
if (boundary && value instanceof Bytes) {
|
|
throw invalidData(
|
|
"A Uint8Array cannot cross to the host; pass text instead, e.g. `new TextDecoder().decode(bytes)` or `bytes.toBase64()`.",
|
|
)
|
|
}
|
|
if (boundary && value instanceof URLSearchParamsObj) return value.params.toString()
|
|
if (value instanceof HeadersObj) return Object.fromEntries(value.headers)
|
|
const target = boundary && value instanceof SetObj ? new Arr(ctx.builtins.Array, [...value.set]) : value
|
|
if (stack.has(target)) throw typeError("Converting circular structure to JSON.")
|
|
stack.add(target)
|
|
if (target instanceof Arr) {
|
|
const items: Array<Json> = []
|
|
for (let index = 0; index < target.items.length; index += 1) {
|
|
items.push((yield* visit(target, String(index), depth + 1)) ?? null)
|
|
}
|
|
stack.delete(target)
|
|
return items
|
|
}
|
|
const copied: Record<string, Json> = {}
|
|
// Own data property regardless of the key, so "__proto__" never reaches the Object.prototype setter.
|
|
const put = (name: string, item: Json | undefined) => {
|
|
if (item !== undefined)
|
|
Object.defineProperty(copied, name, { value: item, enumerable: true, writable: true, configurable: true })
|
|
}
|
|
// Errors serialize as { name, message, ...own }: both may be inherited, and neither is enumerable in JS.
|
|
if (target instanceof ErrorObj) {
|
|
put("name", yield* visit(target, "name", depth + 1))
|
|
put("message", yield* visit(target, "message", depth + 1))
|
|
}
|
|
for (const name of keys(target)) {
|
|
if (boundary && name === "__proto__") continue
|
|
put(name, yield* visit(target, name, depth + 1))
|
|
}
|
|
stack.delete(target)
|
|
return copied
|
|
}
|
|
return visit(record(ctx.builtins.Object, { "": value }), "", 0)
|
|
}
|
|
|
|
/** Host JSON as program values: objects and arrays are copied, primitives pass through. */
|
|
export const fromJson = <R>(ctx: Interpreter<R>, value: Json | undefined): Value => {
|
|
if (value === null || typeof value !== "object") return value
|
|
if (Array.isArray(value))
|
|
return new Arr(
|
|
ctx.builtins.Array,
|
|
value.map((item) => fromJson(ctx, item)),
|
|
)
|
|
const copied = new Obj(ctx.builtins.Object)
|
|
for (const [key, item] of Object.entries(value)) define(copied, key, fromJson(ctx, item))
|
|
return copied
|
|
}
|