UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

361 lines • 14.1 kB
/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ const DEFAULT_EVENTS_PER_BATCH = 10_000; const DEFAULT_MAX_BATCH_BYTES = 32 * 1024 * 1024; // ASCII character byte constants used by the state machine. // All values are strictly less than 0x80 (standard ASCII). // In UTF-8 encoding, multi-byte code units only contain bytes from 0x80 through 0xFF. // Therefore, scanning for these byte values will never match bytes inside multi-byte characters. const BYTE_QUOTE = 0x22; const BYTE_BACKSLASH = 0x5c; const BYTE_OPEN_BRACE = 0x7b; const BYTE_CLOSE_BRACE = 0x7d; const BYTE_OPEN_BRACKET = 0x5b; const BYTE_CLOSE_BRACKET = 0x5d; const BYTE_COLON = 0x3a; const BYTE_COMMA = 0x2c; /** * Advances past any RFC 8259 JSON whitespace bytes. * * @param buffer - The trace byte buffer to inspect. * @param start - The buffer index where scanning begins. * @returns The index of the first non-whitespace byte, or the buffer length if the buffer ends. */ function skipWhitespace(buffer, start) { let i = start; while (i < buffer.length) { const byte = buffer[i]; if (byte !== 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0d) { break; } i++; } return i; } /** * Scans past a JSON string starting at the opening quotation mark. * * @param buffer - The trace byte buffer to scan. * @param startQuotePos - The buffer index of the opening quotation mark. * @returns The index immediately following the closing quotation mark. * @throws {SyntaxError} If the string is unterminated before the end of the buffer. */ function skipString(buffer, startQuotePos) { let pos = startQuotePos + 1; while (pos < buffer.length) { const byte = buffer[pos]; if (byte === BYTE_BACKSLASH) { if (pos + 1 >= buffer.length) { throw new SyntaxError('Unterminated string in JSON'); } pos += 2; continue; } if (byte === BYTE_QUOTE) { return pos + 1; } pos++; } throw new SyntaxError('Unterminated string in JSON'); } /** * Scans past a complete JSON value starting at the specified buffer position. * Skips strings, compound objects, arrays, and primitive values without parsing them into memory. * * @param buffer - The trace byte buffer to scan. * @param startPos - The buffer index where the value begins. * @returns The index immediately following the skipped JSON value. * @throws {SyntaxError} If the value contains unbalanced delimiters, unterminated strings, or empty primitive tokens. */ function skipValue(buffer, startPos) { let pos = skipWhitespace(buffer, startPos); if (pos >= buffer.length) { return pos; } const firstByte = buffer[pos]; if (firstByte === BYTE_QUOTE) { return skipString(buffer, pos); } if (firstByte === BYTE_OPEN_BRACE || firstByte === BYTE_OPEN_BRACKET) { const delimiterStack = []; delimiterStack.push(firstByte === BYTE_OPEN_BRACE ? BYTE_CLOSE_BRACE : BYTE_CLOSE_BRACKET); pos++; while (pos < buffer.length) { const byte = buffer[pos]; if (byte === BYTE_QUOTE) { pos = skipString(buffer, pos); continue; } if (byte === BYTE_OPEN_BRACE) { delimiterStack.push(BYTE_CLOSE_BRACE); } else if (byte === BYTE_OPEN_BRACKET) { delimiterStack.push(BYTE_CLOSE_BRACKET); } else if (byte === BYTE_CLOSE_BRACE || byte === BYTE_CLOSE_BRACKET) { const expected = delimiterStack.pop(); if (expected !== byte) { throw new SyntaxError('Unexpected delimiter in JSON'); } if (delimiterStack.length === 0) { pos++; return pos; } } pos++; } throw new SyntaxError('Unexpected end of JSON input'); } const valStart = pos; while (pos < buffer.length) { const byte = buffer[pos]; if (byte === BYTE_COMMA || byte === BYTE_CLOSE_BRACE || byte === BYTE_CLOSE_BRACKET || byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d) { break; } pos++; } if (pos === valStart) { throw new SyntaxError('Expected JSON value'); } return pos; } /** * Scans trace events from a JSON array in bounded batches to limit memory consumption. * * @param buffer - The raw byte buffer containing the events array. * @param startPos - The index immediately after the opening array bracket. * @param config - Configuration settings controlling batch sizes and text decoding. * @returns An object containing the parsed events and the ending buffer position. * @throws {SyntaxError} If the event array contains malformed JSON, unbalanced delimiters, or unexpected tokens. */ function parseEventsArray(buffer, startPos, config) { const events = []; let batchStart = -1; let lastEventEnd = -1; let batchEventCount = 0; let depth = 0; let i = startPos; let closed = false; let state = 'expect_element_or_end'; const flushBatch = () => { if (batchStart !== -1 && lastEventEnd > batchStart) { // Slicing between batchStart and lastEventEnd is safe for UTF-8 decoding. // Both boundaries are ASCII structural braces ({ and }) that align with code unit boundaries. const slice = buffer.subarray(batchStart, lastEventEnd); const chunkText = config.decoder.decode(slice); // Wrapping comma-delimited event objects in brackets creates a valid JSON array for the parser. const parsed = JSON.parse(`[${chunkText}]`); // Append items iteratively to avoid exceeding V8 call stack size limits on large batches. for (const event of parsed) { events.push(event); } batchStart = -1; batchEventCount = 0; } }; while (i < buffer.length) { const byte = buffer[i]; if (depth > 0) { if (byte === BYTE_QUOTE) { i = skipString(buffer, i); continue; } if (byte === BYTE_OPEN_BRACE) { depth++; i++; continue; } if (byte === BYTE_CLOSE_BRACE) { depth--; if (depth === 0) { lastEventEnd = i + 1; batchEventCount++; state = 'expect_comma_or_end'; // Check thresholds after recording the completed event. // Checking batchEventCount after incrementing ensures batches cap at exactly eventsPerBatch. // Individual events are atomic JSON objects and cannot be split mid-event; when an event // pushes currentBatchBytes over maxBatchBytes, the completed event is flushed immediately. const currentBatchBytes = lastEventEnd - batchStart; if (batchEventCount >= config.eventsPerBatch || currentBatchBytes >= config.maxBatchBytes) { flushBatch(); } } i++; continue; } i++; continue; } // When depth is 0, scan top-level array delimiter and separator tokens. if (byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d) { i++; continue; } if (byte === BYTE_OPEN_BRACE) { if (state !== 'expect_element_or_end' && state !== 'expect_element') { throw new SyntaxError('Expected "," or "]" in JSON array'); } depth = 1; if (batchStart === -1) { batchStart = i; } i++; continue; } if (byte === BYTE_COMMA) { if (state !== 'expect_comma_or_end') { throw new SyntaxError('Unexpected token "," in JSON array'); } state = 'expect_element'; i++; continue; } if (byte === BYTE_CLOSE_BRACKET) { if (state === 'expect_element') { throw new SyntaxError('Unexpected token "]" in JSON array'); } closed = true; i++; break; } if (byte === BYTE_CLOSE_BRACE) { throw new SyntaxError('Unexpected token "}" in JSON'); } throw new SyntaxError('Unexpected token in JSON array'); } if (!closed || depth !== 0) { throw new SyntaxError('Unexpected end of JSON input'); } flushBatch(); return { events, endPos: i }; } /** * Parses Chrome DevTools trace events and metadata from a raw byte buffer. * Supports both root array format ([...]) and object container format ({"traceEvents": [...]}). * Parses events in chunks to prevent V8 string length and memory exhaustion errors. * * @param buffer - The raw trace buffer to parse. * @param options - Optional configuration settings for batch processing. * @returns A {@link ParsedTraceBuffer} containing parsed trace events and optional file metadata, or an object with an empty events array if the buffer is empty or contains only whitespace. * @throws {Error} If the buffer contains non-whitespace data that does not begin with a valid JSON array or object. * @throws {SyntaxError} If the underlying JSON syntax within batches or metadata is invalid. */ export function parseTraceEventsFromBuffer(buffer, options = {}) { const eventsPerBatch = typeof options.eventsPerBatch === 'number' && Number.isFinite(options.eventsPerBatch) && options.eventsPerBatch > 0 ? options.eventsPerBatch : DEFAULT_EVENTS_PER_BATCH; const maxBatchBytes = typeof options.maxBatchBytes === 'number' && Number.isFinite(options.maxBatchBytes) && options.maxBatchBytes > 0 ? options.maxBatchBytes : DEFAULT_MAX_BATCH_BYTES; const decoder = new TextDecoder(); const config = { eventsPerBatch, maxBatchBytes, decoder, }; let pos = skipWhitespace(buffer, 0); if (pos >= buffer.length) { return { events: [] }; } const firstByte = buffer[pos]; if (firstByte === BYTE_OPEN_BRACKET) { const { events, endPos } = parseEventsArray(buffer, pos + 1, config); const trailingPos = skipWhitespace(buffer, endPos); if (trailingPos < buffer.length) { throw new SyntaxError('Unexpected non-whitespace character after JSON'); } return { events }; } if (firstByte === BYTE_OPEN_BRACE) { pos++; let events = []; let metadata; let closed = false; let expectComma = false; while (pos < buffer.length) { pos = skipWhitespace(buffer, pos); if (pos >= buffer.length) { break; } if (buffer[pos] === BYTE_CLOSE_BRACE) { closed = true; pos++; break; } if (expectComma) { if (buffer[pos] !== BYTE_COMMA) { throw new SyntaxError('Expected "," or "}" after property value in JSON'); } pos++; pos = skipWhitespace(buffer, pos); } if (pos >= buffer.length) { break; } if (buffer[pos] !== BYTE_QUOTE) { throw new SyntaxError('Expected property name or "}" in JSON'); } const keyStart = pos; pos = skipString(buffer, pos); const keyJson = decoder.decode(buffer.subarray(keyStart, pos)); const parsedKey = JSON.parse(keyJson); if (typeof parsedKey !== 'string') { throw new SyntaxError('Expected property name in JSON'); } const key = parsedKey; pos = skipWhitespace(buffer, pos); if (pos >= buffer.length || buffer[pos] !== BYTE_COLON) { throw new SyntaxError('Expected ":" after property name in JSON'); } pos++; pos = skipWhitespace(buffer, pos); if (key === 'traceEvents') { if (pos >= buffer.length || buffer[pos] !== BYTE_OPEN_BRACKET) { throw new SyntaxError('Expected "[" for "traceEvents" in JSON'); } const arrayResult = parseEventsArray(buffer, pos + 1, config); events = arrayResult.events; pos = arrayResult.endPos; } else if (key === 'metadata') { if (pos >= buffer.length || buffer[pos] !== BYTE_OPEN_BRACE) { throw new SyntaxError('Expected "{" for "metadata" in JSON'); } const metaStart = pos; pos = skipValue(buffer, pos); const metaSlice = buffer.subarray(metaStart, pos); const metaText = decoder.decode(metaSlice); const parsedMetadata = JSON.parse(metaText); metadata = parsedMetadata; } else { pos = skipValue(buffer, pos); } expectComma = true; } if (!closed) { throw new SyntaxError('Unexpected end of JSON input'); } const trailingPos = skipWhitespace(buffer, pos); if (trailingPos < buffer.length) { throw new SyntaxError('Unexpected non-whitespace character after JSON'); } return { events, metadata }; } throw new Error('Invalid trace buffer: expected JSON object or array.'); } //# sourceMappingURL=ChunkedTraceParser.js.map