Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions docs/js.md
Original file line number Diff line number Diff line change
Expand Up @@ -543,16 +543,17 @@ The flag only affects tracing in debug-enabled builds.

## Simple API

A preconfigured module instance exported as `simple`.
A preconfigured module instance exported as `simple({ maxSpans = 65536 } = {})`.

It is equivalent to calling `setup(...)` with all callbacks wired to collect parser spans automatically.
It is equivalent to calling `setup(...)` with all callbacks wired to collect parser spans automatically. `maxSpans` is a non-negative safe integer and limits retained spans per parser.

Differences from a plain `setup()` instance:

- `create()` enables all callbacks automatically (`CALLBACK_ACTIVE_ALL`).
- `destroy(parser)` also clears collected spans for that parser.
- `destroy(parser)` also clears collected spans and overflow state for that parser.
- A `spans` object is exposed on the module, keyed by parser id.
- Every callback appends `[callbackType, offset, length]` to `spans[parser]`.
- Callbacks append `[callbackType, offset, length]` until `maxSpans` is reached. Further spans are dropped and `spanOverflow[parser]` becomes `true`.
- `drain(parser)` atomically returns `{ spans, truncated }` and resets that parser's collection. Drain long-lived parsers regularly; `truncated` reports whether spans were dropped since the previous drain.

This API is useful when you want a minimal integration that records parser events without manually providing callback functions.

Expand All @@ -570,7 +571,10 @@ const buffer = Buffer.from(milo.memory.buffer, ptr, message.length)
buffer.set(message, 0)
milo.parse(parser, ptr, message.length)

for (const [type, at, len] of milo.spans[parser]) {
const batch = milo.drain(parser)
if (batch.truncated) throw new Error('span limit reached')

for (const [type, at, len] of batch.spans) {
console.log(`[${at.toString().padStart(3, ' ')}, ${len.toString().padStart(3, ' ')}] -> ${milo.Callbacks[type]}`)
}

Expand Down
51 changes: 43 additions & 8 deletions parser/wasm/src/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,42 @@ function hasDebug () {

/* REPLACE: setters */

function simpleCreate (spans, create) {
// The postbuild step injects generated callback references.
// eslint-disable-next-line no-unused-vars
function collectSpan (spans, spanOverflow, maxSpans, parser, span) {
if (spans[parser].length < maxSpans) {
spans[parser].push(span)
} else {
spanOverflow[parser] = true
}
}

function simpleCreate (spans, spanOverflow, create) {
const parser = create()
spans[parser] = []
spanOverflow[parser] = false
this.setActiveCallbacks(parser, this.CALLBACK_ACTIVE_ALL)
return parser
}

function simpleDestroy (spans, destroy, parser) {
spans[parser] = undefined
function simpleDestroy (spans, spanOverflow, destroy, parser) {
delete spans[parser]
delete spanOverflow[parser]
destroy(parser)
}

function simpleDrain (spans, spanOverflow, parser) {
const drained = spans[parser] ?? []
const truncated = spanOverflow[parser] ?? false

if (spans[parser] !== undefined) {
spans[parser] = []
spanOverflow[parser] = false
}

return { spans: drained, truncated }
}

export function noop () {}

export function setup (env = {}) {
Expand Down Expand Up @@ -116,14 +140,25 @@ export function setup (env = {}) {
return milo
}

export function simple () {
const spans = {}
export function simple ({ maxSpans = 65536 } = {}) {
if (!Number.isSafeInteger(maxSpans) || maxSpans < 0) {
throw new RangeError('maxSpans must be a non-negative safe integer')
}

const milo = setup({/* REPLACE: callbacks:simple */})
const spans = Object.create(null)
const spanOverflow = Object.create(null)

/* prettier-ignore */
const milo = setup({
/* REPLACE: callbacks:simple */
})

milo.spans = spans
milo.create = simpleCreate.bind(milo, spans, milo.create)
milo.destroy = simpleDestroy.bind(milo, spans, milo.destroy)
milo.spanOverflow = spanOverflow
milo.maxSpans = maxSpans
milo.create = simpleCreate.bind(milo, spans, spanOverflow, milo.create)
milo.destroy = simpleDestroy.bind(milo, spans, spanOverflow, milo.destroy)
milo.drain = simpleDrain.bind(undefined, spans, spanOverflow)

return milo
}
4 changes: 3 additions & 1 deletion parser/wasm/test/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Exercise the generated packages, including their JavaScript bindings.
process.env.MILO_VARIANT ??= 'simd'
export const { setup } = await import(`../../../dist/wasm/release/package/src/${process.env.MILO_VARIANT}/index.js`)
export const { setup, simple } = await import(
`../../../dist/wasm/release/package/src/${process.env.MILO_VARIANT}/index.js`
)

export function createParser (t, setup) {
const headers = []
Expand Down
41 changes: 41 additions & 0 deletions parser/wasm/test/simple.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import assert from 'node:assert/strict'
import { it } from 'node:test'
import { simple } from './helpers.js'

function parse (milo, parser, input) {
const bytes = Buffer.from(input)
const ptr = milo.alloc(bytes.length)
try {
new Uint8Array(milo.memory.buffer, ptr, bytes.length).set(bytes)
return milo.parse(parser, ptr, bytes.length)
} finally {
milo.dealloc(ptr, bytes.length)
}
}

it('simple_span_collection_is_bounded_and_drainable', () => {
assert.throws(() => simple({ maxSpans: -1 }), RangeError)
assert.throws(() => simple({ maxSpans: 1.5 }), RangeError)

const milo = simple({ maxSpans: 2 })
const parser = milo.create()

assert.equal(parse(milo, parser, 'GET / HTTP/1.1\r\n\r\n'), 18)
assert.equal(milo.spans[parser].length, 2)
assert.equal(milo.spanOverflow[parser], true)

const first = milo.drain(parser)
assert.equal(first.spans.length, 2)
assert.equal(first.truncated, true)
assert.deepEqual(milo.spans[parser], [])
assert.equal(milo.spanOverflow[parser], false)

assert.equal(parse(milo, parser, 'GET / HTTP/1.1\r\n\r\n'), 18)
const second = milo.drain(parser)
assert.equal(second.spans.length, 2)
assert.equal(second.truncated, true)

milo.destroy(parser)
assert.equal(parser in milo.spans, false)
assert.equal(parser in milo.spanOverflow, false)
})
4 changes: 3 additions & 1 deletion scripts/src/bin/postbuild-wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ fn generate_simple_callbacks(info: &BuildInfo) -> String {
.iter()
.map(|(key, value)| {
let name = key.replacen("CALLBACK_", "", 1).to_lowercase();
format!("{name} (parser, at, len) {{\n spans[parser].push([{value}, at, len])\n}}")
format!(
"{name} (parser, at, len) {{\n collectSpan(spans, spanOverflow, maxSpans, parser, [{value}, at, len])\n}}"
)
})
.collect::<Vec<_>>()
.join(",\n")
Expand Down
Loading