diff --git a/packages/core/src/types.test.ts b/packages/core/src/types.test.ts index d7a90bef..59d4ee10 100644 --- a/packages/core/src/types.test.ts +++ b/packages/core/src/types.test.ts @@ -562,6 +562,65 @@ describe("createSpecStreamCompiler", () => { const result = compiler.getResult(); expect(result).toEqual({ z: 1, w: 2 }); }); + + describe("onError callback", () => { + it("calls onError for malformed JSON", () => { + const errors: string[] = []; + const compiler = createSpecStreamCompiler({ + onError: (line) => errors.push(line), + }); + + compiler.push('{"op":"add"\n'); // Missing closing brace + compiler.push('not-json\n'); + compiler.push('{"invalid":}\n'); // Invalid JSON + + expect(errors).toHaveLength(2); + expect(errors[0]).toContain('{"op":"add"'); + expect(errors[1]).toContain('{"invalid":}'); + }); + + it("calls onError when patch application fails", () => { + const errors: Array<{ line: string; error?: unknown }> = []; + const compiler = createSpecStreamCompiler({ + onError: (line, error) => errors.push({ line, error }), + }); + + // Valid JSON but test operation fails + compiler.push('{"op":"test","path":"/x","value":"wrong"}\n'); + + expect(errors).toHaveLength(1); + expect(errors[0]?.error).toBeDefined(); + }); + + it("does not call onError for valid patches", () => { + const errors: string[] = []; + const compiler = createSpecStreamCompiler({ + onError: (line) => errors.push(line), + }); + + compiler.push('{"op":"add","path":"/x","value":1}\n'); + const result = compiler.getResult(); + + expect(errors).toHaveLength(0); + expect(result).toEqual({ x: 1 }); + }); + + it("continues processing after errors", () => { + const errors: string[] = []; + const compiler = createSpecStreamCompiler({ + onError: (line) => errors.push(line), + }); + + compiler.push('{"invalid"}\n'); + compiler.push('{"op":"add","path":"/x","value":1}\n'); + compiler.push('{broken\n'); + compiler.push('{"op":"add","path":"/y","value":2}\n'); + + expect(errors).toHaveLength(2); + const result = compiler.getResult(); + expect(result).toEqual({ x: 1, y: 2 }); + }); + }); }); // ============================================================================= diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index cbc28082..a3ba5239 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -791,6 +791,19 @@ export interface SpecStreamCompiler { reset(initial?: Partial): void; } +/** + * Options for createSpecStreamCompiler. + */ +export interface SpecStreamCompilerOptions { + /** Initial state */ + initial?: Partial; + /** + * Called when a line fails to parse as a valid patch. + * Useful for logging malformed JSON during debugging. + */ + onError?: (line: string, error?: unknown) => void; +} + /** * Create a streaming SpecStream compiler. * @@ -798,7 +811,9 @@ export interface SpecStreamCompiler { * line by line, and this compiler progressively builds the final spec. * * @example - * const compiler = createSpecStreamCompiler(); + * const compiler = createSpecStreamCompiler({ + * onError: (line) => console.warn('Failed to parse:', line), + * }); * * // Process streaming response * const reader = response.body.getReader(); @@ -813,8 +828,9 @@ export interface SpecStreamCompiler { * } */ export function createSpecStreamCompiler>( - initial: Partial = {}, + options: SpecStreamCompilerOptions = {}, ): SpecStreamCompiler { + const { initial = {}, onError } = options; let result = { ...initial } as T; let buffer = ""; const appliedPatches: SpecStreamLine[] = []; @@ -836,9 +852,19 @@ export function createSpecStreamCompiler>( const patch = parseSpecStreamLine(trimmed); if (patch) { - applySpecStreamPatch(result as Record, patch); - appliedPatches.push(patch); - newPatches.push(patch); + try { + applySpecStreamPatch(result as Record, patch); + appliedPatches.push(patch); + newPatches.push(patch); + } catch (error) { + // Patch failed to apply (e.g., test operation failed) + if (onError) { + onError(trimmed, error); + } + } + } else if (trimmed.startsWith("{") && onError) { + // Line looks like JSON but failed to parse + onError(trimmed); } } @@ -853,12 +879,21 @@ export function createSpecStreamCompiler>( getResult(): T { // Process any remaining buffer if (buffer.trim()) { - const patch = parseSpecStreamLine(buffer); - if (patch && !processedLines.has(buffer.trim())) { - processedLines.add(buffer.trim()); - applySpecStreamPatch(result as Record, patch); - appliedPatches.push(patch); - result = { ...result }; + const trimmed = buffer.trim(); + const patch = parseSpecStreamLine(trimmed); + if (patch && !processedLines.has(trimmed)) { + processedLines.add(trimmed); + try { + applySpecStreamPatch(result as Record, patch); + appliedPatches.push(patch); + result = { ...result }; + } catch (error) { + if (onError) { + onError(trimmed, error); + } + } + } else if (trimmed.startsWith("{") && !patch && onError) { + onError(trimmed); } buffer = ""; }