/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Message, MessageWriterOptions} from "vscode-jsonrpc/node";
/**
* The content-type encoder shape vscode-jsonrpc's {@link MessageWriterOptions} accepts. Derived
* from the writer options rather than deep-imported, because the concrete `ContentTypeEncoder`
* type is not re-exported from the package entrypoint.
*/
export type JsonRpcContentTypeEncoder = NonNullable;
// Flush the accumulated JSON text to a Buffer once it reaches this many UTF-16 code units. This
// keeps the transient string far below V8's ~512 MB String.kMaxLength while bounding how many
// intermediate Buffers we concatenate. A single scalar/key fragment is never split, so the pending
// string only ever exceeds the threshold by one whole fragment.
const FLUSH_THRESHOLD = 1 << 20; // 1 MiB
/**
* True for values that {@code JSON.stringify} drops: an object property with such a value is
* omitted, and such an array element serializes as {@code null}.
*/
function isJsonIgnored(value: unknown): boolean {
return value === undefined || typeof value === "function" || typeof value === "symbol";
}
/**
* Yields a value as the sequence of JSON text fragments that concatenate to exactly
* {@code JSON.stringify(value)}, without ever building the whole document as one string. Every
* scalar and every object key is serialized by the engine's {@code JSON.stringify} — so escaping,
* number formatting, {@code undefined}/function/symbol handling, and {@code toJSON} all match it
* exactly — and only the structural punctuation is emitted here.
*/
export function* jsonFragments(value: unknown): Generator {
// Mirror JSON.stringify's toJSON step before deciding container vs. scalar, so e.g. a Date
// collapses to its string form rather than being walked as an object.
if (value !== null && typeof value === "object" && typeof (value as { toJSON?: unknown }).toJSON === "function") {
yield* jsonFragments((value as { toJSON(): unknown }).toJSON());
return;
}
if (Array.isArray(value)) {
yield "[";
for (let i = 0; i < value.length; i++) {
if (i > 0) {
yield ",";
}
if (isJsonIgnored(value[i])) {
yield "null";
} else {
yield* jsonFragments(value[i]);
}
}
yield "]";
return;
}
if (value !== null && typeof value === "object") {
yield "{";
let first = true;
for (const key of Object.keys(value as object)) {
const propValue = (value as Record)[key];
if (isJsonIgnored(propValue)) {
continue;
}
if (!first) {
yield ",";
}
first = false;
yield JSON.stringify(key);
yield ":";
yield* jsonFragments(propValue);
}
yield "}";
return;
}
// Scalar: string, number, boolean, or null (a bigint throws here, exactly as JSON.stringify does).
yield JSON.stringify(value) as string;
}
/**
* A drop-in replacement for vscode-jsonrpc's default `application/json` content-type encoder that
* serializes a message to UTF-8 bytes without ever materializing the whole document as a single JS
* string. The default encoder does {@code Buffer.from(JSON.stringify(msg))}; a large enough
* PrepareRecipe response (a deep recipe tree) overflows V8's single-string limit and throws
* "RangeError: Invalid string length", hanging the RPC call. Streaming the
* fragments into a Buffer raises the ceiling to Node's Buffer.MAX_LENGTH (multiple GB) while
* producing byte-identical output. (True unbounded streaming needs chunked framing — Content-Length
* still requires the whole body up front — so this is a ceiling raise, not a removal.)
*/
export const chunkedJsonEncoder: JsonRpcContentTypeEncoder = {
name: "application/json",
encode(msg: Message, options: { charset: BufferEncoding }): Promise {
const buffers: Buffer[] = [];
let pending = "";
for (const fragment of jsonFragments(msg)) {
pending += fragment;
if (pending.length >= FLUSH_THRESHOLD) {
buffers.push(Buffer.from(pending, options.charset));
pending = "";
}
}
if (pending.length > 0) {
buffers.push(Buffer.from(pending, options.charset));
}
return Promise.resolve(Buffer.concat(buffers));
}
};