Skip to content

Per-match callbacks

The .on(path, callback) API fires your handler once per fully-formed match, with the parsed value as the first argument and the path stack as the second.

js
fetchStream("/api/users")
  .on("$.users.*", (user, path) => {
    // user: { id: 1, name: "Alex", ... }
    // path: ["users", 0]
  });

When does a callback fire?

The moment the parser sees the closing token of the matched value:

Match typeFires when
object} arrives
array] arrives
stringclosing " arrives
numberfirst non-numeric byte after the last digit
literallast byte of true / false / null

So if you subscribe to $.users.* against:

json
{ "users": [{ "id": 1 }, { "id": 2 }] }

...your callback fires twice — once at the } after id: 1, and again at the } after id: 2.

What you receive

The callback receives the fully materialized JS value — exactly what JSON.parse would have given you for that subtree:

js
fetchStream(url).on("$.users.*", (user) => {
  user.name;       // string
  user.tags;       // array
  user.address;    // object
  user.active;     // boolean
});

Strings, numbers, booleans, null, objects, and arrays are all hydrated normally. Nested matches inside a parent match still work too:

js
fetchStream(url)
  .on("$.users.*", (user) => { /* fires once per user */ })
  .on("$.users.*.email", (email) => { /* fires once per user, slightly earlier */ });

Multiple subscriptions, single parse

Register as many handlers as you want — they're all evaluated against a single byte-stream pass:

js
fetchStream(url)
  .on("$.meta", (meta) => setMeta(meta))         // 1×
  .on("$.totalCount", (n) => setCount(n))        // 1×
  .on("$.products.*", (p) => addProduct(p))      // N×
  .on("$.products.*.price", (p) => trackPrice(p)) // N× (nested under products.*)
  .on("$.errors.*", (e) => logError(e));         // 0..N×

The picker layer only allocates objects for paths that have at least one subscription. Sibling subtrees pass through the parser untouched.

Path argument

The second argument is an immutable snapshot of the path stack at the time of the match:

js
fetchStream(url).on("$.users.*", (user, path) => {
  // path is a frozen array, e.g. ["users", 7]
  console.log("Got user at index", path[1]);
});

Useful when you need to know which index/key matched a wildcard.

Async callbacks

Per-match callbacks are fire-and-forget — the parser does not await them:

js
fetchStream(url).on("$.users.*", async (user) => {
  await saveToDatabase(user); // ❌ parser keeps going regardless
});

If you need backpressure (parser pauses until your handler finishes), use .iterate() instead:

js
for await (const user of fetchStream(url).iterate("$.users.*")) {
  await saveToDatabase(user); // ✅ parser blocks until done
}

See Async iteration.

Errors in callbacks

Throwing from a callback rejects the handle:

js
try {
  await fetchStream(url).on("$.users.*", (user) => {
    if (!user.id) throw new Error("Bad user");
  });
} catch (err) {
  // err is the thrown error
}

The body reader is canceled when this happens — no more bytes are pulled.

Released under the MIT License.