mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
156 lines
4.9 KiB
TypeScript
156 lines
4.9 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { encodeNdjson, createNdjsonParser, NDJSON_MAX_LINE_BYTES } from './ndjson'
|
|
|
|
describe('encodeNdjson', () => {
|
|
it('encodes an object as a JSON line ending with newline', () => {
|
|
const result = encodeNdjson({ type: 'hello', version: 1 })
|
|
expect(result).toBe('{"type":"hello","version":1}\n')
|
|
})
|
|
|
|
it('encodes nested objects', () => {
|
|
const msg = { id: 'req-1', type: 'write', payload: { sessionId: 'abc', data: 'ls\n' } }
|
|
const result = encodeNdjson(msg)
|
|
expect(result.endsWith('\n')).toBe(true)
|
|
expect(JSON.parse(result.trim())).toEqual(msg)
|
|
})
|
|
})
|
|
|
|
describe('createNdjsonParser', () => {
|
|
it('exports a bounded default line size', () => {
|
|
expect(NDJSON_MAX_LINE_BYTES).toBe(16 * 1024 * 1024)
|
|
})
|
|
|
|
it('parses a single complete message', () => {
|
|
const onMessage = vi.fn()
|
|
const onError = vi.fn()
|
|
const parser = createNdjsonParser(onMessage, onError)
|
|
|
|
parser.feed('{"type":"hello"}\n')
|
|
|
|
expect(onMessage).toHaveBeenCalledOnce()
|
|
expect(onMessage).toHaveBeenCalledWith({ type: 'hello' })
|
|
expect(onError).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('parses multiple messages in a single chunk', () => {
|
|
const onMessage = vi.fn()
|
|
const parser = createNdjsonParser(onMessage)
|
|
|
|
parser.feed('{"a":1}\n{"b":2}\n{"c":3}\n')
|
|
|
|
expect(onMessage).toHaveBeenCalledTimes(3)
|
|
expect(onMessage).toHaveBeenNthCalledWith(1, { a: 1 })
|
|
expect(onMessage).toHaveBeenNthCalledWith(2, { b: 2 })
|
|
expect(onMessage).toHaveBeenNthCalledWith(3, { c: 3 })
|
|
})
|
|
|
|
it('handles messages split across multiple chunks', () => {
|
|
const onMessage = vi.fn()
|
|
const parser = createNdjsonParser(onMessage)
|
|
|
|
parser.feed('{"type":"hel')
|
|
expect(onMessage).not.toHaveBeenCalled()
|
|
|
|
parser.feed('lo","version":1}\n')
|
|
expect(onMessage).toHaveBeenCalledOnce()
|
|
expect(onMessage).toHaveBeenCalledWith({ type: 'hello', version: 1 })
|
|
})
|
|
|
|
it('handles a chunk that ends mid-line followed by more data', () => {
|
|
const onMessage = vi.fn()
|
|
const parser = createNdjsonParser(onMessage)
|
|
|
|
parser.feed('{"id":"1"}\n{"id":')
|
|
expect(onMessage).toHaveBeenCalledOnce()
|
|
|
|
parser.feed('"2"}\n')
|
|
expect(onMessage).toHaveBeenCalledTimes(2)
|
|
expect(onMessage).toHaveBeenNthCalledWith(2, { id: '2' })
|
|
})
|
|
|
|
it('ignores empty lines', () => {
|
|
const onMessage = vi.fn()
|
|
const parser = createNdjsonParser(onMessage)
|
|
|
|
parser.feed('\n\n{"ok":true}\n\n')
|
|
|
|
expect(onMessage).toHaveBeenCalledOnce()
|
|
expect(onMessage).toHaveBeenCalledWith({ ok: true })
|
|
})
|
|
|
|
it('calls onError for malformed JSON', () => {
|
|
const onMessage = vi.fn()
|
|
const onError = vi.fn()
|
|
const parser = createNdjsonParser(onMessage, onError)
|
|
|
|
parser.feed('not json\n')
|
|
|
|
expect(onMessage).not.toHaveBeenCalled()
|
|
expect(onError).toHaveBeenCalledOnce()
|
|
expect(onError.mock.calls[0][0]).toBeInstanceOf(Error)
|
|
})
|
|
|
|
it('recovers after malformed JSON and parses next line', () => {
|
|
const onMessage = vi.fn()
|
|
const onError = vi.fn()
|
|
const parser = createNdjsonParser(onMessage, onError)
|
|
|
|
parser.feed('bad\n{"good":true}\n')
|
|
|
|
expect(onError).toHaveBeenCalledOnce()
|
|
expect(onMessage).toHaveBeenCalledOnce()
|
|
expect(onMessage).toHaveBeenCalledWith({ good: true })
|
|
})
|
|
|
|
it('drops oversized complete lines and parses following messages', () => {
|
|
const onMessage = vi.fn()
|
|
const onError = vi.fn()
|
|
const parser = createNdjsonParser(onMessage, onError, { maxLineBytes: 24 })
|
|
|
|
parser.feed(`${'x'.repeat(25)}\n{"good":true}\n`)
|
|
|
|
expect(onError).toHaveBeenCalledOnce()
|
|
expect(onError.mock.calls[0][0].message).toContain('NDJSON line exceeds max 24 bytes')
|
|
expect(onMessage).toHaveBeenCalledOnce()
|
|
expect(onMessage).toHaveBeenCalledWith({ good: true })
|
|
})
|
|
|
|
it('discards oversized partial lines until the next delimiter', () => {
|
|
const onMessage = vi.fn()
|
|
const onError = vi.fn()
|
|
const parser = createNdjsonParser(onMessage, onError, { maxLineBytes: 24 })
|
|
|
|
parser.feed('{"too":')
|
|
parser.feed(`"${'x'.repeat(24)}"}`)
|
|
parser.feed('\n{"fresh":true}\n')
|
|
|
|
expect(onError).toHaveBeenCalledOnce()
|
|
expect(onMessage).toHaveBeenCalledOnce()
|
|
expect(onMessage).toHaveBeenCalledWith({ fresh: true })
|
|
})
|
|
|
|
it('handles messages with embedded newlines in strings', () => {
|
|
const onMessage = vi.fn()
|
|
const parser = createNdjsonParser(onMessage)
|
|
|
|
// JSON.stringify escapes newlines as \n (two chars), so the actual
|
|
// newline delimiter is still unambiguous.
|
|
const msg = { data: 'line1\nline2' }
|
|
parser.feed(`${JSON.stringify(msg)}\n`)
|
|
|
|
expect(onMessage).toHaveBeenCalledWith(msg)
|
|
})
|
|
|
|
it('resets buffer state on reset()', () => {
|
|
const onMessage = vi.fn()
|
|
const parser = createNdjsonParser(onMessage)
|
|
|
|
parser.feed('{"partial":')
|
|
parser.reset()
|
|
parser.feed('{"fresh":true}\n')
|
|
|
|
expect(onMessage).toHaveBeenCalledOnce()
|
|
expect(onMessage).toHaveBeenCalledWith({ fresh: true })
|
|
})
|
|
})
|