feat: add Pi process and RPC foundation
This commit is contained in:
91
electron/coding-runtime/pi/rpc-framer.ts
Normal file
91
electron/coding-runtime/pi/rpc-framer.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { PiProcessError } from './process-errors';
|
||||
|
||||
export const DEFAULT_MAX_PI_RPC_LINE_BYTES = 1024 * 1024;
|
||||
|
||||
type StrictLfJsonlFramerOptions = {
|
||||
maxLineBytes?: number;
|
||||
onRecord(record: unknown): void;
|
||||
};
|
||||
|
||||
function protocolError(message: string): PiProcessError {
|
||||
return new PiProcessError('PI_RPC_PROTOCOL_ERROR', message);
|
||||
}
|
||||
|
||||
export class StrictLfJsonlFramer {
|
||||
private readonly decoder = new TextDecoder('utf-8', { fatal: true });
|
||||
private readonly maxLineBytes: number;
|
||||
private readonly onRecord: (record: unknown) => void;
|
||||
private buffered = Buffer.alloc(0);
|
||||
private finished = false;
|
||||
|
||||
constructor(options: StrictLfJsonlFramerOptions) {
|
||||
const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_PI_RPC_LINE_BYTES;
|
||||
if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) {
|
||||
throw new Error('maxLineBytes must be a positive safe integer');
|
||||
}
|
||||
this.maxLineBytes = maxLineBytes;
|
||||
this.onRecord = options.onRecord;
|
||||
}
|
||||
|
||||
push(chunk: Uint8Array | string): void {
|
||||
if (this.finished) throw protocolError('Pi RPC stdout continued after stream end');
|
||||
const bytes = typeof chunk === 'string'
|
||||
? Buffer.from(chunk, 'utf8')
|
||||
: Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
||||
let offset = 0;
|
||||
|
||||
while (offset < bytes.length) {
|
||||
const newline = bytes.indexOf(0x0a, offset);
|
||||
if (newline === -1) {
|
||||
this.append(bytes.subarray(offset));
|
||||
return;
|
||||
}
|
||||
this.append(bytes.subarray(offset, newline));
|
||||
this.emitLine();
|
||||
offset = newline + 1;
|
||||
}
|
||||
}
|
||||
|
||||
finish(): void {
|
||||
if (this.finished) return;
|
||||
this.finished = true;
|
||||
if (this.buffered.length > 0) {
|
||||
throw protocolError('Pi RPC stdout ended with a partial line');
|
||||
}
|
||||
}
|
||||
|
||||
private append(segment: Uint8Array): void {
|
||||
if (this.buffered.length + segment.byteLength > this.maxLineBytes) {
|
||||
throw protocolError(`Pi RPC stdout line exceeded ${this.maxLineBytes} bytes`);
|
||||
}
|
||||
if (segment.byteLength === 0) return;
|
||||
this.buffered = this.buffered.length === 0
|
||||
? Buffer.from(segment)
|
||||
: Buffer.concat([this.buffered, segment], this.buffered.length + segment.byteLength);
|
||||
}
|
||||
|
||||
private emitLine(): void {
|
||||
let line = this.buffered;
|
||||
this.buffered = Buffer.alloc(0);
|
||||
if (line.at(-1) === 0x0d) line = line.subarray(0, -1);
|
||||
if (line.length === 0) throw protocolError('Pi RPC stdout emitted a blank line');
|
||||
|
||||
let source: string;
|
||||
try {
|
||||
source = this.decoder.decode(line);
|
||||
} catch (error) {
|
||||
throw new PiProcessError('PI_RPC_PROTOCOL_ERROR', 'Pi RPC stdout was not valid UTF-8', {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this.onRecord(JSON.parse(source));
|
||||
} catch (error) {
|
||||
if (error instanceof PiProcessError) throw error;
|
||||
throw new PiProcessError('PI_RPC_PROTOCOL_ERROR', 'Pi RPC stdout contained malformed JSON', {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user